Public/Get-CwCertInventory.ps1

function Get-CwCertInventory {
    <#
    .SYNOPSIS
        Reads local certificate stores and normalizes certificates into the
        cert-watchtower model including role classification.
    .DESCRIPTION
        Strictly read-only scan of the given stores (default LocalMachine\My).
        Each certificate is normalized (subject, thumbprint, validity, EKUs,
        template, SANs) and classified into an operational role
        (CA/LDAPS/RDP/IIS/Client/CodeSigning/Other). Severity is derived from
        the remaining lifetime; CA certificates get their own (longer)
        thresholds.
    .PARAMETER StoreName
        One or more store names under the store location. Default: My.
    .PARAMETER StoreLocation
        LocalMachine (default) or CurrentUser.
    .PARAMETER WarnDays
        Warning threshold in days for non-CA certificates. Default 30.
    .PARAMETER CritDays
        Critical threshold in days for non-CA certificates. Default 7.
    .PARAMETER CaWarnDays
        Warning threshold for CA certificates. Default 180.
    .PARAMETER CaCritDays
        Critical threshold for CA certificates. Default 30.
    .EXAMPLE
        Get-CwCertInventory -WarnDays 30 -CritDays 7 | Where-Object Severity -ne 'OK'
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [string[]]$StoreName = @('My'),

        [ValidateSet('LocalMachine', 'CurrentUser')]
        [string]$StoreLocation = 'LocalMachine',

        [int]$WarnDays = 30,
        [int]$CritDays = 7,
        [int]$CaWarnDays = 180,
        [int]$CaCritDays = 30
    )

    $isDc = $false
    try {
        $isDc = (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop).DomainRole -ge 4
    }
    catch {
        Write-Verbose "Could not determine domain role: $_"
    }

    $now = Get-Date
    foreach ($store in $StoreName) {
        $path = "Cert:\$StoreLocation\$store"
        if (-not (Test-Path -Path $path)) {
            Write-Warning "Certificate store '$path' not found - skipping."
            continue
        }

        foreach ($cert in (Get-ChildItem -Path $path)) {
            if ($cert -isnot [System.Security.Cryptography.X509Certificates.X509Certificate2]) { continue }

            $ekuOids = @()
            $isCa = $false
            $template = ''
            $sanNames = @()

            foreach ($ext in $cert.Extensions) {
                switch ($ext.Oid.Value) {
                    '2.5.29.37' { $ekuOids = @($ext.EnhancedKeyUsages | ForEach-Object Value) }
                    '2.5.29.19' { $isCa = $ext.CertificateAuthority }
                    '1.3.6.1.4.1.311.21.7' {
                        # msPKI-Certificate-Template: Format() text contains 'Template=<name>(...)'
                        if ($ext.Format($false) -match '(?:Template|Vorlage)\s*=\s*([^,(\r\n]+)') { $template = $Matches[1].Trim() }
                    }
                    '1.3.6.1.4.1.311.20.2' {
                        # legacy V1 certificate template name (BMPString)
                        if (-not $template) { $template = ($ext.Format($false)).Trim() }
                    }
                    '2.5.29.17' {
                        $sanNames = @([regex]::Matches($ext.Format($false), 'DNS[ -]?Name\s*=\s*([^,\s]+)') |
                            ForEach-Object { $_.Groups[1].Value })
                    }
                }
            }

            $role = Get-CwCertRole -EkuOids $ekuOids -IsCa $isCa -IsDomainController $isDc -TemplateName $template
            $daysRemaining = [math]::Round(($cert.NotAfter - $now).TotalDays, 2)

            $sevParams = if ($role -eq 'CA') { @{ WarnDays = $CaWarnDays; CritDays = $CaCritDays } }
            else { @{ WarnDays = $WarnDays; CritDays = $CritDays } }

            [pscustomobject]@{
                PSTypeName    = 'CertWatchtower.Certificate'
                Subject       = $cert.Subject
                Issuer        = $cert.Issuer
                Thumbprint    = $cert.Thumbprint
                SerialNumber  = $cert.SerialNumber
                NotBefore     = $cert.NotBefore
                NotAfter      = $cert.NotAfter
                DaysRemaining = $daysRemaining
                HasPrivateKey = $cert.HasPrivateKey
                Role          = $role
                Template      = $template
                EkuOids       = $ekuOids
                SanDnsNames   = $sanNames
                IsCa          = $isCa
                Store         = "$StoreLocation\$store"
                Severity      = ConvertTo-CwSeverity -DaysRemaining $daysRemaining @sevParams
            }
        }
    }
}