Get-TlsCertInfo.psm1


<#
.SYNOPSIS
    Retrieves TLS certificate details from a remote endpoint (via SslStream) and performs expiration checks.
#>

function Get-TlsCertInfo {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Server,
        [int]$Port = 443,
        [int]$TimeoutMs = 10000,
        [bool]$IgnoreCertErrors = $true,
        [int]$WarnDays = 30
    )

    # TCP connect with timeout
    $tcp  = [System.Net.Sockets.TcpClient]::new()
    $task = $tcp.ConnectAsync($Server, $Port)
    if (-not $task.Wait($TimeoutMs)) {
        $tcp.Close()
        throw "Connection to ${Server}:${Port} timed out after ${TimeoutMs} ms."
    }

    try {
        # Validation callback
        $callback = if ($IgnoreCertErrors) {
            [System.Net.Security.RemoteCertificateValidationCallback]{ param($sender,$cert,$chain,$errors) $true }
        } else {
            [System.Net.Security.RemoteCertificateValidationCallback]{
                param($sender,$cert,$chain,$errors) ($errors -eq [System.Net.Security.SslPolicyErrors]::None)
            }
        }

        $sslStream = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, $callback)

        # Prefer SslClientAuthenticationOptions on PS7+
        $useAdvancedAuth = $false
        if ($PSVersionTable.PSVersion.Major -ge 7) {
            try {
                $sslOptions = [System.Net.Security.SslClientAuthenticationOptions]::new()
                $sslOptions.TargetHost = $Server
                $sslOptions.EnabledSslProtocols =
                    [System.Security.Authentication.SslProtocols]::Tls12 -bor
                    [System.Security.Authentication.SslProtocols]::Tls13
                $sslOptions.CertificateRevocationCheckMode =
                    [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online

                $sslStream.AuthenticateAsClient($sslOptions)
                $useAdvancedAuth = $true
            } catch {
                $useAdvancedAuth = $false
            }
        }

        if (-not $useAdvancedAuth) {
            # PS5.1 fallback
            $sslStream.AuthenticateAsClient($Server)
        }

        # Certificate
        $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($sslStream.RemoteCertificate)

        # Expiration
        $now         = [DateTime]::UtcNow
        $notAfterUtc = $cert.NotAfter.ToUniversalTime()
        $daysLeft    = [Math]::Floor(($notAfterUtc - $now).TotalDays)

        # SAN (OID 2.5.29.17)
        $sanList = @()
        foreach ($ext in $cert.Extensions) {
            if ($ext.Oid.Value -eq '2.5.29.17') {
                $text = $ext.Format($true)
                $sanList += ($text -split "`r?`n") |
                    Where-Object { $_ -match '=' } |
                    ForEach-Object { ($_ -split '=', 2)[1].Trim() }
            }
        }

        # Chain
        $chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
        $null  = $chain.Build($cert)
        $chainSubjects = $chain.ChainElements | ForEach-Object { $_.Certificate.Subject }

        # Status
        $status =
            if ($daysLeft -lt 0) { "EXPIRED ($(-1 * $daysLeft) days ago)" }
            elseif ($daysLeft -le $WarnDays) { "EXPIRING SOON ($daysLeft days left ≤ $WarnDays)" }
            else { "OK ($daysLeft days left)" }

        # Cipher (PS7+)
        $negCipher = $null
        if ($sslStream.PSObject.Properties.Name -contains 'NegotiatedCipherSuite') {
            try { $negCipher = $sslStream.NegotiatedCipherSuite.ToString() } catch { $negCipher = $null }
        }

        [PSCustomObject]@{
            Server              = $Server
            Port                = $Port
            Subject             = $cert.Subject
            Issuer              = $cert.Issuer
            Thumbprint          = $cert.Thumbprint
            SerialNumber        = $cert.SerialNumber
            NotBeforeUtc        = $cert.NotBefore.ToUniversalTime()
            NotAfterUtc         = $notAfterUtc
            DaysRemaining       = $daysLeft
            ExpirationStatus    = $status
            SAN                 = $sanList
            SignatureAlgorithm  = $cert.SignatureAlgorithm.FriendlyName
            PublicKeyAlgorithm  = $cert.PublicKey.Oid.FriendlyName
            KeyLengthBits       = $cert.PublicKey.Key.KeySize
            ChainSubjects       = $chainSubjects
            IgnoreCertErrors    = $IgnoreCertErrors
            ProtocolsRequested  = if ($useAdvancedAuth) {
                                      $sslOptions.EnabledSslProtocols.ToString()
                                  } else {
                                      'Default (AuthenticateAsClient(server))'
                                  }
            SslNegotiatedCipher = $negCipher
        }
    }
    finally {
        if ($sslStream) { $sslStream.Dispose() }
        if ($tcp)       { $tcp.Close() }
    }
}

Export-ModuleMember -Function Get-TlsCertInfo