Query Details
// Rule : Workload Identity - SP Sensitive API Permission Grant Detected
// Severity: High
// Tactics : PrivilegeEscalation, Persistence
// MITRE : T1098.003 (Account Manipulation: Additional Cloud Roles),
// T1078.004 (Valid Accounts: Cloud Accounts)
// Freq : PT1H Period: PT1H
// Tables : AuditLogs
// Built-in differentiation: Sentinel built-in "Service Principal assigned privileged role"
// detects Azure RBAC assignment (Microsoft.Authorization role assignments). This rule
// detects OAuth2 / Microsoft Graph API permission consent grants — a separate and equally
// dangerous escalation vector that assigns abilities like reading all mailboxes, writing
// directory objects, or managing all app registrations, which are not Azure RBAC roles.
//==========================================================================================
// Attackers who compromise or control an app registration use OAuth2 permission consent
// to grant the SP access to sensitive Microsoft Graph scopes without assigning any
// traditional Azure RBAC role. These permissions can survive SP certificate rotation
// and are often overlooked in role-based access reviews.
// Sensitive OAuth2 scopes / app roles to monitor
// ---- 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 DangerousPermissions = dynamic([
"RoleManagement.ReadWrite.Directory", // Can assign any directory role
"Application.ReadWrite.All", // Can create/modify apps and add creds
"AppRoleAssignment.ReadWrite.All", // Can grant itself or others any app role
"Directory.ReadWrite.All", // Full directory write access
"Mail.ReadWrite", // Read/write all mailboxes
"MailboxSettings.ReadWrite", // Modify mailbox forwarding rules
"full_access_as_app", // Exchange full tenant access
"Exchange.ManageAsApp", // Exchange management
"User.ReadWrite.All", // Modify all user accounts
"Group.ReadWrite.All", // Modify all groups
"Contacts.ReadWrite", // Access all contact data
"Files.ReadWrite.All" // Read/write all OneDrive/SharePoint files
]);
// --- Capture permission grant events ---
let PermissionGrants = AuditLogs
| where TimeGenerated > ago(1h)
| where OperationName in (
"Add delegated permission grant",
"Add app role assignment to service principal",
"Consent to application",
"Add OAuth2PermissionGrant"
)
| where Result =~ "success"
| extend InitiatorId = tostring(InitiatedBy.user.id)
| extend InitiatorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatorSPId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatorDisplayName = coalesce(
tostring(InitiatedBy.user.userPrincipalName),
tostring(InitiatedBy.app.displayName),
"Unknown"
)
| extend TargetSPObjectId = tostring(TargetResources[0].id)
| extend TargetSPName = tostring(TargetResources[0].displayName)
// Check the raw modifiedProperties array for the permission value
| mv-expand ModProp = TargetResources[0].modifiedProperties
| where tostring(ModProp.displayName) in (
"DelegatedPermissionGrant.Scope",
"ServicePrincipal.OAuth2PermissionGrants",
"AppRole.Value",
"ServicePrincipal.Oauth2PermissionGrants"
)
| extend PermissionValue = tostring(ModProp.newValue)
| where PermissionValue has_any (DangerousPermissions)
| summarize
GrantCount = count(),
GrantedPerms = make_set(PermissionValue, 20),
TargetSPs = make_set(TargetSPName, 10),
TargetSPIds = make_set(TargetSPObjectId, 10),
Operations = make_set(OperationName, 5),
FirstGrant = min(TimeGenerated),
LastGrant = max(TimeGenerated)
by InitiatorDisplayName, InitiatorId, InitiatorSPId;
// --- Correlate with recent SP creation (possible new malicious app) ---
let RecentSPCreations = AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName == "Add service principal"
| extend CreatedSPId = tostring(TargetResources[0].id)
| extend CreatedSPName = tostring(TargetResources[0].displayName)
| project CreatedSPId, CreatedSPName, SPCreatedAt = TimeGenerated;
PermissionGrants
| mv-expand TargetSPId = TargetSPIds
| extend TargetSPId = tostring(TargetSPId)
| join kind=leftouter (RecentSPCreations) on $left.TargetSPId == $right.CreatedSPId
| extend IsNewSP = isnotempty(CreatedSPId) and (LastGrant - SPCreatedAt) < 7d
| extend HasDangerousCombo = GrantedPerms has "RoleManagement.ReadWrite.Directory"
or GrantedPerms has "Application.ReadWrite.All"
or GrantedPerms has "full_access_as_app"
or GrantedPerms has "Exchange.ManageAsApp"
| extend SeverityLevel = case(
IsNewSP and HasDangerousCombo, "Critical — New SP granted tenant-wide admin Graph scope",
HasDangerousCombo, "High — Dangerous OAuth2 scope granted",
IsNewSP, "High — Sensitive scope granted to recently created SP",
"Medium — Sensitive Graph API scope granted")
| project-away CreatedSPId, TargetSPId
This query is designed to detect potentially dangerous permission grants to service principals (SPs) in an Azure environment. Here's a simplified breakdown of what it does:
Purpose: The query aims to identify when sensitive permissions are granted to service principals via OAuth2 or Microsoft Graph API, which can be a sign of privilege escalation or persistence tactics by attackers.
Sensitive Permissions: It focuses on a list of dangerous permissions that allow extensive access, such as reading/writing all mailboxes, modifying directory objects, or managing app registrations.
Network Allowlist: The query excludes events from trusted IP addresses or ranges, which are specified in a watchlist called 'NetworkAllowlist'.
Permission Grant Events: It looks for successful permission grant events in the last hour, specifically those that add delegated permissions, app role assignments, or consent to applications.
Dangerous Permissions Detection: It checks if any of the granted permissions match the list of dangerous permissions.
Recent SP Creations: The query also checks if any of these permissions were granted to service principals created in the last 7 days, which could indicate a newly created malicious app.
Severity Assessment: It assigns a severity level based on the combination of factors, such as whether the SP is new and if it has been granted particularly dangerous permissions.
Output: The query outputs information about the initiator of the grant, the permissions granted, the target service principals, and the severity level of the detected activity.
In essence, this query helps security teams identify and prioritize potentially risky permission grants that could lead to unauthorized access or control over critical resources in an Azure environment.

David Alonso
Released: April 21, 2026
Tables
Keywords
Operators