Public/Get-CwCaHealth.ps1

function Get-CwCaHealth {
    <#
    .SYNOPSIS
        Collects ADCS CA health: CertSvc status, CA type and lifetime,
        pending/failed request backlog and database size.
    .DESCRIPTION
        Read-only. Detects a local ADCS installation via the CertSvc
        configuration registry hive. Backlog figures come from PSPKI when the
        module is available, otherwise via certutil restrictions (best effort:
        values are $null when neither source works). Always emits one object;
        IsCa=$false on machines without ADCS.
    .PARAMETER FailedLookbackHours
        Time window for the failed-request counter. Default 24.
    .PARAMETER WarnDays
        Warning threshold for the CA certificate lifetime. Default 180.
    .PARAMETER CritDays
        Critical threshold for the CA certificate lifetime. Default 30.
    .PARAMETER PendingWarnCount
        Pending requests at or above this count raise WARNING. Default 25.
    .EXAMPLE
        Get-CwCaHealth | Format-List
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [int]$FailedLookbackHours = 24,
        [int]$WarnDays = 180,
        [int]$CritDays = 30,
        [int]$PendingWarnCount = 25
    )

    $configKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration'
    if (-not (Test-Path -Path $configKey)) {
        return [pscustomobject]@{
            PSTypeName = 'CertWatchtower.CaHealth'
            IsCa       = $false
            CaName     = $null
            CaType     = $null
            ServiceStatus = $null
            CaCertNotAfter = $null
            CaCertDaysRemaining = $null
            PendingRequests = $null
            FailedRequests24h = $null
            DbSizeMB   = $null
            ActiveTemplateCount = $null
            Severity   = 'OK'
        }
    }

    $caName = (Get-ItemProperty -Path $configKey -ErrorAction SilentlyContinue).Active
    $caKey = Join-Path $configKey $caName
    $caProps = Get-ItemProperty -Path $caKey -ErrorAction SilentlyContinue

    # CAType registry value: 0=EnterpriseRoot 1=EnterpriseSub 3=StandaloneRoot 4=StandaloneSub
    $caType = switch ($caProps.CAType) {
        0 { 'EnterpriseRoot' }
        1 { 'EnterpriseSub' }
        3 { 'StandaloneRoot' }
        4 { 'StandaloneSub' }
        default { "Unknown($($caProps.CAType))" }
    }

    $serviceStatus = try { (Get-Service -Name CertSvc -ErrorAction Stop).Status.ToString() } catch { 'NotFound' }

    # CA certificate: find it in LocalMachine\My by subject = CA common name
    $caCert = Get-ChildItem -Path Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
        Where-Object { $_.Subject -match [regex]::Escape($caName) } |
        Sort-Object NotAfter -Descending | Select-Object -First 1
    $caCertNotAfter = $caCert.NotAfter
    $caCertDays = if ($caCertNotAfter) { [math]::Round(($caCertNotAfter - (Get-Date)).TotalDays, 1) } else { $null }

    # backlog: PSPKI preferred, certutil fallback
    $pending = $null; $failed = $null
    if (Get-Module -ListAvailable -Name PSPKI) {
        try {
            Import-Module PSPKI -ErrorAction Stop
            $ca = Get-CertificationAuthority -ComputerName $env:COMPUTERNAME -ErrorAction Stop
            $pending = @($ca | Get-PendingRequest -ErrorAction Stop).Count
            $failed = @($ca | Get-FailedRequest -Filter "Request.SubmittedWhen -ge $((Get-Date).AddHours(-$FailedLookbackHours))" -ErrorAction Stop).Count
        }
        catch {
            Write-Verbose "PSPKI backlog query failed: $_"
        }
    }
    if ($null -eq $pending) {
        try {
            $out = & certutil.exe -view -restrict 'Disposition=9' -out 'RequestID' csv 2>$null
            if ($LASTEXITCODE -eq 0) { $pending = [math]::Max(0, @($out | Where-Object { $_ -match '^\d' }).Count) }
        }
        catch { Write-Verbose "certutil pending query failed: $_" }
    }
    if ($null -eq $failed) {
        try {
            $since = (Get-Date).AddHours(-$FailedLookbackHours).ToString('MM/dd/yyyy HH:mm', [cultureinfo]::InvariantCulture)
            $out = & certutil.exe -view -restrict "Disposition=30,Request.SubmittedWhen>=$since" -out 'RequestID' csv 2>$null
            if ($LASTEXITCODE -eq 0) { $failed = [math]::Max(0, @($out | Where-Object { $_ -match '^\d' }).Count) }
        }
        catch { Write-Verbose "certutil failed-request query failed: $_" }
    }

    # DB size
    $dbSizeMb = $null
    if ($caProps.DBDirectory -and (Test-Path -Path $caProps.DBDirectory)) {
        $edb = Get-ChildItem -Path $caProps.DBDirectory -Filter '*.edb' -ErrorAction SilentlyContinue
        if ($edb) { $dbSizeMb = [math]::Round((($edb | Measure-Object Length -Sum).Sum) / 1MB, 1) }
    }

    # active templates (rough hygiene indicator, Enterprise CAs only)
    $templateCount = $null
    if ($caProps.PSObject.Properties['Templates'] -and $caProps.Templates) {
        $templateCount = @($caProps.Templates | Where-Object { $_ }).Count
    }

    # severity: stopped service or CA cert below crit threshold => CRITICAL
    $severity = 'OK'
    if ($null -ne $caCertDays) {
        $severity = ConvertTo-CwSeverity -DaysRemaining $caCertDays -WarnDays $WarnDays -CritDays $CritDays
    }
    if ($null -ne $pending -and $pending -ge $PendingWarnCount -and $severity -eq 'OK') { $severity = 'WARNING' }
    if ($serviceStatus -ne 'Running') { $severity = 'CRITICAL' }

    [pscustomobject]@{
        PSTypeName          = 'CertWatchtower.CaHealth'
        IsCa                = $true
        CaName              = $caName
        CaType              = $caType
        ServiceStatus       = $serviceStatus
        CaCertNotAfter      = $caCertNotAfter
        CaCertDaysRemaining = $caCertDays
        PendingRequests     = $pending
        FailedRequests24h   = $failed
        DbSizeMB            = $dbSizeMb
        ActiveTemplateCount = $templateCount
        Severity            = $severity
    }
}