tests/Test-Assessment.41029.ps1
|
<#
.SYNOPSIS Anti-malware policies are configured according to recommended settings #> function Test-Assessment-41029 { [ZtTest( Category = 'Email and collaboration security', ImplementationCost = 'Low', Service = ('ExchangeOnline'), CompatibleLicense = ('EXCHANGE_S_STANDARD'), Pillar = 'SecOps', RiskLevel = 'High', SfiPillar = 'Protect tenants and isolate production systems', TenantType = ('Workforce'), TestId = 41029, Title = 'Anti-malware policies are configured according to recommended settings', UserImpact = 'Low' )] [CmdletBinding()] param() #region Data Collection Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose $activity = 'Checking anti-malware policy configuration' Write-ZtProgress -Activity $activity -Status 'Getting anti-malware filter policies' # Q1: Enumerate all anti-malware filter policies and their settings $policies = $null try { $policies = @(Get-MalwareFilterPolicy -ErrorAction Stop | Select-Object Identity, IsDefault, EnableFileFilter, FileTypes, ZapEnabled, QuarantineTag, EnableInternalSenderAdminNotifications, EnableExternalSenderAdminNotifications, ExternalSenderAdminAddress) } catch { Write-PSFMessage "Failed to retrieve anti-malware filter policies: $_" -Tag Test -Level Warning $params = @{ TestId = '41029' Title = 'Anti-malware policies are configured according to recommended settings' Status = $false Result = '⚠️ Anti-malware filter policies could not be retrieved; verify Exchange Online permissions and re-run.' CustomStatus = 'Investigate' } Add-ZtTestResultDetail @params return } # Q2: Enumerate all anti-malware filter rules (required to determine which custom policies are in-scope) Write-ZtProgress -Activity $activity -Status 'Getting anti-malware filter rules' $rules = $null try { $rules = @(Get-MalwareFilterRule -ErrorAction Stop | Select-Object Name, MalwareFilterPolicy, Priority, State) } catch { Write-PSFMessage "Failed to retrieve anti-malware filter rules: $_" -Tag Test -Level Warning $params = @{ TestId = '41029' Title = 'Anti-malware policies are configured according to recommended settings' Status = $false Result = '⚠️ Anti-malware filter 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 $passed = $false $customStatus = $null $inScopePolicies = [System.Collections.Generic.List[object]]::new() $orphanedRules = @() # Step 2: Empty policy result indicates API failure — default policy always exists in Exchange Online if ($policies.Count -eq 0) { $customStatus = 'Investigate' $testResultMarkdown = "⚠️ **Investigate:** ``Get-MalwareFilterPolicy`` returned no results. The default policy is always present in Exchange Online; an empty result indicates an API or connectivity failure." } else { # Step 3: Build in-scope set = default policy + policies referenced by enabled rules $enabledRules = @($rules | Where-Object { $_.State -eq 'Enabled' }) # Build case-insensitive lookup by Identity for rule-to-policy resolution. # The spec says match on "Identity or Name", but for Get-MalwareFilterPolicy the returned # Identity IS the policy name string (Exchange accepts name, GUID, or DN as input; the # cmdlet echoes back the name as Identity). MalwareFilterRule.MalwareFilterPolicy also # stores the policy name, so a single Identity-keyed lookup satisfies both identifiers. $policyByIdentity = @{} foreach ($policy in $policies) { $policyByIdentity[$policy.Identity] = $policy } # Track in-scope identities to avoid duplicates $inScopeIdentities = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) # Default policy is always in-scope (catch-all for any recipient not matched by a custom rule) foreach ($policy in ($policies | Where-Object { $_.IsDefault -eq $true })) { if ($inScopeIdentities.Add($policy.Identity)) { [void]$inScopePolicies.Add($policy) } } # Step 4: Add policies referenced by enabled rules; flag orphaned rules (reference missing policy) foreach ($rule in $enabledRules) { $referencedName = $rule.MalwareFilterPolicy if ($policyByIdentity.ContainsKey($referencedName)) { $referencedPolicy = $policyByIdentity[$referencedName] if ($inScopeIdentities.Add($referencedPolicy.Identity)) { [void]$inScopePolicies.Add($referencedPolicy) } } else { $orphanedRules += $rule } } if ($orphanedRules.Count -gt 0) { $customStatus = 'Investigate' foreach ($policy in $inScopePolicies) { $policy | Add-Member -NotePropertyName RowResult -NotePropertyValue '⚠️ Investigate' -Force } $orphanNames = ($orphanedRules | ForEach-Object { Get-SafeMarkdown -Text $_.Name }) -join ', ' $testResultMarkdown = "⚠️ **Investigate:** $($orphanedRules.Count) enabled rule(s) reference a policy that does not exist (orphan rule); manual review is required. Orphaned rules: $orphanNames.`n`n%TestResult%" } else { # Steps 5–7: Evaluate compliance for each in-scope policy $nonCompliantPolicies = @() $investigatePolicies = @() foreach ($policy in $inScopePolicies) { if ($policy.QuarantineTag -ne 'AdminOnlyAccessPolicy') { # Step 6: Customer-defined quarantine policy — release behavior cannot be confirmed $policy | Add-Member -NotePropertyName RowResult -NotePropertyValue '⚠️ Investigate' -Force $investigatePolicies += $policy } elseif ($policy.EnableFileFilter -ne $true -or $policy.ZapEnabled -ne $true) { # Step 7 (fail): Common Attachment Filter or ZAP is not enabled $policy | Add-Member -NotePropertyName RowResult -NotePropertyValue '❌ Fail' -Force $nonCompliantPolicies += $policy } else { $policy | Add-Member -NotePropertyName RowResult -NotePropertyValue '✅ Pass' -Force } } if ($investigatePolicies.Count -gt 0) { $customStatus = 'Investigate' $policyNames = ($investigatePolicies | ForEach-Object { Get-SafeMarkdown -Text $_.Identity }) -join ', ' $testResultMarkdown = "⚠️ **Investigate:** One or more in-scope policies apply a customer-defined quarantine policy whose release behavior cannot be confirmed by this check — cross-check spec 41030. Affected: $policyNames.`n`n%TestResult%" } elseif ($nonCompliantPolicies.Count -gt 0) { $testResultMarkdown = "❌ One or more in-scope anti-malware policies have the Common Attachment Filter disabled, ZAP disabled, or apply a quarantine policy that permits recipient self-release of malware.`n`n%TestResult%" } else { $passed = $true $testResultMarkdown = "✅ All in-scope anti-malware policies have the Common Attachment Filter enabled, zero-hour auto purge enabled, and route detected malware to an admin-only quarantine.`n`n%TestResult%" } } } #endregion Assessment Logic #region Report Generation if ($inScopePolicies.Count -gt 0) { $totalInScope = $inScopePolicies.Count $maxDisplay = 10 $hasMoreItems = $totalInScope -gt $maxDisplay $tableRows = '' foreach ($policy in ($inScopePolicies | Select-Object -First $maxDisplay)) { $identity = Get-SafeMarkdown -Text $policy.Identity $isDefault = if ($policy.IsDefault -eq $true) { 'Yes' } else { 'No' } $fileFilter = if ($policy.EnableFileFilter -eq $true) { '✅ Yes' } else { '❌ No' } $fileTypesCount = if ($policy.FileTypes) { $policy.FileTypes.Count } else { 0 } $zap = if ($policy.ZapEnabled -eq $true) { '✅ Yes' } else { '❌ No' } $quarantineTag = Get-SafeMarkdown -Text $policy.QuarantineTag $applied = if ($policy.IsDefault -eq $true) { 'Default catch-all' } else { 'Via enabled rule' } $rowResult = $policy.RowResult $tableRows += "| $identity | $isDefault | $fileFilter | $fileTypesCount | $zap | $quarantineTag | $applied | $rowResult |`n" } if ($hasMoreItems) { $remaining = $totalInScope - $maxDisplay $tableRows += "`n... and $remaining more. [View all anti-malware policies](https://security.microsoft.com/antimalwarev2)`n" } $internalNotifyCount = @($inScopePolicies | Where-Object { $_.EnableInternalSenderAdminNotifications -eq $true }).Count $externalNotifyCount = @($inScopePolicies | Where-Object { $_.EnableExternalSenderAdminNotifications -eq $true }).Count $notificationNote = "_Of $totalInScope in-scope policies, $internalNotifyCount have internal sender admin notifications enabled and $externalNotifyCount have external sender admin notifications enabled. Microsoft's baseline recommends both off; these settings do not affect this check's Pass/Fail._" $antimalwarePortalUrl = 'https://security.microsoft.com/antimalwarev2' $formatTemplate = @' ## [In-scope anti-malware policies]({0}) | Identity | Is default | Common attachment filter | File types (count) | ZAP | Quarantine tag | Applied | Result | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | {1} {2} '@ $mdInfo = $formatTemplate -f $antimalwarePortalUrl, $tableRows, $notificationNote $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo } #endregion Report Generation $params = @{ TestId = '41029' Title = 'Anti-malware policies are configured according to recommended settings' Status = $passed Result = $testResultMarkdown } if ($null -ne $customStatus) { $params.CustomStatus = $customStatus } Add-ZtTestResultDetail @params } |