Vulnerability/Test-CertificateHealth.ps1
|
function Test-CertificateHealth { <# .SYNOPSIS Tests certificate health and identifies expiring or invalid certificates .DESCRIPTION Scans system certificates, identifies expired, expiring, weak, or invalid certificates across multiple certificate stores. Provides detailed findings with remediation steps for certificate management. .PARAMETER ExpiringDays Number of days to check for expiring certificates (default: 30) .PARAMETER IncludeUserStore Include current user certificate store in scan .PARAMETER CheckRevocation Check certificate revocation status (slower) .PARAMETER OutputPath Path to save certificate health report .PARAMETER Format Report format: HTML, JSON, CSV, or All .EXAMPLE Test-CertificateHealth Test-CertificateHealth -ExpiringDays 60 -IncludeUserStore -OutputPath "C:\Reports" .OUTPUTS PSCustomObject with certificate health findings #> [CmdletBinding()] param( [Parameter()] [int]$ExpiringDays = 30, [Parameter()] [switch]$IncludeUserStore, [Parameter()] [switch]$CheckRevocation, [Parameter()] [string]$OutputPath, [Parameter()] [ValidateSet('HTML', 'JSON', 'CSV', 'All')] [string]$Format = 'HTML' ) begin { Write-Host "=== CERTIFICATE HEALTH CHECK ===" -ForegroundColor Cyan Write-Host "Start Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Yellow Write-Host "Expiring Threshold: $ExpiringDays days" -ForegroundColor Yellow Write-Host "Include User Store: $(if($IncludeUserStore){'Yes'}else{'No'})" -ForegroundColor Yellow Write-Host "Check Revocation: $(if($CheckRevocation){'Yes'}else{'No'})" -ForegroundColor Yellow $results = [PSCustomObject]@{ ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" ComputerName = $env:COMPUTERNAME TotalCertificates = 0 HealthyCertificates = 0 Issues = @() Summary = @{ Expired = 0 ExpiringSoon = 0 SelfSigned = 0 WeakSignature = 0 Revoked = 0 InvalidChain = 0 DuplicateThumbprints = 0 } Recommendations = @() } # Weak signature algorithms $weakAlgorithms = @('md5', 'sha1') } process { try { # Define certificate stores to scan $storesToScan = @( @{Location='LocalMachine'; Stores=@('My', 'Root', 'CA', 'TrustedPublisher', 'WebHosting')} ) if ($IncludeUserStore) { $storesToScan += @{Location='CurrentUser'; Stores=@('My', 'Root', 'CA', 'TrustedPublisher')} } Write-Host "`n[1/3] Scanning certificate stores..." -ForegroundColor Cyan $allCertificates = @() $currentDate = Get-Date $expiringThreshold = $currentDate.AddDays($ExpiringDays) foreach ($storeConfig in $storesToScan) { $location = $storeConfig.Location foreach ($storeName in $storeConfig.Stores) { try { Write-Host " Scanning: $location\$storeName..." -ForegroundColor Gray $store = New-Object System.Security.Cryptography.X509Certificates.X509Store($storeName, $location) $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) $certificates = $store.Certificates foreach ($cert in $certificates) { $certInfo = [PSCustomObject]@{ Store = "$location\$storeName" Subject = $cert.Subject Issuer = $cert.Issuer Thumbprint = $cert.Thumbprint NotBefore = $cert.NotBefore NotAfter = $cert.NotAfter SignatureAlgorithm = $cert.SignatureAlgorithm.FriendlyName HasPrivateKey = $cert.HasPrivateKey IsSelfSigned = ($cert.Subject -eq $cert.Issuer) Status = 'Healthy' } $allCertificates += $certInfo } $store.Close() } catch { # FIXED: Remove colon from warning message Write-Warning "Could not access store $location\$storeName - $_" } } } $results.TotalCertificates = $allCertificates.Count Write-Host " Found $($allCertificates.Count) certificates" -ForegroundColor Green # 2. ANALYZE CERTIFICATES Write-Host "`n[2/3] Analyzing certificates..." -ForegroundColor Cyan # Check for expired certificates $expired = $allCertificates | Where-Object { $_.NotAfter -lt $currentDate } foreach ($cert in $expired) { $daysExpired = [Math]::Round(($currentDate - $cert.NotAfter).TotalDays) $results.Issues += [PSCustomObject]@{ Severity = 'High' Category = 'Expired' Certificate = $cert.Subject Store = $cert.Store Thumbprint = $cert.Thumbprint Issue = "Certificate expired $daysExpired days ago" NotAfter = $cert.NotAfter.ToString('yyyy-MM-dd') Remediation = "Remove expired certificate or renew - Remove-Item Cert:\$($cert.Store)\$($cert.Thumbprint)" } $results.Summary.Expired++ Write-Host " [!] HIGH: Expired certificate - $($cert.Subject)" -ForegroundColor Yellow } # Check for expiring certificates $expiring = $allCertificates | Where-Object { $_.NotAfter -gt $currentDate -and $_.NotAfter -le $expiringThreshold } foreach ($cert in $expiring) { $daysUntilExpiry = [Math]::Round(($cert.NotAfter - $currentDate).TotalDays) $results.Issues += [PSCustomObject]@{ Severity = if($daysUntilExpiry -le 7){'High'}elseif($daysUntilExpiry -le 14){'Medium'}else{'Low'} Category = 'Expiring Soon' Certificate = $cert.Subject Store = $cert.Store Thumbprint = $cert.Thumbprint Issue = "Certificate expires in $daysUntilExpiry days" NotAfter = $cert.NotAfter.ToString('yyyy-MM-dd') Remediation = "Renew certificate before expiration" } $results.Summary.ExpiringSoon++ $color = if($daysUntilExpiry -le 7){'Yellow'}else{'Gray'} Write-Host " [*] Expiring: $($cert.Subject) - $daysUntilExpiry days" -ForegroundColor $color } # Check for self-signed certificates in sensitive stores $sensitiveStores = @('LocalMachine\My', 'LocalMachine\WebHosting') $selfSignedInSensitive = $allCertificates | Where-Object { $_.IsSelfSigned -and $_.Store -in $sensitiveStores } foreach ($cert in $selfSignedInSensitive) { $results.Issues += [PSCustomObject]@{ Severity = 'Medium' Category = 'Self-Signed' Certificate = $cert.Subject Store = $cert.Store Thumbprint = $cert.Thumbprint Issue = "Self-signed certificate in production store" NotAfter = $cert.NotAfter.ToString('yyyy-MM-dd') Remediation = "Replace with CA-issued certificate for production use" } $results.Summary.SelfSigned++ Write-Host " [*] Self-signed certificate: $($cert.Subject)" -ForegroundColor Gray } # Check for weak signature algorithms foreach ($cert in $allCertificates) { $isWeak = $false foreach ($weakAlg in $weakAlgorithms) { if ($cert.SignatureAlgorithm -like "*$weakAlg*") { $isWeak = $true break } } if ($isWeak) { $results.Issues += [PSCustomObject]@{ Severity = 'High' Category = 'Weak Signature' Certificate = $cert.Subject Store = $cert.Store Thumbprint = $cert.Thumbprint Issue = "Uses weak signature algorithm - $($cert.SignatureAlgorithm)" NotAfter = $cert.NotAfter.ToString('yyyy-MM-dd') Remediation = "Replace with certificate using SHA-256 or stronger" } $results.Summary.WeakSignature++ Write-Host " [!] HIGH: Weak signature - $($cert.Subject) ($($cert.SignatureAlgorithm))" -ForegroundColor Yellow } } # Check for duplicate thumbprints across stores $thumbprintGroups = $allCertificates | Group-Object Thumbprint | Where-Object { $_.Count -gt 1 } foreach ($group in $thumbprintGroups) { $duplicates = $group.Group $stores = ($duplicates | Select-Object -ExpandProperty Store) -join ', ' $results.Issues += [PSCustomObject]@{ Severity = 'Low' Category = 'Duplicate' Certificate = $duplicates[0].Subject Store = $stores Thumbprint = $group.Name Issue = "Certificate duplicated in $($group.Count) stores" NotAfter = $duplicates[0].NotAfter.ToString('yyyy-MM-dd') Remediation = "Review and remove unnecessary duplicates" } $results.Summary.DuplicateThumbprints++ } # Check certificate chain validity (for LocalMachine\My) Write-Host " Validating certificate chains..." -ForegroundColor Gray $myStoreCerts = $allCertificates | Where-Object { $_.Store -eq 'LocalMachine\My' -and $_.NotAfter -gt $currentDate } foreach ($certInfo in $myStoreCerts) { try { $store = New-Object System.Security.Cryptography.X509Certificates.X509Store('My', 'LocalMachine') $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) $cert = $store.Certificates | Where-Object { $_.Thumbprint -eq $certInfo.Thumbprint } | Select-Object -First 1 if ($cert) { $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain $chain.ChainPolicy.RevocationMode = if($CheckRevocation){ [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online } else { [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck } $isValid = $chain.Build($cert) if (-not $isValid) { $chainErrors = $chain.ChainStatus | ForEach-Object { $_.StatusInformation } | Select-Object -Unique $errorMsg = $chainErrors -join '; ' # Check if it's a revocation error if ($CheckRevocation -and $errorMsg -match 'revok') { $results.Issues += [PSCustomObject]@{ Severity = 'Critical' Category = 'Revoked' Certificate = $certInfo.Subject Store = $certInfo.Store Thumbprint = $certInfo.Thumbprint Issue = "Certificate has been revoked" NotAfter = $certInfo.NotAfter.ToString('yyyy-MM-dd') Remediation = "Remove revoked certificate immediately and replace" } $results.Summary.Revoked++ Write-Host " [!] CRITICAL: Revoked certificate - $($certInfo.Subject)" -ForegroundColor Red } else { $results.Issues += [PSCustomObject]@{ Severity = 'Medium' Category = 'Invalid Chain' Certificate = $certInfo.Subject Store = $certInfo.Store Thumbprint = $certInfo.Thumbprint Issue = "Certificate chain validation failed - $errorMsg" NotAfter = $certInfo.NotAfter.ToString('yyyy-MM-dd') Remediation = "Install intermediate CA certificates or verify root CA trust" } $results.Summary.InvalidChain++ } } $chain.Dispose() } $store.Close() } catch { Write-Warning "Could not validate chain for $($certInfo.Subject) - $_" } } # Count distinct unhealthy certificates: a certificate can raise several # issues (e.g. expired AND weak signature), so Total - Issues.Count can # go negative. 'Duplicate' issues are not a health problem. $unhealthyCertificates = @($results.Issues | Where-Object { $_.Category -ne 'Duplicate' } | ForEach-Object { "$($_.Store)|$($_.Thumbprint)" } | Select-Object -Unique) $results.HealthyCertificates = [Math]::Max(0, $results.TotalCertificates - $unhealthyCertificates.Count) # 3. GENERATE RECOMMENDATIONS Write-Host "`n[3/3] Generating recommendations..." -ForegroundColor Cyan if ($results.Summary.Revoked -gt 0) { $results.Recommendations += "CRITICAL: Remove $($results.Summary.Revoked) revoked certificate(s) immediately" } if ($results.Summary.Expired -gt 0) { $results.Recommendations += "HIGH: Remove or renew $($results.Summary.Expired) expired certificate(s)" } if ($results.Summary.WeakSignature -gt 0) { $results.Recommendations += "HIGH: Replace $($results.Summary.WeakSignature) certificate(s) using weak signature algorithms" } if ($results.Summary.ExpiringSoon -gt 0) { $results.Recommendations += "Plan renewal for $($results.Summary.ExpiringSoon) expiring certificate(s)" } if ($results.Summary.InvalidChain -gt 0) { $results.Recommendations += "Fix certificate chain issues for $($results.Summary.InvalidChain) certificate(s)" } if ($results.Summary.SelfSigned -gt 0) { $results.Recommendations += "Replace self-signed certificates with CA-issued certificates for production" } if ($results.Issues.Count -eq 0) { $results.Recommendations += "All certificates are healthy - maintain regular monitoring" } $results.Recommendations += "Implement automated certificate renewal (e.g., ACME/Let's Encrypt)" $results.Recommendations += "Set up certificate expiration alerts (60, 30, 14, 7 days)" $results.Recommendations += "Enable certificate revocation checking in production" $results.Recommendations += "Document certificate inventory and ownership" $results.Recommendations += "Remove unused and expired certificates quarterly" # 4. EXPORT REPORTS if ($OutputPath) { Write-Host "`n[*] Exporting certificate health reports..." -ForegroundColor Cyan if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null } $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $reportName = "CertificateHealth_$timestamp" # Export JSON if ($Format -in @('JSON', 'All')) { $jsonPath = Join-Path $OutputPath "$reportName.json" $results | ConvertTo-Json -Depth 10 | Out-File $jsonPath -Encoding UTF8 Write-Host " JSON report saved: $jsonPath" -ForegroundColor Green } # Export CSV if ($Format -in @('CSV', 'All')) { $csvPath = Join-Path $OutputPath "$reportName.csv" $results.Issues | Export-Csv $csvPath -NoTypeInformation Write-Host " CSV report saved: $csvPath" -ForegroundColor Green } # Export HTML if ($Format -in @('HTML', 'All')) { $htmlPath = Join-Path $OutputPath "$reportName.html" $html = Generate-CertificateHealthHTML -Results $results $html | Out-File $htmlPath -Encoding UTF8 Write-Host " HTML report saved: $htmlPath" -ForegroundColor Green } } } catch { Write-Error "Error during certificate health check: $_" throw } } end { Write-Host "`n=== CERTIFICATE HEALTH CHECK COMPLETE ===" -ForegroundColor $( if ($results.Summary.Revoked -gt 0) { 'Red' } elseif ($results.Summary.Expired -gt 0 -or $results.Summary.WeakSignature -gt 0) { 'Yellow' } else { 'Green' } ) Write-Host "`nSummary:" -ForegroundColor Cyan Write-Host " Total Certificates: $($results.TotalCertificates)" -ForegroundColor White Write-Host " Healthy: $($results.HealthyCertificates)" -ForegroundColor Green Write-Host " Issues Found: $($results.Issues.Count)" -ForegroundColor $( if ($results.Issues.Count -gt 0) { 'Yellow' } else { 'Green' } ) if ($results.Summary.Revoked -gt 0) { Write-Host " Revoked: $($results.Summary.Revoked)" -ForegroundColor Red } if ($results.Summary.Expired -gt 0) { Write-Host " Expired: $($results.Summary.Expired)" -ForegroundColor Yellow } if ($results.Summary.ExpiringSoon -gt 0) { Write-Host " Expiring Soon: $($results.Summary.ExpiringSoon)" -ForegroundColor Yellow } if ($results.Summary.WeakSignature -gt 0) { Write-Host " Weak Signatures: $($results.Summary.WeakSignature)" -ForegroundColor Yellow } if ($results.Summary.InvalidChain -gt 0) { Write-Host " Invalid Chains: $($results.Summary.InvalidChain)" -ForegroundColor Gray } if ($results.Summary.SelfSigned -gt 0) { Write-Host " Self-Signed: $($results.Summary.SelfSigned)" -ForegroundColor Gray } if ($results.Issues.Count -gt 0) { Write-Host "`nTop Issues:" -ForegroundColor Cyan $results.Issues | Sort-Object { switch ($_.Severity) { 'Critical' { 0 } 'High' { 1 } 'Medium' { 2 } 'Low' { 3 } } } | Select-Object -First 10 | ForEach-Object { $color = switch ($_.Severity) { 'Critical' { 'Red' } 'High' { 'Yellow' } default { 'Gray' } } Write-Host " [$($_.Severity)] $($_.Category) - $($_.Certificate)" -ForegroundColor $color } } if ($results.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Cyan $results.Recommendations | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } } return $results } } # Helper function to generate HTML report function Generate-CertificateHealthHTML { param($Results) $healthColor = if($Results.Summary.Revoked -gt 0){'critical'} elseif($Results.Summary.Expired -gt 0 -or $Results.Summary.WeakSignature -gt 0){'high'} else{'safe'} $html = @" <!DOCTYPE html> <html> <head> <title>Certificate Health Report - $($Results.ComputerName)</title> <style> body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; } .container { max-width: 1400px; margin: 0 auto; background: white; padding: 30px; box-shadow: 0 0 10px rgba(0,0,0,0.1); } h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; } h2 { color: #34495e; margin-top: 30px; } .summary { background: #ecf0f1; padding: 20px; border-radius: 5px; margin: 20px 0; } .metric { display: inline-block; margin: 10px 20px 10px 0; } .metric-label { font-weight: bold; color: #7f8c8d; } .metric-value { font-size: 24px; font-weight: bold; } .critical { color: #e74c3c; } .high { color: #e67e22; } .medium { color: #f39c12; } .low { color: #95a5a6; } .safe { color: #27ae60; } table { width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13px; } th { background: #3498db; color: white; padding: 10px; text-align: left; } td { padding: 8px; border-bottom: 1px solid #ddd; vertical-align: top; } tr:hover { background: #f8f9fa; } .severity-badge { padding: 3px 6px; border-radius: 3px; color: white; font-weight: bold; font-size: 11px; } .severity-critical { background: #e74c3c; } .severity-high { background: #e67e22; } .severity-medium { background: #f39c12; } .severity-low { background: #95a5a6; } .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #ddd; color: #7f8c8d; font-size: 12px; } .no-issues { color: #27ae60; font-weight: bold; font-size: 16px; padding: 20px; text-align: center; } .thumbprint { font-family: monospace; font-size: 11px; color: #7f8c8d; } </style> </head> <body> <div class="container"> <h1>Certificate Health Report</h1> <p><strong>Computer:</strong> $($Results.ComputerName) | <strong>Scan Date:</strong> $($Results.ScanDate)</p> <div class="summary"> <h2>Summary</h2> <div class="metric"> <div class="metric-label">Total Certificates</div> <div class="metric-value">$($Results.TotalCertificates)</div> </div> <div class="metric"> <div class="metric-label">Healthy</div> <div class="metric-value safe">$($Results.HealthyCertificates)</div> </div> <div class="metric"> <div class="metric-label">Issues</div> <div class="metric-value $(if($Results.Issues.Count -gt 0){'high'}else{'safe'})"> $($Results.Issues.Count) </div> </div> <div class="metric"> <div class="metric-label">Expired</div> <div class="metric-value high">$($Results.Summary.Expired)</div> </div> <div class="metric"> <div class="metric-label">Expiring Soon</div> <div class="metric-value medium">$($Results.Summary.ExpiringSoon)</div> </div> <div class="metric"> <div class="metric-label">Weak Signature</div> <div class="metric-value high">$($Results.Summary.WeakSignature)</div> </div> </div> <h2>Certificate Issues</h2> "@ if ($Results.Issues.Count -gt 0) { $html += @" <table> <tr> <th style="width: 70px;">Severity</th> <th style="width: 100px;">Category</th> <th style="width: 250px;">Certificate</th> <th style="width: 150px;">Store</th> <th style="width: 100px;">Expires</th> <th>Issue</th> <th>Remediation</th> </tr> "@ foreach ($issue in ($Results.Issues | Sort-Object { switch ($_.Severity) { 'Critical' { 0 } 'High' { 1 } 'Medium' { 2 } 'Low' { 3 } } })) { $severityClass = "severity-$($issue.Severity.ToLower())" $cert = $issue.Certificate -replace '<', '<' -replace '>', '>' $issueText = $issue.Issue -replace '<', '<' -replace '>', '>' $remediation = $issue.Remediation -replace '<', '<' -replace '>', '>' $thumbShort = if($issue.Thumbprint.Length -gt 16) { $issue.Thumbprint.Substring(0,16) + '...' } else { $issue.Thumbprint } $html += @" <tr> <td><span class="severity-badge $severityClass">$($issue.Severity)</span></td> <td>$($issue.Category)</td> <td><strong>$cert</strong><br><span class="thumbprint">$thumbShort</span></td> <td><small>$($issue.Store)</small></td> <td>$($issue.NotAfter)</td> <td>$issueText</td> <td><small>$remediation</small></td> </tr> "@ } $html += " </table>`n" } else { $html += " <p class=`"no-issues`">All certificates are healthy</p>`n" } $html += " `n <h2>Recommendations</h2>`n <ul>`n" foreach ($rec in $Results.Recommendations) { $recEscaped = $rec -replace '<', '<' -replace '>', '>' $html += " <li>$recEscaped</li>`n" } $currentDate = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $html += @" </ul> <div class="footer"> Generated by WindowsSecurityAudit Module | Certificate Health Check | $currentDate </div> </div> </body> </html> "@ return $html } |