Checks/EXO/MET-EXO020-ConnectionFilterPolicy.ps1

try {
    $policies = @(Get-HostedConnectionFilterPolicy -ErrorAction Stop)
}
catch {
    New-METCheckResult -CheckId 'MET-EXO020' -Category EXO -Name 'Connection Filter Policy Hygiene' `
        -Result Fail -Severity High -AffectedObject 'Connection Filter Policies' `
        -Finding 'Unable to retrieve connection filter policies' `
        -Recommendation 'Ensure the account has Security Reader or Exchange View-Only Configuration or higher permissions.' `
        -ReferenceUrl 'https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure' -ErrorMessage $_.ToString()
    return
}

if ($policies.Count -eq 0) {
    New-METCheckResult -CheckId 'MET-EXO020' -Category EXO -Name 'Connection Filter Policy Hygiene' `
        -Result Info -Severity High -AffectedObject 'Connection Filter Policies' `
        -Finding 'No connection filter policies were returned by the tenant' `
        -ReferenceUrl 'https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure'
    return
}

foreach ($policy in $policies) {
    $policyName = [string]$policy.Name
    if ([string]::IsNullOrWhiteSpace($policyName)) {
        $policyName = 'Unnamed connection filter policy'
    }

    $affectedObject = $policyName
    if ($policy.IsDefault -eq $true) {
        $affectedObject = "$policyName (default)"
    }

    $allowList = @(@($policy.IPAllowList) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
    $blockList = @(@($policy.IPBlockList) | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) })
    $safeListEnabled = ($policy.EnableSafeList -eq $true)

    $broadEntries = [System.Collections.Generic.List[string]]::new()
    foreach ($entry in $allowList) {
        $entryText = ([string]$entry).Trim()
        $parts = $entryText.Split('/')
        if ($parts.Count -ne 2) { continue }

        [System.Net.IPAddress] $parsedAddress = $null
        if (-not [System.Net.IPAddress]::TryParse($parts[0], [ref] $parsedAddress)) { continue }
        if ($parsedAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) { continue }

        [int] $prefixLength = 0
        if (-not [int]::TryParse($parts[1], [ref] $prefixLength)) { continue }
        if ($prefixLength -lt 0 -or $prefixLength -gt 32) { continue }

        if ($prefixLength -lt 24) {
            $broadEntries.Add($entryText)
        }
    }

    $issues = [System.Collections.Generic.List[string]]::new()

    if ($allowList.Count -gt 0) {
        $shown = @($allowList | Select-Object -First 10) -join ', '
        if ($allowList.Count -gt 10) {
            $shown = "$shown (+$($allowList.Count - 10) more)"
        }
        $issues.Add("The IP allow list contains $($allowList.Count) entr$(if ($allowList.Count -eq 1) { 'y' } else { 'ies' }): $shown - mail arriving from a listed IP skips spam filtering and spoof intelligence entirely, so an attacker able to relay through any listed host inherits a trusted path into every mailbox")
    }

    if ($broadEntries.Count -gt 0) {
        $issues.Add("$($broadEntries.Count) of those allow-list entries are broad CIDR ranges shorter than /24: $($broadEntries -join ', ') - a single range of this size covers hundreds or thousands of hosts the tenant does not own or control, and every one of them inherits the same filtering bypass")
    }

    if ($safeListEnabled) {
        $issues.Add('The third-party safe list (EnableSafeList) is enabled - it is an externally sourced allow list that cannot be enumerated or audited from PowerShell, so the set of senders currently bypassing filtering through it cannot be reviewed')
    }

    $recommendation = "Empty the allow list with: Set-HostedConnectionFilterPolicy -Identity '$policyName' -IPAllowList @(). Disable the safe list with: Set-HostedConnectionFilterPolicy -Identity '$policyName' -EnableSafeList `$false. Re-home genuinely trusted senders onto an authenticated inbound connector with a certificate or IP-plus-TLS binding (assessed by MET-EXO011) so the sender is authenticated rather than merely allow-listed."

    if ($allowList.Count -gt 0) {
        New-METCheckResult -CheckId 'MET-EXO020' -Category EXO -Name 'Connection Filter Policy Hygiene' `
            -Result Fail -Severity High -AffectedObject $affectedObject `
            -Finding ($issues -join '; ') `
            -Recommendation $recommendation `
            -ReferenceUrl 'https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure' `
            -Metadata @{ IPAllowListCount = $allowList.Count; BroadAllowEntryCount = $broadEntries.Count; IPBlockListCount = $blockList.Count; EnableSafeList = $safeListEnabled }
    }
    elseif ($safeListEnabled) {
        New-METCheckResult -CheckId 'MET-EXO020' -Category EXO -Name 'Connection Filter Policy Hygiene' `
            -Result Warning -Severity High -AffectedObject $affectedObject `
            -Finding ($issues -join '; ') `
            -Recommendation $recommendation `
            -ReferenceUrl 'https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure' `
            -Metadata @{ IPAllowListCount = 0; BroadAllowEntryCount = 0; IPBlockListCount = $blockList.Count; EnableSafeList = $true }
    }
    else {
        New-METCheckResult -CheckId 'MET-EXO020' -Category EXO -Name 'Connection Filter Policy Hygiene' `
            -Result Pass -Severity High -AffectedObject $affectedObject `
            -Finding "The IP allow list is empty and the third-party safe list is disabled, so no sender bypasses spam filtering and spoof intelligence through this policy (IP block list entries: $($blockList.Count))" `
            -ReferenceUrl 'https://learn.microsoft.com/en-us/defender-office-365/connection-filter-policies-configure' `
            -Metadata @{ IPAllowListCount = 0; BroadAllowEntryCount = 0; IPBlockListCount = $blockList.Count; EnableSafeList = $false }
    }
}