Find new consent grants with sensitive scopes
Finding
The starting search identifies newly observed service principals whose consent includes offline access or sensitive delegated scopes.
This is the behavior-first candidate population and does not depend on a campaign IOC.
View query
Q-01First searchFind new consent grants with sensitive scopes
What this checks
Use a wider hunt window to identify newly observed service principals receiving user consent with offline access or sensitive delegated permissions.
KQL
let detection_window = 7d;
let baseline_window = 60d;
let historical_apps =
AuditLogs
| where TimeGenerated between (ago(baseline_window) .. ago(detection_window))
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Consent to application"
| mv-apply TargetResource = TargetResources on (
where TargetResource.type =~ "ServicePrincipal"
| extend ServicePrincipalId = tostring(TargetResource.id)
)
| summarize HistoricalConsents=count() by ServicePrincipalId;
AuditLogs
| where TimeGenerated >= ago(detection_window)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Consent to application"
| where Result =~ "success"
| extend
InitiatingUser = tolower(tostring(InitiatedBy.user.userPrincipalName)),
InitiatingIP = tostring(InitiatedBy.user.ipAddress)
| mv-apply TargetResource = TargetResources on (
where TargetResource.type =~ "ServicePrincipal"
| extend
AppDisplayName = tostring(TargetResource.displayName),
ServicePrincipalId = tostring(TargetResource.id),
ModifiedProperties = TargetResource.modifiedProperties
)
| mv-apply Property = ModifiedProperties on (
where Property.displayName =~ "ConsentAction.Permissions"
| extend ConsentFull = trim(@'"', tostring(Property.newValue))
)
| parse ConsentFull with * "ConsentType: " ConsentType ", Scope: " GrantScope "]" *
| extend SensitiveScope =
GrantScope has "offline_access"
or GrantScope has_any (
"Mail.Read",
"Mail.ReadWrite",
"Mail.Send",
"Files.Read.All",
"Files.ReadWrite.All",
"Contacts.Read",
"Contacts.ReadWrite"
)
| join kind=leftouter historical_apps on ServicePrincipalId
| extend HistoricalConsents = coalesce(HistoricalConsents, 0)
| where HistoricalConsents == 0 and SensitiveScope
| project
TimeGenerated,
InitiatingUser,
InitiatingIP,
AppDisplayName,
ServicePrincipalId,
ConsentType,
GrantScope,
HistoricalConsents,
CorrelationId
| order by TimeGenerated descSPL
index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
earliest=-60d
(OperationName="Consent to application" OR ActivityDisplayName="Consent to application")
| eval
user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
src=coalesce(src, ipAddress, InitiatingIpAddress),
app_name=lower(coalesce(app_name, TargetResourceDisplayName, AppDisplayName)),
service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId),
permissions=lower(coalesce(permissions, ConsentActionPermissions, GrantScope)),
consent_type=coalesce(consent_type, ConsentType),
correlation_id=coalesce(correlation_id, CorrelationId),
result=lower(coalesce(result, Result)),
is_current=if(_time>=relative_time(now(),"-7d"),1,0)
| where result="success"
| eval sensitive_scope=if(
like(permissions,"%offline_access%")
OR like(permissions,"%mail.read%")
OR like(permissions,"%mail.readwrite%")
OR like(permissions,"%mail.send%")
OR like(permissions,"%files.read.all%")
OR like(permissions,"%files.readwrite.all%")
OR like(permissions,"%contacts.read%"),
1,0
)
| eventstats count(eval(is_current=0)) as historical_consents by service_principal_id
| where is_current=1 AND historical_consents=0 AND sensitive_scope=1
| table _time user src app_name service_principal_id consent_type permissions historical_consents correlation_id
| sort 0 - _timeWhat to look for
A population of new/sensitive consent grants suitable for app and user scoping.
Technical details
Tested signal
New consent plus durable or sensitive permission scope.
Assumptions
- ApplicationManagement audit events are retained for at least sixty days.
Data requirements and relevant fields
- identity
Entra consent events with initiating identity, target service principal, permission scope, and correlation context.
TimeGeneratedLoggedByServiceCategoryOperationNameResultInitiatedByTargetResourcesCorrelationId
KQL schema
Uses documented Microsoft Entra/Azure Monitor fields; validate connector availability and local retention.
SPL schema
Replace index/sourcetype placeholders and map the documented concepts to the local Entra audit or sign-in source.
Limitations
- A sixty-day baseline can still miss very rarely used approved applications.
This is the behavior-first starting point. It intentionally does not depend on the source-campaign app name.