Private/ConvertTo-TripwireHtmlReport.ps1
|
<# .SYNOPSIS Render scored trips into a single self-contained HTML file. .DESCRIPTION No external CSS, no fonts, no scripts from a CDN: an incident report has to open on an isolated workstation. Everything is escaped before it reaches the document, because every string in a trip -- display names, user agents, operations -- is attacker- influenced by construction. A deception tool that renders attacker-chosen text into a responder's browser has handed the attacker the responder. The header states coverage BEFORE findings: how many trips were withheld signals, and how many were suppressed. A low trip count on a degraded run is not good news, and this report refuses to let it look like good news. #> function ConvertTo-TripwireHtmlEncoded { [CmdletBinding()] [OutputType([string])] param([AllowNull()] $Value) if ($null -eq $Value) { return '' } return [System.Net.WebUtility]::HtmlEncode([string]$Value) } function ConvertTo-TripwireHtmlReport { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]] $Trip, [string] $TenantLabel = 'unspecified tenant', [switch] $Demo ) $ordered = @($Trip | Sort-Object -Property Confidence -Descending) $high = @($ordered | Where-Object { $_.Band -eq 'High' }).Count $medium = @($ordered | Where-Object { $_.Band -eq 'Medium' }).Count $low = @($ordered | Where-Object { $_.Band -eq 'Low' }).Count $suppressed = @($ordered | Where-Object { $_.Band -eq 'Suppressed' }).Count $degraded = @($ordered | Where-Object { $_.Degraded }).Count $rows = foreach ($t in $ordered) { $withheld = if (@($t.Withheld).Count -gt 0) { 'withheld: ' + (@($t.Withheld) -join ', ') } else { 'all signals evaluated' } $signalHtml = foreach ($s in $t.Signals) { $state = if ($s.Available) { "$([Math]::Round($s.Raw * 100))%" } else { 'n/a' } "<li><b>$(ConvertTo-TripwireHtmlEncoded $s.Name)</b> <span class=""w"">x$($s.Weight)</span> <span class=""r"">$state</span><br><span class=""why"">$(ConvertTo-TripwireHtmlEncoded $s.Rationale)</span></li>" } @" <tr class="band-$(ConvertTo-TripwireHtmlEncoded $t.Band.ToLowerInvariant())"> <td class="score"><span class="pill">$([int]$t.Confidence)</span><br><span class="band">$(ConvertTo-TripwireHtmlEncoded $t.Band)</span></td> <td> <div class="hdr">$(ConvertTo-TripwireHtmlEncoded $t.DecoyName) <span class="mono">$(ConvertTo-TripwireHtmlEncoded $t.Touch.operation)</span></div> <div class="meta">$(ConvertTo-TripwireHtmlEncoded $t.Touch.timestamp.ToString('u')) · actor $(ConvertTo-TripwireHtmlEncoded $t.ActorLabel) · $(ConvertTo-TripwireHtmlEncoded $t.Touch.ip) $(ConvertTo-TripwireHtmlEncoded $t.Touch.country) · $(ConvertTo-TripwireHtmlEncoded $t.Touch.source)</div> <div class="cov">$(ConvertTo-TripwireHtmlEncoded $withheld)</div> $(if ($t.SuppressionReason) { "<div class=""sup"">Suppressed: $(ConvertTo-TripwireHtmlEncoded $t.SuppressionReason)</div>" }) <ul class="signals">$($signalHtml -join '')</ul> </td> </tr> "@ } $demoBanner = if ($Demo) { '<div class="demo">DEMO MODE — every trip below was scored from the synthetic fixtures shipped with the module. No tenant was contacted.</div>' } else { '' } $degradedBanner = if ($degraded -gt 0) { "<div class=""warn"">$degraded trip(s) were scored with at least one signal withheld. Their confidence is a FLOOR, not a measurement — a withheld signal contributes zero and can only push a score down.</div>" } else { '' } @" <!doctype html> <html lang="en"><head><meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>EntraTripwire report</title> <style> :root{color-scheme:light dark;--bg:#0f1115;--fg:#e6e8ee;--mut:#9aa3b2;--card:#171a21;--line:#252a34; --hi:#ff5d5d;--md:#ffb020;--lo:#4a9eff;--sp:#6b7280} @media (prefers-color-scheme:light){:root{--bg:#f7f8fa;--fg:#12151b;--mut:#5b6472;--card:#fff;--line:#e3e6ec}} *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg); font:14px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:28px} h1{font-size:20px;margin:0 0 4px}.sub{color:var(--mut);margin-bottom:18px} .cards{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:18px} .c{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 16px;min-width:110px} .c .n{font-size:22px;font-weight:600}.c .l{color:var(--mut);font-size:12px} .demo{background:#1d3a5f;color:#cfe4ff;border-radius:8px;padding:10px 14px;margin-bottom:14px} .warn{background:#4a3410;color:#ffd9a0;border-radius:8px;padding:10px 14px;margin-bottom:14px} table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:10px;overflow:hidden} td{padding:12px 14px;border-top:1px solid var(--line);vertical-align:top} tr:first-child td{border-top:none} .score{width:96px;text-align:center} .pill{display:inline-block;min-width:44px;padding:4px 8px;border-radius:999px;font-weight:700;color:#0b0d11;background:var(--sp)} .band-high .pill{background:var(--hi)}.band-medium .pill{background:var(--md)}.band-low .pill{background:var(--lo)} .band{font-size:11px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em} .hdr{font-weight:600}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--mut);font-size:12px} .meta,.cov{color:var(--mut);font-size:12px}.cov{font-style:italic} .sup{color:var(--mut);font-size:12px;margin-top:4px} ul.signals{list-style:none;margin:8px 0 0;padding:0;display:grid;gap:4px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))} ul.signals li{border-left:2px solid var(--line);padding-left:8px;font-size:12px} .w,.r{color:var(--mut);font-size:11px}.why{color:var(--mut)} footer{color:var(--mut);font-size:12px;margin-top:18px} </style></head><body> <h1>EntraTripwire — decoy activity</h1> <div class="sub">$(ConvertTo-TripwireHtmlEncoded $TenantLabel) · generated $([DateTime]::UtcNow.ToString('u'))</div> $demoBanner $degradedBanner <div class="cards"> <div class="c"><div class="n">$high</div><div class="l">High confidence</div></div> <div class="c"><div class="n">$medium</div><div class="l">Medium</div></div> <div class="c"><div class="n">$low</div><div class="l">Low</div></div> <div class="c"><div class="n">$suppressed</div><div class="l">Suppressed</div></div> <div class="c"><div class="n">$degraded</div><div class="l">Scored degraded</div></div> </div> <table>$($rows -join '')</table> <footer>Confidence is a weighted score, not a probability. A withheld signal contributes zero, so a degraded score is a lower bound. Containment is never eligible below the High band.</footer> </body></html> "@ } |