Designing your Exchange Online Protection and Defender for Office 365 policies is half the job. The other half is operating what they produce every day: messages land in quarantine, users report false positives, an attacker slips one through, and someone has to decide — in minutes, defensibly — whether to release, submit, block, or hunt. This is the SecOps side: a repeatable loop your analysts run under pressure without breaking the protection you spent weeks tuning.
One principle governs every decision below: a release fixes one mailbox, a submission fixes the verdict, and a Tenant Allow/Block List (TABL) entry is a temporary override you owe an expiry date. Reach for them in that order and you will rarely create the standing allow entries that quietly become the next breach path.
This guide assumes the policy layer is already engineered — verdict-to-quarantine-policy mapping, anti-spam thresholds, Safe Links/Safe Attachments. Build that first; the operating loop depends on those mappings. Everything drives from the Exchange Online PowerShell V3 module plus the Defender portal at Email & collaboration.
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName analyst@contoso.com
1. Quarantine architecture: verdicts, retention, and who can release
Quarantine is not one bucket. Each message carries a verdict (the reason it was held) and is governed by the quarantine policy bound to that verdict in the policy that scoped the recipient — and that pairing decides who may release it.
Quarantine reason (QuarantineTypes) |
Typical source policy setting | Default release authority |
|---|---|---|
HighConfPhish |
Anti-spam HighConfidencePhishAction |
Admin only — users can request release at most |
Phish |
Anti-spam PhishSpamAction / anti-phishing |
Per quarantine policy |
Malware |
Anti-malware, Safe Attachments | Admin only |
Spam / HighConfSpam |
Anti-spam spam actions | Per quarantine policy |
Bulk |
Anti-spam BulkSpamAction |
Usually self-release |
SPOMalware |
Safe Attachments for SharePoint/OneDrive/Teams | Admin only |
TransportRule (mail flow rule) |
Exchange transport rule action | Admin only by default |
FileTypeBlock / ContentMalicious |
Common attachment filter | Admin only |
Two architectural facts drive triage:
- High-confidence phishing and malware are non-releasable by end users, period. Even with a permissive quarantine policy, Microsoft hard-blocks user self-release for
HighConfPhishand malware; the most a user gets is request release, which routes to you. Do not engineer around it. - Default retention is 30 days, set on the anti-spam policy, not on quarantine.
QuarantineRetentionPeriod(1–30 days) on theHostedContentFilterPolicyis how long a held message survives before automatic, unrecoverable deletion. A message reported on day 31 is already gone. Know this number cold — it bounds your investigation window.
The end-user experience (view, preview, release, request-release, block-sender, delete) is the quarantine-policy permission bitmask. You operate within it here; changing a verdict’s release authority is a policy-design change, not a triage action.
# What is in quarantine right now, with the verdict and the policy governing it
Get-QuarantineMessage -StartReceivedDate (Get-Date).AddDays(-3) -EndReceivedDate (Get-Date) |
Sort-Object ReceivedTime -Descending |
Format-Table ReceivedTime, SenderAddress, RecipientAddress, Subject, `
Type, QuarantineTypes, PolicyName, Released, ReleaseStatus -AutoSize
2. Admin triage: filter, preview headers, then decide
The job in triage is to reach a verdict on the verdict: filter to the population you care about, read the headers, then act. “Release because a user asked” is where triage starts, not ends.
Filter from PowerShell — the portal (Defender portal > Review > Quarantine) is fine for one-offs, but scripted filtering is reviewable and reproducible:
# A specific reported message: narrow by sender + recipient + window
$q = Get-QuarantineMessage -SenderAddress "billing@vendor-mail.net" `
-RecipientAddress "ap@contoso.com" `
-StartReceivedDate (Get-Date).AddDays(-2) -EndReceivedDate (Get-Date)
$q | Format-List Identity, Subject, QuarantineTypes, PolicyName, Released, RecipientCount
The message identity is the handle for every subsequent action. Pull the full headers before judging — they are authoritative about what Defender decided, and where impersonation and authentication results live:
# Headers are the ground truth. Read them before you release anything.
Get-QuarantineMessageHeader -Identity $q[0].Identity | Format-List
What to read in those headers:
X-Forefront-Antispam-Report:—CAT=is the verdict (PHSH,HPHSH,SPM,HSPM,BULK,MALW),SFV=shows skips (SKA/SKNmean a bypass fired — investigate why), andSCL=/BCL=give the scores.Authentication-Results:—spf=,dkim=,dmarc=, and especiallycompauth=with itsreasoncode, which is why spoof intelligence passed or failed.X-Microsoft-Antispam:— the authoritativeBCL:.
To inspect the body without releasing, use the portal preview pane (links and scripts neutralized) or pull the message for offline analysis:
# Export the raw message for sandbox/header analysis without delivering it
Export-QuarantineMessage -Identity $q[0].Identity |
ForEach-Object { [IO.File]::WriteAllBytes("C:\triage\sample.eml",
[Convert]::FromBase64String($_.Eml)) }
When triage concludes the message is legitimate, release vs report is a real fork:
- Release to recipients delivers it but teaches Defender nothing — the verdict stands and the next identical message is quarantined again. Use it only for genuine urgency where you also intend to submit.
- Report (admin submission) as a false positive delivers it and sends the sample to Microsoft, which is what changes the outcome going forward (step 3).
# Release a single confirmed-legitimate message to its original recipients
Release-QuarantineMessage -Identity $q[0].Identity -ReleaseToAll
# Or release only to specific recipients on a multi-recipient message
Release-QuarantineMessage -Identity $q[0].Identity `
-User "ap@contoso.com","ap-backup@contoso.com"
Do not bulk-release by verdict (
Get-QuarantineMessage -QuarantineTypes Spam | Release-QuarantineMessage). One poisoned newsletter list in a spam batch and you have hand-delivered phishing to every recipient. Bulk-release only by a known sender + tight window you have already inspected, never by verdict alone.
3. Admin submissions and the automatic allow entries they create
Admin submissions are the supported path to correct a verdict, and the mechanism most teams under-use because they reach for a TABL allow first. Submitting a false positive does two things at once: it requests Microsoft reanalysis (which can fix the classification for every tenant), and it auto-creates a temporary allow in your own TABL so the message type is not re-quarantined while reanalysis runs.
The portal lives at Defender portal > Actions & submissions > Submissions. From PowerShell, New-ReportSubmission (V3 module) drives it:
# Submit a false positive (good mail wrongly quarantined). This auto-creates a
# bounded allow in TABL AND sends the sample to Microsoft for reclassification.
New-ReportSubmission -Recipient "ap@contoso.com" `
-SenderAddress "billing@vendor-mail.net" `
-Subject "Invoice 99213" `
-ReportType "NotJunk"
# Submit a false negative (bad mail that reached the inbox) for analysis + ZAP
New-ReportSubmission -Recipient "user@contoso.com" `
-SenderAddress "ceo@contoso-payroll.io" `
-Subject "Urgent wire request" `
-ReportType "Junk"
Submission-generated allows are the right kind: scoped to what was submitted (sender/domain, URL, or file), time-boxed (typically 30 days, extended automatically only while Microsoft still classifies the item as bad), and self-removing once the classification flips clean. That managed lifecycle is exactly why submissions beat hand-built allows.
A user report via the built-in Report button can land in your reporting mailbox and go to Microsoft, depending on
ReportSubmissionPolicy. Wire that policy so reports feed the same submission pipeline rather than dying in a shared mailbox — a clean reporting path is what makes the runbook in step 8 fast.
4. Tenant Allow/Block List: senders, domains, spoofed pairs, URLs, and files
When you do need a manual override — a block on something Microsoft has not yet caught, or an allow a submission cannot create — the TABL is the supported surface, overriding verdicts across four entry families, each with its own cmdlet path.
# Inspect what exists first — every entry is technical debt until proven otherwise
Get-TenantAllowBlockListItems -ListType Sender
Get-TenantAllowBlockListItems -ListType Url
Get-TenantAllowBlockListItems -ListType FileHash
Get-TenantAllowBlockListSpoofItems
Blocks are the entries you should be liberal with; allows are the ones you must justify. Build blocks immediately on confirmed-bad indicators:
# Block a confirmed-malicious sender and a phishing domain
New-TenantAllowBlockListItems -ListType Sender `
-Block -Entries "ceo@contoso-payroll.io" -NoExpiration `
-Notes "Confirmed BEC sender, INC-5532"
# Block a malicious URL (wildcards match path/subdomain per Microsoft's rules)
New-TenantAllowBlockListItems -ListType Url `
-Block -Entries "contoso-payroll.io/*" -NoExpiration `
-Notes "BEC payload host, INC-5532"
# Block a malicious attachment by SHA256 (the only file identifier TABL accepts)
New-TenantAllowBlockListItems -ListType FileHash `
-Block -Entries "0123abcd...<64-hex-sha256>..." -NoExpiration `
-Notes "Dropper, INC-5532"
Spoofed-sender pairs are their own list — for legitimate mail that fails DMARC alignment because the sending infrastructure does not match the visible domain. Allow the exact pair, not the whole domain:
# Allow a specific spoofed pair: sending infra + spoofed (visible) domain
New-TenantAllowBlockListSpoofItems `
-Action Allow `
-SpoofType External `
-SendingInfrastructure "mail.partner-relay.net" `
-SpoofedUser "bigfreight.com"
Correctness points that bite people:
- A
Senderallow is the visibleFrom(5322) address or domain; it does not bypass spoof intelligence. If mail is held because it failed spoof checks, a sender allow will not help — you need a spoof pair allow. Match the override to the actual block reason in the headers (CAT=/compauth=), not to intuition. - Files are SHA256 only — no MD5, no filename. Compute the hash from the exported sample.
- URL entries use Microsoft’s wildcard syntax, not regex —
contoso.com/*covers paths, a leading wildcard covers subdomains, and arbitrary substrings do not match. Test the entry against the actual URL before trusting it.
5. Allow expiration, the override hierarchy, and why allows are temporary
The most dangerous object in this system is a permanent allow entry — a standing instruction to skip protection while the threat landscape moves underneath it. Today’s legitimate newsletter domain is tomorrow’s hijacked sender, still wearing your allow.
Make every allow expire:
# Allow entries get an expiry. Always. 30 days is a sane default for an FP.
New-TenantAllowBlockListItems -ListType Url `
-Allow -Entries "newsletter.partner.com/track*" `
-ExpirationDate (Get-Date).AddDays(30) `
-Notes "FP - partner newsletter, INC-4821"
# Audit: find any allow that never expires — these are your liabilities
Get-TenantAllowBlockListItems -ListType Sender |
Where-Object { $_.Action -eq "Allow" -and -not $_.ExpirationDate } |
Format-Table Value, Action, Notes
Understand the override hierarchy so you can predict which control wins. From strongest:
- Connection-filter IP Allow List (SCL -1) and certain mail-flow-rule SCL overrides — these turn filtering off for a path and outrank almost everything. (Avoid them; covered in the EOP tuning guide.)
- TABL block entries — blocks beat allows and win ties.
- TABL allow entries and submission-generated allows.
- Filtering-stack verdicts (anti-spam, anti-phishing, Safe Attachments/Links).
Two rules fall out:
- Block beats allow. Where a block and an allow both match an item, the block wins — which is what makes a TABL block a reliable kill switch.
- An IP allow (SCL -1) outranks a TABL block for that path. Connection-filter-allow a relay and a TABL block on a sender through it may not fire. That is the strongest argument against the connection-filter allow list as a convenience — it sits above your override layer.
Mental model: blocks are durable, allows are temporary, and the connection-filter allow list is a tenant-wide off switch above both. An allow you cannot let expire is a signal to fix the root cause (sender authentication, a policy exclusion) instead.
6. Hunting related mail with Threat Explorer and taking remediation actions
One reported message is rarely alone. Threat Explorer (Plan 2: Email & collaboration > Explorer; Plan 1 has the narrower Real-time detections) is where you pivot from one sample to the whole campaign and act on the set.
The hunting loop:
- Pivot on the indicator — search by sender domain, sender IP, URL, file hash, or subject. Switch to All email to see delivered messages, not just blocked ones; Malware and Phish views pre-filter to those verdicts.
- Scope the blast radius — the result grid shows every recipient, the delivery location (Inbox, Junk, Quarantine, Failed), and the detection technology. That answers “how many got it, and did any land in the inbox.”
- Remediate from the grid — select messages and Take action: Soft delete (recoverable, lands in Deleted Items), Move to Junk, Move to Inbox (confirmed FP campaign), or submit to Microsoft. Defender logs every remediation to the Action center for audit.
Explorer’s reach is 30 days on Plan 2 — the same horizon as default quarantine retention, not by accident; both bound your investigation window. For longer-range hunting, move to Advanced Hunting (KQL over EmailEvents/EmailUrlInfo/EmailAttachmentInfo) to correlate across that edge or join mail to endpoint signals:
// Blast radius: every recipient of a campaign sender domain, and where it landed
EmailEvents
| where Timestamp > ago(7d)
| where SenderFromDomain == "contoso-payroll.io"
| project Timestamp, NetworkMessageId, SenderFromAddress, RecipientEmailAddress,
DeliveryAction, DeliveryLocation, ThreatTypes
| sort by Timestamp desc
The output of this step is almost always a block (step 4) on the campaign indicators plus a remediation on everything already delivered — not a release. Hunting is how you confirm a single quarantined sample was not the visible edge of a hundred that reached inboxes.
7. Zero-hour Auto Purge (ZAP) and post-delivery remediation
ZAP is the safety net for mail that was clean at delivery and turned malicious afterward — a URL benign when scanned and weaponized an hour later, or a campaign Microsoft reclassified post-delivery. It relocates already-delivered mail retroactively.
Operationally:
- ZAP acts only on delivered mail, and relocates rather than deletes — phishing/malware ZAP moves the message to quarantine, spam ZAP to Junk. A successful ZAP therefore shows up as a new quarantine entry for a message the user may already have seen; triage it like any other.
- ZAP cannot act on a message the user already moved out of the inbox, and it respects
Set-MailboxJunkEmailConfigurationsafe-sender entries — a user who safe-listed a sender can suppress ZAP for it. Check that gap when ZAP “should have fired but didn’t.” - ZAP is enabled via the anti-spam policy (
SpamZapEnabled/PhishZapEnabled) — the difference between catching a delayed-weaponization campaign automatically and hearing about it from a user.
# Confirm ZAP is enabled on the policies that matter
Get-HostedContentFilterPolicy | Format-Table Name, SpamZapEnabled, PhishZapEnabled
When ZAP has not caught something you have now confirmed bad, the manual equivalent is Explorer/Advanced Hunting Take action > Soft delete across the step-6 recipient set — post-delivery remediation by hand. Pair it with a TABL block so the next wave is stopped at the front door, not purged after delivery.
Enterprise scenario
A financial-services firm (~22k mailboxes, Defender for Office 365 Plan 2) had its SOC paged at 08:10 on a payroll-run morning: a user reported a wire-request email impersonating the CFO. The analyst’s first instinct — and the help desk’s loud request — was to release a near-identical message from another mailbox’s quarantine “so finance isn’t blocked,” which would have hand-delivered the lure to a second target.
The constraint was real: payroll was mid-run, leadership was escalating, the SOC had minutes. But the message was a true positive (CAT=HPHSH, compauth=fail, lookalike domain contoso-payroll.io), so releasing anything was exactly wrong.
The analyst ran the loop instead of the instinct. Threat Explorer, pivoted on the sender domain, showed fourteen recipients across finance and HR — eleven correctly quarantined, but three delivered to the inbox because those mailboxes had safe-listed a similar partner domain, suppressing the verdict; ZAP had not fired on those three for the same reason. The fix was a tight sequence:
# 1) Kill switch: block the BEC sender, domain, and payload URL — blocks beat allows
New-TenantAllowBlockListItems -ListType Sender -Block `
-Entries "ceo@contoso-payroll.io" -NoExpiration -Notes "BEC, INC-7781"
New-TenantAllowBlockListItems -ListType Url -Block `
-Entries "contoso-payroll.io/*" -NoExpiration -Notes "BEC payload, INC-7781"
# 2) Submit for reanalysis + ZAP-style cleanup signal
New-ReportSubmission -Recipient "cfo-office@contoso.com" `
-SenderAddress "ceo@contoso-payroll.io" `
-Subject "Urgent wire request" -ReportType "Junk"
Then, from the Explorer grid, the three inbox-delivered copies were soft-deleted across all recipients — the manual equivalent of the ZAP the safe-sender entries had blocked — and logged to the Action center. Time from page to contained: under fifteen minutes, with zero releases. The durable follow-up was a review of user safe-sender lists for lookalike-domain entries, the exact gap that let three messages through.
The lesson the team wrote into its runbook: on a true-positive report, block and hunt, never release. Urgency is a reason to be faster, not a reason to deliver the attack to one more inbox.
Verify
Confirm the operating surface is healthy and your overrides are clean.
# 1. Quarantine retention is what you think it is (bounds your investigation window)
Get-HostedContentFilterPolicy |
Format-Table Name, QuarantineRetentionPeriod, SpamZapEnabled, PhishZapEnabled
# 2. Current quarantine population by verdict — sanity-check the mix
Get-QuarantineMessage -StartReceivedDate (Get-Date).AddDays(-1) -EndReceivedDate (Get-Date) |
Group-Object QuarantineTypes | Sort-Object Count -Descending |
Format-Table Name, Count
# 3. Every TABL list, flagging non-expiring ALLOWS (the liabilities)
foreach ($lt in 'Sender','Url','FileHash') {
"== $lt =="; Get-TenantAllowBlockListItems -ListType $lt |
Select-Object Value, Action, ExpirationDate, Notes,
@{n='Liability';e={$_.Action -eq 'Allow' -and -not $_.ExpirationDate}}
}
Get-TenantAllowBlockListSpoofItems |
Format-Table SpoofedUser, SendingInfrastructure, Action, SpoofType
In the portal, confirm the human-facing surfaces: Review > Quarantine shows a sensible population; Actions & submissions > Submissions shows recent reports with result status; the Action center lists Explorer remediations with operator and timestamp. A reported true-positive should appear as a block plus a remediation, never a release.
Checklist
Pitfalls
The recurring failures are operational, not technical. Releasing because a user asked is the headline — the request opens triage, headers close it, and for true positives the answer is block-and-hunt. Bulk-releasing by verdict turns one poisoned list into mass delivery; release only by inspected sender and tight window. Reaching for a TABL allow before a submission skips the managed-lifecycle, fix-it-for-everyone path and leaves you a manual entry to garbage-collect. Permanent allows age into liabilities — expire them. Mismatching the override to the block reason (a sender allow for a spoof failure, a filename instead of a SHA256) produces an entry that silently does nothing. And trusting ZAP blindly misses the safe-sender suppression gap — when ZAP “should have” fired, check junk configuration and remediate by hand from Explorer.
Operate quarantine and TABL as one disciplined loop: triage on evidence, submit to fix the verdict, block to kill the campaign, allow only temporarily, hunt to be sure the sample was not the tip of a delivered wave. Source-control the PowerShell, log the remediations, and let submissions carry your exceptions so the allow list stays short enough to actually review.
Next steps
- Wire user reporting (
ReportSubmissionPolicy) so the Report button feeds this submission pipeline, and pair this runbook with the EOP anti-spam/quarantine-policy and Safe Links/Safe Attachments tuning guides for the policy layer underneath it.