Detection rules › Kusto

GCP Audit Logs - DNSSEC Disabled on Managed DNS Zone

Status
available
Severity
high
Time window
1h
Source
github.com/Azure/Azure-Sentinel

Detects when DNSSEC (DNS Security Extensions) is disabled on a Google Cloud DNS managed zone. DNSSEC provides cryptographic authentication of DNS data, preventing DNS spoofing and cache poisoning attacks. Adversaries may disable DNSSEC to enable DNS-based command and control, phishing campaigns, or to redirect traffic to malicious infrastructure without cryptographic validation. This rule monitors DNS zone patch operations where DNSSEC state changes from ON to OFF.

MITRE ATT&CK coverage

Telemetry coverage

Rule body

id: 9129a43e-e204-4a9a-969e-d8861ce3437c
name: GCP Audit Logs - DNSSEC Disabled on Managed DNS Zone
description: |
  'Detects when DNSSEC (DNS Security Extensions) is disabled on a Google Cloud DNS managed zone.
  DNSSEC provides cryptographic authentication of DNS data, preventing DNS spoofing and cache poisoning attacks.
  Adversaries may disable DNSSEC to enable DNS-based command and control, phishing campaigns, or
  to redirect traffic to malicious infrastructure without cryptographic validation.
  This rule monitors DNS zone patch operations where DNSSEC state changes from ON to OFF.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - CommandAndControl
  - ResourceDevelopment
relevantTechniques:
  - T1562.001
  - T1071.004
  - T1584.002
tags:
  - GCP
  - DNS
  - DNSSEC
  - Cloud Security
query: |
  GCPAuditLogs
  | where ServiceName == "dns.googleapis.com"
  | where MethodName in ("dns.managedZones.update", "dns.managedZones.patch")
  | where GCPResourceType == "dns_managed_zone" and Severity == "NOTICE"
  | extend 
      ResponseJson = parse_json(Response),
      RequestMetadataJson = parse_json(RequestMetadata),
      AuthInfoJson = parse_json(AuthenticationInfo)
  | extend ZoneContext = ResponseJson.operation.zoneContext
  | where isnotempty(ZoneContext)
  | extend 
      OldDnsSecState = tostring(ZoneContext.oldValue.dnssecConfig.state),
      NewDnsSecState = tostring(ZoneContext.newValue.dnssecConfig.state)
  | where OldDnsSecState == "ON" and NewDnsSecState == "OFF"
  | extend 
      ManagedZoneName = extract(@"managedZones/([^/]+)", 1, GCPResourceName),
      DnsName = tostring(ResponseJson.managedZone.dnsName),
      ZoneId = tostring(ResponseJson.managedZone.id),
      ZoneDescription = tostring(ResponseJson.managedZone.description),
      Visibility = tostring(ResponseJson.managedZone.visibility),
      OperationId = tostring(ResponseJson.operation.id),
      CallerIpAddress = tostring(RequestMetadataJson.callerIp),
      AuthEmail = tostring(AuthInfoJson.principalEmail)
  | extend 
      AccountName = tostring(split(PrincipalEmail, "@")[0]), 
      AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
  | project TimeGenerated,
            PrincipalEmail,
            AuthEmail,
            ProjectId,
            ManagedZoneName,
            DnsName,
            ResourceName = GCPResourceName,
            Visibility,
            ZoneId,
            ZoneDescription,
            OperationId,
            CallerIpAddress,
            MethodName,
            ServiceName,
            Severity,
            LogName,
            InsertId,
            AccountName,
            AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PrincipalEmail
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectId
      - identifier: InstanceName
        columnName: ResourceName
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DnsName
customDetails:
  ProjectId: ProjectId
  ManagedZoneName: ManagedZoneName
  DnsName: DnsName
  ResourceName: ResourceName
  Visibility: Visibility
  ZoneId: ZoneId
alertDetailsOverride:
  alertDisplayNameFormat: "DNSSEC Disabled on DNS Zone {{ManagedZoneName}} ({{DnsName}}) by {{PrincipalEmail}}"
  alertDescriptionFormat: |-
    User {{PrincipalEmail}} disabled DNSSEC on DNS managed zone {{ManagedZoneName}} ({{DnsName}}).    
    This action removes cryptographic validation of DNS responses and may indicate an attempt to facilitate DNS-based attacks.
    Investigate immediately to determine if this change was authorized and assess potential security impact.
    Review DNS query logs for suspicious activity and consider re-enabling DNSSEC if unauthorized.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GCPAuditLogs

Stage 2: where

| where ServiceName == "dns.googleapis.com"

Stage 3: where

| where MethodName in ("dns.managedZones.update", "dns.managedZones.patch")

Stage 4: where

| where GCPResourceType == "dns_managed_zone" and Severity == "NOTICE"

Stage 5: extend

| extend 
    ResponseJson = parse_json(Response),
    RequestMetadataJson = parse_json(RequestMetadata),
    AuthInfoJson = parse_json(AuthenticationInfo)

Stage 6: extend

| extend ZoneContext = ResponseJson.operation.zoneContext

Stage 7: where

| where isnotempty(ZoneContext)

Stage 8: extend

| extend 
    OldDnsSecState = tostring(ZoneContext.oldValue.dnssecConfig.state),
    NewDnsSecState = tostring(ZoneContext.newValue.dnssecConfig.state)

Stage 9: where

| where OldDnsSecState == "ON" and NewDnsSecState == "OFF"

Stage 10: extend

| extend 
    ManagedZoneName = extract(@"managedZones/([^/]+)", 1, GCPResourceName),
    DnsName = tostring(ResponseJson.managedZone.dnsName),
    ZoneId = tostring(ResponseJson.managedZone.id),
    ZoneDescription = tostring(ResponseJson.managedZone.description),
    Visibility = tostring(ResponseJson.managedZone.visibility),
    OperationId = tostring(ResponseJson.operation.id),
    CallerIpAddress = tostring(RequestMetadataJson.callerIp),
    AuthEmail = tostring(AuthInfoJson.principalEmail)

Stage 11: extend

| extend 
    AccountName = tostring(split(PrincipalEmail, "@")[0]), 
    AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])

Stage 12: project

| project TimeGenerated,
          PrincipalEmail,
          AuthEmail,
          ProjectId,
          ManagedZoneName,
          DnsName,
          ResourceName = GCPResourceName,
          Visibility,
          ZoneId,
          ZoneDescription,
          OperationId,
          CallerIpAddress,
          MethodName,
          ServiceName,
          Severity,
          LogName,
          InsertId,
          AccountName,
          AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
AuthEmailproject
CallerIpAddressproject
DnsNameproject
InsertIdproject
LogNameproject
ManagedZoneNameproject
MethodNameproject
OperationIdproject
PrincipalEmailproject
ProjectIdproject
ResourceNameproject
ServiceNameproject
Severityproject
TimeGeneratedproject
Visibilityproject
ZoneDescriptionproject
ZoneIdproject