Public/certificate/Get-ExpiringCertificate.ps1

#Requires -Version 5.1
function Get-ExpiringCertificate {
    <#
    .SYNOPSIS
        Find local-store certificates expiring within a threshold of days
 
    .DESCRIPTION
        Scans one or more local machine certificate stores (My, WebHosting, CA, ...) on one or
        more computers and returns the certificates whose NotAfter falls within the given number
        of days, sorted by expiry date ascending. Optionally includes already-expired
        certificates. Complements Get-SSLCertificate (network endpoints) and
        Get-IISCertificateBinding (IIS bindings) on the local-store side.
 
    .PARAMETER ComputerName
        One or more computer names to target. Defaults to the local computer.
        Accepts pipeline input by value and by property name.
 
    .PARAMETER Credential
        Optional PSCredential for authenticating to remote computers.
        Not used for local queries.
 
    .PARAMETER DaysUntilExpiration
        Threshold in days. Certificates whose NotAfter is on or before
        (Get-Date).AddDays($DaysUntilExpiration) are returned. Defaults to 30.
 
    .PARAMETER StoreName
        One or more certificate store names to scan under LocalMachine (for example My,
        WebHosting, CA, Root). Defaults to 'My'. A store that does not exist on the target
        machine triggers a warning and is skipped; other stores and machines still process.
 
    .PARAMETER IncludeExpired
        Also include certificates whose NotAfter has already passed. DaysRemaining is negative
        for those certificates.
 
    .EXAMPLE
        Get-ExpiringCertificate -DaysUntilExpiration 30
 
        Local usage example.
 
    .EXAMPLE
        Get-ExpiringCertificate -ComputerName 'SRV01' -StoreName 'My','WebHosting' -DaysUntilExpiration 60
 
        Remote single-machine example.
 
    .EXAMPLE
        'SRV01','SRV02' | Get-ExpiringCertificate -IncludeExpired
 
        Pipeline usage example.
 
    .OUTPUTS
        PSWinOps.ExpiringCertificate
        One object per matching certificate, sorted by NotAfter ascending per machine.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-09-04
        Requires: PowerShell 5.1+ / Windows only
 
    .LINK
        https://github.com/k9fr4n/PSWinOps
 
    .LINK
        https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509store
    #>

    [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')]
    [OutputType('PSWinOps.ExpiringCertificate')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, [int]::MaxValue)]
        [int]$DaysUntilExpiration = 30,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string[]]$StoreName = @('My'),

        [Parameter(Mandatory = $false)]
        [switch]$IncludeExpired
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        $scriptBlock = {
            param(
                [string[]]$StoreNames,
                [int]$Threshold,
                [bool]$IncludeExpiredCerts
            )

            $results = [System.Collections.Generic.List[hashtable]]::new()
            $now = Get-Date
            $tsNow = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'

            foreach ($sn in $StoreNames) {
                try {
                    $certs = @(Get-ChildItem -Path "Cert:\LocalMachine\$sn" -ErrorAction Stop)
                }
                catch {
                    Write-Warning -Message "Certificate store '$sn' not found on '$env:COMPUTERNAME': $($_.Exception.Message)"
                    continue
                }

                foreach ($cert in $certs) {
                    $isExpired = $cert.NotAfter -lt $now
                    if ($isExpired -and -not $IncludeExpiredCerts) {
                        continue
                    }
                    if ($cert.NotAfter -gt $now.AddDays($Threshold)) {
                        continue
                    }

                    $daysRemaining = [int][math]::Floor(($cert.NotAfter - $now).TotalDays)
                    $eku = @($cert.EnhancedKeyUsageList | ForEach-Object { $_.FriendlyName })

                    $results.Add(@{
                        ComputerName     = $env:COMPUTERNAME
                        StoreName        = $sn
                        Subject          = $cert.Subject
                        Issuer           = $cert.Issuer
                        Thumbprint       = $cert.Thumbprint
                        NotAfter         = $cert.NotAfter
                        DaysRemaining    = $daysRemaining
                        HasPrivateKey    = $cert.HasPrivateKey
                        EnhancedKeyUsage = $eku
                        Timestamp        = $tsNow
                    })
                }
            }

            return @($results | Sort-Object -Property { $_.NotAfter })
        }
    }

    process {
        foreach ($targetComputer in $ComputerName) {
            try {
                $invokeParams = @{
                    ComputerName = $targetComputer
                    ScriptBlock  = $scriptBlock
                    ArgumentList = @(
                        $StoreName,
                        $DaysUntilExpiration,
                        $IncludeExpired.IsPresent
                    )
                }
                if ($Credential) {
                    $invokeParams['Credential'] = $Credential
                }

                $rawResults = Invoke-RemoteOrLocal @invokeParams
                foreach ($r in $rawResults) {
                    [PSCustomObject]([ordered]@{
                        PSTypeName       = 'PSWinOps.ExpiringCertificate'
                        ComputerName     = $r.ComputerName
                        StoreName        = $r.StoreName
                        Subject          = $r.Subject
                        Issuer           = $r.Issuer
                        Thumbprint       = $r.Thumbprint
                        NotAfter         = $r.NotAfter
                        DaysRemaining    = $r.DaysRemaining
                        HasPrivateKey    = $r.HasPrivateKey
                        EnhancedKeyUsage = $r.EnhancedKeyUsage
                        Timestamp        = $r.Timestamp
                    })
                }
            }
            catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_"
            }
        }
    }

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