Playbook ID: PB-ATO | Default severity: SEV-3 (SEV-2 if the principal holds a privileged role, a service principal is involved, or regulated data is in reach; SEV-1 for a tenant-wide consent grant, multiple accounts, or a production cloud control plane) | Owner: Operations Lead (Identity)
RiskLevel eq 'high'.Consent to application record in Purview Audit, or a Google Workspace OAuth Token log event for an unrecognized app.UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS / .InsideAWS, UnauthorizedAccess:IAMUser/ResourceCredentialExfiltration.OutsideAWS, CredentialAccess:IAMUser/CompromisedCredentials, or the PrivilegeEscalation:IAMUser/AnomalousBehavior family (finding types).Not for: compromise of the identity provider itself, federation trust, token-signing material, or a Tier-0 administrator — Playbook 14.4. Mailbox rules used to redirect payment — 14.2. A vendor breach that leaked their copy of your tokens — 14.5, then return here for the revocation work. Workload identity inside a cluster — 14.10.
Somebody is logged in as your user, and they no longer need the password to stay that way. Thirty-five percent of cloud incidents involve valid account abuse and 82% of CrowdStrike's detections were malware-free (CrowdStrike 2026 GTR) — no binary to find, no hash to block, and your EDR has nothing to say. The evidence is authentication telemetry, and it expires fast.
Two mechanisms dominate. Adversary-in-the-middle kits — Tycoon 2FA, Evilginx2 — proxy the genuine login page and lift the session token after the victim completes real MFA (Group-IB). MFA was not bypassed; it was made irrelevant. The other is OAuth consent abuse: an app named to resemble a storage or verification service, approved on a real consent screen, granting standing access. The FBI's IC3 has an active PSA on that campaign, running since late 2025 (Help Net Security). A consented grant is a spare key you handed to a contractor — changing the locks does not get it back. Microsoft says it plainly: "normal remediation steps (for example, resetting passwords or requiring multifactor authentication (MFA)) aren't effective against this type of attack" (illicit consent grants).
The non-human half is worse, because nobody gets a push notification about a service principal. In the Salesloft Drift incident, attackers stole the OAuth refresh tokens customers had issued to a chat integration and exported records from 700+ organizations over ten days, with no customer-side vulnerability at all (AppOmni).
The mistake teams make is ordering. They reset the password first, out of 2015 muscle memory — which tips off the adversary, locks out the user, and leaves every refresh token, app session and OAuth grant alive. Tokens first. Then the credential. Every time.
| Role | Responsibility in PB-ATO |
|---|---|
| Incident Commander | Owns the observe-vs-contain call and the containment window; authorises tenant-wide actions. |
| Operations Lead (Identity) | Revocation, credential reset and grant removal as one burst; the non-human identity branch. |
| Operations Lead (Cloud) | AWS/GCP session revocation, permission denial, key deactivation, control-plane log export. |
| Communications Lead | Out-of-band user contact; help-desk brief; notification drafting. |
| Scribe | Timeline in UTC/ISO 8601; chain of custody; artefact register. |
| Legal Liaison | Legal hold, privilege, notification assessment, vendor notice. |
| Executive Sponsor | Approves outage-causing action on a production integration or service principal. |
Marking used below: `TIP-OFF = the adversary can see this action. EVIDENCE` = this degrades or ends a telemetry stream.
| # | Action | Who | Done when | Evidence to capture |
|---|---|---|---|---|
| 1 | Declare T+0. Set a hard scoping time box (60 min) after which containment fires regardless of completeness. | IC | Time box recorded | Declaration time (UTC/ISO 8601), trigger alert ID |
| 2 | Export logs before anything else. Entra audit and sign-in logs hold 7 days Free / 30 days P1-P2; risky sign-ins 30 days P1, 90 days P2; retention changes are not retroactive (Entra retention). CloudTrail console Event history is 90 days, management events only. | Ops Lead | Raw exports in the evidence store | File hashes, source, query window, exporter identity |
| 3 | Place legal hold — Purview eDiscovery hold on mailbox/OneDrive/Teams locations, S3 Object Lock legal hold on evidence objects. Hold first, scope second. | Legal Liaison | Hold confirmed in the case | Case ID, custodians, hold timestamp |
| 4 | Pull the risk and sign-in picture: Get-MgRiskyUser -Filter "RiskLevel eq 'high'", Get-MgRiskDetection, Get-MgRiskyUserHistory -RiskyUserId <id>. Separate attacker sessions from the user's by IP, ASN, user agent, session ID. | Ops Lead (Identity) | Attacker session set identified | Risk and sign-in exports, session/IP list |
| 5 | Inventory OAuth grants for the principal and tenant-wide; triage ConsentType = AllPrincipals and .All permissions first. | Ops Lead (Identity) | Permissions.csv produced and triaged | The CSV, ClientDisplayName, consenting user |
| 6 | Search Purview Audit for Consent to application; check IsAdminConsent: True. Records take 30 minutes to 24 hours to appear — a nil result in the first hour is not an answer. | Ops Lead (Identity) | Search complete, latency noted | Audit records, search parameters, run time |
| 7 | Check mailbox persistence: Get-InboxRule; mailbox-level forwarding set via Set-Mailbox (ForwardingAddress / ForwardingSmtpAddress, which do not appear in Get-InboxRule); added MFA methods; added devices. | Ops Lead (Identity) | All four checked | Rule and forwarding output, auth-method changes |
| 8 | Cloud branch: identify the principal. AKIA = long-term IAM user key, ASIA = STS short-term credential (compromised credentials). Pivot on userIdentity.principalId (role ID plus attacker-chosen session name), sessionContext.attributes.mfaAuthenticated, and readOnly to split recon from modification. Query all Regions. | Ops Lead (Cloud) | Principal and session set identified | CloudTrail export, principal ARN, session names |
| 9 | Scope what was read. M365: MailItemsAccessed — check IsThrottled, because 1,000+ records on a mailbox in 24 hours halts logging for 24 hours, and throttling is itself a compromise indicator (CISA Expanded Cloud Logs Playbook). AWS: CloudTrail Lake query over the session. | Ops Lead | Read-scope estimate recorded | MailItemsAccessed records, Lake query IDs and results |
# M365: tenant-wide OAuth grant inventory (Microsoft's documented method).
# One row per delegated/application grant. ConsentType = AllPrincipals means that
# client can reach every user's content in the tenant — triage those first.
.\Get-AzureADPSPermissions.ps1 | Export-csv -Path "Permissions.csv" -NoTypeInformation
# Who created or changed inbox rules, and what those rules do.
# Without -SessionCommand this cmdlet returns at most 100 records however high you set
# -ResultSize — and Phase 1 step 7 is only a persistence check if the set is complete.
# ReturnLargeSet comes back unsorted; re-run it with the SAME -SessionId until it returns
# zero rows, then sort what you have.
Get-InboxRule -Mailbox <mailbox> | FL Name,Description,DeleteMessage,MoveToFolder,Enabled
Search-UnifiedAuditLog -StartDate <start> -EndDate <end> -UserIds <user1,user2> `
-Operations New-InboxRule,Set-InboxRule,Remove-InboxRule `
-SessionCommand ReturnLargeSet -SessionId <id> -ResultSize 1000Containment is one burst, not a sequence of tickets. Splitting it across an hour hands the adversary a window to re-establish.
| # | Action | Who | Done when | Evidence to capture |
|---|---|---|---|---|
| 1 | Revoke sessions before touching the credential: Revoke-MgUserSignInSession -UserId $User.Id. This invalidates refresh tokens and browser session cookies by resetting signInSessionsValidFromDateTime. `TIP-OFF` | Ops Lead (Identity) | Cmdlet returns success | Transcript, UTC timestamp, operator identity |
| 2 | In the same burst, reset the credential. Hybrid identities: disable in AD and double-reset the on-prem password first — Microsoft's stated reason is pass-the-hash risk under replication delay (revoke user access). | Ops Lead (Identity) | Both resets complete | AD and Entra change records |
| 3 | Remove the malicious grants: Remove-MgOauth2PermissionGrant for delegated consent, Remove-MgServicePrincipalAppRoleAssignment for application permissions. Removing the app from one user's list does nothing to an AllPrincipals grant. | Ops Lead (Identity) | Grant absent on re-inventory | Before/after Permissions.csv, grant IDs |
| 4 | Capture rule definitions, then delete attacker inbox rules, mailbox forwarding and registered MFA methods. `EVIDENCE` | Ops Lead (Identity) | Removed and verified | Exports taken before deletion; deletion records |
| 5 | Google Workspace: sign out and revoke the grant. signOut resets sign-in cookies but does not revoke a third-party OAuth token, so the app keeps working (users.signOut, tokens.delete). | Ops Lead (Identity) | Both calls succeed | API call log, revoked clientId values |
| 6 | Disable registered devices: Get-MgUserRegisteredDevice -UserId $User.Id -All piped to Update-MgDevice -AccountEnabled:$false. Requires Cloud Device Administrator. `TIP-OFF` | Ops Lead (Identity) | Devices disabled | Device IDs, before/after state |
| 7 | Confirm-MgRiskyUserCompromised -UserIds "<id>" — raises the user to high risk, which is a CAE critical event and feeds the ID Protection model. Requires Security Administrator. | Ops Lead (Identity) | User at high risk | Cmdlet transcript |
| 8 | AWS role branch: revoke sessions (attaches the AWSRevokeOlderSessions inline policy; needs PutRolePolicy) and change permissions — AWS states revocation alone is insufficient. Identity Center permission-set roles cannot be edited in IAM; revoke there instead. Service-linked role sessions cannot be revoked at all. | Ops Lead (Cloud) | Both applied | Policy JSON with aws:TokenIssueTime, IAM change record |
| 9 | AWS key branch: run aws iam get-access-key-last-used to record final use, then aws iam update-access-key --status Inactive. Deactivate before deleting. | Ops Lead (Cloud) | Key inactive | Key ID, last-used record, status change |
| 10 | If the account holds admin in a member account, use a quarantine SCP from the management account, not an in-account deny — a member-account admin cannot detach an SCP: aws organizations attach-policy --policy-id <p-id> --target-id <account-id>. `TIP-OFF` | Ops Lead (Cloud) | Policy attached | Policy document, target ID, approver |
| 11 | GCP branch: disabling a key does not revoke short-lived credentials minted from it — disable or delete the service account itself (disable service account keys). | Ops Lead (Cloud) | Account disabled and verified silent | Key ID, SA email, disable record |
| 12 | Contact the user out-of-band — phone or in person, never through the compromised channel. | Communications Lead | User reached, statement taken | Contact log, statement in timeline |
# M365 emergency revocation. Steps 1 and 2 run together, not as separate tickets.
Connect-MgGraph -Scopes "User.ReadWrite.All","Directory.AccessAsUser.All"
$User = Get-MgUser -Search UserPrincipalName:'johndoe@contoso.com' -ConsistencyLevel eventual
Revoke-MgUserSignInSession -UserId $User.Id # refresh tokens + session cookies
Update-MgUser -UserId $User.Id -AccountEnabled:$false # only if you have decided to disable
Get-MgUserRegisteredDevice -UserId $User.Id -All | ForEach-Object {
Update-MgDevice -DeviceId $_.Id -AccountEnabled:$false
}# Google Workspace: BOTH calls are required. signOut alone leaves the OAuth app working.
# scope: https://www.googleapis.com/auth/admin.directory.user.security
POST https://admin.googleapis.com/admin/directory/v1/users/{userKey}/signOut
DELETE https://admin.googleapis.com/admin/directory/v1/users/{userKey}/tokens/{clientId}# GCP: disable a suspect service-account key. Does NOT kill tokens already minted from it.
gcloud iam service-accounts keys disable KEY_ID \
--iam-account=SA_NAME@PROJECT_ID.iam.gserviceaccount.com \
--project=PROJECT_ID| # | Action | Who | Done when | Evidence to capture |
|---|---|---|---|---|
| 1 | Re-run the tenant-wide grant inventory and diff against the Phase 1 baseline. Any grant issued after containment means an unrevoked path remains. | Ops Lead (Identity) | Clean diff | Both CSVs, diff output |
| 2 | Hunt identity persistence: new app registrations and client secrets, new service principals, credentials added to existing registrations, CreateUser / CreateAccessKey events, new federated identity credentials. | Ops Lead | All dispositioned | Object list with creation time and creating principal |
| 3 | Non-human branch: enumerate every service principal, workload identity and integration the compromised principal could create or modify, and rotate their secrets. | Ops Lead (Cloud) | Rotation complete | Rotation register, old/new credential IDs |
| 4 | Review the grants that are not malicious but are over-scoped. Every non-Microsoft app with ConsentType = AllPrincipals gets a named business owner or it goes. | Ops Lead (Identity) | Each app owned or removed | Decision record per app |
| 5 | Rebuild the role to least privilege rather than restoring the old policy — IAM Access Analyzer generates one from observed CloudTrail activity (Access Analyzer). | Ops Lead (Cloud) | New policy applied | Generated policy, diff vs. prior |
| 6 | Search ticket and support-case bodies for pasted credentials — in the Drift incident the highest-value loss was API keys and cloud credentials customers had pasted into support cases. Rotate anything found. | Ops Lead | Search complete | Search terms, hits, rotation records |
| 7 | Enable the audit actions that were missing. If rule history returned nothing because auditing was off: Set-Mailbox <mailbox> -AuditEnabled $true -AuditOwner @{Add="Create","Update"}, then re-verify the full action list. | Ops Lead (Identity) | Auditing on and verified | `Get-Mailbox … \ |
| # | Action | Who | Done when | Evidence to capture |
|---|---|---|---|---|
| 1 | Re-enable with a new credential delivered out-of-band and a supervised first sign-in. Budget for the documented lag: 15 minutes for SharePoint and Teams, 35–40 minutes for Exchange Online. | Ops Lead (Identity) | Supervised sign-in succeeds | Re-enable timestamp, verification method |
| 2 | Re-register MFA from scratch on a phishing-resistant method — FIDO/WebAuthn or PKI. CISA is explicit that number matching is a push-fatigue mitigation, not phishing-resistant MFA (AA23-320A). | Ops Lead (Identity) | New method registered, old ones removed | Auth-method inventory before and after |
| 3 | Apply sign-in frequency "Every time" for a defined watch period; confirm break-glass accounts remain excluded from every Conditional Access policy. | Ops Lead (Identity) | Policy in enforce mode | Policy JSON, exclusion list |
| 4 | Restore disabled integrations at reduced scope with a named owner. Never restore the original scope by default. | Ops Lead | Integration working, scope reduced | Old vs. new scope comparison |
| 5 | Verify containment by observation over a defined window: no new token issuance, no new sign-ins, no new API calls from the principal. | Ops Lead | Window elapsed clean | Query results per source, window start/end |
| 6 | Close the read-scope question. Where MailItemsAccessed shows Sync, the folder was accessed as a unit — treat it as read unless you can prove otherwise; pivot InternetMessageId into eDiscovery for the content list. Hand to Legal. | Ops Lead + Legal Liaison | Statement written and handed over | Message/file inventory, eDiscovery search IDs |
| # | Action | Who | Done when | Evidence to capture |
|---|---|---|---|---|
| 1 | Rebuild the timeline in UTC/ISO 8601 from exported logs, not from memory or console screenshots. | Scribe | Signed off by IC | Timeline with a source reference per entry |
| 2 | Blameless review on two numbers: first adversary sign-in to detection, and detection to full revocation (MTTC). | IC | Review held, actions assigned | Review record, owners, due dates |
| 3 | State honestly whether you had the telemetry. If retention or licensing blinded you, that is a budget finding with a price on it, not a detection-engineering finding. | Ops Lead + Exec Sponsor | Gap documented with cost | Gap statement, retention settings, quoted cost |
| 4 | Convert the detection that caught this — or the one that should have — into a version-controlled rule with a validation test. | Ops Lead | Rule merged and validated | PR link, validation run date |
| 5 | Run an unused-access review: unused roles, unused keys, unused passwords, dormant service principals. | Ops Lead (Cloud) | Review complete, revocations made | Analyzer findings, revocation list |
| 6 | Re-brief the service desk on out-of-band verification for recovery and MFA-reset requests, including the pattern where an attacker splits the password reset and the MFA change across two separate contacts (AA23-320A). | Comms Lead | Brief acknowledged | Attendance record, updated verification script |
The clock starts at confirmed unauthorized access to a data set, not at the first alert. Three things move it: the Phase 4 read-scope statement, whether that data is personal or regulated, and whether the account held data on behalf of a customer.
Notify in this order: the affected user, out-of-band, immediately; the service desk, so they do not process a follow-up recovery request from the attacker; Legal Liaison the moment access is confirmed, not when it is quantified; the SaaS or cloud vendor if their platform or integration is implicated; customers and regulators only on Legal's assessment. Chapter 15 holds the notification decision tree and every regulatory clock — do not reconstruct them here, and never commit to a deadline from memory.
Automate freely — anything that gathers, enriches or preserves: pull the sign-in and audit exports on trigger, snapshot the OAuth grant inventory, run the inbox-rule and forwarding checks, open the eDiscovery hold, enrich source IPs, assemble the draft timeline. Reversible, evidence-generating, verifiable after the fact.
Automate behind a human gate — the containment burst for a single non-privileged user. Revoke-plus-reset is a good one-click, human-triggered action: a false positive costs a help-desk call, not an outage. Rate-limit it and log the approver.
Never automate — tenant-wide grant removal, quarantine SCP attachment, OIDC provider deletion, disabling a service principal, or account disable at scale. Blast radius scales with the false-positive rate. The documented failure modes of agentic triage are overconfident closure on weak proof and hallucinated detail in the narrative, so every automated action here carries the evidence that justified it. "Closed by agent" with no artefact is how a real incident gets buried.
Takeaway: the order is the whole playbook — preserve the telemetry, scope in a time box, then revoke sessions, grants and credentials in one burst. A password reset on its own evicts nobody and announces you. And do not close on a quiet screen: record the query time, re-run after the audit lag, and only then call it contained.