Query Details

RULE 08 SP Credential Added Privileged Role

Query

// Rule    : Azure - Service Principal Credential Added Then Privileged Role Assigned (Same Initiator, 60 min)
// Severity: High
// Tactics : Persistence, PrivilegeEscalation
// MITRE   : T1098
// Freq    : PT1H   Period: PT2H
//==========================================================================================

let CredentialAdditions = AuditLogs
    | where TimeGenerated > ago(2h)
    | where OperationName has_any ("Add service principal credentials", "Update application – Certificates and secrets management")
    | where Result =~ "success"
    | extend SPName = tostring(TargetResources[0].displayName)
    | extend Initiator = coalesce(tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
    | extend CredInitiatorIP = tostring(InitiatedBy.user.ipAddress)
    | where isnotempty(Initiator)
    | project CredTime = TimeGenerated, Initiator, CredInitiatorIP, SPName, CorrelationId;
// Explicit high-privilege role names (avoids over-matching e.g. "Storage Blob Data Contributor"
// via a bare "Contributor" substring match).
let HighPrivRoles = dynamic([
    "Owner",
    "Contributor",
    "User Access Administrator",
    "Global Administrator",
    "Company Administrator",
    "Privileged Role Administrator",
    "Privileged Authentication Administrator",
    "Security Administrator",
    "Application Administrator",
    "Cloud Application Administrator",
    "Hybrid Identity Administrator"
]);
let PrivilegedRoleAssignments = AuditLogs
    | where TimeGenerated > ago(2h)
    | where OperationName has_any ("Add member to role", "Add app role assignment to service principal", "Add eligible member to role")
    | where Result =~ "success"
    | extend RoleName = tostring(TargetResources[0].displayName)
    | where RoleName in~ (HighPrivRoles)
    | extend Initiator = coalesce(tostring(InitiatedBy.user.userPrincipalName), tostring(InitiatedBy.app.displayName))
    | extend InitiatorIP = tostring(InitiatedBy.user.ipAddress)
    | where isnotempty(Initiator)
    | project RoleTime = TimeGenerated, Initiator, InitiatorIP, RoleName, CorrelationId;
CredentialAdditions
| join kind=inner PrivilegedRoleAssignments on Initiator
| where RoleTime >= CredTime and RoleTime <= CredTime + 1h
// Tuned: require same source IP for cred-add and role-assign (drops NAT-coincidence FPs where
// two admins share an identity via an on-prem gateway). Falls back to Initiator-only when the
// audit event omits IP (managed identity / app callers).
| where isempty(CredInitiatorIP) or isempty(InitiatorIP) or CredInitiatorIP =~ InitiatorIP
| summarize
    EventCount = count(),
    AssignedRoles = make_set(RoleName, 5),
    AffectedSPs = make_set(SPName, 5),
    InitiatorIPs = make_set(CredInitiatorIP, 5),
    CorrelationIds = make_set(CorrelationId, 5),
    FirstEvent = min(CredTime),
    LastEvent = max(RoleTime)
    by Initiator
| extend
    AccountName = tostring(split(Initiator, "@")[0]),
    AccountUPNSuffix = tostring(split(Initiator, "@")[1])

Explanation

This query is designed to detect potentially suspicious activity in Azure, specifically when a service principal credential is added and then a privileged role is assigned by the same initiator within a 60-minute window. Here's a simplified breakdown of what the query does:

  1. Credential Additions:

    • It looks at audit logs from the past 2 hours to find successful operations where service principal credentials were added or updated.
    • It extracts details like the name of the service principal, the initiator of the action, and their IP address.
  2. Privileged Role Assignments:

    • It checks the same audit logs for successful operations where high-privilege roles (like Owner, Contributor, Global Administrator, etc.) were assigned.
    • It captures details such as the role name, initiator, and their IP address.
  3. Joining and Filtering:

    • The query joins the two sets of data (credential additions and role assignments) based on the initiator.
    • It filters the results to ensure that the role assignment happened within 1 hour after the credential was added.
    • It also checks that both actions came from the same IP address to reduce false positives.
  4. Summarization:

    • The query summarizes the findings by counting the events, listing the roles assigned, affected service principals, initiator IPs, and correlation IDs.
    • It also notes the first and last event times for each initiator.
  5. Output:

    • Finally, it extracts the account name and domain from the initiator's email address for easier identification.

This query helps identify potential security risks by flagging when someone adds credentials to a service principal and then assigns a high-privilege role shortly after, which could indicate an attempt at persistence or privilege escalation.

Details

David Alonso profile picture

David Alonso

Released: July 27, 2026

Tables

AuditLogs

Keywords

AzureAuditLogsOperationNameResultSPNameInitiatorCredInitiatorIPHighPrivRolesRoleNameInitiatorIPEventCountAssignedRolesAffectedSPsInitiatorIPsCorrelationIdsFirstEventLastEventAccountNameAccountUPNSuffix

Operators

let|wherehas_any=~extendtostringcoalesceisnotemptyprojectdynamicin~joinkind=inner>=<=isemptysummarizecountmake_setminmaxbysplit

MITRE Techniques

Actions

GitHub