Detection rules › Kusto

Azure Diagnostic settings removed from a resource

Severity
medium
Time window
2h
Group by
CorrelationId, MonitoredResourcePath, OperationNameValue, _ResourceId
Author
KennethMLdk
Source
github.com/Azure/Azure-Sentinel

This query looks for diagnostic settings that are removed from a resource. This could indicate an attacker or malicious internal trying to evade detection before malicious act is performed. If the diagnostic settings are being deleted as part of a parent resource deletion, the event is ignores.

MITRE ATT&CK coverage

Event coverage

Rule body kusto

id: 6e95aef3-a1e0-4063-8e74-cd59aa59f245
name: Azure Diagnostic settings removed from a resource
description: |
  'This query looks for diagnostic settings that are removed from a resource.
  This could indicate an attacker or malicious internal trying to evade detection before malicious act is performed.
  If the diagnostic settings are being deleted as part of a parent resource deletion, the event is ignores.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryFrequency: 1h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.008
query: |
  AzureActivity
  | where OperationNameValue =~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"
  | summarize
      TimeGenerated = arg_max(TimeGenerated, Properties),
      ActivityStatusValue = make_set(ActivityStatusValue, 5),
      take_any(Caller, CallerIpAddress, OperationName, ResourceGroup, Resource)
      by CorrelationId, _ResourceId, OperationNameValue
  | extend ResourceHierarchy = split(_ResourceId, "/")
  | extend MonitoredResourcePath = strcat_array(array_slice(ResourceHierarchy, 0, array_length(ResourceHierarchy)-5), "/")
  | join kind=leftanti (
      AzureActivity
      | where OperationNameValue !~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" and OperationNameValue endswith "/DELETE" and ActivityStatusValue has_any ("Success", "Succeeded")
      | project _ResourceId
  ) on $left.MonitoredResourcePath == $right._ResourceId
  | extend
      Name = iif(Caller has "@", tostring(split(Caller, "@")[0]), ""),
      UPNSuffix = iif(Caller has "@", tostring(split(Caller, "@")[1]), ""),
      AadUserId = iif(Caller has "@", "", Caller)
  | project TimeGenerated, Caller, CallerIpAddress, OperationNameValue, OperationName, ActivityStatusValue, ResourceGroup, MonitoredResourcePath, Resource, Properties, Name, UPNSuffix, AadUserId, _ResourceId, CorrelationId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: AadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
version: 1.0.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: KennethMLdk
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Platform" ]

Stages and Predicates

Stage 1: source

AzureActivity

Stage 2: where

| where OperationNameValue =~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE"

Stage 3: summarize

| summarize
    TimeGenerated = arg_max(TimeGenerated, Properties),
    ActivityStatusValue = make_set(ActivityStatusValue, 5),
    take_any(Caller, CallerIpAddress, OperationName, ResourceGroup, Resource)
    by CorrelationId, _ResourceId, OperationNameValue

Stage 4: extend

| extend ResourceHierarchy = split(_ResourceId, "/")

Stage 5: extend

| extend MonitoredResourcePath = strcat_array(array_slice(ResourceHierarchy, 0, array_length(ResourceHierarchy)-5), "/")

Stage 6: join (negated)

| join kind=leftanti (
    AzureActivity
    | where OperationNameValue !~ "MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" and OperationNameValue endswith "/DELETE" and ActivityStatusValue has_any ("Success", "Succeeded")
    | project _ResourceId
) on $left.MonitoredResourcePath == $right._ResourceId

Stage 7: extend

| extend
    Name = iif(Caller has "@", tostring(split(Caller, "@")[0]), ""),
    UPNSuffix = iif(Caller has "@", tostring(split(Caller, "@")[1]), ""),
    AadUserId = iif(Caller has "@", "", Caller)

Stage 8: project

| project TimeGenerated, Caller, CallerIpAddress, OperationNameValue, OperationName, ActivityStatusValue, ResourceGroup, MonitoredResourcePath, Resource, Properties, Name, UPNSuffix, AadUserId, _ResourceId, CorrelationId

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
ActivityStatusValuematchSuccess, Succeeded
OperationNameValueends_with/DELETE
OperationNameValueneMICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
OperationNameValueeq
  • MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AadUserIdproject
ActivityStatusValueproject
Callerproject
CallerIpAddressproject
CorrelationIdproject
MonitoredResourcePathproject
Nameproject
OperationNameproject
OperationNameValueproject
Propertiesproject
Resourceproject
ResourceGroupproject
TimeGeneratedproject
UPNSuffixproject
_ResourceIdproject