Private/Entra/Checks/Invoke-M365ExchangeChecks.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function Invoke-M365ExchangeChecks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [hashtable]$AuditData
    )

    $checkDefs = Get-AuditCategoryDefinitions -Category 'M365ExchangeChecks'
    $findings = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($check in $checkDefs.checks) {
        $funcName = "Test-Infiltration$($check.id -replace '-', '')"
        if (Get-Command $funcName -ErrorAction SilentlyContinue) {
            try {
                $finding = & $funcName -AuditData $AuditData -CheckDefinition $check
                if ($finding) { $findings.Add($finding) }
            } catch {
                $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' `
                    -CurrentValue "Check failed: $_"))
            }
        } else {
            $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' `
                -CurrentValue 'Check not yet implemented'))
        }
    }

    return @($findings)
}

# ── M365EXO-001: Exchange Organization Configuration ────────────────────
function Test-InfiltrationM365EXO001 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.OrganizationConfig) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange Online data not available (EXO module not connected)'
    }

    $orgConfig = $exo.OrganizationConfig
    $auditDisabled = $orgConfig.AuditDisabled
    $oauth2ClientProfileEnabled = $orgConfig.OAuth2ClientProfileEnabled

    $status = if ($auditDisabled -eq $false) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Exchange Org: AuditDisabled=$auditDisabled, OAuth2ClientProfile=$oauth2ClientProfileEnabled" `
        -Details @{
            AuditDisabled = $auditDisabled
            OAuth2ClientProfileEnabled = $oauth2ClientProfileEnabled
            Name = $orgConfig.Name
            DefaultGroupAccessType = $orgConfig.DefaultGroupAccessType
        }
}

# ── M365EXO-002: Anti-Spam Policies ────────────────────────────────────
function Test-InfiltrationM365EXO002 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiSpamPolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange anti-spam data not available'
    }

    $policies = $exo.AntiSpamPolicies
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No anti-spam policies found'
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($policies.Count) anti-spam (hosted content filter) policies configured" `
        -Details @{
            PolicyCount = $policies.Count
            Policies = @($policies | ForEach-Object {
                @{
                    Name = $_.Name
                    Identity = $_.Identity
                    IsDefault = $_.IsDefault
                    SpamAction = $_.SpamAction
                    HighConfidenceSpamAction = $_.HighConfidenceSpamAction
                    BulkSpamAction = $_.BulkSpamAction
                    BulkThreshold = $_.BulkThreshold
                }
            })
        }
}

# ── M365EXO-003: Anti-Phish Policies ───────────────────────────────────
function Test-InfiltrationM365EXO003 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiPhishPolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange anti-phish data not available'
    }

    $policies = $exo.AntiPhishPolicies
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No anti-phish policies found'
    }

    # Check if impersonation protection is enabled
    $impersonationEnabled = @($policies | Where-Object {
        $_.EnableTargetedUserProtection -eq $true -or
        $_.EnableTargetedDomainsProtection -eq $true -or
        $_.EnableMailboxIntelligenceProtection -eq $true
    })

    $status = if ($impersonationEnabled.Count -gt 0) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) anti-phish policies ($($impersonationEnabled.Count) with impersonation protection)" `
        -Details @{
            PolicyCount = $policies.Count
            ImpersonationProtectedCount = $impersonationEnabled.Count
            Policies = @($policies | ForEach-Object {
                @{
                    Name = $_.Name
                    Enabled = $_.Enabled
                    EnableTargetedUserProtection = $_.EnableTargetedUserProtection
                    EnableTargetedDomainsProtection = $_.EnableTargetedDomainsProtection
                    EnableMailboxIntelligenceProtection = $_.EnableMailboxIntelligenceProtection
                    PhishThresholdLevel = $_.PhishThresholdLevel
                }
            })
        }
}

# ── M365EXO-004: Malware Filter Policies ───────────────────────────────
function Test-InfiltrationM365EXO004 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange malware filter data not available'
    }

    $policies = $exo.MalwarePolicies
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No malware filter policies found'
    }

    # Check ZAP and common attachment filter
    $zapEnabled = @($policies | Where-Object { $_.ZapEnabled -eq $true })
    $fileFilterEnabled = @($policies | Where-Object { $_.EnableFileFilter -eq $true })

    $status = if ($zapEnabled.Count -eq $policies.Count) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) malware policies ($($zapEnabled.Count) ZAP enabled, $($fileFilterEnabled.Count) file filter enabled)" `
        -Details @{
            PolicyCount = $policies.Count
            ZapEnabledCount = $zapEnabled.Count
            FileFilterEnabledCount = $fileFilterEnabled.Count
            Policies = @($policies | ForEach-Object {
                @{
                    Name = $_.Name
                    ZapEnabled = $_.ZapEnabled
                    EnableFileFilter = $_.EnableFileFilter
                    EnableInternalSenderAdminNotifications = $_.EnableInternalSenderAdminNotifications
                }
            })
        }
}

# ── M365EXO-005: Safe Attachments Policies ─────────────────────────────
function Test-InfiltrationM365EXO005 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.SafeAttachmentPolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Safe Attachments data not available (requires Defender for Office 365)'
    }

    $policies = $exo.SafeAttachmentPolicies
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No Safe Attachment policies configured — Defender for Office 365 may not be licensed' `
            -Details @{ PolicyCount = 0 }
    }

    $dynamicDelivery = @($policies | Where-Object { $_.Action -eq 'DynamicDelivery' })
    $block = @($policies | Where-Object { $_.Action -eq 'Block' })
    $replace = @($policies | Where-Object { $_.Action -eq 'Replace' })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($policies.Count) Safe Attachment policies ($($dynamicDelivery.Count) dynamic delivery, $($block.Count) block)" `
        -Details @{
            PolicyCount = $policies.Count
            DynamicDelivery = $dynamicDelivery.Count
            Block = $block.Count
            Replace = $replace.Count
            Policies = @($policies | ForEach-Object {
                @{
                    Name = $_.Name
                    Action = $_.Action
                    Enable = $_.Enable
                    Redirect = $_.Redirect
                }
            })
        }
}

# ── M365EXO-006: Safe Links Policies ───────────────────────────────────
function Test-InfiltrationM365EXO006 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.SafeLinksPolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Safe Links data not available (requires Defender for Office 365)'
    }

    $policies = $exo.SafeLinksPolicies
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No Safe Links policies configured — Defender for Office 365 may not be licensed' `
            -Details @{ PolicyCount = 0 }
    }

    $urlTracking = @($policies | Where-Object { $_.EnableSafeLinksForEmail -eq $true -or $_.IsEnabled -eq $true })
    $scanUrls = @($policies | Where-Object { $_.ScanUrls -eq $true })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($policies.Count) Safe Links policies ($($urlTracking.Count) enabled for email, $($scanUrls.Count) scan URLs)" `
        -Details @{
            PolicyCount = $policies.Count
            EnabledForEmail = $urlTracking.Count
            ScanUrls = $scanUrls.Count
            Policies = @($policies | ForEach-Object {
                @{
                    Name = $_.Name
                    EnableSafeLinksForEmail = $_.EnableSafeLinksForEmail
                    ScanUrls = $_.ScanUrls
                    EnableForInternalSenders = $_.EnableForInternalSenders
                    DeliverMessageAfterScan = $_.DeliverMessageAfterScan
                    DisableUrlRewrite = $_.DisableUrlRewrite
                }
            })
        }
}

# ── M365EXO-007: Transport Rules ───────────────────────────────────────
function Test-InfiltrationM365EXO007 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.TransportRules) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange transport rule data not available'
    }

    $rules = $exo.TransportRules
    if ($rules.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'No mail flow (transport) rules configured' `
            -Details @{ RuleCount = 0 }
    }

    $enabled = @($rules | Where-Object { $_.State -eq 'Enabled' })
    $disabled = @($rules | Where-Object { $_.State -ne 'Enabled' })

    # Flag rules that redirect or forward mail externally
    $forwardingRules = @($rules | Where-Object {
        $_.RedirectMessageTo -or
        $_.BlindCopyTo -or
        $_.CopyTo -or
        $_.AddToRecipients
    })

    $status = if ($forwardingRules.Count -gt 0) { 'WARN' } else { 'PASS' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($rules.Count) transport rules ($($enabled.Count) enabled, $($forwardingRules.Count) with redirect/forward actions)" `
        -Details @{
            RuleCount = $rules.Count
            EnabledCount = $enabled.Count
            DisabledCount = $disabled.Count
            ForwardingRuleCount = $forwardingRules.Count
            Rules = @($rules | ForEach-Object {
                @{
                    Name = $_.Name
                    State = $_.State
                    Priority = $_.Priority
                    Mode = $_.Mode
                }
            })
        }
}

# ── M365EXO-008: Remote Domains ────────────────────────────────────────
function Test-InfiltrationM365EXO008 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.RemoteDomains) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange remote domain data not available'
    }

    $domains = $exo.RemoteDomains
    # Check default remote domain settings
    $defaultDomain = $domains | Where-Object { $_.DomainName -eq '*' } | Select-Object -First 1

    $autoForwardEnabled = $false
    if ($defaultDomain) {
        $autoForwardEnabled = $defaultDomain.AutoForwardEnabled -eq $true
    }

    $status = if ($autoForwardEnabled) { 'FAIL' } else { 'PASS' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Remote domains: $($domains.Count) configured. Default domain auto-forward: $autoForwardEnabled" `
        -Details @{
            DomainCount = $domains.Count
            DefaultAutoForward = $autoForwardEnabled
            Domains = @($domains | ForEach-Object {
                @{
                    DomainName = $_.DomainName
                    AutoForwardEnabled = $_.AutoForwardEnabled
                    AutoReplyEnabled = $_.AutoReplyEnabled
                    AllowedOOFType = $_.AllowedOOFType
                }
            })
        }
}

# ── M365EXO-009: DKIM Signing Configuration ────────────────────────────
function Test-InfiltrationM365EXO009 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DkimSigningConfig) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DKIM signing configuration data not available'
    }

    $configs = $exo.DkimSigningConfig
    if ($configs.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No DKIM signing configurations found'
    }

    $enabled = @($configs | Where-Object { $_.Enabled -eq $true })
    $disabled = @($configs | Where-Object { $_.Enabled -ne $true })

    $status = if ($disabled.Count -eq 0) { 'PASS' }
              elseif ($enabled.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($configs.Count) DKIM configs: $($enabled.Count) enabled, $($disabled.Count) disabled" `
        -Details @{
            TotalConfigs = $configs.Count
            EnabledCount = $enabled.Count
            DisabledCount = $disabled.Count
            Configs = @($configs | ForEach-Object {
                @{
                    Domain = $_.Domain
                    Enabled = $_.Enabled
                    Status = $_.Status
                    Selector1CNAME = $_.Selector1CNAME
                    Selector2CNAME = $_.Selector2CNAME
                    KeyCreationTime = $_.KeyCreationTime
                }
            })
        }
}

# ── M365EXO-010: CAS Mailbox Plans (Protocol Access) ──────────────────
function Test-InfiltrationM365EXO010 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.CASMailboxPlans) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'CAS mailbox plan data not available'
    }

    $plans = $exo.CASMailboxPlans
    if ($plans.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
            -CurrentValue 'No CAS mailbox plans found'
    }

    # Check for legacy protocols enabled
    $legacyEnabled = @($plans | Where-Object {
        $_.ImapEnabled -eq $true -or
        $_.PopEnabled -eq $true
    })

    $status = if ($legacyEnabled.Count -eq 0) { 'PASS' }
              elseif ($legacyEnabled.Count -le 1) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($plans.Count) CAS mailbox plans ($($legacyEnabled.Count) with IMAP/POP enabled)" `
        -Details @{
            PlanCount = $plans.Count
            LegacyProtocolEnabledCount = $legacyEnabled.Count
            Plans = @($plans | ForEach-Object {
                @{
                    Name = $_.Name
                    ImapEnabled = $_.ImapEnabled
                    PopEnabled = $_.PopEnabled
                    ActiveSyncEnabled = $_.ActiveSyncEnabled
                    OWAEnabled = $_.OWAEnabled
                    MAPIEnabled = $_.MAPIEnabled
                    EwsEnabled = $_.EwsEnabled
                }
            })
        }
}

# ── M365EXO-011: External Email Forwarding ─────────────────────────────
function Test-InfiltrationM365EXO011 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange Online data not available'
    }

    # Check transport rules for forwarding AND remote domain auto-forward
    $issues = [System.Collections.Generic.List[string]]::new()

    # Check remote domain auto-forward
    if ($exo.RemoteDomains) {
        $defaultDomain = $exo.RemoteDomains | Where-Object { $_.DomainName -eq '*' } | Select-Object -First 1
        if ($defaultDomain -and $defaultDomain.AutoForwardEnabled -eq $true) {
            $issues.Add('Default remote domain allows auto-forwarding')
        }
    }

    # Check transport rules for forwarding
    if ($exo.TransportRules) {
        $forwardRules = @($exo.TransportRules | Where-Object {
            $_.State -eq 'Enabled' -and
            ($_.RedirectMessageTo -or $_.BlindCopyTo)
        })
        if ($forwardRules.Count -gt 0) {
            $issues.Add("$($forwardRules.Count) active transport rules redirect/BCC mail")
        }
    }

    $status = if ($issues.Count -eq 0) { 'PASS' }
              elseif ($issues.Count -eq 1) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "External forwarding controls: $($issues.Count) issue(s). $(if ($issues.Count -gt 0) { $issues -join '; ' } else { 'All controls in place' })" `
        -Details @{
            IssueCount = $issues.Count
            Issues = @($issues)
        }
}

# ── M365EXO-012: Exchange Audit Configuration ──────────────────────────
function Test-InfiltrationM365EXO012 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.OrganizationConfig) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Exchange organization config not available'
    }

    $orgConfig = $exo.OrganizationConfig
    $auditDisabled = $orgConfig.AuditDisabled

    if ($auditDisabled -eq $true) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'Exchange mailbox auditing is DISABLED at the organization level' `
            -Details @{ AuditDisabled = $true }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue 'Exchange mailbox auditing is enabled at the organization level' `
        -Details @{
            AuditDisabled = $false
        }
}

# ════════════════════════════════════════════════════════════════════════
# SCuBA EXO baseline — dedicated per-control checks (MS.EXO.1.1 .. 17.3)
# Each reads $AuditData.M365Services.Exchange.<prop>. Honesty rule:
# return SKIP / Not Assessed when the underlying data is null/empty —
# never PASS on uncollectable data.
# ════════════════════════════════════════════════════════════════════════

# ── M365EXO-013: MS.EXO.1.1 — Auto-forwarding to external domains disabled ──
function Test-InfiltrationM365EXO013 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.RemoteDomains -or @($exo.RemoteDomains).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Remote domain data not available — Not Assessed'
    }

    $domains = @($exo.RemoteDomains)
    $autoFwd = @($domains | Where-Object { $_.AutoForwardEnabled -eq $true })

    $status = if ($autoFwd.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($domains.Count) remote domains; $($autoFwd.Count) permit auto-forwarding" `
        -Details @{
            DomainCount = $domains.Count
            AutoForwardEnabledDomains = @($autoFwd | ForEach-Object { $_.DomainName })
            Domains = @($domains | ForEach-Object {
                @{ DomainName = $_.DomainName; AutoForwardEnabled = $_.AutoForwardEnabled }
            })
        }
}

# ── M365EXO-014: MS.EXO.2.1 — Approved sending IP list maintained ──
function Test-InfiltrationM365EXO014 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # The approved-sender list is an organizational artifact reflected in SPF.
    # Agentless, we can only confirm SPF records exist; the maintained list
    # itself is a process control we cannot verify -> Not Assessed.
    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DomainMailSecurity -or @($exo.DomainMailSecurity).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Approved sender list is a process artifact not directly collectable; no SPF data to corroborate — Not Assessed'
    }

    $domains = @($exo.DomainMailSecurity)
    $withSpf = @($domains | Where-Object { $_.SPF -and $_.SPF.Record })

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue "Maintained approved-sender list cannot be verified agentlessly; $($withSpf.Count)/$($domains.Count) domains publish an SPF record (corroborating evidence only) — verify the approved IP list manually" `
        -Details @{
            DomainCount = $domains.Count
            DomainsWithSpf = $withSpf.Count
            Note = 'Process control: approved sending IP list maintained per domain. Confirm out-of-band.'
        }
}

# ── M365EXO-015: MS.EXO.2.2 — SPF published for each domain (DNS) ──
function Test-InfiltrationM365EXO015 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DomainMailSecurity -or @($exo.DomainMailSecurity).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DNS-based check — SPF records not resolved (no accepted-domain DNS data). Verify SPF (-all) for accepted domains — Not Assessed'
    }

    $domains = @($exo.DomainMailSecurity)
    $valid = @($domains | Where-Object { $_.SPF -and $_.SPF.Valid -eq $true })
    $missing = @($domains | Where-Object { -not ($_.SPF -and $_.SPF.Record) })

    $status = if ($domains.Count -gt 0 -and $valid.Count -eq $domains.Count) { 'PASS' }
              elseif ($valid.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($valid.Count)/$($domains.Count) domains publish a valid SPF policy; $($missing.Count) missing SPF" `
        -Details @{
            DomainCount = $domains.Count
            ValidSpfCount = $valid.Count
            MissingSpfDomains = @($missing | ForEach-Object { $_.Domain })
            Domains = @($domains | ForEach-Object {
                @{ Domain = $_.Domain; SpfValid = $_.SPF.Valid; SpfRecord = $_.SPF.Record; Details = $_.SPF.Details }
            })
        }
}

# ── M365EXO-016: MS.EXO.3.1 — DKIM enabled for all domains ──
function Test-InfiltrationM365EXO016 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DkimSigningConfig -or @($exo.DkimSigningConfig).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DKIM signing configuration not available — Not Assessed'
    }

    $configs = @($exo.DkimSigningConfig)
    $enabled = @($configs | Where-Object { $_.Enabled -eq $true })
    $disabled = @($configs | Where-Object { $_.Enabled -ne $true })

    $status = if ($disabled.Count -eq 0) { 'PASS' }
              elseif ($enabled.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($enabled.Count)/$($configs.Count) domains have DKIM signing enabled" `
        -Details @{
            TotalConfigs = $configs.Count
            EnabledCount = $enabled.Count
            DisabledDomains = @($disabled | ForEach-Object { $_.Domain })
        }
}

# ── M365EXO-017: MS.EXO.4.1 — DMARC published for each domain (DNS) ──
function Test-InfiltrationM365EXO017 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DomainMailSecurity -or @($exo.DomainMailSecurity).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DNS-based check — DMARC records not resolved. Verify _dmarc TXT for accepted domains — Not Assessed'
    }

    $domains = @($exo.DomainMailSecurity)
    $valid = @($domains | Where-Object { $_.DMARC -and $_.DMARC.Valid -eq $true })
    $missing = @($domains | Where-Object { -not ($_.DMARC -and $_.DMARC.Record) })

    $status = if ($domains.Count -gt 0 -and $valid.Count -eq $domains.Count) { 'PASS' }
              elseif ($valid.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($valid.Count)/$($domains.Count) domains publish a DMARC record; $($missing.Count) missing DMARC" `
        -Details @{
            DomainCount = $domains.Count
            ValidDmarcCount = $valid.Count
            MissingDmarcDomains = @($missing | ForEach-Object { $_.Domain })
            Domains = @($domains | ForEach-Object {
                @{ Domain = $_.Domain; DmarcValid = $_.DMARC.Valid; Policy = $_.DMARC.Policy; Record = $_.DMARC.Record }
            })
        }
}

# ── M365EXO-018: MS.EXO.4.2 — DMARC p=reject (DNS) ──
function Test-InfiltrationM365EXO018 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DomainMailSecurity -or @($exo.DomainMailSecurity).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DNS-based check — DMARC policy not resolved. Verify p=reject for accepted domains — Not Assessed'
    }

    $domains = @($exo.DomainMailSecurity | Where-Object { $_.DMARC -and $_.DMARC.Record })
    if ($domains.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No DMARC records found to evaluate enforcement policy' `
            -Details @{ DomainsWithDmarc = 0 }
    }

    $reject = @($domains | Where-Object { $_.DMARC.Policy -eq 'reject' })

    $status = if ($reject.Count -eq $domains.Count) { 'PASS' }
              elseif ($reject.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($reject.Count)/$($domains.Count) DMARC-enabled domains use p=reject" `
        -Details @{
            DomainsWithDmarc = $domains.Count
            RejectCount = $reject.Count
            Policies = @($domains | ForEach-Object { @{ Domain = $_.Domain; Policy = $_.DMARC.Policy } })
        }
}

# ── M365EXO-019: MS.EXO.4.3 — DMARC aggregate report contact (DNS) ──
function Test-InfiltrationM365EXO019 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.DomainMailSecurity -or @($exo.DomainMailSecurity).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DNS-based check — DMARC RUA not resolved. Verify rua= aggregate report destination for accepted domains — Not Assessed'
    }

    $domains = @($exo.DomainMailSecurity | Where-Object { $_.DMARC -and $_.DMARC.Record })
    if ($domains.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No DMARC records found to evaluate aggregate report (rua) contact' `
            -Details @{ DomainsWithDmarc = 0 }
    }

    $withRua = @($domains | Where-Object { $_.DMARC.Record -match 'rua=' })

    $status = if ($withRua.Count -eq $domains.Count) { 'PASS' }
              elseif ($withRua.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($withRua.Count)/$($domains.Count) DMARC records specify an aggregate report (rua) destination" `
        -Details @{
            DomainsWithDmarc = $domains.Count
            WithRuaCount = $withRua.Count
            Note = 'Federal executive-branch agencies must include reports@dmarc.cyber.dhs.gov in RUA.'
            Domains = @($domains | ForEach-Object { @{ Domain = $_.Domain; Record = $_.DMARC.Record } })
        }
}

# ── M365EXO-020: MS.EXO.5.1 — SMTP AUTH disabled org-wide ──
function Test-InfiltrationM365EXO020 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.TransportConfig) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Transport configuration not available — Not Assessed'
    }

    $disabled = $exo.TransportConfig.SmtpClientAuthenticationDisabled

    if ($null -eq $disabled) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'SmtpClientAuthenticationDisabled not present on transport config — Not Assessed'
    }

    $status = if ($disabled -eq $true) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "SMTP AUTH org-wide default disabled: $disabled" `
        -Details @{ SmtpClientAuthenticationDisabled = $disabled }
}

# ── M365EXO-021: MS.EXO.6.1 — Contact sharing not open to all domains ──
function Test-InfiltrationM365EXO021 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or $null -eq $exo.SharingPolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Sharing policy data not available — Not Assessed'
    }

    $policies = @($exo.SharingPolicies)
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'No sharing policies defined — contacts not shared externally by policy' `
            -Details @{ PolicyCount = 0 }
    }

    # A wildcard-domain contact-sharing rule looks like "*:ContactsSharing" in Domains
    $offenders = [System.Collections.Generic.List[string]]::new()
    foreach ($p in $policies) {
        foreach ($d in @($p.Domains)) {
            $dStr = "$d"
            if ($dStr -match '^\*' -and $dStr -match 'Contact') {
                $offenders.Add("$($p.Name): $dStr")
            }
        }
    }

    $status = if ($offenders.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) sharing policies; $($offenders.Count) share contacts with all domains (*)" `
        -Details @{
            PolicyCount = $policies.Count
            WildcardContactSharing = @($offenders)
            Policies = @($policies | ForEach-Object { @{ Name = $_.Name; Default = $_.Default; Domains = @($_.Domains | ForEach-Object { "$_" }) } })
        }
}

# ── M365EXO-022: MS.EXO.6.2 — Calendar sharing not open to all domains ──
function Test-InfiltrationM365EXO022 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or $null -eq $exo.SharingPolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Sharing policy data not available — Not Assessed'
    }

    $policies = @($exo.SharingPolicies)
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
            -CurrentValue 'No sharing policies defined — calendar details not shared externally by policy' `
            -Details @{ PolicyCount = 0 }
    }

    $offenders = [System.Collections.Generic.List[string]]::new()
    foreach ($p in $policies) {
        foreach ($d in @($p.Domains)) {
            $dStr = "$d"
            if ($dStr -match '^\*' -and $dStr -match 'Calendar') {
                $offenders.Add("$($p.Name): $dStr")
            }
        }
    }

    $status = if ($offenders.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) sharing policies; $($offenders.Count) share calendar details with all domains (*)" `
        -Details @{
            PolicyCount = $policies.Count
            WildcardCalendarSharing = @($offenders)
        }
}

# ── M365EXO-023: MS.EXO.7.1 — External sender warning implemented ──
function Test-InfiltrationM365EXO023 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    $haveTag = $null -ne $exo.ExternalInOutlook
    $haveRules = $null -ne $exo.TransportRules
    if (-not $exo -or (-not $haveTag -and -not $haveRules)) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Neither external-tag nor transport-rule data available — Not Assessed'
    }

    # Native external sender tag
    $tagEnabled = $false
    if ($haveTag) {
        $tagEnabled = @($exo.ExternalInOutlook | Where-Object { $_.Enabled -eq $true }).Count -gt 0
    }

    # Mail flow rule that prepends an external marker to the subject
    $ruleMarker = $false
    if ($haveRules) {
        $ruleMarker = @($exo.TransportRules | Where-Object {
            $_.State -eq 'Enabled' -and
            ($_.PrependSubject -or $_.ApplyHtmlDisclaimerText) -and
            ($_.FromScope -eq 'NotInOrganization' -or "$($_.SenderAddressLocation)" -match 'Header')
        }).Count -gt 0
    }

    $status = if ($tagEnabled -or $ruleMarker) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "External sender warning: native tag=$tagEnabled, mail-flow-rule marker=$ruleMarker" `
        -Details @{
            NativeExternalTagEnabled = $tagEnabled
            SubjectMarkerRulePresent = $ruleMarker
        }
}

# ── M365EXO-024: MS.EXO.8.1 — DLP solution in use ──
function Test-InfiltrationM365EXO024 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or $null -eq $exo.DlpCompliancePolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DLP policy data not available (Purview/IPPSSession may not be connected). A comparable third-party DLP cannot be detected agentlessly — Not Assessed'
    }

    $policies = @($exo.DlpCompliancePolicies)
    # Policies covering Exchange workload and enabled
    $exoEnabled = @($policies | Where-Object {
        $_.Enabled -eq $true -and (
            $_.Mode -match 'Enforce' -or $_.Mode -eq $null
        ) -and (
            -not $_.ExchangeLocation -or @($_.ExchangeLocation).Count -gt 0
        )
    })

    $status = if ($policies.Count -gt 0 -and $exoEnabled.Count -gt 0) { 'PASS' }
              elseif ($policies.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) DLP policies; $($exoEnabled.Count) enabled and scoped to Exchange" `
        -Details @{
            PolicyCount = $policies.Count
            ExchangeEnabledCount = $exoEnabled.Count
            Policies = @($policies | ForEach-Object { @{ Name = $_.Name; Enabled = $_.Enabled; Mode = $_.Mode } })
        }
}

# ── M365EXO-025: MS.EXO.8.2 — DLP protects PII (CCN/TIN/SSN) ──
function Test-InfiltrationM365EXO025 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or $null -eq $exo.DlpCompliancePolicies) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'DLP policy data not available; sensitive-info-type coverage cannot be confirmed agentlessly — Not Assessed'
    }

    $policies = @($exo.DlpCompliancePolicies)
    if ($policies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No DLP policies present — PII (credit card/TIN/SSN) restriction not in place' `
            -Details @{ PolicyCount = 0 }
    }

    # Sensitive information types live in DLP rules (Get-DlpComplianceRule), not in the
    # policy object. Without rule-level data we cannot confirm CCN/TIN/SSN coverage.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' `
        -CurrentValue "$($policies.Count) DLP policies present; sensitive-info-type coverage (credit card/TIN/SSN) requires rule-level inspection — verify CCN/TIN/SSN rules manually" `
        -Details @{
            PolicyCount = $policies.Count
            Note = 'Confirm DLP rules include Credit Card Number, U.S. Taxpayer Identification Number, and U.S. SSN sensitive info types with a block/restrict action.'
        }
}

# ── M365EXO-026: MS.EXO.9.1 — Email filtered by attachment file type ──
function Test-InfiltrationM365EXO026 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies -or @($exo.MalwarePolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Malware filter policy data not available — Not Assessed'
    }

    $policies = @($exo.MalwarePolicies)
    $fileFilter = @($policies | Where-Object { $_.EnableFileFilter -eq $true })

    $status = if ($fileFilter.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($fileFilter.Count)/$($policies.Count) malware policies have the common attachment filter enabled" `
        -Details @{
            PolicyCount = $policies.Count
            FileFilterEnabledCount = $fileFilter.Count
        }
}

# ── M365EXO-027: MS.EXO.9.2 — Attachment filter assesses true file type ──
function Test-InfiltrationM365EXO027 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies -or @($exo.MalwarePolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Malware filter policy data not available — Not Assessed'
    }

    $policies = @($exo.MalwarePolicies)
    # The common attachment filter performs true-type detection when enabled.
    $fileFilter = @($policies | Where-Object { $_.EnableFileFilter -eq $true })

    if ($fileFilter.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'Common attachment filter disabled — true-file-type assessment not active' `
            -Details @{ FileFilterEnabledCount = 0 }
    }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' `
        -CurrentValue "$($fileFilter.Count)/$($policies.Count) policies use the common attachment filter (true-type detection)" `
        -Details @{
            PolicyCount = $policies.Count
            FileFilterEnabledCount = $fileFilter.Count
            Note = 'Common attachment filter inspects true file type, not just the extension.'
        }
}

# ── M365EXO-028: MS.EXO.9.3 — Disallowed file types set ──
function Test-InfiltrationM365EXO028 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies -or @($exo.MalwarePolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Malware filter policy data not available — Not Assessed'
    }

    $policies = @($exo.MalwarePolicies)
    $clickToRun = @('exe','cmd','vbe','vbs','js','ps1','bat')

    # Evaluate policies that have the filter enabled and a populated block list
    $enabledPolicies = @($policies | Where-Object { $_.EnableFileFilter -eq $true })
    if ($enabledPolicies.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'Common attachment filter disabled — no disallowed file types in effect' `
            -Details @{ FileFilterEnabledCount = 0 }
    }

    $weak = [System.Collections.Generic.List[string]]::new()
    foreach ($p in $enabledPolicies) {
        $types = @($p.FileTypes | ForEach-Object { "$_".ToLower() })
        if ($types.Count -eq 0) {
            $weak.Add("$($p.Name): block list empty")
        } else {
            $missing = @($clickToRun | Where-Object { $_ -notin $types })
            if ($missing.Count -gt 0) {
                $weak.Add("$($p.Name): missing $($missing -join ',')")
            }
        }
    }

    $status = if ($weak.Count -eq 0) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($enabledPolicies.Count) filter-enabled policies; $($weak.Count) miss core click-to-run types" `
        -Details @{
            FilterEnabledCount = $enabledPolicies.Count
            WeakPolicies = @($weak)
            RequiredMinimum = $clickToRun
        }
}

# ── M365EXO-029: MS.EXO.10.1 — Emails scanned for malware ──
function Test-InfiltrationM365EXO029 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies -or @($exo.MalwarePolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Malware filter policy data not available — Not Assessed'
    }

    $policies = @($exo.MalwarePolicies)
    # Presence of an anti-malware policy means inbound mail is scanned.
    $status = if ($policies.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) anti-malware policies active — inbound mail is scanned for malware" `
        -Details @{ PolicyCount = $policies.Count }
}

# ── M365EXO-030: MS.EXO.10.2 — Malware emails quarantined/dropped ──
function Test-InfiltrationM365EXO030 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies -or @($exo.MalwarePolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Malware filter policy data not available — Not Assessed'
    }

    $policies = @($exo.MalwarePolicies)
    # Anti-malware always quarantines positive detections; flag any policy that
    # is explicitly weakened (filter disabled would still quarantine, so this
    # is informational). Pass when policies exist and admin notifications align.
    $status = if ($policies.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) anti-malware policies — malware-positive messages are quarantined" `
        -Details @{
            PolicyCount = $policies.Count
            Note = 'Exchange Online anti-malware quarantines malware detections by default; verify no custom routing weakens this.'
        }
}

# ── M365EXO-031: MS.EXO.10.3 — Post-delivery (ZAP) scanning ──
function Test-InfiltrationM365EXO031 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.MalwarePolicies -or @($exo.MalwarePolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Malware filter policy data not available — Not Assessed'
    }

    $policies = @($exo.MalwarePolicies)
    $zap = @($policies | Where-Object { $_.ZapEnabled -eq $true })

    $status = if ($zap.Count -eq $policies.Count) { 'PASS' }
              elseif ($zap.Count -gt 0) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($zap.Count)/$($policies.Count) anti-malware policies have zero-hour auto purge (ZAP) enabled" `
        -Details @{
            PolicyCount = $policies.Count
            ZapEnabledCount = $zap.Count
        }
}

# ── M365EXO-032: MS.EXO.11.1 — Impersonation protection ──
function Test-InfiltrationM365EXO032 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiPhishPolicies -or @($exo.AntiPhishPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Anti-phish policy data not available (requires Defender for Office 365) — Not Assessed'
    }

    $policies = @($exo.AntiPhishPolicies)
    $impersonation = @($policies | Where-Object {
        $_.EnableTargetedUserProtection -eq $true -or
        $_.EnableTargetedDomainsProtection -eq $true -or
        $_.EnableOrganizationDomainsProtection -eq $true
    })

    $status = if ($impersonation.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($impersonation.Count)/$($policies.Count) anti-phish policies enable user/domain impersonation protection" `
        -Details @{
            PolicyCount = $policies.Count
            ImpersonationEnabledCount = $impersonation.Count
        }
}

# ── M365EXO-033: MS.EXO.11.2 — User safety tips/warnings ──
function Test-InfiltrationM365EXO033 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiPhishPolicies -or @($exo.AntiPhishPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Anti-phish policy data not available — Not Assessed'
    }

    $policies = @($exo.AntiPhishPolicies)
    $tips = @($policies | Where-Object {
        $_.EnableFirstContactSafetyTips -eq $true -or
        $_.EnableSimilarUsersSafetyTips -eq $true -or
        $_.EnableSimilarDomainsSafetyTips -eq $true -or
        $_.EnableSpoofIntelligence -eq $true
    })

    $status = if ($tips.Count -gt 0) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($tips.Count)/$($policies.Count) anti-phish policies enable user safety tips/spoof intelligence" `
        -Details @{
            PolicyCount = $policies.Count
            SafetyTipsEnabledCount = $tips.Count
        }
}

# ── M365EXO-034: MS.EXO.11.3 — AI-based (mailbox intelligence) detection ──
function Test-InfiltrationM365EXO034 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiPhishPolicies -or @($exo.AntiPhishPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Anti-phish policy data not available (mailbox intelligence requires Defender for Office 365) — Not Assessed'
    }

    $policies = @($exo.AntiPhishPolicies)
    $mbi = @($policies | Where-Object { $_.EnableMailboxIntelligence -eq $true })

    $status = if ($mbi.Count -gt 0) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($mbi.Count)/$($policies.Count) anti-phish policies enable mailbox intelligence (AI-based detection)" `
        -Details @{
            PolicyCount = $policies.Count
            MailboxIntelligenceEnabledCount = $mbi.Count
        }
}

# ── M365EXO-035: MS.EXO.12.1 — No IP allow list in connection filter ──
function Test-InfiltrationM365EXO035 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.ConnectionFilterPolicies -or @($exo.ConnectionFilterPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Connection filter policy data not available — Not Assessed'
    }

    $policies = @($exo.ConnectionFilterPolicies)
    $withAllow = @($policies | Where-Object { @($_.IPAllowList).Count -gt 0 })

    $status = if ($withAllow.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($withAllow.Count)/$($policies.Count) connection filter policies have a non-empty IP allow list" `
        -Details @{
            PolicyCount = $policies.Count
            PoliciesWithAllowList = @($withAllow | ForEach-Object { @{ Name = $_.Name; IPAllowList = @($_.IPAllowList) } })
        }
}

# ── M365EXO-036: MS.EXO.12.2 — Connection filter safe list disabled ──
function Test-InfiltrationM365EXO036 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.ConnectionFilterPolicies -or @($exo.ConnectionFilterPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Connection filter policy data not available — Not Assessed'
    }

    $policies = @($exo.ConnectionFilterPolicies)
    $safeOn = @($policies | Where-Object { $_.EnableSafeList -eq $true })

    $status = if ($safeOn.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($safeOn.Count)/$($policies.Count) connection filter policies have the safe list enabled" `
        -Details @{
            PolicyCount = $policies.Count
            PoliciesWithSafeList = @($safeOn | ForEach-Object { $_.Name })
        }
}

# ── M365EXO-037: MS.EXO.13.1 — Mailbox auditing enabled ──
function Test-InfiltrationM365EXO037 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.OrganizationConfig) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Organization configuration not available — Not Assessed'
    }

    $auditDisabled = $exo.OrganizationConfig.AuditDisabled

    if ($null -eq $auditDisabled) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'AuditDisabled not present on organization config — Not Assessed'
    }

    $status = if ($auditDisabled -eq $false) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Mailbox auditing enabled org-wide (AuditDisabled=$auditDisabled)" `
        -Details @{ AuditDisabled = $auditDisabled }
}

# ── M365EXO-038: MS.EXO.14.1 — Inbound spam filter enabled ──
function Test-InfiltrationM365EXO038 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiSpamPolicies -or @($exo.AntiSpamPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Anti-spam (hosted content filter) policy data not available — Not Assessed'
    }

    $policies = @($exo.AntiSpamPolicies)
    $status = if ($policies.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($policies.Count) inbound anti-spam policies active" `
        -Details @{ PolicyCount = $policies.Count }
}

# ── M365EXO-039: MS.EXO.14.2 — Spam routed to junk or quarantine ──
function Test-InfiltrationM365EXO039 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiSpamPolicies -or @($exo.AntiSpamPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Anti-spam policy data not available — Not Assessed'
    }

    $policies = @($exo.AntiSpamPolicies)
    $good = @('MoveToJmf','Quarantine','Redirect')
    $bad = @($policies | Where-Object {
        ($_.SpamAction -and $_.SpamAction -notin $good) -or
        ($_.HighConfidenceSpamAction -and $_.HighConfidenceSpamAction -notin $good)
    })

    $status = if ($bad.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($bad.Count)/$($policies.Count) anti-spam policies deliver spam to the inbox instead of junk/quarantine" `
        -Details @{
            PolicyCount = $policies.Count
            NonCompliant = @($bad | ForEach-Object { @{ Name = $_.Name; SpamAction = $_.SpamAction; HighConfidenceSpamAction = $_.HighConfidenceSpamAction } })
        }
}

# ── M365EXO-040: MS.EXO.14.3 — No allowed domains in anti-spam policy ──
function Test-InfiltrationM365EXO040 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.AntiSpamPolicies -or @($exo.AntiSpamPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Anti-spam policy data not available — Not Assessed'
    }

    $policies = @($exo.AntiSpamPolicies)
    $withAllowedDomains = @($policies | Where-Object { @($_.AllowedSenderDomains).Count -gt 0 })

    $status = if ($withAllowedDomains.Count -eq 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($withAllowedDomains.Count)/$($policies.Count) anti-spam policies contain allowed sender domains" `
        -Details @{
            PolicyCount = $policies.Count
            PoliciesWithAllowedDomains = @($withAllowedDomains | ForEach-Object {
                @{ Name = $_.Name; AllowedSenderDomains = @($_.AllowedSenderDomains | ForEach-Object { "$_" }) }
            })
        }
}

# ── M365EXO-041: MS.EXO.15.1 — URL block-list comparison (Safe Links) ──
function Test-InfiltrationM365EXO041 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.SafeLinksPolicies -or @($exo.SafeLinksPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Safe Links data not available (requires Defender for Office 365). A comparable third-party solution cannot be detected agentlessly — Not Assessed'
    }

    $policies = @($exo.SafeLinksPolicies)
    $email = @($policies | Where-Object { $_.EnableSafeLinksForEmail -eq $true -or $_.IsEnabled -eq $true })

    $status = if ($email.Count -gt 0) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($email.Count)/$($policies.Count) Safe Links policies enable URL scanning for email" `
        -Details @{
            PolicyCount = $policies.Count
            EnabledForEmailCount = $email.Count
        }
}

# ── M365EXO-042: MS.EXO.15.2 — Direct download links scanned ──
function Test-InfiltrationM365EXO042 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.SafeLinksPolicies -or @($exo.SafeLinksPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Safe Links data not available — Not Assessed'
    }

    $policies = @($exo.SafeLinksPolicies)
    $scan = @($policies | Where-Object { $_.ScanUrls -eq $true })

    $status = if ($scan.Count -gt 0) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($scan.Count)/$($policies.Count) Safe Links policies scan links/direct downloads in real time" `
        -Details @{
            PolicyCount = $policies.Count
            ScanUrlsCount = $scan.Count
        }
}

# ── M365EXO-043: MS.EXO.15.3 — User click tracking enabled ──
function Test-InfiltrationM365EXO043 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or -not $exo.SafeLinksPolicies -or @($exo.SafeLinksPolicies).Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Safe Links data not available — Not Assessed'
    }

    $policies = @($exo.SafeLinksPolicies)
    # Click tracking on means DoNotTrackUserClicks is False.
    $tracked = @($policies | Where-Object { $_.DoNotTrackUserClicks -eq $false })

    $status = if ($tracked.Count -gt 0) { 'PASS' } else { 'WARN' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($tracked.Count)/$($policies.Count) Safe Links policies enable user click tracking" `
        -Details @{
            PolicyCount = $policies.Count
            ClickTrackingEnabledCount = $tracked.Count
        }
}

# ── M365EXO-044: MS.EXO.16.1 — Required EXO alerts enabled ──
function Test-InfiltrationM365EXO044 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or $null -eq $exo.ProtectionAlerts) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Protection alert policy data not available — Not Assessed'
    }

    $alerts = @($exo.ProtectionAlerts)
    if ($alerts.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' `
            -CurrentValue 'No alert policies present — required EXO alerts not enabled' `
            -Details @{ AlertCount = 0 }
    }

    $required = @(
        'Suspicious email sending patterns detected',
        'Suspicious Connector Activity',
        'Suspicious Email Forwarding Activity',
        'Messages have been delayed',
        'Tenant restricted from sending unprovisioned email',
        'Tenant restricted from sending email',
        'A potentially malicious URL click was detected'
    )

    $enabledNames = @($alerts | Where-Object { $_.Disabled -ne $true } | ForEach-Object { "$($_.Name)" })
    $missing = [System.Collections.Generic.List[string]]::new()
    foreach ($r in $required) {
        $hit = $enabledNames | Where-Object { $_ -like "*$r*" -or $r -like "*$_*" }
        if (-not $hit) { $missing.Add($r) }
    }

    $status = if ($missing.Count -eq 0) { 'PASS' }
              elseif ($missing.Count -lt $required.Count) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($required.Count - $missing.Count)/$($required.Count) required EXO alerts enabled" `
        -Details @{
            TotalAlertPolicies = $alerts.Count
            MissingRequiredAlerts = @($missing)
        }
}

# ── M365EXO-045: MS.EXO.16.2 — Alerts routed to monitored destination ──
function Test-InfiltrationM365EXO045 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    if (-not $exo -or $null -eq $exo.ProtectionAlerts) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Protection alert policy data not available — Not Assessed'
    }

    $alerts = @($exo.ProtectionAlerts | Where-Object { $_.Disabled -ne $true })
    if ($alerts.Count -eq 0) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'No enabled alert policies to evaluate notification routing — Not Assessed'
    }

    $noRecipient = @($alerts | Where-Object {
        $_.NotifyUser -eq $null -or @($_.NotifyUser).Count -eq 0
    })

    $status = if ($noRecipient.Count -eq 0) { 'PASS' }
              elseif ($noRecipient.Count -lt $alerts.Count) { 'WARN' }
              else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "$($alerts.Count - $noRecipient.Count)/$($alerts.Count) enabled alert policies notify a recipient (SIEM ingestion not detectable agentlessly)" `
        -Details @{
            EnabledAlertCount = $alerts.Count
            AlertsWithoutRecipient = @($noRecipient | ForEach-Object { "$($_.Name)" })
            Note = 'SIEM-based ingestion satisfies this control but cannot be confirmed from EXO config.'
        }
}

# ── M365EXO-046: MS.EXO.17.1 — Purview Audit (Standard) / UAL enabled ──
function Test-InfiltrationM365EXO046 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    $exo = $AuditData.M365Services.Exchange
    $cfg = $exo.AdminAuditLogConfig
    if (-not $exo -or -not $cfg) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'Admin audit log configuration not available — Not Assessed'
    }

    $ual = $cfg.UnifiedAuditLogIngestionEnabled
    if ($null -eq $ual) {
        return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
            -CurrentValue 'UnifiedAuditLogIngestionEnabled not present — Not Assessed'
    }

    $status = if ($ual -eq $true) { 'PASS' } else { 'FAIL' }

    return New-AuditFinding -CheckDefinition $CheckDefinition -Status $status `
        -CurrentValue "Unified audit log ingestion enabled: $ual" `
        -Details @{ UnifiedAuditLogIngestionEnabled = $ual }
}

# ── M365EXO-047: MS.EXO.17.2 — Purview Audit (Premium) enabled ──
function Test-InfiltrationM365EXO047 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Premium audit status (e.g. MailItemsAccessed event availability) is not
    # exposed by Get-AdminAuditLogConfig and depends on E5/G5 licensing, which
    # is not collected in this agentless EXO model -> Not Assessed.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
        -CurrentValue 'Purview Audit (Premium) status and E5/G5 licensing are not exposed via EXO config — Not Assessed. Verify Premium audit event types (e.g. MailItemsAccessed) and licensing manually.' `
        -Details @{
            Note = 'Premium audit features and required licensing cannot be confirmed agentlessly.'
        }
}

# ── M365EXO-048: MS.EXO.17.3 — Audit log retention >= OMB minimum ──
function Test-InfiltrationM365EXO048 {
    [CmdletBinding()]
    param([hashtable]$AuditData, [hashtable]$CheckDefinition)

    # Audit log retention policies are managed in Purview (Get-UnifiedAuditLogRetentionPolicy)
    # which is not collected in this EXO model. Retention duration therefore cannot be
    # confirmed -> Not Assessed.
    return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' `
        -CurrentValue 'Audit log retention policy (>=12 months per OMB M-21-31) is managed in Purview and not collected here — Not Assessed. Verify a retention policy of at least 12 months for unified audit logs.' `
        -Details @{
            Note = 'Custom audit log retention requires E5/G5 or add-on licensing and Purview inspection; not confirmable agentlessly.'
        }
}