Detection rules › Kusto

Conditional Access - Dynamic Group Exclusion Changes

Severity
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

// Detects changes to Dynamic Membership Rules for specified groups (often used in CA exclusions)

MITRE ATT&CK coverage

TacticTechniques
Privilege Escalation

Telemetry coverage

PlatformRecord / event type
AzureUpdate group

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: c385944b-17b9-4b2b-921e-0e8d0341a675
name: Conditional Access - Dynamic Group Exclusion Changes
version: 1.0.1
kind: Scheduled
description: // Detects changes to Dynamic Membership Rules for specified groups (often used in CA exclusions)
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1484
query: |-
  // Detects changes to Dynamic Membership Rules for specified groups (often used in CA exclusions)
  let monitoredGroups = dynamic(["Group1", "Group2"]);  // <-- Customize this list
  AuditLogs
  | where OperationName == "Update group"
  | where AdditionalDetails[0].value == "DynamicMembership"
  | extend DynamicGroupName = tostring(TargetResources[0].displayName)
  | where DynamicGroupName in (monitoredGroups)
  | extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
  | extend accountName = tostring(split(modifiedBy, "@")[0])
  | extend upnSuffix = tostring(split(modifiedBy, "@")[1])
  | extend oldRule = tostring(TargetResources[0].modifiedProperties[0].oldValue)
  | extend newRule = tostring(TargetResources[0].modifiedProperties[0].newValue)
  | where oldRule != newRule
  | project
      TimeGenerated,
      OperationName,
      DynamicGroupName,
      modifiedBy,
      accountName,
      upnSuffix,
      result = Result,
      oldRule,
      newRule
  | order by TimeGenerated desc
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: accountName
  - identifier: UPNSuffix
    columnName: upnSuffix
suppressionEnabled: false
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []

Stages and Predicates

Parameters

let monitoredGroups = dynamic(["Group1", "Group2"]);

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName == "Update group"

Stage 3: where

| where AdditionalDetails[0].value == "DynamicMembership"

Stage 4: extend

| extend DynamicGroupName = tostring(TargetResources[0].displayName)

Stage 5: where

| where DynamicGroupName in (monitoredGroups)

Stage 6: extend (5 consecutive steps)

| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])
| extend oldRule = tostring(TargetResources[0].modifiedProperties[0].oldValue)
| extend newRule = tostring(TargetResources[0].modifiedProperties[0].newValue)

Stage 7: where

| where oldRule != newRule

Stage 8: project

| project
    TimeGenerated,
    OperationName,
    DynamicGroupName,
    modifiedBy,
    accountName,
    upnSuffix,
    result = Result,
    oldRule,
    newRule

Stage 9: sort

| order by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
DynamicGroupNameproject
OperationNameproject
TimeGeneratedproject
accountNameproject
modifiedByproject
newRuleproject
oldRuleproject
resultproject
upnSuffixproject