Query Details

RULE 24 SP O Auth Consent Phishing Email

Query

// Rule    : Workload Identity - Newly Registered OAuth App with Immediate M365 Data Access
// Severity: High
// Tactics : InitialAccess, Persistence, Collection
// MITRE   : T1566.002 (Phishing: Spearphishing Link), T1078.004, T1098.003, T1530
// Freq    : PT1H   Period: PT24H
// Tables  : AuditLogs, OfficeActivity
// Built-in differentiation: No Sentinel built-in correlates a freshly registered Entra ID
// application with immediate OfficeActivity data access from that same application. This
// rule surfaces illicit consent grant outcomes: the phishing app is already accessing
// Exchange, SharePoint, or Teams data within hours of registration — proof that at least
// one user clicked and granted consent. The registration-to-access time delta is the
// primary severity driver.
//==========================================================================================
// Illicit OAuth consent grant attacks: attacker registers a new app → delivers phishing
// link to victims containing a Microsoft OAuth2 consent URL for that app → victim clicks
// and grants delegated permissions. This rule detects the post-consent outcome by finding
// apps registered within the past 24 hours that already appear in OfficeActivity via
// AppAccessContext.ClientAppId — confirming consent was granted and the app is active.

// ---- 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 LookbackWindow   = 24h;
let NewAppLookback   = 24h;
let RapidAccessHours = 6;     // access within 6 hours of registration = highly suspicious

// Recently registered applications and service principals
let NewApps = AuditLogs
    | where TimeGenerated > ago(NewAppLookback)
    | where OperationName in~ ("Add application", "Add service principal")
    | where Result =~ "success"
    | extend AppId      = tostring(TargetResources[0].id)
    | extend AppName    = tostring(TargetResources[0].displayName)
    | extend CreatedBy  = tostring(InitiatedBy.user.userPrincipalName)
    | extend CreatorIP  = tostring(InitiatedBy.user.ipAddress)
    | summarize
        AppName   = any(AppName),
        CreatedBy = any(CreatedBy),
        CreatorIP = any(CreatorIP),
        CreatedAt = min(TimeGenerated)
        by AppId;

// OfficeActivity by the new apps via AppAccessContext.ClientAppId
let AppM365Access = OfficeActivity
    | where TimeGenerated > ago(LookbackWindow)
    | where isnotempty(AppAccessContext)
    | extend AppCtx      = parse_json(AppAccessContext)
    | extend OfficeAppId = tostring(AppCtx.ClientAppId)
    | where isnotempty(OfficeAppId)
    | summarize
        M365AccessCount  = count(),
        M365Operations   = make_set(Operation, 10),
        M365Workloads    = make_set(OfficeWorkload, 5),
        AffectedUsers    = dcount(UserId),
        SampleUsers      = make_set(UserId, 10),
        M365Resources    = make_set(OfficeObjectId, 5),
        M365ClientIPs    = make_set(ClientIP, 5),
        FirstAccess      = min(TimeGenerated)
        by OfficeAppId;

// Only surfaces apps that are BOTH newly registered AND already accessing M365
NewApps
| join kind=inner AppM365Access on $left.AppId == $right.OfficeAppId
| where FirstAccess > CreatedAt
| extend HoursToFirstAccess = datetime_diff("hour", FirstAccess, CreatedAt)
| extend AlertSeverity = case(
    HoursToFirstAccess <= RapidAccessHours and AffectedUsers > 5, "Critical",
    HoursToFirstAccess <= RapidAccessHours,                        "High",
    AffectedUsers > 10,                                             "High",
    "Medium")
| project
    AppId, AppName, CreatedBy, CreatorIP, CreatedAt,
    HoursToFirstAccess, FirstAccess,
    M365AccessCount, M365Operations, M365Workloads,
    AffectedUsers, SampleUsers, M365Resources, M365ClientIPs,
    AlertSeverity
| order by HoursToFirstAccess asc, AffectedUsers desc

Explanation

This query is designed to detect potentially malicious activities involving newly registered OAuth applications in a Microsoft 365 environment. Here's a simplified breakdown of what the query does:

  1. Purpose: The query identifies new applications that have been registered and have quickly gained access to Microsoft 365 data, which could indicate a phishing attack where a user has unknowingly granted permissions to a malicious app.

  2. Data Sources: It uses data from two tables:

    • AuditLogs: To find newly registered applications.
    • OfficeActivity: To track activities performed by these applications in Microsoft 365.
  3. Process:

    • Network Allowlist: It excludes activities from trusted IP addresses to reduce false positives.
    • New Applications: It identifies applications registered within the last 24 hours.
    • M365 Access: It checks if these new applications have accessed Microsoft 365 data within the same period.
  4. Detection Criteria:

    • The query looks for applications that have accessed Microsoft 365 data within 6 hours of registration, which is considered suspicious.
    • It evaluates the number of affected users and assigns a severity level (Critical, High, or Medium) based on how quickly the app accessed data and the number of users affected.
  5. Output:

    • The query outputs details about the suspicious applications, including their ID, name, creator, IP address, time of creation, time to first access, and the severity of the alert.
    • It also provides information about the operations performed, workloads accessed, and users affected.

Overall, this query helps security teams identify and respond to potential OAuth consent grant attacks, where attackers trick users into granting permissions to malicious applications.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AuditLogsOfficeActivity

Keywords

WorkloadIdentityOAuthAppM365DataAccessPhishingExchangeSharePointTeamsUserIPAddressApplicationServicePrincipalOfficeActivityAppAccessContextClientAppIdAuditLogsOperationNameResultAppIdAppNameCreatedByCreatorIPTimeGeneratedOfficeWorkloadOfficeObjectIdClientIPAlertSeverity

Operators

materializeunionprinttakeprojectwhereisnotemptymatches regexextendiffstrcatsummarizemake_listtoscalararray_lengthisnullipv4_is_in_any_rangemv-applysplitipv4_comparemaxtointproject-awayagoin~=~tostringparse_jsoncountmake_setdcountjoinkind=inneron==datetime_diffcase<=>order byascdesc

Actions