Orchestrator/Invoke-DnsAuthentication.ps1

function Invoke-DnsAuthentication {
    [CmdletBinding()]
    param(
        [string]$AssessmentFolder,
        [string]$ProjectRoot,
        [System.Collections.Generic.List[PSCustomObject]]$SummaryResults,
        [System.Collections.Generic.List[PSCustomObject]]$Issues,
        [hashtable]$DnsCollector
    )

if ($script:runDnsAuthentication) {
    $acceptedDomains = $script:cachedAcceptedDomains
    if (-not $acceptedDomains -or $acceptedDomains.Count -eq 0) {
        try {
            $acceptedDomains = @(Get-AcceptedDomain -ErrorAction Stop)
        }
        catch {
            Write-AssessmentLog -Level WARN -Message "Skipping deferred DNS checks -- no cached domains and EXO unavailable" -Section 'Email'
        }
    }

    if ($acceptedDomains -and $acceptedDomains.Count -gt 0) {

    # Collect prefetched DNS cache (started during Graph connect)
    $dnsCache = @{}
    if ($script:dnsPrefetchJobs) {
        Write-Verbose "Collecting DNS prefetch results..."
        $prefetchResults = $script:dnsPrefetchJobs | Wait-Job | Receive-Job
        $script:dnsPrefetchJobs | Remove-Job -Force
        foreach ($pr in $prefetchResults) { $dnsCache[$pr.Domain] = $pr }
        $script:dnsPrefetchJobs = $null
    }

    # --- DNS Security Config collector (uses prefetch cache) ---
    $dnsSecConfigCollector = @{ Name = '12b-DNS-Security-Config'; Label = 'DNS Security Config' }
    $dnsSecStart = Get-Date
    $dnsSecCsvPath = Join-Path -Path $assessmentFolder -ChildPath "$($dnsSecConfigCollector.Name).csv"
    $dnsSecStatus = 'Skipped'
    $dnsSecItemCount = 0
    $dnsSecError = ''

    Write-AssessmentLog -Level INFO -Message "Running: $($dnsSecConfigCollector.Label)" -Section 'Email' -Collector $dnsSecConfigCollector.Label
    try {
        $dnsSecScriptPath = Join-Path -Path $projectRoot -ChildPath 'Exchange-Online\Get-DnsSecurityConfig.ps1'
        $dnsSecDkimData = if ($script:cachedDkimConfigs) { $script:cachedDkimConfigs } else { $null }
        $dnsSecResults = & $dnsSecScriptPath -AcceptedDomains $acceptedDomains -DkimConfigs $dnsSecDkimData
        if ($dnsSecResults) {
            $dnsSecItemCount = Export-AssessmentCsv -Path $dnsSecCsvPath -Data @($dnsSecResults) -Label $dnsSecConfigCollector.Label
            $dnsSecStatus = 'Complete'
        }
    }
    catch {
        $dnsSecError = $_.Exception.Message
        $dnsSecStatus = 'Failed'
        Write-AssessmentLog -Level ERROR -Message "DNS Security Config failed: $dnsSecError" -Section 'Email' -Collector $dnsSecConfigCollector.Label
    }

    $dnsSecDuration = (Get-Date) - $dnsSecStart
    $summaryResults.Add([PSCustomObject]@{
        Section   = 'Email'
        Collector = $dnsSecConfigCollector.Label
        FileName  = "$($dnsSecConfigCollector.Name).csv"
        Status    = $dnsSecStatus
        Items     = $dnsSecItemCount
        Duration  = '{0:mm\:ss}' -f $dnsSecDuration
        Error     = $dnsSecError
    })
    Show-CollectorResult -Label $dnsSecConfigCollector.Label -Status $dnsSecStatus -Items $dnsSecItemCount -DurationSeconds $dnsSecDuration.TotalSeconds -ErrorMessage $dnsSecError
    Write-AssessmentLog -Level INFO -Message "Completed: $($dnsSecConfigCollector.Label) -- $dnsSecStatus, $dnsSecItemCount items" -Section 'Email' -Collector $dnsSecConfigCollector.Label

    # --- DNS Authentication enumeration ---
    $dnsStart = Get-Date
    $dnsCsvPath = Join-Path -Path $assessmentFolder -ChildPath "$($dnsCollector.Name).csv"
    $dnsStatus = 'Skipped'
    $dnsItemCount = 0
    $dnsError = ''

    Write-AssessmentLog -Level INFO -Message "Running: $($dnsCollector.Label)" -Section 'Email' -Collector $dnsCollector.Label

    try {
        $dnsResults = foreach ($domain in $acceptedDomains) {
            $domainName = $domain.DomainName
            $cached = $dnsCache[$domainName]

            # ------- SPF -------
            $spf = 'Not configured'
            $spfEnforcement = 'N/A'
            $spfLookupCount = 'N/A'
            $spfDuplicates = 'No'

            try {
                $txtRecords = if ($cached -and $cached.PSObject.Properties['Spf']) { @($cached.Spf) } else { @(Resolve-DnsRecord -Name $domainName -Type TXT -ErrorAction SilentlyContinue) }
                $spfRecords = @($txtRecords | Where-Object { $_.Strings -and ($_.Strings -join '' -match '^v=spf1') })

                if ($spfRecords.Count -gt 1) {
                    $spfDuplicates = "Yes ($($spfRecords.Count) records -- PermError)"
                }

                if ($spfRecords.Count -ge 1) {
                    $spfValue = $spfRecords[0].Strings -join ''
                    $spf = $spfValue

                    if ($spfValue -match '-all$') { $spfEnforcement = 'Hard Fail (-all)' }
                    elseif ($spfValue -match '~all$') { $spfEnforcement = 'Soft Fail (~all)' }
                    elseif ($spfValue -match '\?all$') { $spfEnforcement = 'Neutral (?all)' }
                    elseif ($spfValue -match '\+all$') { $spfEnforcement = 'Pass (+all) WARNING' }
                    else { $spfEnforcement = 'No all mechanism' }

                    $lookupMechanisms = @(
                        [regex]::Matches($spfValue, '\b(include:|a:|a/|mx:|mx/|ptr:|exists:|redirect=)').Count
                    )
                    $spfLookupCount = "$($lookupMechanisms[0]) / 10"
                    if ($lookupMechanisms[0] -gt 10) {
                        $spfLookupCount = "$($lookupMechanisms[0]) / 10 -- EXCEEDS LIMIT"
                    }
                }
            }
            catch {
                $spf = 'DNS lookup failed'
                Write-Verbose "SPF lookup failed for $domainName`: $_"
            }

            # ------- DMARC -------
            $dmarc = 'Not configured'
            $dmarcPolicy = 'N/A'
            $dmarcPct = 'N/A'
            $dmarcReporting = 'N/A'
            $dmarcDuplicates = 'No'

            try {
                $dmarcTxtRecords = if ($cached -and $cached.PSObject.Properties['Dmarc']) { @($cached.Dmarc) } else { @(Resolve-DnsRecord -Name "_dmarc.$domainName" -Type TXT -ErrorAction SilentlyContinue) }
                $dmarcRecords = @($dmarcTxtRecords | Where-Object { $_.Strings -and ($_.Strings -join '' -match '^v=DMARC1') })

                if ($dmarcRecords.Count -gt 1) {
                    $dmarcDuplicates = "Yes ($($dmarcRecords.Count) records -- PermError)"
                }

                if ($dmarcRecords.Count -ge 1) {
                    $dmarcValue = $dmarcRecords[0].Strings -join ''
                    $dmarc = $dmarcValue

                    if ($dmarcValue -match 'p=(\w+)') {
                        $dmarcPolicy = $Matches[1]
                        if ($dmarcPolicy -eq 'none') { $dmarcPolicy = 'none (monitoring only)' }
                    }

                    if ($dmarcValue -match 'pct=(\d+)') {
                        $dmarcPct = "$($Matches[1])%"
                    }
                    else {
                        $dmarcPct = '100% (default)'
                    }

                    $reportingParts = @()
                    if ($dmarcValue -match 'rua=([^;]+)') { $reportingParts += "rua=$($Matches[1])" }
                    if ($dmarcValue -match 'ruf=([^;]+)') { $reportingParts += "ruf=$($Matches[1])" }
                    $dmarcReporting = if ($reportingParts.Count -gt 0) { $reportingParts -join '; ' } else { 'No reporting configured' }
                }
            }
            catch {
                $dmarc = 'Not configured'
                Write-Verbose "DMARC lookup failed for $domainName`: $_"
            }

            # ------- DKIM (both selectors) -------
            $dkimSelector1 = 'Not configured'
            $dkimSelector2 = 'Not configured'

            try {
                $dkim1Records = if ($cached -and $cached.PSObject.Properties['Dkim1']) { $cached.Dkim1 } else { Resolve-DnsRecord -Name "selector1._domainkey.$domainName" -Type CNAME -ErrorAction SilentlyContinue }
                if ($dkim1Records.NameHost) { $dkimSelector1 = $dkim1Records.NameHost }
            }
            catch { Write-Verbose "DKIM selector1 lookup failed for $domainName`: $_" }

            try {
                $dkim2Records = if ($cached -and $cached.PSObject.Properties['Dkim2']) { $cached.Dkim2 } else { Resolve-DnsRecord -Name "selector2._domainkey.$domainName" -Type CNAME -ErrorAction SilentlyContinue }
                if ($dkim2Records.NameHost) { $dkimSelector2 = $dkim2Records.NameHost }
            }
            catch { Write-Verbose "DKIM selector2 lookup failed for $domainName`: $_" }

            # ------- DKIM EXO cross-reference -------
            $dkimStatus = 'N/A'
            $dkimDnsFound = ($dkimSelector1 -ne 'Not configured') -or ($dkimSelector2 -ne 'Not configured')
            if ($script:cachedDkimConfigs) {
                $exoDkim = @($script:cachedDkimConfigs | Where-Object { $_.Domain -eq $domainName })
                $exoDkimEnabled = [bool]($exoDkim | Where-Object { $_.Enabled })

                if ($dkimDnsFound -and $exoDkimEnabled) {
                    $dkimStatus = 'OK'
                }
                elseif (-not $dkimDnsFound -and $exoDkimEnabled) {
                    if ($domainName -match '\.onmicrosoft\.com$') {
                        $dkimStatus = 'EXO Confirmed (DNS not public for .onmicrosoft.com)'
                    }
                    else {
                        $dkimStatus = 'Mismatch: EXO enabled but DNS CNAME not found'
                    }
                }
                elseif ($dkimDnsFound -and -not $exoDkimEnabled) {
                    $dkimStatus = 'Mismatch: DNS CNAME exists but EXO signing disabled'
                }
                else {
                    $dkimStatus = 'Not configured'
                }
            }

            # ------- MTA-STS (RFC 8461) -------
            $mtaSts = 'Not configured'
            try {
                $mtaStsRecords = if ($cached -and $cached.PSObject.Properties['MtaSts']) { @($cached.MtaSts) } else { @(Resolve-DnsRecord -Name "_mta-sts.$domainName" -Type TXT -ErrorAction SilentlyContinue) }
                $mtaStsRecord = $mtaStsRecords | Where-Object { $_.Strings -and ($_.Strings -join '' -match 'v=STSv1') } | Select-Object -First 1
                if ($mtaStsRecord) {
                    $mtaSts = $mtaStsRecord.Strings -join ''
                }
            }
            catch { Write-Verbose "MTA-STS lookup failed for $domainName`: $_" }

            # ------- TLS-RPT (RFC 8460) -------
            $tlsRpt = 'Not configured'
            try {
                $tlsRptRecords = if ($cached -and $cached.PSObject.Properties['TlsRpt']) { @($cached.TlsRpt) } else { @(Resolve-DnsRecord -Name "_smtp._tls.$domainName" -Type TXT -ErrorAction SilentlyContinue) }
                $tlsRptRecord = $tlsRptRecords | Where-Object { $_.Strings -and ($_.Strings -join '' -match '^v=TLSRPTv1') } | Select-Object -First 1
                if ($tlsRptRecord) {
                    $tlsRpt = $tlsRptRecord.Strings -join ''
                }
            }
            catch { Write-Verbose "TLS-RPT lookup failed for $domainName`: $_" }

            # ------- Public DNS Validation -------
            $publicDnsConfirmed = 'N/A'
            if ($spf -ne 'Not configured' -and $spf -ne 'DNS lookup failed') {
                $publicChecks = @()
                foreach ($publicServer in @('8.8.8.8', '1.1.1.1')) {
                    try {
                        $publicTxt = @(Resolve-DnsRecord -Name $domainName -Type TXT -Server $publicServer -ErrorAction Stop)
                        $publicSpf = $publicTxt | Where-Object { $_.Strings -and ($_.Strings -join '' -match '^v=spf1') } | Select-Object -First 1
                        if ($publicSpf) { $publicChecks += $publicServer }
                    }
                    catch { Write-Verbose "Public DNS check ($publicServer) failed for $domainName`: $_" }
                }

                if ($publicChecks.Count -eq 2) {
                    $publicDnsConfirmed = 'Confirmed (Google + Cloudflare)'
                }
                elseif ($publicChecks.Count -eq 1) {
                    $publicDnsConfirmed = "Partial ($($publicChecks[0]) only)"
                }
                else {
                    $publicDnsConfirmed = 'NOT visible from public DNS'
                }
            }

            [PSCustomObject]@{
                Domain           = $domainName
                DomainType       = $domain.DomainType
                Default          = $domain.Default
                SPF              = if ($spf) { $spf } else { 'Not configured' }
                SPFEnforcement   = $spfEnforcement
                SPFLookupCount   = $spfLookupCount
                SPFDuplicates    = $spfDuplicates
                DMARC            = if ($dmarc) { $dmarc } else { 'Not configured' }
                DMARCPolicy      = $dmarcPolicy
                DMARCPct         = $dmarcPct
                DMARCReporting   = $dmarcReporting
                DMARCDuplicates  = $dmarcDuplicates
                DKIMSelector1    = $dkimSelector1
                DKIMSelector2    = $dkimSelector2
                DKIMStatus       = $dkimStatus
                MTASTS           = $mtaSts
                TLSRPT           = $tlsRpt
                PublicDNSConfirm = $publicDnsConfirmed
            }
        }

        if ($dnsResults) {
            $dnsItemCount = Export-AssessmentCsv -Path $dnsCsvPath -Data @($dnsResults) -Label $dnsCollector.Label
            $dnsStatus = 'Complete'
        }
        else {
            $dnsStatus = 'Complete'
        }
    }
    catch {
        $dnsError = $_.Exception.Message
        if ($dnsError -match 'not recognized|not found|not connected') {
            $dnsStatus = 'Skipped'
        }
        else {
            $dnsStatus = 'Failed'
        }
        Write-AssessmentLog -Level ERROR -Message "DNS Authentication failed" -Section 'Email' -Collector $dnsCollector.Label -Detail $_.Exception.ToString()
        $issues.Add([PSCustomObject]@{
            Severity     = if ($dnsStatus -eq 'Skipped') { 'WARNING' } else { 'ERROR' }
            Section      = 'Email'
            Collector    = $dnsCollector.Label
            Description  = 'DNS Authentication check failed'
            ErrorMessage = $dnsError
            Action       = Get-RecommendedAction -ErrorMessage $dnsError
        })
    }

    $dnsEnd = Get-Date
    $dnsDuration = $dnsEnd - $dnsStart

    $summaryResults.Add([PSCustomObject]@{
        Section   = 'Email'
        Collector = $dnsCollector.Label
        FileName  = "$($dnsCollector.Name).csv"
        Status    = $dnsStatus
        Items     = $dnsItemCount
        Duration  = '{0:mm\:ss}' -f $dnsDuration
        Error     = $dnsError
    })

    Show-CollectorResult -Label $dnsCollector.Label -Status $dnsStatus -Items $dnsItemCount -DurationSeconds $dnsDuration.TotalSeconds -ErrorMessage $dnsError
    Write-AssessmentLog -Level INFO -Message "Completed: $($dnsCollector.Label) -- $dnsStatus, $dnsItemCount items" -Section 'Email' -Collector $dnsCollector.Label

    }
}

# Clean up check progress display
if (Get-Command -Name Complete-CheckProgress -ErrorAction SilentlyContinue) {
    Complete-CheckProgress
}

}