diff --git a/DNSHealth/Public/DNS/Read-DkimRecord.ps1 b/DNSHealth/Public/DNS/Read-DkimRecord.ps1 deleted file mode 100644 index 88e81f0..0000000 --- a/DNSHealth/Public/DNS/Read-DkimRecord.ps1 +++ /dev/null @@ -1,259 +0,0 @@ -function Read-DkimRecord { - <# - .SYNOPSIS - Read DKIM record from DNS - - .DESCRIPTION - Validates DKIM records on a domain a selector - - .PARAMETER Domain - Domain to check - - .PARAMETER Selectors - Selector records to check - - .PARAMETER MxLookup - Lookup record based on MX - - .EXAMPLE - PS> Read-DkimRecord -Domain example.com -Selector test - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain, - - [Parameter()] - [System.Collections.Generic.List[string]]$Selectors = @() - ) - - $MXRecord = $null - $MinimumSelectorPass = 0 - $SelectorPasses = 0 - - $DkimAnalysis = [PSCustomObject]@{ - Domain = $Domain - Selectors = $Selectors - MailProvider = '' - Records = [System.Collections.Generic.List[object]]::new() - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - } - - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationWarns = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - # MX lookup, check for defined selectors - try { - $MXRecord = Read-MXRecord -Domain $Domain - foreach ($Selector in $MXRecord.Selectors) { - try { - $Selectors.Add($Selector) | Out-Null - } - - catch { Write-Verbose $_.Exception.Message } - } - $DkimAnalysis.MailProvider = $MXRecord.MailProvider - if ($MXRecord.MailProvider.PSObject.Properties.Name -contains 'MinimumSelectorPass') { - $MinimumSelectorPass = $MXRecord.MailProvider.MinimumSelectorPass - } - $DkimAnalysis.Selectors = $Selectors - } - - catch { Write-Verbose $_.Exception.Message } - - # Get unique selectors - $Selectors = $Selectors | Sort-Object -Unique - - if (($Selectors | Measure-Object | Select-Object -ExpandProperty Count) -gt 0) { - foreach ($Selector in $Selectors) { - if (![string]::IsNullOrEmpty($Selector)) { - # Initialize object - $DkimRecord = [PSCustomObject]@{ - Selector = '' - Record = '' - Version = '' - PublicKey = '' - PublicKeyInfo = '' - KeyType = '' - Flags = '' - Notes = '' - HashAlgorithms = '' - ServiceType = '' - Granularity = '' - UnrecognizedTags = [System.Collections.Generic.List[object]]::new() - } - - $DnsQuery = @{ - RecordType = 'TXT' - Domain = "$Selector._domainkey.$Domain" - } - - try { - $QueryResults = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - } - - catch { - $Message = "{0}`r`n{1}" -f $_.Exception.Message, ($DnsQuery | ConvertTo-Json) - throw $Message - } - if ([string]::IsNullOrEmpty($Selector)) { continue } - - if ($QueryResults.Status -eq 2 -and $QueryResults.AD -eq $false) { - $ValidationFails.Add('DNSSEC validation failed.') | Out-Null - } - if ($QueryResults -eq '' -or $QueryResults.Status -ne 0) { - if ($QueryResults.Status -eq 3) { - if ($MinimumSelectorPass -eq 0) { - $ValidationFails.Add("$Selector - The selector record does not exist for this domain.") | Out-Null - } - } - - else { - $ValidationFails.Add("$Selector - DKIM record is missing, check the selector and try again") | Out-Null - } - $Record = '' - } - - else { - $QueryData = ($QueryResults.Answer).data | Where-Object { $_ -match '(v=|k=|t=|p=)' } - if (( $QueryData | Measure-Object).Count -gt 1) { - $Record = $QueryData[-1] - } - - else { - $Record = $QueryData - } - } - $DkimRecord.Selector = $Selector - - if ($null -eq $Record) { $Record = '' } - $DkimRecord.Record = $Record - - # Split DKIM record into name/value pairs - $TagList = [System.Collections.Generic.List[object]]::new() - Foreach ($Element in ($Record -split ';')) { - if ($Element -ne '') { - $Name, $Value = $Element.trim() -split '=' - $TagList.Add( - [PSCustomObject]@{ - Name = $Name - Value = $Value - } - ) | Out-Null - } - } - - # Loop through name/value pairs and set object properties - $x = 0 - foreach ($Tag in $TagList) { - if ($x -eq 0 -and $Tag.Value -ne 'DKIM1') { $ValidationFails.Add("$Selector - The record must being with 'v=DKIM1'.") | Out-Null } - - switch ($Tag.Name) { - 'v' { - # REQUIRED: Version - if ($x -ne 0) { $ValidationFails.Add("$Selector - The record must being with 'v=DKIM1'.") | Out-Null } - $DkimRecord.Version = $Tag.Value - } - 'p' { - # REQUIRED: Public Key - if ($Tag.Value -ne '') { - $DkimRecord.PublicKey = "-----BEGIN PUBLIC KEY-----`n {0}`n-----END PUBLIC KEY-----" -f $Tag.Value - $DkimRecord.PublicKeyInfo = Get-RsaPublicKeyInfo -EncodedString $Tag.Value - } - - else { - if ($MXRecord.MailProvider.Name -eq 'Null') { - $ValidationPasses.Add("$Selector - DKIM configuration is valid for a Null MX record configuration.") | Out-Null - } - - else { - $ValidationFails.Add("$Selector - There is no public key specified for this DKIM record or the key is revoked.") | Out-Null - } - } - } - 'k' { - $DkimRecord.KeyType = $Tag.Value - } - 't' { - $DkimRecord.Flags = $Tag.Value - } - 'n' { - $DkimRecord.Notes = $Tag.Value - } - 'h' { - $DkimRecord.HashAlgorithms = $Tag.Value - } - 's' { - $DkimRecord.ServiceType = $Tag.Value - } - 'g' { - $DkimRecord.Granularity = $Tag.Value - } - default { - $DkimRecord.UnrecognizedTags.Add($Tag) | Out-Null - } - } - $x++ - } - - if ($Record -ne '') { - if ($DkimRecord.KeyType -eq '') { $DkimRecord.KeyType = 'rsa' } - - if ($DkimRecord.HashAlgorithms -eq '') { $DkimRecord.HashAlgorithms = 'all' } - - $UnrecognizedTagCount = $UnrecognizedTags | Measure-Object | Select-Object -ExpandProperty Count - if ($UnrecognizedTagCount -gt 0) { - $TagString = ($UnrecognizedTags | ForEach-Object { '{0}={1}' -f $_.Tag, $_.Value }) -join ', ' - $ValidationWarns.Add("$Selector - $UnrecognizedTagCount urecognized tag(s) were detected in the DKIM record. This can cause issues with some mailbox providers. Tags: $TagString") - } - if ($DkimRecord.Flags -eq 'y') { - $ValidationWarns.Add("$Selector - The flag 't=y' indicates that this domain is testing mode currently. If DKIM is fully deployed, this flag should be changed to t=s unless subdomaining is required.") | Out-Null - } - - if ($DkimRecord.PublicKeyInfo.SignatureAlgorithm -ne $DkimRecord.KeyType -and $MXRecord.MailProvider.Name -ne 'Null') { - $ValidationWarns.Add("$Selector - Key signature algorithm $($DkimRecord.PublicKeyInfo.SignatureAlgorithm) does not match $($DkimRecord.KeyType)") | Out-Null - } - - if ($DkimRecord.PublicKeyInfo.KeySize -lt 1024 -and $MXRecord.MailProvider.Name -ne 'Null') { - $ValidationFails.Add("$Selector - Key size is less than 1024 bit, found $($DkimRecord.PublicKeyInfo.KeySize).") | Out-Null - } - - else { - if ($MXRecord.MailProvider.Name -ne 'Null') { - $ValidationPasses.Add("$Selector - DKIM key validation succeeded.") | Out-Null - } - $SelectorPasses++ - } - - if (($ValidationFails | Measure-Object | Select-Object -ExpandProperty Count) -eq 0) { - $ValidationPasses.Add("$Selector - No errors detected with DKIM record.") | Out-Null - } - } - ($DkimAnalysis.Records).Add($DkimRecord) | Out-Null - } - } - } - if (($DkimAnalysis.Records | Measure-Object | Select-Object -ExpandProperty Count) -eq 0 -and [string]::IsNullOrEmpty($DkimAnalysis.Selectors)) { - $ValidationWarns.Add('No DKIM selectors provided, set them in the domain options.') | Out-Null - } - - if ($MinimumSelectorPass -gt 0 -and $SelectorPasses -eq 0) { - $ValidationFails.Add(('{0} DKIM record(s) found. The minimum number of valid records ({1}) was not met.' -f $SelectorPasses, $MinimumSelectorPass)) | Out-Null - } - - elseif ($MinimumSelectorPass -gt 0 -and $SelectorPasses -ge $MinimumSelectorPass) { - $ValidationPasses.Add(('Minimum number of valid DKIM records were met {0}/{1}.' -f $SelectorPasses, $MinimumSelectorPass)) - } - - # Collect validation results - $DkimAnalysis.ValidationPasses = @($ValidationPasses) - $DkimAnalysis.ValidationWarns = @($ValidationWarns) - $DkimAnalysis.ValidationFails = @($ValidationFails) - - # Return analysis - $DkimAnalysis -} diff --git a/DNSHealth/Public/DNS/Read-DmarcPolicy.ps1 b/DNSHealth/Public/DNS/Read-DmarcPolicy.ps1 deleted file mode 100644 index 6adf5da..0000000 --- a/DNSHealth/Public/DNS/Read-DmarcPolicy.ps1 +++ /dev/null @@ -1,272 +0,0 @@ -function Read-DmarcPolicy { - <# - .SYNOPSIS - Resolve and validate DMARC policy - - .DESCRIPTION - Query domain for DMARC policy (_dmarc.domain.com) and parse results. Record is checked for issues. - - .PARAMETER Domain - Domain to process DMARC policy - - .EXAMPLE - PS> Read-DmarcPolicy -Domain gmail.com - - Domain : gmail.com - Record : v=DMARC1; p=none; sp=quarantine; rua=mailto:mailauth-reports@google.com - Version : DMARC1 - Policy : none - SubdomainPolicy : quarantine - Percent : 100 - DkimAlignment : r - SpfAlignment : r - ReportFormat : afrf - ReportInterval : 86400 - ReportingEmails : {mailauth-reports@google.com} - ForensicEmails : {} - FailureReport : 0 - ValidationPasses : {Aggregate reports are being sent} - ValidationWarns : {Policy is not being enforced, Subdomain policy is only partially enforced with quarantine, Failure report option 0 will only generate a report on both SPF and DKIM misalignment. It is recommended to set this value to 1} - ValidationFails : {} - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - - # Initialize object - $DmarcAnalysis = [PSCustomObject]@{ - Domain = $Domain - Record = '' - Version = '' - Policy = '' - SubdomainPolicy = '' - Percent = 100 - DkimAlignment = 'r' - SpfAlignment = 'r' - ReportFormat = 'afrf' - ReportInterval = 86400 - ReportingEmails = [System.Collections.Generic.List[string]]::new() - ForensicEmails = [System.Collections.Generic.List[string]]::new() - FailureReport = '' - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - } - - # Validation lists - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationWarns = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - # Email report domains - $ReportDomains = [System.Collections.Generic.List[string]]::new() - - # Validation ranges - $PolicyValues = @('none', 'quarantine', 'reject') - $FailureReportValues = @('0', '1', 'd', 's') - $ReportFormatValues = @('afrf') - - $RecordCount = 0 - - $DnsQuery = @{ - RecordType = 'TXT' - Domain = "_dmarc.$Domain" - } - - # Resolve DMARC record - - $Query = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - - $RecordCount = 0 - $Query.Answer | Where-Object { $_.data -match '^v=DMARC1' } | ForEach-Object { - $DmarcRecord = $_.data - $DmarcAnalysis.Record = $DmarcRecord - $RecordCount++ - } - if ($Query.Status -eq 2 -and $Query.AD -eq $false) { - $ValidationFails.Add('DNSSEC validation failed.') | Out-Null - } - - elseif ($Query.Status -ne 0 -or $RecordCount -eq 0) { - $ValidationFails.Add('This domain does not have a DMARC record.') | Out-Null - } - - elseif (($Query.Answer | Measure-Object).Count -eq 1 -and $RecordCount -eq 0) { - $ValidationFails.Add("The record must begin with 'v=DMARC1'.") | Out-Null - } - - elseif ($RecordCount -gt 1) { - $ValidationFails.Add('This domain has multiple records. The policy evaluation will fail.') | Out-Null - } - - # Split DMARC record into name/value pairs - $TagList = [System.Collections.Generic.List[object]]::new() - Foreach ($Element in ($DmarcRecord -split ';').trim()) { - $Name, $Value = $Element -split '=' - $TagList.Add( - [PSCustomObject]@{ - Name = $Name - Value = $Value - } - ) | Out-Null - } - - # Loop through name/value pairs and set object properties - $x = 0 - foreach ($Tag in $TagList) { - switch ($Tag.Name) { - 'v' { - # REQUIRED: Version - $DmarcAnalysis.Version = $Tag.Value - } - 'p' { - # REQUIRED: Policy - $DmarcAnalysis.Policy = $Tag.Value - } - 'sp' { - # Subdomain policy, defaults to policy record - $DmarcAnalysis.SubdomainPolicy = $Tag.Value - } - 'rua' { - # Aggregate report emails - $ReportingEmails = $Tag.Value -split ', ' - $ReportEmailsSet = $false - foreach ($MailTo in $ReportingEmails) { - if ($MailTo -notmatch '^mailto:') { $ValidationFails.Add("Aggregate report email addresses must begin with 'mailto:', multiple addresses must be separated by commas.") | Out-Null } - else { - $ReportEmailsSet = $true - if ($MailTo -match '^mailto:(?.+@(?[^!]+?))(?:!(?[0-9]+[kmgt]?))?$') { - if ($ReportDomains -notcontains $Matches.Domain -and $Matches.Domain -ne $Domain) { - $ReportDomains.Add($Matches.Domain) | Out-Null - } - $DmarcAnalysis.ReportingEmails.Add($Matches.Email) | Out-Null - } - } - } - if (!$DmarcAnalysis.ReportingEmails) { $DmarcAnalysis.ReportingEmails.Add($null) } - if ($ReportEmailsSet) { - $ValidationPasses.Add('Aggregate reports are being sent.') | Out-Null - } - - else { - $ValidationWarns.Add('Aggregate reports are not being sent.') | Out-Null - } - } - 'ruf' { - # Forensic reporting emails - foreach ($MailTo in ($Tag.Value -split ', ')) { - if ($MailTo -notmatch '^mailto:') { $ValidationFails.Add("Forensic report email must begin with 'mailto:', multiple addresses must be separated by commas - found $($Tag.Value)") | Out-Null } - else { - if ($MailTo -match '^mailto:(?.+@(?[^!]+?))(?:!(?[0-9]+[kmgt]?))?$') { - if ($ReportDomains -notcontains $Matches.Domain -and $Matches.Domain -ne $Domain) { - $ReportDomains.Add($Matches.Domain) | Out-Null - } - $DmarcAnalysis.ForensicEmails.Add($Matches.Email) | Out-Null - } - } - } - } - 'fo' { - # Failure reporting options - $DmarcAnalysis.FailureReport = $Tag.Value - } - 'pct' { - # Percentage of email to check - $DmarcAnalysis.Percent = [int]$Tag.Value - } - 'adkim' { - # DKIM Alignmenet - $DmarcAnalysis.DkimAlignment = $Tag.Value - } - 'aspf' { - # SPF Alignment - $DmarcAnalysis.SpfAlignment = $Tag.Value - } - 'rf' { - # Report Format - $DmarcAnalysis.ReportFormat = $Tag.Value - } - 'ri' { - # Report Interval - $DmarcAnalysis.ReportInterval = $Tag.Value - } - } - $x++ - } - - if ($RecordCount -gt 0) { - # Check report domains for DMARC reporting record - $ReportDomainCount = $ReportDomains | Measure-Object | Select-Object -ExpandProperty Count - if ($ReportDomainCount -gt 0) { - $ReportDomainsPass = $true - foreach ($ReportDomain in $ReportDomains) { - $ReportDomainQuery = "$Domain._report._dmarc.$ReportDomain" - $DnsQuery['Domain'] = $ReportDomainQuery - $ReportDmarcQuery = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - $ReportDmarcRecord = $ReportDmarcQuery.Answer.data - if ($null -eq $ReportDmarcQuery -or $ReportDmarcQuery.Status -ne 0) { - $ValidationWarns.Add("Report DMARC policy for $Domain is missing from $ReportDomain, reports will not be delivered. Expected record: '$Domain._report._dmarc.$ReportDomain' - Expected value: 'v=DMARC1;'") | Out-Null - $ReportDomainsPass = $false - } - - elseif ($ReportDmarcRecord -notmatch '^v=DMARC1') { - $ValidationWarns.Add("Report DMARC policy for $Domain is missing from $ReportDomain, reports will not be delivered. Expected record: '$Domain._report._dmarc.$ReportDomain' - Expected value: 'v=DMARC1;'.") | Out-Null - $ReportDomainsPass = $false - } - } - - if ($ReportDomainsPass) { - $ValidationPasses.Add('All external reporting domains allow this domain to send DMARC reports.') | Out-Null - } - - } - # Check for missing record tags and set defaults - if ($DmarcAnalysis.Policy -eq '') { $ValidationFails.Add('The policy tag (p=) is missing from this record. Set this to none, quarantine or reject.') | Out-Null } - if ($DmarcAnalysis.SubdomainPolicy -eq '') { $DmarcAnalysis.SubdomainPolicy = $DmarcAnalysis.Policy } - - # Check policy for errors and best practice - if ($PolicyValues -notcontains $DmarcAnalysis.Policy) { $ValidationFails.Add("The policy must be one of the following: none, quarantine or reject. Found $($Tag.Value)") | Out-Null } - if ($DmarcAnalysis.Policy -eq 'reject') { $ValidationPasses.Add('The domain policy is set to reject, this is best practice.') | Out-Null } - if ($DmarcAnalysis.Policy -eq 'quarantine') { $ValidationWarns.Add('The domain policy is only partially enforced with quarantine. Set this to reject to be fully compliant.') | Out-Null } - if ($DmarcAnalysis.Policy -eq 'none') { $ValidationFails.Add('The domain policy is not being enforced.') | Out-Null } - - # Check subdomain policy - if ($PolicyValues -notcontains $DmarcAnalysis.SubdomainPolicy) { $ValidationFails.Add("The subdomain policy must be one of the following: none, quarantine or reject. Found $($DmarcAnalysis.SubdomainPolicy)") | Out-Null } - if ($DmarcAnalysis.SubdomainPolicy -eq 'reject') { $ValidationPasses.Add('The subdomain policy is set to reject, this is best practice.') | Out-Null } - if ($DmarcAnalysis.SubdomainPolicy -eq 'quarantine') { $ValidationWarns.Add('The subdomain policy is only partially enforced with quarantine. Set this to reject to be fully compliant.') | Out-Null } - if ($DmarcAnalysis.SubdomainPolicy -eq 'none') { $ValidationFails.Add('The subdomain policy is not being enforced.') | Out-Null } - - # Check percentage - validate range and ensure 100% - if ($DmarcAnalysis.Percent -lt 100 -and $DmarcAnalysis.Percent -ge 0) { $ValidationWarns.Add('Not all emails will be processed by the DMARC policy.') | Out-Null } - if ($DmarcAnalysis.Percent -gt 100 -or $DmarcAnalysis.Percent -lt 0) { $ValidationFails.Add('The percentage tag (pct=) must be between 0 and 100.') | Out-Null } - - # Check report format - if ($ReportFormatValues -notcontains $DmarcAnalysis.ReportFormat) { $ValidationFails.Add("The report format '$($DmarcAnalysis.ReportFormat)' is not supported.") | Out-Null } - - # Check forensic reports and failure options - $ForensicCount = ($DmarcAnalysis.ForensicEmails | Measure-Object | Select-Object -ExpandProperty Count) - if ($ForensicCount -eq 0 -and $DmarcAnalysis.FailureReport -ne '') { $ValidationWarns.Add('Forensic email reports recipients are not defined and failure report options are set. No reports will be sent. This is not an issue unless you are expecting forensic reports.') | Out-Null } - if ($DmarcAnalysis.FailureReport -eq '' -and $null -ne $DmarcRecord) { $DmarcAnalysis.FailureReport = '0' } - if ($ForensicCount -gt 0) { - $ReportOptions = $DmarcAnalysis.FailureReport -split ':' - foreach ($ReportOption in $ReportOptions) { - if ($FailureReportValues -notcontains $ReportOption) { $ValidationFails.Add("Failure report option '$ReportOption' is not a valid choice.") | Out-Null } - if ($ReportOption -eq '1') { $ValidationPasses.Add('Failure report option 1 generates forensic reports on SPF or DKIM misalignment.') | Out-Null } - if ($ReportOption -eq '0' -and $ReportOptions -notcontains '1') { $ValidationWarns.Add('Failure report option 0 will only generate a forensic report on both SPF and DKIM misalignment. It is recommended to set this value to 1.') | Out-Null } - if ($ReportOption -eq 'd' -and $ReportOptions -notcontains '1') { $ValidationWarns.Add('Failure report option d will only generate a forensic report on failed DKIM evaluation. It is recommended to set this value to 1.') | Out-Null } - if ($ReportOption -eq 's' -and $ReportOptions -notcontains '1') { $ValidationWarns.Add('Failure report option s will only generate a forensic report on failed SPF evaluation. It is recommended to set this value to 1.') | Out-Null } - } - } - } - - # Add the validation lists - $DmarcAnalysis.ValidationPasses = @($ValidationPasses) - $DmarcAnalysis.ValidationWarns = @($ValidationWarns) - $DmarcAnalysis.ValidationFails = @($ValidationFails) - - # Return DMARC analysis - $DmarcAnalysis -} diff --git a/DNSHealth/Public/DNS/Read-MXRecord.ps1 b/DNSHealth/Public/DNS/Read-MXRecord.ps1 deleted file mode 100644 index 7ff15ff..0000000 --- a/DNSHealth/Public/DNS/Read-MXRecord.ps1 +++ /dev/null @@ -1,152 +0,0 @@ -function Read-MXRecord { - <# - .SYNOPSIS - Reads MX records for domain - - .DESCRIPTION - Queries DNS servers to get MX records and returns in PSCustomObject list with Preference and Hostname - - .PARAMETER Domain - Domain to query - - .EXAMPLE - PS> Read-MXRecord -Domain gmail.com - - Preference Hostname - ---------- -------- - 5 gmail-smtp-in.l.google.com. - 10 alt1.gmail-smtp-in.l.google.com. - 20 alt2.gmail-smtp-in.l.google.com. - 30 alt3.gmail-smtp-in.l.google.com. - 40 alt4.gmail-smtp-in.l.google.com. - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - $MXResults = [PSCustomObject]@{ - Domain = '' - Records = [System.Collections.Generic.List[object]]::new() - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - MailProvider = '' - ExpectedInclude = '' - Selectors = '' - } - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - $DnsQuery = @{ - RecordType = 'mx' - Domain = $Domain - } - - $NoMxValidation = 'There are no mail exchanger records for this domain. If you do not want to receive mail for this domain use a Null MX record of . with a priority 0 (RFC 7505).' - - $MXResults.Domain = $Domain - - try { - $Result = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - } - - catch { $Result = $null } - if ($Result.Status -eq 2 -and $Result.AD -eq $false) { - $ValidationFails.Add('DNSSEC validation failed.') | Out-Null - } - - elseif ($Result.Status -ne 0 -or -not ($Result.Answer)) { - if ($Result.Status -eq 3) { - $ValidationFails.Add($NoMxValidation) | Out-Null - $MXResults.MailProvider = Get-Content 'MailProviders\Null.json' | ConvertFrom-Json - $MXResults.Selectors = $MXRecords.MailProvider.Selectors - } - - else { - $ValidationFails.Add($NoMxValidation) | Out-Null - $MXResults.MailProvider = Get-Content 'MailProviders\Null.json' | ConvertFrom-Json - $MXResults.Selectors = $MXRecords.MailProvider.Selectors - } - $MXRecords = $null - } - - else { - $MXRecords = $Result.Answer | ForEach-Object { - $Priority, $Hostname = $_.Data.Split(' ') - try { - [PSCustomObject]@{ - Priority = [int]$Priority - Hostname = $Hostname - } - } - - catch { Write-Verbose $_.Exception.Message } - } - $ValidationPasses.Add('Mail exchanger records record(s) are present for this domain.') | Out-Null - $MXRecords = $MXRecords | Sort-Object -Property Priority - - # Attempt to identify mail provider based on MX record - if (Test-Path "$PSScriptRoot\MailProviders") { - $ReservedVariables = @{ - 'DomainNameDashNotation' = $Domain -replace '\.', '-' - } - if ($MXRecords.Hostname -eq '') { - $ValidationFails.Add($NoMxValidation) | Out-Null - $MXResults.MailProvider = Get-Content "$PSScriptRoot\MailProviders\Null.json" | ConvertFrom-Json - } - - else { - $ProviderList = Get-ChildItem "$PSScriptRoot\MailProviders" -Exclude '_template.json' | ForEach-Object { - try { Get-Content $_ | ConvertFrom-Json -ErrorAction Stop } - catch { Write-Verbose $_.Exception.Message } - } - foreach ($Record in $MXRecords) { - $ProviderMatched = $false - foreach ($Provider in $ProviderList) { - try { - if ($Record.Hostname -match $Provider.MxMatch) { - $MXResults.MailProvider = $Provider - if (($Provider.SpfReplace | Measure-Object | Select-Object -ExpandProperty Count) -gt 0) { - $ReplaceList = [System.Collections.Generic.List[string]]::new() - foreach ($Var in $Provider.SpfReplace) { - if ($ReservedVariables.Keys -contains $Var) { - $ReplaceList.Add($ReservedVariables.$Var) | Out-Null - } - - else { - $ReplaceList.Add($Matches.$Var) | Out-Null - } - } - - $ExpectedInclude = $Provider.SpfInclude -f ($ReplaceList -join ', ') - } - - else { - $ExpectedInclude = $Provider.SpfInclude - } - - # Set ExpectedInclude and Selector fields based on provider details - $MXResults.ExpectedInclude = $ExpectedInclude - $MXResults.Selectors = $Provider.Selectors - $ProviderMatched = $true - break - } - } - - catch { Write-Verbose $_.Exception.Message } - } - if ($ProviderMatched) { - break - } - } - } - } - $MXResults.Records = $MXRecords - } - $MXResults.ValidationPasses = @($ValidationPasses) - $MXResults.ValidationFails = @($ValidationFails) - $MXResults.Records = @($MXResults.Records) - $MXResults -} diff --git a/DNSHealth/Public/DNS/Read-MtaStsPolicy.ps1 b/DNSHealth/Public/DNS/Read-MtaStsPolicy.ps1 deleted file mode 100644 index 0b5dba7..0000000 --- a/DNSHealth/Public/DNS/Read-MtaStsPolicy.ps1 +++ /dev/null @@ -1,125 +0,0 @@ -function Read-MtaStsPolicy { - <# - .SYNOPSIS - Resolve and validate MTA-STS policy - - .DESCRIPTION - Retrieve mta-sts.txt from .well-known directory on domain - - .PARAMETER Domain - Domain to process MTA-STS policy - - .EXAMPLE - PS> Read-MtaStsPolicy -Domain gmail.com - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - - $StsPolicyAnalysis = [PSCustomObject]@{ - Domain = $Domain - Version = '' - Mode = '' - Mx = [System.Collections.Generic.List[string]]::new() - MaxAge = '' - IsValid = $false - HasWarnings = $false - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - } - - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationWarns = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - # Valid policy modes - $StsPolicyModes = @('testing', 'enforce') - - # Request policy file from domain, only accept text/plain results - $RequestParams = @{ - Uri = ('https://mta-sts.{0}/.well-known/mta-sts.txt' -f $Domain) - Headers = @{ - Accept = 'text/plain' - } - } - - $PolicyExists = $false - try { - $wr = Invoke-WebRequest @RequestParams -ErrorAction Stop - $PolicyExists = $true - } - - catch { - $ValidationFails.Add(('MTA-STS policy does not exist for {0}' -f $Domain)) | Out-Null - } - - # Policy file is key value pairs split on new lines - $StsPolicyEntries = [System.Collections.Generic.List[object]]::new() - $Entries = $wr.Content -split "`r?`n" - foreach ($Entry in $Entries) { - if ($null -ne $Entry) { - try { - $Name, $Value = $Entry -split ':' - $StsPolicyEntries.Add( - [PSCustomObject]@{ - Name = $Name.trim() - Value = $Value.trim() - } - ) | Out-Null - } - - catch { Write-Verbose $_.Exception.Message } - } - } - - foreach ($StsPolicyEntry in $StsPolicyEntries) { - switch ($StsPolicyEntry.Name) { - 'version' { - # REQUIRED: Version - $StsPolicyAnalysis.Version = $StsPolicyEntry.Value - } - 'mode' { - $StsPolicyAnalysis.Mode = $StsPolicyEntry.Value - } - 'mx' { - $StsPolicyAnalysis.Mx.Add($StsPolicyEntry.Value) | Out-Null - } - 'max_age' { - $StsPolicyAnalysis.MaxAge = $StsPolicyEntry.Value - } - } - } - - # Check policy for issues - if ($PolicyExists) { - if ($StsPolicyAnalysis.Version -ne 'STSv1') { - $ValidationFails.Add("Version must be STSv1 - found $($StsPolicyEntry.Value)") | Out-Null - } - if ($StsPolicyAnalysis.Version -eq '') { - $ValidationFails.Add('Version is missing from policy') | Out-Null - } - if ($StsPolicyModes -notcontains $StsPolicyAnalysis.Mode) { - $ValidationFails.Add(('Policy mode "{0}" is not valid. (Options: {1})' -f $StsPolicyAnalysis.Mode, $StsPolicyModes -join ', ')) - } - if ($StsPolicyAnalysis.Mode -eq 'Testing') { - $ValidationWarns.Add('MTA-STS policy is in testing mode, no action will be taken') | Out-Null - $StsPolicyAnalysis.HasWarnings = $true - } - - $ValidationFailCount = ($ValidationFails | Measure-Object).Count - if ($ValidationFailCount -eq 0) { - $ValidationPasses.Add('MTA-STS policy is valid') - $StsPolicyAnalysis.IsValid = $true - } - } - - # Aggregate validation results - $StsPolicyAnalysis.ValidationPasses = @($ValidationPasses) - $StsPolicyAnalysis.ValidationWarns = @($ValidationWarns) - $StsPolicyAnalysis.ValidationFails = @($ValidationFails) - - $StsPolicyAnalysis -} diff --git a/DNSHealth/Public/DNS/Read-MtaStsRecord.ps1 b/DNSHealth/Public/DNS/Read-MtaStsRecord.ps1 deleted file mode 100644 index 54b7032..0000000 --- a/DNSHealth/Public/DNS/Read-MtaStsRecord.ps1 +++ /dev/null @@ -1,135 +0,0 @@ -function Read-MtaStsRecord { - <# - .SYNOPSIS - Resolve and validate MTA-STS record - - .DESCRIPTION - Query domain for DMARC policy (_mta-sts.domain.com) and parse results. Record is checked for issues. - - .PARAMETER Domain - Domain to process MTA-STS record - - .EXAMPLE - PS> Read-MtaStsRecord -Domain gmail.com - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - - # Initialize object - $StsAnalysis = [PSCustomObject]@{ - Domain = $Domain - Record = '' - Version = '' - Id = '' - IsValid = $false - HasWarnings = $false - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - } - - # Validation lists - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationWarns = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - # Validation ranges - - $RecordCount = 0 - - $DnsQuery = @{ - RecordType = 'TXT' - Domain = "_mta-sts.$Domain" - } - - # Resolve DMARC record - - $Query = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - - $RecordCount = 0 - $Query.Answer | Where-Object { $_.data -match '^v=STSv1' } | ForEach-Object { - $StsRecord = $_.data - $StsAnalysis.Record = $StsRecord - $RecordCount++ - } - if ($Query.Status -eq 2 -and $Query.AD -eq $false) { - $ValidationFails.Add('DNSSEC validation failed.') | Out-Null - } - - elseif ($Query.Status -ne 0 -or $RecordCount -eq 0) { - if ($Query.Status -eq 3) { - $ValidationFails.Add('Record does not exist (NXDOMAIN)') | Out-Null - } - - else { - $ValidationFails.Add("$Domain does not have an MTA-STS record") | Out-Null - } - } - - elseif ($RecordCount -gt 1) { - $ValidationFails.Add("$Domain has multiple MTA-STS records") | Out-Null - } - - # Split DMARC record into name/value pairs - $TagList = [System.Collections.Generic.List[object]]::new() - Foreach ($Element in ($StsRecord -split ';').trim()) { - $Name, $Value = $Element -split '=' - $TagList.Add( - [PSCustomObject]@{ - Name = $Name - Value = $Value - } - ) | Out-Null - } - - # Loop through name/value pairs and set object properties - $x = 0 - foreach ($Tag in $TagList) { - switch ($Tag.Name) { - 'v' { - # REQUIRED: Version - if ($x -ne 0) { $ValidationFails.Add('v=STSv1 must be at the beginning of the record') | Out-Null } - if ($Tag.Value -ne 'STSv1') { $ValidationFails.Add("Version must be STSv1 - found $($Tag.Value)") | Out-Null } - $StsAnalysis.Version = $Tag.Value - } - 'id' { - # REQUIRED: Id - $StsAnalysis.Id = $Tag.Value - } - - } - $x++ - } - - if ($RecordCount -gt 0) { - # Check for missing record tags and set defaults - if ($StsAnalysis.Id -eq '') { $ValidationFails.Add('Id record is missing') | Out-Null } - elseif ($StsAnalysis.Id -notmatch '^[A-Za-z0-9]+$') { - $ValidationFails.Add('STS Record ID must be alphanumeric') | Out-Null - } - - if ($RecordCount -gt 1) { - $ValidationWarns.Add('Multiple MTA-STS records detected, this may cause unexpected behavior.') | Out-Null - $StsAnalysis.HasWarnings = $true - } - - $ValidationWarnCount = ($Test.ValidationWarns | Measure-Object).Count - $ValidationFailCount = ($Test.ValidationFails | Measure-Object).Count - if ($ValidationFailCount -eq 0 -and $ValidationWarnCount -eq 0) { - $ValidationPasses.Add('MTA-STS record is valid') | Out-Null - $StsAnalysis.IsValid = $true - } - } - - # Add the validation lists - $StsAnalysis.ValidationPasses = @($ValidationPasses) - $StsAnalysis.ValidationWarns = @($ValidationWarns) - $StsAnalysis.ValidationFails = @($ValidationFails) - - # Return MTA-STS analysis - $StsAnalysis -} diff --git a/DNSHealth/Public/DNS/Read-NSRecord.ps1 b/DNSHealth/Public/DNS/Read-NSRecord.ps1 deleted file mode 100644 index 651b44b..0000000 --- a/DNSHealth/Public/DNS/Read-NSRecord.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -function Read-NSRecord { - <# - .SYNOPSIS - Reads NS records for domain - - .DESCRIPTION - Queries DNS servers to get NS records and returns in PSCustomObject list - - .PARAMETER Domain - Domain to query - - .EXAMPLE - PS> Read-NSRecord -Domain gmail.com - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - $NSResults = [PSCustomObject]@{ - Domain = '' - Records = [System.Collections.Generic.List[string]]::new() - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - NameProvider = '' - } - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - $DnsQuery = @{ - RecordType = 'ns' - Domain = $Domain - } - - $NSResults.Domain = $Domain - - try { - $Result = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - } - - catch { $Result = $null } - if ($Result.Status -eq 2 -and $Result.AD -eq $false) { - $ValidationFails.Add('DNSSEC Validation failed.') | Out-Null - } - - elseif ($Result.Status -ne 0 -or -not ($Result.Answer)) { - $ValidationFails.Add('No nameservers found for this domain.') | Out-Null - $NSRecords = $null - } - - else { - $NSRecords = $Result.Answer.data - $ValidationPasses.Add('Nameserver record is present.') | Out-Null - $NSResults.Records = @($NSRecords) - } - $NSResults.ValidationPasses = $ValidationPasses - $NSResults.ValidationFails = $ValidationFails - $NSResults -} diff --git a/DNSHealth/Public/DNS/Read-SPFRecord.ps1 b/DNSHealth/Public/DNS/Read-SPFRecord.ps1 deleted file mode 100644 index d47b9f9..0000000 --- a/DNSHealth/Public/DNS/Read-SPFRecord.ps1 +++ /dev/null @@ -1,547 +0,0 @@ -function Read-SpfRecord { - <# - .SYNOPSIS - Reads SPF record for specified domain - - .DESCRIPTION - Uses Get-GoogleDNSQuery to obtain TXT records for domain, searching for v=spf1 at the beginning of the record - Also parses include records and obtains their SPF as well - - .PARAMETER Domain - Domain to obtain SPF record for - - .EXAMPLE - PS> Read-SpfRecord -Domain gmail.com - - Domain : gmail.com - Record : v=spf1 redirect=_spf.google.com - RecordCount : 1 - LookupCount : 4 - AllMechanism : ~ - ValidationPasses : {Expected SPF record was included, No PermError detected in SPF record} - ValidationWarns : {} - ValidationFails : {SPF record should end in -all to prevent spamming} - RecordList : {@{Domain=_spf.google.com; Record=v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all; RecordCount=1; LookupCount=4; AllMechanism=~; ValidationPasses=System.Collections.ArrayList; ValidationWarns=System.Collections.ArrayList; ValidationFails=System.Collections.ArrayList; RecordList=System.Collections.ArrayList; TypeLookups=System.Collections.ArrayList; IPAddresses=System.Collections.ArrayList; PermError=False}} - TypeLookups : {} - IPAddresses : {} - PermError : False - - .NOTES - Author: John Duprey - #> - [CmdletBinding(DefaultParameterSetName = 'Lookup')] - Param( - [Parameter(Mandatory = $true, ParameterSetName = 'Lookup')] - [Parameter(ParameterSetName = 'Manual')] - [string]$Domain, - - [Parameter(Mandatory = $true, ParameterSetName = 'Manual')] - [string]$Record, - - [Parameter(ParameterSetName = 'Lookup')] - [Parameter(ParameterSetName = 'Manual')] - [string]$Level = 'Parent', - - [Parameter(ParameterSetName = 'Lookup')] - [Parameter(ParameterSetName = 'Manual')] - [string]$ExpectedInclude = '' - ) - $SpfResults = [PSCustomObject]@{ - Domain = '' - Record = '' - RecordCount = 0 - LookupCount = 0 - AllMechanism = '' - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - RecordList = [System.Collections.Generic.List[object]]::new() - TypeLookups = [System.Collections.Generic.List[object]]::new() - Recommendations = [System.Collections.Generic.List[object]]::new() - RecommendedRecord = '' - IPAddresses = [System.Collections.Generic.List[string]]::new() - MailProvider = '' - Explanation = '' - Status = '' - - } - - - - # Initialize lists to hold all records - $RecordList = [System.Collections.Generic.List[object]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationWarns = [System.Collections.Generic.List[string]]::new() - $Recommendations = [System.Collections.Generic.List[object]]::new() - $LookupCount = 0 - $AllMechanism = '' - $Status = '' - $RecommendedRecord = '' - - $TypeLookups = [System.Collections.Generic.List[object]]::new() - $IPAddresses = [System.Collections.Generic.List[string]]::new() - - $DnsQuery = @{ - RecordType = 'TXT' - Domain = $Domain - } - - $NoSpfValidation = 'No SPF record was detected for this domain.' - - # Query DNS for SPF Record - try { - switch ($PSCmdlet.ParameterSetName) { - 'Lookup' { - if ($Domain -eq 'Not Specified') { - # don't perform lookup if domain is not specified - } - - else { - $Query = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - if ($Query.Status -eq 2 -and $Query.AD -eq $false) { - $ValidationFails.Add('DNSSEC validation failed.') | Out-Null - } - - elseif ($Query.Status -ne 0) { - if ($Query.Status -eq 3) { - $ValidationFails.Add($NoSpfValidation) | Out-Null - $Status = 'permerror' - } - - else { - #Write-Host $Query - $ValidationFails.Add($NoSpfValidation) | Out-Null - $Status = 'temperror' - } - } - - else { - - $Answer = ($Query.answer | Where-Object { $_.data -match '^v=spf1' }) - $RecordCount = ($Answer.data | Measure-Object).count - $Record = $Answer.data - if ($RecordCount -eq 0) { - $ValidationFails.Add($NoSpfValidation) | Out-Null - $Status = 'permerror' - } - # Check for the correct number of records - elseif ($RecordCount -gt 1 -and $Level -eq 'Parent') { - $ValidationFails.Add("There must only be one SPF record per domain, we found $RecordCount.") | Out-Null - $Recommendations.Add([pscustomobject]@{ - Message = 'Delete one of the records beginning with v=spf1' - Match = '' - }) | Out-Null - $Status = 'permerror' - $Record = $Answer.data[0] - } - } - } - } - 'Manual' { - if ([string]::IsNullOrEmpty($Domain)) { $Domain = 'Not Specified' } - $RecordCount = 1 - } - } - $SpfResults.Domain = $Domain - - if ($Record -ne '' -and $RecordCount -gt 0) { - # Split records and parse - if ($Record -match '^v=spf1(:?\s+(?(?![+-~?]all).+?))?(:?\s+(?[+-~?]all)(:?\s+(?(?!all).+))?)?$') { - if ($Matches.Terms) { - $RecordTerms = $Matches.Terms -split '\s+' - } - - else { - $RecordTerms = @() - } - Write-Verbose "########### RECORD: $Record" - - if ($Level -eq 'Parent' -or $Level -eq 'Redirect') { - $AllMechanism = $Matches.AllMechanism - } - - if ($null -ne $Matches.Discard) { - if ($Matches.Discard -notmatch '^exp=(?.+)$') { - $ValidationWarns.Add("The terms '$($Matches.Discard)' are past the all mechanism and will be discarded.") | Out-Null - $Recommendations.Add([pscustomobject]@{ - Message = 'Remove entries following all'; - Match = $Matches.Discard - Replace = '' - }) | Out-Null - } - - } - - foreach ($Term in $RecordTerms) { - Write-Verbose "TERM $Term" - # Redirect modifier - if ($Term -match 'redirect=(?.+)') { - Write-Verbose '-----REDIRECT-----' - $LookupCount++ - if ($Record -match '(?[+-~?])all') { - $ValidationFails.Add('A record with a redirect modifier must not contain an all mechanism. This will result in a failure.') | Out-Null - $Status = 'permerror' - $Recommendations.Add([pscustomobject]@{ - Message = "Remove the 'all' mechanism from this record."; - Match = '{0}all' -f $Matches.Qualifier - Replace = '' - }) | Out-Null - } - - else { - # Follow redirect modifier - $RedirectedLookup = Read-SpfRecord -Domain $Matches.Domain -Level 'Redirect' - if (($RedirectedLookup | Measure-Object).Count -eq 0) { - $ValidationFails.Add("$Domain Redirected lookup does not contain a SPF record, this will result in a failure.") | Out-Null - $Status = 'permerror' - } - - else { - $RecordList.Add($RedirectedLookup) | Out-Null - $AllMechanism = $RedirectedLookup.AllMechanism - $ValidationFails.AddRange([string[]]$RedirectedLookup.ValidationFails) | Out-Null - $ValidationWarns.AddRange([string[]]$RedirectedLookup.ValidationWarns) | Out-Null - $ValidationPasses.AddRange([string[]]$RedirectedLookup.ValidationPasses) | Out-Null - $IPAddresses.AddRange([string[]]$RedirectedLookup.IPAddresses) | Out-Null - } - } - # Record has been redirected, stop evaluating terms - break - } - - # Include mechanism - elseif ($Term -match '^(?[+-~?])?include:(?.+)$') { - $LookupCount++ - Write-Verbose '-----INCLUDE-----' - Write-Verbose "Looking up include $($Matches.Value)" - $IncludeLookup = Read-SpfRecord -Domain $Matches.Value -Level 'Include' - - if ([string]::IsNullOrEmpty($IncludeLookup.Record) -and $Level -eq 'Parent') { - Write-Verbose '-----END INCLUDE (SPF MISSING)-----' - $ValidationFails.Add("Include lookup for $($Matches.Value) does not contain a SPF record, this will result in a failure.") | Out-Null - $Status = 'permerror' - } - - else { - Write-Verbose '-----END INCLUDE (SPF FOUND)-----' - $RecordList.Add($IncludeLookup) | Out-Null - $ValidationFails.AddRange([string[]]$IncludeLookup.ValidationFails) | Out-Null - $ValidationWarns.AddRange([string[]]$IncludeLookup.ValidationWarns) | Out-Null - $ValidationPasses.AddRange([string[]]$IncludeLookup.ValidationPasses) | Out-Null - $IPAddresses.AddRange([string[]]$IncludeLookup.IPAddresses) | Out-Null - } - } - - # Exists mechanism - elseif ($Term -match '^(?[+-~?])?exists:(?.+)$') { - $LookupCount++ - } - - # ip4/ip6 mechanism - elseif ($Term -match '^(?[+-~?])?ip[4,6]:(?.+)$') { - if (-not ($Matches.Qualifier) -or $Matches.Qualifier -eq '+') { - $IPAddresses.Add($Matches.Value) | Out-Null - } - } - - # Remaining type mechanisms a,mx,ptr - elseif ($Term -match '^(?[+-~?])?(?(?:a|mx|ptr))(?:[:](?.+))?$') { - $LookupCount++ - - if ($Matches.TypeDomain) { - $TypeDomain = $Matches.TypeDomain - } - - else { - $TypeDomain = $Domain - } - - if ($TypeDomain -ne 'Not Specified') { - try { - $TypeQuery = @{ Domain = $TypeDomain; RecordType = $Matches.RecordType } - Write-Verbose "Looking up $($TypeQuery.Domain)" - $TypeResult = Resolve-DnsHttpsQuery @TypeQuery -ErrorAction Stop - if ($Matches.RecordType -eq 'mx') { - $MxCount = 0 - if ($TypeResult.Answer) { - foreach ($mx in $TypeResult.Answer.data) { - $MxCount++ - $Preference, $MxDomain = $mx -replace '\.$' -split '\s+' - try { - Write-Verbose "MX: Lookup $MxDomain" - $MxQuery = Resolve-DnsHttpsQuery -Domain $MxDomain -ErrorAction Stop - $MxIps = $MxQuery.Answer.data - - foreach ($MxIp in $MxIps) { - $IPAddresses.Add($MxIp) | Out-Null - } - - if ($MxCount -gt 10) { - $ValidationWarns.Add("$Domain - Mechanism 'mx' lookup for $MxDomain has exceeded the 10 A or AAAA record lookup limit (RFC 7208, Section 4.6.4).") | Out-Null - $TypeResult = $null - break - } - } - - catch { - Write-Verbose $_.Exception.Message - $TypeResult = $null - } - } - } - - else { - $ValidationWarns.Add("$Domain - Mechanism 'mx' lookup for $($TypeQuery.Domain) did not have any records") | Out-Null - } - } - - elseif ($Matches.RecordType -eq 'ptr') { - $ValidationWarns.Add("$Domain - The mechanism 'ptr' should not be published in an SPF record (RFC 7208, Section 5.5)") - } - } - - catch { - $TypeResult = $null - } - - if ($null -eq $TypeResult -or $TypeResult.Status -ne 0) { - $Message = "$Domain - Type lookup for the mechanism '$($TypeQuery.RecordType)' did not return any results." - switch ($Level) { - 'Parent' { - $ValidationFails.Add("$Message") | Out-Null - $Status = 'permerror' - } - 'Include' { $ValidationWarns.Add("$Message") | Out-Null } - } - $Result = $false - } - - else { - if ($TypeResult.Answer) { - if ($TypeQuery.RecordType -match 'mx') { - - $Result = $TypeResult.Answer | ForEach-Object { - #$LookupCount++ - $_.Data.Split(' ')[1] - } - } - - else { - $Result = $TypeResult.answer.data - } - } - } - $TypeLookups.Add( - [PSCustomObject]@{ - Domain = $TypeQuery.Domain - RecordType = $TypeQuery.RecordType - Result = $Result - } - ) | Out-Null - - } - - else { - $ValidationWarns.Add("No domain was specified and mechanism '$Term' does not have one defined. Specify a domain to perform a lookup on this record.") | Out-Null - } - } - - elseif ($null -ne $Term) { - $ValidationWarns.Add("$Domain - Unknown term specified '$Term'") | Out-Null - } - } - - # Explanation modifier - if ($Record -match 'exp=(?.+)$') { - Write-Verbose '-----EXPLAIN-----' - $ExpQuery = @{ Domain = $Domain; MacroExpand = $Matches.MacroExpand; RecordType = 'TXT' } - $ExpResult = Resolve-DnsHttpsQuery @ExpQuery -ErrorAction Stop - if ($ExpResult.Status -eq 0 -and $ExpResult.Answer.Type -eq 16) { - $Explain = @{ - Record = $ExpResult.Answer.data - Example = Get-DomainMacros -Domain $Domain -MacroExpand $ExpResult.Answer.data - } - } - } - - else { - $Explain = @{ Example = ''; Record = '' } - } - } - } - } - - catch { - Write-Verbose "EXCEPTION: $($_.InvocationInfo.ScriptLineNumber) $($_.Exception.Message)" - } - - # Lookup MX record for expected include information if not supplied - if ($Level -eq 'Parent' -and $ExpectedInclude -eq '') { - try { - #Write-Information $Domain - $MXRecord = Read-MXRecord -Domain $Domain - $SpfResults.MailProvider = $MXRecord.MailProvider - if ($MXRecord.ExpectedInclude -ne '') { - $ExpectedInclude = $MXRecord.ExpectedInclude - } - - if ($MXRecord.MailProvider.Name -eq 'Null') { - if ($Record -eq 'v=spf1 -all') { - $ValidationPasses.Add('This SPF record is valid for a Null MX configuration') | Out-Null - } - - else { - $ValidationFails.Add('This SPF record is not valid for a Null MX configuration. Expected record: "v=spf1 -all"') | Out-Null - } - } - - if ($TypeLookups.RecordType -contains 'mx') { - $Recommendations.Add([pscustomobject]@{ - Message = "Remove the 'mx' modifier from your record. Check the mail provider documentation for the correct SPF include."; - Match = '\s*([+-~?]?mx)\s+' - Replace = ' ' - }) | Out-Null - } - } - - catch { Write-Verbose $_.Exception.Message } - } - - # Look for expected include record and report pass or fail - if ($ExpectedInclude -ne '') { - if ($RecordList.Domain -notcontains $ExpectedInclude) { - $ExpectedIncludeSpf = Read-SpfRecord -Domain $ExpectedInclude -Level ExpectedInclude - $ExpectedIPCount = $ExpectedIncludeSpf.IPAddresses | Measure-Object | Select-Object -ExpandProperty Count - $FoundIPCount = Compare-Object $IPAddresses $ExpectedIncludeSpf.IPAddresses -IncludeEqual | Where-Object -Property SideIndicator -EQ '==' | Measure-Object | Select-Object -ExpandProperty Count - if ($ExpectedIPCount -eq $FoundIPCount) { - $ValidationPasses.Add('The expected mail provider IP address ranges were found.') | Out-Null - } - - else { - $ValidationFails.Add('The expected mail provider entry was not found in the record.') | Out-Null - $Recommendations.Add([pscustomobject]@{ - Message = ("Add 'include:{0} to your record." -f $ExpectedInclude) - Match = '^v=spf1 (.+?)([-~?+]all)?$' - Replace = "v=spf1 include:$ExpectedInclude `$1 `$2" - }) | Out-Null - } - } - - else { - $ValidationPasses.Add('The expected mail provider entry is part of the record.') | Out-Null - } - } - - # Count total lookups - $LookupCount = $LookupCount + ($RecordList | Measure-Object -Property LookupCount -Sum).Sum - - if ($Domain -ne 'Not Specified') { - # Check legacy SPF type - $LegacySpfType = Resolve-DnsHttpsQuery -Domain $Domain -RecordType 'SPF' -ErrorAction Stop - if ($null -ne $LegacySpfType -and $LegacySpfType -eq 0) { - $ValidationWarns.Add("The record type 'SPF' was detected, this is legacy and should not be used. It is recommeded to delete this record (RFC 7208 Section 14.1).") | Out-Null - } - } - if ($Level -eq 'Parent' -and $RecordCount -gt 0) { - # Check for the correct all mechanism - if ($AllMechanism -eq '' -and $Record -ne '') { - $ValidationFails.Add("The 'all' mechanism is missing from SPF record, the default is a neutral qualifier (?all).") | Out-Null - $AllMechanism = '?all' - } - - if ($AllMechanism -eq '-all') { - $ValidationPasses.Add('The SPF record ends with a hard fail qualifier (-all). This is best practice and will instruct recipients to discard unauthorized senders.') | Out-Null - } - - elseif ($Record -ne '') { - $ValidationFails.Add('The SPF record should end in -all to prevent spamming.') | Out-Null - $Recommendations.Add([PSCustomObject]@{ - Message = "Replace '{0}' with '-all' to make a SPF failure result in a hard fail." -f $AllMechanism - Match = [regex]::escape($AllMechanism) - Replace = '-all' - }) | Out-Null - } - - # SPF lookup count - if ($LookupCount -ge 9) { - $SpecificLookupsFound = $false - foreach ($SpfRecord in $RecordList) { - if ($SpfRecord.LookupCount -ge 5) { - $SpecificLookupsFound = $true - $IncludeLookupCount = $SpfRecord.LookupCount + 1 - $Match = ('[+-~?]?include:{0}' -f $SpfRecord.Domain) - $Recommendations.Add([PSCustomObject]@{ - Message = ("Remove the include modifier for domain '{0}', this adds {1} lookups towards the max of 10. Alternatively, reduce the number of lookups inside this record if you are able to." -f $SpfRecord.Domain, $IncludeLookupCount) - Match = $Match - Replace = '' - }) | Out-Null - } - } - if (!($SpecificLookupsFound)) { - $Recommendations.Add([PSCustomObject]@{ - Message = 'Review include modifiers to ensure that your lookup count stays below 10.' - Match = '' - }) | Out-Null - } - } - - if ($LookupCount -gt 10) { - $ValidationFails.Add("Lookup count: $LookupCount/10. The SPF evaluation will fail with a permanent error (RFC 7208 Section 4.6.4).") | Out-Null - $Status = 'permerror' - } - - elseif ($LookupCount -ge 9 -and $LookupCount -le 10) { - $ValidationWarns.Add("Lookup count: $LookupCount/10. Excessive lookups can cause the SPF evaluation to fail (RFC 7208 Section 4.6.4).") | Out-Null - } - - else { - $ValidationPasses.Add("Lookup count: $LookupCount/10.") | Out-Null - } - - # Report pass if no PermErrors are found - if ($Status -ne 'permerror') { - $ValidationPasses.Add('No permanent errors detected in the SPF record.') | Out-Null - } - - # Report pass if no errors are found - if (($ValidationFails | Measure-Object | Select-Object -ExpandProperty Count) -eq 0) { - $ValidationPasses.Add('All validation checks passed.') | Out-Null - } - } - - # Check recommendations for replacement regexes - if (($Recommendations | Measure-Object).Count -gt 0) { - $RecommendedRecord = $Record - foreach ($Rec in $Recommendations) { - if ($Rec.Match -ne '') { - # Replace item in record with recommended - $RecommendedRecord = $RecommendedRecord -replace $Rec.Match, $Rec.Replace - } - } - # Cleanup extra spaces - $RecommendedRecord = $RecommendedRecord -replace '\s+', ' ' - } - - # Set SPF result object - $SpfResults.Record = $Record - $SpfResults.RecordCount = $RecordCount - $SpfResults.LookupCount = $LookupCount - $SpfResults.AllMechanism = $AllMechanism - $SpfResults.ValidationPasses = @($ValidationPasses) - $SpfResults.ValidationWarns = @($ValidationWarns) - $SpfResults.ValidationFails = @($ValidationFails) - $SpfResults.RecordList = @($RecordList) - $SpfResults.Recommendations = @($Recommendations) - $SpfResults.RecommendedRecord = $RecommendedRecord - $SpfResults.TypeLookups = @($TypeLookups) - $SpfResults.IPAddresses = @($IPAddresses) - $SpfResults.Explanation = $Explain - $SpfResults.Status = $Status - - - Write-Verbose "-----END SPF RECORD ($Level)-----" - - # Output SpfResults object - $SpfResults -} \ No newline at end of file diff --git a/DNSHealth/Public/DNS/Read-TlsRptRecord.ps1 b/DNSHealth/Public/DNS/Read-TlsRptRecord.ps1 deleted file mode 100644 index 09799b2..0000000 --- a/DNSHealth/Public/DNS/Read-TlsRptRecord.ps1 +++ /dev/null @@ -1,150 +0,0 @@ -function Read-TlsRptRecord { - <# - .SYNOPSIS - Resolve and validate TLSRPT record - - .DESCRIPTION - Query domain for TLSRPT record (_smtp._tls.domain.com) and parse results. Record is checked for issues. - - .PARAMETER Domain - Domain to process TLSRPT record - - .EXAMPLE - PS> Read-TlsRptRecord -Domain gmail.com - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - - # Initialize object - $TlsRptAnalysis = [PSCustomObject]@{ - Domain = $Domain - Record = '' - Version = '' - RuaEntries = [System.Collections.Generic.List[string]]::new() - IsValid = $false - HasWarnings = $false - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - } - - $ValidRuaProtocols = @( - '^(?https:.+)$' - '^mailto:(?.+)$' - ) - - # Validation lists - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationWarns = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - # Validation ranges - - $RecordCount = 0 - - $DnsQuery = @{ - RecordType = 'TXT' - Domain = "_smtp._tls.$Domain" - } - - # Resolve DMARC record - - $Query = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - - $RecordCount = 0 - $Query.Answer | Where-Object { $_.data -match '^v=TLSRPTv1' } | ForEach-Object { - $TlsRtpRecord = $_.data - $TlsRptAnalysis.Record = $TlsRtpRecord - $RecordCount++ - } - if ($Query.Status -eq 2 -and $Query.AD -eq $false) { - $ValidationFails.Add('DNSSEC validation failed.') | Out-Null - } - if ($Query.Status -ne 0 -or $RecordCount -eq 0) { - if ($Query.Status -eq 3) { - $ValidationFails.Add('Record does not exist (NXDOMAIN)') | Out-Null - } - - else { - $ValidationFails.Add("$Domain does not have an TLSRPT record") | Out-Null - } - } - - elseif ($RecordCount -gt 1) { - $ValidationFails.Add("$Domain has multiple TLSRPT records") | Out-Null - } - - # Split DMARC record into name/value pairs - $TagList = [System.Collections.Generic.List[object]]::new() - Foreach ($Element in ($TlsRtpRecord -split ';').trim()) { - $Name, $Value = $Element -split '=' - $TagList.Add( - [PSCustomObject]@{ - Name = $Name - Value = $Value - } - ) | Out-Null - } - - # Loop through name/value pairs and set object properties - $x = 0 - foreach ($Tag in $TagList) { - switch ($Tag.Name) { - 'v' { - # REQUIRED: Version - if ($x -ne 0) { $ValidationFails.Add('v=TLSRPTv1 must be at the beginning of the record') | Out-Null } - if ($Tag.Value -ne 'TLSRPTv1') { $ValidationFails.Add("Version must be TLSRPTv1 - found $($Tag.Value)") | Out-Null } - $TlsRptAnalysis.Version = $Tag.Value - } - 'rua' { - $RuaMatched = $false - $RuaEntries = $Tag.Value -split ',' - foreach ($RuaEntry in $RuaEntries) { - foreach ($Protocol in $ValidRuaProtocols) { - if ($RuaEntry -match $Protocol) { - $TlsRptAnalysis.RuaEntries.Add($Matches.Rua) | Out-Null - $RuaMatched = $true - } - } - } - if ($RuaMatched) { - $ValidationPasses.Add('Aggregate reports are being sent') | Out-Null - } - - else { - $ValidationWarns.Add('Aggregate reports are not being sent') | Out-Null - $TlsRptAnalysis.HasWarnings = $true - } - } - } - $x++ - } - - if ($RecordCount -gt 0) { - # Check for missing record tags and set defaults - - if ($RecordCount -gt 1) { - $ValidationWarns.Add('Multiple TLSRPT records detected, this may cause unexpected behavior.') | Out-Null - $TlsRptAnalysis.HasWarnings = $true - } - - $ValidationWarnCount = ($Test.ValidationWarns | Measure-Object).Count - $ValidationFailCount = ($Test.ValidationFails | Measure-Object).Count - if ($ValidationFailCount -eq 0 -and $ValidationWarnCount -eq 0) { - $ValidationPasses.Add('TLSRPT record is valid') | Out-Null - $TlsRptAnalysis.IsValid = $true - } - } - - # Add the validation lists - $TlsRptAnalysis.ValidationPasses = $ValidationPasses - $TlsRptAnalysis.ValidationWarns = $ValidationWarns - $TlsRptAnalysis.ValidationFails = $ValidationFails - - # Return MTA-STS analysis - $TlsRptAnalysis -} diff --git a/DNSHealth/Public/DNS/Read-WhoisRecord.ps1 b/DNSHealth/Public/DNS/Read-WhoisRecord.ps1 deleted file mode 100644 index 7bbcee7..0000000 --- a/DNSHealth/Public/DNS/Read-WhoisRecord.ps1 +++ /dev/null @@ -1,172 +0,0 @@ -function Read-WhoisRecord { - <# - .SYNOPSIS - Reads Whois record data for queried information - - .DESCRIPTION - Connects to top level registrar servers (IANA, ARIN) and performs recursion to find Whois data - - .PARAMETER Query - Whois query to perform (e.g. microsoft.com) - - .PARAMETER Server - Whois server to query, defaults to whois.iana.org - - .PARAMETER Port - Whois server port, default 43 - - .EXAMPLE - PS> Read-WhoisRecord -Query microsoft.com - - #> - [CmdletBinding()] - param ( - [Parameter (Position = 0, Mandatory = $true)] - [String]$Query, - [String]$Server = 'whois.iana.org', - $Port = 43 - ) - $HasReferral = $false - - # Top level referring servers, IANA, ARIN and AUDA - $TopLevelReferrers = @('whois.iana.org', 'whois.arin.net', 'whois.auda.org.au') - - # Record Pattern Matching - $ServerPortRegex = '(?[^:\r\n]+)(:(?\d+))?' - $ReferralMatch = @{ - 'ReferralServer' = "whois://$ServerPortRegex" - 'Whois Server' = $ServerPortRegex - 'Registrar Whois Server' = $ServerPortRegex - 'refer' = $ServerPortRegex - 'remarks' = '(?whois\.[0-9a-z\-\.]+\.[a-z]{2,})(:(?\d+))?' - } - - # List of properties for Registrars - $RegistrarProps = @( - 'Registrar', 'Registrar Name' - ) - - # Whois parser, generic Property: Value format with some multi-line support and comment handlers - $WhoisRegex = '^(?!(?:%|>>>|-+|#|[*]))[^\S\n]*(?.+?):(?:[\r\n]+)?(:?(?!([0-9]|[/]{2}))[^\S\r\n]*(?.+))?$' - - # TCP Client for Whois - $Client = New-Object System.Net.Sockets.TcpClient($Server, 43) - try { - # Open TCP connection and send query - $Stream = $Client.GetStream() - $ReferralServers = [System.Collections.Generic.List[string]]::new() - $ReferralServers.Add($Server) | Out-Null - - # WHOIS query to send - $Data = [System.Text.Encoding]::Ascii.GetBytes("$Query`r`n") - $Stream.Write($Data, 0, $data.length) - - # Read response from stream - $Reader = New-Object System.IO.StreamReader $Stream, [System.Text.Encoding]::ASCII - $Raw = $Reader.ReadToEnd() - - # Split comments and parse raw whois results - $data, $comment = $Raw -split '(>>>|\n\s+--)' - $PropMatches = [regex]::Matches($data, $WhoisRegex, ([System.Text.RegularExpressions.RegexOptions]::MultiLine, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)) - - # Hold property count in hashtable for auto increment - $PropertyCounts = @{} - - # Create ordered list for properties - $Results = [ordered]@{} - foreach ($PropMatch in $PropMatches) { - $PropName = $PropMatch.Groups['PropName'].value - if ($Results.Contains($PropName)) { - $PropertyCounts.$PropName++ - $PropName = '{0}{1}' -f $PropName, $PropertyCounts.$PropName - $Results[$PropName] = $PropMatch.Groups['PropValue'].value.trim() - } - - else { - $Results[$PropName] = $PropMatch.Groups['PropValue'].value.trim() - $PropertyCounts.$PropName = 0 - } - } - - foreach ($RegistrarProp in $RegistrarProps) { - if ($Results.Contains($RegistrarProp)) { - $Results._Registrar = $Results.$RegistrarProp - if ($Results.$RegistrarProp -eq 'Registrar') { - break # Means we always favour Registrar if it exists, or keep looking - } - } - } - - # Store raw results and query metadata - $Results._Raw = $Raw - $Results._ReferralServers = [System.Collections.Generic.List[string]]::new() - $Results._Query = $Query - $LastResult = $Results - - # Loop through keys looking for referral server match - foreach ($Key in $ReferralMatch.Keys) { - if ([bool]($Results.Keys -match $Key)) { - if ($Results.$Key -match $ReferralMatch.$Key) { - $ReferralServer = $Matches.refsvr - if ($Server -ne $ReferralServer) { - if ($Matches.port) { $Port = $Matches.port } - else { $Port = 43 } - $HasReferral = $true - break - } - } - } - } - - # Recurse through referrals - if ($HasReferral) { - if ($Server -ne $ReferralServer) { - $LastResult = $Results - $Results = Read-WhoisRecord -Query $Query -Server $ReferralServer -Port $Port - if ($Results._Raw -Match '(No match|Not Found|No Data|The queried object does not exist)' -and $TopLevelReferrers -notcontains $Server) { - $Results = $LastResult - } - - else { - foreach ($s in $Results._ReferralServers) { - $ReferralServers.Add($s) | Out-Null - } - } - - } - } - - else { - if ($Results._Raw -Match '(No match|Not Found|No Data)') { - $first, $newquery = ($Query -split '\.') - if (($newquery | Measure-Object).Count -gt 1) { - $Query = $newquery -join '.' - $Results = Read-WhoisRecord -Query $Query -Server $Server -Port $Port - foreach ($s in $Results._ReferralServers) { - $ReferralServers.Add($s) | Out-Null - } - } - } - } - } - - catch { - Write-Error $_.Exception.Message - } - - finally { - IF ($Stream) { - $Stream.Close() - $Stream.Dispose() - } - } - - # Collect referral server list - $Results._ReferralServers = $ReferralServers - - # Convert to json and back to preserve object order - $WhoisResults = $Results | ConvertTo-Json | ConvertFrom-Json - - # Return Whois results as PSObject - $WhoisResults -} diff --git a/DNSHealth/Public/DNS/Test-DNSSEC.ps1 b/DNSHealth/Public/DNS/Test-DNSSEC.ps1 deleted file mode 100644 index dba2d1c..0000000 --- a/DNSHealth/Public/DNS/Test-DNSSEC.ps1 +++ /dev/null @@ -1,77 +0,0 @@ -function Test-DNSSEC { - <# - .SYNOPSIS - Test Domain for DNSSEC validation - - .DESCRIPTION - Requests dnskey record from DNS and checks response validation (AD=True) - - .PARAMETER Domain - Domain to check - - .EXAMPLE - PS> Test-DNSSEC -Domain example.com - - Domain : example.com - ValidationPasses : {example.com - DNSSEC enabled and validated} - ValidationFails : {} - Keys : {...} - - #> - [CmdletBinding()] - Param( - [Parameter(Mandatory = $true)] - [string]$Domain - ) - $DSResults = [PSCustomObject]@{ - Domain = $Domain - ValidationPasses = [System.Collections.Generic.List[string]]::new() - ValidationWarns = [System.Collections.Generic.List[string]]::new() - ValidationFails = [System.Collections.Generic.List[string]]::new() - Keys = [System.Collections.Generic.List[string]]::new() - } - $ValidationPasses = [System.Collections.Generic.List[string]]::new() - $ValidationFails = [System.Collections.Generic.List[string]]::new() - - $DnsQuery = @{ - RecordType = 'dnskey' - Domain = $Domain - } - - $Result = Resolve-DnsHttpsQuery @DnsQuery -ErrorAction Stop - if ($Result.Status -eq 2 -and $Result.AD -eq $false) { - $ValidationFails.Add('DNSSEC Validation failed.') | Out-Null - } - - else { - $RecordCount = ($Result.Answer.data | Measure-Object).Count - if ($null -eq $Result) { - $ValidationFails.Add('DNSSEC is not set up for this domain.') | Out-Null - } - - else { - if ($Result.Status -eq 3) { - $ValidationFails.Add('DNSSEC is not set up for this domain.') | Out-Null - } - - elseif ($RecordCount -gt 0) { - if ($Result.AD -eq $false) { - $ValidationFails.Add('DNSSEC is enabled, but the DNS query response was not validated. Ensure DNSSEC has been enabled on your domain provider.') | Out-Null - } - - else { - $ValidationPasses.Add('DNSSEC is enabled and validated for this domain.') | Out-Null - } - $DSResults.Keys = $Result.answer.data - } - - else { - $ValidationFails.Add('DNSSEC is not set up for this domain.') | Out-Null - } - } - } - - $DSResults.ValidationPasses = $ValidationPasses - $DSResults.ValidationFails = $ValidationFails - $DSResults -}