Checks/EXO/MET-EXO009-QuarantinePolicyVerdictAlignment.ps1
|
# Verifies that quarantine policies assigned to Malware and High-Confidence Phish verdicts # prevent user self-release, the only two verdicts with a restrictive floor per Microsoft's # own Standard/Strict preset matrix. Every # other verdict (Phish, Mailbox Intelligence Phish, Spoof, both impersonation types, spam # tiers, Bulk) uses a full-access quarantine policy even under Strict, so they are not # evaluated. Assignments sourced from preset-generated policies (Standard/Strict Preset # Security Policy*) are skipped entirely - their tags are guaranteed correct by Microsoft and # not admin-actionable. Custom policies are still checked against the actual # PermissionToRelease bit, not the tag's name. $policyPermissions = @{} try { Get-QuarantinePolicy -ErrorAction Stop | ForEach-Object { $policyPermissions[$_.Name] = $_ } } catch { New-METCheckResult -CheckId 'MET-EXO009' -Category EXO -Name 'Quarantine Policy Verdict Alignment' ` -Result Fail -Severity High -AffectedObject 'Quarantine Policies' ` -Finding 'Unable to retrieve quarantine policies' ` -Recommendation 'Ensure the account has Security Reader or higher permissions.' ` -ReferenceUrl 'https://aka.ms/mdo-quarantinepolicies' -ErrorMessage $_.ToString() return } # Only these two verdicts have a restrictive floor (PermissionToRelease must be $false) in # Microsoft's own Default/Standard/Strict matrix. Every other verdict has no floor to enforce. $restrictedVerdicts = @( 'Malware' 'High-Confidence Phish' ) $assignments = [System.Collections.Generic.List[hashtable]]::new() $retrievalErrors = [System.Collections.Generic.List[string]]::new() # Anti-spam (EOP + MDO) try { foreach ($p in (Get-HostedContentFilterPolicy -ErrorAction Stop)) { if (Test-METIsPresetSecurityPolicyName -Name $p.Name) { continue } $verdictMap = [ordered]@{ HighConfidencePhishQuarantineTag = 'High-Confidence Phish' PhishQuarantineTag = 'Phish' HighConfidenceSpamQuarantineTag = 'High-Confidence Spam' SpamQuarantineTag = 'Spam' BulkQuarantineTag = 'Bulk' } foreach ($entry in $verdictMap.GetEnumerator()) { $tag = $p.($entry.Key) if ($tag) { $null = $assignments.Add(@{ Source = $p.Name; Verdict = $entry.Value; Tag = $tag }) } } } } catch { $retrievalErrors.Add("Anti-spam policies: $($_.ToString())") } # Anti-malware try { foreach ($p in (Get-MalwareFilterPolicy -ErrorAction Stop)) { if (Test-METIsPresetSecurityPolicyName -Name $p.Name) { continue } if ($p.QuarantineTag) { $null = $assignments.Add(@{ Source = $p.Name; Verdict = 'Malware'; Tag = $p.QuarantineTag }) } } } catch { $retrievalErrors.Add("Anti-malware policies: $($_.ToString())") } # Anti-phish impersonation verdicts (MDO Plan 1+) try { foreach ($p in (Get-AntiPhishPolicy -ErrorAction Stop)) { if (Test-METIsPresetSecurityPolicyName -Name $p.Name) { continue } $verdictMap = [ordered]@{ TargetedUserQuarantineTag = 'Impersonated User' TargetedDomainQuarantineTag = 'Impersonated Domain' MailboxIntelligenceQuarantineTag = 'Mailbox Intelligence Phish' SpoofQuarantineTag = 'Spoof' } foreach ($entry in $verdictMap.GetEnumerator()) { $tag = $p.($entry.Key) if ($tag) { $null = $assignments.Add(@{ Source = $p.Name; Verdict = $entry.Value; Tag = $tag }) } } } } catch { Write-Verbose "MET-EXO009: Get-AntiPhishPolicy unavailable - may not be MDO licensed" } # Safe Attachments (MDO Plan 1+) - only Block action results in quarantine try { foreach ($p in (Get-SafeAttachmentPolicy -ErrorAction Stop | Where-Object { $_.Action -eq 'Block' })) { if (Test-METIsPresetSecurityPolicyName -Name $p.Name) { continue } if ($p.QuarantineTag) { $null = $assignments.Add(@{ Source = $p.Name; Verdict = 'Malware'; Tag = $p.QuarantineTag }) } } } catch { Write-Verbose "MET-EXO009: Get-SafeAttachmentPolicy unavailable - may not be MDO licensed" } if ($retrievalErrors.Count -gt 0 -and $assignments.Count -eq 0) { New-METCheckResult -CheckId 'MET-EXO009' -Category EXO -Name 'Quarantine Policy Verdict Alignment' ` -Result Fail -Severity High -AffectedObject 'Filter Policies' ` -Finding "Unable to retrieve filter policies needed for verdict alignment check: $($retrievalErrors -join '; ')" ` -Recommendation 'Ensure the account has Security Reader or higher permissions.' ` -ReferenceUrl 'https://aka.ms/mdo-quarantinepolicies' return } $fails = [System.Collections.Generic.List[string]]::new() foreach ($a in $assignments) { if ($a.Verdict -notin $restrictedVerdicts) { continue } $qp = $policyPermissions[$a.Tag] if (-not $qp) { $null = $fails.Add("Policy '$($a.Source)': $($a.Verdict) verdict references quarantine tag '$($a.Tag)' which does not exist") continue } if ($qp.EndUserQuarantinePermissions.PermissionToRelease) { $null = $fails.Add("Policy '$($a.Source)': $($a.Verdict) verdict uses '$($a.Tag)' which allows users to self-release quarantined messages") } } if ($fails.Count -gt 0) { New-METCheckResult -CheckId 'MET-EXO009' -Category EXO -Name 'Quarantine Policy Verdict Alignment' ` -Result Fail -Severity High -AffectedObject 'Quarantine Tag Assignments' ` -Finding ($fails -join '; ') ` -Recommendation 'For Malware and High-Confidence Phish verdicts, assign a quarantine policy with PermissionToRelease disabled. Use AdminOnlyAccessPolicy or a custom policy with equivalent restrictions.' ` -ReferenceUrl 'https://aka.ms/mdo-quarantinepolicies' } else { New-METCheckResult -CheckId 'MET-EXO009' -Category EXO -Name 'Quarantine Policy Verdict Alignment' ` -Result Pass -Severity High -AffectedObject 'Quarantine Tag Assignments' ` -Finding 'Malware and High-Confidence Phish verdicts use quarantine policies that prevent user self-release' ` -ReferenceUrl 'https://aka.ms/mdo-quarantinepolicies' } |