Detection rules › Kusto
Threat Essentials - Multiple admin membership removals from newly created admin.
This query detects when newly created Global admin removes multiple existing global admins which can be an attempt by adversaries to lock down organization and retain sole access. Investigate reasoning and intention of multiple membership removal by new Global admins and take necessary actions accordingly.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
Rule body
id: 199978c5-cd6d-4194-b505-8ef5800739df
name: Threat Essentials - Multiple admin membership removals from newly created admin.
description: |
'This query detects when newly created Global admin removes multiple existing global admins which can be an attempt by adversaries to lock down organization and retain sole access.
Investigate reasoning and intention of multiple membership removal by new Global admins and take necessary actions accordingly.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1531
tags:
- DEV-0537
query: |
let lookback = 7d;
let timeframe = 1h;
let GlobalAdminsRemoved = AuditLogs
| where TimeGenerated > ago(timeframe)
| where Category =~ "RoleManagement"
| where AADOperationType in~ ("Unassign", "RemoveEligibleRole")
| where ActivityDisplayName has_any ("Remove member from role", "Remove eligible member from role")
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| where displayName_ =~ "Role.DisplayName"
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue)))
| where RoleName =~ "Global Administrator" // Add other Privileged role if applicable
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax" // Filtering PIM events
| extend Target = tostring(TargetResources.userPrincipalName)
| summarize RemovedGlobalAdminTime = max(TimeGenerated), TargetAdmins = make_set(Target) by OperationName, RoleName, Initiator, Result;
let GlobalAdminsAdded = AuditLogs
| where TimeGenerated > ago(lookback)
| where Category =~ "RoleManagement"
| where AADOperationType in~ ("Assign", "AssignEligibleRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role") and Result =~ "success"
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| where displayName_ =~ "Role.DisplayName"
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
| where RoleName =~ "Global Administrator" // Add other Privileged role if applicable
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax" // Filtering PIM events
| extend Target = tostring(TargetResources.userPrincipalName)
| summarize AddedGlobalAdminTime = max(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result;
GlobalAdminsAdded
| join kind= inner GlobalAdminsRemoved on $left.Target == $right.Initiator
| where AddedGlobalAdminTime < RemovedGlobalAdminTime
| extend NoofAdminsRemoved = array_length(TargetAdmins)
| where NoofAdminsRemoved > 1
| project AddedGlobalAdminTime, Initiator, Target, RemovedGlobalAdminTime, TargetAdmins, NoofAdminsRemoved
| extend Name=split(Target, "@")[0], UPNSuffix=split(Target, "@")[1]
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 7d;
let timeframe = 1h;
let GlobalAdminsAdded is inlined into the numbered stages below.
Let binding: GlobalAdminsRemoved
let GlobalAdminsRemoved = AuditLogs
| where TimeGenerated > ago(timeframe)
| where Category =~ "RoleManagement"
| where AADOperationType in~ ("Unassign", "RemoveEligibleRole")
| where ActivityDisplayName has_any ("Remove member from role", "Remove eligible member from role")
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
| where displayName_ =~ "Role.DisplayName"
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.oldValue)))
| where RoleName =~ "Global Administrator"
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
| extend Target = tostring(TargetResources.userPrincipalName)
| summarize RemovedGlobalAdminTime = max(TimeGenerated), TargetAdmins = make_set(Target) by OperationName, RoleName, Initiator, Result;
Stages 1 to 16 define let GlobalAdminsAdded (the rule's main pipeline source); stages 17 to 22 run on it.
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(lookback)
Stage 3: where
| where Category =~ "RoleManagement"
Stage 4: where
| where AADOperationType in~ ("Assign", "AssignEligibleRole")
Stage 5: where
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role") and Result =~ "success"
Stage 6: mv-expand
| mv-expand TargetResources
Stage 7: mv-expand
| mv-expand TargetResources.modifiedProperties
Stage 8: extend
| extend displayName_ = tostring(TargetResources_modifiedProperties.displayName)
Stage 9: where
| where displayName_ =~ "Role.DisplayName"
Stage 10: extend
| extend RoleName = tostring(parse_json(tostring(TargetResources_modifiedProperties.newValue)))
Stage 11: where
| where RoleName =~ "Global Administrator"
Stage 12: extend
| extend InitiatingApp = tostring(parse_json(tostring(InitiatedBy.app)).displayName)
Stage 13: extend
| extend Initiator = iif(isnotempty(InitiatingApp), InitiatingApp, tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName))
Stage 14: where
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
Stage 15: extend
| extend Target = tostring(TargetResources.userPrincipalName)
Stage 16: summarize
| summarize AddedGlobalAdminTime = max(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result
Stage 17: join
GlobalAdminsAdded
| join kind= inner GlobalAdminsRemoved on $left.Target == $right.Initiator
Stage 18: where
| where AddedGlobalAdminTime < RemovedGlobalAdminTime
Stage 19: extend
| extend NoofAdminsRemoved = array_length(TargetAdmins)
Stage 20: where
| where NoofAdminsRemoved > 1
Stage 21: project
| project AddedGlobalAdminTime, Initiator, Target, RemovedGlobalAdminTime, TargetAdmins, NoofAdminsRemoved
Stage 22: extend
| extend Name=split(Target, "@")[0], UPNSuffix=split(Target, "@")[1]
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | in |
| field:"AADOperationType" kind:in |
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match |
AddedGlobalAdminTime | cross_field_compare |
| field:"AddedGlobalAdminTime" kind:cross_field_compare value:"RemovedGlobalAdminTime" |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
Initiator | ne |
| field:"Initiator" kind:ne |
NoofAdminsRemoved | gt |
| field:"NoofAdminsRemoved" kind:gt value:"1" |
Result | eq |
| field:"Result" kind:eq value:"success" |
RoleName | eq |
| field:"RoleName" kind:eq value:"Global Administrator" |
displayName_ | eq |
| field:"displayName_" kind:eq value:"Role.DisplayName" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AddedGlobalAdminTime | project |
Initiator | project |
NoofAdminsRemoved | project |
RemovedGlobalAdminTime | project |
Target | project |
TargetAdmins | project |
Name | extend |
UPNSuffix | extend |