Src/Private/Get-AbrExoDKIMDMARC.ps1

function Get-AbrExoDKIMDMARC {
    <#
    .SYNOPSIS
    Documents DKIM signing configuration and DMARC DNS records for all Exchange Online accepted domains,
    with ACSC E8 and CIS compliance assessments.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting DKIM/DMARC configuration for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'DKIM_DMARC'
    }

    process {
        Section -Style Heading2 'DKIM & DMARC' {
            Paragraph "The following section documents DKIM signing and DMARC policy configuration for tenant $TenantId."
            BlankLine

            # Compliance variable defaults
            $DkimTotalDomainCount    = 0
            $DkimEnabledDomainCount  = 0
            $Dkim2048BitCount        = 0
            $DmarcDomainsTotal       = 0
            $DmarcDomainsConfigured  = 0
            $DmarcDomainsCoveredPct  = 0
            $DmarcEnforcedDomainCount = 0
            $DmarcRuaConfiguredCount = 0

            #region DKIM
            try {
                Write-Host " - Retrieving DKIM signing configuration..."
                $DkimConfigs = Get-DkimSigningConfig -ErrorAction Stop | Sort-Object Domain

                # Also get accepted domains for accurate total (DKIM configs only appear after setup)
                $AcceptedDomainsForDkim = @(Get-AcceptedDomain -ErrorAction SilentlyContinue |
                    Where-Object { $_.DomainType -eq 'Authoritative' -and $_.DomainName -notlike '*.onmicrosoft.com' })

                $DkimTotalDomainCount   = if (@($DkimConfigs).Count -gt 0) { @($DkimConfigs).Count } else { $AcceptedDomainsForDkim.Count }
                $DkimEnabledDomainCount = @($DkimConfigs | Where-Object { $_.Enabled -eq $true }).Count
                $Dkim2048BitCount       = @($DkimConfigs | Where-Object { $_.Enabled -eq $true -and $_.KeySize -eq 2048 }).Count

                Section -Style Heading3 'DKIM Signing Configuration' {
                    if (@($DkimConfigs).Count -eq 0) {
                        Paragraph "No DKIM signing configurations found. DKIM has not been explicitly configured for any domain in tenant $TenantId. To enable DKIM, visit the Microsoft 365 Defender portal at security.microsoft.com > Email & Collaboration > Policies & Rules > Threat Policies > Email Authentication Settings > DKIM."
                        BlankLine
                        Paragraph "Custom domains require DKIM to be explicitly enabled. The onmicrosoft.com domain uses Microsoft-managed DKIM signing by default."
                    } else {
                        Paragraph "DKIM signing configuration for $(@($DkimConfigs).Count) domain(s) in tenant $TenantId. $DkimEnabledDomainCount of $(@($DkimConfigs).Count) domain(s) have DKIM signing enabled."
                        BlankLine

                        $DkimObj = [System.Collections.ArrayList]::new()
                        foreach ($Dkim in $DkimConfigs) {
                            $dkimInObj = [ordered] @{
                                'Domain'                = $Dkim.Domain
                                'DKIM Enabled'          = $Dkim.Enabled
                                'Key Size (bits)'       = if ($Dkim.KeySize) { $Dkim.KeySize } else { 'Unknown' }
                                'Selector 1 Key'        = if ($Dkim.Selector1PublicKey) { 'Published' } else { 'Not Published' }
                                'Selector 2 Key'        = if ($Dkim.Selector2PublicKey) { 'Published' } else { 'Not Published' }
                                'Last Checked'          = if ($Dkim.LastChecked) { $Dkim.LastChecked.ToString('yyyy-MM-dd') } else { 'Never' }
                                'Status'                = if ($Dkim.Status) { $Dkim.Status } else { 'Unknown' }
                            }
                            $DkimObj.Add([pscustomobject](ConvertTo-HashToYN $dkimInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.DKIM) {
                                $null = ($DkimObj | Where-Object { $_.'DKIM Enabled' -eq 'No' } | Set-Style -Style Critical | Out-Null)
                                $null = ($DkimObj | Where-Object { $_.'Key Size (bits)' -ne '2048' -and $_.'DKIM Enabled' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                                $null = ($DkimObj | Where-Object { $_.'Selector 1 Key' -eq 'Not Published' -or $_.'Selector 2 Key' -eq 'Not Published' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $DkimTableParams = @{ Name = "DKIM Signing Configuration - $TenantId"; List = $false; ColumnWidths = 26, 10, 12, 12, 12, 14, 14 }
                        if ($Report.ShowTableCaptions) { $DkimTableParams['Caption'] = "- $($DkimTableParams.Name)" }
                        $DkimObj | Table @DkimTableParams
                        $script:ExcelSheets['DKIM'] = $DkimObj
                    }
                }
            } catch {
                Write-ExoError 'DKIM' "Unable to retrieve DKIM configuration: $($_.Exception.Message)"
                Paragraph "Unable to retrieve DKIM configuration: $($_.Exception.Message)"
            }
            #endregion

            #region DMARC - DNS Lookup
            try {
                Write-Host " - Checking DMARC DNS records..."
                $AcceptedDomains = Get-AcceptedDomain -ErrorAction Stop | Where-Object { $_.DomainType -eq 'Authoritative' }
                $DmarcDomainsTotal = @($AcceptedDomains).Count

                $DmarcObj = [System.Collections.ArrayList]::new()
                foreach ($Domain in $AcceptedDomains) {
                    $DmarcRecord   = $null
                    $DmarcPolicy   = 'None'
                    $DmarcPct      = 100
                    $DmarcRua      = 'Not Set'
                    $DmarcRuf      = 'Not Set'
                    $DmarcFound    = $false
                    $DmarcEnforced = $false

                    try {
                        $DnsResult = Resolve-DnsName -Name "_dmarc.$($Domain.DomainName)" -Type TXT -ErrorAction SilentlyContinue
                        $DmarcTxt  = ($DnsResult | Where-Object { $_.Strings -like 'v=DMARC1*' }).Strings

                        if ($DmarcTxt) {
                            $DmarcFound  = $true
                            $DmarcRecord = $DmarcTxt -join ''

                            if ($DmarcRecord -match 'p=(\w+)') {
                                $DmarcPolicy = $Matches[1]
                                $DmarcEnforced = $DmarcPolicy -in @('quarantine', 'reject')
                            }
                            if ($DmarcRecord -match 'pct=(\d+)') { $DmarcPct = [int]$Matches[1] }
                            if ($DmarcRecord -match 'rua=([^\s;]+)') { $DmarcRua = $Matches[1] }
                            if ($DmarcRecord -match 'ruf=([^\s;]+)') { $DmarcRuf = $Matches[1] }
                        }
                    } catch {
                        # DNS lookup failed -- treat as not found
                    }

                    if ($DmarcFound) { $DmarcDomainsConfigured++ }
                    if ($DmarcEnforced) { $DmarcEnforcedDomainCount++ }
                    if ($DmarcRua -ne 'Not Set') { $DmarcRuaConfiguredCount++ }

                    $dmarcInObj = [ordered] @{
                        'Domain'          = $Domain.DomainName
                        'DMARC Record'    = if ($DmarcFound) { 'Found' } else { 'Not Found' }
                        'Policy (p=)'     = if ($DmarcFound) { $DmarcPolicy } else { '--' }
                        'Pct (%)'         = if ($DmarcFound) { $DmarcPct } else { '--' }
                        'Aggregate (rua)' = $DmarcRua
                        'Forensic (ruf)'  = $DmarcRuf
                        'Enforced'        = $DmarcEnforced
                    }
                    $DmarcObj.Add([pscustomobject](ConvertTo-HashToYN $dmarcInObj)) | Out-Null
                }

                $DmarcDomainsCoveredPct = if ($DmarcDomainsTotal -gt 0) {
                    [math]::Round(($DmarcDomainsConfigured / $DmarcDomainsTotal) * 100, 0)
                } else { 0 }

                Section -Style Heading3 'DMARC DNS Records' {
                    Paragraph "DMARC record status for $DmarcDomainsTotal authoritative domain(s). $DmarcDomainsConfigured domain(s) have a DMARC record published ($DmarcDomainsCoveredPct% coverage). $DmarcEnforcedDomainCount domain(s) enforce quarantine or reject policy."
                    BlankLine

                    $null = (& {
                        if ($HealthCheck.ExchangeOnline.DMARC) {
                            $null = ($DmarcObj | Where-Object { $_.'DMARC Record' -eq 'Not Found' } | Set-Style -Style Critical | Out-Null)
                            $null = ($DmarcObj | Where-Object { $_.'DMARC Record' -eq 'Found' -and $_.'Enforced' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                            $null = ($DmarcObj | Where-Object { $_.'Aggregate (rua)' -eq 'Not Set' -and $_.'DMARC Record' -eq 'Found' } | Set-Style -Style Warning | Out-Null)
                        }
                    })

                    $DmarcTableParams = @{ Name = "DMARC Records - $TenantId"; List = $false; ColumnWidths = 20, 12, 12, 8, 22, 18, 8 }
                    if ($Report.ShowTableCaptions) { $DmarcTableParams['Caption'] = "- $($DmarcTableParams.Name)" }
                    if ($DmarcObj.Count -gt 0) {
                        $DmarcObj | Table @DmarcTableParams
                        $script:ExcelSheets['DMARC'] = $DmarcObj
                    } else {
                        Paragraph "No authoritative domains found to check DMARC records against."
                    }
                }
            } catch {
                Write-ExoError 'DMARC' "Unable to retrieve DMARC records: $($_.Exception.Message)"
                Paragraph "Unable to check DMARC DNS records: $($_.Exception.Message)"
            }
            #endregion


            #region ACSC E8 - DKIM
            BlankLine
            Paragraph "ACSC Essential Eight Assessment"
            BlankLine
            $e8VarsDkim = @{
                DkimTotalDomainCount   = $DkimTotalDomainCount
                DkimEnabledDomainCount = $DkimEnabledDomainCount
                Dkim2048BitCount       = $Dkim2048BitCount
            }
            $e8ChecksDkim = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoE8Checks 'DKIM') -Framework 'E8' -CallerVariables $e8VarsDkim
            New-AbrExoE8AssessmentTable -Checks $e8ChecksDkim -Name 'DKIM' -TenantId $TenantId
            if ($e8ChecksDkim) {
                foreach ($row in $e8ChecksDkim) {
                    $null = $script:E8AllChecks.Add([pscustomobject]@{
                        Section = 'DKIM'
                        ML      = $row.ML
                        Control = $row.Control
                        Status  = $row.Status
                        Detail  = $row.Detail
                    })
                }
            }
            #endregion

            #region ACSC E8 - DMARC
            BlankLine
            Paragraph "ACSC Essential Eight Assessment"
            BlankLine
            $e8VarsDmarc = @{
                DmarcDomainsTotal        = $DmarcDomainsTotal
                DmarcDomainsConfigured   = $DmarcDomainsConfigured
                DmarcDomainsCoveredPct   = $DmarcDomainsCoveredPct
                DmarcEnforcedDomainCount = $DmarcEnforcedDomainCount
                DmarcRuaConfiguredCount  = $DmarcRuaConfiguredCount
            }
            $e8ChecksDmarc = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoE8Checks 'DMARC') -Framework 'E8' -CallerVariables $e8VarsDmarc
            New-AbrExoE8AssessmentTable -Checks $e8ChecksDmarc -Name 'DMARC' -TenantId $TenantId
            if ($e8ChecksDmarc) {
                foreach ($row in $e8ChecksDmarc) {
                    $null = $script:E8AllChecks.Add([pscustomobject]@{
                        Section = 'DMARC'
                        ML      = $row.ML
                        Control = $row.Control
                        Status  = $row.Status
                        Detail  = $row.Detail
                    })
                }
            }
            #endregion

            #region CIS - DKIM
            BlankLine
            Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment"
            BlankLine
            $cisVarsDkim = @{
                DkimTotalDomainCount   = $DkimTotalDomainCount
                DkimEnabledDomainCount = $DkimEnabledDomainCount
            }
            $cisChecksDkim = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoCISChecks 'DKIM') -Framework 'CIS' -CallerVariables $cisVarsDkim
            New-AbrExoCISAssessmentTable -Checks $cisChecksDkim -Name 'DKIM' -TenantId $TenantId
            if ($cisChecksDkim) {
                foreach ($row in $cisChecksDkim) {
                    $null = $script:CISAllChecks.Add([pscustomobject]@{
                        Section    = 'DKIM'
                        CISControl = $row.CISControl
                        Level      = $row.Level
                        Status     = $row.Status
                        Detail     = $row.Detail
                    })
                }
            }
            #endregion

            #region CIS - DMARC
            BlankLine
            Paragraph "CIS Microsoft 365 Foundations Benchmark Assessment"
            BlankLine
            $cisVarsDmarc = @{
                DmarcDomainsTotal        = $DmarcDomainsTotal
                DmarcDomainsConfigured   = $DmarcDomainsConfigured
                DmarcDomainsCoveredPct   = $DmarcDomainsCoveredPct
                DmarcEnforcedDomainCount = $DmarcEnforcedDomainCount
            }
            $cisChecksDmarc = Build-AbrExoComplianceChecks -Definitions (Get-AbrExoCISChecks 'DMARC') -Framework 'CIS' -CallerVariables $cisVarsDmarc
            New-AbrExoCISAssessmentTable -Checks $cisChecksDmarc -Name 'DMARC' -TenantId $TenantId
            if ($cisChecksDmarc) {
                foreach ($row in $cisChecksDmarc) {
                    $null = $script:CISAllChecks.Add([pscustomobject]@{
                        Section    = 'DMARC'
                        CISControl = $row.CISControl
                        Level      = $row.Level
                        Status     = $row.Status
                        Detail     = $row.Detail
                    })
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'DKIM_DMARC'
    }
}