Query Details

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:

  1. 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.

  2. Read-Only Roles: It specifies a list of role IDs that are considered read-only and should not trigger alerts.

  3. Data Source: The query analyzes the AzureActivity log to find role assignment activities.

  4. Time Frame: It looks at activities that occurred in the last hour.

  5. Operation and Status: It filters for successful role assignment operations.

  6. Caller Filtering: It excludes activities from known system accounts and internal Azure IP addresses.

  7. Role and Principal Filtering: It excludes assignments to read-only roles and processes the role and principal IDs for further analysis.

  8. 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.

  9. 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.

  10. 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 profile picture

David Alonso

Released: July 27, 2026

Tables

AzureActivity

Keywords

AzureActivityCallerRoleAssignmentPrincipalResourceGroupSubscriptionAccount

Operators

letdynamicAzureActivitywhereago=~nottolowerhas_anyisnotempty!startswithextendtostringparse_jsonsummarizecountdcountmake_setanyminmaxbybinsplit

MITRE Techniques

Actions

GitHub