Exchange-Online/Get-EmailSecurityReport.ps1

<#
.SYNOPSIS
    Reports on email security policies and DNS authentication configuration in Exchange Online.
.DESCRIPTION
    Collects anti-spam (hosted content filter), anti-phish, anti-malware, and DKIM signing
    configurations. Optionally checks SPF and DMARC DNS records for accepted domains.
    Provides a consolidated view for M365 security assessments and compliance reviews.
 
    Requires ExchangeOnlineManagement module and an active Exchange Online connection.
    DNS checks use Resolve-DnsRecord (cross-platform: Resolve-DnsName on Windows, dig on macOS/Linux).
.PARAMETER IncludeDnsChecks
    When specified, performs SPF and DMARC DNS lookups for each accepted domain in the tenant.
    Requires Resolve-DnsName (Windows) or dig (macOS/Linux). Failures are handled gracefully.
.PARAMETER OutputPath
    Optional path to export results as CSV. If not specified, results are returned
    to the pipeline.
.EXAMPLE
    PS> . .\Common\Connect-Service.ps1
    PS> Connect-Service -Service ExchangeOnline
    PS> .\Exchange-Online\Get-EmailSecurityReport.ps1
 
    Displays all email security policies (anti-spam, anti-phish, anti-malware, DKIM).
.EXAMPLE
    PS> .\Exchange-Online\Get-EmailSecurityReport.ps1 -IncludeDnsChecks -OutputPath '.\email-security.csv'
 
    Exports email security policies along with SPF/DMARC DNS checks to CSV.
.EXAMPLE
    PS> .\Exchange-Online\Get-EmailSecurityReport.ps1 -IncludeDnsChecks -Verbose
 
    Displays email security report with DNS authentication checks and detailed progress.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [switch]$IncludeDnsChecks,

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

$ErrorActionPreference = 'Stop'

# Verify EXO connection
try {
    $null = Get-OrganizationConfig -ErrorAction Stop
}
catch {
    Write-Error "Not connected to Exchange Online. Run Connect-Service -Service ExchangeOnline first."
    return
}

$results = [System.Collections.Generic.List[PSCustomObject]]::new()

# Anti-Spam Policies (Hosted Content Filter)
Write-Verbose "Retrieving anti-spam policies..."
try {
    $antiSpamPolicies = @(Get-HostedContentFilterPolicy)
    Write-Verbose "Found $($antiSpamPolicies.Count) anti-spam policies"

    foreach ($policy in $antiSpamPolicies) {
        $keySettings = @(
            "BulkThreshold=$($policy.BulkThreshold)"
            "SpamAction=$($policy.SpamAction)"
            "HighConfidenceSpamAction=$($policy.HighConfidenceSpamAction)"
            "PhishSpamAction=$($policy.PhishSpamAction)"
            "BulkSpamAction=$($policy.BulkSpamAction)"
            "QuarantineRetentionPeriod=$($policy.QuarantineRetentionPeriod)"
            "InlineSafetyTipsEnabled=$($policy.InlineSafetyTipsEnabled)"
            "SpamZapEnabled=$($policy.SpamZapEnabled)"
            "PhishZapEnabled=$($policy.PhishZapEnabled)"
        )

        if ($policy.AllowedSenders.Count -gt 0 -or $policy.AllowedSenderDomains.Count -gt 0) {
            $keySettings += "AllowedSenderDomains=$($policy.AllowedSenderDomains.Count)"
            $keySettings += "AllowedSenders=$($policy.AllowedSenders.Count)"
        }

        if ($policy.BlockedSenders.Count -gt 0 -or $policy.BlockedSenderDomains.Count -gt 0) {
            $keySettings += "BlockedSenderDomains=$($policy.BlockedSenderDomains.Count)"
            $keySettings += "BlockedSenders=$($policy.BlockedSenders.Count)"
        }

        $results.Add([PSCustomObject]@{
            PolicyType  = 'AntiSpam'
            Name        = $policy.Name
            Enabled     = $policy.IsDefault -or ($null -ne $policy.IsEnabled -and $policy.IsEnabled)
            KeySettings = $keySettings -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve anti-spam policies: $_"
}

# Anti-Phish Policies
Write-Verbose "Retrieving anti-phish policies..."
try {
    $antiPhishPolicies = @(Get-AntiPhishPolicy)
    Write-Verbose "Found $($antiPhishPolicies.Count) anti-phish policies"

    foreach ($policy in $antiPhishPolicies) {
        $keySettings = @(
            "Enabled=$($policy.Enabled)"
            "PhishThresholdLevel=$($policy.PhishThresholdLevel)"
            "EnableMailboxIntelligence=$($policy.EnableMailboxIntelligence)"
            "EnableMailboxIntelligenceProtection=$($policy.EnableMailboxIntelligenceProtection)"
            "EnableSpoofIntelligence=$($policy.EnableSpoofIntelligence)"
            "EnableFirstContactSafetyTips=$($policy.EnableFirstContactSafetyTips)"
            "EnableUnauthenticatedSender=$($policy.EnableUnauthenticatedSender)"
            "EnableViaTag=$($policy.EnableViaTag)"
        )

        if ($policy.EnableTargetedUserProtection) {
            $keySettings += "TargetedUserProtection=Enabled"
            $keySettings += "TargetedUsersToProtectCount=$($policy.TargetedUsersToProtect.Count)"
        }
        if ($policy.EnableTargetedDomainsProtection) {
            $keySettings += "TargetedDomainsProtection=Enabled"
        }
        if ($policy.EnableOrganizationDomainsProtection) {
            $keySettings += "OrganizationDomainsProtection=Enabled"
        }

        $results.Add([PSCustomObject]@{
            PolicyType  = 'AntiPhish'
            Name        = $policy.Name
            Enabled     = $policy.Enabled
            KeySettings = $keySettings -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve anti-phish policies: $_"
}

# Anti-Malware Policies
Write-Verbose "Retrieving anti-malware policies..."
try {
    $malwarePolicies = @(Get-MalwareFilterPolicy)
    Write-Verbose "Found $($malwarePolicies.Count) anti-malware policies"

    foreach ($policy in $malwarePolicies) {
        $keySettings = @(
            "EnableFileFilter=$($policy.EnableFileFilter)"
            "FileFilterAction=$($policy.FileFilterAction)"
            "ZapEnabled=$($policy.ZapEnabled)"
            "EnableInternalSenderAdminNotifications=$($policy.EnableInternalSenderAdminNotifications)"
            "EnableExternalSenderAdminNotifications=$($policy.EnableExternalSenderAdminNotifications)"
        )

        if ($policy.FileTypes.Count -gt 0) {
            $keySettings += "FileTypesCount=$($policy.FileTypes.Count)"
        }

        if ($policy.EnableInternalSenderAdminNotifications -or $policy.EnableExternalSenderAdminNotifications) {
            if ($policy.InternalSenderAdminAddress) {
                $keySettings += "InternalAdminNotify=$($policy.InternalSenderAdminAddress)"
            }
            if ($policy.ExternalSenderAdminAddress) {
                $keySettings += "ExternalAdminNotify=$($policy.ExternalSenderAdminAddress)"
            }
        }

        $results.Add([PSCustomObject]@{
            PolicyType  = 'AntiMalware'
            Name        = $policy.Name
            Enabled     = $policy.IsDefault -or ($null -ne $policy.IsEnabled -and $policy.IsEnabled)
            KeySettings = $keySettings -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve anti-malware policies: $_"
}

# DKIM Signing Configuration
Write-Verbose "Retrieving DKIM signing configuration..."
try {
    $dkimConfigs = @(Get-DkimSigningConfig)
    Write-Verbose "Found $($dkimConfigs.Count) DKIM configurations"

    foreach ($config in $dkimConfigs) {
        $keySettings = @(
            "Domain=$($config.Domain)"
            "Enabled=$($config.Enabled)"
            "Status=$($config.Status)"
        )

        if ($config.Selector1CNAME) {
            $keySettings += "Selector1CNAME=$($config.Selector1CNAME)"
        }
        if ($config.Selector2CNAME) {
            $keySettings += "Selector2CNAME=$($config.Selector2CNAME)"
        }

        $results.Add([PSCustomObject]@{
            PolicyType  = 'DKIM'
            Name        = $config.Domain
            Enabled     = $config.Enabled
            KeySettings = $keySettings -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve DKIM signing configuration: $_"
}

# DNS Authentication Checks (SPF and DMARC)
if ($IncludeDnsChecks) {
    Write-Verbose "Performing DNS authentication checks (SPF/DMARC)..."

    # Load cross-platform DNS resolver (Resolve-DnsName on Windows, dig on macOS/Linux)
    $dnsHelperPath = Join-Path -Path $PSScriptRoot -ChildPath '..\Common\Resolve-DnsRecord.ps1'
    $dnsCommandAvailable = $false
    if (Test-Path -Path $dnsHelperPath) {
        . $dnsHelperPath
        $dnsCommandAvailable = $null -ne (Get-Command -Name Resolve-DnsRecord -ErrorAction SilentlyContinue)
    }
    if (-not $dnsCommandAvailable) {
        Write-Warning "Resolve-DnsRecord helper is not available. Skipping DNS checks."
    }

    if ($dnsCommandAvailable) {
        # Retrieve accepted domains for DNS checks
        $domainsForDns = @()
        try {
            $domainsForDns = @(Get-AcceptedDomain)
            Write-Verbose "Checking DNS records for $($domainsForDns.Count) accepted domains"
        }
        catch {
            Write-Warning "Failed to retrieve accepted domains for DNS checks: $_"
        }

        foreach ($domain in $domainsForDns) {
            $domainName = $domain.DomainName

            # SPF check
            $spfRecord = $null
            try {
                $txtRecords = @(Resolve-DnsRecord -Name $domainName -Type TXT -ErrorAction Stop)
                $spfRecord = ($txtRecords | Where-Object {
                    $_.Strings -and ($_.Strings -join '' -match '^v=spf1')
                } | Select-Object -First 1)
            }
            catch {
                Write-Verbose "DNS lookup failed for SPF on $domainName`: $_"
            }

            $spfValue = if ($spfRecord) {
                $spfRecord.Strings -join ''
            }
            else {
                'Not Found'
            }

            $spfKeySettings = @(
                "Domain=$domainName"
                "Record=$spfValue"
            )

            # Evaluate SPF configuration quality
            if ($spfValue -ne 'Not Found') {
                if ($spfValue -match '-all$') {
                    $spfKeySettings += "Enforcement=HardFail (-all)"
                }
                elseif ($spfValue -match '~all$') {
                    $spfKeySettings += "Enforcement=SoftFail (~all)"
                }
                elseif ($spfValue -match '\?all$') {
                    $spfKeySettings += "Enforcement=Neutral (?all)"
                }
                elseif ($spfValue -match '\+all$') {
                    $spfKeySettings += "Enforcement=Pass (+all) WARNING"
                }
            }

            $results.Add([PSCustomObject]@{
                PolicyType  = 'SPF'
                Name        = $domainName
                Enabled     = ($spfValue -ne 'Not Found')
                KeySettings = $spfKeySettings -join '; '
            })

            # DMARC check
            $dmarcRecord = $null
            try {
                $dmarcTxtRecords = @(Resolve-DnsRecord -Name "_dmarc.$domainName" -Type TXT -ErrorAction Stop)
                $dmarcRecord = ($dmarcTxtRecords | Where-Object {
                    $_.Strings -and ($_.Strings -join '' -match '^v=DMARC1')
                } | Select-Object -First 1)
            }
            catch {
                Write-Verbose "DNS lookup failed for DMARC on $domainName`: $_"
            }

            $dmarcValue = if ($dmarcRecord) {
                $dmarcRecord.Strings -join ''
            }
            else {
                'Not Found'
            }

            $dmarcKeySettings = @(
                "Domain=$domainName"
                "Record=$dmarcValue"
            )

            # Parse DMARC policy
            if ($dmarcValue -ne 'Not Found') {
                if ($dmarcValue -match 'p=(\w+)') {
                    $dmarcKeySettings += "Policy=$($Matches[1])"
                }
                if ($dmarcValue -match 'sp=(\w+)') {
                    $dmarcKeySettings += "SubdomainPolicy=$($Matches[1])"
                }
                if ($dmarcValue -match 'pct=(\d+)') {
                    $dmarcKeySettings += "Percentage=$($Matches[1])%"
                }
                if ($dmarcValue -match 'rua=([^;]+)') {
                    $dmarcKeySettings += "AggregateReport=$($Matches[1])"
                }
                if ($dmarcValue -match 'ruf=([^;]+)') {
                    $dmarcKeySettings += "ForensicReport=$($Matches[1])"
                }
            }

            $results.Add([PSCustomObject]@{
                PolicyType  = 'DMARC'
                Name        = $domainName
                Enabled     = ($dmarcValue -ne 'Not Found')
                KeySettings = $dmarcKeySettings -join '; '
            })
        }
    }
}

Write-Verbose "Email security report complete: $($results.Count) total entries"

if ($OutputPath) {
    $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported email security report ($($results.Count) entries) to $OutputPath"
}
else {
    Write-Output $results
}