‹ Omar Yousef Project Dossier // Fragments
Network Security Platform Source-verified 🏆 1st Place CTF

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.

19
REST + WS endpoints
6
Backend subsystems
2
Data stores
23
Backend tests
100%
Local inference
i

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.

01 System Overview

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

LayerTechnologyWhat it does
APIFastAPI · uvicorn · Python 3.11Async REST + a WebSocket event bus. One process, one background scan loop.
FrontendNext.js 16 · React 19 · TS · D3 v7App-Router dashboard; D3 force-directed topology graph; markdown chat.
RelationalSQLite (WAL)Devices, alerts, scans (+JSON snapshots), frameworks, assessments, CVE cache.
VectorChromaDB (persistent)Cosine HNSW over scan-derived text chunks for RAG retrieval.
LLMOllama · qwen3:30b-a3bLocal inference over HTTP — the network map never leaves the host.
Scannerscapy · python-nmap · icmplib · manufL2 ARP discovery, ICMP sweep fallback, service/OS scan, MAC-vendor lookup.
ReportingreportlabMulti-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.

02 Architecture

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.

03 Technical Design

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 icmplib ping sweep catches them, bounded to ≤1024 addresses so a /16 can'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.

BandMaxBasis
Open ports30Per-port danger weights (Telnet 8, SMB 6, RDP 5…), summed then capped.
Identity gaps15+5 each for missing hostname, missing vendor, unknown type.
OS currency20EOL keyword match → 20; unknown → 10; current → 0.
CVEs258 points per CVE, capped. A CVSS-weighted revision is on the roadmap (§12).
Insecure protocols10Telnet/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.

devices

MAC-keyed inventory; ports/services/CVEs as JSON.

alerts

Typed, severity-tagged, ack-able.

scans

Run records with full JSON device snapshot.

compliance_frameworks

Parsed controls per uploaded framework.

compliance_assessments

Verdicts + report path per run.

cve_cache

24-hour NVD result cache for enrichment.

04 Threat Model

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

ThreatExposure in the current buildMitigation direction
SpoofingNo 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.
TamperingUnauthenticated 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.
RepudiationNo 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 disclosureScan 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.
DoSAn 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.
ElevationThe 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.

05 Deployment

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
i

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

VARDefaultEffect
FRAGMENTS_MOCK01 = offline fixture mode.
SCAN_SUBNET192.168.1.0/24CIDR scanned in live mode.
SCAN_INTERVAL60Seconds between background scans.
OLLAMA_BASE_URLlocalhost:11434LLM endpoint — kept local.
LLM_MODELqwen3:30b-a3bOllama model tag.
06 Testing Strategy

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.

SuiteTestsCovers
test_risk_scorer5Band math, clamping, EOL detection.
test_rag_pipeline6Chunking, ingestion, retrieval shape.
test_compliance5Parser normalization, verdict heuristics.
test_database4Upsert, trust preservation, queries.
test_arp_scanner3Parsing 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.

07 Demo Script

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.

08 Recruiter Pitch

Ninety seconds, no jargon

The hook · 15s

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 · 35s

It 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 · 25s

I 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 · 15s

It runs fully offline in a demo mode, so I can show the entire thing end to end in five minutes.

"I built the async scanner, the retrieval pipeline, and the dashboard — and I kept every AI call on the box, because a network's weak points are the last thing you want leaving the building."
09 Five-Minute Deep Dive

The technical explanation

Problem & shape · ~45s

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 · ~60s

Live 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 · ~90s

Each 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 · ~45s

Two 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.

10 Interview Q&A

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."

11 Limitations & Future Work

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.

12 Hardening Roadmap

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.

HighF1 · Methodology
Risk score ignores CVSS — CVEs are counted, not weighted
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.
HighF2 · Security posture
Unauthenticated API on a root-capable process
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.
MediumF3 · Framing
Attack simulation is deterministic, not model-driven
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.
MediumF4 · Enrichment
NVD CVE lookup isn't wired into the live scan pipeline
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.
MediumF5 · Correctness
Manual scans re-alert a known device; the background loop doesn't
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).
LowF6 · Hardening
Report download builds a path from a URL segment
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.
LowF7 · Ops maturity
Test pyramid, containerization, and CI to finish
Detail
Strong unit base, but no endpoint/E2E tiers, no CI gate, and container packaging still to complete.
Fix
Add httpx.ASGITransport API 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.