src/Public/Report/New-FotonBaselineReport.ps1
|
function New-FotonBaselineReport { <# .SYNOPSIS HTML snapshot of one baseline observation (offline). Not a findings report. .DESCRIPTION Tables per record type from baseline.json (or a FotonBaseline_* folder). Caps rows. Escapes every cell. Does not say the host is clean or compromised. The JSON file remains the source of truth for Compare-FotonBaseline. .PARAMETER Path baseline.json or a baseline package folder. .PARAMETER OutputPath HTML file. Defaults to Foton_Baseline.html beside the JSON. .PARAMETER MaxRows Max rows per table (default 200). .EXAMPLE New-FotonBaselineReport -Path D:\Baselines\FotonBaseline_PC1_20260822-120000Z Writes Foton_Baseline.html in that folder. .OUTPUTS Path to the HTML file. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path, [string]$OutputPath, [int]$MaxRows = 200 ) if ($null -eq $script:FotonSession) { Initialize-FotonSession } $jsonPath = Get-FotonBaselineJsonPath -Path $Path $doc = Get-Content -LiteralPath $jsonPath -Raw -Encoding UTF8 | ConvertFrom-Json if (-not $OutputPath) { $OutputPath = Join-Path (Split-Path -Parent $jsonPath) 'Foton_Baseline.html' } $h = $doc.Header $hostName = [string]$h.Hostname if (-not $hostName) { $hostName = '(unknown host)' } $schema = [string]$h.BaselineSchemaVersion $tool = [string]$h.ToolVersion $captured = [string]$h.CapturedUtc $elev = $false if ($h.Elevated) { $elev = [bool]$h.Elevated } $payload = [string]$h.PayloadSha256 $note = [string]$h.Note $provByType = @{} foreach ($p in @($doc.Providers)) { $provByType[[string]$p.RecordType] = $p } $css = @' <style> :root{ --bg:#0b1016; --panel:#121a24; --panel2:#0f1720; --line:#22303f; --text:#dfe8f1; --beam:#37d6ff; --beam2:#7cf5c4; --med:#ffb340; } *{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 "Segoe UI",system-ui,sans-serif} .wrap{max-width:1240px;margin:0 auto;padding:28px 22px 80px} header.hero{border:1px solid var(--line);border-radius:14px;padding:26px 28px;background: radial-gradient(1100px 200px at 12% -40%,rgba(55,214,255,.18),transparent 60%), linear-gradient(180deg,#121b26,#0e151e)} .brand{display:flex;align-items:center;gap:18px;flex-wrap:wrap} .mark{flex:0 0 auto;display:block;color:var(--beam)} h1{margin:0;font-size:28px;letter-spacing:3px;font-weight:700;color:var(--beam);text-transform:uppercase} .tagline{color:#93a7bb;font-size:13px;font-style:italic;margin-top:6px} .meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(215px,1fr));gap:10px 26px;margin-top:20px} .meta div{font-size:13px;color:#a9bccf} .meta b{display:block;color:#6f8399;font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.9px;margin-bottom:2px} .notice{margin-top:18px;border-left:3px solid var(--med);background:rgba(255,179,64,.08);padding:12px 16px;border-radius:0 8px 8px 0;font-size:13px;color:#e8d4b4} h2{font-size:17px;margin:34px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--line)} h2 small{color:#6f8399;font-weight:400;font-size:12px;margin-left:8px} .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin:16px 0} .card{background:var(--panel);border:1px solid var(--line);border-radius:11px;padding:14px 16px} .card .n{font-size:22px;font-weight:600;color:var(--beam2)} .card .l{font-size:11px;text-transform:uppercase;letter-spacing:.9px;color:#7f93a8;margin-top:4px} table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;font-size:12.5px} th{background:var(--panel2);text-align:left;padding:9px 12px;color:#8fa3b8;font-size:11px;text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid var(--line)} td{padding:8px 12px;border-bottom:1px solid rgba(34,48,63,.6);vertical-align:top;word-break:break-word} tr:last-child td{border-bottom:none} .dim{color:#7f93a8} footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--line);color:#6f8399;font-size:12px} </style> '@ $css = $css + (Get-FotonReportNavCss) $sb = New-Object System.Text.StringBuilder $null = $sb.AppendLine('<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">') $null = $sb.AppendLine('<meta name="viewport" content="width=device-width,initial-scale=1">') $null = $sb.AppendLine("<title>Foton Baseline Snapshot - $(ConvertTo-FotonSafeHtml $hostName)</title>") $null = $sb.AppendLine($css) $null = $sb.AppendLine('</head><body><div class="wrap" id="top">') $null = $sb.AppendLine('<header class="hero">') $null = $sb.AppendLine('<div class="brand">') $null = $sb.AppendLine((Get-FotonReportMarkSvg)) $null = $sb.AppendLine('<div class="brand-text">') $null = $sb.AppendLine('<h1>Foton</h1>') $null = $sb.AppendLine('<div class="tagline">Baseline snapshot - one observation of stable configuration. Not a triage report. Not a clean-host certificate.</div>') $null = $sb.AppendLine('</div></div>') $null = $sb.AppendLine('<div class="meta">') $null = $sb.AppendLine("<div><b>Hostname</b>$(ConvertTo-FotonSafeHtml $hostName)</div>") $null = $sb.AppendLine("<div><b>Captured (UTC)</b>$(ConvertTo-FotonSafeHtml $captured)</div>") $null = $sb.AppendLine("<div><b>Schema</b>$(ConvertTo-FotonSafeHtml $schema)</div>") $null = $sb.AppendLine("<div><b>Tool</b>$(ConvertTo-FotonSafeHtml $tool)</div>") $priv = 'Standard user - some providers may be thin' if ($elev) { $priv = 'Elevated (Administrator)' } $null = $sb.AppendLine("<div><b>Privilege</b>$priv</div>") $null = $sb.AppendLine("<div><b>Payload SHA-256</b>$(ConvertTo-FotonSafeHtml $payload)</div>") $null = $sb.AppendLine('</div>') $caveat = $note if (-not $caveat) { $caveat = 'A baseline taken after an incident is a picture of that state. Diff proves change between two observations, never when or who.' } $null = $sb.AppendLine("<div class=""notice""><b>Read this first.</b> $(ConvertTo-FotonSafeHtml $caveat) Full tables live in baseline.json. This HTML is a capped preview.</div>") $null = $sb.AppendLine('</header>') $null = $sb.AppendLine((Get-FotonReportNavBarHtml)) $null = $sb.AppendLine('<h2 id="sec-counts">Record counts</h2><div class="cards">') $typeNames = @($doc.Records.PSObject.Properties.Name | Sort-Object) foreach ($type in $typeNames) { $n = @($doc.Records.$type).Count $sid = Get-FotonReportSectionId -Name $type $null = $sb.AppendLine("<a class=""card"" href=""#$sid""><div class=""n"">$n</div><div class=""l"">$(ConvertTo-FotonSafeHtml $type)</div></a>") } $null = $sb.AppendLine('</div>') foreach ($type in $typeNames) { $rows = @($doc.Records.$type) $meta = $provByType[$type] $cols = @() if ($meta) { foreach ($f in @($meta.IdentityFields)) { if ($cols -notcontains [string]$f) { $cols += [string]$f } } foreach ($f in @($meta.ComparedFields)) { if ($cols -notcontains [string]$f) { $cols += [string]$f } } } if ($cols.Count -eq 0 -and $rows.Count -gt 0) { foreach ($pr in $rows[0].PSObject.Properties) { $cols += $pr.Name } } $shown = $rows.Count $extra = '' if ($shown -gt $MaxRows) { $extra = " (showing $MaxRows of $shown)" $rows = @($rows | Select-Object -First $MaxRows) } $sid = Get-FotonReportSectionId -Name $type $null = $sb.AppendLine("<h2 id=""$sid"">$(ConvertTo-FotonSafeHtml $type) <small>$($rows.Count) rows$extra</small></h2>") if ($meta -and $meta.FalsePositives) { $null = $sb.AppendLine("<p class=""dim"">$(ConvertTo-FotonSafeHtml ([string]$meta.FalsePositives))</p>") } if ($cols.Count -eq 0 -or $rows.Count -eq 0) { $null = $sb.AppendLine('<p class="dim">No rows.</p>') continue } $null = $sb.AppendLine('<table><thead><tr>') foreach ($c in $cols) { $null = $sb.AppendLine("<th>$(ConvertTo-FotonSafeHtml $c)</th>") } $null = $sb.AppendLine('</tr></thead><tbody>') foreach ($row in $rows) { $null = $sb.AppendLine('<tr>') foreach ($c in $cols) { $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$row.$c))</td>") } $null = $sb.AppendLine('</tr>') } $null = $sb.AppendLine('</tbody></table>') } $null = $sb.AppendLine("<footer>Foton baseline snapshot v$(ConvertTo-FotonSafeHtml $tool) · schema $(ConvertTo-FotonSafeHtml $schema) · $(ConvertTo-FotonSafeHtml $hostName) · compare with Compare-FotonBaseline against baseline.json</footer>") $null = $sb.AppendLine('</div>') $null = $sb.AppendLine((Get-FotonReportNavScript)) $null = $sb.AppendLine('</body></html>') $dir = Split-Path -Parent $OutputPath if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } [System.IO.File]::WriteAllText($OutputPath, $sb.ToString(), (New-Object System.Text.UTF8Encoding $false)) Write-FotonStatus "Baseline snapshot report: $OutputPath" 'Good' return $OutputPath } |