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:
-
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.
-
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.
-
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.
-
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.
-
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
Released: July 27, 2026
Tables
AuditLogs
Keywords
AzureAuditLogsOperationNameResultSPNameInitiatorCredInitiatorIPHighPrivRolesRoleNameInitiatorIPEventCountAssignedRolesAffectedSPsInitiatorIPsCorrelationIdsFirstEventLastEventAccountNameAccountUPNSuffix
Operators
let|wherehas_any=~extendtostringcoalesceisnotemptyprojectdynamicin~joinkind=inner>=<=isemptysummarizecountmake_setminmaxbysplit
MITRE Techniques