Behavior-based detection engineering
Rare OAuth Consent with Sensitive Delegated Permissions
Detects newly observed OAuth application consent when the grant includes offline access or sensitive delegated permissions, then exposes app, user, and permission pivots for investigation.
Behavior
What it detects
A successful Consent to application event creates access for a service principal that was not observed in the historical consent baseline and the granted scope contains offline access or sensitive mail, file, contact, or send permissions.
Engineering decision
Why this detection
Consent events are not malicious by default. The durable signal is a newly observed application integration receiving access that is both unusual for the environment and meaningful for an attacker.
This analytic therefore combines service-principal rarity with offline access or sensitive delegated scopes. It deliberately avoids trusting display names as application identity and keeps approval, publisher, permission, owner, and rollout context in the analyst-confirmation stage.
The drilldowns turn a candidate into a concrete investigation: reconstruct the grant, find other affected users, review permission expansion, and inspect app/user sign-in context.
Signal chain
Detection logic
- Collect recent successful Consent to application events from Entra AuditLogs.
- Parse the target service-principal identifier and the granted delegated permission scope.
- Compare the service principal with a historical consent baseline rather than relying on display name.
- Require offline access or sensitive delegated permissions before generating a candidate.
- Use user, IP, app, permission, and CorrelationId as investigation pivots.
- Confirm approved application inventory and business purpose before containment.
Primary analytic
Query
Q-01Detection logicRare consent with sensitive delegated permissions
What this checks
Generate candidates when a newly observed service principal receives successful user consent with offline access or sensitive delegated permissions.
KQL
let detection_window = 1d;
let baseline_window = 30d;
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=-30d
(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(),"-1d"),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 successful recent consent to a service principal absent from the historical baseline where the grant includes offline access or sensitive mail/file/contact scopes.
Technical details
Tested signal
First-seen service-principal consent plus durable or high-value delegated scope.
Assumptions
- ApplicationManagement audit events are retained for at least thirty days.
- Service-principal identifiers and the granted delegated permission scope are available.
Data requirements and relevant fields
- identity
Entra application-management audit events with initiating identity, target service principal, permission scope, result, 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
- New legitimate applications can match during onboarding.
- A thirty-day baseline can miss seasonal or rarely used approved applications.
- Display name is not used as the primary baseline key.
The candidate logic borrows the durable ideas from Microsoft Sentinel's rare-consent and offline-access analytics but uses the service-principal identifier as the historical key and treats the query as an adaptable production pattern.
Analyst workflow
What the analyst should look for
- Is this service principal approved by the organization?
- Is the publisher verified and expected?
- What delegated scopes were granted?
- Does the grant include offline_access or sensitive mail/file/contact permissions?
- Has this service principal received consent before?
- Did other users authorize the same app?
- Were delegated permission grants or app-role assignments added around the same time?
- Do sign-in logs show the affected user interacting with the candidate app?
- Does the activity align with an approved software deployment or change?
Expected result
A successful recent consent to a service principal absent from the historical baseline where the grant includes offline access or sensitive mail/file/contact scopes.
Investigation pivots
Drilldowns
View query — Reconstruct the candidate consent
Q-02DrilldownReconstruct the candidate consent
What this checks
Recover the exact candidate consent event by user, service-principal identifier, or CorrelationId.
KQL
let target_user = "denis@example.com";
AuditLogs
| where TimeGenerated >= ago(7d)
| 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)
| where InitiatingUser == tolower(target_user)
| 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 "]" *
| project
TimeGenerated,
InitiatingUser,
InitiatingIP,
AppDisplayName,
ServicePrincipalId,
ConsentType,
GrantScope,
CorrelationId
| order by TimeGenerated ascSPL
index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
earliest=-7d
(OperationName="Consent to application" OR ActivityDisplayName="Consent to application")
| eval
user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
src=coalesce(src, ipAddress, InitiatingIpAddress),
app_name=coalesce(app_name, TargetResourceDisplayName, AppDisplayName),
service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId),
permissions=coalesce(permissions, ConsentActionPermissions, GrantScope),
consent_type=coalesce(consent_type, ConsentType),
correlation_id=coalesce(correlation_id, CorrelationId),
result=lower(coalesce(result, Result))
| where user="denis@example.com" AND result="success"
| fields _time user src app_name service_principal_id permissions consent_type correlation_id
| sort 0 _timeWhat to look for
The exact consent event and full permission scope that generated the candidate.
Technical details
Tested signal
Candidate Consent to application event with original permission string.
Assumptions
- The detection candidate provides at least one stable pivot: user, service-principal identifier, or CorrelationId.
Data requirements and relevant fields
- identity
Candidate Entra audit event with initiating identity, target service principal, permission string, result, and correlation ID.
TimeGeneratedOperationNameResultInitiatedByTargetResourcesCorrelationId
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
- Exports can flatten TargetResources differently.
KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.
View query — Find other users consenting to the same app
Q-03DrilldownFind other users consenting to the same app
What this checks
Measure whether the candidate application received consent from additional users.
KQL
let candidate_service_principal = "<service_principal_id>";
AuditLogs
| where TimeGenerated >= ago(30d)
| 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)
)
| where ServicePrincipalId == candidate_service_principal
| summarize
FirstSeen=min(TimeGenerated),
LastSeen=max(TimeGenerated),
ConsentEvents=count(),
Users=make_set(InitiatingUser, 500),
SourceIPs=make_set(InitiatingIP, 100)
by AppDisplayName, ServicePrincipalId
| extend UniqueUsers=array_length(Users)
| order by UniqueUsers descSPL
index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
earliest=-30d
(OperationName="Consent to application" OR ActivityDisplayName="Consent to application")
| eval
user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
src=coalesce(src, ipAddress, InitiatingIpAddress),
app_name=coalesce(app_name, TargetResourceDisplayName, AppDisplayName),
service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId)
| where service_principal_id="<service_principal_id>"
| stats
min(_time) as first_seen
max(_time) as last_seen
count as consent_events
dc(user) as unique_users
values(user) as users
values(src) as source_ips
by app_name service_principal_id
| convert ctime(first_seen) ctime(last_seen)
| sort - unique_usersWhat to look for
Multiple unique users granting the candidate app or a rapid recent increase in consent.
Technical details
Tested signal
Same service principal authorized by several identities.
Assumptions
- Replace the app/service-principal placeholder with the detection candidate.
Data requirements and relevant fields
- identity
Consent events with user, source, target service principal, result, and correlation context.
TimeGeneratedOperationNameResultInitiatedByTargetResourcesCorrelationId
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
- Legitimate enterprise onboarding can create a multi-user pattern.
KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.
View query — Review related permission grants
Q-04DrilldownReview related permission grants
What this checks
Find delegated-permission grants or app-role assignments around the candidate application to determine whether access expanded beyond the original user consent.
KQL
let candidate_service_principal = "<service_principal_id>";
AuditLogs
| where TimeGenerated >= ago(7d)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName in~ (
"Add delegated permission grant",
"Add app role assignment to the service principal"
)
| where Result =~ "success"
| where tostring(TargetResources) has candidate_service_principal
| extend
InitiatingUser=tolower(tostring(InitiatedBy.user.userPrincipalName)),
InitiatingApp=tostring(InitiatedBy.app.displayName),
InitiatingIP=tostring(InitiatedBy.user.ipAddress)
| project TimeGenerated, OperationName, InitiatingUser, InitiatingApp, InitiatingIP, TargetResources, AdditionalDetails, CorrelationId
| order by TimeGenerated ascSPL
index=<entra_audit_index> sourcetype=<entra_audit_sourcetype>
earliest=-7d
(OperationName="Add delegated permission grant"
OR OperationName="Add app role assignment to the service principal"
OR ActivityDisplayName="Add delegated permission grant"
OR ActivityDisplayName="Add app role assignment to the service principal")
| eval
user=lower(coalesce(user, userPrincipalName, InitiatedByUserPrincipalName)),
src=coalesce(src, ipAddress, InitiatingIpAddress),
operation=coalesce(OperationName, ActivityDisplayName),
service_principal_id=coalesce(service_principal_id, TargetResourceId, ServicePrincipalId),
correlation_id=coalesce(correlation_id, CorrelationId)
| where service_principal_id="<service_principal_id>"
| table _time user src operation app_name service_principal_id permissions correlation_id
| sort 0 _timeWhat to look for
Additional permission-grant or app-role operations involving the candidate app and lacking an approved change.
Technical details
Tested signal
Add delegated permission grant or app-role assignment related to the candidate service principal.
Assumptions
- ApplicationManagement audit activity is retained and the candidate service-principal identifier is known.
Data requirements and relevant fields
- identity
Application-management audit events for delegated permission grants and app-role assignments.
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
- Administrative app onboarding can legitimately create these operations.
KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.
View query — Review user sign-ins to the candidate app
Q-05DrilldownReview user sign-ins to the candidate app
What this checks
Rebuild the user's sign-in context for the candidate application after consent.
KQL
let target_user = "denis@example.com";
let target_app = "Share-File Point Document";
SigninLogs
| where TimeGenerated >= ago(7d)
| where UserPrincipalName =~ target_user
| where AppDisplayName =~ target_app
| project
TimeGenerated,
UserPrincipalName,
AppDisplayName,
AppId,
IPAddress,
ResourceDisplayName,
ResultType,
ConditionalAccessStatus,
AuthenticationRequirement,
CorrelationId
| order by TimeGenerated ascSPL
index=<identity_index> sourcetype=<entra_user_signin_sourcetype>
earliest=-7d
| eval
user=lower(coalesce(user, UserPrincipalName, user_principal_name)),
app_name=coalesce(app_name, AppDisplayName),
app_id=coalesce(app_id, AppId),
src=coalesce(src, IPAddress, ip_address),
resource=coalesce(resource, ResourceDisplayName),
result=coalesce(result, ResultType),
conditional_access=coalesce(conditional_access, ConditionalAccessStatus),
auth_requirement=coalesce(auth_requirement, AuthenticationRequirement),
correlation_id=coalesce(correlation_id, CorrelationId)
| where user="denis@example.com" AND app_name="Share-File Point Document"
| fields _time user app_name app_id src resource result conditional_access auth_requirement correlation_id
| sort 0 _timeWhat to look for
A timeline showing when and from where the affected identity used the newly authorized app.
Technical details
Tested signal
Sign-in activity where the candidate app and affected user appear together.
Assumptions
- SigninLogs is available and the candidate user/app values are known.
Data requirements and relevant fields
- authentication
Entra user sign-ins with user, app, app ID, source, resource, result, Conditional Access, authentication requirement, and correlation ID.
TimeGeneratedUserPrincipalNameAppDisplayNameAppIdIPAddressResourceDisplayNameResultTypeConditionalAccessStatusAuthenticationRequirementCorrelationId
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
- Token/API use may not produce a distinctive interactive sign-in for every resource operation.
KQL uses Microsoft Entra/Azure Monitor tables; SPL is a raw normalized adaptation scaffold and requires local field mapping.
Legitimate resemblance
What the analyst should confirm
A newly introduced legitimate SaaS application receives user consent for permissions required by an approved business workflow.
The service principal, publisher, requested scopes, owner, change record, rollout window, and affected users all match the approved onboarding plan.A vendor application requires offline access or mail/file permissions as part of an established integration.
The exact application/service-principal identifier and permission set match approved inventory, the publisher is expected, and consent volume aligns with the deployment population.A security or compliance application is intentionally granted sensitive delegated access.
The app owner, service principal, permissions, administrative approval, and post-grant activity match the documented control deployment.
Confirmed match
Action after a confirmed match
- Revoke the illicit delegated consent grant.
- Disable or remove the malicious enterprise application/service principal when appropriate.
- Identify and remediate every affected user.
- Review all granted permissions and exposed resources.
- Review sign-in and SaaS activity after the grant.
- Reset credentials only if separate credential compromise is suspected.
- Restrict user consent and route higher-risk requests through an admin-consent workflow.
Threat hunt
Could this be happening elsewhere?
Hunt for this behavior across the environment.Technical boundary
Telemetry and limitations
- Identity
Entra application-management audit events with initiating identity, target service principal, permission scope, result, and correlation context.
Required fieldsTimeGeneratedLoggedByServiceCategoryOperationNameResultInitiatedByTargetResourcesAdditionalDetailsCorrelationId
- Authentication
Entra user sign-ins with user, application, app ID, source, resource, result, Conditional Access, authentication requirement, and correlation context.
Required fieldsTimeGeneratedUserPrincipalNameAppDisplayNameAppIdIPAddressResourceDisplayNameResultTypeConditionalAccessStatusAuthenticationRequirementCorrelationId
Blind spots
- Tenants that do not ingest Entra AuditLogs cannot directly reconstruct Consent to application events.
- Flattened or truncated target-resource permission details can hide the full delegated scope.
- Application display names can be spoofed; investigations that do not preserve stable identifiers can misattribute apps.
- A malicious app can use valid tokens without producing a distinctive interactive sign-in for every downstream API action.
- Approved inventory that is stale or incomplete can cause either false positives or missed unapproved applications.
Behavior mapping
MITRE ATT&CK
T1528· Steal Application Access TokenConsent phishing can provide application access and refresh tokens tied to granted scopes.
T1671· Cloud Application IntegrationUnexpected OAuth application integrations can create persistent SaaS access.
T1566.002· Spearphishing LinkThe consent request can be delivered through a phishing link.
T1204.001· Malicious LinkUser interaction with the consent link and authorization flow enables the grant.
Mappings describe the behavior examined by this analytic. They do not prove attribution, deployment, or technique-wide coverage.
Review boundary
Sources and limits
- External sources
- 5
Exact fields, retention, and operational thresholds remain environment-specific.