TL;DR. If you’re reading this during an active incident, jump to Hour 0.
- Hour 0: confirm it’s actually a compromise via unified audit log signs, suspicious sign-ins, mailbox rule changes, and unfamiliar OAuth consents. Lock down break-glass access first.
- Hours 0–1: reset passwords for affected users, revoke every refresh token, document and disable forwarding rules, block sign-in via Conditional Access for the dirty accounts.
- Hours 1–4: hunt for persistence. Review consented OAuth apps, mailbox permission grants, role and PIM activations, Conditional Access policy edits, and newly-created admin accounts and service principals.
- Hours 4–24: scope the blast radius. Pull
MailItemsAccessed,FileAccessed, andSharingSetevents for every dirty account. Document IPs, user agents, timestamps. - Then hand off to the 30-day arc covered in the 25-step data breach response checklist.
How this checklist is organized
This is the active-incident playbook. If you have not yet confirmed the compromise and you’re still in “something feels wrong” territory, start with signs your M365 tenant is hacked and come back here once you’ve seen one of the confirming indicators below.
The structure is tactical and time-anchored. Hour 0 is confirmation. Hours 0–1 are containment. Hours 1–4 are persistence hunting. Hours 4–24 are scoping. Each phase has exact PowerShell, exact admin-portal paths, and a short list of things you absolutely do not do.
The reader of this page is panicking. The author is not. Work the steps in order, document as you go, and resist the urge to skip ahead. Most botched M365 incidents we’ve been called into were botched in the first three hours, not the first three weeks.
Hour 0: Confirm and triage
Before you reset a single password, confirm you actually have a compromise. A user clicking a phishing link is not a tenant compromise. A user clicking a phishing link, entering credentials, and the attacker successfully signing in and creating an inbox rule is. The distinction matters, because the response work is wildly different and the wrong response burns hours and destroys evidence.
Five indicators that confirm an actual compromise, in rough order of how often we see each:
- Successful sign-in from an unfamiliar IP or country followed by activity. Not a failed sign-in attempt. A successful one, with mailbox or SharePoint events afterward.
- An inbox rule the user did not create. Especially rules with whitespace-only names, rules that move messages to RSS Feeds or Conversation History, rules that delete messages matching keywords like “invoice,” “wire,” or “ACH.”
- An OAuth app consented in the last 30 days with mailbox or files scopes that nobody on the team remembers approving. Especially apps with names that mimic Microsoft properties.
- A new global admin or privileged role assignment that doesn’t match a change ticket.
- Mailbox forwarding to an external address set at the mailbox level, not via a user-visible rule. This shows up in
Get-EXOMailboxoutput and almost never in Outlook’s settings UI.
If you can confirm one of those, you have a compromise. Move to containment. If you have only soft signals (a phish click, an MFA push the user denied, a brief lockout), keep watching but don’t kick off the full response yet. False-positive responses burn out the team and devalue the alarm next time.
One more confirmation step before you touch anything: protect your break-glass accounts. Sign out of the admin portal, then sign in to a separate emergency-access account from a clean device. Verify the break-glass account is still excluded from the Conditional Access policy you’re about to use to lock the tenant down. If you don’t have break-glass accounts configured yet, stop, create two now (one stored in a fireproof safe, one in a separate physical location), and add them to the exclusion list before going further. You do not want to lock yourself out at hour two of an incident.
For the email-side variant of this confirmation work (when the entry point looks like a single mailbox rather than a tenant-wide event), cross-reference is your email compromised: the M365 detection guide.
Hours 0–1: Contain the affected accounts
Order matters here. Don’t reset a password before you’ve revoked sessions. Don’t delete a forwarding rule before you’ve documented it. Don’t notify users before you have containment in place.
Revoke every active session for the dirty account
A password reset alone leaves the attacker signed in, because their refresh token is still valid for up to 90 days. You have to invalidate the token first, and then reset the password.
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.AccessAsUser.All"
# Revoke ALL refresh tokens for the affected user
Revoke-MgUserSignInSession -UserId "[email protected]"
# Then force a password reset
$tempPassword = "BreachReset!$(Get-Random -Minimum 100000 -Maximum 999999)"
Update-MgUser -UserId "[email protected]" `
-PasswordProfile @{ ForceChangePasswordNextSignIn = $true; Password = $tempPassword }
Write-Host "Temp password for [email protected]: $tempPassword"
Run this for every account that shows up in your hour-zero confirmation as compromised. If you’re not sure whether an account is dirty, treat it as dirty. The cost of an extra password reset is fifteen minutes of help-desk time. The cost of leaving an attacker signed in is the entire next phase of the incident.
Document, then disable, every suspicious forwarding rule
The forwarding rule itself is your most important indicator of compromise. Don’t delete it. Disable it, export it, then leave the disabled artifact in place for the carrier’s forensic firm.
Connect-ExchangeOnline
# Find all inbox rules with forward, redirect, or delete actions across the tenant
$suspiciousRules = Get-EXOMailbox -ResultSize Unlimited |
ForEach-Object {
Get-InboxRule -Mailbox $_.UserPrincipalName -ErrorAction SilentlyContinue |
Where-Object {
$_.ForwardTo -or $_.ForwardAsAttachmentTo -or
$_.RedirectTo -or $_.DeleteMessage -eq $true
} |
Select-Object @{N='Mailbox';E={$_.MailboxOwnerId}}, Name, Identity, Enabled,
ForwardTo, ForwardAsAttachmentTo, RedirectTo, DeleteMessage,
Description, WhenChanged
}
# Export the evidence FIRST
$suspiciousRules | Export-Csv "incident-inbox-rules-$(Get-Date -Format yyyyMMdd-HHmm).csv" -NoTypeInformation
# Then disable (do NOT remove) each one
$suspiciousRules | ForEach-Object {
Disable-InboxRule -Identity $_.Identity -Confirm:$false
}
# Mailbox-level forwarding (separate from rules)
Get-EXOMailbox -ResultSize Unlimited |
Where-Object { $_.ForwardingSmtpAddress -or $_.ForwardingAddress } |
Select-Object UserPrincipalName, ForwardingSmtpAddress,
ForwardingAddress, DeliverToMailboxAndForward |
Export-Csv "incident-mailbox-forwarding-$(Get-Date -Format yyyyMMdd-HHmm).csv" -NoTypeInformation
Pay attention to rule names. Attackers favor names that hide in the Outlook UI: a single space, a period, the letter “a,” or invisible Unicode characters. The rule still runs, but the user can’t see it in the standard rules manager. Sort the CSV by WhenChanged descending, look at anything from the last 30 days, and flag anything that doesn’t match a known business workflow.
Block sign-in via Conditional Access for the dirty accounts
Resetting passwords is half the containment story. The other half is making sure that even if the attacker has another credential, they can’t use it. Build a Conditional Access policy named CA-IR-Block-Compromised-Users, scope it to a security group called IR-Compromised-Users, and set it to block all access. Add the dirty UPNs to that group.
That gives you a containment toggle. As you confirm or clear each account, you move it in or out of the group without rewriting policy. The block is at the Entra layer, so it survives password resets, MFA re-registration, and token re-issuance.
Verify your break-glass accounts are still excluded from this policy and from the policy that blocks legacy auth. The post-eradication hardening guide on how to block legacy authentication in Microsoft 365 covers the exclusion pattern in detail; that’s the policy you want functional both during and after the incident.
Hours 1–4: Hunt for persistence
Containment without persistence hunting is theater. You lock the front door, the attacker comes back through the side window you didn’t check, and three weeks later you’re running the whole response again with the carrier asking why nothing was eradicated the first time.
Six places to look, in order of how commonly we find something there.
Recently consented OAuth applications
OAuth consent is the modern persistence king. The attacker phishes a user into approving a malicious app, the app gets a refresh token with Mail.ReadWrite or Files.Read.All, and even after you reset the user’s password the app keeps reading mail. Password resets do not invalidate OAuth grants. You have to revoke them explicitly.
Connect-MgGraph -Scopes "Application.Read.All", "Directory.Read.All",
"DelegatedPermissionGrant.ReadWrite.All", "AppRoleAssignment.ReadWrite.All"
$cutoff = (Get-Date).AddDays(-90)
# Service principals (apps) created in last 90 days
$recentApps = Get-MgServicePrincipal -All |
Where-Object { $_.AdditionalProperties.createdDateTime -as [datetime] -gt $cutoff }
# Delegated grants on those apps (user-consented permissions)
foreach ($sp in $recentApps) {
$grants = Get-MgOauth2PermissionGrant -Filter "clientId eq '$($sp.Id)'" -ErrorAction SilentlyContinue
foreach ($g in $grants) {
[PSCustomObject]@{
AppName = $sp.DisplayName
AppId = $sp.AppId
Created = $sp.AdditionalProperties.createdDateTime
ConsentType = $g.ConsentType
Scopes = $g.Scope
PrincipalId = $g.PrincipalId
}
}
} | Where-Object { $_.Scopes -match "Mail|Files|Sites|User\.Read\.All|Directory" } |
Format-Table -AutoSize
Anything in that output with mail, files, sites, or directory scopes that you don’t recognize is a candidate for revocation. Revoke the grant with Remove-MgOauth2PermissionGrant -OAuth2PermissionGrantId <id> and disable the service principal with Update-MgServicePrincipal -ServicePrincipalId <id> -AccountEnabled:$false before deleting it. The disabled-then-deleted pattern leaves an audit trail.
New mailbox permission grants
Attackers grant themselves FullAccess or SendAs on executive mailboxes. The CFO’s mailbox shows nothing wrong from the CFO’s side. The attacker reads everything from a different account.
Get-EXOMailbox -ResultSize Unlimited |
Get-MailboxPermission |
Where-Object {
$_.User -notlike "NT AUTHORITY\*" -and
$_.User -notlike "S-1-5-*" -and
$_.IsInherited -eq $false -and
$_.AccessRights -contains "FullAccess"
} |
Select-Object Identity, User, AccessRights |
Sort-Object Identity
Role assignments and PIM activations in the last 30 days
Open Microsoft Entra admin center → Roles & admins → Audit logs. Filter on activity type Add member to role and Add eligible member to role, last 30 days. Anything you can’t map to a documented change is a finding. Pay particular attention to Global Administrator, Privileged Authentication Administrator, Application Administrator, and Cloud Application Administrator additions, in that order.
Conditional Access policy modifications
A favorite advanced-attacker move: add a Conditional Access policy that excludes the attacker’s newly-created admin account from MFA, then assign that account to a named location nobody’s watching. Look at Entra → Protection → Conditional Access → Audit logs, filter on policy create and modify events.
Newly-created admin accounts
List every account created in the last 60 days, then check which ones have administrative roles assigned.
$cutoff = (Get-Date).AddDays(-60)
Get-MgUser -All -Property Id, UserPrincipalName, CreatedDateTime, AccountEnabled |
Where-Object { $_.CreatedDateTime -gt $cutoff } |
Select-Object UserPrincipalName, CreatedDateTime, AccountEnabled |
Sort-Object CreatedDateTime -Descending
App registrations and service principals from the last 30 days
Distinct from consented OAuth apps. These are tenant-registered applications, often with their own credentials (client secrets or certificates). An attacker who creates a service principal with admin consent has effectively built themselves a backdoor that survives every user-level remediation you’re about to do.
Run Get-MgApplication -All filtered by createdDateTime, then for each unfamiliar app inspect Get-MgApplicationFederatedIdentityCredential and Get-MgApplicationPasswordCredential for credentials you didn’t add. Disable, document, then delete.
The four persistence techniques most responders miss
When we’re called back into a re-compromised tenant, the persistence the previous response missed is almost always one of these four. Read each one twice.
- A Conditional Access policy that excludes the attacker’s admin account. Often named to look benign, like “Service Account Exclusion” or “Legacy App Bypass.” The new admin account is the only member of the excluded group. Containment policies don’t apply to that account because the policy itself excludes it.
- An OAuth app with
Mail.ReadWritescope and admin consent. The attacker tricks an admin into granting tenant-wide consent to an app that mimics a Microsoft property (e.g., “Microsoft 365 Compliance Sync”). The app keeps reading mail across every mailbox in the tenant until you explicitly revoke it. Password resets don’t touch it. - A hidden inbox rule named with whitespace or near-invisible characters. The Outlook UI hides it.
Get-InboxRuleshows it. The rule typically forwards messages matching keywords like “wire,” “invoice,” or vendor names to an external address, and deletes the original from the user’s view. Run the PowerShell, not the UI check. - Mailbox-level forwarding chained through an internal account. User A’s mailbox forwards to user B internally. User B has a transport rule or mailbox forwarding to an external address. Most one-mailbox audits miss this because the forward looks legitimate from the source side. Pull
ForwardingSmtpAddresstenant-wide and trace every chain.
Hours 4–24: Scope the blast radius
Containment stops the bleeding. Persistence hunting closes the doors. Scoping tells you what was actually taken, which determines your notification obligations, your insurance claim narrative, and how much of the next 60 days you spend on the legal side versus the technical side.
For each compromised account, pull unified audit log entries covering the suspected dwell time plus a 14-day buffer on either side:
$start = (Get-Date).AddDays(-90)
$end = Get-Date
$user = "[email protected]"
Search-UnifiedAuditLog -StartDate $start -EndDate $end `
-UserIds $user `
-Operations "MailItemsAccessed","Send","SendAs","SendOnBehalf",
"FileAccessed","FileDownloaded","FileSyncDownloadedFull",
"SharingSet","AddedToGroup","Set-Mailbox","New-InboxRule",
"Set-InboxRule","UpdateInboxRules","Add-MailboxPermission",
"Update-MailboxPermission" `
-ResultSize 5000 |
Export-Csv "incident-audit-$user-$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation
That CSV becomes the evidentiary backbone of the incident. From it you derive: which mailboxes were read, which files were downloaded, which SharePoint or OneDrive items had sharing changed, which sends went out, which rules were created. Document IPs, user agents, timestamps, and tie each event to either the legitimate user or the attacker session based on the IP and user agent fingerprint.
One reminder that catches a lot of teams: MailItemsAccessed only logs for accounts with E5 or the Microsoft Defender for Office 365 P2 license at the time of access. If your tenant doesn’t have that, the audit log will undercount mail access. The carrier’s forensic firm will know this; flag it in your handoff so they can adjust their assumptions.
Pull the plug, or surgical containment?
Most M365 incidents call for surgical containment: identify the dirty accounts, contain them, hunt persistence, scope. The tenant stays up, business keeps moving, the rest of the org is unaffected.
A small subset call for the nuclear option: temporarily disable sign-in for every non-break-glass account in the tenant. You do this when:
- The attacker has Global Administrator and you can’t determine for certain which other admin accounts are clean.
- You see active data exfiltration in progress and surgical containment is moving slower than the exfil.
- You discover a service principal with admin consent and tenant-wide permissions, and you need 30 minutes to revoke without the attacker reacting.
- You have evidence of ransomware or wiper activity targeting SharePoint or OneDrive, and stopping all sign-ins is faster than identifying the dirty accounts individually.
The mechanism is a tenant-wide Conditional Access policy that blocks all users except your break-glass exclusion group. Set it to Report-only first to validate the exclusions actually exclude, flip it to On, then start your surgical work in a fully quiet tenant.
Pulling the plug costs you a half-day of business operations. Not pulling the plug when you needed to costs you the full breach. The decision criterion is “am I confident I can contain faster than the attacker can persist?” If the answer is no, pull the plug.
M365Shield handles your Microsoft 365 security. For full compliance readiness — incident response plans, governance frameworks, and executive security leadership — Iron Path Advisory provides fractional CIO and CISO services.
What you do NOT do
Active-incident anti-patterns we see repeatedly. Each one looks like a sensible move under stress. Each one makes the response measurably worse.
- Do not delete forwarding rules before documenting them. The rule is your evidence of intent and your IOC for hunting elsewhere. Disable it, export it, leave the disabled artifact in place.
- Do not reset passwords without revoking sessions first. A reset without revocation leaves the attacker signed in via refresh token while the user gets locked out trying to use the new password. Token revocation, then password reset, in that order. Always.
- Do not notify users until you have containment in place. A premature notification tips off the attacker (especially if the attacker is reading the user’s mailbox via OAuth) and gives them a chance to burn additional persistence before you finish the hunt. Contain first, communicate second.
- Do not restore from backup until you’ve eradicated persistence. A backup taken during the dwell window contains the attacker’s artifacts. Restoring it reintroduces the inbox rule, the mailbox forwarding, the OAuth grant. Eradicate first, then validate the backup is from before initial access, then restore.
- Do not wipe the affected user’s laptop yet. If the entry point was endpoint malware, the laptop has the forensic artifacts you need to determine root cause. Image it, then leave it powered down until the forensic firm has it.
- Do not turn off auditing because the noise is overwhelming. Yes, your audit log volume is going to spike during the response. Yes, that costs money. Yes, your team will want to filter out the noise. Don’t. The audit log is the evidence backbone of the incident report, the carrier claim, and the regulator notification. Filter your views, don’t cut the data.
A clean response that takes 18 hours beats a sloppy response that takes 6. The carrier and the regulator both reward methodical work over fast work. So does the attacker, who tends to come back when the response was sloppy.
Hand off to the 30-day arc
The first 24 hours are containment, persistence eradication, and scoping. The next 30 to 60 days are everything else: forensic investigation, attacker timeline reconstruction, legal notification analysis, carrier coordination, mailbox restoration, device re-enrollment, post-incident hardening, board reporting, and the tabletop you should have run six months ago.
That work is structured in the 25-step M365 data breach response checklist, which picks up where this page leaves off. Read it as soon as your hour-24 work is stable, because several of the legal-notification timers start running from the moment you confirmed the compromise, not from the moment the response work is finished.
For pattern matching against incidents that look like yours, the M365 breach recovery case study walks through a real 30-seat business through a 60-day response. For the questions that come up most often when leadership asks for context after the immediate work is done, the M365 breach FAQ is the short reference.
One last item before you leave this page. Add a calendar event for 14 days from today titled “persistence re-check.” Two weeks after the response, re-run the OAuth, role-assignment, and Conditional Access queries from the persistence-hunt section. If anything new appears that you can’t map to a documented change, you missed something. Better to find out at day 14 than at day 60 when the next forwarding rule gets discovered.
Common questions
How fast does the attacker actually move once they have access?
For commodity BEC attackers, expect a forwarding rule and a SharePoint download within the first hour, an OAuth consent attempt within 24 hours, and an attempt to escalate to admin within a week. For targeted attackers, the timeline compresses. We’ve seen full mailbox exfiltration plus an admin-account creation within 90 minutes of initial access. Plan your response speed against the targeted timeline, not the commodity one.
Should I keep the affected mailbox online during the response?
Yes, in almost every case. Disabling the mailbox or the user account stops audit logging on that mailbox going forward, which makes scoping harder. Block sign-in via Conditional Access (which prevents the attacker from using the account while preserving the mailbox), revoke tokens, reset the password. The mailbox stays up, the audit log keeps writing, the data stays accessible for forensic review.
What if I find an OAuth app with admin consent and I’m not sure if it’s legitimate?
Disable the service principal first (which prevents new sign-ins through it), then investigate. Update-MgServicePrincipal -ServicePrincipalId <id> -AccountEnabled:$false. If it turns out to be legitimate, you re-enable it and apologize to whoever was using it. If it turns out to be malicious, you’ve already cut it off. The cost of disabling a legitimate app for an hour is much smaller than the cost of leaving a malicious app live for an hour.
Do I need to involve law enforcement at hour zero?
Generally no. Involve your insurance carrier first; they have a panel of incident-response firms, breach counsel, and forensic providers, and most policies require carrier notification before you engage outside help if you want the costs covered. The carrier’s breach coach decides whether and when to loop in FBI or local law enforcement, usually around days 3 to 5 after the immediate technical work is stable. Calling the FBI at hour zero before you’ve called the carrier is a common claim-handling friction point.
My tenant doesn’t have E5 or Defender P2. Am I going to be able to scope this?
Partly. Without MailItemsAccessed logging at the read level, you can’t prove which specific messages the attacker read; you can only prove which mailboxes had attacker sign-ins and what other actions (sends, rules, deletes) happened during those sessions. For notification purposes, regulators in most US states treat “attacker had access to the mailbox during this window” as equivalent to “assume attacker accessed everything in the mailbox during this window.” Your forensic firm and breach counsel will translate that into the notification scope.
Once the immediate fire is out
M365Shield deploys the security baseline that prevents the next compromise. Tenant assessment, hardening, and CIS-aligned controls in 72 hours.
Check My Tenant RiskNext: 18 warning signs your M365 tenant is hacked if you’re still confirming, or the 25-step data breach response checklist for the 30-day arc.