Public/healthcheck/Get-CertificateAuthorityHealth.ps1
|
#Requires -Version 5.1 function Get-CertificateAuthorityHealth { <# .SYNOPSIS Checks Active Directory Certificate Services health on CA servers .DESCRIPTION Performs comprehensive health checks on AD CS Certificate Authority servers. Validates CertSvc service status, CA certificate expiration, CRL publication, and CA responsiveness via certutil diagnostics. Returns one typed object per server suitable for List view display. .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. .EXAMPLE Get-CertificateAuthorityHealth Checks AD CS health on the local computer. .EXAMPLE Get-CertificateAuthorityHealth -ComputerName 'CA01' Checks AD CS health on a single remote CA server. .EXAMPLE 'CA01', 'CA02' | Get-CertificateAuthorityHealth -Credential (Get-Credential) Checks AD CS health on multiple remote CA servers via pipeline. .OUTPUTS PSWinOps.CertificateAuthorityHealth Returns one object per server with service status, CA name, CA type, certificate expiry, CRL publish status, ping status, and overall health. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-03-26 Requires: PowerShell 5.1+ / Windows only Requires: AD-Certificate role (ADCS-Cert-Authority) .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/windows-server/identity/ad-cs/ #> [CmdletBinding()] [OutputType('PSWinOps.CertificateAuthorityHealth')] 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 = [System.Management.Automation.PSCredential]::Empty ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" $localNames = @($env:COMPUTERNAME, 'localhost', '.') $scriptBlock = { $data = @{ ServiceStatus = 'NotFound' CAName = 'Unknown' CAType = 'Unknown' CACertExpiry = 'Unknown' CACertDaysRemaining = -1 CRLPublishOK = $false CAPingOK = $false } # 1. Check CertSvc service try { $certSvc = Get-Service -Name 'CertSvc' -ErrorAction Stop $data.ServiceStatus = $certSvc.Status.ToString() } catch { $data.ServiceStatus = 'NotFound' } # 2. Verify certutil.exe availability $certutilAvailable = $null -ne (Get-Command -Name 'certutil.exe' -ErrorAction SilentlyContinue) if (-not $certutilAvailable) { return $data } # 3. certutil -CAInfo : CA name, CA type, cert expiry try { $caInfoOutput = & certutil.exe -CAInfo 2>&1 if ($LASTEXITCODE -eq 0 -and $null -ne $caInfoOutput) { $caInfoLines = $caInfoOutput | ForEach-Object -Process { $_.ToString() } foreach ($line in $caInfoLines) { if ($line -match '^\s*CA\s+name:\s*(.+)') { $data.CAName = $Matches[1].Trim() } if ($line -match '^\s*CA\s+type:\s*\d+\s*-\s*(.+)') { $data.CAType = $Matches[1].Trim() } elseif ($line -match '^\s*CA\s+type:\s*(.+)') { $data.CAType = $Matches[1].Trim() } } # Parse NotAfter from CA cert[0] $inCert0 = $false foreach ($line in $caInfoLines) { if ($line -match 'CA\s+cert\[0\]') { $inCert0 = $true continue } if ($inCert0 -and $line -match 'CA\s+cert\[\d+\]') { break } if ($inCert0 -and $line -match 'NotAfter:\s*(.+)') { $expiryString = $Matches[1].Trim() $data.CACertExpiry = $expiryString try { $expiryDate = [datetime]::Parse($expiryString) $data.CACertDaysRemaining = ($expiryDate - (Get-Date)).Days } catch { $data.CACertDaysRemaining = -1 } break } } } } catch { Write-Verbose -Message "certutil -CAInfo failed: $_" } # 4. certutil -CRL : CRL publication check try { $crlOutput = & certutil.exe -CRL 2>&1 if ($LASTEXITCODE -eq 0) { $crlText = ($crlOutput | ForEach-Object -Process { $_.ToString() }) -join "`n" if ($crlText -notmatch '(?i)error') { $data.CRLPublishOK = $true } } } catch { $data.CRLPublishOK = $false } # 5. certutil -ping : CA responsiveness try { $pingOutput = & certutil.exe -ping 2>&1 if ($LASTEXITCODE -eq 0) { $data.CAPingOK = $true } else { $pingText = ($pingOutput | ForEach-Object -Process { $_.ToString() }) -join "`n" if ($pingText -match '(?i)successfully') { $data.CAPingOK = $true } } } catch { $data.CAPingOK = $false } return $data } } process { foreach ($machine in $ComputerName) { $displayName = $machine.ToUpper() Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying '${machine}'" try { $isLocal = $localNames -contains $machine if ($isLocal) { $result = & $scriptBlock } else { $invokeParams = @{ ComputerName = $machine ScriptBlock = $scriptBlock ErrorAction = 'Stop' } if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $invokeParams['Credential'] = $Credential } $result = Invoke-Command @invokeParams } # Compute OverallHealth $serviceNotFound = ($result.ServiceStatus -eq 'NotFound') $certutilMissing = ( $result.CAName -eq 'Unknown' -and $result.CACertExpiry -eq 'Unknown' -and $result.CACertDaysRemaining -eq -1 -and -not $result.CRLPublishOK -and -not $result.CAPingOK ) if ($serviceNotFound -and $certutilMissing) { $healthStatus = 'RoleUnavailable' } elseif ($result.ServiceStatus -ne 'Running' -or ($result.CACertDaysRemaining -ne -1 -and $result.CACertDaysRemaining -le 0) -or -not $result.CAPingOK) { $healthStatus = 'Critical' } elseif (($result.CACertDaysRemaining -ne -1 -and $result.CACertDaysRemaining -lt 30) -or -not $result.CRLPublishOK) { $healthStatus = 'Degraded' } else { $healthStatus = 'Healthy' } [PSCustomObject]@{ PSTypeName = 'PSWinOps.CertificateAuthorityHealth' ComputerName = $displayName ServiceName = 'CertSvc' ServiceStatus = [string]$result.ServiceStatus CAName = [string]$result.CAName CAType = [string]$result.CAType CACertExpiry = [string]$result.CACertExpiry CACertDaysRemaining = [int]$result.CACertDaysRemaining CRLPublishOK = [bool]$result.CRLPublishOK CAPingOK = [bool]$result.CAPingOK OverallHealth = $healthStatus Timestamp = Get-Date -Format 'o' } } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed on '${machine}': $_" continue } } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |