Free Reference

The M365 and Entra ID Detection Reference

Every attacker technique that appears in real Microsoft 365 and Entra ID intrusions, mapped to the MITRE ATT&CK Identity Provider and Office Suite matrices, with the KQL to detect it, the data source it queries, and the false positives to expect. Built for the analyst who has to catch this in production, not memorize a framework.

What this is

Microsoft 365 and Entra ID are attacked through their own features, not through software flaws. An adversary who phishes one session cookie, consents one malicious app, or registers one rogue device is using the platform exactly as designed. Detection is how you catch that, and detection in M365 means knowing which log records the behavior, what normal looks like, and the query that separates the two.

This reference covers the M365 and Entra attack surface the way MITRE scopes it: the Identity Provider matrix (Entra ID, the identity plane) and the Office Suite matrix (Exchange, SharePoint, Teams, the productivity plane). For each technique that appears in real intrusions you get what it looks like in the logs, the KQL that detects it, the data source it queries, and the false positives that will fool you if you deploy it without tuning.

Not another query dump. Every detection here is explained, not just listed. The point is that you understand why the query works and what it misses, so you can tune it to your tenant rather than paste it and hope. Where a technique has a deeper treatment, the section links to the full cheatsheet that covers it.

The reference, in four parts

This hub pulls together four detailed references into one working order. Read them as a sequence: understand the attack surface, hunt for activity, write the detection, engineer it as a repeatable discipline.

Coverage map

The reference maps to the MITRE ATT&CK Identity Provider and Office Suite matrices. Each technique below links to its detection guidance. This is a living reference: sections marked authoring are being written to the same verified standard as the rest and will link through as they land.

Detection published, links to its section Covered within a related detection section
Initial Access
Credential Access
Persistence and Privilege Escalation
Defense Evasion and Discovery
Collection and Exfiltration
Impact and Fraud

Credential Access

This is where most M365 intrusions start. The attacker wants a valid session, and in a cloud-first tenant that means phishing a token, spraying a password, or fatiguing a user into approving MFA. The detections below query the identity plane. One schema note before you deploy any of them: on December 9, 2025 the advanced-hunting table AADSignInEventsBeta was replaced by EntraIdSignInEvents. If you are copying older queries that reference the beta table, update them. The queries here use the current tables.

T1557Adversary-in-the-Middle (AiTM)
also T1539 Steal Web Session Cookie, T1111 MFA Interception
Source: SigninLogs / EntraIdSignInEvents

AiTM phishing puts a reverse proxy between the user and the real login page, captures the authenticated session cookie after MFA completes, and replays it. The attacker never needs the password or the second factor again. The tell is in the sign-in risk signals: Entra Identity Protection flags these sessions with specific risk event types. Hunt for them directly rather than waiting for an alert.

SigninLogs | where TimeGenerated > ago(7d) | where RiskLevelDuringSignIn in ("high", "medium") // or match the risk detections Identity Protection assigns to AiTM or RiskEventTypes_V2 has_any ("adversaryInTheMiddle", "anomalousToken", "tokenIssuerAnomaly") | project TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, RiskLevelDuringSignIn, RiskEventTypes_V2, ConditionalAccessStatus, ResultType | sort by TimeGenerated desc

The stronger signal is correlation. A user who clicks a phishing URL and then produces a risky sign-in within the hour is the AiTM pattern in two events. If you have Defender for Office 365, join the click to the sign-in:

let clicks = UrlClickEvents | where Timestamp > ago(7d) | where ThreatTypes has "Phish" | project ClickTime = Timestamp, AccountUpn, Url; clicks | join kind=inner ( EntraIdSignInEvents | where RiskLevelDuringSignIn in ("high", "medium") | project SignInTime = Timestamp, AccountUpn, IPAddress, RiskLevelDuringSignIn ) on AccountUpn | where SignInTime between (ClickTime .. (ClickTime + 1h)) | project ClickTime, SignInTime, AccountUpn, Url, IPAddress, RiskLevelDuringSignIn
False positives: legitimate VPN egress, corporate proxies, and users travelling produce medium-risk sign-ins. Tune by excluding known egress IPs and treating high-risk plus a phishing click as the high-confidence combination. Risk level alone is a hunt lead, not an alert.
T1528Steal Application Access Token
OAuth token abuse, illicit consent
Source: AuditLogs / CloudAppEvents

Rather than steal a session, the attacker gets the user to consent to a malicious OAuth application, which is then granted a token with the scopes the user approved. The token persists independent of the user's password and MFA. Detection lives in the consent and app-role-assignment events in AuditLogs.

AuditLogs | where TimeGenerated > ago(30d) | where OperationName in ("Consent to application", "Add app role assignment grant to user") | extend target = tostring(TargetResources[0].displayName) | extend actor = tostring(InitiatedBy.user.userPrincipalName) | mv-expand ModifiedProperties = TargetResources[0].modifiedProperties | where ModifiedProperties.displayName == "ConsentAction.Permissions" | project TimeGenerated, actor, target, Permissions = tostring(ModifiedProperties.newValue) | sort by TimeGenerated desc
False positives: admins legitimately consenting to new SaaS integrations. The signal to prioritise is user consent (not admin) to an app requesting high-value Graph scopes such as Mail.ReadWrite, Files.ReadWrite.All, or full directory access. Maintain an allowlist of approved app IDs and alert on anything outside it.
T1110.003Brute Force: Password Spraying
also T1110.004 Credential Stuffing
Source: SigninLogs

Password spraying tries one password across many accounts to stay under lockout thresholds. In the sign-in logs it is a single source (or a small set of sources) generating failures across a large set of distinct users, with the occasional success. The detection is a fan-out on failed sign-ins grouped by source.

SigninLogs | where TimeGenerated > ago(1d) | where ResultType != 0 // 50126 = invalid password; the classic spray error code | where ResultType == 50126 | summarize FailedUsers = dcount(UserPrincipalName), Attempts = count(), Users = make_set(UserPrincipalName, 15) by IPAddress, bin(TimeGenerated, 1h) | where FailedUsers >= 10 | sort by FailedUsers desc
False positives: a misconfigured application or a shared NAT egress can produce many failures from one IP without being an attack. Confirm by checking whether the failures span many distinct users (spray) or repeat against one user (misconfig), and whether any success followed. Credential stuffing (T1110.004) looks similar but pairs many users with many unique passwords from breach dumps; the same fan-out query catches it, distinguished by a higher password-uniqueness rate.
T1621MFA Request Generation
MFA fatigue, push bombing
Source: SigninLogs / AADUserRiskEvents

Once an attacker has a valid password (from a spray or a dump), they trigger repeated MFA push notifications hoping the user approves one out of annoyance. The pattern is a burst of MFA challenges to a single user in a short window, ending in either repeated denials or one approval. The sign-in logs record each challenge and its result.

SigninLogs | where TimeGenerated > ago(1d) | where ResultType in (0, 50074, 50076, 500121) // 50074/50076/500121 = MFA required / denied / not satisfied | summarize Challenges = count(), Denials = countif(ResultType != 0), Approved = countif(ResultType == 0) by UserPrincipalName, IPAddress, bin(TimeGenerated, 15m) | where Challenges >= 5 and Denials >= 3 | sort by Challenges desc
False positives: a user with a flaky device or bad connectivity can generate repeated MFA prompts legitimately. The high-confidence version pairs the burst with a prior password success from an unfamiliar IP, and an eventual approval after multiple denials, which is the fatigue succeeding.
T1556.006Modify Authentication Process: MFA
attacker registers or alters MFA methods for persistence
Source: AuditLogs

After compromising an account, the attacker registers their own MFA method so they can authenticate independently of the user going forward. This is quiet persistence and it is logged as a security-info registration in AuditLogs. Correlating it with a preceding risky sign-in turns a noisy event into a high-fidelity one.

AuditLogs | where TimeGenerated > ago(7d) | where OperationName in ("User registered security info", "User registered all required security info", "Admin registered security info") | extend actor = tostring(InitiatedBy.user.userPrincipalName) | extend method = tostring(TargetResources[0].displayName) | project TimeGenerated, actor, method, ResultDescription | sort by TimeGenerated desc
False positives: genuine new-device enrolment and onboarding produce these events constantly. This detection only earns its keep correlated: an MFA registration within a short window of a high-risk sign-in for the same user is the persistence pattern. Registration alone is normal.
T1606.002Forge Web Credentials: SAML Tokens (Golden SAML)
T1649-adjacent: theft or forging of the AD FS token-signing certificate
Source: SecurityEvent (AD FS Auditing) + SigninLogs

If an attacker extracts the AD FS token-signing certificate, they can forge SAML tokens claiming any identity and any privilege, and those tokens are cryptographically valid. There is no clean signature, which is why this technique is dangerous. Detection is three anomaly patterns, not one rule. First, on the AD FS side, watch token-issuance events for volume anomalies from accounts that do not normally request them.

SecurityEvent | where TimeGenerated > ago(7d) | where EventSourceName == "AD FS Auditing" // 1200/1201/1202 = token issuance events | where EventID in (1200, 1201, 1202) | summarize TokensIssued = count() by Account, bin(TimeGenerated, 1h) | where TokensIssued > 50 | sort by TokensIssued desc

Second, on the cloud side, the forged authentications arrive at the service provider. Golden SAML logins appear in the sign-in logs with unusual IPs, unexpected user agents, or an auth context that does not match the user's normal pattern. The Microsoft 365 sign-in logs record the federation source, so a federated sign-in from an anomalous location for a privileged account is the second pattern to hunt.

False positives: the AD FS service account itself requests key access at service start-up, so DKM-access detections fire on legitimate restarts. Tune by excluding the AD FS service account name and correlating key access that happens out of sync with a service restart. Because the token is valid, cloud-side detection depends on claims-versus-reality mismatch: a token asserting an admin role the account does not currently hold in Entra.
What this batch does not yet cover, and where it is going. Certificate theft and forging (T1649) beyond the SAML case, and the full conditional-access and hybrid-identity tampering set (T1556.007, T1556.009), land in the Persistence section, since in practice they are persistence and privilege-escalation moves as much as credential-access ones. The next batches author Persistence, then Collection and Exfiltration, then Discovery and Defense Evasion.

Persistence and Privilege Escalation

Once an attacker has a session, they want to keep it and widen it. In M365 that means planting something that survives a password reset: a device they control, a credential on an application, a federated domain pointing at their own identity provider, or a weakened conditional access policy. These are the quiet moves. Most of them are logged in AuditLogs as routine administrative operations, which is exactly why they hide. The detections below separate the attacker's version from the admin's.

T1098.005Account Manipulation: Device Registration
register an attacker-controlled device to satisfy conditional access
Source: AuditLogs + SigninLogs

Conditional access that requires a compliant or managed device is only as strong as the device-registration process behind it. An attacker who registers their own device, or joins one that gets marked compliant, turns a device-based control into a door they hold the key to. The high-signal pattern is not the registration alone, it is the sequence: a sign-in from an unusual platform where no CA policy applied, followed shortly by a new device registration for the same user.

let newDevices = AuditLogs | where TimeGenerated > ago(7d) | where OperationName in ("Add device", "Update device") | extend actor = tostring(InitiatedBy.user.userPrincipalName) | where isnotempty(actor) | project DeviceTime = TimeGenerated, actor, DeviceName = tostring(TargetResources[0].displayName); newDevices | join kind=inner ( SigninLogs | where TimeGenerated > ago(7d) | where ResultType == 0 | project SignInTime = TimeGenerated, actor = UserPrincipalName, DeviceOS = tostring(DeviceDetail.operatingSystem), IPAddress ) on actor | where DeviceTime between (SignInTime .. (SignInTime + 2h)) | where DeviceOS in ("Linux", "", "Other") | project SignInTime, DeviceTime, actor, DeviceOS, DeviceName, IPAddress
False positives: genuine onboarding and BYOD enrolment register devices constantly, which is why registration alone is worthless as a signal. The platform-mismatch correlation (sign-in from an unmanaged or unknown OS, then a compliant device appears) is what separates the attack from Tuesday. Exclude your enrolment service accounts and known provisioning flows.
T1098.001Additional Cloud Credentials
attacker adds a secret or certificate to an app or service principal
Source: AuditLogs

An application or service principal can hold its own credentials. An attacker who adds a client secret or certificate to an existing app registration gets a persistent, password-independent way back in that authenticates as that app with all its granted permissions. This is one of the most durable M365 persistence techniques and it is a single audit event.

AuditLogs | where TimeGenerated > ago(30d) | where ActivityDisplayName has_any ("Add service principal credentials", "Update application", "Add key credential", "Add application credentials") | where TargetResources[0].type =~ "Application" or TargetResources[0].type =~ "ServicePrincipal" | extend AppName = tostring(TargetResources[0].displayName) | extend actor = tostring(InitiatedBy.user.displayName) | extend ChangedProps = TargetResources[0].modifiedProperties | where ChangedProps has_any ("keyCredentials", "passwordCredentials") | project TimeGenerated, AppName, ActivityDisplayName, actor | sort by TimeGenerated desc
False positives: developers and CI pipelines rotate app secrets legitimately. Prioritise credentials added to high-privilege apps (those with Graph application permissions like Directory.ReadWrite.All), credentials added by a user who does not normally manage that app, and certificates with unusually long validity. A credential add to a privileged app by an unexpected actor is the alert.
T1556.009Modify Authentication Process: Conditional Access
weaken or exclude a CA policy to remove MFA
Source: AuditLogs

Conditional access is the control plane for authentication in Entra. An attacker with sufficient privilege does not bypass it, they edit it: disable a policy, switch it to report-only, or add their compromised account to an exclusion group. The change is logged, and the highest-fidelity version of this detection flags a CA modification made by someone who does not normally touch CA.

let known = AuditLogs | where TimeGenerated between (ago(14d) .. ago(1d)) | where OperationName has "conditional access policy" | where Result =~ "success" | extend actor = tostring(InitiatedBy.user.userPrincipalName) | summarize by actor; AuditLogs | where TimeGenerated > ago(1d) | where OperationName in ("Add conditional access policy", "Update conditional access policy", "Delete conditional access policy") | where Result =~ "success" | extend actor = tostring(InitiatedBy.user.userPrincipalName) | extend CAPolicy = tostring(TargetResources[0].displayName) | where actor !in (known) | project TimeGenerated, actor, OperationName, CAPolicy, NewValue = tostring(TargetResources[0].modifiedProperties[0].newValue)
False positives: a new identity admin legitimately editing policy for the first time will fire this. That is acceptable: CA changes are rare and high-impact enough that a review-on-every-new-actor posture is defensible. Pair with alerting on the specific dangerous changes, a policy moved to report-only or an exclusion group gaining members.
T1556.007Modify Authentication Process: Hybrid Identity
also T1484: federate a domain to an attacker-controlled IdP
Source: AuditLogs (DirectoryManagement)

This is the cloud-side cousin of Golden SAML and one of the most powerful persistence techniques in a hybrid tenant. An attacker who can configure domain authentication federates a domain to an identity provider they control, or alters federation settings, and can then mint valid tokens for any user in that domain. Microsoft's own analytic rule watches the exact operations, and the full attack has a precursor chain worth hunting as a unit.

AuditLogs | where TimeGenerated > ago(30d) | where OperationName in ("Set domain authentication", "Set federation settings on domain", "Add unverified domain", "Verify domain") | extend actor = tostring(InitiatedBy.user.userPrincipalName) | extend Domain = tostring(TargetResources[0].displayName) | extend actorIP = tostring(InitiatedBy.user.ipAddress) | project TimeGenerated, OperationName, Domain, actor, actorIP, Result | sort by Domain asc, TimeGenerated asc

Sort by domain and time as above and the attack chain reads in order: the same actor adding an unverified domain, verifying it, then setting domain authentication to federated within a short window is the federation backdoor being built. The federation configuration detail itself is not in the audit event, so confirm with Graph: Get-MgDomainFederationConfiguration to inspect the issuer URI and signing certificate for an attacker-controlled IdP.

False positives: legitimate federation changes happen during identity-platform migrations and M&A integration, but they are rare and planned. Any "Set domain authentication" event should be reconcilable to a known change ticket. An unplanned one, especially federating to an unfamiliar issuer, is a tenant-level compromise.
T1098.003Additional Cloud Roles
grant a privileged directory role for persistence and escalation
Source: AuditLogs

The clearest privilege-escalation signal in Entra is a role assignment to a high-privilege directory role: Global Administrator, Privileged Role Administrator, Application Administrator. An attacker who reaches one of these can grant themselves or a backdoor account standing admin rights. This is logged as a role-membership add.

AuditLogs | where TimeGenerated > ago(7d) | where OperationName == "Add member to role" | extend actor = tostring(InitiatedBy.user.userPrincipalName) | extend role = tostring(TargetResources[0].modifiedProperties[1].newValue) | extend target = tostring(TargetResources[0].userPrincipalName) | where role has_any ("Global Administrator", "Privileged Role Administrator", "Application Administrator", "Privileged Authentication Administrator") | project TimeGenerated, actor, target, role | sort by TimeGenerated desc
False positives: legitimate admin onboarding and PIM activations produce these events. If you use Privileged Identity Management, most privileged-role additions should flow through it, so an assignment made outside PIM (a permanent grant rather than an eligible or activated one) is the higher-signal event. Alert on standing grants to the tier-zero roles above.
Next: Collection and Exfiltration. How data actually leaves an M365 tenant, mailbox rules and forwarding, SharePoint and OneDrive bulk access, Teams data, and transfer to attacker-controlled cloud storage. Then Discovery and Defense Evasion, then Impact and fraud.

Collection and Exfiltration

This is the objective for most M365 intrusions: read the email, take the files, move the data out. The collection techniques are logged in the Exchange and SharePoint audit streams, which surface in the CloudAppEvents table in Defender and OfficeActivity in Sentinel. One prerequisite that trips teams up: mailbox and SharePoint auditing must be enabled in Microsoft Purview for these events to exist at all. Confirm that before you trust an empty result to mean nothing happened.

T1114.003Email Collection: Email Forwarding Rule
the single most common post-compromise M365 action
Source: CloudAppEvents (or OfficeActivity)

An attacker in a mailbox creates an inbox rule that forwards or redirects mail to an external address, then keeps reading the victim's email long after the session ends and without logging back in. It is the stealthiest email-collection method because it persists silently. The detection watches the rule-creation operations for the forwarding parameters, and the key tuning move is excluding internal destinations so only external forwarding surfaces.

CloudAppEvents | where Timestamp > ago(1d) | where Application == "Microsoft Exchange Online" | where ActionType in ("New-InboxRule", "Set-InboxRule", "Set-Mailbox", "UpdateInboxRules") | extend RawData = tostring(RawEventData) | where RawData has_any ("ForwardTo", "ForwardAsAttachmentTo", "RedirectTo", "DeliverToMailboxAndForward") | where RawData !has "@yourdomain.com" // exclude internal forwarding; set to your tenant domain | project Timestamp, AccountDisplayName, ActionType, IPAddress, CountryCode, ISP, RuleConfig = RawEventData.Parameters | sort by Timestamp desc

The refinement that cuts false positives, straight from Microsoft's alert-grading playbook, is checking whether the ISP or country that created the rule is normal for that user. A forwarding rule created from an ISP the user has never authenticated from in 30 days is the high-confidence version.

False positives: users legitimately forward mail to personal accounts or set up delegate flows. The built-in alert fires on every inbox rule, which is why it is noisy. Filter to external destinations, and prioritise rules that also hide their tracks (moving mail to a rarely-used folder, marking as read, or deleting), which is the manipulation pattern rather than a convenience rule.
T1098.002Additional Email Delegate Permissions
grant mailbox access to an attacker-controlled account
Source: CloudAppEvents / OfficeActivity

Instead of forwarding, an attacker grants their account delegate or full-access permissions on the victim's mailbox, then reads it directly. This is quieter than a forwarding rule because no mail leaves the tenant, and it is logged as an Exchange permission change.

CloudAppEvents | where Timestamp > ago(7d) | where Application == "Microsoft Exchange Online" | where ActionType in ("Add-MailboxPermission", "Add-RecipientPermission") | extend RawData = tostring(RawEventData) | where RawData has_any ("FullAccess", "SendAs", "SendOnBehalf") | project Timestamp, actor = AccountDisplayName, ActionType, IPAddress, Grant = RawEventData.Parameters | sort by Timestamp desc
False positives: shared mailboxes, executive-assistant delegation, and migration tooling grant these permissions legitimately. The signal is a permission grant where the grantee is not a known delegate and the grant was made shortly after a risky sign-in for the mailbox owner. Baseline your legitimate shared-mailbox delegations and alert on the exceptions.
T1213.002Data from Information Repositories: SharePoint
also T1530 Data from Cloud Storage (OneDrive)
Source: CloudAppEvents

When an attacker reaches SharePoint or OneDrive, the objective is bulk collection: download many files quickly. Normal users download a handful; an attacker staging exfiltration downloads dozens or hundreds in a short window. The detection is a volume threshold on file-download events per user per time bin.

let threshold = 50; CloudAppEvents | where Timestamp > ago(1d) | where ActionType == "FileDownloaded" | where Application in ("Microsoft SharePoint Online", "Microsoft OneDrive for Business") | summarize FilesDownloaded = dcount(ObjectName), Files = make_set(ObjectName, 15) by Actor = AccountDisplayName, ClientIP = tostring(RawEventData.ClientIP), bin(Timestamp, 1h) | where FilesDownloaded > threshold | sort by FilesDownloaded desc
False positives: legitimate bulk work exists, a user syncing a document library, a departing employee archiving their files, an automated backup. Set the threshold to your environment (start high and lower it), and raise confidence by correlating the download burst with an unfamiliar IP or a sign-in flagged risky. Downloads of files with names like payroll, financials, or credentials sharpen the priority further.
T1564.008Email Hiding Rules
rules that delete or hide mail to conceal the intrusion
Source: CloudAppEvents

A subset of malicious inbox rules exists not to forward mail but to hide it: moving incoming messages to a rarely-checked folder, marking them read, or deleting them, so the victim never sees the security notifications, replies to the attacker's lateral phishing, or bounce messages. This is defense evasion inside the mailbox, and it is the tell that a forwarding or BEC operation is actively being concealed.

CloudAppEvents | where Timestamp > ago(7d) | where Application == "Microsoft Exchange Online" | where ActionType in ("New-InboxRule", "Set-InboxRule") | extend RawData = tostring(RawEventData) | where RawData has_any ("DeleteMessage", "MarkAsRead", "MoveToFolder") | where RawData has_any ("Deleted Items", "RSS", "Conversation History", "Archive") or RawData has "DeleteMessage" | project Timestamp, AccountDisplayName, IPAddress, RuleConfig = RawEventData.Parameters | sort by Timestamp desc
False positives: plenty of users have legitimate organising rules that move newsletters to folders. The malicious pattern is a rule that combines hiding with deletion, targets messages containing security or authentication keywords, or was created from an anomalous session. A hide-and-delete rule created minutes after a suspicious sign-in is high-confidence.
Next: Discovery and Defense Evasion. Cloud account and service enumeration, and the log-tampering that hides all of the above, disabling audit, purging mailbox audit, and cloud-log modification. Then Impact and fraud, internal spearphishing and BEC financial theft.

Discovery and Defense Evasion

Before an attacker moves, they look around: who are the admins, what applications exist, where is the sensitive data. And when they want to stay hidden, they go after the logs themselves. The evasion techniques here matter more than their frequency suggests, because a successful one blinds every other detection in this reference. Treat audit-tampering events as the highest-priority tripwires you have.

T1562.008Impair Defenses: Disable or Modify Cloud Logs
turn off the unified audit log to blind detection
Source: OfficeActivity / CloudAppEvents

The Microsoft 365 unified audit log is the source behind most of the detections in this reference. An attacker who reaches the Audit Logs role in Exchange Online can disable it with a single command, and from that moment mailbox access, file downloads, and rule creation stop being recorded. The disable action is itself audited, so this is your canary: if you see it and did not authorise it, assume active compromise and that subsequent activity is going dark.

OfficeActivity | where TimeGenerated > ago(30d) | where Operation == "Set-AdminAuditLogConfig" | extend params = tostring(Parameters) | where params has "UnifiedAuditLogIngestionEnabled" // the disable is the dangerous case: value set to False | where params has "False" | project TimeGenerated, UserId, ClientIP, Operation, params | sort by TimeGenerated desc
False positives: almost none, and that is the point. Legitimately disabling the unified audit log is rare and should always map to a known administrative action. Any occurrence deserves immediate investigation. Alert on it at high severity and, because the attacker may disable logging early, treat its appearance as reason to pull all available history before the gap begins.
T1070.008Indicator Removal: Clear Mailbox Data
delete sent phishing or evidence from the mailbox
Source: CloudAppEvents

After using a compromised mailbox to send internal phishing or move funds, an attacker deletes the evidence: hard-deleting sent items, purging the Deleted Items folder, or removing specific messages so the real user and investigators cannot see what was sent from their account. Exchange logs these as hard-delete operations.

CloudAppEvents | where Timestamp > ago(7d) | where Application == "Microsoft Exchange Online" | where ActionType in ("HardDelete", "SoftDelete", "MoveToDeletedItems") | extend items = tostring(RawEventData.AffectedItems) | summarize DeleteActions = count(), Sample = make_set(items, 10) by AccountDisplayName, ActionType, bin(Timestamp, 1h) | where DeleteActions > 20 | sort by DeleteActions desc
False positives: users delete mail constantly, so raw delete volume is meaningless on its own. The signal is a burst of hard-deletes shortly after the account sent mail it does not normally send (internal phishing), or deletes concentrated on Sent Items after a BEC attempt. Correlate the delete burst with a preceding send anomaly or risky sign-in rather than alerting on deletions alone.
T1087.004Account Discovery: Cloud Account
enumerate users and admins to plan the next move
Source: CloudAppEvents / AuditLogs

Having gained access, an attacker enumerates the directory: who holds privileged roles, which accounts to target next, where the value is. In M365 this shows up as bursts of directory-read activity or, from a compromised session, tooling that lists users and roles. The pattern is a single principal reading an unusually broad slice of the directory in a short window.

CloudAppEvents | where Timestamp > ago(1d) | where ActionType in ("SearchQueryInitiatedExchange", "SearchQueryInitiatedSharePoint") or ActionType has "Directory" | summarize Queries = count(), Actions = make_set(ActionType, 10) by AccountDisplayName, ClientIP = tostring(RawEventData.ClientIP), bin(Timestamp, 1h) | where Queries > 30 | sort by Queries desc
False positives: admins, compliance staff running eDiscovery, and legitimate reporting tools enumerate the directory routinely. Baseline who normally performs broad directory reads and alert on a new principal doing it, especially from an unfamiliar IP or immediately after a risky sign-in. Enumeration by a user who has never done it before is the lead worth chasing.
Next: Impact and fraud. The endgame for many M365 intrusions, internal spearphishing from the compromised account, impersonation, and business email compromise aimed at financial theft. Then the reference closes with cross-links across the four detailed cheatsheets that make up this reference.

Impact and Fraud

For a large share of M365 intrusions, this is the objective the whole chain was building toward: use the compromised mailbox to move money or spread further. Business email compromise is the costly one, and the hard part is that it is a behavioral attack wearing normal infrastructure. There is no malware and no exploit, just a trusted account making a plausible request. That is why the detection here is less about a single query and more about correlating the signals from every earlier section into one story.

T1534Internal Spearphishing
the compromised account phishes its own colleagues
Source: EmailEvents

Once inside a mailbox, an attacker uses it to phish other employees. These messages come from a real internal, trusted sender, so they sail past the filters tuned for external threats and land with the credibility of a known colleague. The detection looks for internal-to-internal mail carrying phishing indicators, or a sudden spike in a single internal sender messaging many internal recipients.

EmailEvents | where Timestamp > ago(1d) | where SenderFromDomain == RecipientEmailAddress // same-tenant sender and recipient or (EmailDirection == "Intra-org") | where ThreatTypes has_any ("Phish", "Malware") or PhishFilterVerdict != "" | summarize Recipients = dcount(RecipientEmailAddress), Subjects = make_set(Subject, 10) by SenderFromAddress, bin(Timestamp, 1h) | where Recipients > 5 | sort by Recipients desc
False positives: legitimate internal mass mail (a manager emailing their department, an all-hands notice) will show high recipient counts. Filter on the phishing verdict rather than volume alone, and prioritise a sender who does not normally message many people suddenly fanning out, especially one whose account also shows a recent risky sign-in or a new inbox rule. The compromise chain is the context that makes the send suspicious.
T1657Financial Theft (Business Email Compromise)
the payload is a wire transfer, invoice change, or payroll redirect
Source: correlation across CloudAppEvents, SigninLogs, EmailEvents

BEC has no clean signature because every individual action is something a normal user does: sign in, read mail, create a rule, send a message. It is the sequence that betrays it. The detection that works is a correlation rule chaining the earlier signals in this reference: a risky sign-in, followed by an inbox rule that hides or forwards mail, followed by outbound mail to finance or an external party about payment. No single event, the chain.

let risky = SigninLogs | where TimeGenerated > ago(2d) | where RiskLevelDuringSignIn in ("high", "medium") | distinct UserPrincipalName, RiskTime = TimeGenerated; let rules = CloudAppEvents | where Timestamp > ago(2d) | where ActionType in ("New-InboxRule", "Set-InboxRule") | extend RawData = tostring(RawEventData) | where RawData has_any ("ForwardTo", "RedirectTo", "DeleteMessage", "MoveToFolder") | project RuleTime = Timestamp, UserPrincipalName = AccountDisplayName; risky | join kind=inner rules on UserPrincipalName | where RuleTime between (RiskTime .. (RiskTime + 12h)) | project UserPrincipalName, RiskTime, RuleTime
False positives: this correlation is deliberately narrow, a risky sign-in and a mail-manipulation rule within the same short window is rare and high-signal. Widen or narrow the window to your tolerance. To go further, add a third leg: outbound mail from the same user to an external finance contact or containing payment keywords in the hours after the rule appears. The more legs of the chain you require, the higher the confidence and the fewer the alerts.
T1531Account Access Removal
lock the legitimate user out to buy time
Source: AuditLogs

To slow the response once the fraud is in motion, or during destructive attacks, an attacker may reset the victim's password, revoke their sessions, or disable the account, locking out the real user while the attacker keeps a live token. In Entra this is a password reset or account state change made by an unexpected actor.

AuditLogs | where TimeGenerated > ago(1d) | where OperationName in ("Reset user password", "Change user password", "Disable account", "Update user") | extend actor = tostring(InitiatedBy.user.userPrincipalName) | extend target = tostring(TargetResources[0].userPrincipalName) | where actor != target // someone acting on another user's account | project TimeGenerated, actor, target, OperationName | sort by TimeGenerated desc
False positives: help-desk password resets and admin account management produce these constantly. The signal is a reset or disable performed by an actor who is not a known help-desk or admin identity, or one performed on a privileged account, or a burst of them. Baseline your legitimate account-management identities and alert on actions taken by anyone outside that set.
The reference is a living document. These detections cover the M365 and Entra techniques that appear in real intrusions, mapped to the ATT&CK Identity Provider and Office Suite matrices. Each is a starting point to tune to your tenant, not a paste-and-forget rule. The four cheatsheets linked at the top go deeper on the attack surface, the hunt method, the full query set, and the engineering discipline behind turning these into production detections.

Correlation Detections

Single-technique rules catch single techniques. Real intrusions are chains, and the highest-fidelity detections in M365 fire on the sequence rather than any one link. These correlation detections join signals across the identity, email, and cloud-app planes. They produce far fewer alerts than their component rules, and the alerts they do produce are close to actionable on arrival. A note on joins: across Defender advanced hunting, the stable identity key is AccountObjectId, and email correlates on NetworkMessageId. Using a display name or a bare UPN as a join key is the usual reason a correlation query silently returns nothing.

CHAINPhishing click to token replay
T1566.002 to T1557 to T1078.004: the AiTM kill chain in one query
Source: UrlClickEvents + EntraIdSignInEvents

The AiTM chain is a phishing click, then a session from a new location using the stolen token. Neither event alarms on its own: users click links and travel constantly. Joined within a tight window, they are the attack. This correlation catches the compromise at the moment the token is first replayed, which is the earliest point you can act.

let window = 1h; let clicks = UrlClickEvents | where Timestamp > ago(2d) | where ThreatTypes has "Phish" or IsClickedThrough == true | project ClickTime = Timestamp, AccountUpn, Url; clicks | join kind=inner ( EntraIdSignInEvents | where Timestamp > ago(2d) | where RiskLevelDuringSignIn in ("high", "medium") | project SignInTime = Timestamp, AccountUpn, IPAddress, Country, RiskLevelDuringSignIn ) on AccountUpn | where SignInTime between (ClickTime .. (ClickTime + window)) | project AccountUpn, ClickTime, Url, SignInTime, IPAddress, Country, RiskLevelDuringSignIn | sort by SignInTime desc
Why this beats the parts: a risky sign-in alert alone drowns you in VPN and travel noise; a phishing-click alert alone tells you nothing happened yet. The join collapses both into the small set of users who clicked and then produced a risky session, which is a queue you can actually work. Tighten the window to reduce coincidence, widen it if your token lifetimes are long.
CHAINCompromise to BEC staging
T1078.004 to T1114.003 to T1564.008: sign-in, rule, conceal
Source: SigninLogs + CloudAppEvents

Business email compromise has a recognisable staging sequence: a suspicious sign-in, then an inbox rule that forwards or hides mail, usually within hours. This correlation is the single most useful BEC tripwire because it fires during staging, before the fraudulent mail goes out and before money moves.

let risky = SigninLogs | where TimeGenerated > ago(2d) | where RiskLevelDuringSignIn in ("high", "medium") or ResultType == 0 and IsInteractive == true | where isnotempty(UserPrincipalName) | project RiskTime = TimeGenerated, UserPrincipalName, IPAddress, Country = tostring(LocationDetails.countryOrRegion); risky | join kind=inner ( CloudAppEvents | where Timestamp > ago(2d) | where ActionType in ("New-InboxRule", "Set-InboxRule", "UpdateInboxRules") | extend RawData = tostring(RawEventData) | where RawData has_any ("ForwardTo", "RedirectTo", "DeleteMessage", "MoveToFolder", "MarkAsRead") | project RuleTime = Timestamp, UserPrincipalName = AccountDisplayName, RuleData = RawData ) on UserPrincipalName | where RuleTime between (RiskTime .. (RiskTime + 6h)) | project UserPrincipalName, RiskTime, IPAddress, Country, RuleTime, RuleData | sort by RuleTime desc
Why this beats the parts: inbox-rule alerts are notoriously noisy because legitimate rules are common. Gating rule creation on a preceding risky sign-in for the same user cuts the volume to the cases that matter and adds the context (the source IP and country of the compromise) that an analyst needs to triage in one look. Add a third leg, outbound mail to finance in the following hours, for the highest confidence.
CHAINConsent grant to mass mailbox access
T1528 to T1114: illicit app consent, then it reads the mail
Source: AuditLogs + CloudAppEvents

Illicit consent phishing tricks a user into approving a malicious OAuth app, which then reads mail through the Graph API. The consent event and the subsequent programmatic mail access are separated by minutes to hours. Correlating the consent grant with a burst of application-driven mail reads confirms the app is doing what it was granted to do, which turns a noisy consent alert into a confirmed incident.

let consents = AuditLogs | where TimeGenerated > ago(7d) | where OperationName in ("Consent to application", "Add delegated permission grant") | extend AppId = tostring(TargetResources[0].id) | extend actor = tostring(InitiatedBy.user.userPrincipalName) | project ConsentTime = TimeGenerated, AppId, actor; consents | join kind=inner ( CloudAppEvents | where Timestamp > ago(7d) | where ActionType in ("MailItemsAccessed", "Send", "MessagesListed") | extend AppId = tostring(RawEventData.AppId) | summarize Reads = count(), FirstRead = min(Timestamp) by AppId ) on AppId | where FirstRead > ConsentTime | project actor, AppId, ConsentTime, FirstRead, Reads | where Reads > 20 | sort by Reads desc
Why this beats the parts: most consent grants are benign, so a consent alert has a poor signal ratio. Requiring the app to then read mail at volume filters to apps that are actually exercising their access, and the read count separates a curious integration from one hoovering a mailbox. Baseline your approved app IDs and this becomes a near-clean detection.
CHAINImpossible travel to privileged action
T1078.004 to T1098.003 or T1556: the takeover that immediately escalates
Source: SigninLogs + AuditLogs

When an attacker takes over a privileged account, the giveaway is speed: a sign-in from an anomalous location followed almost immediately by a high-impact administrative action, a role grant, a CA policy change, a credential added to an app. Legitimate admins do administrative things from their normal locations and devices. The correlation flags admin actions taken shortly after a sign-in that Entra scored risky.

let riskyAdmin = SigninLogs | where TimeGenerated > ago(2d) | where RiskLevelDuringSignIn in ("high", "medium") | project RiskTime = TimeGenerated, UserPrincipalName, IPAddress, Country = tostring(LocationDetails.countryOrRegion); riskyAdmin | join kind=inner ( AuditLogs | where TimeGenerated > ago(2d) | where OperationName in ("Add member to role", "Add conditional access policy", "Update conditional access policy", "Add service principal credentials", "Set domain authentication") | extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName) | project ActionTime = TimeGenerated, UserPrincipalName, OperationName ) on UserPrincipalName | where ActionTime between (RiskTime .. (RiskTime + 2h)) | project UserPrincipalName, RiskTime, IPAddress, Country, ActionTime, OperationName | sort by ActionTime desc
Why this beats the parts: the tier-zero admin operations here are individually worth watching but generate routine-change noise. Gating them on a preceding risky sign-in isolates the privileged action taken from a compromised session, which is the one you must respond to inside the hour. This is the closest thing to a standing alarm on tenant takeover.
Build these into your discipline. The correlation detections above are the pattern this reference exists to teach: chain the signals, gate the noisy technique on the confirming one, and alert on the sequence. The four cheatsheets linked at the top take each plane deeper, and the courses show you how to turn a chain like these into a tuned, entity-mapped production detection with a response playbook behind it.

Additional Techniques

The techniques below round out the coverage of the two matrices. Some are variants of the detections above, some are lower-frequency but worth knowing where they surface.

T1137.005Office Application Startup: Outlook Rules
a client-side rule that runs an attacker action on mail arrival
Source: CloudAppEvents

Distinct from server-side inbox rules, Outlook client rules (and the related Outlook Forms and Home Page techniques) can execute an action when mail arrives, historically including launching a payload. They persist in the mailbox and survive password resets. The detection watches rule creation for the client-side rule types and the tell-tale start-application or run-executable actions.

CloudAppEvents | where Timestamp > ago(7d) | where Application == "Microsoft Exchange Online" | where ActionType in ("New-InboxRule", "Set-InboxRule", "UpdateInboxRules") | extend RawData = tostring(RawEventData) | where RawData has_any ("StartApplication", "RunExecutable", "ForwardTo") | project Timestamp, AccountDisplayName, IPAddress, ActionType, RawData | sort by Timestamp desc
False positives: the run-application rule actions are rare in modern tenants and largely deprecated by Microsoft, so a hit is high-signal. Client rules that only organise mail are benign; the priority is any rule action that launches or runs something.
T1213.005Data from Information Repositories: Teams
read or exfiltrate Teams chats and channel data
Source: CloudAppEvents (MessagesListed)

Teams holds as much sensitive conversation as email holds mail, and an attacker with a session or a stolen token reads it through the Graph API. Microsoft's expanded cloud logs record message-read access as the MessagesListed event, and the strongest signal is an anomalous application ID reading many messages quickly, the fingerprint of scripting rather than a human scrolling.

CloudAppEvents | where Timestamp > ago(1d) | where ActionType == "MessagesListed" | extend AppId = tostring(RawEventData.ClientAppId) | extend AppName = tostring(RawEventData.ClientAppName) | summarize MessagesRead = count(), Threads = dcount(tostring(RawEventData.ChatThreadId)) by AccountDisplayName, AppId, AppName, bin(Timestamp, 1h) | where MessagesRead > 50 | sort by MessagesRead desc
False positives: compliance and backup tooling reads Teams messages at volume through known app IDs. Baseline those AppIds and alert on an unfamiliar one, or on a user principal reading messages through Graph when they normally use the client. The anomalous AppId is the discriminator.
T1567.004Exfiltration Over Webhook
Power Automate or a connector quietly ships data out
Source: CloudAppEvents / AuditLogs

Rather than download files, an attacker builds a Power Automate flow or adds an outbound connector that copies mail or files to an external endpoint automatically. It is exfiltration with no interactive download to catch, and it persists as an automation. The detection watches for new flow creation and connector additions, especially those touching mail or files and pointing at an external destination.

CloudAppEvents | where Timestamp > ago(7d) | where ActionType has_any ("Created flow", "Edited flow", "Created connection", "PutConnection") | extend flow = tostring(RawEventData.ObjectName) | project Timestamp, AccountDisplayName, ActionType, flow, IPAddress | sort by Timestamp desc
False positives: Power Automate is used legitimately across most tenants, so flow creation is common. Prioritise flows created shortly after a risky sign-in, flows built by a user who does not normally automate, and connectors pointing at consumer or unknown external endpoints. The context of the account, not the flow alone, is the signal.
T1649Steal or Forge Authentication Certificates
theft of AD FS or token-signing material to forge identity
Source: SecurityEvent + AuditLogs

Beyond the Golden SAML case, an attacker who steals a token-signing or AD CS certificate can forge authentication material directly. On-premises, watch for access to the AD FS DKM key and certificate export events; in the cloud, the forged tokens surface as the same claims-versus-reality mismatch described in the Golden SAML section. This technique overlaps with hybrid identity abuse, and the two are best hunted together.

SecurityEvent | where TimeGenerated > ago(7d) | where EventID == 4662 // operation on AD object; watch DKM container access | where ObjectName has "CN=ADFS" or Properties has "DKM" | where AccountType == "User" | project TimeGenerated, Account, ObjectName, AccessMask | sort by TimeGenerated desc
False positives: the AD FS service account accesses the DKM key legitimately at service start. Exclude that account and correlate access that does not align with a service restart. Any other principal reading the DKM container is a strong indicator of certificate theft in progress.
Also tracked, covered in the sections above. Several matrix techniques are variants or preconditions of detections already here, and are best hunted through those: Spearphishing Link (T1566.002) surfaces through the AiTM and internal-spearphishing detections; Valid Accounts: Cloud Accounts (T1078.004) is the compromised-session precondition that the risky-sign-in correlation throughout this reference depends on; MFA modification (T1556.006) is covered under Credential Access; and Create Account (T1136.003) shows up in the same AuditLogs role and account operations as the persistence detections. Cloud Service Discovery and Dashboard access (T1526, T1538) are low-signal enumeration best caught by the broad directory-read anomaly in the Discovery section rather than a dedicated rule, since in isolation they generate more noise than signal.

Learn to build these detections, not just run them

This reference shows you the detections. The courses teach you to engineer them for your own tenant: the data model, the tuning, the coverage measurement, and the response.

Explore the courses