SOCLIFE
SOCLIFE

Evidence-led security analysis

Published knowledge

Search SOC//LIFE

DetectionsDET-004

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

  1. Collect recent successful Consent to application events from Entra AuditLogs.
  2. Parse the target service-principal identifier and the granted delegated permission scope.
  3. Compare the service principal with a historical consent baseline rather than relying on display name.
  4. Require offline access or sensitive delegated permissions before generating a candidate.
  5. Use user, IP, app, permission, and CorrelationId as investigation pivots.
  6. Confirm approved application inventory and business purpose before containment.

Primary analytic

Query

KQL and SPL express the same analytical intent using source-specific schemas.
Q-01Detection logic

Rare 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 desc
SPL
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 - _time

What 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.

  • TimeGenerated
  • LoggedByService
  • Category
  • OperationName
  • Result
  • InitiatedBy
  • TargetResources
  • CorrelationId
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

Use the candidate context to reconstruct what executed, what changed, and what communicated next.
View query — Reconstruct the candidate consent
Q-02Drilldown

Reconstruct 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 asc
SPL
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 _time

What 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.

  • TimeGenerated
  • OperationName
  • Result
  • InitiatedBy
  • TargetResources
  • CorrelationId
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-03Drilldown

Find 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 desc
SPL
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_users

What 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.

  • TimeGenerated
  • OperationName
  • Result
  • InitiatedBy
  • TargetResources
  • CorrelationId
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-04Drilldown

Review 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 asc
SPL
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 _time

What 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.

  • TimeGenerated
  • LoggedByService
  • Category
  • OperationName
  • Result
  • InitiatedBy
  • TargetResources
  • CorrelationId
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-05Drilldown

Review 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 asc
SPL
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 _time

What 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.

  • TimeGenerated
  • UserPrincipalName
  • AppDisplayName
  • AppId
  • IPAddress
  • ResourceDisplayName
  • ResultType
  • ConditionalAccessStatus
  • AuthenticationRequirement
  • CorrelationId
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

Similar activity can be legitimate. Confirm the approved purpose and expected context before escalating.
  • 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.
View threat hunt

Technical boundary

Telemetry and limitations

Identity

Entra application-management audit events with initiating identity, target service principal, permission scope, result, and correlation context.

Required fields
  • TimeGenerated
  • LoggedByService
  • Category
  • OperationName
  • Result
  • InitiatedBy
  • TargetResources
  • AdditionalDetails
  • CorrelationId
Authentication

Entra user sign-ins with user, application, app ID, source, resource, result, Conditional Access, authentication requirement, and correlation context.

Required fields
  • TimeGenerated
  • UserPrincipalName
  • AppDisplayName
  • AppId
  • IPAddress
  • ResourceDisplayName
  • ResultType
  • ConditionalAccessStatus
  • AuthenticationRequirement
  • CorrelationId

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

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.