public/cisa/exchange/ConvertFrom-MailAuthenticationRecordSpf.ps1
|
<# .SYNOPSIS Returns a structured RFC compliant object for the supplied SPF record .DESCRIPTION Adapted from: - https://cloudbrothers.info/en/powershell-tip-resolve-spf/ - https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1 - https://xkln.net/blog/getting-mx-spf-dmarc-dkim-and-smtp-banners-with-powershell/ - SPF https://datatracker.ietf.org/doc/html/rfc7208 ``` record : v=spf1 include:_spf-a.microsoft.com include:_spf-b.microsoft.com include:_spf-c.microsoft.com include:_spf-ssg-a.msft.net include:spf-a.hotmail.com include:_spf1-meo.microsoft.com -all terms : {SPFRecordTerm, SPFRecordTerm, SPFRecordTerm, SPFRecordTerm…} warnings : ``` .EXAMPLE ConvertFrom-MailAuthenticationRecordSpf -DomainName "microsoft.com" Returns [SPFRecord] object or "Failure to obtain record" .LINK https://maester.dev/docs/commands/ConvertFrom-MailAuthenticationRecordSpf #> function ConvertFrom-MailAuthenticationRecordSpf { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are beautiful')] [OutputType([SPFRecord], [System.String])] [cmdletbinding()] param( [Parameter(Mandatory)] # Domain name to check. [string]$DomainName, # DNS-server to use for lookup. [ipaddress]$DnsServerIpAddress = "1.1.1.1", # Use a shorter timeout value for the DNS lookup. [switch]$QuickTimeout, # Ignore hosts file for domain lookup. [switch]$NoHostsFile ) begin { #todo, check that all is always at end, ###check that ptr isn't used, ###check for repeat modifiers, ###check for all and redirect, ###check for unrecognized modifiers, ###recommend exp if not found #https://datatracker.ietf.org/doc/html/rfc7208#section-6.2, ###check for macros #https://datatracker.ietf.org/doc/html/rfc7208#section-7, ###check for 10* include, a, mx, ptr, exists #[SPFRecordTerm]::new("include:_spf-a.microsoft.com") class SPFRecordTerm { [string]$term #term [string]$directive #directive [ValidateSet("+", "-", "~", "?", "")] [string]$qualifier #qual [ValidateSet("all", "include", "a", "mx", "ptr", "ip4", "ip6", "exists", "")] [string]$mechanism #mech [string]$mechanismTarget #mechTarget [string]$mechanismTargetCidr #cidr [ValidateSet("redirect", "exp", "")] [string]$modifier #mod [string]$modifierTarget #modTarget hidden $option = [Text.RegularExpressions.RegexOptions]::IgnoreCase hidden $matchTerms = "\s*(?'term'(?'directive'(?'qual'\+|-|~|\?)?(?'mech'all|include|a|mx|ptr|ip4|ip6|exists)(?::?(?'mechTarget'[^\s]+?(?'cidr'\/[^\s]+)?))?)(?:\s|$)|(?'mod'redirect|exp)(?:=(?'modTarget'[^\s]+))(?:\s|$))" SPFRecordTerm([string]$term) { $this.term = $term $match = [regex]::Match($term, $this.matchTerms, $this.option) $this.directive = ($match.Groups | Where-Object { $_.Name -eq "directive" }).Value $qVal = ($match.Groups | Where-Object { $_.Name -eq "qual" }).Value if ($qVal -eq "") { $q = "?" } else { $q = $qVal } $this.qualifier = $q $this.mechanism = ($match.Groups | Where-Object { $_.Name -eq "mech" }).Value $this.mechanismTarget = ($match.Groups | Where-Object { $_.Name -eq "mechTarget" }).Value $this.mechanismTargetCidr = ($match.Groups | Where-Object { $_.Name -eq "cidr" }).Value $this.modifier = ($match.Groups | Where-Object { $_.Name -eq "mod" }).Value $this.modifierTarget = ($match.Groups | Where-Object { $_.Name -eq "modTarget" }).Value } } #[spfrecord]::new("v=spf1 include:_spf-a.microsoft.com include:_spf-b.microsoft.com include:_spf-c.microsoft.com include:_spf-ssg-a.msft.net include:spf-a.hotmail.com include:_spf1-meo.microsoft.com -all") class SPFRecord { [string]$record [SPFRecordTerm[]]$terms [string]$warnings hidden $option = [Text.RegularExpressions.RegexOptions]::IgnoreCase# -bor [Text.RegularExpressions.RegexOptions]::Singleline hidden $matchRecord = "^v=spf1 .*$" #https://datatracker.ietf.org/doc/html/rfc7208#section-12 hidden $matchTerms = "\s*(?'term'(?'directive'(?'qual'\+|-|~|\?)?(?'mech'all|include|a|mx|ptr|ip4|ip6|exists)(?::?(?'mechTarget'[^\s]+?(?'cidr'\/[^\s]+)?))?)(?:\s|$)|(?'mod'redirect|exp)(?:=(?'modTarget'[^\s]+))(?:\s|$))" SPFRecord([string]$inputRecord) { $this.record = $inputRecord $match = [regex]::Matches($inputRecord, $this.matchRecord, $this.option) if (-not $match) { $this.warnings += "v: Record does not match spf1 version format" break } if (($match | Measure-Object).Count -gt 1) { $this.warnings += "v: Multiple records match spf1 version format" break } $recordTerms = [regex]::Matches($inputRecord, $this.matchTerms, $this.option) foreach ($term in ($recordTerms.Groups | Where-Object { $_.Name -eq "term" })) { $this.terms += [SPFRecordTerm]::new($term.Value) } } } } process { $matchRecord = "^v=spf1 .*$" $spfSplat = @{ Name = $DomainName Type = "TXT" Server = $DnsServerIpAddress NoHostsFile = $NoHostsFile QuickTimeout = $QuickTimeout ErrorAction = "Stop" } try { if ( $isWindows -or $PSVersionTable.PSEdition -eq "Desktop" ) { $spfRecord = [SPFRecord]::new((Resolve-DnsName @spfSplat | ` Where-Object { $_.Type -eq "TXT" } | ` Where-Object { $_.Strings -imatch $matchRecord }).Strings) } else { $cmdletCheck = Get-Command "Resolve-Dns" -ErrorAction SilentlyContinue if ($cmdletCheck) { $spfSplatAlt = @{ Query = $spfSplat.Name QueryType = $spfSplat.Type NameServer = $spfSplat.Server ErrorAction = $spfSplat.ErrorAction } $spfRecord = [SPFRecord]::new(((Resolve-Dns @spfSplatAlt).Answers | ` Where-Object { $_.RecordType -eq "TXT" } | ` Where-Object { $_.Text -imatch $matchRecord }).Text) } else { Write-Verbose "`nFor non-Windows platforms, please install DnsClient-PS module." Write-Verbose "`n Install-Module DnsClient-PS -Scope CurrentUser`n" -ForegroundColor Yellow return "Missing dependency, Resolve-Dns not available" } } } catch [System.Management.Automation.CommandNotFoundException] { Write-Verbose $_ return "Unsupported platform, Resolve-DnsName not available" } catch { Write-Verbose $_ return "Failure to obtain record" } return $spfRecord } } |