public/cisa/exchange/Test-MtCisaSpfRestriction.ps1

<#
.SYNOPSIS
    Checks state of SPF records for all exo domains

.DESCRIPTION
    A list of approved IP addresses for sending mail SHALL be maintained.

.EXAMPLE
    Test-MtCisaSpfRestriction

    Returns true if SPF record exists and has a fail all modifier for all exo domains

.LINK
    https://maester.dev/docs/commands/Test-MtCisaSpfRestriction
#>

function Test-MtCisaSpfRestriction {
    [CmdletBinding()]
    [OutputType([bool])]
    param()

    if(!(Test-MtConnection ExchangeOnline)){
        Add-MtTestResultDetail -SkippedBecause NotConnectedExchange
        return $null
    }

    $acceptedDomains = Get-MtExo -Request AcceptedDomain
    <# Parked domains should have SPF ending in -all too
    $sendingDomains = $acceptedDomains | Where-Object {`
        -not $_.SendingFromDomainDisabled
    }
    #>


    $spfRecords = @()
    foreach($domain in $acceptedDomains){
        $spfRecord = Get-MailAuthenticationRecord -DomainName $domain.DomainName -Records SPF
        $spfRecord | Add-Member -MemberType NoteProperty -Name "pass" -Value "Failed"
        $spfRecord | Add-Member -MemberType NoteProperty -Name "reason" -Value ""

        if($spfRecord.spfRecord.GetType().Name -eq "SPFRecord"){
            if ($spfRecord.spfRecord.terms[-1].directive -eq "-all"){
                $spfRecord.pass = "Passed"
                $spfRecord.reason = "Last directive is '-all'"
            } elseif ($spfRecord.spfRecord.terms[-1].modifier -eq "redirect"){
                $spfRecord.pass = "Skipped"
                $spfRecord.reason = "Redirect modifier"
            }
        } elseif ($spfRecord.spfRecord -like "*not available"){
            $spfRecord.pass = "Skipped"
            $spfRecord.reason = $spfRecord.spfRecord
        } else {
            #$spfRecord.reason = "Last directive is not '-all'"
            $spfRecord.reason = "Failure to obtain record"
        }

        #Hacky sort, doesn't handle IPv6
        #$spfRecord.spfLookups.IPAddress|sort -Property {[system.version]($_ -replace "\/\d{1,3}$","")}
        #Proper but will need to update Resolve-SPFRecord
        #Too: https://learn.microsoft.com/en-us/dotnet/api/system.net.ipnetwork
        #[ipaddress]::HostToNetworkOrder(([ipaddress]$_).address)

        $spfRecords += $spfRecord
    }

    if("Failed" -in $spfRecords.pass){
        $testResult = $false
    }else{
        $testResult = $true
    }

    if($testResult){
        $testResultMarkdown = "Well done. Your tenant's domains have a restricted SPF, review authorized senders for accuracy.`n`n%TestResult%"
    }else{
        $testResultMarkdown = "Your tenant's domains do not restrict authorized senders with SPF fully. Ensure all domain's SPF records end in '-all'.`n`n%TestResult%"
    }

    $passResult = "✅ Pass"
    $failResult = "❌ Fail"
    $skipResult = "🗄️ Skip"
    $result = "| Domain | Result | Reason | Addresses |`n"
    $result += "| --- | --- | --- | --- |`n"
    foreach ($item in $spfRecords | Sort-Object -Property domain) {
        switch($item.pass){
            "Passed" {$itemResult = $passResult}
            "Skipped" {$itemResult = $skipResult}
            "Failed" {$itemResult = $failResult}
        }
        $itemAddressCount = ($item.spfLookups.IPAddress|Measure-Object).Count
        switch($itemAddressCount){
            0 {
                $itemAddressList = ""
            }
            1 {
                $itemAddressList = "$($item.spfLookups.IPAddress[0])"
            }
            2 {
                $itemAddressList = "$($item.spfLookups.IPAddress[0]), "
                $itemAddressList += "$($item.spfLookups.IPAddress[1])"
            }
            Default {
                $itemAddressList = "$($item.spfLookups.IPAddress[0]), "
                $itemAddressList += "$($item.spfLookups.IPAddress[1]), "
                $itemAddressList += "& ...$($itemAddressCount-2) addresses"
            }
        }
        $result += "| $($item.domain) | $($itemResult) | $($item.reason) | $($itemAddressList) |`n"
    }

    $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $result

    Add-MtTestResultDetail -Result $testResultMarkdown

    return $testResult
}