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 '&mdash;' }
        [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) &mdash; 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 (&le;7d)</span></div>")
    [void]$html.AppendLine("<div class='summary-card'><span class='value value-warning'>$($SummaryData.Warning)</span><span class='label'>Warning (&le;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 &amp; 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 { '&mdash;' }
            [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 &mdash; Generated by Invoke-CertificateAudit
</footer>
</div>
</body>
</html>
'@
)

    $html.ToString() | Out-File -FilePath $OutputPath -Encoding utf8 -Force
    Write-Verbose "HTML dashboard written to $OutputPath"
}