public/Invoke-MtaSts.ps1

<#>
.HelpInfoURI 'https://github.com/T13nn3s/Show-SpfDkimDmarc/blob/main/public/CmdletHelp/Invoke-MtaSts.md'
#>

function Invoke-MtaSts {
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $True,
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True,
            HelpMessage = "Specifies the domain for resolving the MTA-STS record."
        )][string[]]$Name,

        [Parameter(Mandatory = $false,
            HelpMessage = "DNS Server to use.")]
        [string]$Server
    )

    begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        $PSBoundParameters | Out-String | Write-Verbose

        if ($PSBoundParameters.ContainsKey('Server')) {
            $SplatParameters = @{
                'Server'      = $Server
                'ErrorAction' = 'SilentlyContinue'
            }
        }
        Else {
            $SplatParameters = @{
                'ErrorAction' = 'SilentlyContinue'
            }
        }

        $MtaObject = New-Object System.Collections.Generic.List[System.Object]

        # Test if the MX record mail server supports TLS
        # Test-MxTls -MxHostname aspmx.l.google.com
        function Test-MxTls {
            [CmdletBinding()]
            param(
                [string]$MxHostname
            )

            # https://www.checktls.com/TestReceiver
            # https://www.alitajran.com/test-smtp-connection-with-telnet-powershell-script/

            try {
                $ret = @()
                $socket = New-Object System.Net.Sockets.TcpClient($MxHostname, 25)

                $stream = $socket.GetStream()
                $writer = New-Object System.IO.StreamWriter($stream)
                $buffer = New-Object System.Byte[] 1024
                $encoding = New-Object System.Text.AsciiEncoding
                Start-Sleep -Milliseconds 100
                While ( $stream.DataAvailable ) {
                    $read = $stream.Read($buffer, 0, 1024)
                    $ret += $encoding.GetString($buffer, 0, $read)
                }

                $writer.WriteLine("EHLO TestingTLS")
                $writer.Flush()
                Start-Sleep -Milliseconds 100
                While ( $stream.DataAvailable ) {
                    $read = $stream.Read($buffer, 0, 1024)
                    $ret += $encoding.GetString($buffer, 0, $read)
                }

                $writer.WriteLine("STARTTLS")
                $writer.Flush()
                Start-Sleep -Milliseconds 500
                While ( $stream.DataAvailable ) {
                    $read = $stream.Read($buffer, 0, 1024)
                    $ret += $encoding.GetString($buffer, 0, $read)
                }

                return !!($ret -match "220")
            }
            catch {
                Write-Error $_
                return $false
            }
        }
    }

    Process {
        # Check if the MX records are all covered by the MTA file and there's no extra or few MTA MX records configured. return true if all ok
        function Get-MxMta {
            [CmdletBinding()]
            param(
                [string]$domainName,
                [string]$mtsStsFileContents
            )

            $_mx = Resolve-DnsName -Name $domainName -Type MX @SplatParameters | Select-Object -ExpandProperty NameExchange
            Write-Verbose "MX: $($_mx)"
            $_mta = $mtsStsFileContents.Split("`n") -match '(?<=mx: ).*$' | ForEach-Object { $_.Replace("mx:", "").Trim() }
            Write-Verbose "MTA: $($_mta)"

            # Get all MX and MTA matches
            $ret = $_mx | ForEach-Object { $i = $_; $_mta | ForEach-Object { if ( $i -like $_ ) { [PSCustomObject]@{ MX = $i; MTA = $_ } } } }
            Write-Verbose "Matches: `n $($ret | Out-String)"

            $res = Compare-Object -ReferenceObject $ret.MX -DifferenceObject (Resolve-DnsName -Name $domainName -Type MX @SplatParameters | Select-Object -ExpandProperty NameExchange) # if differences, some MX records are not in MTA
            $res += Compare-Object -ReferenceObject ($ret.MTA | Select-Object -Unique) -DifferenceObject $_mta

            return (!$res)
        }

        $cti = (Get-Culture).TextInfo

        foreach ($domain in $Name) {

            $mtsStsDns = (Resolve-DnsName -Name "_mta-sts.$($domain)" -Type TXT -QuickTimeout -ErrorAction SilentlyContinue | Where-Object { $_.Name -eq "_mta-sts.$($domain)" -and $_.Strings -match "v=STSv1" } | Select-Object -ExpandProperty Strings) -join "`n"
            Write-Verbose "mtsStsDns: $($mtsStsDns)"

            try { 
                $mtsStsFile = Invoke-WebRequest "https://mta-sts.$($domain)/.well-known/mta-sts.txt" -UseBasicParsing -DisableKeepAlive | Select-Object -ExpandProperty Content 
            }
            catch { 
                $mtsStsFile = $null 
            }
            Write-Verbose "mtsStsFile: $($mtsStsFile)"

            $mtaRecord = $mtsStsDns

            switch -Regex ($mtsStsDns) {
                { !$_ } {
                    $mtaAdvisory = "The MTA-STS DNS record doesn't exist. "; continue
                }
                { $_.Split("`n").Count -ne 1 } {
                    $mtaAdvisory = "There are multiple MTA-STS DNS records. "
                }
                { $_ -notmatch 'STSv1' } {
                    $mtaAdvisory = "The MTA-STS version is not configured properly. Only STSv1 is supported. " 
                }
                { $_ -notmatch 'id=([^;\s]{1,32})(?:;|$)' } {
                    $mtaAdvisory = "The MTA-STS id must be alphanumeric and no longer than 32 characters. " 
                }
                default {

                    switch -Regex ($mtsStsFile) {
                        { !$_ } {
                            $mtaAdvisory = "The MTA-STS file doesn't exist. "; continue 
                        }
                        { $_ -notmatch 'version:\s*(STSv1)' } { 
                            $mtaAdvisory = "The MTA-STS version is not configured in the file. The only options is STSv1. " 
                        }
                        { $_ -notmatch 'mode:\s*(enforce|none|testing)' } { 
                            $mtaAdvisory = "The MTA-STS mode is not configured in the file. Options are Enforce, Testing and None. " 
                        }
                        { $_ -match 'mode:\sF*(enforce|none|testing)' -and $_ -notmatch 'mode:\s*Enforce' } {
                            $mtaAdvisory = "The MTA-STS file is configured in $($null = $_ -match 'mode:\s*(enforce|none|testing)'; $cti.ToTitleCase($Matches[1].ToLower()) ) mode and not protecting interception or tampering. " 
                        }
                        { !($_.Split("`n") -match '(?<=mx: ).*$') } {
                            $mtaAdvisory = "The MTA-STS file doesn't have any MX record configured. " 
                        }
                        { $_.Split("`n") -match '(?<=mx: ).*$' -and ( !(Get-MxMta -domainName $domain -mtsStsFileContents $mtsStsFile) ) } { 
                            $mtaAdvisory = "The MTA-STS file MX records don't match with the MX records configured in the domain. " 
                        }
                        { $_.Split("`n") -match '(?<=mx: ).*$' -and ( Resolve-DnsName -Name $domain -Type MX @SplatParameters | Select-Object -ExpandProperty NameExchange | ForEach-Object { Test-MxTls -MxHostname $_ -Verbose } ) -contains $false } {
                            $mtaAdvisory = "At least one of the MX records configured in the MTA-STS file MX records list doesn't support TLS. " 
                        }
                        { $_ -notmatch 'max_age:\s*(604800|31557600)' } {
                            $mtaAdvisory = "The MTA-STS max age configured in the file should be greater than 604800 seconds and less than 31557600 seconds. " 
                        }
                        default {
                            $mtaAdvisory = "The domain has the MTA-STS DNS record and file configured and protected against interception or tampering."
                        }
                    }
                }
            }
            $MtaReturnValues = New-Object psobject 
            $MtaReturnValues | Add-Member NoteProperty "Name" $domain
            $MtaReturnValues | Add-Member NoteProperty "mtaRecord" $mtaRecord
            $MtaReturnValues | Add-Member NoteProperty "mtaAdvisory" $mtaAdvisory
            $MtaObject.Add($MtaReturnValues)
            $MtaReturnValues
            
        }
    } End {}
}

Set-Alias -Name gmta -Value Get-MTASTS