Checks/EXO/MET-EXO002-DKIM.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'METCheckInfo',
    Justification = 'Check metadata. Read from the AST by Get-METCheck and never executed.')]
param()

$METCheckInfo = @{
    Name           = 'DKIM'
    Severity       = 'High'
    Description    = 'Verifies DKIM signing is enabled for every accepted domain with a key length of at least 2048 bits.'
    RequiresModule = @('ExchangeOnlineManagement')
}

try {
    $dkimConfigs = Get-DkimSigningConfig -ErrorAction Stop
}
catch {
    New-METCheckResult -CheckId 'MET-EXO002' -Category EXO -Name 'DKIM' `
        -Result Fail -Severity High -AffectedObject 'DKIM Signing Configs' `
        -Finding 'Unable to retrieve DKIM signing configurations' `
        -Recommendation 'Ensure the account has Security Reader or higher permissions.' `
        -ReferenceUrl 'https://aka.ms/dkim' -ErrorMessage $_.ToString()
    return
}

if (-not $dkimConfigs) {
    New-METCheckResult -CheckId 'MET-EXO002' -Category EXO -Name 'DKIM' `
        -Result Fail -Severity High -AffectedObject 'DKIM' `
        -Finding 'No DKIM signing configurations found' `
        -Recommendation 'Enable DKIM signing for all accepted domains in the Microsoft 365 Defender portal.' `
        -ReferenceUrl 'https://aka.ms/dkim'
    return
}

foreach ($config in $dkimConfigs) {
    $issues = [System.Collections.Generic.List[string]]::new()

    $enabledProperty = $config.PSObject.Properties['Enabled']
    if (-not $enabledProperty -or $null -eq $enabledProperty.Value) {
        $issues.Add('Enabled was not returned for this domain, so whether DKIM signing is on was not established')
    }
    elseif (-not $enabledProperty.Value) {
        $issues.Add('DKIM signing is disabled for this domain')
    }

    # Key size is reported per selector (Selector1KeySize/Selector2KeySize), never as a
    # flat KeySize property - KeySize exists only as an input parameter on New-/Rotate-.
    # Only the currently-signing selector is asserted: raising the key size applies to
    # the next active selector at the following rotation, so a domain legitimately mid-
    # migration has one 1024-bit and one 2048-bit selector and must not be failed for it.
    $activeSelector = if ($config.RotateOnDate -and ([datetime]::UtcNow -ge $config.RotateOnDate)) {
        $config.SelectorAfterRotateOnDate
    } else {
        $config.SelectorBeforeRotateOnDate
    }

    $selectorSizes = @{
        selector1 = $config.Selector1KeySize
        selector2 = $config.Selector2KeySize
    }

    $activeKeySize = $null
    if ($activeSelector -and $selectorSizes.ContainsKey([string]$activeSelector)) {
        $activeKeySize = $selectorSizes[[string]$activeSelector]
    }
    if ($null -eq $activeKeySize) {
        $reported = @($selectorSizes.Values | Where-Object { $_ })
        if ($reported.Count -gt 0) {
            $activeKeySize = ($reported | Measure-Object -Minimum).Minimum
        }
    }

    if ($null -ne $activeKeySize -and $activeKeySize -lt 2048) {
        $issues.Add("DKIM key size is $activeKeySize bits - minimum recommended is 2048 bits")
    }

    # Only assert which selector signs "after the next rotation" when the active selector
    # is actually known. With SelectorBeforeRotateOnDate/SelectorAfterRotateOnDate absent,
    # [string]$activeSelector is '' and every selector would compare as inactive - a
    # misleading rotation note. (The <2048 case reaches the Fail branch via the key-size
    # fallback above regardless, so this guard is defensive, not a live-bug fix.)
    $inactiveNote = @()
    if ($activeSelector) {
        foreach ($sel in @('selector1', 'selector2')) {
            $size = $selectorSizes[$sel]
            if ($null -ne $size -and $sel -ne [string]$activeSelector -and $size -lt 2048) {
                $inactiveNote += "$sel is $size-bit and will sign after the next key rotation"
            }
        }
    }

    # Microsoft documents three PowerShell-facing Status values. NoDKIMKeys and
    # CnameMissing are distinct problems with distinct fixes, so they are reported
    # separately rather than under one CNAME-flavoured message.
    switch ([string]$config.Status) {
        'Valid'        { }
        'NoDKIMKeys'   { $issues.Add('No DKIM keypair has been generated for this domain') }
        'CnameMissing' { $issues.Add('DKIM CNAME records are not published in DNS') }
        ''             { $issues.Add('DKIM status was not reported by Exchange Online') }
        default        { $issues.Add("DKIM record status is '$($config.Status)'") }
    }

    $cnames = @()
    if ($config.Selector1CNAME) { $cnames += "selector1: $($config.Selector1CNAME)" }
    if ($config.Selector2CNAME) { $cnames += "selector2: $($config.Selector2CNAME)" }
    $cnameDetail = if ($cnames.Count -gt 0) { " | $($cnames -join ', ')" } else { '' }

    $domainLabel = if ($config.Domain) { $config.Domain } else { $config.Name }

    if ($issues.Count -gt 0) {
        New-METCheckResult -CheckId 'MET-EXO002' -Category EXO -Name 'DKIM' `
            -Result Fail -Severity High -AffectedObject $domainLabel `
            -Finding "$($issues -join '; ')$cnameDetail" `
            -Recommendation 'Enable DKIM signing, rotate keys to 2048-bit if needed, and publish the provided CNAME records in DNS.' `
            -ReferenceUrl 'https://aka.ms/dkim'
    }
    elseif ($null -eq $activeKeySize) {
        New-METCheckResult -CheckId 'MET-EXO002' -Category EXO -Name 'DKIM' `
            -Result Warning -Severity High -AffectedObject $domainLabel `
            -Finding "DKIM signing is enabled and status is '$($config.Status)', but neither Selector1KeySize nor Selector2KeySize was reported, so the key length could not be verified against the 2048-bit minimum$cnameDetail" `
            -Recommendation 'Verify the DKIM key length in the Defender portal, and rotate to 2048-bit with Rotate-DkimSigningConfig -Identity <domain> -KeySize 2048 if it is still 1024-bit.' `
            -ReferenceUrl 'https://aka.ms/dkim'
    }
    else {
        $rotationNote = if ($inactiveNote.Count -gt 0) { " (note: $($inactiveNote -join '; '))" } else { '' }
        New-METCheckResult -CheckId 'MET-EXO002' -Category EXO -Name 'DKIM' `
            -Result Pass -Severity High -AffectedObject $domainLabel `
            -Finding "DKIM signing is enabled with a $activeKeySize-bit key on the active selector and status '$($config.Status)'$rotationNote$cnameDetail" `
            -ReferenceUrl 'https://aka.ms/dkim'
    }
}