Checks/MDO/MET-MDO005-AntiMalware.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'METCheckInfo',
    Justification = 'Check metadata. Read from the AST by Get-METCheck and never executed.')]
param()

$METCheckInfo = @{
    Name           = 'Anti-Malware Effective Coverage'
    Severity       = 'High'
    Description    = 'Resolves the effective anti-malware policy per mailbox and checks ZapEnabled, EnableFileFilter, admin notifications, and the common attachment filter.'
    RequiresModule = @('ExchangeOnlineManagement')
}

$allMailboxes = $null
try {
    $allMailboxes = if ($METContext -and $METContext.AllMailboxes) { @($METContext.AllMailboxes) } else { @(Get-METAssessableMailboxes) }
    if ($METContext -and -not $METContext.AllMailboxes) { $METContext.AllMailboxes = $allMailboxes }
}
catch {
    New-METCheckResult -CheckId 'MET-MDO005' -Category MDO -Name 'Anti-Malware Effective Coverage' -Result Warning -Severity High -AffectedObject 'All Mailboxes' -Finding 'Unable to determine effective anti-malware coverage because the mailbox list could not be retrieved.' -Recommendation 'Ensure the account has Exchange View-Only Recipients permission and rerun the assessment.' -ReferenceUrl 'https://aka.ms/mdo-antimalware' -ErrorMessage $_.ToString()
    return
}
if ($allMailboxes.Count -eq 0) {
    New-METCheckResult -CheckId 'MET-MDO005' -Category MDO -Name 'Anti-Malware Effective Coverage' -Result NotApplicable -Severity High -AffectedObject 'Tenant (0 mailboxes)' -Finding 'No assessable mailboxes were found in the tenant.' -ReferenceUrl 'https://aka.ms/mdo-antimalware'
    return
}

$errors = [System.Collections.Generic.List[string]]::new()
try { $rules = @(Get-MalwareFilterRule -ErrorAction Stop) } catch { $rules=@(); $errors.Add("Unable to retrieve anti-malware rules. $($_.ToString())") }
try { $policies = @(Get-MalwareFilterPolicy -ErrorAction Stop) } catch { $policies=@(); $errors.Add("Unable to retrieve anti-malware policies. $($_.ToString())") }
try { $presets = @(Get-EOPProtectionPolicyRule -ErrorAction Stop) } catch { $presets=@(); $errors.Add("Unable to retrieve preset policy rules. $($_.ToString())") }
$groupCache = if ($METContext -and $METContext.GroupMembers) { $METContext.GroupMembers } else { @{} }
$resolution = Resolve-METEffectivePolicy -Subjects $allMailboxes -GroupCache $groupCache -Rules $rules -Policies $policies -PresetRules $presets -IncludePresets -PolicyLinkProperty MalwareFilterPolicy -ProtectionType 'anti-malware' -RetrievalErrors $errors

$evaluate = {
    param($policy, $policyType)
    $issues = [System.Collections.Generic.List[string]]::new()
    if (-not $policy) { $issues.Add('Policy settings could not be retrieved'); return $issues.ToArray() }

    $zapProperty = $policy.PSObject.Properties['ZapEnabled']
    if (-not $zapProperty -or $null -eq $zapProperty.Value) {
        $issues.Add('ZapEnabled was not returned for this policy, so whether ZAP for malware is enabled was not established')
    }
    elseif (-not $zapProperty.Value) {
        $issues.Add('ZAP for malware is disabled')
    }

    $fileFilterProperty = $policy.PSObject.Properties['EnableFileFilter']
    $fileFilterConfirmedEnabled = $false
    if (-not $fileFilterProperty -or $null -eq $fileFilterProperty.Value) {
        $issues.Add('EnableFileFilter was not returned for this policy, so whether the common attachment filter is enabled was not established')
    }
    elseif (-not $fileFilterProperty.Value) {
        $issues.Add('Common attachment filter is disabled')
    }
    else {
        $fileFilterConfirmedEnabled = $true
    }

    if ($fileFilterConfirmedEnabled) {
        $fileTypeActionProperty = $policy.PSObject.Properties['FileTypeAction']
        if (-not $fileTypeActionProperty -or $null -eq $fileTypeActionProperty.Value) {
            $issues.Add('FileTypeAction was not returned for this policy, so whether the common attachment filter action is Reject was not established')
        }
        elseif ($fileTypeActionProperty.Value -ne 'Reject') {
            $issues.Add("Common attachment action is '$($fileTypeActionProperty.Value)' - recommended Reject")
        }
    }
    if ($fileFilterConfirmedEnabled -and $policy.PSObject.Properties['FileTypes']) {
        $configuredFileTypes = @(@($policy.FileTypes) | Where-Object { $_ } | ForEach-Object { $_.ToString().Trim().TrimStart('.').ToLowerInvariant() })
        if ($configuredFileTypes.Count -eq 0) {
            $issues.Add('Common attachment filter is enabled but its file type list is empty, so it blocks nothing')
        }
        else {
            $highRiskFileTypes = @('exe','com','scr','pif','cmd','bat','vbs','vbe','js','jse','wsf','wsh','hta','ps1','msi','lnk','jar','reg','cpl','dll')
            $missingFileTypes = @($highRiskFileTypes | Where-Object { $configuredFileTypes -notcontains $_ } | Sort-Object)
            if ($missingFileTypes.Count -gt 0) {
                $issues.Add("Common attachment filter does not block high-risk file types: $($missingFileTypes -join ', ')")
            }
        }
    }
    if ($policy.QuarantineTag -and $policy.QuarantineTag -ne 'AdminOnlyAccessPolicy') { $issues.Add("Malware quarantine policy is '$($policy.QuarantineTag)' - recommended AdminOnlyAccessPolicy") }
    $issues.ToArray()
}
New-METEffectivePolicyCoverageResult -CheckId 'MET-MDO005' -Name 'Anti-Malware Effective Coverage' -ProtectionType 'Anti-Malware' -Severity High -Subjects $allMailboxes -Resolution $resolution -GetPolicyIssues $evaluate -RetrievalErrors $errors -ReferenceUrl 'https://aka.ms/mdo-antimalware' -Recommendation 'Apply a Standard/Strict preset or a compliant custom anti-malware policy to every affected recipient. Enable malware ZAP and the common attachment filter, use Reject for blocked file types, and use the recommended malware quarantine policy.'