RULE 03 Mass Privileged Role Assignments
Query
// Rule : Azure - Mass Privileged Role Assignments by Single Identity
// Severity: High
// Tactics : PrivilegeEscalation, Persistence
// MITRE : T1098
// Freq : PT30M Period: PT1H
//==========================================================================================
let ExcludedCallerPatterns = dynamic(["ms-pim", "privilegedidentity", "identitygovernance", "aadidentitygovernance", "azure ad sync", "aad connect"]);
let ReadOnlyRoleIds = dynamic(["acdd72a7-3385-48ef-bd42-f606fba81ae7", "fa23ad8b-c56e-40d8-ac0c-ce449e1d2c64", "72fafb9e-0641-4937-9268-a91bfd8191a3", "73c42c96-874c-492b-b04d-ab87d138a893", "21090545-7ca7-4776-b22c-e363652d74d2"]);
AzureActivity
| where TimeGenerated > ago(1h)
| where OperationNameValue =~ "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
| where ActivityStatusValue =~ "Success"
| where not(tolower(Caller) has_any (ExcludedCallerPatterns))
| where isnotempty(CallerIpAddress)
| where CallerIpAddress !startswith "168.63."
| extend RoleDefId = tolower(tostring(parse_json(Properties).requestbody.properties.roleDefinitionId))
| extend PrincipalId = tolower(tostring(parse_json(Properties).requestbody.properties.principalId))
| where not(RoleDefId has_any (ReadOnlyRoleIds))
| summarize
AssignmentCount = count(),
DistinctRoles = dcount(RoleDefId),
DistinctPrincipals = dcount(PrincipalId),
AssignedRoles = make_set(RoleDefId, 10),
AffectedScopes = make_set(ResourceGroup, 10),
SourceIPs = make_set(CallerIpAddress, 5),
CallerIP = any(CallerIpAddress),
FirstAssignment = min(TimeGenerated),
LastAssignment = max(TimeGenerated)
by Caller, SubscriptionId, bin(TimeGenerated, 30m)
// Tuned: raised burst threshold 5 -> 8 and require role diversity (>=2) or many principals (>=3)
// to filter benign single-role loops (e.g., scripted contributor grants to a batch of SPs).
| where AssignmentCount >= 8 and (DistinctRoles >= 2 or DistinctPrincipals >= 3)
| extend
AccountName = tostring(split(Caller, "@")[0]),
AccountUPNSuffix = tostring(split(Caller, "@")[1])Explanation
This query is designed to detect suspicious activity in Azure where a single user is making a large number of privileged role assignments within a short period. Here's a simplified breakdown of what the query does:
-
Excluded Callers: It defines a list of patterns for caller identities that should be ignored, such as system accounts related to Azure's identity management services.
-
Read-Only Roles: It specifies a list of role IDs that are considered read-only and should not trigger alerts.
-
Data Source: The query analyzes the
AzureActivitylog to find role assignment activities. -
Time Frame: It looks at activities that occurred in the last hour.
-
Operation and Status: It filters for successful role assignment operations.
-
Caller Filtering: It excludes activities from known system accounts and internal Azure IP addresses.
-
Role and Principal Filtering: It excludes assignments to read-only roles and processes the role and principal IDs for further analysis.
-
Summarization: It counts the number of assignments, distinct roles, and distinct principals involved. It also collects sets of role IDs, affected resource groups, and source IP addresses.
-
Thresholds: It flags cases where there are at least 8 assignments and either multiple distinct roles (at least 2) or multiple principals (at least 3) involved, to avoid false positives from benign activities.
-
Output: It extends the results with additional information about the caller's account name and domain.
In essence, this query is looking for potential privilege escalation or persistence tactics by identifying unusual patterns of role assignments that could indicate a security threat.
Details

David Alonso
Released: July 27, 2026
Tables
Keywords
Operators
MITRE Techniques