Detection rules › Kusto
Bulk Changes to Privileged Account Permissions
Identifies when changes to multiple users permissions are changed at once. Investigate immediately if not a planned change. This setting could enable an attacker access to Azure subscriptions in your environment. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-identity-management
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
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: 218f60de-c269-457a-b882-9966632b9dc6
name: Bulk Changes to Privileged Account Permissions
description: |
'Identifies when changes to multiple users permissions are changed at once. Investigate immediately if not a planned change. This setting could enable an attacker access to Azure subscriptions in your environment.
Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-identity-management'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let AdminRecords = AuditLogs
| where Category =~ "RoleManagement"
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName),
props = TargetResource.modifiedProperties
)
| mv-apply Property = props on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"',tostring(Property.newValue))
)
| where RoleName contains "Admin";
AdminRecords
| summarize dcount(TargetUserPrincipalName) by bin(TimeGenerated, 1h)
| where dcount_TargetUserPrincipalName > 9
| join kind=rightsemi (
AdminRecords
| extend TimeWindow = bin(TimeGenerated, 1h)
) on $left.TimeGenerated == $right.TimeWindow
| extend InitiatedByUser = iff(isnotempty(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.user.userPrincipalName), "")
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatingAppName
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetUserPrincipalName
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
customDetails:
InitiatedByUser: InitiatedByUser
TargetUser: TargetUserPrincipalName
version: 1.0.7
kind: Scheduled
Stages and Predicates
let AdminRecords is inlined into the numbered stages below.
Stage 0: let
let AdminRecords = AuditLogs <inlined as stages below>;
Stages 1 to 6 define let AdminRecords (the rule's main pipeline source); stages 7 to 10 run on it.
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "RoleManagement"
Stage 3: where
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
Stage 4: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "User"
| extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName),
props = TargetResource.modifiedProperties
)
Stage 5: kusto:mv-apply
| mv-apply Property = props on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"',tostring(Property.newValue))
)
Stage 6: where
| where RoleName contains "Admin"
Stage 7: summarize
AdminRecords
| summarize dcount(TargetUserPrincipalName) by bin(TimeGenerated, 1h)
Stage 8: where
| where dcount_TargetUserPrincipalName > 9
Stage 9: join
| join kind=rightsemi (
AdminRecords
| extend TimeWindow = bin(TimeGenerated, 1h)
) on $left.TimeGenerated == $right.TimeWindow
Stage 10: extend (8 consecutive steps)
| extend InitiatedByUser = iff(isnotempty(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.user.userPrincipalName), "")
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
InitiatedByUser =if
isnotempty(userPrincipalName)tostring(InitiatedBy.user.userPrincipalName)else
""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
RoleName | contains |
| field:"RoleName" kind:contains value:"Admin" |
dcount_TargetUserPrincipalName | gt |
| field:"dcount_TargetUserPrincipalName" kind:gt value:"9" |
displayName | eq |
| field:"displayName" kind:eq value:"Role.DisplayName" |
type | eq |
| field:"type" kind:eq value:"User" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatedByUser | extend |
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
TargetName | extend |
TargetUPNSuffix | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |