A Microsoft 365 Incident Should Not Start With Twelve Disconnected Scripts
A compromised Microsoft 365 account rarely stays inside one administrative surface. Sign-ins live in Microsoft Entra ID. Mailbox rules, forwarding, and message traces live in Exchange Online. OAuth grants, Teams, SharePoint, Purview, Defender, Conditional Access, and device records each bring their own commands, permissions, retention limits, and failure modes.
That fragmentation creates a second incident-response problem: the responder has to remember which scripts ran, which services returned partial results, where each export landed, and whether a containment command changed the tenant. A folder full of CSV files does not answer those questions by itself.
I built M365 Incident Response Console to explore a different operating model. It is a one-file PowerShell application that keeps investigation, evidence collection, guarded containment, remediation, and reporting inside one case-oriented workflow.
This article is not an argument that one script can erase Microsoft 365’s service boundaries. It cannot. It is an argument that PowerShell can make those boundaries visible, keep dangerous actions behind a common control, and preserve enough context for the next investigator to understand what happened.
Start with reads, not containment
The console starts in Audit mode. Audit mode permits collection and local evidence export, but blocks every tenant-changing operation before its operation scriptblock can execute. It also replaces reviewed Microsoft Graph write scopes with read-only alternatives. If a requested write scope has no reviewed read-only mapping, the connection fails closed instead of requesting it.
This makes the default path useful before the responder decides that a tenant change is authorized. An analyst can inspect sign-ins, risky-user status, authentication methods, mailbox configuration, message flow, audit events, application grants, and collaboration data without silently turning an investigation into a remediation session.
Audit mode is not a synonym for harmless. Read access can expose sensitive identity, message, device, application, and policy data. The operator still needs an approved case, least-privilege roles, and a protected evidence location. The distinction is narrower: Audit mode cannot perform a tenant mutation.
Live mode is an explicit transition. Returning from Live mode to Audit mode disconnects cached service sessions so a Graph context with write-capable scopes is not silently reused.
Put every change through one gateway
The most important design choice was not adding another service collector. It was routing tenant-changing operations through one PowerShell function, Invoke-IRChange.
That gateway checks the current mode, requires a case for durable logging, honors PowerShell’s ShouldProcess behavior, displays the operation and impact, requires an operation-specific confirmation value, records approval before execution, and then records completion or failure.
Different actions require different confirmation text. A password reset can require RESET followed by the exact user principal name. Removing an OAuth grant can require the grant identifier. Sending warning mail can require the exact recipient count. The confirmation is tied to the selected target rather than being a generic YES prompt that an operator can type from muscle memory.
The operation still depends on Microsoft 365 authorization. A gateway does not manufacture permissions, make a destructive command reversible, or replace a change process. It creates one auditable place to enforce the rules that should apply to every change.
PowerShell authors can use the same pattern without adopting this application. Put mutations behind a small number of functions, give those functions SupportsShouldProcess, validate a target-specific confirmation, and log the intent before executing the scriptblock. Reviewers then have one control path to inspect instead of dozens of unrelated write commands.
Treat collection status as evidence
Cloud collection is not binary. A command can succeed while returning only the data the service still retains. A licensed workload can be unavailable in one tenant. A query can hit a record ceiling. A page token can repeat. A role can permit one property but not another.
The dangerous response is to turn all of those conditions into an empty array and continue. Empty results are easy to misread as “nothing happened.”
The console keeps completed, partial, failed, unavailable, and not-applicable states distinct. For example:
- Message Trace V2 has supported time-window and row-count limits, so longer requests are split into service-compliant windows and incomplete results remain visible.
- Unified Audit Log results depend on retention, licensing, workload, and assigned roles.
- Defender, risky-user, Intune, Safe Links, Safe Attachments, and other premium sources can be unavailable without making the entire case a software failure.
- Heuristic findings are placed in an investigator work queue; they are leads, not declarations of compromise.
The sanitized case report demonstrates this with synthetic data. Its Unified Audit Log collection is marked partial and its Defender source unavailable. Those gaps survive into the decision queue instead of disappearing behind a polished summary.
That is a general PowerShell lesson: record the coverage boundary beside the result. A downstream report should not need to infer whether zero rows means no activity, no license, insufficient permission, expired retention, throttling, or a failed request.
Make the case portable and reviewable
Each console case can contain raw and normalized exports, case metadata, a human-readable HTML report, a portable analyst import package, and threat-hunting query files. Actions are written to a sequence-numbered JSONL log with a SHA-256 hash chain. Evidence files can be listed in SHA-256 manifests and packaged in a protected archive.
Those controls make unnoticed modification easier to detect. They do not create a trusted timestamp, prove who possessed a file, establish publisher identity, or replace an organization’s chain-of-custody procedure. A hash manifest is useful precisely when its claim stays narrow.
The case report is also designed around decisions rather than collection volume. It puts incomplete sources, mailbox persistence, message-flow leads, audit indicators, and the investigator’s unresolved queue near the front. Raw exports remain available for deeper analysis, but the first view answers a more operational question: what still needs to be resolved before this case can close?
Evaluate it without a tenant
The current public release is v5.1.0 and requires PowerShell 7.6 or later. The release asset is not Authenticode-signed, so the quick-start path verifies its published SHA-256 digest. Digest verification confirms the exact GitHub release asset; it does not establish publisher identity through a signing certificate.
Use an empty working directory, then download and verify the release:
| |
Review the script according to your software-intake policy. Then run the two non-authenticated paths:
| |
-OfflineSelfTest exercises deterministic safety and evidence-integrity checks. -PreflightOnly reports the local PowerShell and Microsoft module prerequisites. Neither command authenticates to Microsoft 365 or changes a tenant.
The project’s complete automated suite is also offline. Passing it does not prove that every workflow will work in a particular tenant because live behavior still depends on official Microsoft modules, tenant configuration, licensing, retention, roles, and service behavior.
Make the first connected run a lab run
Use the safe lab guide before connecting. Prepare an approved test tenant or isolated lab segment with synthetic users and data. Use a dedicated operator protected by MFA, assign only the roles needed for the workflow under review, and protect the case-output directory.
Audit mode is the default, so the first connected command does not need a mode switch:
| |
Before granting consent, review the delegated scopes displayed by Microsoft. The console requests scopes as workflows need them rather than using one universal permission bundle. Remain in Audit mode until you understand the requested scopes, case location, collection limitations, and output.
If you later exercise Live mode, use a disposable target with a documented starting state. Obtain the same approval that a production response would require, test -WhatIf where available, make one change at a time, verify the resulting tenant state, and test the rollback path. Do not grant Global Administrator, disable MFA, or weaken execution policy to make a demonstration easier.
The reusable pattern
The application is one implementation, but the pattern is portable:
- Default to observation. Make the read-only path the path of least resistance.
- Centralize mutations. Enforce
ShouldProcess, authorization checks, exact confirmation, and logging at one gateway. - Preserve uncertainty. Carry partial, unavailable, and failed collection states into reports.
- Log intent before action. A failure after approval should not erase the fact that the operation was attempted.
- Separate tamper evidence from custody claims. State exactly what hashes and logs do and do not prove.
- Provide an offline evaluation path. Let operators inspect safety behavior before authentication.
- Keep the human decision queue visible. Automation should organize evidence, not decide that an account is compromised.
Microsoft 365 will continue to have separate services, modules, roles, and retention rules. PowerShell cannot make that complexity disappear. It can stop responders from hiding the complexity from themselves.
The project, release, synthetic report, and lab guide are available in the M365 Incident Response Console repository. I welcome feedback from responders about the points where they still have to leave the workflow and assemble evidence by hand.
About the Author

Jeff Friedler
Security engineering, incident response, and practical automation
Jeff Friedler builds security and infrastructure automation at Fusion Technology Strategies. His work focuses on making incident response, cloud security, and systems administration safer to operate and easier to audit.
He publishes open-source PowerShell and security tools with explicit safety boundaries, reproducible validation, and practical documentation for operators.
Related Articles
Find What Wakes a Windows PC With Get-WinEvent
A PC that turns itself on at night leaves a record in the System log every time it does. Windows writes one event when the PC …
Read moreFind Free Azure CIDR Blocks with PowerShell
Suppose the network team assigns 10.172.0.0/16 to an Azure landing zone. You need to divide it into correctly aligned virtual …
Read moreHow to Toggle Logon Restrictions for AD Accounts
Written by Tino JR This script will allow an administrator to enable or disable logon restrictions for an Active Directory …
Read more