Checks/EXO/MET-EXO003-SPF.ps1
|
# 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-EXO003' -Category EXO -Name 'SPF' ` -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/spf' -ErrorMessage $_.ToString() return } } function Measure-SpfLookups { param([string] $DomainName, [int] $Depth = 0, [System.Collections.Generic.HashSet[string]] $Visited = $null) if ($Depth -gt 5) { return 0 } if (-not $Visited) { $Visited = [System.Collections.Generic.HashSet[string]]::new() } if (-not $Visited.Add($DomainName)) { return 0 } $count = 0 try { $txt = Resolve-METDnsName -Name $DomainName -Type TXT | Where-Object { $_.Strings -match '^v=spf1' } | Select-Object -First 1 if (-not $txt) { return 0 } $record = $txt.Strings -join '' $terms = $record -split '\s+' | Where-Object { $_ } foreach ($term in $terms) { if ($term -eq 'v=spf1') { continue } $normalized = $term -replace '^[\+\-\~\?]', '' if ($normalized -match '^include:([^\s]+)$') { $count += 1 $count += Measure-SpfLookups -DomainName $Matches[1] -Depth ($Depth + 1) -Visited $Visited continue } if ($normalized -match '^redirect=([^\s]+)$') { $count += 1 $count += Measure-SpfLookups -DomainName $Matches[1] -Depth ($Depth + 1) -Visited $Visited continue } if ($normalized -match '^(a|mx|ptr)([:/].*)?$' -or $normalized -match '^exists:([^\s]+)$') { $count += 1 } } } catch { Write-Verbose "DNS lookup failed for '$DomainName' during SPF lookup count: $_" } return $count } foreach ($domain in $domains) { $spfRecord = $null $lookupError = $null try { $dns = Resolve-METDnsName -Name $domain.DomainName -Type TXT $spfRecord = $dns | Where-Object { $_.Strings -match '^v=spf1' } | Select-Object -First 1 } catch { $lookupError = $_ Write-Verbose "DNS lookup failed for '$($domain.DomainName)': $_" } if ($lookupError) { New-METCheckResult -CheckId 'MET-EXO003' -Category EXO -Name 'SPF' ` -Result Warning -Severity High -AffectedObject $domain.DomainName ` -Finding 'Unable to determine SPF status because the DNS lookup failed' ` -Recommendation 'Restore DNS connectivity or install dig/nslookup, then rerun the assessment.' ` -ReferenceUrl 'https://aka.ms/spf' -ErrorMessage $lookupError.ToString() continue } if (-not $spfRecord) { New-METCheckResult -CheckId 'MET-EXO003' -Category EXO -Name 'SPF' ` -Result Fail -Severity High -AffectedObject $domain.DomainName ` -Finding 'No SPF TXT record found' ` -Recommendation "Publish an SPF record: 'v=spf1 include:spf.protection.outlook.com -all'" ` -ReferenceUrl 'https://aka.ms/spf' continue } $record = $spfRecord.Strings -join '' $issues = [System.Collections.Generic.List[string]]::new() if ($record -match '\+all') { $issues.Add("SPF record uses '+all' (allow all) - any server can send as this domain") } elseif ($record -notmatch '-all' -and $record -notmatch '~all') { $issues.Add("SPF record does not end with '-all' or '~all' - enforcement is missing") } elseif ($record -match '~all') { $issues.Add("SPF record uses '~all' (soft fail) - consider '-all' for strict enforcement") } $lookupCount = Measure-SpfLookups -DomainName $domain.DomainName if ($lookupCount -gt 10) { $issues.Add("SPF record exceeds 10 DNS lookups ($lookupCount) - may cause SPF permerror") } if ($issues.Count -gt 0) { $result = if ($record -match '\+all') { 'Fail' } else { 'Warning' } New-METCheckResult -CheckId 'MET-EXO003' -Category EXO -Name 'SPF' ` -Result $result -Severity High -AffectedObject $domain.DomainName ` -Finding "$($issues -join '; ') | Record: $record" ` -Recommendation "Use '-all' to strictly reject unauthorised senders. Reduce includes to stay within the 10-lookup limit." ` -ReferenceUrl 'https://aka.ms/spf' } else { New-METCheckResult -CheckId 'MET-EXO003' -Category EXO -Name 'SPF' ` -Result Pass -Severity High -AffectedObject $domain.DomainName ` -Finding "SPF record is present and correctly configured ($lookupCount DNS lookups) | Record: $record" ` -ReferenceUrl 'https://aka.ms/spf' } } |