tests/Test-Assessment.41031.ps1
|
<#
.SYNOPSIS Checks that Safe Attachments policies in Microsoft Defender for Office 365 are configured to detonate and block. #> function Test-Assessment-41031 { [ZtTest( Category = 'Email and collaboration security', CompatibleLicense = ('ATP_ENTERPRISE'), ImplementationCost = 'Low', Pillar = 'SecOps', RiskLevel = 'High', Service = ('ExchangeOnline'), SfiPillar = 'Protect tenants and isolate production systems', TenantType = ('Workforce'), TestId = 41031, Title = 'Safe Attachments policies in Microsoft Defender for Office 365 are configured to detonate and block', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking Safe Attachments policies in Microsoft Defender for Office 365' Write-ZtProgress -Activity $activity -Status 'Retrieving Safe Attachments policies' # Q1: Retrieve all Safe Attachments policies and associated rules from Exchange Online. $allPolicies = $null try { $allPolicies = @(Get-SafeAttachmentPolicy -ErrorAction Stop | Select-Object Identity, Enable, Action, QuarantineTag, Redirect, RedirectAddress, IsBuiltInProtection, IsDefault) } catch { Write-PSFMessage "Failed to retrieve Safe Attachments policies: $_" -Tag Test -Level Warning # Spec: ATP_ENTERPRISE gates this test, so a cmdlet failure points to an Exchange Online # permission or connectivity issue rather than an unlicensed tenant. $params = @{ TestId = '41031' Title = 'Safe Attachments policies in Microsoft Defender for Office 365 are configured to detonate and block' Status = $false Result = '⚠️ Safe Attachments policies could not be retrieved (no policies were returned, the cmdlet is unavailable, or the Exchange Online rule set cannot be read), or an enabled rule references a policy that does not exist; because MDO licensing gates this test, an empty or failed result points to an Exchange Online permission or connectivity issue rather than absence of protection — verify Exchange Online access and re-run.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } if ($allPolicies.Count -eq 0) { # Spec: Built-in Protection is always present in every MDO-licensed tenant, so zero rows # indicates an Exchange Online permission or connectivity anomaly, not an unlicensed tenant. $params = @{ TestId = '41031' Title = 'Safe Attachments policies in Microsoft Defender for Office 365 are configured to detonate and block' Status = $false Result = '⚠️ Safe Attachments policies could not be retrieved (no policies were returned, the cmdlet is unavailable, or the Exchange Online rule set cannot be read), or an enabled rule references a policy that does not exist; because MDO licensing gates this test, an empty or failed result points to an Exchange Online permission or connectivity issue rather than absence of protection — verify Exchange Online access and re-run.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } Write-ZtProgress -Activity $activity -Status 'Retrieving Safe Attachments rules' $allRules = $null try { $allRules = @(Get-SafeAttachmentRule -ErrorAction Stop | Select-Object Name, SafeAttachmentPolicy, State) } catch { Write-PSFMessage "Failed to retrieve Safe Attachments rules: $_" -Tag Test -Level Warning # Spec: if Get-SafeAttachmentRule fails while policies succeeded, return Investigate — # the rule set needed to determine which policies are actively applied cannot be read. $params = @{ TestId = '41031' Title = 'Safe Attachments policies in Microsoft Defender for Office 365 are configured to detonate and block' Status = $false Result = '⚠️ Safe Attachments policies were retrieved but the associated rules could not be read; the set of actively applied policies cannot be determined. Verify Exchange Online permissions and re-run.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } #endregion Data Collection #region Assessment Logic # Build lookup: policy Identity → policy object $policyByIdentity = @{} foreach ($policy in $allPolicies) { $policyByIdentity[$policy.Identity] = $policy } # Spec: zero rows = no custom rules; evaluate Built-in Protection policy alone. $enabledRules = @($allRules | Where-Object { $_.State -eq 'Enabled' }) # Map: policy identity → rule name (join key: rule.SafeAttachmentPolicy == policy.Identity; 1-1 enforced by Exchange Online) $rulesForPolicy = @{} foreach ($rule in $enabledRules) { $rulesForPolicy[$rule.SafeAttachmentPolicy] = $rule.Name } # Built-in protection policy (IsBuiltInProtection == True) $builtInPolicy = $allPolicies | Where-Object { $_.IsBuiltInProtection -eq $true } | Select-Object -First 1 # Collect in-scope policy identities: built-in first, then all referenced by enabled rules (deduplicated) $inScopeIdentities = [System.Collections.Generic.List[string]]::new() if ($builtInPolicy) { $inScopeIdentities.Add($builtInPolicy.Identity) } foreach ($policyName in $rulesForPolicy.Keys) { if (-not $inScopeIdentities.Contains($policyName)) { $inScopeIdentities.Add($policyName) } } $passed = $true $hasInvestigate = $false # Missing built-in protection policy is an immediate fail if ($null -eq $builtInPolicy) { $passed = $false } $policyRows = [System.Collections.Generic.List[PSCustomObject]]::new() # Process in-scope policies: built-in + rule-referenced foreach ($identity in $inScopeIdentities) { $policy = $policyByIdentity[$identity] $ruleName = if ($rulesForPolicy.ContainsKey($identity)) { $rulesForPolicy[$identity] } else { '' } if ($null -eq $policy) { # Orphan rule: enabled rule references a policy deleted from Get-SafeAttachmentPolicy → Investigate $hasInvestigate = $true $policyRows.Add([PSCustomObject]@{ Identity = "$identity (not found)" PolicyType = '—' Enable = $null Action = '—' QuarantineTag = '—' Redirect = $null RedirectAddress = '—' AppliedViaRule = $ruleName RowResult = 'Investigate' }) continue } # Type column: precedence Built-in > Default > Custom $policyType = if ($policy.IsBuiltInProtection) { 'Built-in' } elseif ($policy.IsDefault) { 'Default' } else { 'Custom' } if ($policy.Enable -eq $true -and $policy.Action -eq 'Block' -and $policy.QuarantineTag -eq 'AdminOnlyAccessPolicy') { $rowResult = 'Pass' } else { $rowResult = 'Fail' $passed = $false } # Built-in Protection applies as a preset with no explicit rule; Applied via Rule is blank for it. $displayRuleName = if ($policy.IsBuiltInProtection) { '' } else { $ruleName } $policyRows.Add([PSCustomObject]@{ Identity = $identity PolicyType = $policyType Enable = $policy.Enable Action = $policy.Action QuarantineTag = $policy.QuarantineTag Redirect = $policy.Redirect RedirectAddress = if ($policy.RedirectAddress) { $policy.RedirectAddress } else { '—' } AppliedViaRule = $displayRuleName RowResult = $rowResult }) } # Unapplied policies: custom policies with no enabled rule — informational only, do not affect verdict foreach ($policy in $allPolicies) { if ($inScopeIdentities.Contains($policy.Identity)) { continue } $policyType = if ($policy.IsBuiltInProtection) { 'Built-in' } elseif ($policy.IsDefault) { 'Default' } else { 'Custom' } $policyRows.Add([PSCustomObject]@{ Identity = $policy.Identity PolicyType = $policyType Enable = $policy.Enable Action = $policy.Action QuarantineTag = $policy.QuarantineTag Redirect = $policy.Redirect RedirectAddress = if ($policy.RedirectAddress) { $policy.RedirectAddress } else { '—' } AppliedViaRule = '' RowResult = 'Not applied' }) } # Aggregate verdict: Fail > Investigate > Pass (matching SecOps peer tests) $customStatus = $null if (-not $passed) { $testResultMarkdown = "❌ The Built-in Protection policy is missing or non-compliant, or one or more policies referenced by enabled rules is disabled, set to Allow/DynamicDelivery, or applies a quarantine policy that lets recipients self-release; detonated malware can still reach end users.`n`n%TestResult%" } elseif ($hasInvestigate) { $passed = $false $customStatus = 'Investigate' $testResultMarkdown = "⚠️ An enabled Safe Attachments rule references a policy that does not exist; manual review is required.`n`n%TestResult%" } else { $testResultMarkdown = "✅ Safe Attachments is enabled across the Built-in Protection preset and any policies referenced by enabled rules, set to Block detonated malware, and routes detections to admin-only quarantine.`n`n%TestResult%" } #endregion Assessment Logic #region Report Generation $portalUrl = 'https://security.microsoft.com/safeattachmentv2' $maxDisplay = 10 $totalCount = $policyRows.Count # Sort: worst verdict first, then alphabetically by identity; Not applied rows last $statusPriority = @{ Fail = 0; Investigate = 1; Pass = 2; 'Not applied' = 3 } $sortedRows = @($policyRows | Sort-Object { $statusPriority[$_.RowResult] }, Identity) $displayRows = @($sortedRows | Select-Object -First $maxDisplay) $tableRows = '' foreach ($row in $displayRows) { $identityMd = Get-SafeMarkdown $row.Identity $ruleNameMd = if ($row.AppliedViaRule) { Get-SafeMarkdown $row.AppliedViaRule } else { '' } $enableDisplay = if ($null -eq $row.Enable) { '—' } elseif ($row.Enable) { '✅ Yes' } else { '❌ No' } $actionDisplay = if ($row.Action -eq '—') { '—' } elseif ($row.Action -eq 'Block') { '✅ Block' } else { "❌ $($row.Action)" } $quarantineDisplay = if ($row.QuarantineTag -eq '—') { '—' } elseif ($row.QuarantineTag -eq 'AdminOnlyAccessPolicy') { '✅ AdminOnlyAccessPolicy' } else { "❌ $($row.QuarantineTag)" } $redirectDisplay = if ($null -eq $row.Redirect) { '—' } elseif ($row.Redirect) { 'Yes' } else { 'No' } $resultDisplay = switch ($row.RowResult) { 'Pass' { '✅ Pass' } 'Fail' { '❌ Fail' } 'Investigate' { '⚠️ Investigate' } 'Not applied' { 'Not applied' } } $tableRows += "| $identityMd | $($row.PolicyType) | $enableDisplay | $actionDisplay | $quarantineDisplay | $redirectDisplay | $($row.RedirectAddress) | $ruleNameMd | $resultDisplay |`n" } if ($totalCount -gt $maxDisplay) { $tableRows += "| ... | ... | ... | ... | ... | ... | ... | ... | ... |`n" } $preTableLines = '' if ($totalCount -gt $maxDisplay) { $preTableLines = "Showing $maxDisplay of $totalCount policies. [View all in Microsoft 365 Defender > Policies & rules > Threat policies > Safe Attachments]($portalUrl)`n`n" } $formatTemplate = @' {0} | Identity | Type | Enable | Action | Quarantine policy | Redirect | Redirect address | Applied via rule | Result | | :------- | :--- | :----- | :----- | :---------------- | :------- | :--------------- | :--------------- | :----- | {1} '@ $mdInfo = $formatTemplate -f $preTableLines, $tableRows $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo #endregion Report Generation $params = @{ TestId = '41031' Title = 'Safe Attachments policies in Microsoft Defender for Office 365 are configured to detonate and block' Status = $passed Result = $testResultMarkdown } if ($customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |