Private/Invoke-InfraPulseCertificateCheck.ps1
|
function Invoke-InfraPulseCertificateCheck { [CmdletBinding()] param( [Parameter(Mandatory)] [psobject]$Context, [Parameter(Mandatory)] [System.Collections.IDictionary]$Settings ) $storePaths = @($Settings.StorePaths) if ($storePaths.Count -eq 0) { return New-InfraPulseResult -Status 'Skipped' -CheckName 'Certificates' -Category 'Security' -ComputerName $Context.ComputerName -Target 'Certificate stores' -Message 'No certificate stores are configured.' -Recommendation 'Add LocalMachine certificate-provider paths under Checks.Certificates.StorePaths.' } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() $scriptBlock = { param($CheckSettings) if ($env:OS -ne 'Windows_NT') { throw 'The Certificates check requires a Windows target.' } foreach ($storePath in @($CheckSettings.StorePaths)) { if (-not (Test-Path -LiteralPath $storePath)) { [pscustomobject]@{ RecordType = 'Store' StorePath = [string]$storePath Exists = $false } continue } [pscustomobject]@{ RecordType = 'Store' StorePath = [string]$storePath Exists = $true } foreach ($certificate in @(Get-ChildItem -LiteralPath $storePath -ErrorAction Stop)) { if ($certificate -isnot [System.Security.Cryptography.X509Certificates.X509Certificate2]) { continue } if ([bool]$CheckSettings.RequirePrivateKey -and -not $certificate.HasPrivateKey) { continue } if ([string]$certificate.Thumbprint -in @($CheckSettings.ThumbprintExclude)) { continue } $excluded = $false foreach ($pattern in @($CheckSettings.SubjectExcludePatterns)) { if ([string]$certificate.Subject -like [string]$pattern) { $excluded = $true break } } if ($excluded) { continue } foreach ($pattern in @($CheckSettings.IssuerExcludePatterns)) { if ([string]$certificate.Issuer -like [string]$pattern) { $excluded = $true break } } if ($excluded) { continue } $totalLifetimeDays = (([datetime]$certificate.NotAfter) - ([datetime]$certificate.NotBefore)).TotalDays if ($totalLifetimeDays -lt [double]$CheckSettings.MinTotalLifetimeDays) { continue } [pscustomobject]@{ RecordType = 'Certificate' StorePath = [string]$storePath Subject = [string]$certificate.Subject Issuer = [string]$certificate.Issuer Thumbprint = [string]$certificate.Thumbprint NotBefore = [datetime]$certificate.NotBefore NotAfter = [datetime]$certificate.NotAfter DaysRemaining = [math]::Round((([datetime]$certificate.NotAfter) - (Get-Date)).TotalDays, 2) HasPrivateKey = [bool]$certificate.HasPrivateKey SerialNumber = [string]$certificate.SerialNumber } } } } $raw = @(Invoke-InfraPulseCommand -Context $Context -ScriptBlock $scriptBlock -ArgumentList @($Settings)) $stopwatch.Stop() $stores = @($raw | Where-Object { $_.RecordType -eq 'Store' }) $certificates = @($raw | Where-Object { $_.RecordType -eq 'Certificate' }) $existingStores = @($stores | Where-Object { $_.Exists }).Count $missingStores = @($stores | Where-Object { -not $_.Exists } | ForEach-Object { $_.StorePath }) if ($existingStores -eq 0) { return New-InfraPulseResult -Status 'Unknown' -CheckName 'Certificates' -Category 'Security' -ComputerName $Context.ComputerName -Target 'Certificate stores' -Message 'None of the configured certificate stores exist on the target.' -Recommendation 'Review Checks.Certificates.StorePaths for the target role.' -Evidence ([ordered]@{ MissingStores = $missingStores }) -DurationMs $stopwatch.Elapsed.TotalMilliseconds } $results = @() $healthyCertificates = @() foreach ($missingStore in $missingStores) { $results += New-InfraPulseResult -Status 'Unknown' -CheckName 'Certificates' -Category 'Security' -ComputerName $Context.ComputerName -Target ([string]$missingStore) -Message "Configured certificate store '$missingStore' does not exist on the target." -ObservedValue 'Missing' -Recommendation 'Remove the store from Checks.Certificates.StorePaths or confirm the target role provisions it.' -Evidence ([ordered]@{ StorePath = [string]$missingStore; Exists = $false }) -DurationMs ($stopwatch.Elapsed.TotalMilliseconds / [math]::Max($storePaths.Count, 1)) } $rotatingCertificates = 0 foreach ($certificate in $certificates) { $daysRemaining = [double]$certificate.DaysRemaining $totalLifetimeDays = [math]::Round((([datetime]$certificate.NotAfter) - ([datetime]$certificate.NotBefore)).TotalDays, 2) # A certificate that never lives longer than the warning window cannot # satisfy the expiry policy by construction; by default it is treated # as auto-rotating, and only a missed rotation (expiry) is alarming. $isRotating = [bool]$Settings.TreatShortLivedAsRotating -and $totalLifetimeDays -le [double]$Settings.WarningDays if ($isRotating) { $rotatingCertificates++ } if ($daysRemaining -lt 0) { $status = 'Critical' if ($isRotating) { $message = "Auto-rotating certificate expired $([math]::Abs([math]::Round($daysRemaining, 0))) day(s) ago on $(([datetime]$certificate.NotAfter).ToString('yyyy-MM-dd')); the automatic rotation appears broken." $recommendation = 'Investigate why the issuing agent or service stopped renewing this automatically rotated certificate.' } else { $message = "Certificate expired $([math]::Abs([math]::Round($daysRemaining, 0))) day(s) ago on $(([datetime]$certificate.NotAfter).ToString('yyyy-MM-dd'))." $recommendation = 'Replace or remove the expired certificate after confirming its bindings and dependent services.' } } elseif ($isRotating) { $status = 'Healthy' $message = "Auto-rotating certificate (total lifetime $([math]::Round($totalLifetimeDays, 0)) day(s)) is valid until $(([datetime]$certificate.NotAfter).ToString('yyyy-MM-dd'))." $recommendation = '' $healthyCertificates += $certificate } elseif ($daysRemaining -le [double]$Settings.CriticalDays) { $status = 'Critical' $message = "Certificate expires in $([math]::Round($daysRemaining, 0)) day(s) on $(([datetime]$certificate.NotAfter).ToString('yyyy-MM-dd'))." $recommendation = 'Renew and deploy the certificate immediately, then validate all bindings and trust chains.' } elseif ($daysRemaining -le [double]$Settings.WarningDays) { $status = 'Warning' $message = "Certificate expires in $([math]::Round($daysRemaining, 0)) day(s) on $(([datetime]$certificate.NotAfter).ToString('yyyy-MM-dd'))." $recommendation = 'Begin the renewal process and identify every service or endpoint using this certificate.' } else { $healthyCertificates += $certificate continue } $thumbprint = [string]$certificate.Thumbprint $shortThumbprint = if ($thumbprint.Length -gt 12) { $thumbprint.Substring($thumbprint.Length - 12) } else { $thumbprint } $target = if ([string]::IsNullOrWhiteSpace([string]$certificate.Subject)) { $shortThumbprint } else { '{0} [{1}]' -f [string]$certificate.Subject, $shortThumbprint } $evidence = [ordered]@{ Subject = [string]$certificate.Subject Issuer = [string]$certificate.Issuer Thumbprint = $thumbprint SerialNumber = [string]$certificate.SerialNumber StorePath = [string]$certificate.StorePath NotBefore = [datetime]$certificate.NotBefore NotAfter = [datetime]$certificate.NotAfter DaysRemaining = $daysRemaining TotalLifetimeDays = $totalLifetimeDays Rotating = $isRotating HasPrivateKey = [bool]$certificate.HasPrivateKey } $results += New-InfraPulseResult -Status $status -CheckName 'Certificates' -Category 'Security' -ComputerName $Context.ComputerName -Target $target -Message $message -ObservedValue ("{0:N2} days" -f $daysRemaining) -WarningThreshold ("<= {0} days" -f $Settings.WarningDays) -CriticalThreshold ("<= {0} days or expired" -f $Settings.CriticalDays) -Recommendation $recommendation -Evidence $evidence -DurationMs ($stopwatch.Elapsed.TotalMilliseconds / [math]::Max($certificates.Count, 1)) } $summaryMessage = if ($certificates.Count -eq 0) { 'No certificates matched the configured filters in the available stores.' } else { "$($healthyCertificates.Count) of $($certificates.Count) certificate(s) satisfy the expiry policy." } $summaryEvidence = [ordered]@{ TotalCertificates = $certificates.Count HealthyCertificates = $healthyCertificates.Count RotatingCertificates = $rotatingCertificates ExistingStores = @($stores | Where-Object { $_.Exists } | ForEach-Object { $_.StorePath }) MissingStores = $missingStores } $results += New-InfraPulseResult -Status 'Healthy' -CheckName 'Certificates' -Category 'Security' -ComputerName $Context.ComputerName -Target 'Certificate inventory' -Message $summaryMessage -ObservedValue $healthyCertificates.Count -WarningThreshold ("Expiry <= {0} days" -f $Settings.WarningDays) -CriticalThreshold ("Expiry <= {0} days" -f $Settings.CriticalDays) -Evidence $summaryEvidence -DurationMs $stopwatch.Elapsed.TotalMilliseconds return $results } |