Query Details

RULE 10 SP Mass Privilege Escalation

Query

// Rule    : Workload Identity - SP Mass Privilege Escalation (Multiple High-Priv Role Assignments)
// Severity: High
// Tactics : PrivilegeEscalation, Persistence
// MITRE   : T1098.003 (Additional Cloud Roles), T1078.004
// Freq    : PT1H   Period: PT2H
//==========================================================================================

// ---- Network Allowlist (exclude trusted IPs / CIDR / ranges) --------------
let _allow = materialize(union isfuzzy=true (print R="" | take 0), (_GetWatchlist('NetworkAllowlist') | project R = tostring(IPOrRange)) | where isnotempty(R));
let _allowCIDR  = toscalar(_allow | where not(R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$') | extend R = iff(R has '/', R, strcat(R, '/32')) | summarize make_list(R));
let _allowRange = toscalar(_allow | where R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$' | summarize make_list(R));
let _ExcludeAllowlistedIPs = (T:(IPAddress:string)) {
    T
    | extend IPAddress = tostring(IPAddress)
    | where array_length(_allowCIDR) == 0 or isnull(ipv4_is_in_any_range(IPAddress, _allowCIDR)) or not(ipv4_is_in_any_range(IPAddress, _allowCIDR))
    | mv-apply _r = _allowRange to typeof(string) on (
        extend _lo = tostring(split(_r,'-')[0]), _hi = tostring(split(_r,'-')[1])
        | extend _inRange = ipv4_compare(IPAddress, _lo) >= 0 and ipv4_compare(IPAddress, _hi) <= 0
        | summarize _anyInRange = max(toint(_inRange)))
    | where isnull(_anyInRange) or _anyInRange == 0
    | project-away _anyInRange
};
// ---------------------------------------------------------------------------
let HighPrivRoles = dynamic([
    "Owner", "Contributor", "User Access Administrator",
    "Global Administrator", "Privileged Role Administrator",
    "Security Administrator", "Application Administrator",
    "Cloud Application Administrator", "Hybrid Identity Administrator",
    "Authentication Administrator"
]);

// --- SP-initiated role assignments (SP as the actor, not user) ---
let SPRoleAssignments = AuditLogs
    | where TimeGenerated > ago(2h)
    | where OperationName has_any (
        "Add member to role", "Add eligible member to role",
        "Add app role assignment to service principal",
        "Add app role assignment grant")
    | where Result =~ "success"
    | extend RoleName    = tostring(TargetResources[0].displayName)
    | extend TargetId    = tostring(TargetResources[0].id)
    | extend TargetType  = tostring(TargetResources[0].type)
    | extend Initiator   = coalesce(
        tostring(InitiatedBy.app.displayName),
        tostring(InitiatedBy.user.userPrincipalName))
    | extend InitiatorId = coalesce(
        tostring(InitiatedBy.app.servicePrincipalId),
        tostring(InitiatedBy.user.id))
    | where isnotempty(Initiator)
    | where RoleName in (HighPrivRoles)
    | project AssignTime = TimeGenerated, Initiator, InitiatorId,
        RoleName, TargetId, TargetType, CorrelationId;

SPRoleAssignments
| summarize
    AssignmentCount  = count(),
    AssignedRoles    = make_set(RoleName, 10),
    TargetIds        = make_set(TargetId, 10),
    CorrelationIds   = make_set(CorrelationId, 5),
    FirstAssign      = min(AssignTime),
    LastAssign       = max(AssignTime)
    by Initiator, InitiatorId
| where AssignmentCount >= 3
// High if 5+ assignments, Critical if includes Global Admin or Owner
| extend SeverityLevel = case(
    AssignedRoles has_any ("Global Administrator", "Owner") and AssignmentCount >= 3, "Critical",
    AssignmentCount >= 5,                                                              "High",
    "Medium")

Explanation

This query is designed to detect suspicious activity related to privilege escalation in a cloud environment, specifically focusing on service principals (SPs) that are making multiple high-privilege role assignments within a short time frame. Here's a simplified breakdown of what the query does:

  1. Network Allowlist: The query starts by defining a list of trusted IP addresses or ranges. Any IPs in this list will be excluded from further analysis to reduce false positives.

  2. High-Privilege Roles: A list of roles considered high-privilege is defined. These roles include "Owner", "Global Administrator", "Security Administrator", and others that have significant control over the environment.

  3. Role Assignments by Service Principals: The query looks at audit logs from the past two hours to find instances where service principals (not users) have successfully assigned these high-privilege roles. It captures details like the role name, the target of the assignment, and the initiator of the action.

  4. Summarization: The query groups the data by the initiator (the service principal making the assignments) and counts how many assignments were made. It also collects the roles assigned, the targets of these assignments, and the correlation IDs for tracking.

  5. Severity Assessment: The query assesses the severity of the detected activity:

    • If a service principal makes 3 or more assignments and includes roles like "Global Administrator" or "Owner", the activity is marked as "Critical".
    • If there are 5 or more assignments, the activity is marked as "High".
    • Otherwise, it is marked as "Medium".

In summary, this query is a security rule that identifies potentially malicious behavior where a service principal is rapidly assigning high-privilege roles, which could indicate an attempt to escalate privileges or persist access within the cloud environment.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AuditLogs

Keywords

WorkloadIdentityPrivilegeEscalationPersistenceCloudRolesNetworkIPsCIDRRangesIPAddressRoleAssignmentsAuditLogsOperationNameResultRoleNameTargetResourcesInitiatorInitiatedByCorrelationIdSeverityLevel

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptymatchesregexextendiffhasstrcatsummarizemake_listtoscalarnotipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awaydynamicinagohas_any=~coalesceinprojectsummarizecountmake_setminmaxby>=has_anycase

Actions