src/Public/Report/New-FotonParseReport.ps1
|
function New-FotonParseReport { <# .SYNOPSIS HTML preview of Invoke-FotonParse rows (offline). Not a findings report. .DESCRIPTION Capped tables grouped by parser Format. Escapes every cell. CSV remains the source of truth. Does not say the host executed malware. .PARAMETER Records Normalized parse rows (SourcePath, Format, Supported, Identity, SizeBytes, SHA256, Note). .PARAMETER OutputPath HTML file. .PARAMETER EmptyReason Shown when Records is empty (visibility gap, not a clean host). .PARAMETER MaxRows Max rows per Format table (default 400). .EXAMPLE New-FotonParseReport -Records $rows -OutputPath D:\Cases\CASE-0001\analysis\Foton_Parse.html .OUTPUTS Path to the HTML file. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$OutputPath, [object[]]$Records, [string]$EmptyReason, [int]$MaxRows = 400 ) if ($null -eq $script:FotonSession) { Initialize-FotonSession } $rows = @() if ($null -ne $Records) { $rows = @($Records | Where-Object { $null -ne $_ }) } $hostName = '(unknown host)' $osInfo = $null if ($script:FotonSession.Data) { $osInfo = $script:FotonSession.Data['os_info'] } if ($osInfo -and $osInfo.Hostname) { $hostName = [string]$osInfo.Hostname } $supported = @($rows | Where-Object { $_.Supported -eq $true -or [string]$_.Supported -eq 'True' }).Count $mam = @($rows | Where-Object { [string]$_.Format -eq 'MAM-compressed' }).Count $lnk = @($rows | Where-Object { [string]$_.Format -eq 'LNK' }).Count $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 Parse - $(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">Offline parse - Prefetch and LNK already in the package. Not triage findings. Not a compromise verdict.</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>Rows</b>$($rows.Count)</div>") $null = $sb.AppendLine("<div><b>Supported names</b>$supported</div>") $null = $sb.AppendLine("<div><b>MAM-compressed</b>$mam</div>") $null = $sb.AppendLine("<div><b>LNK</b>$lnk</div>") $null = $sb.AppendLine('</div>') $caveat = 'CSV is the source of truth (analysis/parsed_records.csv). This HTML is a capped preview. MAM-compressed Prefetch is not inflated. EVTX rows are selected Event IDs only, capped. Amcache KeyLastWriteUtc is hive key last-write, not proof of execution. Legacy Amcache File keys and SRUM ESE copies are not parsed here.' $null = $sb.AppendLine("<div class=""notice""><b>Read this first.</b> $(ConvertTo-FotonSafeHtml $caveat)</div>") $null = $sb.AppendLine('</header>') $null = $sb.AppendLine((Get-FotonReportNavBarHtml)) $null = $sb.AppendLine('<h2 id="sec-counts">Parse counts</h2><div class="cards">') $null = $sb.AppendLine("<div class=""card""><div class=""n"">$($rows.Count)</div><div class=""l"">Total</div></div>") $null = $sb.AppendLine("<div class=""card""><div class=""n"">$supported</div><div class=""l"">Supported</div></div>") $null = $sb.AppendLine("<div class=""card""><div class=""n"">$mam</div><div class=""l"">MAM Prefetch</div></div>") $null = $sb.AppendLine("<div class=""card""><div class=""n"">$lnk</div><div class=""l"">LNK</div></div>") $null = $sb.AppendLine('</div>') $formats = @($rows | ForEach-Object { [string]$_.Format } | Where-Object { $_ } | Sort-Object -Unique) if ($formats.Count -gt 0) { $null = $sb.AppendLine('<div class="controls">') $null = $sb.AppendLine('<button type="button" class="btn on" data-change="all">All</button>') foreach ($fmt in $formats) { $null = $sb.AppendLine("<button type=""button"" class=""btn"" data-change=""$(ConvertTo-FotonSafeHtml $fmt)"">$(ConvertTo-FotonSafeHtml $fmt)</button>") } $null = $sb.AppendLine('</div>') } if ($rows.Count -eq 0) { $msg = $EmptyReason if (-not $msg) { $msg = 'No Prefetch or LNK files were parsed.' } $null = $sb.AppendLine('<h2 id="sec-empty">Nothing parsed</h2>') $null = $sb.AppendLine("<p class=""dim"">$(ConvertTo-FotonSafeHtml $msg)</p>") } else { foreach ($fmt in $formats) { $chunk = @($rows | Where-Object { [string]$_.Format -eq $fmt }) $shown = $chunk $extra = '' if ($shown.Count -gt $MaxRows) { $extra = " (showing $MaxRows of $($shown.Count))" $shown = @($shown | Select-Object -First $MaxRows) } $sid = Get-FotonReportSectionId -Name $fmt $null = $sb.AppendLine("<h2 id=""$sid"">$(ConvertTo-FotonSafeHtml $fmt) <small>$($chunk.Count) row(s)$extra</small></h2>") $null = $sb.AppendLine('<table><thead><tr>') foreach ($col in @('Identity', 'Supported', 'SizeBytes', 'SHA256', 'SourcePath', 'Note')) { $null = $sb.AppendLine("<th>$(ConvertTo-FotonSafeHtml $col)</th>") } $null = $sb.AppendLine('</tr></thead><tbody>') foreach ($c in $shown) { $null = $sb.AppendLine("<tr data-change=""$(ConvertTo-FotonSafeHtml ([string]$c.Format))"">") $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$c.Identity))</td>") $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$c.Supported))</td>") $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$c.SizeBytes))</td>") $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$c.SHA256))</td>") $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$c.SourcePath))</td>") $null = $sb.AppendLine("<td>$(ConvertTo-FotonSafeHtml ([string]$c.Note))</td>") $null = $sb.AppendLine('</tr>') } $null = $sb.AppendLine('</tbody></table>') } } $null = $sb.AppendLine('<footer>Foton parse report · not findings.csv · source of truth is parsed_records.csv</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 "Parse report: $OutputPath" 'Good' return $OutputPath } |