Query Details

RULE 05 M365 Exchange Mailbox Delegate Anomaly

Query

// Rule    : M365 - Exchange Online Mailbox Accessed by Unusual Delegate
// Severity: High
// Tactics : Collection, CredentialAccess
// MITRE   : T1114.002 (Email Collection: Remote Email Collection),
//           T1078.004 (Valid Accounts: Cloud Accounts)
// Freq    : PT1H   Period: PT1H
// Description: Detects access to a mailbox by a delegate or service account
//              (MailboxOwnerUPN != UserId) where the pattern is new in the past
//              14 days — potential OAuth app abuse or compromised admin account.
//==========================================================================================

let LookbackPeriod    = 1h;
let BaselineDays      = 14d;

// Establish known delegate pairs from the past 14 days
let KnownDelegatePairs = OfficeActivity
    | where TimeGenerated between (ago(BaselineDays) .. ago(LookbackPeriod))
    | where RecordType == "ExchangeItem" or RecordType == "ExchangeItemGroup"
    | where isnotempty(MailboxOwnerUPN) and isnotempty(UserId)
    | where MailboxOwnerUPN != UserId
    | distinct MailboxOwnerUPN, UserId;

// Current window
OfficeActivity
| where TimeGenerated > ago(LookbackPeriod)
| where RecordType in ("ExchangeItem", "ExchangeItemGroup")
| where isnotempty(MailboxOwnerUPN) and isnotempty(UserId)
| where MailboxOwnerUPN != UserId         // accessed by delegate / another user
| join kind=leftanti KnownDelegatePairs
    on MailboxOwnerUPN, UserId            // new pair not seen in baseline
| summarize
    AccessCount     = count(),
    Operations      = make_set(Operation, 10),
    Subjects        = make_set(MailboxOwnerUPN, 5),
    ClientIPs       = make_set(ClientIP, 5),
    FirstSeen       = min(TimeGenerated),
    LastSeen        = max(TimeGenerated)
    by MailboxOwnerUPN, UserId
| extend AlertSeverity = case(
    AccessCount >= 100, "High",
    AccessCount >= 20,  "Medium",
    "Low")
| project
    TimeGenerated  = LastSeen,
    AccessedMailbox = MailboxOwnerUPN,
    AccessedBy     = UserId,
    AccessCount,
    Operations,
    ClientIPs,
    AlertSeverity

Explanation

This query is designed to detect unusual access to Exchange Online mailboxes by delegates or service accounts, which could indicate potential security issues like OAuth app abuse or compromised admin accounts. Here's a simplified breakdown of what the query does:

  1. Lookback Period: The query examines mailbox access over the past hour (LookbackPeriod = 1h).

  2. Baseline Establishment: It first establishes a baseline of known delegate access pairs (where a delegate accesses someone else's mailbox) from the past 14 days (BaselineDays = 14d). This is done by identifying instances where the mailbox owner and the accessing user are different.

  3. Current Access Check: The query then checks for mailbox access events in the last hour that involve a delegate or another user accessing a mailbox. It filters out any access pairs that were already seen in the baseline, focusing only on new delegate access patterns.

  4. Summarization: For these new access patterns, it summarizes the data by counting how many times the mailbox was accessed, listing the operations performed, identifying the IP addresses used, and noting the first and last time the access was seen.

  5. Alert Severity: It assigns a severity level to each access pattern based on how many times the mailbox was accessed:

    • "High" if accessed 100 or more times.
    • "Medium" if accessed 20 to 99 times.
    • "Low" if accessed less than 20 times.
  6. Output: Finally, it outputs the time of the last access, the mailbox accessed, the user who accessed it, the count of accesses, the operations performed, the client IPs used, and the alert severity.

This query helps identify potentially unauthorized or unusual access to mailboxes, which could be a sign of security threats.

Details

David Alonso profile picture

David Alonso

Released: March 18, 2026

Tables

OfficeActivity

Keywords

OfficeActivityMailboxOwnerUPNUserIdRecordTypeOperationClientIPTimeGenerated

Operators

letbetweenagoorisnotempty!=distinctinjoin kind=leftantionsummarizecountmake_setminmaxbyextendcase>=project

Actions