Public/iis/Get-IISCertificateBinding.ps1

#Requires -Version 5.1
function Get-IISCertificateBinding {
    <#
        .SYNOPSIS
            Inventories every IIS HTTPS binding joined to the X509 certificate it actually presents.
 
        .DESCRIPTION
            Enumerates all https bindings on one or more IIS hosts and joins each binding
            to the X509 certificate it points at, surfacing site, ip:port:hostheader,
            SNI/CCS flags, thumbprint, subject, SAN, issuer, validity window, days until
            expiration, certificate store of record and presence of the private key.
            Provides the read-only typed counterpart of Set-IISBindingCertificate that
            IISAdministration does not expose in a single cmdlet. Falls back gracefully
            from WebAdministration to IISAdministration to appcmd, and pipes cleanly into
            Set-IISBindingCertificate for rotation workflows.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local machine.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional PSCredential for authenticating to remote computers.
            Not used for local queries.
 
        .PARAMETER SiteName
            Filter to one or more site names. Supports -like wildcards. Defaults to all
            https-bound sites.
 
        .PARAMETER Thumbprint
            Filter to one or more certificate thumbprints (40 hex characters, uppercased
            internally). Useful to track a specific certificate across multiple bindings.
 
        .PARAMETER HostHeader
            Filter on the host header part of the binding. Supports -like wildcards.
 
        .PARAMETER Port
            Filter to specific TCP ports. Defaults to all https ports (typically 443).
 
        .PARAMETER ExpiringInDays
            Returns only certificates with DaysUntilExpiration less than or equal to this
            value. Includes already-expired certificates (negative DaysUntilExpiration).
 
        .PARAMETER IncludeExpired
            When used alone, returns only rows where Expired is $true. Implied when
            -ExpiringInDays is used.
 
        .EXAMPLE
            Get-IISCertificateBinding
 
            Inventories all https bindings and their certificates on the local machine.
 
        .EXAMPLE
            'WEB01','WEB02','WEB03' | Get-IISCertificateBinding -Credential (Get-Credential)
 
            Audits certificate bindings across a web farm using alternate credentials.
 
        .EXAMPLE
            Get-IISCertificateBinding -ComputerName WEB01 -ExpiringInDays 30
 
            Returns bindings whose certificate expires within the next 30 days.
 
        .EXAMPLE
            Get-IISCertificateBinding -SiteName 'www*' -HostHeader '*.contoso.com'
 
            Filters by site name wildcard and host header wildcard.
 
        .EXAMPLE
            Get-IISCertificateBinding -ComputerName WEB01 -ExpiringInDays 15 | Set-IISBindingCertificate -Thumbprint $newTp -Confirm:$false
 
            Pipes expiring bindings directly into the rotation cmdlet.
 
        .EXAMPLE
            Get-IISCertificateBinding | Where-Object Status -eq 'CertNotFound'
 
            Surfaces orphan bindings whose certificate has been removed from the store.
 
        .OUTPUTS
            PSCustomObject (PSTypeName='PSWinOps.IISCertificateBinding')
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-05-16
            Requires: PowerShell 5.1+ / Windows only
            Requires: Web-Server (IIS) role
            Optional: Module WebAdministration or IISAdministration (falls back to appcmd)
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.IISCertificateBinding')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

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

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [string[]]$SiteName,

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidatePattern('^[A-Fa-f0-9]{40}$')]
        [string[]]$Thumbprint,

        [Parameter(Mandatory = $false)]
        [string[]]$HostHeader,

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

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 3650)]
        [int]$ExpiringInDays,

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

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

        $scriptBlock = {
            param(
                [string[]]$FilterSiteName,
                [string[]]$FilterThumbprint,
                [string[]]$FilterHostHeader,
                [int[]]$FilterPort,
                [object]$FilterExpiringInDays,
                [bool]$FilterIncludeExpired
            )

            $results = [System.Collections.Generic.List[hashtable]]::new()

            $tsNow = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'

            # ── 1. Verify W3SVC / IIS installed ───────────────────────────────
            try {
                $null = Get-Service -Name 'W3SVC' -ErrorAction Stop
            }
            catch {
                $results.Add(@{
                    ComputerName            = $env:COMPUTERNAME
                    SiteName                = $null; SiteId = $null; SiteState = $null
                    BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                    Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                    Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                    Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                    NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                    SubjectAlternativeNames = @()
                    SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                    HasPrivateKey           = $null; FriendlyName = $null
                    Status                  = 'IISNotInstalled'
                    ErrorMessage            = "W3SVC service not found: $($_.Exception.Message)"
                    Timestamp               = $tsNow
                })
                return $results
            }

            # ── 2. Detect IIS module chain ─────────────────────────────────────
            $iisModule = $null
            if (Get-Module -Name 'WebAdministration' -ListAvailable -ErrorAction SilentlyContinue) {
                $iisModule = 'WebAdministration'
            }
            elseif (Get-Module -Name 'IISAdministration' -ListAvailable -ErrorAction SilentlyContinue) {
                $iisModule = 'IISAdministration'
            }

            # ── 3. Collect raw binding descriptors ────────────────────────────
            $bindingRows = [System.Collections.Generic.List[hashtable]]::new()

            if ($iisModule -eq 'WebAdministration') {
                try {
                    Import-Module -Name 'WebAdministration' -ErrorAction Stop

                    $allSites = @(Get-Website -ErrorAction Stop)
                    if ($FilterSiteName) {
                        $allSites = @($allSites | Where-Object {
                            $sn = $_.Name
                            $null -ne ($FilterSiteName | Where-Object { $sn -like $_ })
                        })
                    }

                    foreach ($site in $allSites) {
                        $httpsBindings = @(Get-WebBinding -Name $site.Name -Protocol 'https' -ErrorAction SilentlyContinue)
                        if ($httpsBindings.Count -eq 0) {
                            if ($FilterSiteName) {
                                $bindingRows.Add(@{
                                    SiteNameVal    = $site.Name
                                    SiteIdVal      = [int]$site.Id
                                    SiteStateVal   = $site.State
                                    BindingInfoVal = ''
                                    SslFlagsVal    = 0
                                    ThumbprintVal  = $null
                                    StoreNameVal   = $null
                                    StatusVal      = 'BindingNotFound'
                                    ErrorVal       = "No https bindings on site '$($site.Name)'."
                                })
                            }
                            continue
                        }
                        foreach ($b in $httpsBindings) {
                            $sslHash  = $b.certificateHash
                            $sslStore = if ($b.certificateStoreName) { $b.certificateStoreName } else { 'My' }
                            $bindingRows.Add(@{
                                SiteNameVal    = $site.Name
                                SiteIdVal      = [int]$site.Id
                                SiteStateVal   = $site.State
                                BindingInfoVal = $b.bindingInformation
                                SslFlagsVal    = [int]$b.sslFlags
                                ThumbprintVal  = $sslHash
                                StoreNameVal   = $sslStore
                                StatusVal      = if ($sslHash) { 'Resolved' } else { 'CertNotFound' }
                                ErrorVal       = if (-not $sslHash) { 'Binding has no certificateHash (unbound https listener).' } else { $null }
                            })
                        }
                    }
                }
                catch {
                    $results.Add(@{
                        ComputerName            = $env:COMPUTERNAME
                        SiteName                = $null; SiteId = $null; SiteState = $null
                        BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                        Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                        Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                        Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                        NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                        SubjectAlternativeNames = @()
                        SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                        HasPrivateKey           = $null; FriendlyName = $null
                        Status                  = 'Failed'
                        ErrorMessage            = "WebAdministration error: $($_.Exception.Message)"
                        Timestamp               = $tsNow
                    })
                    return $results
                }
            }
            elseif ($iisModule -eq 'IISAdministration') {
                try {
                    Import-Module -Name 'IISAdministration' -ErrorAction Stop
                    $mgr = [Microsoft.Web.Administration.ServerManager]::OpenRemote('localhost')
                    $allSites = @($mgr.Sites)
                    if ($FilterSiteName) {
                        $allSites = @($allSites | Where-Object {
                            $sn = $_.Name
                            $null -ne ($FilterSiteName | Where-Object { $sn -like $_ })
                        })
                    }
                    foreach ($site in $allSites) {
                        $httpsBindings = @($site.Bindings | Where-Object { $_.Protocol -eq 'https' })
                        if ($httpsBindings.Count -eq 0) {
                            if ($FilterSiteName) {
                                $bindingRows.Add(@{
                                    SiteNameVal    = $site.Name
                                    SiteIdVal      = [long]$site.Id
                                    SiteStateVal   = $site.State.ToString()
                                    BindingInfoVal = ''
                                    SslFlagsVal    = 0
                                    ThumbprintVal  = $null
                                    StoreNameVal   = $null
                                    StatusVal      = 'BindingNotFound'
                                    ErrorVal       = "No https bindings on site '$($site.Name)'."
                                })
                            }
                            continue
                        }
                        foreach ($b in $httpsBindings) {
                            $rawHash  = [System.BitConverter]::ToString($b.CertificateHash) -replace '-', ''
                            $sslStore = if ($b.CertificateStoreName) { $b.CertificateStoreName } else { 'My' }
                            $bindingRows.Add(@{
                                SiteNameVal    = $site.Name
                                SiteIdVal      = [long]$site.Id
                                SiteStateVal   = $site.State.ToString()
                                BindingInfoVal = $b.BindingInformation
                                SslFlagsVal    = [int]$b.SslFlags
                                ThumbprintVal  = $rawHash
                                StoreNameVal   = $sslStore
                                StatusVal      = if ($rawHash) { 'Resolved' } else { 'CertNotFound' }
                                ErrorVal       = if (-not $rawHash) { 'Binding has no certificate hash (unbound https listener).' } else { $null }
                            })
                        }
                    }
                    $mgr.Dispose()
                }
                catch {
                    $results.Add(@{
                        ComputerName            = $env:COMPUTERNAME
                        SiteName                = $null; SiteId = $null; SiteState = $null
                        BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                        Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                        Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                        Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                        NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                        SubjectAlternativeNames = @()
                        SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                        HasPrivateKey           = $null; FriendlyName = $null
                        Status                  = 'Failed'
                        ErrorMessage            = "IISAdministration error: $($_.Exception.Message)"
                        Timestamp               = $tsNow
                    })
                    return $results
                }
            }
            else {
                # appcmd fallback
                $appcmdExe = Join-Path -Path $env:windir -ChildPath 'system32\inetsrv\appcmd.exe'
                if (-not (Test-Path -LiteralPath $appcmdExe -PathType Leaf)) {
                    $results.Add(@{
                        ComputerName            = $env:COMPUTERNAME
                        SiteName                = $null; SiteId = $null; SiteState = $null
                        BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                        Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                        Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                        Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                        NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                        SubjectAlternativeNames = @()
                        SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                        HasPrivateKey           = $null; FriendlyName = $null
                        Status                  = 'IISNotInstalled'
                        ErrorMessage            = 'Neither WebAdministration nor IISAdministration module is available, and appcmd.exe was not found.'
                        Timestamp               = $tsNow
                    })
                    return $results
                }
                try {
                    [xml]$sitesXml = & $appcmdExe list sites /config:* /xml 2>$null
                    foreach ($siteNode in $sitesXml.appcmd.SITE) {
                        $siteName  = $siteNode.'SITE.NAME'
                        $siteId    = [int]$siteNode.site.id
                        $siteState = $siteNode.'SITE.STATE'

                        if ($FilterSiteName) {
                            $matched = $FilterSiteName | Where-Object { $siteName -like $_ }
                            if (-not $matched) { continue }
                        }

                        $httpsNodes = @($siteNode.site.bindings.binding | Where-Object { $_.protocol -eq 'https' })
                        if ($httpsNodes.Count -eq 0) {
                            if ($FilterSiteName) {
                                $bindingRows.Add(@{
                                    SiteNameVal    = $siteName
                                    SiteIdVal      = $siteId
                                    SiteStateVal   = $siteState
                                    BindingInfoVal = ''
                                    SslFlagsVal    = 0
                                    ThumbprintVal  = $null
                                    StoreNameVal   = $null
                                    StatusVal      = 'BindingNotFound'
                                    ErrorVal       = "No https bindings on site '$siteName'."
                                })
                            }
                            continue
                        }
                        foreach ($bn in $httpsNodes) {
                            $sslHash  = $bn.certificateHash
                            $sslStore = if ($bn.certificateStoreName) { $bn.certificateStoreName } else { 'My' }
                            $sslFlags = if ($bn.sslFlags) { [int]$bn.sslFlags } else { 0 }
                            $bindingRows.Add(@{
                                SiteNameVal    = $siteName
                                SiteIdVal      = $siteId
                                SiteStateVal   = $siteState
                                BindingInfoVal = $bn.bindingInformation
                                SslFlagsVal    = $sslFlags
                                ThumbprintVal  = $sslHash
                                StoreNameVal   = $sslStore
                                StatusVal      = if ($sslHash) { 'Resolved' } else { 'CertNotFound' }
                                ErrorVal       = if (-not $sslHash) { 'Binding has no certificateHash (unbound https listener).' } else { $null }
                            })
                        }
                    }
                }
                catch {
                    $results.Add(@{
                        ComputerName            = $env:COMPUTERNAME
                        SiteName                = $null; SiteId = $null; SiteState = $null
                        BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                        Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                        Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                        Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                        NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                        SubjectAlternativeNames = @()
                        SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                        HasPrivateKey           = $null; FriendlyName = $null
                        Status                  = 'Failed'
                        ErrorMessage            = "appcmd fallback error: $($_.Exception.Message)"
                        Timestamp               = $tsNow
                    })
                    return $results
                }
            }

            # ── 4. No bindings at all ──────────────────────────────────────────
            if ($bindingRows.Count -eq 0) {
                $results.Add(@{
                    ComputerName            = $env:COMPUTERNAME
                    SiteName                = $null; SiteId = $null; SiteState = $null
                    BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                    Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                    Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                    Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                    NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                    SubjectAlternativeNames = @()
                    SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                    HasPrivateKey           = $null; FriendlyName = $null
                    Status                  = 'BindingNotFound'
                    ErrorMessage            = 'No https bindings found on this host.'
                    Timestamp               = $tsNow
                })
                return $results
            }

            # ── 5. Build full rows: parse binding + resolve cert + filter ──────
            foreach ($br in $bindingRows) {
                # Parse BindingInformation: ip:port:hostheader
                # Last colon-segment = hostheader, second-to-last = port,
                # everything before second-to-last joined = IP (safe for IPv6).
                $bindInfo  = $br.BindingInfoVal
                $bParts    = $bindInfo -split ':'
                $hostHeader = ''
                $portVal    = 0
                $ipAddress  = '*'
                if ($bParts.Count -ge 2) {
                    $hostHeader = $bParts[-1]
                    $portVal    = [int]$bParts[-2]
                    $ipRaw      = ($bParts[0..($bParts.Count - 3)]) -join ':'
                    $ipAddress  = if ([string]::IsNullOrEmpty($ipRaw)) { '*' } else { $ipRaw }
                }

                # HostHeader filter
                if ($FilterHostHeader) {
                    $hh      = $hostHeader
                    $matched = $FilterHostHeader | Where-Object { $hh -like $_ }
                    if (-not $matched) { continue }
                }

                # Port filter
                if ($FilterPort -and ($portVal -notin $FilterPort)) { continue }

                # Thumbprint filter (pre-resolution, fast path)
                if ($FilterThumbprint) {
                    $tpRaw   = if ($br.ThumbprintVal) { $br.ThumbprintVal.ToUpper() } else { '' }
                    $matched = $FilterThumbprint | Where-Object { $tpRaw -eq $_.ToUpper() }
                    if (-not $matched) { continue }
                }

                # Resolve X509 certificate
                $tp           = if ($br.ThumbprintVal) { $br.ThumbprintVal.ToUpper() } else { $null }
                $storeName    = $br.StoreNameVal
                $statusVal    = $br.StatusVal
                $errorVal     = $br.ErrorVal

                $certSubject   = $null; $certSubjectCN = $null
                $certIssuer    = $null; $certSerial    = $null
                $certNotBefore = $null; $certNotAfter  = $null
                $certDays      = $null; $certExpired   = $null
                $certSAN       = @()
                $certSigAlg    = $null; $certKeyAlg    = $null
                $certKeySize   = $null; $certHasPK     = $null
                $certFriendly  = $null

                if ($statusVal -eq 'Resolved' -and $tp -and $storeName) {
                    $storePath = "Cert:\LocalMachine\$storeName"
                    $cert      = $null
                    try {
                        $cert = Get-ChildItem -LiteralPath $storePath -ErrorAction Stop |
                            Where-Object { $_.Thumbprint -eq $tp } |
                            Select-Object -First 1
                    }
                    catch {
                        Write-Verbose -Message "[Get-IISCertificateBinding] Could not open store '$storePath': $($_.Exception.Message)"
                    }

                    if ($null -eq $cert) {
                        $statusVal = 'CertNotFound'
                        $errorVal  = "Certificate with thumbprint '$tp' not found in '$storePath'."
                    }
                    else {
                        $now           = Get-Date
                        $certDays      = [int][math]::Floor(($cert.NotAfter - $now).TotalDays)
                        $certExpired   = ($cert.NotAfter -lt $now)
                        $certSubject   = $cert.Subject
                        $certIssuer    = $cert.Issuer
                        $certSerial    = $cert.SerialNumber.ToUpper()
                        $certNotBefore = $cert.NotBefore
                        $certNotAfter  = $cert.NotAfter
                        $certHasPK     = $cert.HasPrivateKey
                        $certFriendly  = $cert.FriendlyName
                        $certSigAlg    = $cert.SignatureAlgorithm.FriendlyName

                        # SubjectCN extraction
                        $cnMatch = [regex]::Match($cert.Subject, '(?:^|,\s*)CN=([^,]+)')
                        if ($cnMatch.Success) {
                            $certSubjectCN = $cnMatch.Groups[1].Value.Trim()
                        }

                        # KeyAlgorithm and KeySize
                        try {
                            $certKeyAlg = $cert.PublicKey.Oid.FriendlyName
                            $rsaKey     = $cert.GetRSAPublicKey()
                            if ($null -ne $rsaKey) {
                                $certKeySize = $rsaKey.KeySize
                            }
                            else {
                                $ecKey = $cert.GetECDsaPublicKey()
                                if ($null -ne $ecKey) {
                                    $certKeySize = $ecKey.KeySize
                                }
                            }
                        }
                        catch {
                            Write-Verbose -Message "[Get-IISCertificateBinding] Key size resolution failed for '$tp': $($_.Exception.Message)"
                        }

                        # SAN (OID 2.5.29.17)
                        $sanExt = $cert.Extensions |
                            Where-Object { $_.Oid.Value -eq '2.5.29.17' } |
                            Select-Object -First 1
                        if ($null -ne $sanExt) {
                            $sanFormatted = $sanExt.Format($false)
                            if (-not [string]::IsNullOrWhiteSpace($sanFormatted)) {
                                $certSAN = @($sanFormatted -split ',\s*' |
                                    ForEach-Object { $_ -replace '^(DNS Name|IP Address)=', '' } |
                                    Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
                            }
                        }
                    }
                }

                # ExpiringInDays / IncludeExpired filters
                if ($null -ne $FilterExpiringInDays) {
                    if ($null -eq $certDays) { continue }
                    if ($certDays -gt $FilterExpiringInDays) { continue }
                }
                elseif ($FilterIncludeExpired) {
                    if ($certExpired -ne $true) { continue }
                }

                $results.Add(@{
                    ComputerName            = $env:COMPUTERNAME
                    SiteName                = $br.SiteNameVal
                    SiteId                  = $br.SiteIdVal
                    SiteState               = $br.SiteStateVal
                    BindingInformation      = $bindInfo
                    IPAddress               = $ipAddress
                    Port                    = $portVal
                    HostHeader              = $hostHeader
                    Protocol                = 'https'
                    SslFlags                = $br.SslFlagsVal
                    SniEnabled              = (($br.SslFlagsVal -band 1) -eq 1)
                    CentralCertStore        = (($br.SslFlagsVal -band 2) -eq 2)
                    Thumbprint              = $tp
                    CertStoreLocation       = if ($storeName) { "Cert:\LocalMachine\$storeName" } else { $null }
                    CertStoreName           = $storeName
                    Subject                 = $certSubject
                    SubjectCN               = $certSubjectCN
                    Issuer                  = $certIssuer
                    SerialNumber            = $certSerial
                    NotBefore               = $certNotBefore
                    NotAfter                = $certNotAfter
                    DaysUntilExpiration     = $certDays
                    Expired                 = $certExpired
                    SubjectAlternativeNames = $certSAN
                    SignatureAlgorithm      = $certSigAlg
                    KeyAlgorithm            = $certKeyAlg
                    KeySize                 = $certKeySize
                    HasPrivateKey           = $certHasPK
                    FriendlyName            = $certFriendly
                    Status                  = $statusVal
                    ErrorMessage            = $errorVal
                    Timestamp               = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                })
            }

            # ── 6. No rows survived filters ────────────────────────────────────
            if ($results.Count -eq 0) {
                $results.Add(@{
                    ComputerName            = $env:COMPUTERNAME
                    SiteName                = $null; SiteId = $null; SiteState = $null
                    BindingInformation      = $null; IPAddress = $null; Port = $null; HostHeader = $null
                    Protocol                = $null; SslFlags = $null; SniEnabled = $null; CentralCertStore = $null
                    Thumbprint              = $null; CertStoreLocation = $null; CertStoreName = $null
                    Subject                 = $null; SubjectCN = $null; Issuer = $null; SerialNumber = $null
                    NotBefore               = $null; NotAfter = $null; DaysUntilExpiration = $null; Expired = $null
                    SubjectAlternativeNames = @()
                    SignatureAlgorithm      = $null; KeyAlgorithm = $null; KeySize = $null
                    HasPrivateKey           = $null; FriendlyName = $null
                    Status                  = 'BindingNotFound'
                    ErrorMessage            = 'No https bindings matched the specified filters on this host.'
                    Timestamp               = $tsNow
                })
            }

            return $results
        }
    }

    process {
        foreach ($cn in $ComputerName) {
            $filterDays   = if ($PSBoundParameters.ContainsKey('ExpiringInDays')) { $ExpiringInDays } else { $null }
            $invokeParams = @{
                ComputerName = $cn
                ScriptBlock  = $scriptBlock
                ArgumentList = @(
                    $SiteName,
                    $Thumbprint,
                    $HostHeader,
                    $Port,
                    $filterDays,
                    $IncludeExpired.IsPresent
                )
            }
            if ($Credential) {
                $invokeParams['Credential'] = $Credential
            }

            try {
                $rawResults = Invoke-RemoteOrLocal @invokeParams
                foreach ($r in $rawResults) {
                    [PSCustomObject]([ordered]@{
                        PSTypeName              = 'PSWinOps.IISCertificateBinding'
                        ComputerName            = $r.ComputerName
                        SiteName                = $r.SiteName
                        SiteId                  = $r.SiteId
                        SiteState               = $r.SiteState
                        BindingInformation      = $r.BindingInformation
                        IPAddress               = $r.IPAddress
                        Port                    = $r.Port
                        HostHeader              = $r.HostHeader
                        Protocol                = $r.Protocol
                        SslFlags                = $r.SslFlags
                        SniEnabled              = $r.SniEnabled
                        CentralCertStore        = $r.CentralCertStore
                        Thumbprint              = $r.Thumbprint
                        CertStoreLocation       = $r.CertStoreLocation
                        CertStoreName           = $r.CertStoreName
                        Subject                 = $r.Subject
                        SubjectCN               = $r.SubjectCN
                        Issuer                  = $r.Issuer
                        SerialNumber            = $r.SerialNumber
                        NotBefore               = $r.NotBefore
                        NotAfter                = $r.NotAfter
                        DaysUntilExpiration     = $r.DaysUntilExpiration
                        Expired                 = $r.Expired
                        SubjectAlternativeNames = $r.SubjectAlternativeNames
                        SignatureAlgorithm      = $r.SignatureAlgorithm
                        KeyAlgorithm            = $r.KeyAlgorithm
                        KeySize                 = $r.KeySize
                        HasPrivateKey           = $r.HasPrivateKey
                        FriendlyName            = $r.FriendlyName
                        Status                  = $r.Status
                        ErrorMessage            = $r.ErrorMessage
                        Timestamp               = $r.Timestamp
                    })
                }
            }
            catch {
                Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '$cn': $_"
            }
        }
    }

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