Fragments
An AI network-security platform that maps a LAN, scores its risk, and lets you interrogate the scan in plain language — with every AI call running locally.
This is the full engineering dossier behind the project card: what the system actually does, the decisions behind each subsystem, how it's threat-modeled, and an honest account of its current limits and the hardening path forward. Every claim here is verified against the source code, not the docs.
How to read this. It's structured as a real security assessment of the project — architecture and threat model first, then deployment, testing, and a live demo script, closing with an honest hardening roadmap that names every limitation as a tracked finding. The candor is the point: auditing your own system is the core security-engineering skill.
What Fragments is, in one screen
Fragments discovers every device on a local network, assigns each a 0–100 risk score, visualizes the topology in real time, and exposes the whole picture through a retrieval-augmented chat, an attack-path simulator, a compliance assessor, and exportable PDF reports. It ships with a mock mode so the entire product runs offline, with no root and no network access, against a fixed 15-device fixture.
The stack, precisely labeled
| Layer | Technology | What it does |
|---|---|---|
| API | FastAPI · uvicorn · Python 3.11 | Async REST + a WebSocket event bus. One process, one background scan loop. |
| Frontend | Next.js 16 · React 19 · TS · D3 v7 | App-Router dashboard; D3 force-directed topology graph; markdown chat. |
| Relational | SQLite (WAL) | Devices, alerts, scans (+JSON snapshots), frameworks, assessments, CVE cache. |
| Vector | ChromaDB (persistent) | Cosine HNSW over scan-derived text chunks for RAG retrieval. |
| LLM | Ollama · qwen3:30b-a3b | Local inference over HTTP — the network map never leaves the host. |
| Scanner | scapy · python-nmap · icmplib · manuf | L2 ARP discovery, ICMP sweep fallback, service/OS scan, MAC-vendor lookup. |
| Reporting | reportlab | Multi-section security & compliance PDFs. |
The design decision worth foregrounding: inference runs on a local model via Ollama rather than a hosted API. For a tool whose entire output is a map of a network's weaknesses, keeping that data on the host is the correct privacy and security posture — not a limitation.
How the pieces fit together
A single FastAPI process fronts six subsystems and two data stores. The browser talks to it over REST for commands and over one WebSocket for live events. Everything that could touch the network or an LLM is isolated behind the scanner and AI subsystems respectively.
%%{init: {'theme':'base','themeVariables':{'background':'#0c0c0e','primaryColor':'#151517','primaryTextColor':'#e6e6e6','primaryBorderColor':'#00ff41','lineColor':'#555','secondaryColor':'#111','tertiaryColor':'#0a0a0a','clusterBkg':'#0c0c0e','clusterBorder':'#262626','fontFamily':'JetBrains Mono, monospace','fontSize':'13px','textColor':'#c0c0c0'}}}%%
flowchart TB
subgraph CLIENT["Browser · Next.js 16"]
UI["Dashboard · Devices · Threats
Simulate · Chat · Compliance · Report"]
WSH["useWebSocket hook
(auto-reconnect)"]
end
subgraph API["FastAPI process (async)"]
REST["REST router
/api/*"]
WS["WebSocket /ws
ConnectionManager"]
BG["Background scan loop
every SCAN_INTERVAL"]
end
subgraph SUB["Subsystems"]
SCAN["scanner/
ARP · nmap · OUI · OS"]
THREAT["threat/
risk_scorer · cve_lookup"]
AI["ai/
analyzer · attack_sim · RAG"]
COMP["compliance/
parser · assessor · report"]
RPT["report.py
PDF builder"]
end
subgraph DATA["State"]
SQL[("SQLite · WAL")]
VEC[("ChromaDB · cosine")]
end
subgraph EXT["Outside the host"]
NET["LAN — raw sockets
(root)"]
LLM["Ollama :11434
qwen3:30b-a3b"]
NVD["NVD API
(enrichment)"]
end
UI -->|"fetch"| REST
WSH -->|"events"| WS
REST --> SCAN & THREAT & AI & COMP & RPT
BG --> SCAN
SCAN --> NET
SCAN --> THREAT
THREAT -.->|"roadmap"| NVD
AI --> VEC
AI --> LLM
COMP --> AI
REST --> SQL
SCAN --> SQL
SCAN -->|"auto-ingest"| VEC
WS -.->|"broadcast"| WSH
Fig. 1 — Component architecture. Dashed edges are planned enrichment paths (see §12).
The core loop: scan → score → store → ingest → broadcast
A scan is the spine of the system — every other feature reads what a scan produces. The sequence below is the live path; mock mode swaps the first step for a fixture load and skips the network entirely.
%%{init: {'theme':'base','themeVariables':{'background':'#0c0c0e','primaryColor':'#151517','primaryTextColor':'#e6e6e6','primaryBorderColor':'#00ff41','lineColor':'#555','actorBkg':'#151517','actorBorder':'#00ff41','actorTextColor':'#e6e6e6','signalColor':'#666','signalTextColor':'#c0c0c0','noteBkgColor':'#111','noteTextColor':'#c0c0c0','noteBorderColor':'#333','fontFamily':'JetBrains Mono, monospace','fontSize':'12px'}}}%%
sequenceDiagram
participant U as Browser
participant A as FastAPI
participant S as Scanner
participant R as Risk scorer
participant DB as SQLite
participant C as ChromaDB
participant O as Ollama
U->>A: POST /api/scan
A->>S: run_scan()
S->>S: ARP sweep + ICMP fallback + nmap -sV
S->>R: score_device() per host
R-->>S: 0–100 risk
A->>DB: upsert devices + scan record
A->>A: rogue check → insert alerts
A->>C: ingest_scan_data() (chunk + embed)
A-->>U: WS "scan_complete" / "alert"
Note over U,A: later — analyst asks a question
U->>A: POST /api/rag/query
A->>C: retrieve(top_k=10, cosine)
C-->>A: ranked chunks
A->>O: chat(system + context + question)
O-->>A: grounded answer
A-->>U: response + sources
Fig. 2 — Scan and RAG data flow. The answer is grounded only in retrieved scan chunks.
Design decisions & the trade-offs behind them
Discovery: why ARP first, then ICMP
Live discovery leads with a scapy ARP broadcast because ARP operates at layer 2 and returns real MAC addresses — the stable identity the whole system keys on. Two augmentations show the network knowledge worth defending:
- Self-inclusion. ARP never returns the scanning host's own interface, so it is added explicitly from
conf.iface. Miss this and the scanner is invisible to itself. - ICMP sweep fallback. On NAT-isolated networks — phone hotspots with AP client isolation — ARP broadcasts don't reach peers. An
icmplibping sweep catches them, bounded to ≤1024 addresses so a/16can't blow up scan time. Hosts found this way get their MAC from the kernel ARP cache.
Port and service detection is nmap (-sV -T4 --top-ports 100 --host-timeout 30s), run inside asyncio.to_thread because python-nmap is blocking — this keeps the event loop responsive while a scan runs.
Risk scoring: a transparent weighted model
The 0–100 score is a deterministic additive model across five capped bands. It is intentionally simple and explainable — you can point at any device and reproduce its score by hand, which matters more for trust than a black box would.
| Band | Max | Basis |
|---|---|---|
| Open ports | 30 | Per-port danger weights (Telnet 8, SMB 6, RDP 5…), summed then capped. |
| Identity gaps | 15 | +5 each for missing hostname, missing vendor, unknown type. |
| OS currency | 20 | EOL keyword match → 20; unknown → 10; current → 0. |
| CVEs | 25 | 8 points per CVE, capped. A CVSS-weighted revision is on the roadmap (§12). |
| Insecure protocols | 10 | Telnet/FTP/SNMP and friends, by service name and port. |
The honest framing: this is a transparent heuristic v1, not a calibrated model. The weights are hand-chosen and the CVE band counts rather than weighting by severity — deliberate simplicity for a number people need to trust, with a clear upgrade path.
RAG: retrieval that can't hallucinate the inventory
Each device becomes a summary chunk plus one chunk per open port; alerts become their own chunks. These are upserted into ChromaDB, which — with no embedding function specified — uses its default model, all-MiniLM-L6-v2 (384-dim), over cosine space. At query time the analyzer retrieves the top-k, builds a character-budgeted context, and instructs the local LLM to answer only from that context and cite devices by IP/MAC. If ChromaDB or the LLM is unreachable, it degrades to a deterministic rule-based summary rather than failing.
Attack simulation: deterministic by design
Despite living under ai/, the simulator makes no LLM call. It is a greedy graph traversal: from a chosen origin it walks up to five hops, at each step picking the highest-value reachable target (servers and databases weighted up) where a lateral method exists — an open SSH/SMB/RDP/DB port, a shared CVE, or an untrusted host. The narration is templated. This is a deliberate choice: deterministic, reproducible, and incapable of inventing an attack path that isn't there.
Data model
Seven SQLite tables, all accessed through parameterized queries with a row-factory helper; WAL mode for concurrent reads during a scan; device writes are idempotent upserts keyed on MAC that deliberately preserve the manually-set is_trusted flag across rescans.
MAC-keyed inventory; ports/services/CVEs as JSON.
Typed, severity-tagged, ack-able.
Run records with full JSON device snapshot.
Parsed controls per uploaded framework.
Verdicts + report path per run.
24-hour NVD result cache for enrichment.
The tool is itself an attack surface
A network-reconnaissance tool that runs as root and holds a complete map of a network's weaknesses is a high-value target — so it has to be threat-modeled like one. The crown jewels are the scan database (hosts, ports, OS versions, CVEs) and the generated reports: everything an attacker would want for free.
%%{init: {'theme':'base','themeVariables':{'background':'#0c0c0e','primaryColor':'#151517','primaryTextColor':'#e6e6e6','primaryBorderColor':'#00ff41','lineColor':'#555','clusterBkg':'#0c0c0e','clusterBorder':'#262626','fontFamily':'JetBrains Mono, monospace','fontSize':'12px','textColor':'#c0c0c0'}}}%%
flowchart LR
subgraph Z1["Zone: Browser (untrusted client)"]
B["Next.js dashboard"]
end
subgraph Z2["Zone: Host — runs as ROOT"]
F["FastAPI · single process"]
P["Doc parsers"]
D[("SQLite + ChromaDB
plaintext at rest")]
end
subgraph Z3["Zone: LAN (semi-trusted)"]
N["Active scanning
scapy / nmap"]
end
subgraph Z4["Zone: External"]
L["Ollama (local by default)"]
V["NVD API"]
end
B ==>|"TB① auth boundary"| F
P ==>|"TB② untrusted upload"| F
F ==>|"TB③ root raw sockets"| N
F -->|"TB④ recon data at rest"| D
F -->|"TB⑤ local by default"| L
F --> V
Fig. 3 — Trust boundaries. TB① and TB② are the highest-severity crossings.
STRIDE
| Threat | Exposure in the current build | Mitigation direction |
|---|---|---|
| Spoofing | No auth on REST or WS — anyone who can reach the port acts as the operator. MAC spoofing defeats rogue detection (clone a trusted MAC). | Session auth; bind loopback; treat MAC as a hint, not identity. |
| Tampering | Unauthenticated trust/ack endpoints let an attacker whitelist a rogue or silence alerts. DB files have no integrity protection. | AuthZ on state changes; restrictive file perms; signed audit log. |
| Repudiation | No user identity and no audit trail — who triggered a scan, trusted a device, or exported a report is unrecorded. | Authenticated actor + append-only audit log. |
| Info disclosure | Scan DB is a plaintext network map. Report PDFs sit on disk. If the LLM endpoint is ever pointed remote, the map leaves the host. | Encrypt at rest; keep inference local (the default); sanitize download paths. |
| DoS | An unauthenticated scan trigger launches a full nmap sweep — network load plus host load — and the LLM endpoints hold 120s timeouts. | Auth + rate-limit + single-flight scan queue. |
| Elevation | The process runs as root for raw sockets, and untrusted documents are parsed in that same process — a document→root chain. | Privilege separation: tiny root scanner helper, unprivileged API; sandbox parsers. |
The design conversation this invites is privilege separation: split a minimal root-capable scanner helper from an unprivileged, authenticated API process that talks to it over a local socket, and sandbox document parsing. §10 and §12 carry that plan.
Running it
Mock mode — the demo path (no root, no network)
# Backend — offline, deterministic 15-device fixture cd backend python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt FRAGMENTS_MOCK=1 uvicorn backend.main:app --host 127.0.0.1 --port 8000 --reload # Frontend cd frontend && npm install && npm run dev # http://localhost:3000
Mock mode is the demo path. It exercises every feature — scoring, topology, attack sim, RAG chat, compliance, PDF — with zero external dependencies. If Ollama isn't running, the AI endpoints fall back to deterministic summaries, so the demo never dead-ends.
Live mode — real scanning
# Requires root for scapy/nmap raw sockets; Ollama for AI
export SCAN_SUBNET=192.168.1.0/24
sudo -E .venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8000
Hardening note. Because the API is unauthenticated and the process is root-capable, a real deployment binds loopback (as above), puts auth in front, and separates the privileged scanner from the API — rather than exposing the port on all interfaces.
Environment variables that matter
| VAR | Default | Effect |
|---|---|---|
| FRAGMENTS_MOCK | 0 | 1 = offline fixture mode. |
| SCAN_SUBNET | 192.168.1.0/24 | CIDR scanned in live mode. |
| SCAN_INTERVAL | 60 | Seconds between background scans. |
| OLLAMA_BASE_URL | localhost:11434 | LLM endpoint — kept local. |
| LLM_MODEL | qwen3:30b-a3b | Ollama model tag. |
What's covered, and what comes next
The backend has 23 pytest tests across five files, all forced into mock mode, each with an isolated temp database via an autouse fixture — no test pollutes another's state.
| Suite | Tests | Covers |
|---|---|---|
| test_risk_scorer | 5 | Band math, clamping, EOL detection. |
| test_rag_pipeline | 6 | Chunking, ingestion, retrieval shape. |
| test_compliance | 5 | Parser normalization, verdict heuristics. |
| test_database | 4 | Upsert, trust preservation, queries. |
| test_arp_scanner | 3 | Parsing and self-inclusion logic. |
The pyramid, honestly
The base — unit tests on the pure scoring and parsing logic — is solid. The next tiers are scoped and known: an integration layer driving the FastAPI app over httpx.ASGITransport with the mock scanner; a small end-to-end smoke test through the WebSocket event sequence; and CI gating all three. Naming precisely what's missing and in what order it gets built is itself part of the strategy.
A six-minute live walkthrough
Run in mock mode so nothing depends on the network or a model server. The fixture has two built-in villains — 192.168.1.100 (unknown host, Telnet + FTP, risk 88) and 192.168.1.52 (IoT camera, RTSP + Telnet, risk 78). Drive toward them.
- 0:00Open the dashboard. Name what's on screen: 15 devices in a star topology, node color = risk, node size = open-port count, live stat tiles for average risk and open alerts.
- 0:30Hit Scan. Narrate the WebSocket events — nodes pulse, and three rogue device alerts fire for the untrusted hosts. "This is the event bus, not a page refresh."
- 1:15Click
192.168.1.100. Walk the risk breakdown live: dangerous ports + identity gaps + unknown OS + a CVE + insecure protocols → 88. "Every point is traceable — no black box." - 2:15Open Attack Simulation from that host. Show the greedy path hopping toward the database server on
3306. Pre-empt the question: "This is deterministic — a weighted graph walk, not a model guessing." - 3:15Go to Chat. Ask: "Which devices are riskiest and why?" Show the grounded answer with device citations. "The model only sees retrieved scan chunks, and it runs locally — this network map never leaves the host."
- 4:15Compliance: upload a small JSON framework, run the assessment, show the Compliant / Partial / Non-Compliant split tied back to real findings (Telnet → an encryption control fails).
- 5:15Generate the security report PDF. Open it — executive summary, inventory, vulnerability findings, remediation checklist. Close on the roadmap, not the features.
Demo insurance: mock mode is fully offline and the AI endpoints degrade gracefully, so a missing model server or dead Wi-Fi won't break the run. The two pre-emptive lines — attack sim is deterministic, inference is local — are the moments that read as senior.
Ninety seconds, no jargon
Most people have no idea what's actually connected to their network — and neither do a lot of small companies. Fragments answers that. It discovers every device on a network, tells you which ones are dangerous and why, and lets you just ask it questions in plain English.
What it does · 35sIt scans the network, scores every device from 0 to 100 based on things like exposed services and outdated software, and draws a live map that updates the moment a device joins or changes. On top of that sit the features I care about most: a chat you can interrogate — "what are my riskiest devices?" — an attack simulator that shows how an intruder could hop from a weak device toward a database, and one-click compliance and PDF reporting.
Why it's mine to defend · 25sI built the whole stack — the async Python scanning engine, the vector-search chat pipeline, and the React dashboard. And I made a deliberate security call: the AI runs locally, so a complete map of a network's weaknesses never gets shipped off to someone else's servers.
The close · 15sIt runs fully offline in a demo mode, so I can show the entire thing end to end in five minutes.
The technical explanation
Fragments is a network-security platform built around one primitive: a scan. A scan produces a device inventory, and every feature is a different lens on that inventory. It's a single async FastAPI process fronting six subsystems, with SQLite for relational state and ChromaDB for vector search, and a Next.js dashboard that talks REST for commands and one WebSocket for live events.
Discovery · ~60sLive discovery starts with a scapy ARP broadcast, because layer 2 gives me real MAC addresses — that's the stable key everything else hangs off. Two things I'm proud of: I explicitly add the scanning host, because ARP never sees its own interface; and I fall back to an ICMP sweep for NAT-isolated networks where ARP broadcasts don't propagate, bounded so a big subnet can't blow up. Then nmap does service and OS detection, run in a thread so it doesn't block the event loop.
Risk & the AI layer · ~90sEach device gets a transparent 0-to-100 score across five capped bands — open ports, identity gaps, OS end-of-life, CVE count, and insecure protocols. I chose additive-and-explainable over a model on purpose: I can reproduce any score by hand, which is what makes people trust it. For the chat, I turn each device and alert into text chunks, embed them into ChromaDB, and at query time retrieve the top matches, build a bounded context, and have a local LLM answer strictly from that context with device citations. Running inference locally is a deliberate security decision — the whole point of the tool is to find weaknesses, so shipping that map to a third-party API would undercut it.
Honest edges · ~45sTwo things I'd flag myself. The attack simulator is deterministic — a weighted greedy walk toward high-value hosts, not a model, which I prefer because it can't invent a path that isn't there. And the risk model is a heuristic v1: it counts CVEs rather than weighting them by CVSS, which is the first thing I'd fix. It runs fully offline in mock mode, which is how I demo it.
The questions this project invites
Grouped by where the pressure comes from. The answers concede real limitations on purpose — a strong panel rewards that far more than a confident dodge.
On the AI layer
QWhich model, and why local?
Local by design. "It runs a local Qwen 3 model through Ollama. I'd defend that as the right call: the tool produces a full map of a network's vulnerabilities, and I don't want that leaving the host to a third-party API. The abstraction is a single chat endpoint, so swapping in a hosted model is a config change if a deployment wants it."
QIs the attack simulator using the model?
No — and that's intentional. "It's a deterministic greedy graph traversal that prefers high-value reachable targets. I keep it out of the model deliberately: a security tool that invents plausible-but-fake attack paths is worse than useless. If I added an LLM there, it would be a narration layer over the deterministic path, never the pathfinding itself."
QWhat embedding model, and did you evaluate it?
Know the real answer. "ChromaDB's default, all-MiniLM-L6-v2, 384 dimensions, cosine space. It's fine for this scale, but I haven't run a retrieval eval, so I won't claim it's optimal. If retrieval quality mattered at scale I'd benchmark alternatives and add a re-ranking step."
On the risk model
QThree low-severity CVEs outscore one critical RCE. Defend that.
Name the flaw. "That's a real limitation — the CVE band counts CVEs at eight points each and ignores CVSS, so it mis-ranks exactly that case. The fix is to weight by CVSS base score and cap on the worst finding rather than the count. It's the first change I'd make to the scorer."
QWhere did the weights come from?
Own the heuristic. "They're hand-chosen from severity intuition — Telnet worse than HTTP — not calibrated against incident data. It's a transparent heuristic v1. The upgrade path is to treat scoring as a model trained on labeled outcomes, but for a number people need to trust, an explainable additive model was the right start."
On securing the tool itself
QYou run as root with an unauthenticated API. What's the blast radius of one bug?
This is the big one. "Today it's total — any RCE is root, and untrusted document parsing happens in that same root process. The right architecture is privilege separation: a tiny scanner helper that holds the raw-socket capability, and an unprivileged API process that talks to it over a local socket. Add auth, bind loopback, and sandbox the parsers. I built the single-process version for velocity; I know exactly how I'd break it apart."
QActive scanning is itself hostile traffic. What are the consequences?
Show operational awareness. "An nmap sweep will trip an IDS and, on networks you don't own, may violate policy or law. In production I'd gate scanning behind explicit scope authorization, rate-limit it, and offer a passive discovery mode. And since the scan trigger is unauthenticated today, it's also a DoS primitive — a real finding I'd close with auth and rate limiting."
On architecture & scale
QSQLite and one process — where does this fall over?
Know the ceiling. "It's a single-host tool by design, so SQLite in WAL mode is genuinely appropriate — one writer, concurrent readers, no server to run. It falls over the moment you want multiple concurrent scanners or a shared multi-tenant view: I'd move to Postgres, pull scanning into workers behind a queue, and give the vector store its own service. Chat history is a module-level global today — fine for one operator, wrong for many — so that becomes per-session state."
QWhy WebSocket instead of polling?
Tie it to the domain. "Discovery events are inherently push — a device joining or a port changing happens on the scanner's clock, not the browser's. Polling would either lag or hammer the API. The connection manager broadcasts typed events and the client hook auto-reconnects."
The honest roadmap
Stated the way a credible engineer would: current constraints first, then the ordered path out of them.
Single-tenant, single-host
One operator, one process, local state. Multi-user needs auth, per-session state, Postgres, and scanner workers behind a queue.
Heuristic scoring
Transparent but uncalibrated. Next: CVSS-weighted CVE band, then outcome-labeled scoring as a model.
No privilege separation
Root API + in-process parsing. Next: split the raw-socket scanner from an unprivileged, authenticated API.
Enrichment to wire in
NVD lookup exists behind a cache; wiring it into host profiling with CPE matching and rate limiting is the build.
Thin RAG evaluation
Default embeddings, no benchmark, no re-ranking. Next: a small retrieval eval set and a re-rank pass.
Ops maturity
Partial test pyramid, no CI yet. Next: CI gate, containerization, and the API + E2E test tiers.
Genuine strengths to lead with: fully parameterized SQL throughout (no injection surface in the query layer); a network-aware discovery engine with real L2 knowledge; clean async boundaries that keep blocking work off the event loop; a legitimately good D3 force-directed topology with drag, zoom, and live pulse; and a privacy-preserving local-inference design that's the correct call for this domain.
Known limitations, tracked as findings
Auditing your own system is the core security-engineering discipline, so this project is documented like any other target — each limitation logged as a severity-tagged finding with a fix. This is the roadmap from a winning hackathon build toward production hardening.
- Detail
- The CVE band is
min(25, len(cves) * 8), so three low-severity CVEs can outrank one critical RCE. - Fix
- Weight by CVSS base score; cap on the worst finding, not the count. First change to the scorer.
- Detail
- No auth on REST or WS; live mode is root for raw sockets; untrusted documents are parsed in-process; chat history is a shared global.
- Fix
- Privilege-separate the scanner, add authentication, bind loopback, and sandbox parsers.
- Detail
- A deliberate choice — no hallucinated paths — but it should be described as a heuristic simulator, not "AI."
- Fix
- Label it precisely; optionally add an explicit LLM narration layer over the deterministic path.
- Detail
- The lookup and its 24-hour cache exist, but host profiling doesn't call them, so live-mode devices carry no CVEs.
- Fix
- Call enrichment during profiling with CPE-based matching, NVD rate limiting, and an API key.
- Detail
- The manual scan path writes the device before checking existence, so it re-fires a rogue alert each run. The background loop diffs device sets first and is correct.
- Fix
- Snapshot existence before writing in the manual path; dedupe alerts on
(type, mac).
- Detail
- The download route joins the raw path segment to the reports directory. The plain path param blocks slashes, but nothing explicitly sanitizes the input.
- Fix
- Reduce to
os.path.basename, verify containment in the reports directory, and serve by ID.
- Detail
- Strong unit base, but no endpoint/E2E tiers, no CI gate, and container packaging still to complete.
- Fix
- Add
httpx.ASGITransportAPI tests, a WS smoke test, real Dockerfiles, and a CI workflow.
Sequenced: F1 and F2 are the substantive engineering upgrades — a CVSS-weighted score and a privilege-separated, authenticated architecture. F3–F5 are precise fixes with obvious solutions. F6–F7 are the hardening and ops polish that take a hackathon winner to production-grade.