Checks/EXO/MET-EXO001-DMARC.ps1
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'METCheckInfo', Justification = 'Check metadata. Read from the AST by Get-METCheck and never executed.')] param() $METCheckInfo = @{ Name = 'DMARC' Severity = 'High' Description = 'Verifies a DMARC record is present, its policy is quarantine or reject rather than none, and rua reporting is configured.' RequiresModule = @('ExchangeOnlineManagement') } # Use pre-fetched accepted domains from context when available; fall back to a live query. $domains = $null if ($METContext -and $METContext.AcceptedDomains.Count -gt 0) { $domains = @($METContext.AcceptedDomains | Where-Object { $_.Default -or $_.DomainType -eq 'Authoritative' }) } if (-not $domains) { try { $domains = @(Get-AcceptedDomain -ErrorAction Stop | Where-Object { $_.Default -or $_.DomainType -eq 'Authoritative' }) } catch { New-METCheckResult -CheckId 'MET-EXO001' -Category EXO -Name 'DMARC' ` -Result Fail -Severity High -AffectedObject 'Accepted Domains' ` -Finding 'Unable to retrieve accepted domains' ` -Recommendation 'Ensure the account has Exchange View-Only Recipients permission.' ` -ReferenceUrl 'https://aka.ms/dmarc' -ErrorMessage $_.ToString() return } } function Get-METDmarcRecommendation { param( [Parameter(Mandatory)] [string] $DomainName, [Parameter(Mandatory)] [bool] $IsOnMicrosoftDomain ) if ($IsOnMicrosoftDomain) { return "Add a DMARC TXT record for $DomainName in Microsoft 365 admin center (Settings > Domains > $DomainName > DNS records). Recommended value: 'v=DMARC1; p=reject; rua=mailto:dmarc-reports@$DomainName'." } return "Publish a DMARC TXT record at _dmarc.$DomainName with at minimum 'v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@$DomainName'." } foreach ($domain in $domains) { $domainName = [string]$domain.DomainName $isMailOnMicrosoft = $domainName -match '(?i)\.mail\.onmicrosoft\.com$' $isOnMicrosoftDomain = $domainName -match '(?i)\.onmicrosoft\.com$' if ($isMailOnMicrosoft) { New-METCheckResult -CheckId 'MET-EXO001' -Category EXO -Name 'DMARC' ` -Result NotApplicable -Severity Informational -AffectedObject $domainName ` -Finding 'mail.onmicrosoft.com service domain is Microsoft-managed and not intended for customer DMARC DNS management.' ` -Recommendation 'No action needed unless Microsoft guidance for this service domain changes.' ` -ReferenceUrl 'https://aka.ms/dmarc' continue } $dmarcRecord = $null try { $dns = Resolve-METDnsName -Name "_dmarc.$domainName" -Type TXT $dmarcRecord = $dns | Where-Object { $_.Strings -match '^v=DMARC1' } | Select-Object -First 1 } catch { New-METCheckResult -CheckId 'MET-EXO001' -Category EXO -Name 'DMARC' ` -Result Warning -Severity High -AffectedObject $domainName ` -Finding 'Unable to determine DMARC status because the DNS lookup failed' ` -Recommendation 'Restore DNS connectivity or install dig/nslookup, then rerun the assessment.' ` -ReferenceUrl 'https://aka.ms/dmarc' -ErrorMessage $_.ToString() continue } if (-not $dmarcRecord) { New-METCheckResult -CheckId 'MET-EXO001' -Category EXO -Name 'DMARC' ` -Result Fail -Severity High -AffectedObject $domainName ` -Finding 'No DMARC TXT record found' ` -Recommendation (Get-METDmarcRecommendation -DomainName $domainName -IsOnMicrosoftDomain $isOnMicrosoftDomain) ` -ReferenceUrl 'https://aka.ms/dmarc' continue } $record = ($dmarcRecord.Strings -join '') -join '' $issues = [System.Collections.Generic.List[string]]::new() # Tags are matched as ; -delimited name=value pairs, not substrings of the whole # record, so sp=/np= (subdomain / non-existent-subdomain policy) can never be # misread as p= (the domain's own policy) - a plain 'p=none' regex matches inside # both. RFC 7489 6.4 also allows whitespace around '='. $tags = @{} foreach ($part in $record -split ';') { if ($part -match '^\s*([A-Za-z][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$') { $tagName = $Matches[1].ToLowerInvariant() if (-not $tags.ContainsKey($tagName)) { $tags[$tagName] = $Matches[2] } } } if (-not $tags.ContainsKey('p')) { $issues.Add('DMARC record has no policy (p=) tag - the record is not valid and receivers will ignore it') } elseif ($tags['p'] -eq 'none') { $issues.Add("DMARC policy is 'none' - no enforcement; emails failing DMARC are not quarantined or rejected") } elseif ($tags['p'] -notin @('quarantine', 'reject')) { $issues.Add('DMARC policy is not set to quarantine or reject') } if (-not $tags.ContainsKey('rua') -or [string]::IsNullOrWhiteSpace($tags['rua'])) { $issues.Add('No aggregate reporting address (rua=) configured - DMARC reports will not be received') } if ($tags.ContainsKey('sp') -and $tags['sp'] -eq 'none' -and $tags.ContainsKey('p') -and $tags['p'] -in @('quarantine', 'reject')) { $issues.Add("Subdomain policy is 'none' (sp=none) - subdomains of this domain are unprotected even though the domain policy enforces") } if ($issues.Count -gt 0) { New-METCheckResult -CheckId 'MET-EXO001' -Category EXO -Name 'DMARC' ` -Result Fail -Severity High -AffectedObject $domainName ` -Finding "$($issues -join '; ') | Record: $record" ` -Recommendation "Update DMARC policy to 'quarantine' or 'reject' and add an rua= reporting address." ` -ReferenceUrl 'https://aka.ms/dmarc' } else { New-METCheckResult -CheckId 'MET-EXO001' -Category EXO -Name 'DMARC' ` -Result Pass -Severity High -AffectedObject $domainName ` -Finding "DMARC record present with enforcement policy and reporting configured | Record: $record" ` -ReferenceUrl 'https://aka.ms/dmarc' } } |