Phase 5: Findings diagnostics panel; pin Go toolchain 1.27.2 (0 called stdlib vulns); npm audit clean

This commit is contained in:
Andy Dixon
2026-10-09 22:01:03 +01:00
parent 68702f1b2e
commit fa5445e4a4
8 changed files with 185 additions and 0 deletions
View File
+3
View File
@@ -13,6 +13,7 @@ import Conversations from "./pages/Conversations";
import DnsExplorer from "./pages/DnsExplorer";
import TlsExplorer from "./pages/TlsExplorer";
import Statistics from "./pages/Statistics";
import Findings from "./pages/Findings";
import Settings from "./pages/Settings";
export interface Prefs { theme: "dark" | "light" | "system"; density: "compact" | "comfortable"; scale: number; motion: "system" | "reduce" }
@@ -28,6 +29,7 @@ const NAV: [string, string, string][] = [
["/dns", "DNS Explorer", "◇"],
["/tls", "TLS Explorer", "⛨"],
["/statistics", "Statistics", "▦"],
["/findings", "Findings", "⚑"],
["/library", "Capture Library", "▤"],
["/devices", "Devices", "⌬"],
["", "", ""],
@@ -97,6 +99,7 @@ export default function App() {
if (path.startsWith("/dns")) return <DnsExplorer sessionId={path.split("/")[2] ?? ""} />;
if (path.startsWith("/tls")) return <TlsExplorer sessionId={path.split("/")[2] ?? ""} />;
if (path.startsWith("/statistics")) return <Statistics sessionId={path.split("/")[2] ?? ""} />;
if (path.startsWith("/findings")) return <Findings sessionId={path.split("/")[2] ?? ""} />;
if (path === "/library") return <Library />;
if (path === "/devices") return <Devices />;
if (path === "/network") return <NetworkPage />;
+2
View File
@@ -96,3 +96,5 @@ export interface FollowResult { seg: number; stream: number; chunks: StreamChunk
export interface DNSRecord { num: number; seg: number; time: number; client: string; server: string; query: string; type: string; response: boolean; rcode: string; answers: string }
export interface TLSRecord { num: number; seg: number; time: number; client: string; server: string; server_port: number; sni: string; version: string; cipher: string; alpn: string; stream: number | null }
export interface CertRecord { num: number; seg: number; time: number; server: string; subject: string; issuer: string; sans: string; not_before: string; not_after: string }
export interface Finding { rule: string; title: string; severity: "info" | "low" | "medium"; count: number; flows: number; examples: number[]; filter: string; note: string }
+47
View File
@@ -0,0 +1,47 @@
import { type Finding, type Session } from "../api";
import { Badge, go, usePoll } from "../ui";
import { SessionPicker } from "../components/chrome";
const sevRank: Record<string, number> = { medium: 0, low: 1, info: 2 };
export default function Findings({ sessionId }: { sessionId: string }) {
const [sessions] = usePoll<Session[]>("/api/captures", 5000);
const id = sessionId || sessions?.[0]?.id || "";
const [findings, err] = usePoll<Finding[]>(id ? `/api/captures/${id}/findings?min=1` : null);
const sorted = [...(findings ?? [])].sort((a, b) => (sevRank[a.severity] - sevRank[b.severity]) || b.count - a.count);
return (
<div className="page">
<div className="page-head">
<div><h1>Findings</h1><p>Observable network events with evidence. These are facts about the capture, not judgements: each one lists benign explanations.</p></div>
<SessionPicker sessions={sessions} id={id} base="/findings" />
</div>
<div className="banner info" style={{ marginBottom: "1rem" }}><div>
<b>This is a forensic aid, not an intrusion-detection system.</b>
A finding means a pattern was observed (a retransmission, a SERVFAIL). It does not imply an attack or a fault. Use the evidence to investigate.
</div></div>
{err && <div className="banner bad"><div>{err}</div></div>}
{findings && sorted.length === 0 && <div className="empty">No notable events observed in this capture.</div>}
<div className="stack">
{sorted.map((f) => (
<div className="card" key={f.rule}>
<div className="row spread">
<div className="row"><Badge kind={f.severity === "medium" ? "warn" : f.severity === "low" ? "plain" : "ok"}>{f.severity}</Badge><h2 style={{ fontSize: "1rem" }}>{f.title}</h2></div>
<div className="row">
<span className="mono">{f.count.toLocaleString()} packet{f.count === 1 ? "" : "s"}{f.flows ? ` · ${f.flows} flow${f.flows === 1 ? "" : "s"}` : ""}</span>
{f.filter && <button className="small" onClick={() => go(`/analyse/${id}?filter=${encodeURIComponent(f.filter)}`)}>View packets</button>}
</div>
</div>
<p className="hint" style={{ margin: ".5rem 0 .3rem" }}>{f.note}</p>
<div className="row" style={{ gap: ".4rem" }}>
<span className="hint">Detection:</span><code style={{ fontSize: ".8rem" }}>{f.filter || "capture drop counters"}</code>
{f.examples.length > 0 && <><span className="hint">· e.g. packets</span>{f.examples.slice(0, 8).map((n) => (
<a key={n} href="#" className="mono" onClick={(e) => { e.preventDefault(); go(`/analyse/${id}?filter=frame.number==${n}`); }}>#{n}</a>
))}</>}
</div>
</div>
))}
</div>
</div>
);
}