Private/New-HtmlDashboard.ps1
|
function New-HtmlDashboard { <# .SYNOPSIS Generates a dark-themed HTML dashboard for certificate audit results. .DESCRIPTION Internal helper that compiles audit data into a styled HTML report with color-coded findings. Uses a dark background with amber (#f0883e) for warnings and red (#f85149) for critical/expired items. .PARAMETER SummaryData PSCustomObject containing aggregate counts and metadata. .PARAMETER ExpiringCerts Array of expiring certificate objects from Get-ExpiringCertificates. .PARAMETER WeakCerts Array of weak certificate objects from Get-WeakCertificateReport. .PARAMETER IISCerts Array of IIS certificate objects from Get-IISCertificateReport. .PARAMETER StoreReport Array of certificate inventory objects from Get-CertificateStoreReport. .PARAMETER OutputPath Full path for the HTML file to write. #> [CmdletBinding()] param( [Parameter(Mandatory)] [PSCustomObject]$SummaryData, [Parameter()] [object[]]$ExpiringCerts = @(), [Parameter()] [object[]]$WeakCerts = @(), [Parameter()] [object[]]$IISCerts = @(), [Parameter()] [object[]]$StoreReport = @(), [Parameter(Mandatory)] [string]$OutputPath ) # --- Colour helpers --------------------------------------------------- function Get-FindingBadge { param([string]$Finding) switch -Wildcard ($Finding) { 'EXPIRED' { '<span class="badge badge-expired">EXPIRED</span>' } 'CRITICAL' { '<span class="badge badge-critical">CRITICAL</span>' } 'WARNING' { '<span class="badge badge-warning">WARNING</span>' } 'MISSING' { '<span class="badge badge-expired">MISSING</span>' } 'OK' { '<span class="badge badge-ok">OK</span>' } default { '<span class="badge badge-warning">' + [System.Web.HttpUtility]::HtmlEncode($Finding) + '</span>' } } } function ConvertTo-SafeHtml { param([string]$Text) if ([string]::IsNullOrEmpty($Text)) { return '—' } [System.Web.HttpUtility]::HtmlEncode($Text) } # Load System.Web for HtmlEncode Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue # --- Build HTML ------------------------------------------------------- $html = [System.Text.StringBuilder]::new() [void]$html.AppendLine(@' <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Certificate Lifecycle Audit</title> <style> :root { --bg-primary: #0d1117; --bg-secondary: #161b22; --bg-card: #1c2128; --border: #30363d; --text-primary: #e6edf3; --text-muted: #8b949e; --accent-amber: #f0883e; --accent-red: #f85149; --accent-green: #3fb950; --accent-blue: #58a6ff; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; padding: 2rem; } .container { max-width: 1400px; margin: 0 auto; } h1 { font-size: 1.8rem; margin-bottom: 0.25rem; } .subtitle { color: var(--text-muted); margin-bottom: 2rem; font-size: 0.95rem; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .summary-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.2rem; text-align: center; } .summary-card .value { font-size: 2rem; font-weight: 700; display: block; } .summary-card .label { color: var(--text-muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; } .value-expired { color: var(--accent-red); } .value-critical { color: var(--accent-red); } .value-warning { color: var(--accent-amber); } .value-ok { color: var(--accent-green); } .value-total { color: var(--accent-blue); } section { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 1.5rem; overflow: hidden; } section h2 { padding: 1rem 1.25rem; font-size: 1.1rem; border-bottom: 1px solid var(--border); background: var(--bg-card); } table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } th { text-align: left; padding: 0.75rem 1rem; background: var(--bg-card); color: var(--text-muted); font-weight: 600; border-bottom: 1px solid var(--border); white-space: nowrap; } td { padding: 0.6rem 1rem; border-bottom: 1px solid var(--border); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } tr:hover td { background: rgba(255,255,255,0.03); } .badge { display: inline-block; padding: 0.15em 0.6em; border-radius: 4px; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.03em; } .badge-expired, .badge-critical { background: rgba(248,81,73,0.15); color: var(--accent-red); border: 1px solid rgba(248,81,73,0.3); } .badge-warning { background: rgba(240,136,62,0.15); color: var(--accent-amber); border: 1px solid rgba(240,136,62,0.3); } .badge-ok { background: rgba(63,185,80,0.15); color: var(--accent-green); border: 1px solid rgba(63,185,80,0.3); } .empty-state { padding: 2rem; text-align: center; color: var(--text-muted); } footer { text-align: center; color: var(--text-muted); font-size: 0.8rem; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border); } </style> </head> <body> <div class="container"> <h1>Certificate Lifecycle Audit</h1> '@) [void]$html.AppendLine("<p class='subtitle'>Generated $(ConvertTo-SafeHtml $SummaryData.ReportTime) — Computers: $(ConvertTo-SafeHtml $SummaryData.Computers)</p>") # --- Summary cards ---------------------------------------------------- [void]$html.AppendLine('<div class="summary-grid">') [void]$html.AppendLine("<div class='summary-card'><span class='value value-total'>$($SummaryData.TotalScanned)</span><span class='label'>Total Scanned</span></div>") [void]$html.AppendLine("<div class='summary-card'><span class='value value-expired'>$($SummaryData.Expired)</span><span class='label'>Expired</span></div>") [void]$html.AppendLine("<div class='summary-card'><span class='value value-critical'>$($SummaryData.Critical)</span><span class='label'>Critical (≤7d)</span></div>") [void]$html.AppendLine("<div class='summary-card'><span class='value value-warning'>$($SummaryData.Warning)</span><span class='label'>Warning (≤30d)</span></div>") [void]$html.AppendLine("<div class='summary-card'><span class='value value-ok'>$($SummaryData.OK)</span><span class='label'>Healthy</span></div>") [void]$html.AppendLine("<div class='summary-card'><span class='value value-warning'>$($SummaryData.WeakCrypto)</span><span class='label'>Weak Crypto</span></div>") [void]$html.AppendLine('</div>') # --- Expiring Certificates table -------------------------------------- [void]$html.AppendLine('<section><h2>Expiring & Expired Certificates</h2>') $actionable = @($ExpiringCerts | Where-Object { $_.Finding -ne 'OK' }) if ($actionable.Count -gt 0) { [void]$html.AppendLine('<table><tr><th>Computer</th><th>Subject</th><th>Thumbprint</th><th>Expires</th><th>Days Left</th><th>Store</th><th>Finding</th></tr>') foreach ($c in ($actionable | Sort-Object DaysRemaining)) { [void]$html.AppendLine("<tr><td>$(ConvertTo-SafeHtml $c.ComputerName)</td><td>$(ConvertTo-SafeHtml $c.Subject)</td><td><code>$($c.Thumbprint)</code></td><td>$($c.NotAfter.ToString('yyyy-MM-dd'))</td><td>$($c.DaysRemaining)</td><td>$(ConvertTo-SafeHtml $c.Store)</td><td>$(Get-FindingBadge $c.Finding)</td></tr>") } [void]$html.AppendLine('</table>') } else { [void]$html.AppendLine('<div class="empty-state">No expiring or expired certificates found.</div>') } [void]$html.AppendLine('</section>') # --- IIS Certificates table ------------------------------------------- if ($IISCerts.Count -gt 0) { [void]$html.AppendLine('<section><h2>IIS Certificate Bindings</h2>') [void]$html.AppendLine('<table><tr><th>Computer</th><th>Site</th><th>Binding</th><th>Subject</th><th>Expires</th><th>Days Left</th><th>Finding</th></tr>') foreach ($c in ($IISCerts | Sort-Object DaysRemaining)) { $expiresStr = if ($c.NotAfter) { $c.NotAfter.ToString('yyyy-MM-dd') } else { '—' } [void]$html.AppendLine("<tr><td>$(ConvertTo-SafeHtml $c.ComputerName)</td><td>$(ConvertTo-SafeHtml $c.SiteName)</td><td>$(ConvertTo-SafeHtml $c.Binding)</td><td>$(ConvertTo-SafeHtml $c.Subject)</td><td>$expiresStr</td><td>$($c.DaysRemaining)</td><td>$(Get-FindingBadge $c.Finding)</td></tr>") } [void]$html.AppendLine('</table></section>') } # --- Weak Certificates table ------------------------------------------ [void]$html.AppendLine('<section><h2>Weak Certificates</h2>') if ($WeakCerts.Count -gt 0) { [void]$html.AppendLine('<table><tr><th>Computer</th><th>Subject</th><th>Thumbprint</th><th>Key Length</th><th>Signature</th><th>Self-Signed</th><th>Finding</th></tr>') foreach ($c in $WeakCerts) { [void]$html.AppendLine("<tr><td>$(ConvertTo-SafeHtml $c.ComputerName)</td><td>$(ConvertTo-SafeHtml $c.Subject)</td><td><code>$($c.Thumbprint)</code></td><td>$($c.KeyLength)</td><td>$(ConvertTo-SafeHtml $c.SignatureAlgorithm)</td><td>$($c.SelfSigned)</td><td>$(Get-FindingBadge $c.Finding)</td></tr>") } [void]$html.AppendLine('</table>') } else { [void]$html.AppendLine('<div class="empty-state">No weak certificates detected.</div>') } [void]$html.AppendLine('</section>') # --- Full Store Inventory table --------------------------------------- [void]$html.AppendLine('<section><h2>Certificate Store Inventory</h2>') if ($StoreReport.Count -gt 0) { [void]$html.AppendLine('<table><tr><th>Subject</th><th>Issuer</th><th>Thumbprint</th><th>Expires</th><th>Days Left</th><th>Key</th><th>Signature</th><th>Private Key</th><th>Finding</th></tr>') foreach ($c in ($StoreReport | Sort-Object DaysRemaining | Select-Object -First 200)) { [void]$html.AppendLine("<tr><td>$(ConvertTo-SafeHtml $c.Subject)</td><td>$(ConvertTo-SafeHtml $c.Issuer)</td><td><code>$($c.Thumbprint)</code></td><td>$($c.NotAfter.ToString('yyyy-MM-dd'))</td><td>$($c.DaysRemaining)</td><td>$($c.KeyLength)</td><td>$(ConvertTo-SafeHtml $c.SignatureAlgorithm)</td><td>$($c.HasPrivateKey)</td><td>$(Get-FindingBadge $c.Finding)</td></tr>") } [void]$html.AppendLine('</table>') } else { [void]$html.AppendLine('<div class="empty-state">No certificate store data collected.</div>') } [void]$html.AppendLine('</section>') # --- Footer ----------------------------------------------------------- [void]$html.AppendLine(@' <footer> Certificate-LifecycleMonitor — Generated by Invoke-CertificateAudit </footer> </div> </body> </html> '@) $html.ToString() | Out-File -FilePath $OutputPath -Encoding utf8 -Force Write-Verbose "HTML dashboard written to $OutputPath" } |