diff --git a/.build/cspell-words.txt b/.build/cspell-words.txt index 290d1fb6df..c0e2a88254 100644 --- a/.build/cspell-words.txt +++ b/.build/cspell-words.txt @@ -103,6 +103,7 @@ Nego Netlogon netsh nmap +nslookup noderunner notcontains notin diff --git a/M365/MDO/EmailAuthChecker.ps1 b/M365/MDO/EmailAuthChecker.ps1 new file mode 100644 index 0000000000..9369062745 --- /dev/null +++ b/M365/MDO/EmailAuthChecker.ps1 @@ -0,0 +1,7156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# email providers +#cspell:words Gmail, GoogleMail, AmazonSES, Proofpoint, Pphosted, Mimecast + +# DKIM Providers +#cspell:words Mailchimp, smtpapi, Mailgun, mailo, scph, Zendesk, Salesforce, Klaviyo, AWeber, GetResponse, ConvertKit, Infusionsoft, Pardot, Marketo, Eloqua, Sendlane, Moosend, Omnisend, EmailOctopus, Sendinblue, Elasticemail, Pepipost, Socketlabs, Mailjet, Dynadot, Zoho, Protonmail, Fastmail, RackSpace, Bluehost, Namecheap, Plesk + +# protocol words +#cspell:words softfail, softpass, permerror, temperror, compauth, adkim, aspf, NSPM, BIMP, DIMP, FTBP, HPHSH, HPHISH, HSPM, INTOS, MALW, OSPM, PHSH, SPOOF, UIMP, dmarc, domainkey, mxvault + +#html tags +#cspell:words onclick, thead, tbody, colgroup, mouseleave, mouseenter, ctry, darr, minmax, rgba, nowrap, uarr, onmouseover, onmouseout, linecap, dashoffset, dasharray + +#Fonts +#cspell:words Lucida, Verdana, Tahoma, Segoe + +<# +.SYNOPSIS + Comprehensive email authentication analysis tool for SPF, DKIM, and DMARC records with documentation integration and enhanced security analysis. + + This script has been thoroughly tested across various environments and scenarios, and all tests have passed successfully. However, by using this script, you acknowledge and agree that: + 1. You are responsible for how you use the script and any outcomes resulting from its execution. + 2. The entire risk arising out of the use or performance of the script remains with you. + 3. The author and contributors are not liable for any damages, including data loss, business interruption, or other losses, even if warned of the risks. + +.DESCRIPTION + The Email Authentication Checker analyzes email authentication configurations for domains, providing detailed validation of SPF, DKIM, and DMARC records. + The tool performs comprehensive security checks including DNS lookup validation, TTL analysis, macro security assessment, syntax validation, SPF enforcement rule analysis, and DMARC failure options evaluation. + It generates professional HTML reports with interactive visualizations and provides actionable recommendations with direct links to Microsoft's + official documentation if MX record points to Exchange Online or industry standard documentation if MX record points to another provider. Enhanced with authoritative DNS server queries for accurate TTL validation and record retrieval. + + This script operates in parameter-only mode and supports 4 analysis modes: + 1. Single Domain Analysis - Use -Domain parameter + 2. Multiple Domain Analysis - Use -DomainList parameter (comma-separated) + 3. Load Domains from File - Use -FilePath parameter (one domain per line) + 4. Email Header Analysis - Use -HeaderFilePath parameter + + Features 19 comprehensive security checks: + - SPF (9 checks): Record presence, syntax, single record compliance, DNS lookups, length validation, TTL analysis, SPF enforcement rule, macro security, sub-record TTL (A/MX/TXT) + - DMARC (5 checks): Record presence, policy assessment, reporting configuration, alignment modes, TTL validation + - DKIM (5 checks): Selector discovery, syntax validation, key status analysis, strength assessment, TTL validation + +.PARAMETER Domain + Single domain to analyze (e.g., example.com). Use this parameter for single domain analysis. + +.PARAMETER DomainList + Multiple domains separated by commas (e.g., "example.com,contoso.com"). Use this parameter for multiple domain analysis. + +.PARAMETER FilePath + Path to a text file containing domains (one per line). Use this parameter for file-based analysis. + +.PARAMETER HeaderFilePath + Path to a text file containing email headers. Use this parameter for email header analysis. + +.PARAMETER OutputPath + Directory path where the HTML report will be saved. Defaults to current directory if not specified. + +.PARAMETER AutoOpen + Automatically open the HTML report in the default browser when analysis is complete. + +.EXAMPLE + .\EmailAuthChecker.ps1 -Domain "microsoft.com" + Analyze a single domain. + +.EXAMPLE + .\EmailAuthChecker.ps1 -DomainList "microsoft.com,contoso.com,outlook.com" + Analyze multiple domains. + +.EXAMPLE + .\EmailAuthChecker.ps1 -FilePath "C:\temp\domains.txt" -OutputPath "C:\reports" -AutoOpen + Analyze domains from a file, save to specific directory, and auto-open the report. + +.EXAMPLE + .\EmailAuthChecker.ps1 -HeaderFilePath "C:\temp\headers.txt" + Analyze domains extracted from email headers. + +#> + +[CmdletBinding()] +param( + [Parameter(ParameterSetName = 'DomainList', Mandatory = $true)] + [string[]]$DomainList, + + [Parameter(ParameterSetName = 'File', Mandatory = $true)] + [string]$FilePath, + + [Parameter(ParameterSetName = 'Headers', Mandatory = $true)] + [string]$HeaderFilePath, + + [Parameter(ParameterSetName = 'DomainList', Mandatory = $false)] + [Parameter(ParameterSetName = 'File', Mandatory = $false)] + [Parameter(ParameterSetName = 'Headers', Mandatory = $false)] + [string]$OutputPath = ".", + + [Parameter(ParameterSetName = 'DomainList', Mandatory = $false)] + [Parameter(ParameterSetName = 'File', Mandatory = $false)] + [Parameter(ParameterSetName = 'Headers', Mandatory = $false)] + [switch]$AutoOpen, + + [Parameter(ParameterSetName = 'DomainList', Mandatory = $false)] + [Parameter(ParameterSetName = 'File', Mandatory = $false)] + [Parameter(ParameterSetName = 'Headers', Mandatory = $false)] + [switch]$SkipVersionCheck, + + [Parameter(Mandatory = $true, ParameterSetName = "ScriptUpdateOnly")] + [switch]$ScriptUpdateOnly +) + +# Function to get provider-specific documentation URLs +function Get-ProviderSpecificURLs { + param([array]$Providers) + + # Check if Microsoft/Office 365 is in the providers list + if ($Providers -contains "Microsoft/Office 365") { + return @{ + SPFSetup = "https://learn.microsoft.com/defender-office-365/email-authentication-spf-configure" + SPFSyntax = "https://learn.microsoft.com/defender-office-365/email-authentication-spf-configure#syntax-for-spf-txt-records" + SPFMacroSecurity = "https://www.rfc-editor.org/rfc/rfc7208#section-7.2" + DMARCSetup = "https://learn.microsoft.com/defender-office-365/email-authentication-dmarc-configure" + DMARCReports = "https://learn.microsoft.com/defender-office-365/email-authentication-dmarc-configure#syntax-for-dmarc-txt-records" + DKIMSetup = "https://learn.microsoft.com/defender-office-365/email-authentication-dkim-configure" + } + } else { + # Return default URLs for non-Microsoft providers + return @{ + SPFSetup = "https://www.rfc-editor.org/rfc/rfc7208" + SPFSyntax = "https://www.rfc-editor.org/rfc/rfc7208" + SPFMacroSecurity = "https://www.rfc-editor.org/rfc/rfc7208#section-7.2" + DMARCSetup = "https://www.rfc-editor.org/rfc/rfc7489.html" + DMARCReports = "https://www.rfc-editor.org/rfc/rfc7489.html#section-7" + DKIMSetup = "https://www.rfc-editor.org/rfc/rfc6376" + } + } +} + +# Enhanced UI Functions +# Helper function to parse DKIM records into key-value pairs +function ConvertFrom-DKIMRecord { + param([string]$dkimRecord) + + $tags = @{} + if ([string]::IsNullOrWhiteSpace($dkimRecord)) { + return $tags + } + + $parts = $dkimRecord -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + + foreach ($part in $parts) { + if ($part -match '^([a-z]+)=(.*)$') { + $tagName = $matches[1].Trim() + $tagValue = $matches[2].Trim() + $tags[$tagName] = $tagValue + } + } + + return $tags +} + +# Helper function to generate recommendations based on issue patterns +function Get-Recommendation { + param( + [string]$Issue, + [string]$Protocol, + [array]$Providers = @() + ) + + # Get provider-specific URLs + $URLs = Get-ProviderSpecificURLs -Providers $Providers + + # Simplified approach - just return a generic recommendation for now + switch ($Protocol) { + "SPF" { + if ($Issue -like "*+all*") { + return "Fix SPF '+all' mechanism - Microsoft Guide: $($URLs.SPFSetup)" + } elseif ($Issue -like "*'?all'*" -or $Issue -like "*Uses ?all*") { + return "Strengthen SPF '?all' to '~all' or '-all' - Microsoft SPF Setup: $($URLs.SPFSyntax)" + } elseif ($Issue -like "*all mechanism*") { + return "Add proper 'all' mechanism to SPF record - Microsoft Documentation: $($URLs.SPFSetup)" + } elseif ($Issue -like "*too long*" -or $Issue -like "*exceeds*") { + return "Reduce SPF record length (max 255 chars) - Microsoft Best Practices: $($URLs.SPFSyntax)" + } elseif ($Issue -like "*approaching*") { + return "Consider optimizing SPF record length to avoid 255 character limit: $($URLs.SPFSyntax)" + } elseif ($Issue -like "*DNS lookup limit*") { + return "Optimize SPF record to reduce DNS lookups (max 10) - Consider flattening includes or using IP addresses - Microsoft SPF Optimization: $($URLs.SPFSyntax)" + } elseif ($Issue -like "*Near DNS lookup limit*") { + return "Consider optimizing SPF record to avoid DNS lookup limit: $($URLs.SPFSyntax)" + } elseif ($Issue -like "*Syntax:*") { + return "Fix SPF syntax errors - Microsoft SPF Syntax Guide: $($URLs.SPFSyntax)#spf-record-syntax" + } elseif ($Issue -like "*Low TTL for domain*") { + # Only show TTL recommendations for Microsoft/Office 365 providers + if ($Providers -contains "Microsoft/Office 365") { + # Extract domain and TTL from the issue text + if ($Issue -match "Low TTL for domain (.+?) \((\d+) seconds\)") { + $domain = $matches[1] + $currentTTL = $matches[2] + # Check if TTL is less than 3600 + if ([int]$currentTTL -lt 3600) { + return "Increase SPF record TTL for $domain from $currentTTL seconds to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } else { + return "Increase SPF record TTL to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } + } else { + return "Increase SPF record TTL to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } + } else { + # Don't show TTL recommendations for non-Microsoft providers + return "" + } + } elseif ($Issue -like "*Low TTL*") { + # Only show TTL recommendations for Microsoft/Office 365 providers + if ($Providers -contains "Microsoft/Office 365") { + return "Increase SPF record TTL to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } else { + # Don't show TTL recommendations for non-Microsoft providers + return "" + } + } elseif ($Issue -like "*Multiple SPF records*") { + return "Remove duplicate SPF records - Only one SPF record is allowed per domain (RFC 7208) $($URLs.SPFSyntax)" + } elseif ($Issue -like "*Macro Security:*") { + return "Review SPF macro usage and ensure to avoid complex macros that may expose infrastructure or create attack vectors $($URLs.SPFMacroSecurity)" + } elseif ($Issue -like "*TTL Sub-Records:*") { + # Only show TTL Sub-Records recommendations for Microsoft/Office 365 providers + if ($Providers -contains "Microsoft/Office 365") { + return "Increase TTL for A/MX records referenced in SPF to at least 3600 seconds (1 hour) - Low TTL values can impact SPF validation performance and reliability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } else { + # Don't show TTL recommendations for non-Microsoft providers + return "" + } + } else { + return "Review SPF configuration - Microsoft SPF Guide: $($URLs.SPFSetup)" + } + } + "DMARC" { + if ($Issue -like "*reporting email*") { + return "Configure DMARC reporting (rua/ruf) - Microsoft DMARC Reports: $($URLs.DMARCReports)" + } elseif ($Issue -like "*subdomain policy*weaker*") { + return "Strengthen subdomain policy to match or exceed main policy - Weak subdomain policies can be exploited - Microsoft DMARC Best Practices: $($URLs.DMARCSetup)" + } elseif ($Issue -like "*Invalid*alignment*") { + return "Fix DMARC alignment mode syntax - Valid values are 'r' (relaxed) or 's' (strict) - Microsoft DMARC Configuration: $($URLs.DMARCSetup)" + } elseif ($Issue -like "*Invalid subdomain policy*") { + return "Fix DMARC subdomain policy - Valid values are 'none', 'quarantine', or 'reject' - Microsoft DMARC Policies: $($URLs.DMARCSetup)" + } elseif ($Issue -like "*Low TTL*") { + # Only show TTL recommendations for Microsoft/Office 365 providers + if ($Providers -contains "Microsoft/Office 365") { + # Extract domain and TTL from issue text + if ($Issue -match "Low TTL for domain ([^\s]+) \((\d+) seconds\)") { + $domain = $matches[1] + $currentTTL = $matches[2] + # Check if TTL is less than 3600 + if ([int]$currentTTL -lt 3600) { + return "Increase DMARC record TTL for $domain from $currentTTL seconds to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } else { + return "Increase DMARC record TTL to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } + } else { + return "Increase DMARC record TTL to at least 3600 seconds (1 hour) for better DNS caching and stability - Microsoft SPF Troubleshooting Guide: https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure?view=o365-worldwide#troubleshooting-spf-txt-records" + } + } else { + # Don't show TTL recommendations for non-Microsoft providers + return "" + } + } + } + "DKIM" { + return "Fix DKIM syntax errors - Microsoft DKIM Configuration Guide: $($URLs.DKIMSetup)" + } + default { + return "Review email authentication configuration - Microsoft Documentation" + } + } +} + +# Function to analyze MX records for a domain +function Get-MXRecordAnalysis { + param([string]$domain) + + $mxAnalysis = @{ + MXFound = $false + MXRecords = @() + MinTTL = 0 + MaxTTL = 0 + AverageTTL = 0 + MXProviders = @() + PrimaryMX = "" + BackupMX = @() + } + + if ([string]::IsNullOrWhiteSpace($domain)) { + return $mxAnalysis + } + + try { + Write-Host " [MX] Checking MX records..." -ForegroundColor White + + # Query MX records from authoritative servers for accuracy + $authServers = Get-AuthoritativeDNSServers $domain + $mxRecords = Resolve-DnsNameAuthoritative -Name $domain -Type MX -AuthoritativeServers $authServers + + if ($mxRecords -and $mxRecords.Count -gt 0) { + $mxAnalysis.MXFound = $true + $ttlValues = @() + $priorities = @() + + # Sort MX records by priority + $sortedMXRecords = $mxRecords | Sort-Object Preference + + foreach ($mxRecord in $sortedMXRecords) { + if (-not [string]::IsNullOrWhiteSpace($mxRecord.NameExchange)) { + $mxInfo = @{ + Server = $mxRecord.NameExchange + Priority = $mxRecord.Preference + TTL = $mxRecord.TTL + } + + $mxAnalysis.MXRecords += $mxInfo + $ttlValues += $mxRecord.TTL + $priorities += $mxRecord.Preference + + # Identify primary MX (lowest priority number) + if ([string]::IsNullOrWhiteSpace($mxAnalysis.PrimaryMX)) { + $mxAnalysis.PrimaryMX = $mxRecord.NameExchange + } elseif ($mxRecord.Preference -lt $priorities[0]) { + # Move current primary to backup + if ($mxAnalysis.PrimaryMX -notin $mxAnalysis.BackupMX) { + $mxAnalysis.BackupMX += $mxAnalysis.PrimaryMX + } + $mxAnalysis.PrimaryMX = $mxRecord.NameExchange + } else { + # Add to backup MX list + if ($mxRecord.NameExchange -ne $mxAnalysis.PrimaryMX) { + $mxAnalysis.BackupMX += $mxRecord.NameExchange + } + } + + # Check for common email providers + $serverName = $mxRecord.NameExchange.ToLower() + $providerDetected = $false + + if ($serverName -match "outlook|protection\.outlook\.com|mail\.protection\.outlook\.com") { + if ("Microsoft/Office 365" -notin $mxAnalysis.MXProviders) { + $mxAnalysis.MXProviders += "Microsoft/Office 365" + } + $providerDetected = $true + } elseif ($serverName -match "Gmail|Google|GoogleMail") { + if ("Google/Gmail" -notin $mxAnalysis.MXProviders) { + $mxAnalysis.MXProviders += "Google/Gmail" + } + $providerDetected = $true + } elseif ($serverName -match "AmazonSES|ses") { + if ("Amazon SES" -notin $mxAnalysis.MXProviders) { + $mxAnalysis.MXProviders += "Amazon SES" + } + $providerDetected = $true + } elseif ($serverName -match "Proofpoint|Pphosted") { + if ("Proofpoint" -notin $mxAnalysis.MXProviders) { + $mxAnalysis.MXProviders += "Proofpoint" + } + $providerDetected = $true + } elseif ($serverName -match "Mimecast") { + if ("Mimecast" -notin $mxAnalysis.MXProviders) { + $mxAnalysis.MXProviders += "Mimecast" + } + $providerDetected = $true + } + + # Add Unknown provider if no known provider was detected + if (-not $providerDetected) { + if ("Unknown" -notin $mxAnalysis.MXProviders) { + $mxAnalysis.MXProviders += "Unknown" + } + } + + # TTL validation can be added here if needed + } + } + + # Calculate TTL statistics + if ($ttlValues.Count -gt 0) { + $mxAnalysis.MinTTL = ($ttlValues | Measure-Object -Minimum).Minimum + $mxAnalysis.MaxTTL = ($ttlValues | Measure-Object -Maximum).Maximum + $mxAnalysis.AverageTTL = [math]::Round(($ttlValues | Measure-Object -Average).Average, 0) + } + + # MX configuration validation can be added here if needed + + Write-Host " MX records found: $($mxRecords.Count)" -ForegroundColor Green + Write-Host " Primary MX: $($mxAnalysis.PrimaryMX)" -ForegroundColor Cyan + if ($mxAnalysis.BackupMX.Count -gt 0) { + Write-Host " Backup MX: $($mxAnalysis.BackupMX -join ', ')" -ForegroundColor Cyan + } + if ($mxAnalysis.MXProviders.Count -gt 0) { + Write-Host " Provider: $($mxAnalysis.MXProviders -join ', ')" -ForegroundColor Cyan + } + } else { + Write-Host " No MX records found" -ForegroundColor Red + } + } catch { + Write-Host " Error checking MX records: $($_.Exception.Message)" -ForegroundColor Red + } + + return $mxAnalysis +} + +# Function to count DNS lookups in SPF record +function Get-SpfDnsLookupCount { + param([string]$spfRecord) + + $lookupCount = 0 + + # Split SPF record into mechanisms + $mechanisms = $spfRecord -split '\s+' | Where-Object { $_ -ne '' } + + foreach ($mechanism in $mechanisms) { + # Count mechanisms that require DNS lookups or a mechanism without domain (uses current domain) or a/mx mechanisms with CIDR but no domain + if ($mechanism -match '^(include:|a:|mx:|exists:|redirect=)' -or $mechanism -eq 'a' -or $mechanism -eq 'mx' -or $mechanism -match '^(a|mx)/\d+$') { + $lookupCount++ + } + } + + return $lookupCount +} + +# Function to validate SPF record syntax +function Test-SPFSyntax { + param([string]$spfRecord) + + Write-Verbose "Test-SPFSyntax: Calling $($MyInvocation.MyCommand): Processing spfRecord: '$spfRecord'" + + $syntaxIssues = @() + + # Check if record starts with v=spf1 + if (-not ($spfRecord -match '^v=spf1\b')) { + $syntaxIssues += "Must start with 'v=spf1'" + return $syntaxIssues # If this fails, other checks may not be meaningful + } + + # Split record into mechanisms and modifiers + $mechanisms = $spfRecord -split '\s+' | Where-Object { $_ -ne '' -and $_ -ne 'v=spf1' } + + # Check for multiple 'all' mechanisms + $allCount = ($mechanisms | Where-Object { $_ -match '^[+\-~?]?all$' }).Count + if ($allCount -gt 1) { + $syntaxIssues += "Multiple 'all' mechanisms found (only one allowed)" + } + + # Validate each mechanism + foreach ($mechanism in $mechanisms) { + # Check for modifiers (contain '=') + if ($mechanism -match '=' -and $mechanism -notmatch '^(include:|a:|mx:|ptr:|exists:|redirect=)') { + # Check for unknown modifiers/mechanisms + if ($mechanism -notmatch '^(exp=|redirect=)') { + $syntaxIssues += "Unknown modifier or mechanism: '$mechanism'" + } + } elseif ($mechanism -match '^[+\-~?]?(all|include:|a|mx|ptr|exists:|ip4:|ip6:)') { + # Valid mechanism types, check specific syntax + if ($mechanism -match '^[+\-~?]?ip4:') { + # Validate IPv4 address/CIDR + $ipPart = $mechanism -replace '^[+\-~?]?ip4:', '' + if (-not ($ipPart -match '^(\d{1,3}\.){3}\d{1,3}(/\d{1,2})?$')) { + $syntaxIssues += "Invalid IPv4 syntax: '$mechanism'" + } + } elseif ($mechanism -match '^[+\-~?]?ip6:') { + # Basic IPv6 validation (simplified) + $ipPart = $mechanism -replace '^[+\-~?]?ip6:', '' + if (-not ($ipPart -match '^[0-9a-fA-F:]+(/\d{1,3})?$')) { + $syntaxIssues += "Invalid IPv6 syntax: '$mechanism'" + } + } elseif ($mechanism -match '^[+\-~?]?include:') { + # Validate include domain + $domain = $mechanism -replace '^[+\-~?]?include:', '' + if ([string]::IsNullOrEmpty($domain) -or $domain -match '\s') { + $syntaxIssues += "Invalid include syntax: '$mechanism'" + } + } elseif ($mechanism -match '^[+\-~?]?exists:') { + # Validate exists domain + $domain = $mechanism -replace '^[+\-~?]?exists:', '' + if ([string]::IsNullOrEmpty($domain) -or $domain -match '\s') { + $syntaxIssues += "Invalid exists syntax: '$mechanism'" + } + } + } else { + # Unknown mechanism + $syntaxIssues += "Unknown or invalid mechanism: '$mechanism'" + } + } + + # Check for 'all' mechanism (should be present) + $hasAll = $mechanisms | Where-Object { $_ -match '^[+\-~?]?all$' } + if (-not $hasAll) { + $syntaxIssues += "Missing 'all' mechanism (recommended as last mechanism)" + } + + # Check if 'all' is the last mechanism (best practice) + if ($hasAll -and $mechanisms.Count -gt 1) { + $lastMechanism = $mechanisms[-1] + if ($lastMechanism -notmatch '^[+\-~?]?all$') { + $syntaxIssues += "Recommend placing 'all' mechanism as the last mechanism" + } + } + + return $syntaxIssues +} + +# Function to validate DKIM record syntax +function Test-DKIMSyntax { + param([string]$dkimRecord, [string]$selector) + + Write-Verbose "Test-DKIMSyntax: Calling $($MyInvocation.MyCommand): Processing dkimRecord: '$dkimRecord' for selector '$selector'" + + $syntaxIssues = @() + + if ([string]::IsNullOrWhiteSpace($dkimRecord)) { + $syntaxIssues += "Empty DKIM record" + return $syntaxIssues + } + + # Parse DKIM record using helper function + $tags = ConvertFrom-DKIMRecord $dkimRecord + + if ($tags.Count -eq 0) { + $syntaxIssues += "No valid DKIM tags found" + return $syntaxIssues + } + + # Check for invalid tag format in original record + $parts = $dkimRecord -split ';' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + foreach ($part in $parts) { + if ($part -notmatch '^([a-z]+)=(.*)$') { + $syntaxIssues += "Invalid tag format: '$part'" + } + } + + # Check required tags + + # 'v=' tag (version) - optional but recommended + if ($tags.ContainsKey('v')) { + if ($tags['v'] -ne 'DKIM1') { + $syntaxIssues += "Invalid version: expected 'DKIM1', found '$($tags['v'])'" + } + } + + # 'k=' tag (key type) - optional, defaults to 'rsa' + if ($tags.ContainsKey('k')) { + $validKeyTypes = @('rsa', 'ed25519') + if ($tags['k'] -notin $validKeyTypes) { + $syntaxIssues += "Invalid key type: '$($tags['k'])' (valid: $($validKeyTypes -join ', '))" + } + } + + # 'p=' tag (public key) - required and must not be empty for active keys + if (-not $tags.ContainsKey('p')) { + $syntaxIssues += "Missing required 'p=' tag (public key)" + } else { + $publicKey = $tags['p'] + if ([string]::IsNullOrWhiteSpace($publicKey)) { + # Empty p= tag indicates revoked key + $syntaxIssues += "Empty public key (p=) - key is revoked" + } else { + # Basic Base64 validation for public key + try { + $cleanKey = $publicKey -replace '\s', '' + if ($cleanKey -notmatch '^[A-Za-z0-9+/]*={0,2}$') { + $syntaxIssues += "Invalid Base64 format in public key" + } + } catch { + $syntaxIssues += "Invalid public key format" + } + } + } + + # 'h=' tag (hash algorithms) - optional + if ($tags.ContainsKey('h')) { + $validHashAlgorithms = @('sha1', 'sha256') + $hashAlgorithms = $tags['h'] -split ':' | ForEach-Object { $_.Trim() } + foreach ($hash in $hashAlgorithms) { + if ($hash -notin $validHashAlgorithms) { + $syntaxIssues += "Invalid hash algorithm: '$hash' (valid: $($validHashAlgorithms -join ', '))" + } + } + # Recommend sha256 over sha1 + if ($hashAlgorithms -contains 'sha1' -and $hashAlgorithms -notcontains 'sha256') { + $syntaxIssues += "Consider using 'sha256' instead of 'sha1' for better security" + } + } + + # 'g=' tag (granularity) - optional, deprecated + if ($tags.ContainsKey('g')) { + $syntaxIssues += "Granularity tag 'g=' is deprecated and should be removed" + } + + # 's=' tag (service type) - optional + if ($tags.ContainsKey('s')) { + $validServiceTypes = @('email', '*') + $serviceTypes = $tags['s'] -split ':' | ForEach-Object { $_.Trim() } + foreach ($service in $serviceTypes) { + if ($service -notin $validServiceTypes) { + $syntaxIssues += "Invalid service type: '$service' (valid: $($validServiceTypes -join ', '))" + } + } + } + + # 't=' tag (flags) - optional + if ($tags.ContainsKey('t')) { + $validFlags = @('y', 's') + $flags = $tags['t'] -split ':' | ForEach-Object { $_.Trim() } + foreach ($flag in $flags) { + if ($flag -notin $validFlags) { + $syntaxIssues += "Invalid flag: '$flag' (valid: $($validFlags -join ', '))" + } + } + # Check for testing flag + if ($flags -contains 'y') { + $syntaxIssues += "Testing flag 'y' is set - remove for production use" + } + } + + # Check for unknown tags + $knownTags = @('v', 'k', 'p', 'h', 'g', 's', 't', 'n') + foreach ($tagName in $tags.Keys) { + if ($tagName -notin $knownTags) { + $syntaxIssues += "Unknown tag: '$tagName'" + } + } + + return $syntaxIssues +} + +# Function to detect DKIM service providers +function Get-DKIMServiceProvider { + param([hashtable]$dkimRecords, [string]$domain) + + $providerInfo = @{ + DetectedProviders = @() + SelectorPatterns = @() + Details = @() + } + + # Common DKIM provider patterns + $providerPatterns = @{ + 'Microsoft/Office 365' = @('selector1', 'selector2') + 'Google/Gmail' = @('Google', 'Gmail') + 'Amazon SES' = @('AmazonSES') + 'Mailchimp' = @('k1', 'k2', 'k3') + 'SendGrid' = @('s1', 's2', 'smtpapi') + 'Constant Contact' = @('k1', 'k2') + 'Mailgun' = @('k1', 'mailo') + 'Mandrill' = @('mandrill') + 'Postmark' = @('pm', 'postmark') + 'SparkPost' = @('scph') + 'Zendesk' = @('Zendesk1', 'Zendesk2') + 'Salesforce' = @('Salesforce') + 'HubSpot' = @('hs1', 'hs2') + 'Klaviyo' = @('dkim') + 'Campaign Monitor' = @('cm') + 'AWeber' = @('AWeber') + 'GetResponse' = @('GetResponse') + 'ConvertKit' = @('ConvertKit') + 'ActiveCampaign' = @('ac') + 'Drip' = @('drip') + 'Infusionsoft' = @('ifs') + 'Pardot' = @('Pardot') + 'Marketo' = @('Marketo') + 'Eloqua' = @('Eloqua') + 'Braze' = @('braze') + 'Iterable' = @('iterable') + 'Sendlane' = @('Sendlane') + 'Moosend' = @('Moosend') + 'Omnisend' = @('Omnisend') + 'Benchmark' = @('benchmark') + 'EmailOctopus' = @('EmailOctopus') + 'Sendinblue' = @('Sendinblue') + 'Elastic Email' = @('Elasticemail') + 'Pepipost' = @('Pepipost') + 'Socketlabs' = @('Socketlabs') + 'Mailjet' = @('Mailjet') + 'SMTP2GO' = @('smtp2go') + 'Turbo-SMTP' = @('turbo-smtp') + 'Dynadot' = @('Dynadot') + 'Zoho Mail' = @('Zoho') + 'Titan Email' = @('titan') + 'Protonmail' = @('Protonmail') + 'Fastmail' = @('fm1', 'fm2', 'fm3') + 'Rackspace' = @('Rackspace') + 'Bluehost' = @('default') + 'GoDaddy' = @('k1') + 'Namecheap' = @('default') + 'HostGator' = @('default') + 'SiteGround' = @('default') + 'cPanel' = @('default') + 'Plesk' = @('default') + 'Generic' = @('default', 'mail', 'dkim', 'key1', 'key2') + } + + foreach ($selector in $dkimRecords.Keys) { + $selectorName = $selector.ToLower() + $providerInfo.SelectorPatterns += $selectorName + + $matchedProvider = $null + foreach ($provider in $providerPatterns.Keys) { + $patterns = $providerPatterns[$provider] + if ($patterns -contains $selectorName) { + $matchedProvider = $provider + break + } + } + + if ($matchedProvider) { + if ($matchedProvider -notin $providerInfo.DetectedProviders) { + $providerInfo.DetectedProviders += $matchedProvider + } + $providerInfo.Details += "Selector '$selector': Matches $matchedProvider pattern" + } else { + $providerInfo.Details += "Selector '$selector': Custom/Unknown provider" + } + } + + return $providerInfo +} + +# Function to extract and analyze SPF all mechanism +function Get-SPFAllMechanism { + param([string]$spfRecord) + + # Split SPF record into mechanisms + $mechanisms = $spfRecord -split '\s+' | Where-Object { $_ -ne '' } + + # Find all mechanism + $allMechanism = $mechanisms | Where-Object { $_ -match '^[+\-~?]?all$' } | Select-Object -Last 1 + + if ($allMechanism) { + return $allMechanism + } else { + return "" + } +} + +# Function to check for multiple SPF records (RFC violation) +function Test-MultipleSPFRecords { + param([string]$domain) + + Write-Verbose "Test-MultipleSPFRecords: Calling $($MyInvocation.MyCommand): Processing domain: '$domain'" + + $multipleRecordIssues = @() + + try { + # Get authoritative servers for the domain + $authServers = Get-AuthoritativeDNSServers $domain + $allTxtRecords = Resolve-DnsNameAuthoritative -Name $domain -Type TXT -AuthoritativeServers $authServers + $spfRecords = $allTxtRecords | Where-Object { $_.Strings -like "v=spf*" } + + if ($spfRecords.Count -gt 1) { + $multipleRecordIssues += "Multiple SPF records found - RFC 7208 violation (only one allowed)" + for ($i = 0; $i -lt $spfRecords.Count; $i++) { + $recordContent = $spfRecords[$i].Strings -join "" + $multipleRecordIssues += "SPF Record $($i+1): $recordContent" + } + } + } catch { + $multipleRecordIssues += "Error checking for multiple SPF records: $($_.Exception.Message)" + } + + return $multipleRecordIssues +} + +# Function to validate SPF macros and check for security issues +function Test-SPFMacroSecurity { + param([string]$spfRecord) + + Write-Verbose "Test-SPFMacroSecurity: Calling $($MyInvocation.MyCommand): Processing spfRecord: '$spfRecord'" + + $macroSecurityIssues = @() + + if ([string]::IsNullOrWhiteSpace($spfRecord)) { + return $macroSecurityIssues + } + + # Check for SPF macros (% followed by {}) + $macroMatches = [regex]::Matches($spfRecord, '%\{([^}]*)\}') + + if ($macroMatches.Count -eq 0) { + # No macros found - this is good for security + return $macroSecurityIssues + } + + # Validate each macro for security and syntax + foreach ($macroMatch in $macroMatches) { + $fullMacro = $macroMatch.Value + $macroContent = $macroMatch.Groups[1].Value + + # Parse macro components: letter[digits[r]][delimiter[...]] + if ($macroContent -match '^([slodiptcrv])(\d+)?(r)?(\.[^}]*)?$') { + $macroLetter = $matches[1] + $digits = $matches[2] + $reverse = $matches[3] + $delimiter = $matches[4] + + # Check for potentially dangerous macro letters + switch ($macroLetter) { + 'i' { + # IP address - generally safe but can reveal infrastructure + if ($digits -and [int]$digits -lt 16) { + $macroSecurityIssues += "Macro '$fullMacro' uses short IP truncation ($digits chars) - may not provide sufficient uniqueness" + } + } + 'p' { + # PTR record - deprecated and slow, potential security risk + $macroSecurityIssues += "Macro '$fullMacro' uses PTR mechanism (deprecated) - can cause performance issues and DNS dependencies" + } + 'c' { + # Client IP - can be spoofed in some contexts + $macroSecurityIssues += "Macro '$fullMacro' uses client IP validation - ensure this is intended and secure in your environment" + } + 'r' { + # Domain name in reverse - complex processing + if (-not $reverse) { + $macroSecurityIssues += "Macro '$fullMacro' processes domain names - verify the source domain is trusted" + } + } + 't' { + # Timestamp - can be manipulated + $macroSecurityIssues += "Macro '$fullMacro' uses timestamp validation - ensure time synchronization is reliable" + } + } + + # Check for overly complex delimiters + if ($delimiter -and $delimiter.Length -gt 10) { + $macroSecurityIssues += "Macro '$fullMacro' has complex delimiter '$delimiter' - review for necessity and security" + } + + # Check for reverse processing combined with truncation + if ($reverse -and $digits -and [int]$digits -lt 8) { + $macroSecurityIssues += "Macro '$fullMacro' combines reverse processing with short truncation - may cause unexpected behavior" + } + } else { + # Invalid macro syntax + $macroSecurityIssues += "Invalid macro syntax: '$fullMacro' - does not match valid SPF macro format" + } + } + + # Check for macros in exists: mechanisms (often used for complex lookups) + $existsWithMacros = [regex]::Matches($spfRecord, 'exists:[^%]*%\{[^}]*\}') + if ($existsWithMacros.Count -gt 0) { + $macroSecurityIssues += "Complex macro usage in exists: mechanism detected - review for security and necessity (can be used for data exfiltration)" + } + + # Check for multiple macros in a single mechanism + $mechanisms = $spfRecord -split '\s+' | Where-Object { $_ -ne '' -and $_ -ne 'v=spf1' } + foreach ($mechanism in $mechanisms) { + $mechanismMacros = [regex]::Matches($mechanism, '%\{[^}]*\}') + if ($mechanismMacros.Count -gt 2) { + $macroSecurityIssues += "Mechanism '$mechanism' contains $($mechanismMacros.Count) macros - excessive complexity may indicate security risk" + } + } + + # Overall macro count check + if ($macroMatches.Count -gt 5) { + $macroSecurityIssues += "SPF record contains $($macroMatches.Count) macros - high complexity increases attack surface and debugging difficulty" + } + + return $macroSecurityIssues +} + +# Function to check TTL for SPF sub-records (A records referenced in SPF) +function Test-SPFSubRecordsTTL { + param([string]$spfRecord, [string]$domain) + + Write-Verbose "Test-SPFSubRecordsTTL: Calling $($MyInvocation.MyCommand): Processing spfRecord: '$spfRecord' for domain: '$domain'" + + $subRecordIssues = @() + $checkedRecords = @() + + if ([string]::IsNullOrWhiteSpace($spfRecord)) { + return $subRecordIssues + } + + # Extract A record mechanisms from SPF record + $mechanisms = $spfRecord -split '\s+' | Where-Object { $_ -ne '' -and $_ -ne 'v=spf1' } + + foreach ($mechanism in $mechanisms) { + $domainToCheck = $null + + # Check for a: mechanisms with explicit domain + if ($mechanism -match '^[+\-~?]?a:([^/\s]+)') { + $domainToCheck = $matches[1] + } + # Check for a mechanism without domain (uses current domain) + elseif ($mechanism -match '^[+\-~?]?a(/\d+)?$') { + $domainToCheck = $domain + } + # Check for mx: mechanisms with explicit domain + elseif ($mechanism -match '^[+\-~?]?mx:([^/\s]+)') { + $domainToCheck = $matches[1] + } + # Check for mx mechanism without domain (uses current domain) + elseif ($mechanism -match '^[+\-~?]?mx(/\d+)?$') { + $domainToCheck = $domain + } + # Check for include: mechanisms (NEW - check TXT record TTL) + elseif ($mechanism -match '^[+\-~?]?include:([^/\s]+)') { + $includeDomain = $matches[1] + + # Skip if already checked + if ($includeDomain -in $checkedRecords) { + continue + } + + $checkedRecords += $includeDomain + + try { + # Check TXT records for the included domain against authoritative servers + $authServers = Get-AuthoritativeDNSServers $includeDomain + $txtRecords = Resolve-DnsNameAuthoritative -Name $includeDomain -Type TXT -AuthoritativeServers $authServers + + if ($txtRecords) { + foreach ($txtRecord in $txtRecords) { + # Only check SPF records (those starting with "v=spf1") + if ($txtRecord.Strings -match '^v=spf1\b') { + if ($txtRecord.TTL -lt 3600) { + $subRecordIssues += "TXT record (SPF) for include domain '$includeDomain' has low TTL ($($txtRecord.TTL) seconds) - recommend 3600+ seconds for stability" + } + } + } + } else { + $subRecordIssues += "TXT record for include domain '$includeDomain' not found or inaccessible - SPF validation may fail" + } + } catch { + $subRecordIssues += "Error checking TXT records for include domain '$includeDomain': $($_.Exception.Message)" + } + continue + } + + # Skip if no domain to check or already checked + if (-not $domainToCheck -or $domainToCheck -in $checkedRecords) { + continue + } + + $checkedRecords += $domainToCheck + + try { + # Check A records for the domain against authoritative servers + $authServers = Get-AuthoritativeDNSServers $domainToCheck + $aRecords = Resolve-DnsNameAuthoritative -Name $domainToCheck -Type A -AuthoritativeServers $authServers + + if ($aRecords) { + foreach ($aRecord in $aRecords) { + if ($aRecord.TTL -lt 3600) { + $subRecordIssues += "A record for '$domainToCheck' has low TTL ($($aRecord.TTL) seconds) - recommend 3600+ seconds for stability" + } + } + } else { + $subRecordIssues += "A record for '$domainToCheck' not found or inaccessible - SPF validation may fail" + } + + # Also check MX records if it's an MX mechanism + if ($mechanism -match '^[+\-~?]?mx') { + $mxAuthServers = Get-AuthoritativeDNSServers $domainToCheck + $mxRecords = Resolve-DnsNameAuthoritative -Name $domainToCheck -Type MX -AuthoritativeServers $mxAuthServers + + if ($mxRecords) { + foreach ($mxRecord in $mxRecords) { + if ($mxRecord.TTL -lt 3600) { + $subRecordIssues += "MX record for '$domainToCheck' has low TTL ($($mxRecord.TTL) seconds) - recommend 3600+ seconds for stability" + } + } + } else { + $subRecordIssues += "MX record for '$domainToCheck' not found or inaccessible - SPF validation may fail" + } + } + } catch { + $subRecordIssues += "Error checking records for '$domainToCheck': $($_.Exception.Message)" + } + } + + return $subRecordIssues +} + +# Function to collect TTL values for SPF sub-records (A/MX/TXT records referenced in SPF) +function Get-SPFSubRecordsTTLValues { + param([string]$spfRecord, [string]$domain) + + $subRecordTTLValues = @{} + $checkedRecords = @() + + if ([string]::IsNullOrWhiteSpace($spfRecord)) { + return $subRecordTTLValues + } + + # Extract A record mechanisms from SPF record + $mechanisms = $spfRecord -split '\s+' | Where-Object { $_ -ne '' -and $_ -ne 'v=spf1' } + + foreach ($mechanism in $mechanisms) { + $domainToCheck = $null + $recordType = "" + + # Check for a: mechanisms with explicit domain + if ($mechanism -match '^[+\-~?]?a:([^/\s]+)') { + $domainToCheck = $matches[1] + $recordType = "A" + } + # Check for a mechanism without domain (uses current domain) + elseif ($mechanism -match '^[+\-~?]?a(/\d+)?$') { + $domainToCheck = $domain + $recordType = "A" + } + # Check for mx: mechanisms with explicit domain + elseif ($mechanism -match '^[+\-~?]?mx:([^/\s]+)') { + $domainToCheck = $matches[1] + $recordType = "MX" + } + # Check for mx mechanism without domain (uses current domain) + elseif ($mechanism -match '^[+\-~?]?mx(/\d+)?$') { + $domainToCheck = $domain + $recordType = "MX" + } + # Check for include: mechanisms (NEW - collect TXT record TTL) + elseif ($mechanism -match '^[+\-~?]?include:([^/\s]+)') { + $includeDomain = $matches[1] + + # Skip if already checked + if ($includeDomain -in $checkedRecords) { + continue + } + + $checkedRecords += $includeDomain + + try { + # Check TXT records for the included domain against authoritative servers + $authServers = Get-AuthoritativeDNSServers $includeDomain + $txtRecords = Resolve-DnsNameAuthoritative -Name $includeDomain -Type TXT -AuthoritativeServers $authServers + + if ($txtRecords) { + $ttlValues = @() + foreach ($txtRecord in $txtRecords) { + # Only collect SPF records (those starting with "v=spf1") + if ($txtRecord.Strings -match '^v=spf1\b') { + $spfContent = ($txtRecord.Strings -join '') + $ttlValues += "${spfContent}: $($txtRecord.TTL)s" + } + } + if ($ttlValues.Count -gt 0) { + $subRecordTTLValues["$includeDomain (TXT-SPF)"] = $ttlValues -join ", " + } + } else { + $subRecordTTLValues["$includeDomain (TXT-SPF Error)"] = "TXT record not found or inaccessible" + } + } catch { + $subRecordTTLValues["$includeDomain (TXT-SPF Error)"] = "Unable to retrieve TTL: $($_.Exception.Message)" + } + continue + } + + # Skip if no domain to check or already checked + if (-not $domainToCheck -or $domainToCheck -in $checkedRecords) { + continue + } + + $checkedRecords += $domainToCheck + + try { + # Check A records for the domain against authoritative servers + if ($recordType -eq "A" -or $mechanism -match '^[+\-~?]?a') { + $authServers = Get-AuthoritativeDNSServers $domainToCheck + $aRecords = Resolve-DnsNameAuthoritative -Name $domainToCheck -Type A -AuthoritativeServers $authServers + + if ($aRecords) { + $ttlValues = @() + foreach ($aRecord in $aRecords) { + # Only add entries with valid IP addresses + if (-not [string]::IsNullOrWhiteSpace($aRecord.IPAddress)) { + $ttlValues += "$($aRecord.IPAddress): $($aRecord.TTL)s" + } + } + if ($ttlValues.Count -gt 0) { + $subRecordTTLValues["$domainToCheck (A)"] = $ttlValues -join ", " + } + } + } + + # Also check MX records if it's an MX mechanism + if ($mechanism -match '^[+\-~?]?mx') { + $mxAuthServers = Get-AuthoritativeDNSServers $domainToCheck + $mxRecords = Resolve-DnsNameAuthoritative -Name $domainToCheck -Type MX -AuthoritativeServers $mxAuthServers + + if ($mxRecords) { + $ttlValues = @() + foreach ($mxRecord in $mxRecords) { + # Only add entries with valid NameExchange values + if (-not [string]::IsNullOrWhiteSpace($mxRecord.NameExchange)) { + $ttlValues += "$($mxRecord.NameExchange) (Priority: $($mxRecord.Preference)): $($mxRecord.TTL)s" + } + } + if ($ttlValues.Count -gt 0) { + $subRecordTTLValues["$domainToCheck (MX)"] = $ttlValues -join ", " + } + } + } + } catch { + $subRecordTTLValues["$domainToCheck (Error)"] = "Unable to retrieve TTL: $($_.Exception.Message)" + } + } + + return $subRecordTTLValues +} + +# Function to extract DKIM key length from public key +function Get-DKIMKeyLength { + param([string]$dkimRecord) + + if ([string]::IsNullOrWhiteSpace($dkimRecord)) { + return @{ + KeyLength = 0 + KeyType = "Unknown" + IsWeak = $false + Error = "No DKIM record provided" + } + } + + # Parse DKIM record using helper function + $tags = ConvertFrom-DKIMRecord $dkimRecord + + # Get key type (default is RSA if not specified) + $keyType = if ($tags.ContainsKey('k')) { $tags['k'] } else { "rsa" } + + # Check if key is revoked (empty p= tag) + if ($tags.ContainsKey('p') -and [string]::IsNullOrWhiteSpace($tags['p'])) { + return @{ + KeyLength = 0 + KeyType = $keyType + IsWeak = $false + Error = "Key is revoked (empty p= tag)" + } + } + + # Get public key + if (-not $tags.ContainsKey('p')) { + return @{ + KeyLength = 0 + KeyType = $keyType + IsWeak = $false + Error = "No public key (p=) tag found" + } + } + + $publicKey = $tags['p'] + if ([string]::IsNullOrWhiteSpace($publicKey)) { + return @{ + KeyLength = 0 + KeyType = $keyType + IsWeak = $false + Error = "Empty public key" + } + } + + try { + # Clean the Base64 key (remove whitespace) + $cleanKey = $publicKey -replace '\s', '' + + # Validate Base64 format + if ($cleanKey -notmatch '^[A-Za-z0-9+/]*={0,2}$') { + return @{ + KeyLength = 0 + KeyType = $keyType + IsWeak = $false + Error = "Invalid Base64 format in public key" + } + } + + # Decode Base64 to get the DER-encoded key + $keyBytes = [System.Convert]::FromBase64String($cleanKey) + + # For RSA keys, we need to parse the ASN.1 DER structure + if ($keyType -eq "rsa") { + # RSA public key in DER format starts with a sequence + # We'll do a simplified parsing to extract the modulus length + + # Look for the RSA modulus (first large integer in the sequence) + # This is a simplified approach - in a real implementation you'd use proper ASN.1 parsing + + # The modulus typically starts after the algorithm identifier + # We'll search for large byte sequences that likely represent the modulus + $keyLength = 0 + + # Look for typical RSA key patterns + # 1024-bit keys typically have modulus around 128 bytes (256 hex chars) + # 2048-bit keys typically have modulus around 256 bytes (512 hex chars) + # 4096-bit keys typically have modulus around 512 bytes (1024 hex chars) + + $keyLength = switch ($keyBytes.Length) { + { $_ -ge 512 -and $_ -lt 768 } { 4096 } # 4096-bit key + { $_ -ge 294 -and $_ -lt 512 } { 2048 } # 2048-bit key + { $_ -ge 162 -and $_ -lt 294 } { 1024 } # 1024-bit key + { $_ -ge 94 -and $_ -lt 162 } { 512 } # 512-bit key (very weak) + default { + # Try to estimate based on total key size + $estimatedBits = [math]::Round(($keyBytes.Length - 30) * 8 / 1.2, 0) + if ($estimatedBits -gt 4096) { 4096 } + elseif ($estimatedBits -gt 2048) { 2048 } + elseif ($estimatedBits -gt 1024) { 1024 } + elseif ($estimatedBits -gt 512) { 512 } + else { $estimatedBits } + } + } + + $isWeak = $keyLength -lt 1024 # Only keys below 1024 are considered weak + + return @{ + KeyLength = $keyLength + KeyType = $keyType + IsWeak = $isWeak + Error = $null + } + } elseif ($keyType -eq "ed25519") { + # Ed25519 keys are always 256 bits (32 bytes for the public key) + return @{ + KeyLength = 256 + KeyType = $keyType + IsWeak = $false # Ed25519 is considered secure + Error = $null + } + } else { + return @{ + KeyLength = 0 + KeyType = $keyType + IsWeak = $false + Error = "Unsupported key type: $keyType" + } + } + } catch { + return @{ + KeyLength = 0 + KeyType = $keyType + IsWeak = $false + Error = "Failed to parse public key: $($_.Exception.Message)" + } + } +} + +# Helper function to get DKIM key status +function Get-DKIMKeyStatus { + param([string]$dkimRecord) + + if ([string]::IsNullOrWhiteSpace($dkimRecord)) { + return "N/A" + } + + $tags = ConvertFrom-DKIMRecord $dkimRecord + + # Check if this is a revoked key (empty p= tag) + if ($tags.ContainsKey('p') -and [string]::IsNullOrWhiteSpace($tags['p'])) { + return "REVOKED" + } + + # Check for testing flag + if ($tags.ContainsKey('t') -and $tags['t'] -match 'y') { + return "TESTING" + } + + # Check for active key with valid public key + if ($tags.ContainsKey('p') -and -not [string]::IsNullOrWhiteSpace($tags['p'])) { + return "ACTIVE" + } + + return "UNKNOWN" +} + +# Function to get authoritative DNS servers and their IP addresses for a domain +function Get-AuthoritativeDNSServers { + param([string]$domain) + + $authServers = @() + + try { + # Get NS records for the domain + $nsRecords = Resolve-DnsName -Name $domain -Type NS -ErrorAction SilentlyContinue + + if ($nsRecords) { + foreach ($ns in $nsRecords) { + if ($ns.Type -eq "NS") { + try { + # Resolve IP address of NS server + $nsIP = (Resolve-DnsName -Name $ns.NameHost -Type A -ErrorAction SilentlyContinue)[0].IPAddress + if ($nsIP) { + $authServers += [PSCustomObject]@{ + NameHost = $ns.NameHost + IPAddress = $nsIP + } + } + } catch { + Write-Verbose "Could not resolve IP for NS server $($ns.NameHost): $_" + } + } + } + } + + # If no NS records found for the domain, try the parent domain + if ($authServers.Count -eq 0 -and $domain.Contains('.')) { + $parentDomain = $domain.Substring($domain.IndexOf('.') + 1) + $parentNS = Resolve-DnsName -Name $parentDomain -Type NS -ErrorAction SilentlyContinue + + if ($parentNS) { + foreach ($ns in $parentNS) { + if ($ns.Type -eq "NS") { + try { + $nsIP = (Resolve-DnsName -Name $ns.NameHost -Type A -ErrorAction SilentlyContinue)[0].IPAddress + if ($nsIP) { + $authServers += [PSCustomObject]@{ + NameHost = $ns.NameHost + IPAddress = $nsIP + } + } + } catch { + Write-Verbose "Could not resolve IP for parent NS server $($ns.NameHost): $_" + } + } + } + } + } + } catch { + Write-Verbose "Error finding authoritative servers for $domain`: $_" + } + + return $authServers +} + +# Function to perform DNS query against authoritative servers +function Resolve-DnsNameAuthoritative { + param( + [string]$Name, + [string]$Type, + [array]$AuthoritativeServers = @() + ) + + $results = @() + + # If no authoritative servers provided, find them + if ($AuthoritativeServers.Count -eq 0) { + $domain = $Name + # Extract domain from subdomain queries like _dmarc.example.com or selector1._domainkey.example.com + if ($Name.Contains('.')) { + $parts = $Name -split '\.' + if ($parts.Count -gt 2) { + # For DKIM records like selector1._domainkey.example.com, extract example.com + if ($Name -match '_domainkey\.(.+)$') { + $domain = $matches[1] + } + # For DMARC records like _dmarc.example.com, extract example.com + elseif ($Name -match '^_dmarc\.(.+)$') { + $domain = $matches[1] + } + # For other subdomains, try the main domain + else { + $domain = ($parts[-2..-1]) -join '.' + } + } + } + $AuthoritativeServers = Get-AuthoritativeDNSServers $domain + } + + # If we have authoritative servers, query them directly + if ($AuthoritativeServers.Count -gt 0) { + foreach ($server in $AuthoritativeServers) { + try { + Write-Verbose "Querying authoritative server: $($server.NameHost) ($($server.IPAddress)) for $Name ($Type)" + # Query using the IP address of the authoritative server + $result = Resolve-DnsName -Name $Name -Type $Type -Server $server.IPAddress -ErrorAction SilentlyContinue + if ($result) { + $results += $result + Write-Verbose "Successfully retrieved $($result.Count) records from $($server.NameHost)" + break # Use first successful result + } + } catch { + Write-Verbose "Failed to query $($server.NameHost) ($($server.IPAddress)) for $Name`: $_" + continue + } + } + } + + # Fallback to regular DNS query if authoritative query fails + if ($results.Count -eq 0) { + try { + Write-Verbose "Falling back to regular DNS query for $Name ($Type)" + $results = Resolve-DnsName -Name $Name -Type $Type -ErrorAction SilentlyContinue + } catch { + Write-Verbose "Regular DNS query also failed for $Name`: $_" + } + } + + return $results +} + +# Function to validate domain name format +function Test-DomainFormat { + param( + [string]$DomainName, + [string]$Context = "domain" + ) + + Write-Verbose "Test-DomainFormat: Calling $($MyInvocation.MyCommand): Processing domain: '$DomainName' in context: '$Context'" + + # Check for invalid characters based on context + $invalidChars = @() + + if ($Context -eq "single") { + # For single domain analysis, comma and semicolon are not allowed + $invalidChars = @(',', ';') + $invalidPattern = '[,;]' + } elseif ($Context -eq "multiple") { + # For multiple domain analysis, semicolon, backslash, and forward slash are not allowed + $invalidChars = @(';', '\', '/') + $invalidPattern = '[;\\\/]' + } + + # Check for invalid characters + if ($DomainName -match $invalidPattern) { + $foundChars = @() + foreach ($char in $invalidChars) { + if ($DomainName.Contains($char)) { + $foundChars += "'$char'" + } + } + + Write-Host "" + Write-Host "ERROR: Invalid characters detected in domain input!" -ForegroundColor Red + Write-Host "Found invalid character(s): $($foundChars -join ', ')" -ForegroundColor Yellow + + if ($Context -eq "single") { + Write-Host "" + Write-Host "For Single Domain Analysis:" -ForegroundColor Cyan + Write-Host " - Use only valid domain characters (letters, numbers, dots, hyphens)" -ForegroundColor White + Write-Host " - Example: example.com" -ForegroundColor Green + Write-Host " - Do NOT use commas (,) or semicolons (;)" -ForegroundColor Red + Write-Host "" + Write-Host "If you want to analyze multiple domains, please select option [2] instead." -ForegroundColor Yellow + Write-Host "Or if you have email headers with domain information, select option [4]." -ForegroundColor Yellow + } elseif ($Context -eq "multiple") { + Write-Host "" + Write-Host "For Multiple Domain Analysis:" -ForegroundColor Cyan + Write-Host " - Separate domains with commas (,) only" -ForegroundColor White + Write-Host " - Example: example.com,contoso.com,microsoft.com" -ForegroundColor Green + Write-Host " - Do NOT use semicolons (;), backslashes (\), or forward slashes (/)" -ForegroundColor Red + } + + Write-Host "" + Write-Host "Please restart the script and enter valid domain names." -ForegroundColor Yellow + Write-Host "============================================" -ForegroundColor Cyan + return $false + } + + return $true +} + +# Function to analyze Authentication-Results for DMARC Pass check +function Get-AuthenticationResults { + param([string]$HeaderContent) + + $authResults = @{ + SPFResult = "Unknown" + DKIMResult = "Unknown" + DMARCResult = "Unknown" + Action = "" + SMTPMailFrom = "" + HeaderFrom = "" + HeaderD = "" + CompAuth = "" + Reason = "" + AuthenticationResultsRaw = "" + DMARCPass = "No" + Condition1Met = $false + Condition2Met = $false + Details = @() + AntispamMailboxDelivery = "" + AntispamUCF = "" + AntispamJMR = "" + AntispamDest = "" + AntispamOFR = "" + Office365FilteringCorrelationId = "" + ForefrontAntispamReport = "" + ForefrontCIP = "" + ForefrontCTRY = "" + ForefrontLANG = "" + ForefrontSCL = "" + ForefrontSRV = "" + ForefrontIPV = "" + ForefrontSFV = "" + ForefrontPTR = "" + ForefrontCAT = "" + ForefrontDIR = "" + ForefrontSFP = "" + } + + # Parse Authentication-Results header for individual components + # Enhanced regex to capture only Authentication-Results headers that start with spf= and end with reason= + # This excludes ARC-Authentication-Results and other unwanted headers + $authResultsMatches = [regex]::Matches($HeaderContent, '(?\]\);,]$', '' + $authResults.SMTPMailFrom = $smtpDomain + $authResults.Details += "Mail From (P1): $smtpDomain" + } + + # Extract header.from + if ($authLine -match 'header\.from=([^;\s\r\n]+)') { + $headerFromDomain = $matches[1].Trim() -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + $authResults.HeaderFrom = $headerFromDomain + $authResults.Details += "From (P2): $headerFromDomain" + } + + # Extract header.d (for DKIM) + if ($authLine -match 'header\.d=([^;\s\r\n]+)') { + $headerDDomain = $matches[1].Trim() -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + $authResults.HeaderD = $headerDDomain + $authResults.Details += "Header.d: $headerDDomain" + } + + # Extract compauth + if ($authLine -match 'compauth=([^;\s\r\n]+)') { + $compAuthValue = $matches[1].Trim() -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + $authResults.CompAuth = $compAuthValue + $authResults.Details += "CompAuth: $compAuthValue" + } + + # Extract reason + if ($authLine -match 'reason=([^;\s\r\n]+)') { + $reasonValue = $matches[1].Trim() -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + $authResults.Reason = $reasonValue + $authResults.Details += "Reason: $reasonValue" + } + } + + # Check DMARC Pass conditions + $condition1Met = $false + $condition2Met = $false + + # Condition 1: spf=pass AND header.from matches smtp.mailfrom + if ($authResults.SPFResult -eq "pass" -and + -not [string]::IsNullOrEmpty($authResults.HeaderFrom) -and + -not [string]::IsNullOrEmpty($authResults.SMTPMailFrom) -and + $authResults.HeaderFrom.ToLower() -eq $authResults.SMTPMailFrom.ToLower()) { + $condition1Met = $true + $authResults.Condition1Met = $true + $authResults.Details += "DMARC Pass Condition 1 MET: SPF=pass AND header.from ($($authResults.HeaderFrom)) matches smtp.mailfrom ($($authResults.SMTPMailFrom))" + } else { + $authResults.Condition1Met = $false + } + + # Condition 2: dkim=pass AND header.d matches smtp.mailfrom + if ($authResults.DKIMResult -eq "pass" -and + -not [string]::IsNullOrEmpty($authResults.HeaderD) -and + -not [string]::IsNullOrEmpty($authResults.SMTPMailFrom) -and + $authResults.HeaderD.ToLower() -eq $authResults.SMTPMailFrom.ToLower()) { + $condition2Met = $true + $authResults.Condition2Met = $true + $authResults.Details += "DMARC Pass Condition 2 MET: DKIM=pass AND header.d ($($authResults.HeaderD)) matches smtp.mailfrom ($($authResults.SMTPMailFrom))" + } else { + $authResults.Condition2Met = $false + } + + # Set DMARC Pass result + if ($condition1Met -or $condition2Met) { + $authResults.DMARCPass = "Yes" + $authResults.Details += "DMARC Pass: YES (at least one condition met)" + } else { + $authResults.DMARCPass = "No" + $authResults.Details += "DMARC Pass: NO (no conditions met)" + + # Add detailed explanation why conditions weren't met + if ($authResults.SPFResult -ne "pass") { + $authResults.Details += "Condition 1 failed: SPF result is '$($authResults.SPFResult)' (needs 'pass')" + } + if ($authResults.DKIMResult -ne "pass") { + $authResults.Details += "Condition 2 failed: DKIM result is '$($authResults.DKIMResult)' (needs 'pass')" + } + if ([string]::IsNullOrEmpty($authResults.HeaderFrom) -or [string]::IsNullOrEmpty($authResults.SMTPMailFrom)) { + $authResults.Details += "Condition 1 failed: Missing header.from or smtp.mailfrom values" + } elseif ($authResults.HeaderFrom.ToLower() -ne $authResults.SMTPMailFrom.ToLower()) { + $authResults.Details += "Condition 1 failed: header.from ($($authResults.HeaderFrom)) doesn't match smtp.mailfrom ($($authResults.SMTPMailFrom))" + } + if ([string]::IsNullOrEmpty($authResults.HeaderD) -or [string]::IsNullOrEmpty($authResults.SMTPMailFrom)) { + $authResults.Details += "Condition 2 failed: Missing header.d or smtp.mailfrom values" + } elseif ($authResults.HeaderD.ToLower() -ne $authResults.SMTPMailFrom.ToLower()) { + $authResults.Details += "Condition 2 failed: header.d ($($authResults.HeaderD)) doesn't match smtp.mailfrom ($($authResults.SMTPMailFrom))" + } + } + + # Parse X-Microsoft-Antispam-Mailbox-Delivery header + # Enhanced regex to capture complete multi-line X-Microsoft-Antispam-Mailbox-Delivery header + $antiSpamMatches = [regex]::Matches($HeaderContent, 'X-Microsoft-Antispam-Mailbox-Delivery:\s*([^\r\n]*(?:\r?\n\s+[^\r\n]*)*)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline) + + if ($antiSpamMatches.Count -gt 0) { + # Take the first match and get the complete header value + $antiSpamValue = $antiSpamMatches[0].Groups[1].Value + # Clean up the value by removing extra whitespace and line breaks + $cleanedValue = $antiSpamValue -replace '\r?\n\s*', ' ' -replace '\s+', ' ' + $authResults.AntispamMailboxDelivery = $cleanedValue.Trim() + $authResults.Details += "Found X-Microsoft-Antispam-Mailbox-Delivery header" + + # Parse individual parameters from the cleaned value + $cleanedValue = $authResults.AntispamMailboxDelivery + + # Extract UCF (Unified Content Filter) + if ($cleanedValue -match 'ucf:(\d+)') { + $authResults.AntispamUCF = $matches[1] + } + + # Extract JMR (Junk Mail Rule) + if ($cleanedValue -match 'jmr:(\d+)') { + $authResults.AntispamJMR = $matches[1] + } + + # Extract dest (Destination) + if ($cleanedValue -match 'dest:([^;]+)') { + $authResults.AntispamDest = $matches[1] + } + + # Extract OFR (Organizational Filtering Rules) + if ($cleanedValue -match 'OFR:([^;]+)') { + $authResults.AntispamOFR = $matches[1] + } + } + + # Parse X-MS-Office365-Filtering-Correlation-Id header + $office365FilteringMatches = [regex]::Matches($HeaderContent, 'X-MS-Office365-Filtering-Correlation-Id:\s*([^\r\n]+)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + + if ($office365FilteringMatches.Count -gt 0) { + # Take the first match and get the correlation ID value + $correlationIdValue = $office365FilteringMatches[0].Groups[1].Value.Trim() + $authResults.Office365FilteringCorrelationId = $correlationIdValue + $authResults.Details += "Found X-MS-Office365-Filtering-Correlation-Id header" + } + + # Parse X-Forefront-Antispam-Report-Untrusted header + # Enhanced regex to capture complete multi-line X-Forefront-Antispam-Report-Untrusted header + $forefrontMatches = [regex]::Matches($HeaderContent, 'X-Forefront-Antispam-Report-Untrusted:\s*([^\r\n]*(?:\r?\n\s+[^\r\n]*)*)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Multiline) + + if ($forefrontMatches.Count -gt 0) { + # Take the first match and get the complete header value + $forefrontValue = $forefrontMatches[0].Groups[1].Value + # Clean up the value by removing extra whitespace and line breaks + $cleanedForefrontValue = $forefrontValue -replace '\r?\n\s*', ' ' -replace '\s+', ' ' + $authResults.ForefrontAntispamReport = $cleanedForefrontValue.Trim() + $authResults.Details += "Found X-Forefront-Antispam-Report-Untrusted header" + + # Parse individual parameters from the cleaned value + $cleanedForefrontValue = $authResults.ForefrontAntispamReport + + # Extract CIP (Client IP) + if ($cleanedForefrontValue -match 'CIP:([^;]+)') { + $authResults.ForefrontCIP = $matches[1] + } + + # Extract CTRY (Country) + if ($cleanedForefrontValue -match 'CTRY:([^;]*)') { + $authResults.ForefrontCTRY = $matches[1] + } + + # Extract LANG (Language) + if ($cleanedForefrontValue -match 'LANG:([^;]+)') { + $authResults.ForefrontLANG = $matches[1] + } + + # Extract SCL (Spam Confidence Level) + if ($cleanedForefrontValue -match 'SCL:([^;]+)') { + $authResults.ForefrontSCL = $matches[1] + } + + # Extract SRV (Service) + if ($cleanedForefrontValue -match 'SRV:([^;]*)') { + $authResults.ForefrontSRV = $matches[1] + } + + # Extract IPV (IP Version) + if ($cleanedForefrontValue -match 'IPV:([^;]+)') { + $authResults.ForefrontIPV = $matches[1] + } + + # Extract SFV (Sender Filter Verdict) + if ($cleanedForefrontValue -match 'SFV:([^;]+)') { + $authResults.ForefrontSFV = $matches[1] + } + + # Extract PTR (Reverse DNS) + if ($cleanedForefrontValue -match 'PTR:([^;]*)') { + $authResults.ForefrontPTR = $matches[1] + } + + # Extract CAT (Category) + if ($cleanedForefrontValue -match 'CAT:([^;]+)') { + $authResults.ForefrontCAT = $matches[1] + } + + # Extract DIR (Direction) + if ($cleanedForefrontValue -match 'DIR:([^;]+)') { + $authResults.ForefrontDIR = $matches[1] + } + + # Extract SFP (Sender Filter Policy) + if ($cleanedForefrontValue -match 'SFP:([^;]+)') { + $authResults.ForefrontSFP = $matches[1] + } + } + + return $authResults +} + +# Function to parse email headers and extract domains from smtp.mailfrom and header.from +function Get-DomainsFromEmailHeaders { + param([string]$FilePath) + + $domains = @() + $foundEntries = @() + + if (-not (Test-Path -Path $FilePath)) { + Write-Host "Email header file not found: $FilePath" -ForegroundColor Red + return @() + } + + try { + $headerContent = Get-Content -Path $FilePath -ErrorAction Stop -Raw + + Write-Host "Parsing email headers from: $FilePath" -ForegroundColor Cyan + Write-Host "File size: $($headerContent.Length) characters" -ForegroundColor Gray + Write-Host "" + + # Analyze Authentication-Results for DMARC Pass check + Write-Host "=== AUTHENTICATION RESULTS ANALYSIS ===" -ForegroundColor Yellow + $authResults = Get-AuthenticationResults -HeaderContent $headerContent + + # Store authentication results at script scope for later use + $script:AuthenticationResults = $authResults + + Write-Host "SPF Result: $($authResults.SPFResult)" -ForegroundColor $(if ($authResults.SPFResult -eq 'pass') { 'Green' }else { 'Red' }) + Write-Host "DKIM Result: $($authResults.DKIMResult)" -ForegroundColor $(if ($authResults.DKIMResult -eq 'pass') { 'Green' }else { 'Red' }) + Write-Host "DMARC Result: $($authResults.DMARCResult)" -ForegroundColor $(if ($authResults.DMARCResult -eq 'pass') { 'Green' }else { 'Red' }) + Write-Host "Mail From (P1): $($authResults.SMTPMailFrom)" -ForegroundColor Cyan + Write-Host "From (P2): $($authResults.HeaderFrom)" -ForegroundColor Cyan + Write-Host "Header.d: $($authResults.HeaderD)" -ForegroundColor Cyan + Write-Host "" + Write-Host "DMARC PASS CHECK: $($authResults.DMARCPass)" -ForegroundColor $(if ($authResults.DMARCPass -eq 'Yes') { 'Green' }else { 'Red' }) -BackgroundColor $(if ($authResults.DMARCPass -eq 'Yes') { 'DarkGreen' }else { 'DarkRed' }) + Write-Host "" + Write-Host "DMARC Pass Explanation:" -ForegroundColor Cyan + Write-Host " This check determines if the email would pass DMARC authentication based on:" -ForegroundColor White + Write-Host "" + + # Enhanced condition display with status indicators + Write-Host " DMARC Pass Conditions:" -ForegroundColor Yellow + + # Condition 1 with status + $condition1Status = if ($authResults.Condition1Met) { "[PASS] MET" } else { "[FAIL] NOT MET" } + $condition1Color = if ($authResults.Condition1Met) { "Green" } else { "Red" } + $condition1BgColor = if ($authResults.Condition1Met) { "DarkGreen" } else { "DarkRed" } + + Write-Host " [$condition1Status]" -ForegroundColor $condition1Color -BackgroundColor $condition1BgColor -NoNewline + Write-Host " Condition 1: SPF=pass AND header.from matches smtp.mailfrom" -ForegroundColor White + + if ($authResults.Condition1Met) { + Write-Host " [PASS] SPF Result: $($authResults.SPFResult)" -ForegroundColor Green + Write-Host " [PASS] header.from ($($authResults.HeaderFrom)) = smtp.mailfrom ($($authResults.SMTPMailFrom))" -ForegroundColor Green + } else { + Write-Host " [FAIL] SPF Result: $($authResults.SPFResult)" -ForegroundColor Red + if ($authResults.HeaderFrom -and $authResults.SMTPMailFrom) { + if ($authResults.HeaderFrom.ToLower() -eq $authResults.SMTPMailFrom.ToLower()) { + Write-Host " [PASS] header.from ($($authResults.HeaderFrom)) = smtp.mailfrom ($($authResults.SMTPMailFrom))" -ForegroundColor Green + } else { + Write-Host " [FAIL] header.from ($($authResults.HeaderFrom)) != smtp.mailfrom ($($authResults.SMTPMailFrom))" -ForegroundColor Red + } + } else { + Write-Host " [FAIL] Missing domain values" -ForegroundColor Red + } + } + Write-Host "" + + # Condition 2 with status + $condition2Status = if ($authResults.Condition2Met) { "[PASS] MET" } else { "[FAIL] NOT MET" } + $condition2Color = if ($authResults.Condition2Met) { "Green" } else { "Red" } + $condition2BgColor = if ($authResults.Condition2Met) { "DarkGreen" } else { "DarkRed" } + + Write-Host " [$condition2Status]" -ForegroundColor $condition2Color -BackgroundColor $condition2BgColor -NoNewline + Write-Host " Condition 2: DKIM=pass AND header.d matches smtp.mailfrom" -ForegroundColor White + + if ($authResults.Condition2Met) { + Write-Host " [PASS] DKIM Result: $($authResults.DKIMResult)" -ForegroundColor Green + Write-Host " [PASS] header.d ($($authResults.HeaderD)) = smtp.mailfrom ($($authResults.SMTPMailFrom))" -ForegroundColor Green + } else { + Write-Host " [FAIL] DKIM Result: $($authResults.DKIMResult)" -ForegroundColor Red + if ($authResults.HeaderD -and $authResults.SMTPMailFrom) { + if ($authResults.HeaderD.ToLower() -eq $authResults.SMTPMailFrom.ToLower()) { + Write-Host " [PASS] header.d ($($authResults.HeaderD)) = smtp.mailfrom ($($authResults.SMTPMailFrom))" -ForegroundColor Green + } else { + Write-Host " [FAIL] header.d ($($authResults.HeaderD)) != smtp.mailfrom ($($authResults.SMTPMailFrom))" -ForegroundColor Red + } + } else { + Write-Host " [FAIL] Missing domain values" -ForegroundColor Red + } + } + Write-Host "" + + # Final result with enhanced highlighting + $finalResultText = if ($authResults.DMARCPass -eq 'Yes') { + if ($authResults.Condition1Met -and $authResults.Condition2Met) { + "PASS - BOTH conditions met (Excellent!)" + } else { + "PASS - At least one condition met" + } + } else { + "FAIL - No conditions met" + } + + Write-Host " Final Result: $finalResultText" -ForegroundColor $(if ($authResults.DMARCPass -eq 'Yes') { 'Green' }else { 'Red' }) -BackgroundColor $(if ($authResults.DMARCPass -eq 'Yes') { 'DarkGreen' }else { 'DarkRed' }) + Write-Host "" + + # Show detailed analysis + Write-Host "Detailed Analysis:" -ForegroundColor Gray + foreach ($detail in $authResults.Details) { + Write-Host " $detail" -ForegroundColor DarkGray + } + Write-Host "" + + # Show parsed Antispam headers if available + if ($authResults.AntispamMailboxDelivery) { + Write-Host "=== X-Microsoft-Antispam-Mailbox-Delivery PARSED ===" -ForegroundColor Magenta + Write-Host "UCF (Unified Content Filter): $($authResults.AntispamUCF)" -ForegroundColor Cyan + Write-Host "JMR (Junk Mail Rule): $($authResults.AntispamJMR)" -ForegroundColor Cyan + Write-Host "Dest (Destination): $($authResults.AntispamDest)" -ForegroundColor Cyan + Write-Host "OFR (Organizational Filtering Rules): $($authResults.AntispamOFR)" -ForegroundColor Cyan + Write-Host "" + } + + if ($authResults.Office365FilteringCorrelationId) { + Write-Host "=== X-MS-Office365-Filtering-Correlation-Id PARSED ===" -ForegroundColor Magenta + Write-Host "Correlation ID: $($authResults.Office365FilteringCorrelationId)" -ForegroundColor Cyan + Write-Host "" + } + + if ($authResults.ForefrontAntispamReport) { + Write-Host "=== X-Forefront-Antispam-Report-Untrusted PARSED ===" -ForegroundColor Magenta + Write-Host "CIP (Client IP): $($authResults.ForefrontCIP)" -ForegroundColor Yellow + Write-Host "CTRY (Country): $($authResults.ForefrontCTRY)" -ForegroundColor Yellow + Write-Host "LANG (Language): $($authResults.ForefrontLANG)" -ForegroundColor Yellow + Write-Host "SCL (Spam Confidence Level): $($authResults.ForefrontSCL)" -ForegroundColor Yellow + Write-Host "SRV (Service): $($authResults.ForefrontSRV)" -ForegroundColor Yellow + Write-Host "IPV (IP Version): $($authResults.ForefrontIPV)" -ForegroundColor Yellow + Write-Host "SFV (Sender Filter Verdict): $($authResults.ForefrontSFV)" -ForegroundColor Yellow + Write-Host "PTR (Reverse DNS): $($authResults.ForefrontPTR)" -ForegroundColor Yellow + Write-Host "CAT (Category): $($authResults.ForefrontCAT)" -ForegroundColor Yellow + Write-Host "DIR (Direction): $($authResults.ForefrontDIR)" -ForegroundColor Yellow + Write-Host "SFP (Sender Filter Policy): $($authResults.ForefrontSFP)" -ForegroundColor Yellow + Write-Host "" + } + + # Now search for domains as before + Write-Host "=== DOMAIN EXTRACTION ===" -ForegroundColor Yellow + Write-Host "Searching for smtp.mailfrom and header.from entries..." -ForegroundColor Gray + Write-Host "" + + # Look for smtp.mailfrom patterns in the entire content + $smtpMatches = [regex]::Matches($headerContent, 'smtp\.mailfrom=([^;\s\r\n]+)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + + Write-Host "Found $($smtpMatches.Count) smtp.mailfrom matches" -ForegroundColor Gray + + foreach ($match in $smtpMatches) { + $domain = $match.Groups[1].Value.Trim() + Write-Host " Found smtp.mailfrom: '$domain'" -ForegroundColor Cyan + + # Clean up the domain (remove quotes, brackets, etc.) + $domain = $domain -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + Write-Host " Cleaned domain: '$domain'" -ForegroundColor Gray + + # Validate domain format + if ($domain -match '^[a-zA-Z0-9][a-zA-Z0-9\.-]*[a-zA-Z0-9]\.[a-zA-Z]{2,}$') { + $domains += $domain + $foundEntries += "smtp.mailfrom=$domain" + Write-Host " + Valid smtp.mailfrom domain: $domain" -ForegroundColor Green + } else { + Write-Host " - Invalid domain format: '$domain'" -ForegroundColor Yellow + } + } + + # Look for header.from patterns in the entire content + $headerFromMatches = [regex]::Matches($headerContent, 'header\.from=([^;\s\r\n]+)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + + Write-Host "Found $($headerFromMatches.Count) header.from matches" -ForegroundColor Gray + + foreach ($match in $headerFromMatches) { + $domain = $match.Groups[1].Value.Trim() + Write-Host " Found header.from: '$domain'" -ForegroundColor Cyan + + # Clean up the domain (remove quotes, brackets, etc.) + $domain = $domain -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + Write-Host " Cleaned domain: '$domain'" -ForegroundColor Gray + + # Validate domain format + if ($domain -match '^[a-zA-Z0-9][a-zA-Z0-9\.-]*[a-zA-Z0-9]\.[a-zA-Z]{2,}$') { + $domains += $domain + $foundEntries += "header.from=$domain" + Write-Host " + Valid header.from domain: $domain" -ForegroundColor Green + } else { + Write-Host " - Invalid domain format: '$domain'" -ForegroundColor Yellow + } + } + + # Also look for alternative patterns that might be present + # Look for dmarc= patterns to extract header.from domains + $dmarcMatches = [regex]::Matches($headerContent, 'dmarc=[^;]*header\.from=([^;\s\r\n]+)', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + + Write-Host "Found $($dmarcMatches.Count) DMARC header.from matches" -ForegroundColor Gray + + foreach ($match in $dmarcMatches) { + $domain = $match.Groups[1].Value.Trim() + Write-Host " Found DMARC header.from: '$domain'" -ForegroundColor Cyan + + # Clean up the domain + $domain = $domain -replace '^["\''<\[\(]', '' -replace '["\''>\]\);,]$', '' + Write-Host " Cleaned domain: '$domain'" -ForegroundColor Gray + + # Validate domain format + if ($domain -match '^[a-zA-Z0-9][a-zA-Z0-9\.-]*[a-zA-Z0-9]\.[a-zA-Z]{2,}$') { + $domains += $domain + $foundEntries += "header.from=$domain (from DMARC section)" + Write-Host " + Valid DMARC header.from domain: $domain" -ForegroundColor Green + } else { + Write-Host " - Invalid domain format: '$domain'" -ForegroundColor Yellow + } + } + + Write-Host "" + Write-Host "Total domains found before deduplication: $($domains.Count)" -ForegroundColor Gray + + # Remove duplicates while preserving order + $uniqueDomains = @() + $seenDomains = @{} + + foreach ($domain in $domains) { + $domainLower = $domain.ToLower() + if (-not $seenDomains.ContainsKey($domainLower)) { + $uniqueDomains += $domain + $seenDomains[$domainLower] = $true + } + } + + Write-Host "" + if ($uniqueDomains.Count -gt 0) { + Write-Host "SUCCESS: Found $($uniqueDomains.Count) unique domain(s) for analysis:" -ForegroundColor Green + foreach ($domain in $uniqueDomains) { + Write-Host " - $domain" -ForegroundColor White + } + Write-Host "" + Write-Host "Extracted from $($foundEntries.Count) header entries:" -ForegroundColor Gray + foreach ($entry in $foundEntries) { + Write-Host " - $entry" -ForegroundColor DarkGray + } + } else { + Write-Host "No valid domains found in email headers." -ForegroundColor Yellow + Write-Host "" + Write-Host "Troubleshooting:" -ForegroundColor Cyan + Write-Host "The script searches for these patterns in the email headers:" -ForegroundColor White + Write-Host " 1. smtp.mailfrom=domain.com" -ForegroundColor Gray + Write-Host " 2. header.from=domain.com" -ForegroundColor Gray + Write-Host " 3. dmarc=... header.from=domain.com" -ForegroundColor Gray + Write-Host "" + Write-Host "Example of expected email header format:" -ForegroundColor Cyan + Write-Host "Authentication-Results: spf=fail (sender IP is 1.2.3.4)" -ForegroundColor DarkGray + Write-Host " smtp.mailfrom=example.com; dkim=none (message not signed)" -ForegroundColor DarkGray + Write-Host " header.d=none;dmarc=fail action=none header.from=example.com" -ForegroundColor DarkGray + Write-Host "" + Write-Host "If your file has a different format, please check the content." -ForegroundColor White + + # Show first 500 characters of the file for debugging + if ($headerContent.Length -gt 0) { + $preview = if ($headerContent.Length -gt 500) { + $headerContent.Substring(0, 500) + "..." + } else { + $headerContent + } + Write-Host "" + Write-Host "File content preview (first 500 chars):" -ForegroundColor Yellow + Write-Host $preview -ForegroundColor DarkGray + } + } + + Write-Host "" + return $uniqueDomains + } catch { + Write-Host "Error reading email header file: $($_.Exception.Message)" -ForegroundColor Red + return @() + } +} + +# Function to calculate check percentages for donut charts +function Get-ProtocolCheckPercentage { + param( + [PSCustomObject]$result, + [string]$protocol + ) + + switch ($protocol) { + "SPF" { + if (-not $result.SPFFound) { return 0 } + + # Calculate SPF score based on new point system + # Record Present = 8 points, other 8 checks = 4 points each (Total: 40 points) + $maxPoints = 40 + $earnedPoints = 0 + + $spfChecks = Get-ProtocolCheckDetails $result "SPF" + foreach ($check in $spfChecks) { + if ($check.Passed) { + if ($check.Name -eq "Record Present") { + $earnedPoints += 8 # Record present check gets 8 points + } else { + $earnedPoints += 4 # All other SPF checks get 4 points each + } + } + } + + return [math]::Round(($earnedPoints / $maxPoints) * 100, 0) + } + + "DMARC" { + if (-not $result.DMARCFound) { return 0 } + + # Calculate DMARC score based on new point system + # Each of the 5 DMARC checks = 6 points each (Total: 30 points) + $maxPoints = 30 + $earnedPoints = 0 + + $dmarcChecks = Get-ProtocolCheckDetails $result "DMARC" + foreach ($check in $dmarcChecks) { + if ($check.Passed) { + $earnedPoints += 6 # Each DMARC check gets 6 points + } + } + + return [math]::Round(($earnedPoints / $maxPoints) * 100, 0) + } + + "DKIM" { + if (-not $result.DKIMFound) { return 0 } + + # Calculate DKIM score based on new point system + # Each of the 5 DKIM checks = 6 points each (Total: 30 points) + $maxPoints = 30 + $earnedPoints = 0 + + $dkimChecks = Get-ProtocolCheckDetails $result "DKIM" + foreach ($check in $dkimChecks) { + if ($check.Passed) { + $earnedPoints += 6 # Each DKIM check gets 6 points + } + } + + return [math]::Round(($earnedPoints / $maxPoints) * 100, 0) + } + } + + return 0 +} + +# Function to generate enhanced interactive segmented donut chart SVG +function Add-SegmentedDonutChart { + param($checks, $protocol) + + $totalChecks = $checks.Count + $passedChecks = ($checks | Where-Object { $_.Passed }).Count + $percentage = if ($totalChecks -gt 0) { [math]::Round(($passedChecks / $totalChecks) * 100, 0) } else { 0 } + + $circumference = 2 * [math]::PI * 15.915 + $segmentSize = $circumference / $totalChecks + + # Generate unique chart ID for interactivity + $chartId = "chart-$protocol-$(Get-Random)" + + $svg = @" + + + + + + + + + + + + + + + +"@ + + $currentOffset = 0 + for ($i = 0; $i -lt $checks.Count; $i++) { + $check = $checks[$i] + $segmentId = "$chartId-segment-$i" + $segmentColor = if ($check.Passed) { $check.Color } else { "#dee2e6" } + $segmentOpacity = if ($check.Passed) { "1.0" } else { "0.6" } + + $svg += @" + + +"@ + $currentOffset -= $segmentSize + } + + $svg += @" + +
+
$percentage%
+
Compliant
+
+"@ + + return $svg +} + +# Function to get individual check results for segmented charts +function Get-ProtocolCheckDetails { + param($result, $protocol) + + $checks = @() + + switch ($protocol) { + "SPF" { + $checks += @{ + Name = "Record Present" + Passed = $result.SPFFound + Color = if ($result.SPFFound) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "Single Record" + Passed = ($result.SPFFound -and $result.SPFMultipleRecordsCheck) + Color = if ($result.SPFFound -and $result.SPFMultipleRecordsCheck) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "Macro Security" + Passed = ($result.SPFFound -and $result.SPFMacroSecurityCheck) + Color = if ($result.SPFFound -and $result.SPFMacroSecurityCheck) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "TTL Sub-Records" + Passed = ($result.SPFFound -and $result.SPFSubRecordsTTLCheck) + Color = if ($result.SPFFound -and $result.SPFSubRecordsTTLCheck) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "DNS Lookups < 10" + Passed = ($result.SPFFound -and $result.SpfDnsLookups -le 10) + Color = if ($result.SPFFound -and $result.SpfDnsLookups -le 10) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "Record Length < 255" + Passed = ($result.SPFFound -and $result.SPFRecordLength -le 255) + Color = if ($result.SPFFound -and $result.SPFRecordLength -le 255) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "TTL >= 3600" + Passed = ($result.SPFFound -and $result.SpfTTL -ge 3600) + Color = if ($result.SPFFound -and $result.SpfTTL -ge 3600) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "Strict All Mechanism" + Passed = ($result.SPFFound -and ($result.SPFAllMechanism -eq "~all" -or $result.SPFAllMechanism -eq "-all")) + Color = if ($result.SPFFound -and ($result.SPFAllMechanism -eq "~all" -or $result.SPFAllMechanism -eq "-all")) { "#28a745" } else { "#dc3545" } + } + $checks += @{ + Name = "Syntax Valid" + Passed = ($result.SPFFound -and $result.SPFSyntaxValid) + Color = if ($result.SPFFound -and $result.SPFSyntaxValid) { "#28a745" } else { "#dc3545" } + } + } + + "DMARC" { + $checks += @{ + Name = "Record Present" + Passed = $result.DMARCFound + Color = if ($result.DMARCFound) { "#007bff" } else { "#dc3545" } + } + $checks += @{ + Name = "Reporting Configured" + Passed = ($result.DMARCFound -and $result.DMARCRecord -match "rua=") + Color = if ($result.DMARCFound -and $result.DMARCRecord -match "rua=") { "#007bff" } else { "#dc3545" } + } + $checks += @{ + Name = "Strong Policy (reject only)" + Passed = ($result.DMARCFound -and $result.DMARCPolicy -eq "reject") + Color = if ($result.DMARCFound -and $result.DMARCPolicy -eq "reject") { "#007bff" } else { "#dc3545" } + } + $checks += @{ + Name = "Subdomain Policy" + Passed = ($result.DMARCFound -and $result.DMARCSubdomainPolicy -ne "Missing" -and ($result.DMARCSubdomainPolicy -eq $result.DMARCPolicy -or $result.DMARCSubdomainPolicy -eq "quarantine" -or $result.DMARCSubdomainPolicy -eq "reject")) + Color = if ($result.DMARCFound -and $result.DMARCSubdomainPolicy -ne "Missing" -and ($result.DMARCSubdomainPolicy -eq $result.DMARCPolicy -or $result.DMARCSubdomainPolicy -eq "quarantine" -or $result.DMARCSubdomainPolicy -eq "reject")) { "#007bff" } else { "#dc3545" } + } + $checks += @{ + Name = "TTL >= 3600" + Passed = ($result.DMARCFound -and $result.DmarcTTL -ge 3600) + Color = if ($result.DMARCFound -and $result.DmarcTTL -ge 3600) { "#007bff" } else { "#dc3545" } + } + } + + "DKIM" { + $checks += @{ + Name = "Record Present" + Passed = $result.DKIMFound + Color = if ($result.DKIMFound) { "#b007ff" } else { "#dc3545" } + } + $checks += @{ + Name = "Syntax Valid" + Passed = ($result.DKIMFound -and $result.DKIMSyntaxValid) + Color = if ($result.DKIMFound -and $result.DKIMSyntaxValid) { "#b007ff" } else { "#dc3545" } + } + + $activeKeys = 0 + foreach ($status in $result.DKIMAllMechanisms.Values) { + if ($status -eq "ACTIVE") { $activeKeys++ } + } + $checks += @{ + Name = "Keys Active" + Passed = ($result.DKIMFound -and $activeKeys -gt 0) + Color = if ($result.DKIMFound -and $activeKeys -gt 0) { "#b007ff" } else { "#dc3545" } + } + + $hasWeakKeys = $false + foreach ($keyInfo in $result.DKIMKeyLengths.Values) { + if ($keyInfo.IsWeak) { $hasWeakKeys = $true; break } + } + $checks += @{ + Name = "Strong Keys" + Passed = ($result.DKIMFound -and -not $hasWeakKeys) + Color = if ($result.DKIMFound -and -not $hasWeakKeys) { "#b007ff" } else { "#dc3545" } + } + + # Check TTL for all DKIM selectors + $allTTLValid = $true + if ($result.DKIMFound) { + foreach ($selector in $result.DKIMSelectors) { + if ($result.DkimTTL.ContainsKey($selector)) { + if ($result.DkimTTL[$selector] -lt 3600) { + $allTTLValid = $false + break + } + } + } + } else { + $allTTLValid = $false + } + $checks += @{ + Name = "TTL >= 3600" + Passed = ($result.DKIMFound -and $allTTLValid) + Color = if ($result.DKIMFound -and $allTTLValid) { "#b007ff" } else { "#dc3545" } + } + } + } + + return $checks +} + +# Function to get explanation for authentication reason codes +function Get-ReasonCodeExplanation { + param( + [string]$ReasonCode + ) + + if (-not $ReasonCode -or $ReasonCode -eq "") { + return "" + } + + # Handle different reason code patterns + $explanations = @{ + "000" = "The message failed explicit authentication (compauth=fail). For example, the message received a DMARC fail and the DMARC policy action is p=quarantine or p=reject." + "001" = "The message failed implicit authentication (compauth=fail). This result means that the sending domain didn't have email authentication records published, or if they did, they had a weaker failure policy (SPF ~all or ?all, or a DMARC policy of p=none)." + "002" = "The organization has a policy for the sender/domain pair that is explicitly prohibited from sending spoofed email. An admin manually configures this setting." + "010" = "The message failed DMARC, the DMARC policy action is p=reject or p=quarantine, and the sending domain is one of your organization's accepted domains (self-to-self or intra-org spoofing)." + } + + # Check for exact matches first + if ($explanations.ContainsKey($ReasonCode)) { + return $explanations[$ReasonCode] + } + + # Check for pattern matches + if ($ReasonCode -match "^1\d{2}$" -or $ReasonCode -match "^7\d{2}$") { + if ($ReasonCode -eq "130") { + return "The message passed authentication (compauth=pass). The ARC result was used to override a DMARC failure." + } else { + return "The message passed authentication (compauth=pass). The last two digits are internal codes used by Microsoft 365." + } + } elseif ($ReasonCode -match "^2\d{2}$") { + return "The message soft-passed implicit authentication (compauth=softpass). The last two digits are internal codes used by Microsoft 365." + } elseif ($ReasonCode -match "^3\d{2}$") { + return "The message wasn't checked for composite authentication (compauth=none)." + } elseif ($ReasonCode -match "^4\d{2}$" -or $ReasonCode -match "^9\d{2}$") { + return "The message bypassed composite authentication (compauth=none). The last two digits are internal codes used by Microsoft 365." + } elseif ($ReasonCode -match "^6\d{2}$") { + return "The message failed implicit email authentication, and the sending domain is one of your organization's accepted domains (self-to-self or intra-org spoofing)." + } + + # Return empty string if no pattern matches + return "" +} + +. $PSScriptRoot\..\..\Shared\ScriptUpdateFunctions\Test-ScriptVersion.ps1 + +$BuildVersion = "" + +Write-Host ("EmailAuthenticationChecker.ps1 script version $($BuildVersion)") -ForegroundColor Green + +if ($ScriptUpdateOnly) { + switch (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/EmailAuthenticationChecker-VersionsURL" -Confirm:$false) { + ($true) { Write-Host ("Script was successfully updated.") -ForegroundColor Green } + ($false) { Write-Host ("No update of the script performed.") -ForegroundColor Yellow } + default { Write-Host ("Unable to perform ScriptUpdateOnly operation.") -ForegroundColor Red } + } + return +} + +if ((-not($SkipVersionCheck)) -and (Test-ScriptVersion -AutoUpdate -VersionsUrl "https://aka.ms/EmailAuthenticationChecker-VersionsURL" -Confirm:$false)) { + Write-Host ("Script was updated. Please re-run the command.") -ForegroundColor Yellow + return +} + +# Results storage +$allResults = @() + +# Determine input method based on parameters or show interactive menu +if ($Domain) { + # Single domain analysis via parameter + if (-not (Test-DomainFormat -DomainName $Domain -Context "single")) { + exit 1 + } + $domains = @($Domain.Trim()) + # Set menu choice to indicate single domain analysis mode + $menuChoice = '1' +} elseif ($DomainList) { + # Multiple domain analysis via parameter + if (-not (Test-DomainFormat -DomainName $DomainList -Context "multiple")) { + exit 1 + } + $domains = $DomainList -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + # Set menu choice to indicate multiple domain analysis mode + $menuChoice = '2' +} elseif ($FilePath) { + # File-based analysis via parameter + if (-not (Test-Path -Path $FilePath)) { + Write-Host "File not found: $FilePath" -ForegroundColor Red + exit 1 + } + try { + $domains = Get-Content -Path $FilePath | Where-Object { $_.Trim() -ne "" } | ForEach-Object { $_.Trim() } + if ($domains.Count -eq 0) { + Write-Host "No domains found in file: $FilePath" -ForegroundColor Red + exit 1 + } + Write-Host "Loaded $($domains.Count) domains from file." -ForegroundColor Green + } catch { + Write-Host "Error reading file: $($_.Exception.Message)" -ForegroundColor Red + exit 1 + } + # Set menu choice to indicate file-based analysis mode + $menuChoice = '3' +} elseif ($HeaderFilePath) { + # Header-based analysis via parameter + if (-not (Test-Path -Path $HeaderFilePath)) { + Write-Host "Header file not found: $HeaderFilePath" -ForegroundColor Red + exit 1 + } + try { + $domains = Get-DomainsFromEmailHeaders -FilePath $HeaderFilePath + if ($domains -and $domains.Count -gt 0) { + Write-Host "Extracted domains from headers: $($domains -join ', ')" -ForegroundColor Green + } else { + Write-Host "No valid domains found in header file." -ForegroundColor Red + exit 1 + } + } catch { + Write-Host "Error reading header file: $($_.Exception.Message)" -ForegroundColor Red + exit 1 + } + # Set menu choice to indicate header analysis mode + $menuChoice = '4' +} else { + # No parameters provided - show usage and exit + Write-Host "Email Authentication Checker v1.5" -ForegroundColor Cyan + Write-Host "Please provide parameters to run the script:" -ForegroundColor Yellow + Write-Host "" + Write-Host "Examples:" -ForegroundColor White + Write-Host " Single domain: .\EmailAuthChecker.ps1 -Domain 'microsoft.com'" -ForegroundColor Gray + Write-Host " Multiple domains: .\EmailAuthChecker.ps1 -DomainList 'microsoft.com,contoso.com'" -ForegroundColor Gray + Write-Host " From file: .\EmailAuthChecker.ps1 -FilePath 'domains.txt'" -ForegroundColor Gray + Write-Host " From headers: .\EmailAuthChecker.ps1 -HeaderFilePath 'headers.txt'" -ForegroundColor Gray + Write-Host " With custom output: .\EmailAuthChecker.ps1 -Domain 'microsoft.com' -OutputPath 'C:\Reports' -AutoOpen" -ForegroundColor Gray + Write-Host "" + Write-Host "For help: Get-Help .\EmailAuthChecker.ps1 -Full" -ForegroundColor White + exit 1 +} + +# Common DKIM selectors to check - expanded list for better detection +$commonSelectors = @("default", "selector1", "selector2", "Google", "Gmail", "k1", "k2", "dkim", "mail", "email", "s1", "s2", "smtpapi", "AmazonSES", "Mandrill", "Mailgun", "pm", "Zendesk1", "mxvault") + +foreach ($domain in $domains) { + Write-Host "Analyzing domain: $domain" -ForegroundColor Yellow + Write-Host "- * 50" -ForegroundColor DarkGray + + # Get authoritative servers for this domain + $authServers = Get-AuthoritativeDNSServers $domain + if ($authServers.Count -gt 0) { + Write-Host " Authoritative DNS servers found:" -ForegroundColor Gray + foreach ($server in $authServers) { + Write-Host " - $($server.NameHost) ($($server.IPAddress))" -ForegroundColor Gray + } + } else { + Write-Host " No authoritative DNS servers found, using default resolvers" -ForegroundColor Yellow + } + Write-Host "" + + # Initialize result object + $result = [PSCustomObject]@{ + Domain = $domain + SPFFound = $false + SPFRecord = "" + SPFIssues = @() + SpfDnsLookups = 0 + SPFRecordLength = 0 + SpfTTL = 0 + SPFAllMechanism = "" + SPFSyntaxValid = $true + SPFSyntaxIssues = @() + DMARCFound = $false + DMARCRecord = "" + DMARCPolicy = "" + DMARCSubdomainPolicy = "" # sp= tag + DmarcSPFAlignment = "" # aspf= tag + DmarcDKIMAlignment = "" # adkim= tag + DMARCFailureOptions = "" # fo= tag (failure reporting options) + DMARCVersion = "" # v= tag + DMARCPercentage = "" # pct= tag + DmarcTTL = 0 + DMARCIssues = @() + DKIMFound = $false + DKIMSelectors = @() + DKIMRecords = @{} # Dictionary to store selector -> record mapping + DKIMAllMechanisms = @{} # Dictionary to store selector -> all mechanism mapping + DKIMKeyLengths = @{} # Dictionary to store selector -> key length info mapping + DkimTTL = @{} # Dictionary to store selector -> TTL mapping + DkimTTLIssues = @{} # Dictionary to store selector -> TTL issues mapping + DKIMSyntaxValid = $true + DKIMSyntaxIssues = @{} # Dictionary to store selector -> issues mapping + SPFMultipleRecordsCheck = $true # New check for multiple SPF records + SPFMacroSecurityCheck = $true # New check for SPF macro security + SPFSubRecordsTTLCheck = $true # New check for TTL of sub-records (A/MX/TXT records referenced in SPF) + SPFSubRecordsTTLValues = @{} # Dictionary to store domain -> TTL values for A/MX/TXT records referenced in SPF + # MX Record Analysis Results + MXFound = $false + MXRecords = @() + MXMinTTL = 0 + MXMaxTTL = 0 + MXAverageTTL = 0 + MXProviders = @() + MXPrimaryMX = "" + MXBackupMX = @() + # Email Header Analysis Results (for option 4) + EmailHeaderSPFResult = "" + EmailHeaderDKIMResult = "" + EmailHeaderDMARCResult = "" + EmailHeaderAction = "" + EmailHeaderSMTPMailFrom = "" + EmailHeaderHeaderFrom = "" + EmailHeaderHeaderD = "" + EmailHeaderCompAuth = "" + EmailHeaderReason = "" + EmailHeaderAuthenticationResultsRaw = "" + EmailHeaderDMARCPass = "" + EmailHeaderCondition1Met = $false + EmailHeaderCondition2Met = $false + EmailHeaderAntispamMailboxDelivery = "" + EmailHeaderAntispamUCF = "" + EmailHeaderAntispamJMR = "" + EmailHeaderAntispamDest = "" + EmailHeaderAntispamOFR = "" + EmailHeaderOffice365FilteringCorrelationId = "" + EmailHeaderForefrontAntispamReport = "" + EmailHeaderForefrontCIP = "" + EmailHeaderForefrontCTRY = "" + EmailHeaderForefrontLANG = "" + EmailHeaderForefrontSCL = "" + EmailHeaderForefrontSRV = "" + EmailHeaderForefrontIPV = "" + EmailHeaderForefrontSFV = "" + EmailHeaderForefrontPTR = "" + EmailHeaderForefrontCAT = "" + EmailHeaderForefrontDIR = "" + EmailHeaderForefrontSFP = "" + Score = 0 + Status = "" + Recommendations = @() + } + + # CHECK SPF RECORD + Write-Host " [1/4] Checking SPF record..." -ForegroundColor White + + # First, check for multiple SPF records (RFC violation) + $multipleSPFIssues = Test-MultipleSPFRecords $domain + if ($multipleSPFIssues.Count -gt 0) { + $result.SPFMultipleRecordsCheck = $false + foreach ($issue in $multipleSPFIssues) { + $result.SPFIssues += $issue + } + Write-Host " Multiple SPF records detected - RFC violation!" -ForegroundColor Red + foreach ($issue in $multipleSPFIssues) { + if ($issue -like "SPF Record*") { + Write-Host " $issue" -ForegroundColor Yellow + } + } + } else { + $result.SPFMultipleRecordsCheck = $true + Write-Host " Single SPF record compliance: PASSED" -ForegroundColor Green + } + + # Query SPF record from authoritative servers + $authServers = Get-AuthoritativeDNSServers $domain + $spfTxtRecords = Resolve-DnsNameAuthoritative -Name $domain -Type TXT -AuthoritativeServers $authServers + $spfRecord = $spfTxtRecords | Where-Object { $_.Strings -like "v=spf*" } | Select-Object -First 1 + + if ($spfRecord) { + $result.SPFFound = $true + $result.SPFRecord = $spfRecord.Strings -join "" + $result.SpfTTL = $spfRecord.TTL + Write-Host " SPF record found" -ForegroundColor Green + + # Extract and analyze SPF all mechanism + $allMechanism = Get-SPFAllMechanism $result.SPFRecord + $result.SPFAllMechanism = $allMechanism + + # Check SPF all mechanism issues with detailed analysis + if ($allMechanism -eq "+all") { + $result.SPFIssues += "Uses '+all' (allows any server) - CRITICAL SECURITY RISK" + Write-Host " All Mechanism: +all (CRITICAL - allows any server)" -ForegroundColor Red + } elseif ($allMechanism -eq "?all") { + $result.SPFIssues += "Uses '?all' (neutral/weak protection) - provides minimal security" + Write-Host " All Mechanism: ?all (WEAK - neutral protection)" -ForegroundColor Yellow + } elseif ($allMechanism -eq "~all") { + Write-Host " All Mechanism: ~all (GOOD - soft fail recommended)" -ForegroundColor Green + } elseif ($allMechanism -eq "-all") { + Write-Host " All Mechanism: -all (STRICT - hard fail)" -ForegroundColor Green + } elseif ([string]::IsNullOrEmpty($allMechanism)) { + $result.SPFIssues += "Missing 'all' mechanism - SPF policy incomplete" + Write-Host " All Mechanism: MISSING (policy incomplete)" -ForegroundColor Red + } else { + $result.SPFIssues += "Unknown 'all' mechanism format: $allMechanism" + Write-Host " All Mechanism: $allMechanism (UNKNOWN format)" -ForegroundColor Yellow + } + + # Check SPF record length (RFC 7208 - DNS TXT record limit is 255 characters) + $result.SPFRecordLength = $result.SPFRecord.Length + if ($result.SPFRecordLength -gt 255) { + $result.SPFIssues += "Record too long ($($result.SPFRecordLength) characters) - exceeds 255 character limit" + Write-Host " Record Length: $($result.SPFRecordLength) characters (EXCEEDS LIMIT)" -ForegroundColor Red + } elseif ($result.SPFRecordLength -gt 200) { + $result.SPFIssues += "Record approaching length limit ($($result.SPFRecordLength) characters) - consider optimization" + Write-Host " Record Length: $($result.SPFRecordLength) characters (approaching limit)" -ForegroundColor Yellow + } else { + Write-Host " Record Length: $($result.SPFRecordLength) characters" -ForegroundColor Green + } + + # Count DNS lookups in SPF record + $dnsLookupCount = Get-SpfDnsLookupCount $result.SPFRecord + $result.SpfDnsLookups = $dnsLookupCount + + if ($dnsLookupCount -gt 10) { + $result.SPFIssues += "Exceeds DNS lookup limit ($dnsLookupCount/10 lookups) - SPF will fail" + } elseif ($dnsLookupCount -gt 8) { + $result.SPFIssues += "Near DNS lookup limit ($dnsLookupCount/10 lookups) - consider optimization" + } else { + Write-Host " DNS lookups: $dnsLookupCount/10" -ForegroundColor Green + } + + # Check TTL (Time To Live) - recommend minimum 3600 seconds (1 hour) + if ($result.SpfTTL -lt 3600) { + $result.SPFIssues += "Low TTL for domain $domain ($($result.SpfTTL) seconds) - recommend minimum 3600 seconds for stability" + Write-Host " TTL warning: $($result.SpfTTL) seconds (recommend 3600+)" -ForegroundColor Yellow + } else { + Write-Host " TTL: $($result.SpfTTL) seconds" -ForegroundColor Green + } + + # Validate SPF syntax + $syntaxIssues = Test-SPFSyntax $result.SPFRecord + $result.SPFSyntaxIssues = $syntaxIssues + $result.SPFSyntaxValid = ($syntaxIssues.Count -eq 0) + + if ($syntaxIssues.Count -gt 0) { + Write-Host " Syntax issues found: $($syntaxIssues.Count)" -ForegroundColor Yellow + # Add syntax issues to general SPF issues for scoring + foreach ($syntaxIssue in $syntaxIssues) { + $result.SPFIssues += "Syntax: $syntaxIssue" + } + } else { + Write-Host " Syntax validation: PASSED" -ForegroundColor Green + } + + # Validate SPF macro security + $macroSecurityIssues = Test-SPFMacroSecurity $result.SPFRecord + if ($macroSecurityIssues.Count -gt 0) { + $result.SPFMacroSecurityCheck = $false + Write-Host " Macro security issues found: $($macroSecurityIssues.Count)" -ForegroundColor Yellow + foreach ($macroIssue in $macroSecurityIssues) { + $result.SPFIssues += "Macro Security: $macroIssue" + } + } else { + $result.SPFMacroSecurityCheck = $true + Write-Host " Macro security validation: PASSED" -ForegroundColor Green + } + + # Validate TTL for SPF sub-records (A/MX records referenced in SPF) + $subRecordsTTLIssues = Test-SPFSubRecordsTTL $result.SPFRecord $domain + $result.SPFSubRecordsTTLValues = Get-SPFSubRecordsTTLValues $result.SPFRecord $domain + if ($subRecordsTTLIssues.Count -gt 0) { + $result.SPFSubRecordsTTLCheck = $false + Write-Host " TTL sub-records issues found: $($subRecordsTTLIssues.Count)" -ForegroundColor Yellow + foreach ($ttlIssue in $subRecordsTTLIssues) { + $result.SPFIssues += "TTL Sub-Records: $ttlIssue" + } + } else { + $result.SPFSubRecordsTTLCheck = $true + Write-Host " TTL sub-records validation: PASSED" -ForegroundColor Green + } + + if ($result.SPFIssues.Count -gt 0) { + $issuesList = $result.SPFIssues -join '; ' + Write-Host " Warning: $issuesList" -ForegroundColor Yellow + } + } else { + Write-Host " No SPF record found" -ForegroundColor Red + # Set all SPF check flags to false when SPF record is not found + $result.SPFMultipleRecordsCheck = $false + $result.SPFMacroSecurityCheck = $false + $result.SPFSubRecordsTTLCheck = $false + $result.SPFSyntaxValid = $false + # Set specific values for missing SPF record + $result.SPFAllMechanism = "Missing" + $result.SPFIssues += "SPF record not found - implement SPF protection" + } + + # CHECK DMARC RECORD + Write-Host " [2/4] Checking DMARC record..." -ForegroundColor White + $dmarcDomain = "_dmarc.$domain" + # Query DMARC record from authoritative servers + $dmarcAuthServers = Get-AuthoritativeDNSServers $domain + $dmarcTxtRecords = Resolve-DnsNameAuthoritative -Name $dmarcDomain -Type TXT -AuthoritativeServers $dmarcAuthServers + $dmarcRecord = $dmarcTxtRecords | Where-Object { $_.Strings -match "^v=DMARC1" } | Select-Object -First 1 + + if ($dmarcRecord) { + $result.DMARCFound = $true + $result.DMARCRecord = $dmarcRecord.Strings -join "" + $result.DmarcTTL = $dmarcRecord.TTL + Write-Host " DMARC record found" -ForegroundColor Green + + # Extract main policy (p=) + if ($result.DMARCRecord -match "p=(\w+)") { + $result.DMARCPolicy = $matches[1] + Write-Host " Policy: $($result.DMARCPolicy)" -ForegroundColor Cyan + } + + # Extract subdomain policy (sp=) + if ($result.DMARCRecord -match "sp=(\w+)") { + $result.DMARCSubdomainPolicy = $matches[1] + Write-Host " Subdomain Policy: $($result.DMARCSubdomainPolicy)" -ForegroundColor Cyan + } else { + # If sp= is not specified, it defaults to the main policy + $result.DMARCSubdomainPolicy = $result.DMARCPolicy + Write-Host " Subdomain Policy: $($result.DMARCSubdomainPolicy) (inherited from main policy)" -ForegroundColor Gray + } + + # Extract SPF alignment mode (aspf=) + if ($result.DMARCRecord -match "aspf=([rs])") { + $result.DmarcSPFAlignment = $matches[1] + $alignmentText = if ($matches[1] -eq "r") { "relaxed" } else { "strict" } + Write-Host " SPF Alignment: $alignmentText ($($matches[1]))" -ForegroundColor Cyan + } else { + # Default is relaxed if not specified + $result.DmarcSPFAlignment = "r" + Write-Host " SPF Alignment: relaxed (r) - default" -ForegroundColor Gray + } + + # Extract DKIM alignment mode (adkim=) + if ($result.DMARCRecord -match "adkim=([rs])") { + $result.DmarcDKIMAlignment = $matches[1] + $alignmentText = if ($matches[1] -eq "r") { "relaxed" } else { "strict" } + Write-Host " DKIM Alignment: $alignmentText ($($matches[1]))" -ForegroundColor Cyan + } else { + # Default is relaxed if not specified + $result.DmarcDKIMAlignment = "r" + Write-Host " DKIM Alignment: relaxed (r) - default" -ForegroundColor Gray + } + + # Extract failure reporting options (fo=) + if ($result.DMARCRecord -match "fo=([01ds])") { + $result.DMARCFailureOptions = $matches[1] + Write-Host " Failure Options: $($matches[1])" -ForegroundColor Cyan + } else { + # Default is 0 if not specified + $result.DMARCFailureOptions = "0" + Write-Host " Failure Options: 0 (default)" -ForegroundColor Gray + } + + # Extract DMARC version (v=) + if ($result.DMARCRecord -match "v=([^;]+)") { + $result.DMARCVersion = $matches[1].Trim() + Write-Host " Protocol Version: $($result.DMARCVersion)" -ForegroundColor Cyan + } else { + $result.DMARCVersion = "Unknown" + } + + # Extract percentage of messages subjected to filtering (pct=) + if ($result.DMARCRecord -match "pct=(\d+)") { + $result.DMARCPercentage = $matches[1] + Write-Host " Percentage of messages filtered: $($result.DMARCPercentage)%" -ForegroundColor Cyan + } else { + # Default is 100% if not specified + $result.DMARCPercentage = "100" + Write-Host " Percentage of messages filtered: 100% (default)" -ForegroundColor Gray + } + + # Check DMARC issues + if ($result.DMARCPolicy -eq "none") { + $result.DMARCIssues += "Policy is 'none' (monitoring only)" + } + + # Validate subdomain policy + $validPolicies = @("none", "quarantine", "reject") + if ($result.DMARCSubdomainPolicy -notin $validPolicies) { + $result.DMARCIssues += "Invalid subdomain policy: '$($result.DMARCSubdomainPolicy)' (valid: $($validPolicies -join ', '))" + } + + # Check if subdomain policy is weaker than main policy + $policyStrength = @{ "none" = 0; "quarantine" = 1; "reject" = 2 } + if ($policyStrength[$result.DMARCSubdomainPolicy] -lt $policyStrength[$result.DMARCPolicy]) { + $result.DMARCIssues += "Subdomain policy '$($result.DMARCSubdomainPolicy)' is weaker than main policy '$($result.DMARCPolicy)' - consider strengthening" + } + + # Validate alignment modes + $validAlignmentModes = @("r", "s") + if ($result.DmarcSPFAlignment -notin $validAlignmentModes) { + $result.DMARCIssues += "Invalid SPF alignment mode: '$($result.DmarcSPFAlignment)' (valid: r=relaxed, s=strict)" + } + if ($result.DmarcDKIMAlignment -notin $validAlignmentModes) { + $result.DMARCIssues += "Invalid DKIM alignment mode: '$($result.DmarcDKIMAlignment)' (valid: r=relaxed, s=strict)" + } + + if ($result.DMARCRecord -notmatch "rua=") { + $result.DMARCIssues += "No reporting email configured" + } + + # Check TTL (Time To Live) - recommend minimum 3600 seconds (1 hour) + if ($result.DmarcTTL -lt 3600) { + $result.DMARCIssues += "Low TTL for domain $domain ($($result.DmarcTTL) seconds) - recommend minimum 3600 seconds for stability" + Write-Host " TTL warning: $($result.DmarcTTL) seconds (recommend 3600+)" -ForegroundColor Yellow + } else { + Write-Host " TTL: $($result.DmarcTTL) seconds" -ForegroundColor Green + } + + if ($result.DMARCIssues.Count -gt 0) { + $issuesList = $result.DMARCIssues -join '; ' + Write-Host " Warning: $issuesList" -ForegroundColor Yellow + } + } else { + Write-Host " No DMARC record found" -ForegroundColor Red + # Set default values for missing DMARC record + $result.DMARCPolicy = "Missing" + $result.DMARCSubdomainPolicy = "Missing" + $result.DmarcSPFAlignment = "Missing" + $result.DmarcDKIMAlignment = "Missing" + $result.DMARCFailureOptions = "Missing" + $result.DMARCVersion = "Missing" + $result.DMARCPercentage = "Missing" + $result.DmarcTTL = 0 + } + # CHECK DKIM RECORDS + Write-Host " [3/4] Checking DKIM records..." -ForegroundColor White + # DKIM checking with fallback mechanism for better reliability + foreach ($selector in $commonSelectors) { + $dkimDomain = "$selector._domainkey.$domain" + $dkimRecord = $null + + # Debug output + Write-Verbose "Checking DKIM selector: $dkimDomain" + + # Try authoritative servers first, then fallback to regular DNS + try { + # Query DKIM record from authoritative servers for accurate TTL + $dkimAuthServers = Get-AuthoritativeDNSServers $domain + if ($dkimAuthServers.Count -gt 0) { + Write-Verbose "Using $($dkimAuthServers.Count) authoritative servers for DKIM query" + $dkimTxtRecords = Resolve-DnsNameAuthoritative -Name $dkimDomain -Type TXT -AuthoritativeServers $dkimAuthServers + $dkimRecord = $dkimTxtRecords | Where-Object { + # More inclusive pattern - any TXT record containing DKIM-related tags + ($_.Strings -join '') -match "v=DKIM1|k=|p=|t=|s=|h=" + } | Select-Object -First 1 + } + } catch { + Write-Verbose "Authoritative DKIM query failed for $dkimDomain`: $_" + } + + # Fallback to regular DNS query if authoritative failed + if (-not $dkimRecord) { + try { + Write-Verbose "Falling back to regular DNS query for $dkimDomain" + $dkimTxtRecords = Resolve-DnsName -Name $dkimDomain -Type TXT -ErrorAction SilentlyContinue + if ($dkimTxtRecords) { + Write-Verbose "Found $($dkimTxtRecords.Count) TXT records for $dkimDomain" + foreach ($txtRecord in $dkimTxtRecords) { + Write-Verbose "TXT Record: $($txtRecord.Strings -join '')" + } + } + $dkimRecord = $dkimTxtRecords | Where-Object { + # More inclusive pattern - any TXT record containing DKIM-related tags + ($_.Strings -join '') -match "v=DKIM1|k=|p=|t=|s=|h=" + } | Select-Object -First 1 + } catch { + Write-Verbose "Regular DKIM query failed for $dkimDomain`: $_" + } + } + + if ($dkimRecord) { + $result.DKIMFound = $true + $result.DKIMSelectors += $selector + $dkimRecordString = $dkimRecord.Strings -join "" + $result.DKIMRecords[$selector] = $dkimRecordString + $result.DkimTTL[$selector] = $dkimRecord.TTL + + # Display individual selector details + Write-Host " DKIM selector '$selector' found" -ForegroundColor Green + if ($selector -eq "selector1" -or $selector -eq "selector2") { + Write-Host " $selector record: $dkimRecordString" -ForegroundColor Cyan + } + + # Check TTL (Time To Live) - recommend minimum 3600 seconds (1 hour) + $ttlIssues = @() + if ($dkimRecord.TTL -lt 3600) { + $ttlIssues += "Low TTL ($($dkimRecord.TTL) seconds) - recommend minimum 3600 seconds for stability" + Write-Host " $selector TTL warning: $($dkimRecord.TTL) seconds (recommend 3600+)" -ForegroundColor Yellow + } else { + Write-Host " $selector TTL: $($dkimRecord.TTL) seconds" -ForegroundColor Green + } + $result.DkimTTLIssues[$selector] = $ttlIssues + } + } + + if ($result.DKIMFound) { + $selectorsList = $result.DKIMSelectors -join ', ' + Write-Host " DKIM records found: $selectorsList" -ForegroundColor Green + + # Validate DKIM syntax and status for each selector + $totalSyntaxIssues = 0 + foreach ($selector in $result.DKIMSelectors) { + if ($result.DKIMRecords.ContainsKey($selector)) { + $dkimRecord = $result.DKIMRecords[$selector] + + # Syntax validation + $syntaxIssues = Test-DKIMSyntax $dkimRecord $selector + $result.DKIMSyntaxIssues[$selector] = $syntaxIssues + $totalSyntaxIssues += $syntaxIssues.Count + + # Key length analysis + $keyLengthInfo = Get-DKIMKeyLength $dkimRecord + $result.DKIMKeyLengths[$selector] = $keyLengthInfo + + # All mechanism status check + $allMechanism = Get-DKIMKeyStatus $dkimRecord + $result.DKIMAllMechanisms[$selector] = $allMechanism + + if ($syntaxIssues.Count -gt 0) { + Write-Host " $selector syntax issues: $($syntaxIssues.Count)" -ForegroundColor Yellow + } else { + Write-Host " $selector syntax validation: PASSED" -ForegroundColor Green + } + + # Display key length information + if ($keyLengthInfo.Error) { + Write-Host " $selector key length: ERROR - $($keyLengthInfo.Error)" -ForegroundColor Red + } else { + $keyLengthColor = if ($keyLengthInfo.IsWeak) { "Red" } + elseif ($keyLengthInfo.KeyLength -eq 1024) { "Yellow" } + else { "Green" } + + $statusText = if ($keyLengthInfo.IsWeak) { " (WEAK - recommend 2048+ bits)" } + elseif ($keyLengthInfo.KeyLength -eq 1024) { " (WARNING - consider upgrading to 2048+ bits)" } + else { "" } + + Write-Host " $selector key length: $($keyLengthInfo.KeyLength) bits ($($keyLengthInfo.KeyType))$statusText" -ForegroundColor $keyLengthColor + + # Add weakness to syntax issues if key is weak + if ($keyLengthInfo.IsWeak -and $keyLengthInfo.KeyLength -gt 0) { + $syntaxIssues += "Weak key length ($($keyLengthInfo.KeyLength) bits) - recommend 2048+ bits for better security" + $result.DKIMSyntaxIssues[$selector] = $syntaxIssues + $totalSyntaxIssues++ + } + + # Add recommendation for 1024-bit keys (warning, not weakness) + if ($keyLengthInfo.KeyLength -eq 1024) { + $recommendations += "Consider upgrading DKIM key to 2048+ bits for enhanced security (currently using 1024-bit key for selector '$selector') - DKIM Best Practices: https://dkim.org/info/dkim-faq.html" + } + } + + # Display all mechanism status + $statusColor = switch ($allMechanism) { + "ACTIVE" { "Green" } + "TESTING" { "Yellow" } + "REVOKED" { "Red" } + "UNKNOWN" { "Yellow" } + default { "White" } + } + Write-Host " $selector status: $allMechanism" -ForegroundColor $statusColor + + # Display TTL validation results + if ($result.DkimTTLIssues.ContainsKey($selector) -and $result.DkimTTLIssues[$selector].Count -gt 0) { + $ttlIssuesList = $result.DkimTTLIssues[$selector] -join '; ' + Write-Host " $selector TTL issues: $ttlIssuesList" -ForegroundColor Yellow + } else { + Write-Host " $selector TTL validation: PASSED" -ForegroundColor Green + } + } + } + + $result.DKIMSyntaxValid = ($totalSyntaxIssues -eq 0) + + if ($totalSyntaxIssues -gt 0) { + Write-Host " Total DKIM syntax issues found: $totalSyntaxIssues" -ForegroundColor Yellow + } else { + Write-Host " All DKIM syntax validation: PASSED" -ForegroundColor Green + } + + # Display overall TTL validation summary + $totalTTLIssues = 0 + foreach ($selector in $result.DKIMSelectors) { + if ($result.DkimTTLIssues.ContainsKey($selector)) { + $totalTTLIssues += $result.DkimTTLIssues[$selector].Count + } + } + + if ($totalTTLIssues -gt 0) { + Write-Host " Total DKIM TTL issues found: $totalTTLIssues" -ForegroundColor Yellow + } else { + Write-Host " All DKIM TTL validation: PASSED" -ForegroundColor Green + } + + # Enhanced DKIM Analysis + Write-Host " Running enhanced DKIM analysis..." -ForegroundColor Cyan + + # Service Provider Detection + $providerInfo = Get-DKIMServiceProvider $result.DKIMRecords $domain + $result | Add-Member -MemberType NoteProperty -Name "DKIMProviders" -Value $providerInfo + + if ($providerInfo.DetectedProviders.Count -gt 0) { + Write-Host " Service Provider: $($providerInfo.DetectedProviders -join ', ')" -ForegroundColor Cyan + } else { + Write-Host " Service Provider: NOT IDENTIFIED (custom/self-hosted)" -ForegroundColor White + } + + # Display selector1 and selector2 details if found + if ($result.DKIMRecords.ContainsKey("selector1")) { + Write-Host " Selector1 Details: $($result.DKIMRecords['selector1'])" -ForegroundColor White + } + if ($result.DKIMRecords.ContainsKey("selector2")) { + Write-Host " Selector2 Details: $($result.DKIMRecords['selector2'])" -ForegroundColor White + } + } else { + Write-Host " No DKIM records found" -ForegroundColor Red + # Initialize empty TTL issues for missing DKIM records + $result.DkimTTLIssues = @{} + } + + # CALCULATE SCORE AND STATUS + $score = 0 + $recommendations = @() + + # SPF scoring (40 points total) - Granular check-based scoring + # Record Present = 8 points, other 8 checks = 4 points each (8 + 8×4 = 40) + $spfChecks = Get-ProtocolCheckDetails $result "SPF" + foreach ($check in $spfChecks) { + if ($check.Passed) { + if ($check.Name -eq "Record Present") { + $score += 8 # Record present check gets 8 points + } else { + $score += 4 # All other SPF checks get 4 points each + } + } + } + + # Add recommendations for failed SPF checks + if (-not $result.SPFFound) { + $URLs = Get-ProviderSpecificURLs -Providers $result.MXProviders + $recommendations += "Implement SPF record: $($URLs.SPFSetup)" + } else { + foreach ($issue in $result.SPFIssues) { + $recommendation = Get-Recommendation -Issue $issue -Protocol "SPF" -Providers $result.MXProviders + if (-not [string]::IsNullOrWhiteSpace($recommendation)) { + $recommendations += $recommendation + } + } + } + + # DMARC scoring (30 points total) - Granular check-based scoring + # Each of the 5 DMARC checks = 6 points each (5×6 = 30) + $dmarcChecks = Get-ProtocolCheckDetails $result "DMARC" + foreach ($check in $dmarcChecks) { + if ($check.Passed) { + $score += 6 # Each DMARC check gets 6 points + } + } + + # Add recommendations for failed DMARC checks + if (-not $result.DMARCFound) { + $URLs = Get-ProviderSpecificURLs -Providers $result.MXProviders + $recommendations += "Implement DMARC record with 'reject' policy: $($URLs.DMARCSetup)" + } else { + # Add specific recommendations based on policy weakness + $URLs = Get-ProviderSpecificURLs -Providers $result.MXProviders + if ($result.DMARCPolicy -ne "reject") { + if ($result.DMARCPolicy -eq "quarantine") { + $recommendations += "Upgrade DMARC policy from 'quarantine' to 'reject' for maximum security $($URLs.DMARCSetup)" + } else { + $recommendations += "Upgrade DMARC policy from 'none' to 'reject' $($URLs.DMARCSetup)" + } + } + foreach ($issue in $result.DMARCIssues) { + $recommendation = Get-Recommendation -Issue $issue -Protocol "DMARC" -Providers $result.MXProviders + if (-not [string]::IsNullOrWhiteSpace($recommendation)) { + $recommendations += $recommendation + } + } + } + + # CHECK MX RECORDS + Write-Host " [4/4] Checking MX records..." -ForegroundColor White + $mxAnalysis = Get-MXRecordAnalysis $domain + + # Populate MX results + $result.MXFound = $mxAnalysis.MXFound + $result.MXRecords = $mxAnalysis.MXRecords + $result.MXMinTTL = $mxAnalysis.MinTTL + $result.MXMaxTTL = $mxAnalysis.MaxTTL + $result.MXAverageTTL = $mxAnalysis.AverageTTL + $result.MXProviders = $mxAnalysis.MXProviders + $result.MXPrimaryMX = $mxAnalysis.PrimaryMX + $result.MXBackupMX = $mxAnalysis.BackupMX + + # DKIM scoring (30 points total) - Granular check-based scoring + # Each of the 5 DKIM checks = 6 points each (5×6 = 30) + $dkimChecks = Get-ProtocolCheckDetails $result "DKIM" + foreach ($check in $dkimChecks) { + if ($check.Passed) { + $score += 6 # Each DKIM check gets 6 points + } + } + + # Add recommendations for failed DKIM checks + if (-not $result.DKIMFound) { + $URLs = Get-ProviderSpecificURLs -Providers $result.MXProviders + $recommendations += "Implement DKIM record: $($URLs.DKIMSetup)" + } else { + if (-not $result.DKIMSyntaxValid) { + $URLs = Get-ProviderSpecificURLs -Providers $result.MXProviders + $recommendations += "Fix DKIM syntax errors: $($URLs.DKIMSetup)" + } + # Check for TTL issues + $totalTTLIssues = 0 + foreach ($selector in $result.DKIMSelectors) { + if ($result.DkimTTLIssues.ContainsKey($selector)) { + $totalTTLIssues += $result.DkimTTLIssues[$selector].Count + } + } + if ($totalTTLIssues -gt 0) { + # Only show DKIM TTL recommendations for Microsoft/Office 365 providers + if ($result.MXProviders -contains "Microsoft/Office 365") { + $recommendations += "Fix DKIM TTL issues - consider increasing TTL to 3600+ seconds for better stability and to avoid any DNS timeout issues" + } + } + } + + # Determine status with enhanced strictness for DMARC policy + if ($score -ge 95 -and $result.DMARCPolicy -eq "reject") { + $status = "Excellent" + $statusColor = "Green" + } elseif ($score -ge 85) { + $status = "Good" + $statusColor = "Cyan" + } elseif ($score -ge 65) { + $status = "Fair" + $statusColor = "Yellow" + } elseif ($score -ge 40) { + $status = "Poor" + $statusColor = "Red" + } else { + $status = "Critical" + $statusColor = "DarkRed" + } + + $result.Score = $score + $result.Status = $status + $result.Recommendations = $recommendations + + # Add email header authentication results if available (for option 4) + if ($script:AuthenticationResults) { + $result.EmailHeaderSPFResult = $script:AuthenticationResults.SPFResult + $result.EmailHeaderDKIMResult = $script:AuthenticationResults.DKIMResult + $result.EmailHeaderDMARCResult = $script:AuthenticationResults.DMARCResult + $result.EmailHeaderAction = $script:AuthenticationResults.Action + $result.EmailHeaderSMTPMailFrom = $script:AuthenticationResults.SMTPMailFrom + $result.EmailHeaderHeaderFrom = $script:AuthenticationResults.HeaderFrom + $result.EmailHeaderHeaderD = $script:AuthenticationResults.HeaderD + $result.EmailHeaderCompAuth = $script:AuthenticationResults.CompAuth + $result.EmailHeaderReason = $script:AuthenticationResults.Reason + $result.EmailHeaderAuthenticationResultsRaw = $script:AuthenticationResults.AuthenticationResultsRaw + $result.EmailHeaderDMARCPass = $script:AuthenticationResults.DMARCPass + $result.EmailHeaderCondition1Met = $script:AuthenticationResults.Condition1Met + $result.EmailHeaderCondition2Met = $script:AuthenticationResults.Condition2Met + $result.EmailHeaderAntispamMailboxDelivery = $script:AuthenticationResults.AntispamMailboxDelivery + $result.EmailHeaderAntispamUCF = $script:AuthenticationResults.AntispamUCF + $result.EmailHeaderAntispamJMR = $script:AuthenticationResults.AntispamJMR + $result.EmailHeaderAntispamDest = $script:AuthenticationResults.AntispamDest + $result.EmailHeaderAntispamOFR = $script:AuthenticationResults.AntispamOFR + $result.EmailHeaderOffice365FilteringCorrelationId = $script:AuthenticationResults.Office365FilteringCorrelationId + $result.EmailHeaderForefrontAntispamReport = $script:AuthenticationResults.ForefrontAntispamReport + $result.EmailHeaderForefrontCIP = $script:AuthenticationResults.ForefrontCIP + $result.EmailHeaderForefrontCTRY = $script:AuthenticationResults.ForefrontCTRY + $result.EmailHeaderForefrontLANG = $script:AuthenticationResults.ForefrontLANG + $result.EmailHeaderForefrontSCL = $script:AuthenticationResults.ForefrontSCL + $result.EmailHeaderForefrontSRV = $script:AuthenticationResults.ForefrontSRV + $result.EmailHeaderForefrontIPV = $script:AuthenticationResults.ForefrontIPV + $result.EmailHeaderForefrontSFV = $script:AuthenticationResults.ForefrontSFV + $result.EmailHeaderForefrontPTR = $script:AuthenticationResults.ForefrontPTR + $result.EmailHeaderForefrontCAT = $script:AuthenticationResults.ForefrontCAT + $result.EmailHeaderForefrontDIR = $script:AuthenticationResults.ForefrontDIR + $result.EmailHeaderForefrontSFP = $script:AuthenticationResults.ForefrontSFP + } + + # Display summary + Write-Host "" + Write-Host " SUMMARY FOR $domain" -ForegroundColor Cyan + Write-Host " Score: $score/100 ($status)" -ForegroundColor $statusColor + Write-Host " SPF: $(if($result.SPFFound){'FOUND'}else{'MISSING'})" -NoNewline + Write-Host " | DMARC: $(if($result.DMARCFound){'FOUND'}else{'MISSING'})" -NoNewline + Write-Host " | DKIM: $(if($result.DKIMFound){'FOUND'}else{'MISSING'})" + + # Display email header results if available (only for option 4) + if ($menuChoice -eq '4' -and $script:AuthenticationResults) { + Write-Host "" + Write-Host " EMAIL HEADER ANALYSIS:" -ForegroundColor Yellow + Write-Host " SPF Result: $($result.EmailHeaderSPFResult)" -ForegroundColor $(if ($result.EmailHeaderSPFResult -eq 'pass') { 'Green' }else { 'Red' }) + Write-Host " DKIM Result: $($result.EmailHeaderDKIMResult)" -ForegroundColor $(if ($result.EmailHeaderDKIMResult -eq 'pass') { 'Green' }else { 'Red' }) + Write-Host " DMARC Result: $($result.EmailHeaderDMARCResult)" -ForegroundColor $(if ($result.EmailHeaderDMARCResult -eq 'pass') { 'Green' }else { 'Red' }) + Write-Host " DMARC Pass: $($result.EmailHeaderDMARCPass)" -ForegroundColor $(if ($result.EmailHeaderDMARCPass -eq 'Yes') { 'Green' }else { 'Red' }) -BackgroundColor $(if ($result.EmailHeaderDMARCPass -eq 'Yes') { 'DarkGreen' }else { 'DarkRed' }) + } + + if ($recommendations.Count -gt 0) { + Write-Host " Recommendations:" -ForegroundColor Yellow + foreach ($rec in $recommendations) { + Write-Host " - $rec" -ForegroundColor Yellow + } + } + Write-Host "" + $allResults += $result +} + +# === Use provided output path or default to current directory === +if ($OutputPath -and $OutputPath -ne ".") { + $path = $OutputPath +} else { + $path = "." # Default to current directory + Write-Host "Using current directory for output. Use -OutputPath parameter to specify a different location." -ForegroundColor Yellow +} + +# === Create directory if it doesn't exist === +if (-not (Test-Path -Path $path)) { + New-Item -ItemType Directory -Path $path -Force | Out-Null +} + +Write-Host "" +Write-Host "Generating HTML report..." -ForegroundColor Green + +# Generate timestamps and statistics +$reportDate = Get-Date -Format "MMMM d, yyyy" +$fileTimestamp = Get-Date -Format "yyyyMMdd-HHmmss" + +# Calculate overall statistics +$totalDomains = $allResults.Count +$avgScore = if ($totalDomains -gt 0) { [math]::Round(($allResults | Measure-Object -Property Score -Average).Average, 1) } else { 0 } + +# Add check percentages to results +foreach ($result in $allResults) { + $spfPercentage = Get-ProtocolCheckPercentage $result "SPF" + $dmarcPercentage = Get-ProtocolCheckPercentage $result "DMARC" + $dkimPercentage = Get-ProtocolCheckPercentage $result "DKIM" + + $result | Add-Member -MemberType NoteProperty -Name "SPFCheckPercentage" -Value $spfPercentage + $result | Add-Member -MemberType NoteProperty -Name "DMARCCheckPercentage" -Value $dmarcPercentage + $result | Add-Member -MemberType NoteProperty -Name "DKIMCheckPercentage" -Value $dkimPercentage +} + +# Start building HTML content +$html = @" + + + + Email Authentication Report + + + + +
+
+

Email Authentication Report

+

Analyzing SPF, DKIM, and DMARC records

+

Generated on $reportDate at $(Get-Date -Format "HH:mm:ss")

+
+ +
+

Analysis Summary

+
+
+

Total Domains

+
$totalDomains
+
Analyzed
+
+
+

Average Score

+
$avgScore
+
Out of 100
+
+ +
+
+ +
+"@ + +# Add domain sections +$domainIndex = 0 +foreach ($result in $allResults) { + $domainIndex++ + $domainId = ($result.Domain -replace '[^a-zA-Z0-9]', '') + $domainIndex # Create safe ID from domain name + index + + $statusClass = switch ($result.Status) { + "Excellent" { "status-excellent" } + "Good" { "status-good" } + "Fair" { "status-fair" } + "Poor" { "status-poor" } + "Critical" { "status-critical" } + } + + $progressWidth = $result.Score + $progressText = "$($result.Score)%" + + # Set progress bar color based on score with enhanced thresholds + $progressColor = if ($result.Score -ge 95) { "linear-gradient(90deg, #28a745 0%, #20c997 100%)" } # Excellent (95+) + elseif ($result.Score -ge 85) { "linear-gradient(90deg, #17a2b8 0%, #138496 100%)" } # Good (85-94) + elseif ($result.Score -ge 65) { "linear-gradient(90deg, #ffc107 0%, #e0a800 100%)" } # Fair (65-84) + elseif ($result.Score -ge 40) { "linear-gradient(90deg, #fd7e14 0%, #e55353 100%)" } # Poor (40-64) + else { "linear-gradient(90deg, #dc3545 0%, #b02a37 100%)" } # Critical (<40) + + $html += @" +
+
+

🌐$($result.Domain)

+
+ Score: $($result.Score)/100 + $($result.Status) +
+
+ +
+
$progressText
+
+ +
+

📊Protocol Health Overview

+
+
+ Total Checks: + 19 +
+
+ Passed: + 0 +
+
+ Failed: + 0 +
+
+ Overall: + $($result.Score)% +
+
+ +
+
+
+
SPF
+
+
SPF Protection
+
Sender Policy Framework
+
+
+
+$(Add-SegmentedDonutChart (Get-ProtocolCheckDetails $result "SPF") "SPF") +
+
+ $(if($result.SPFFound){"$($result.SPFCheckPercentage)% Compliant"}else{"Not Configured"}) +
+
+$((Get-ProtocolCheckDetails $result "SPF") | ForEach-Object { + $statusIcon = if($_.Passed) { "" } else { "×" } + $statusClass = if($_.Passed) { "legend-passed" } else { "legend-failed" } + "
$statusIcon
$($_.Name)
" +} | Out-String) +
+
+ View Details +
+ +
+ +
+
+
DMARC
+
+
DMARC Policy
+
Domain-based Message Authentication
+
+
+
+$(Add-SegmentedDonutChart (Get-ProtocolCheckDetails $result "DMARC") "DMARC") +
+
+ $(if($result.DMARCFound){"$($result.DMARCCheckPercentage)% Compliant"}else{"Not Configured"}) +
+
+$((Get-ProtocolCheckDetails $result "DMARC") | ForEach-Object { + $statusIcon = if($_.Passed) { "" } else { "×" } + $statusClass = if($_.Passed) { "legend-passed" } else { "legend-failed" } + "
$statusIcon
$($_.Name)
" +} | Out-String) +
+
+ View Details +
+ +
+ +
+
+
DKIM
+
+
DKIM Signatures
+
DomainKeys Identified Mail
+
+
+
+$(Add-SegmentedDonutChart (Get-ProtocolCheckDetails $result "DKIM") "DKIM") +
+
+ $(if($result.DKIMFound){"$($result.DKIMCheckPercentage)% Compliant"}else{"Not Configured"}) +
+
+$((Get-ProtocolCheckDetails $result "DKIM") | ForEach-Object { + $statusIcon = if($_.Passed) { "" } else { "×" } + $statusClass = if($_.Passed) { "legend-passed" } else { "legend-failed" } + "
$statusIcon
$($_.Name)
" +} | Out-String) +
+
+ View Details +
+ +
+
+ + +
+
+ + +
+
+
+
+
+
+ + +
+

+ 📧MX Record Analysis +

+ +$(if($result.MXFound) { +@" +
+
+
$($result.MXRecords.Count)
+
MX Records
+
+
+
$($result.MXAverageTTL)s
+
TTL
+
+$(if($result.MXProviders.Count -gt 0) { +"
+
$($result.MXProviders -join ', ')
+
Email Provider
+
" +}) +
+ +
+ + + + + + + + + + +$( + $rowCount = 0 + foreach($mxRecord in $result.MXRecords) { + $rowCount++ + $rowStyle = if($rowCount % 2 -eq 0) { "background-color: #f8f9fa;" } else { "background-color: white;" } + $ttlColor = if($mxRecord.TTL -lt 3600) { "color: #dc3545; font-weight: bold;" } else { "color: #28a745;" } + $isPrimary = ($mxRecord.Server -eq $result.MXPrimaryMX) + $statusText = if($isPrimary) { "Primary" } else { "Backup" } + $statusColor = if($isPrimary) { "#28a745" } else { "#6c757d" } + +" + + + + + " + } +) + +
PriorityMail ServerTTL (seconds)Status
$($mxRecord.Priority)$($mxRecord.Server)$($mxRecord.TTL) + $statusText +
+
+"@ +} else { +@" +
+
+
No MX Records Found
+

This domain does not have MX records configured, which means it cannot receive email.

+
+"@ +}) +
+ +
+

Security Level Comparison

+
+
+
SPF
+
+
+
+
$($result.SPFCheckPercentage)%
+
+
+
DMARC
+
+
+
+
$($result.DMARCCheckPercentage)%
+
+
+
DKIM
+
+
+
+
$($result.DKIMCheckPercentage)%
+
+
+
+
+ + + $(if ($result.Recommendations.Count -gt 0) { + # Determine section title based on provider + $sectionTitle = if ($result.MXProviders -contains "Microsoft/Office 365") { + "Action Items & Microsoft Recommendations" + } else { + "Action Items & Recommendations From Industry Standard Resources" + } +@" +
+

🔧 $sectionTitle for $($result.Domain)

+
    +$( + foreach ($recommendation in $result.Recommendations) { + # Format recommendations with proper HTML links (both HTTP and HTTPS) + $formattedRec = $recommendation -replace "(https?://[^\s]+)", '$1' + "
  • $formattedRec
  • " + } +) +
+
+"@ + }) + + + $( + $domainURLs = Get-ProviderSpecificURLs -Providers $result.MXProviders + # Determine documentation section title based on provider + $docTitle = if ($result.MXProviders -contains "Microsoft/Office 365") { + "Microsoft Official Documentation" + } else { + "Industry Standard Documentation" + } +@" +
+

📚 $docTitle for $($result.Domain)

+

Provider-specific documentation based on detected email provider: $($result.MXProviders -join ', ')

+ +
+"@ + ) + +$(if($menuChoice -eq '4' -and ($result.EmailHeaderSPFResult -ne "" -or $result.EmailHeaderDKIMResult -ne "" -or $result.EmailHeaderDMARCResult -ne "")) { +@" + + +"@ +}) + +
+"@ +} + +# Add consolidated footer sections after all domains +$html += "
" # Close content div + +# Add footer section with "Understanding Your Results" (appears only once) +$html += ' " +$html += "
" +$html += "" +$html += "" + +# Save HTML report to selected location (moved outside the foreach loop) +$reportFileName = "Email-Auth-Report-$fileTimestamp.html" +$reportPath = Join-Path -Path $path -ChildPath $reportFileName + +# Save the HTML file +$html | Out-File -FilePath $reportPath -Encoding UTF8 -Force + +Write-Host "" +Write-Host "HTML report successfully generated!" -ForegroundColor Green +Write-Host "Report saved to: $reportPath" -ForegroundColor Cyan +Write-Host "" + +# Display final summary +Write-Host "============================================" -ForegroundColor Cyan +Write-Host " FINAL SUMMARY" -ForegroundColor Cyan +Write-Host "============================================" -ForegroundColor Cyan +Write-Host "Total domains analyzed: $totalDomains" -ForegroundColor White +Write-Host "Average security score: $avgScore/100" -ForegroundColor White +Write-Host "" + +# Open the report based on parameter +if ($AutoOpen) { + Start-Process $reportPath + Write-Host "Opening report in your default browser..." -ForegroundColor Green + Write-Host "" + Write-Host "============================================" -ForegroundColor Cyan + return # Exit the script only, keep PowerShell window open +} else { + Write-Host "Report generated successfully. Use -AutoOpen parameter to automatically open the report." -ForegroundColor Green + Write-Host "" + Write-Host "============================================" -ForegroundColor Cyan +} diff --git a/docs/M365/MDO/EmailAuthChecker.md b/docs/M365/MDO/EmailAuthChecker.md new file mode 100644 index 0000000000..3612f29197 --- /dev/null +++ b/docs/M365/MDO/EmailAuthChecker.md @@ -0,0 +1,299 @@ + + +# Email Authentication Checker (CSS Exchange) + +## Overview + +The Email Authentication Checker is a PowerShell tool designed for analyzing email authentication configurations for domains. It provides detailed validation of SPF, DKIM, and DMARC records with enhanced security analysis and professional HTML reporting. + +## Features + +### Core Capabilities +- **19 Comprehensive Security Checks** across SPF, DKIM, and DMARC protocols +- **Professional HTML Reports** with interactive visualizations and charts +- **Provider-Aware Documentation** with direct links to Microsoft or industry documentation +- **Authoritative DNS Queries** for accurate TTL validation and record retrieval +- **Email Header Analysis** for real-world authentication result evaluation + +### Analysis Modes +1. **Multiple Domain Analysis** - Analyze one or multiple domains (comma-separated) +2. **File-Based Analysis** - Load domains from a text file (one per line) +3. **Email Header Analysis** - Extract and analyze domains from email headers + +### Security Checks + +#### SPF (9 Checks) +- Record presence validation +- Syntax validation and compliance +- Single record compliance (RFC 7208) +- DNS lookup count validation (max 10) +- Record length validation (max 255 chars) +- TTL analysis for optimization +- SPF enforcement rule analysis (`all` mechanism) +- Macro security assessment +- Sub-record TTL validation (A/MX/TXT records) + +#### DMARC (5 Checks) +- Record presence validation +- Policy assessment (none/quarantine/reject) +- Reporting configuration validation (rua/ruf) +- Alignment modes validation (aspf/adkim) +- TTL validation for performance + +#### DKIM (5 Checks) +- Selector discovery and validation +- Syntax validation and compliance +- Key status analysis (active/revoked/testing) +- Key strength assessment (bit length) +- TTL validation for reliability + +## Requirements + +### Prerequisites +- **PowerShell 5.1** or later +- **Windows** operating system +- **Internet connectivity** for DNS queries +- **Administrator privileges** (recommended for optimal DNS resolution) + +### Dependencies +- Built-in PowerShell modules (no external dependencies required) +- Uses native Windows DNS resolution capabilities + +## Usage + +### Basic Syntax +```powershell +.\EmailAuthChecker.ps1 [parameters] +``` + +### Parameters + +| Parameter | Type | Description | Required | +|-----------|------|-------------|----------| +| `-DomainList` | String | Domains to analyze (comma-separated) | No | +| `-FilePath` | String | Path to file containing domains (one per line) | No | +| `-HeaderFilePath` | String | Path to file containing email headers | No | +| `-OutputPath` | String | Directory for HTML report (default: current directory) | No | +| `-AutoOpen` | Switch | Automatically open report in browser | No | + +### Usage Examples + +#### Single Domain Analysis +```powershell +.\EmailAuthChecker.ps1 -DomainList microsoft.com +``` + +#### Multiple Domain Analysis +```powershell +.\EmailAuthChecker.ps1 -DomainList microsoft.com, contoso.com, outlook.com +``` + +#### File-Based Analysis +```powershell +.\EmailAuthChecker.ps1 -FilePath "C:\temp\domains.txt" -OutputPath "C:\reports" -AutoOpen +``` + +#### Email Header Analysis +```powershell +.\EmailAuthChecker.ps1 -HeaderFilePath "C:\temp\headers.txt" +``` + +#### Custom Output with Auto-Open +```powershell +.\EmailAuthChecker.ps1 -Domain "example.com" -OutputPath "C:\EmailReports" -AutoOpen +``` + +## Email Header Analysis + +### Supported Header Analysis +The script can parse and analyze the following email headers: + +#### Authentication-Results Headers +- **SPF Results** - Pass/Fail/SoftFail/Neutral/None/PermError/TempError +- **DKIM Results** - Pass/Fail/None/Policy/Neutral/TempError/PermError +- **DMARC Results** - Pass/Fail/None with detailed condition analysis + +#### Microsoft-Specific Headers +- **X-Microsoft-Antispam-Mailbox-Delivery** + - UCF (Unified Content Filter) status + - JMR (Junk Mail Rule) application + - Dest (Destination) routing information + - OFR (Organizational Filtering Rules) status + +- **X-MS-Office365-Filtering-Correlation-Id** + - Correlation ID for tracking through Microsoft systems + +- **X-Forefront-Antispam-Report-Untrusted** + - CIP (Client IP) address + - CTRY (Country) of origin + - LANG (Language) detection + - SCL (Spam Confidence Level) + - SRV (Service) classification + - IPV (IP Version) - IPv4/IPv6 + - SFV (Sender Filter Verdict) + - PTR (Reverse DNS) validation + - CAT (Category) classification + - DIR (Direction) - inbound/outbound + - SFP (Sender Filter Policy) applied + +### Domain Extraction +- Extracts domains from `smtp.mailfrom` fields +- Extracts domains from `header.from` fields +- Validates and cleans extracted domains +- Performs comprehensive analysis on extracted domains + +## Output & Reporting + +### HTML Report Features +- **Interactive Dashboard** with domain scores and status indicators +- **Protocol-Specific Sections** for SPF, DKIM, and DMARC analysis +- **Visual Charts** showing check results and recommendations +- **Provider-Aware Documentation Links** based on detected email providers +- **Email Header Analysis Section** (when using HeaderFilePath) +- **Responsive Design** for desktop and mobile viewing +- **Professional Styling** with modern CSS and interactive elements + +### Report Sections +1. **Executive Summary** - Overall domain health and scores +2. **SPF Analysis** - Detailed SPF record evaluation +3. **DKIM Analysis** - DKIM selector and key analysis +4. **DMARC Analysis** - Policy and configuration assessment +5. **Email Header Analysis** - Real-world authentication results (when applicable) +6. **Recommendations** - Actionable security improvements +7. **Technical Details** - Raw records and technical specifications + +### Security Analysis + +#### Risk Assessment +- **Critical Issues** - Missing records, syntax errors, security vulnerabilities +- **Warning Issues** - Suboptimal configurations, performance concerns +- **Informational Items** - Best practice recommendations, optimization opportunities + +#### Provider Detection +- Automatically detects email providers based on MX records +- Provides provider-specific documentation and recommendations +- Supports Microsoft/Office 365, Google/Gmail, Amazon SES, and others + +## Advanced Features + +### Authoritative DNS Queries +- Queries authoritative DNS servers directly for accurate results +- Bypasses DNS caching issues that can affect analysis accuracy +- Provides real TTL values from authoritative sources + +### TTL Analysis +- Analyzes TTL values for SPF, DKIM, and DMARC records +- Identifies suboptimal TTL settings that impact performance +- Provides recommendations for TTL optimization + +### Macro Security Analysis +- Evaluates SPF macros for potential security risks +- Identifies complex macros that may expose infrastructure +- Warns about macros that could be used for data exfiltration + +### Multi-Provider Support +- Supports analysis across different email service providers +- Provides provider-specific recommendations and documentation +- Handles provider-specific record formats and requirements + +## Error Handling & Validation + +### Input Validation +- Domain format validation with context-specific rules +- File existence and accessibility checks +- Parameter set validation to prevent conflicting inputs + +### DNS Error Handling +- Graceful handling of DNS resolution failures +- Timeout management for slow DNS responses +- Fallback mechanisms for unreachable authoritative servers + +### Output Validation +- HTML sanitization to prevent injection attacks +- Path validation for output directory creation +- File permission checks for report generation + +## Performance Considerations + +### Optimization Features +- Efficient DNS query batching +- Parallel processing where applicable +- Caching of DNS responses within session +- Optimized regular expressions for header parsing + +### Resource Management +- Memory-efficient processing of large domain lists +- Cleanup of temporary variables and objects +- Controlled execution time with appropriate timeouts + +## Security Considerations + +### Script Security +- No external dependencies or downloads +- Uses only built-in PowerShell capabilities +- Input sanitization and validation +- No persistent storage of sensitive data + +### Analysis Security +- Identifies security vulnerabilities in email authentication +- Provides actionable remediation steps +- Warns about configuration risks and exposures + +## Troubleshooting + +### Common Issues + +#### DNS Resolution Problems +- **Symptoms**: "No authoritative servers found" errors +- **Solutions**: Check network connectivity, try different DNS servers, verify domain existence + +#### File Access Issues +- **Symptoms**: "Access denied" or "File not found" errors +- **Solutions**: Verify file paths, check permissions, ensure files exist + +#### Large Domain Lists +- **Symptoms**: Slow performance or timeouts +- **Solutions**: Process domains in smaller batches, increase timeout values + +### Debugging Options +- Use `-Verbose` parameter for detailed operation logging +- Check DNS resolution manually with `nslookup` or `Resolve-DnsName` +- Validate input files for proper formatting and encoding + +## License & Disclaimer + +### Disclaimer +This script has been thoroughly tested across various environments and scenarios. However, by using this script, you acknowledge and agree that: + +1. You are responsible for how you use the script and any outcomes resulting from its execution +2. The entire risk arising out of the use or performance of the script remains with you +3. The author and contributors are not liable for any damages, including data loss, business interruption, or other losses, even if warned of the risks + +## Support & Contribution + +### Getting Help +- Use `Get-Help .\EmailAuthChecker.ps1 -Full` for detailed parameter information +- Review the generated HTML reports for analysis explanations +- Check the troubleshooting section for common issues + +### Best Practices +- Run with administrator privileges for optimal DNS resolution +- Test with known good domains first to verify functionality +- Use the AutoOpen feature for immediate report review +- Save reports with descriptive names including dates/times +- Review recommendations carefully before implementing changes + +## Version History + +### v1.5 Enhanced (Current) +- Enhanced email header analysis capabilities +- Improved provider-specific documentation integration +- Advanced TTL analysis and optimization recommendations +- Comprehensive Microsoft antispam header parsing +- Modern HTML report design with interactive elements +- Parameter-only operation mode for automation +- Enhanced security analysis and risk assessment + +--- + +*This documentation covers the comprehensive email authentication analysis capabilities of the EmailAuthChecker.ps1 script. For the most current information and updates, refer to the script's built-in help documentation using the PowerShell Get-Help cmdlet.* diff --git a/mkdocs.yml b/mkdocs.yml index e5ca2fba74..e577351134 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -84,6 +84,7 @@ nav: - Test-HMAEAS: Hybrid/Test-HMAEAS.md - M365: - MDO: + - EmailAuthChecker: M365/MDO/EmailAuthChecker.md - MDOThreatPolicyChecker: M365/MDO/MDOThreatPolicyChecker.md - ResendFailedMail: M365/MDO/ResendFailedMail.md - DLT365Groupsupgrade: M365/DLT365Groupsupgrade.md