Public/network/Get-SSLCertificate.ps1

#Requires -Version 5.1

function Get-SSLCertificate {
    <#
    .SYNOPSIS
        Retrieves SSL/TLS certificate information from remote endpoints.
    .DESCRIPTION
        Connects to one or more remote hosts using System.Net.Security.SslStream
        and retrieves the server certificate. Returns structured objects with
        subject, issuer, validity dates, days remaining, SAN entries, and
        thumbprint.
 
        Ideal for proactive certificate expiry monitoring.
    .PARAMETER Uri
        One or more hostnames, IP addresses, or URIs to check.
        Accepts pipeline input. If a full URI is provided (https://host),
        the hostname and port are extracted automatically.
    .PARAMETER Port
        TCP port to connect to. Default: 443.
    .PARAMETER TimeoutMs
        Connection timeout in milliseconds. Default: 5000. Valid range: 1000-30000.
    .PARAMETER AcceptInvalidCertificates
        Accept self-signed or untrusted certificates for inspection.
        Default: $true (inspect all certs regardless of trust).
    .EXAMPLE
        Get-SSLCertificate -Uri 'google.com'
 
        Retrieves the SSL certificate from google.com:443.
    .EXAMPLE
        Get-SSLCertificate -Uri 'mail.corp.local', 'intranet.corp.local' -Port 443
 
        Checks certificates on two internal hosts.
    .EXAMPLE
        Get-SSLCertificate -Uri 'myserver' -Port 8443
 
        Checks a certificate on a non-standard HTTPS port.
    .EXAMPLE
        Get-Content servers.txt | Get-SSLCertificate | Where-Object { $_.DaysRemaining -lt 30 }
 
        Pipeline: find certificates expiring within 30 days.
    .OUTPUTS
    PSWinOps.SSLCertificate
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-03-21
        Requires: PowerShell 5.1+ / Windows only
        Permissions: No admin required
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.SSLCertificate')]
    param (
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Host', 'ComputerName', 'CN', 'Url')]
        [string[]]$Uri,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 65535)]
        [int]$Port = 443,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1000, 30000)]
        [int]$TimeoutMs = 5000,

        [Parameter(Mandatory = $false)]
        [bool]$AcceptInvalidCertificates = $true
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting SSL certificate retrieval"
    }

    process {
        foreach ($target in $Uri) {
            try {
                # Extract hostname and optional port from URI
                $targetHost = $target
                $targetPort = $Port

                if ($target -match '^https?://') {
                    $parsed = [System.Uri]::new($target)
                    $targetHost = $parsed.Host
                    if ($parsed.Port -gt 0 -and $parsed.Port -ne 443) {
                        $targetPort = $parsed.Port
                    }
                } elseif ($target -match '^([^:]+):(\d+)$') {
                    $targetHost = $Matches[1]
                    $targetPort = [int]$Matches[2]
                }

                Write-Verbose "[$($MyInvocation.MyCommand)] Connecting to ${targetHost}:${targetPort}"

                $tcpClient = New-Object System.Net.Sockets.TcpClient
                $connectTask = $tcpClient.ConnectAsync($targetHost, $targetPort)
                if (-not $connectTask.Wait($TimeoutMs)) {
                    $tcpClient.Dispose()
                    Write-Error "[$($MyInvocation.MyCommand)] Connection to '${targetHost}:${targetPort}' timed out"
                    continue
                }

                $sslStream = $null
                try {
                    $callback = if ($AcceptInvalidCertificates) {
                        [System.Net.Security.RemoteCertificateValidationCallback]{ $true }
                    } else {
                        $null
                    }

                    $sslStream = New-Object System.Net.Security.SslStream(
                        $tcpClient.GetStream(), $false, $callback
                    )
                    $sslStream.AuthenticateAsClient($targetHost)

                    $cert = $sslStream.RemoteCertificate
                    $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert)

                    # Extract SAN (Subject Alternative Names)
                    $san = @($cert2.Extensions |
                        Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' } |
                        ForEach-Object { $_.Format($false) }) -join ', '

                    $now = Get-Date
                    $daysRemaining = [int][math]::Floor(($cert2.NotAfter - $now).TotalDays)

                    [PSCustomObject]@{
                        PSTypeName     = 'PSWinOps.SSLCertificate'
                        Host           = $targetHost
                        Port           = $targetPort
                        Subject        = $cert2.Subject
                        Issuer         = $cert2.Issuer
                        NotBefore      = $cert2.NotBefore
                        NotAfter       = $cert2.NotAfter
                        DaysRemaining  = $daysRemaining
                        IsExpired      = ($now -gt $cert2.NotAfter)
                        Thumbprint     = $cert2.Thumbprint
                        SerialNumber   = $cert2.SerialNumber
                        SignatureAlgorithm = $cert2.SignatureAlgorithm.FriendlyName
                        KeyLength      = $cert2.PublicKey.Key.KeySize
                        SAN            = $san
                        Protocol       = $sslStream.SslProtocol
                        Timestamp      = Get-Date -Format 'o'
                    }

                    $cert2.Dispose()
                } finally {
                    if ($sslStream) { $sslStream.Dispose() }
                    $tcpClient.Dispose()
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed on '${target}': $_"
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed SSL certificate retrieval"
    }
}