Skip to content

Test-SSLCertificates.ps1

Vérification des certificats SSL/TLS sur endpoints multiples.


Description

  • Test de certificats sur endpoints HTTPS
  • Vérification de la chaîne de confiance
  • Alertes d'expiration (seuils configurables)
  • Support SNI (Server Name Indication)
  • Export JSON pour intégration CI/CD
  • Vérification des certificats locaux (Store)

Prérequis

  • Système : Windows Server 2016+ ou Windows 10/11
  • PowerShell : Version 5.1 minimum
  • Permissions : Lecture réseau et certificats locaux (pas d'élévation requise)
  • Modules : Aucun module externe requis

Cas d'Usage

  • Monitoring certificats : Surveillance automatique des dates d'expiration
  • Audit sécurité : Vérification de la chaîne de confiance et algorithmes
  • CI/CD Integration : Validation des certificats avant déploiement en production
  • Documentation : Inventaire des certificats SSL de l'infrastructure

Utilisation

# Test d'un seul endpoint
.\Test-SSLCertificates.ps1 -Endpoint "https://example.com"

# Test multiple endpoints
.\Test-SSLCertificates.ps1 -Endpoint "example.com","api.example.com" -Port 443

# Alerte expiration < 30 jours
.\Test-SSLCertificates.ps1 -Endpoint "example.com" -WarningDays 30 -CriticalDays 7

# Export JSON pour CI/CD
.\Test-SSLCertificates.ps1 -Endpoint "example.com" -OutputFormat JSON

# Vérifier les certificats du store local
.\Test-SSLCertificates.ps1 -LocalStore -StoreName My -WarningDays 60

Paramètres

Paramètre Type Défaut Description
-Endpoint String[] - URLs ou hostnames à tester
-Port Int 443 Port HTTPS
-WarningDays Int 30 Seuil warning (jours avant expiration)
-CriticalDays Int 7 Seuil critique (jours avant expiration)
-TimeoutSeconds Int 10 Timeout de connexion
-LocalStore Switch - Vérifier le store local
-StoreName String My Nom du store (My, Root, CA)
-OutputFormat String Table Format de sortie (Table, JSON, CSV)
-IgnoreValidation Switch - Ignorer erreurs de validation

Code Source

#Requires -Version 5.1
<#
.SYNOPSIS
    Test SSL/TLS certificates on multiple endpoints.

.DESCRIPTION
    Validates SSL certificates, checks expiration dates, verifies chain of trust,
    and reports on certificate health. Supports both remote endpoints and local
    certificate store verification.

.PARAMETER Endpoint
    URLs or hostnames to test.

.PARAMETER Port
    HTTPS port (default: 443).

.PARAMETER WarningDays
    Days before expiration to trigger warning (default: 30).

.PARAMETER CriticalDays
    Days before expiration to trigger critical alert (default: 7).

.PARAMETER TimeoutSeconds
    Connection timeout in seconds.

.PARAMETER LocalStore
    Check local certificate store instead of remote endpoints.

.PARAMETER StoreName
    Certificate store name (My, Root, CA, etc.).

.PARAMETER OutputFormat
    Output format: Table, JSON, or CSV.

.PARAMETER IgnoreValidation
    Ignore certificate validation errors (self-signed, etc.).

.EXAMPLE
    .\Test-SSLCertificates.ps1 -Endpoint "google.com","github.com"
    Test certificates for multiple endpoints.

.NOTES
    Author: ShellBook
    Version: 1.0
    Date: 2024-01-01
#>

[CmdletBinding(DefaultParameterSetName = 'Remote')]
param(
    [Parameter(ParameterSetName = 'Remote', Position = 0, Mandatory = $true)]
    [ValidateNotNullOrEmpty()]
    [string[]]$Endpoint,

    [Parameter(ParameterSetName = 'Remote')]
    [ValidateRange(1, 65535)]
    [int]$Port = 443,

    [Parameter()]
    [ValidateRange(1, 365)]
    [int]$WarningDays = 30,

    [Parameter()]
    [ValidateRange(1, 90)]
    [int]$CriticalDays = 7,

    [Parameter(ParameterSetName = 'Remote')]
    [ValidateRange(1, 60)]
    [int]$TimeoutSeconds = 10,

    [Parameter(ParameterSetName = 'Local', Mandatory = $true)]
    [switch]$LocalStore,

    [Parameter(ParameterSetName = 'Local')]
    [ValidateSet('My', 'Root', 'CA', 'TrustedPeople', 'TrustedPublisher')]
    [string]$StoreName = 'My',

    [Parameter()]
    [ValidateSet('Table', 'JSON', 'CSV')]
    [string]$OutputFormat = 'Table',

    [Parameter(ParameterSetName = 'Remote')]
    [switch]$IgnoreValidation
)

#region Configuration
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

$Results = [System.Collections.ArrayList]::new()
$ExitCode = 0
#endregion

#region Functions
function Write-Status {
    param(
        [string]$Message,
        [ValidateSet('OK', 'Warning', 'Critical', 'Info', 'Error')]
        [string]$Status = 'Info'
    )

    $colors = @{
        'OK'       = 'Green'
        'Warning'  = 'Yellow'
        'Critical' = 'Red'
        'Info'     = 'Cyan'
        'Error'    = 'Magenta'
    }

    $symbols = @{
        'OK'       = '[OK]'
        'Warning'  = '[WARN]'
        'Critical' = '[CRIT]'
        'Info'     = '[*]'
        'Error'    = '[ERR]'
    }

    Write-Host "$($symbols[$Status]) $Message" -ForegroundColor $colors[$Status]
}

function Get-CertificateStatus {
    param(
        [int]$DaysRemaining,
        [int]$WarnThreshold,
        [int]$CritThreshold
    )

    if ($DaysRemaining -lt 0) {
        return "EXPIRED"
    } elseif ($DaysRemaining -le $CritThreshold) {
        return "CRITICAL"
    } elseif ($DaysRemaining -le $WarnThreshold) {
        return "WARNING"
    } else {
        return "OK"
    }
}

function Test-RemoteCertificate {
    param(
        [string]$HostName,
        [int]$HostPort,
        [int]$Timeout,
        [bool]$SkipValidation
    )

    $result = [PSCustomObject]@{
        Endpoint         = "${HostName}:${HostPort}"
        Subject          = $null
        Issuer           = $null
        NotBefore        = $null
        NotAfter         = $null
        DaysRemaining    = $null
        Status           = "UNKNOWN"
        Thumbprint       = $null
        SignatureAlgo    = $null
        KeySize          = $null
        SANs             = $null
        ChainValid       = $null
        Error            = $null
    }

    try {
        # Create TCP connection
        $tcpClient = [System.Net.Sockets.TcpClient]::new()
        $connectTask = $tcpClient.ConnectAsync($HostName, $HostPort)

        if (-not $connectTask.Wait($Timeout * 1000)) {
            throw "Connection timeout"
        }

        # SSL stream with callback
        $callback = if ($SkipValidation) {
            { $true }
        } else {
            $null
        }

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

        try {
            $sslStream.AuthenticateAsClient($HostName)
            $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
                $sslStream.RemoteCertificate
            )

            # Get certificate details
            $result.Subject = $cert.Subject
            $result.Issuer = $cert.Issuer
            $result.NotBefore = $cert.NotBefore
            $result.NotAfter = $cert.NotAfter
            $result.Thumbprint = $cert.Thumbprint
            $result.SignatureAlgo = $cert.SignatureAlgorithm.FriendlyName
            $result.KeySize = $cert.PublicKey.Key.KeySize

            # Calculate days remaining
            $result.DaysRemaining = [Math]::Floor(($cert.NotAfter - (Get-Date)).TotalDays)
            $result.Status = Get-CertificateStatus -DaysRemaining $result.DaysRemaining `
                -WarnThreshold $WarningDays -CritThreshold $CriticalDays

            # Extract SANs
            $sanExtension = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Subject Alternative Name" }
            if ($sanExtension) {
                $result.SANs = $sanExtension.Format($false)
            }

            # Build and verify chain
            $chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
            $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online
            $result.ChainValid = $chain.Build($cert)

            $cert.Dispose()
            $chain.Dispose()
        }
        finally {
            $sslStream.Dispose()
        }
    }
    catch {
        $result.Error = $_.Exception.Message
        $result.Status = "ERROR"
    }
    finally {
        if ($tcpClient) {
            $tcpClient.Dispose()
        }
    }

    return $result
}

function Get-LocalCertificates {
    param(
        [string]$Store
    )

    $storePath = "Cert:\LocalMachine\$Store"
    $certs = Get-ChildItem -Path $storePath -ErrorAction SilentlyContinue

    $results = foreach ($cert in $certs) {
        $daysRemaining = [Math]::Floor(($cert.NotAfter - (Get-Date)).TotalDays)

        [PSCustomObject]@{
            Endpoint         = "LocalMachine\$Store"
            Subject          = $cert.Subject
            Issuer           = $cert.Issuer
            NotBefore        = $cert.NotBefore
            NotAfter         = $cert.NotAfter
            DaysRemaining    = $daysRemaining
            Status           = Get-CertificateStatus -DaysRemaining $daysRemaining `
                -WarnThreshold $WarningDays -CritThreshold $CriticalDays
            Thumbprint       = $cert.Thumbprint
            SignatureAlgo    = $cert.SignatureAlgorithm.FriendlyName
            KeySize          = if ($cert.PublicKey.Key) { $cert.PublicKey.Key.KeySize } else { $null }
            SANs             = $null
            ChainValid       = $null
            Error            = $null
        }
    }

    return $results
}

function Format-Output {
    param(
        [object[]]$Data,
        [string]$Format
    )

    switch ($Format) {
        'Table' {
            $Data | Format-Table -AutoSize @(
                'Endpoint',
                @{N='Subject'; E={if ($_.Subject.Length -gt 40) { $_.Subject.Substring(0,37) + "..." } else { $_.Subject }}},
                'NotAfter',
                @{N='Days'; E={$_.DaysRemaining}},
                @{N='Status'; E={
                    switch ($_.Status) {
                        'OK'       { Write-Host $_ -ForegroundColor Green -NoNewline; $_ }
                        'WARNING'  { Write-Host $_ -ForegroundColor Yellow -NoNewline; $_ }
                        'CRITICAL' { Write-Host $_ -ForegroundColor Red -NoNewline; $_ }
                        'EXPIRED'  { Write-Host $_ -ForegroundColor Magenta -NoNewline; $_ }
                        'ERROR'    { Write-Host $_ -ForegroundColor Red -NoNewline; $_ }
                        default    { $_ }
                    }
                }},
                'ChainValid'
            )
        }
        'JSON' {
            $Data | ConvertTo-Json -Depth 5
        }
        'CSV' {
            $Data | ConvertTo-Csv -NoTypeInformation
        }
    }
}
#endregion

#region Main
try {
    Write-Status "=== SSL/TLS Certificate Checker ===" -Status Info
    Write-Host ""

    if ($LocalStore) {
        Write-Status "Checking local certificate store: $StoreName" -Status Info
        $Results = Get-LocalCertificates -Store $StoreName
    } else {
        Write-Status "Testing $($Endpoint.Count) endpoint(s)..." -Status Info
        Write-Host ""

        foreach ($ep in $Endpoint) {
            # Clean endpoint
            $hostName = $ep -replace '^https?://' -replace '/.*$'

            Write-Status "Testing: $hostName" -Status Info

            $certResult = Test-RemoteCertificate -HostName $hostName -HostPort $Port `
                -Timeout $TimeoutSeconds -SkipValidation $IgnoreValidation

            [void]$Results.Add($certResult)

            # Log status
            switch ($certResult.Status) {
                'OK'       { Write-Status "  Valid for $($certResult.DaysRemaining) days" -Status OK }
                'WARNING'  { Write-Status "  Expires in $($certResult.DaysRemaining) days" -Status Warning; $ExitCode = 1 }
                'CRITICAL' { Write-Status "  CRITICAL: $($certResult.DaysRemaining) days left!" -Status Critical; $ExitCode = 2 }
                'EXPIRED'  { Write-Status "  EXPIRED!" -Status Critical; $ExitCode = 2 }
                'ERROR'    { Write-Status "  Error: $($certResult.Error)" -Status Error; $ExitCode = 2 }
            }
        }
    }

    Write-Host ""
    Write-Status "=== Results ===" -Status Info
    Write-Host ""

    # Output results
    Format-Output -Data $Results -Format $OutputFormat

    # Summary
    Write-Host ""
    $okCount = ($Results | Where-Object { $_.Status -eq 'OK' }).Count
    $warnCount = ($Results | Where-Object { $_.Status -eq 'WARNING' }).Count
    $critCount = ($Results | Where-Object { $_.Status -in @('CRITICAL', 'EXPIRED') }).Count
    $errCount = ($Results | Where-Object { $_.Status -eq 'ERROR' }).Count

    Write-Status "Summary: OK=$okCount, Warning=$warnCount, Critical=$critCount, Errors=$errCount" -Status Info

    exit $ExitCode
}
catch {
    Write-Status "Fatal error: $_" -Status Error
    exit 2
}
#endregion

Exemples de Sortie

Table Output

Endpoint           Subject                                  NotAfter            Days Status   ChainValid
--------           -------                                  --------            ---- ------   ----------
google.com:443     CN=*.google.com                         2024-04-15 12:00:00   89 OK       True
github.com:443     CN=github.com                           2024-03-20 23:59:59   63 OK       True
expired.badssl.com CN=*.badssl.com                         2023-12-01 00:00:00  -45 EXPIRED  False

JSON Output

[
  {
    "Endpoint": "google.com:443",
    "Subject": "CN=*.google.com",
    "Issuer": "CN=GTS CA 1C3, O=Google Trust Services LLC, C=US",
    "NotBefore": "2024-01-15T08:00:00",
    "NotAfter": "2024-04-15T12:00:00",
    "DaysRemaining": 89,
    "Status": "OK",
    "Thumbprint": "ABC123...",
    "SignatureAlgo": "sha256RSA",
    "KeySize": 2048,
    "SANs": "DNS Name=*.google.com, DNS Name=google.com",
    "ChainValid": true,
    "Error": null
  }
]

Intégration CI/CD

GitHub Actions

- name: Check SSL Certificates
  shell: pwsh
  run: |
    $result = .\Test-SSLCertificates.ps1 -Endpoint "api.example.com" -OutputFormat JSON
    if ($LASTEXITCODE -ne 0) {
      Write-Error "Certificate check failed!"
      exit 1
    }

Monitoring avec Alertes

# Script de monitoring quotidien
$endpoints = @("prod-api.example.com", "staging-api.example.com", "cdn.example.com")
$results = .\Test-SSLCertificates.ps1 -Endpoint $endpoints -WarningDays 30 -OutputFormat JSON | ConvertFrom-Json

$expiring = $results | Where-Object { $_.Status -in @('WARNING', 'CRITICAL') }

if ($expiring) {
    # Envoyer alerte (Slack, Email, PagerDuty...)
    Send-SlackMessage -Message "Certificats expirant bientôt: $($expiring.Endpoint -join ', ')"
}

Voir Aussi