Fragments
An AI tool that finds every device on a network, scores how risky each one is, and answers questions about the network in plain English. All AI runs locally.
Fragments scans a network, gives every device a 0 to 100 risk score you can trace point by point, and draws a live map that updates the moment something joins or changes. You can ask it questions about the network, watch a simulated attacker move through it, and export a full PDF security report. Built in six hours at Hack the Bay 2026, where it won 1st place in the CTF and placed 4th overall.
What Fragments does
Fragments discovers every device on a local network, assigns each a 0 to 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 that runs the entire product offline against a fixed 15-device fixture, so it can be demoed anywhere.
Highlights
- Award winner. 1st place CTF and 4th place overall at Hack the Bay 2026.
- Built in six hours as a complete, working product.
- Explainable scores. Every device's risk score can be traced point by point, no black box.
- Private by design. The AI runs on the local machine, so the network's weak points never leave it.
The stack
| 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. |
One decision shapes the whole product: inference runs on a local model through Ollama rather than a hosted API. The tool's entire output is a map of a network's weaknesses, and that map never leaves the host.
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
(scan target)"]
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 -.->|"optional"| NVD
AI --> VEC
AI --> LLM
COMP --> AI
REST --> SQL
SCAN --> SQL
SCAN -->|"auto-ingest"| VEC
WS -.->|"broadcast"| WSH
Fig. 1 · Component architecture. One async process fronts six subsystems.
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, the 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. Answers are grounded only in retrieved scan chunks.
The engineering behind it
Discovery: 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 details make the discovery engine solid:
- Self-inclusion. ARP never returns the scanning host's own interface, so the scanner adds itself explicitly from
conf.iface. Without this, the scanner is invisible to itself. - ICMP sweep fallback. On NAT-isolated networks like phone hotspots, ARP broadcasts don't reach peers. An
icmplibping sweep catches them, bounded to 1024 addresses so a large subnet 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. The event loop stays responsive while a scan runs.
Risk scoring: a transparent weighted model
The 0 to 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 is what makes people trust the number.
| 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. |
| Insecure protocols | 10 | Telnet/FTP/SNMP and friends, by service name and port. |
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 using its default embedding model, all-MiniLM-L6-v2, over cosine space. At query time the analyzer retrieves the top matches, builds a character-budgeted context, and instructs the local LLM to answer only from that context and cite devices by IP and MAC. If ChromaDB or the LLM is unreachable, it degrades to a deterministic rule-based summary rather than failing.
Attack simulation: deterministic by design
The attack simulator makes no LLM call, on purpose. 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, such as an open SSH/SMB/RDP/DB port, a shared CVE, or an untrusted host. The result is 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.
A security tool built like one
A scanner that holds a complete map of a network's weaknesses is itself a high-value target, so the security decisions are baked into the design rather than bolted on.
Local inference only
The chat runs on a local Qwen 3 model through Ollama. The scan data, the crown jewels, never leaves the host for a third-party API.
No injection surface in the query layer
Every SQLite access goes through parameterized queries with a shared row-factory helper. There is no string-built SQL anywhere in the codebase.
Bounded, polite scanning
The ICMP sweep is capped at 1024 addresses and nmap runs with host timeouts, so a scan can't hammer a network or hang the tool.
Offline demo mode
Mock mode runs the full product with no root, no network access, and no external services. Nothing sensitive is touched to show the tool working.
Grounded AI answers
The chat can only answer from retrieved scan chunks and cites devices by IP and MAC. It cannot invent hosts that aren't in the inventory.
Deterministic attack paths
Attack simulation is a reproducible graph walk over real scan data, so it never fabricates a plausible-but-fake intrusion path.
Two modes: offline demo and live scan
Mock mode: the demo path
# 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 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 a 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
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. |
Deterministic tests, no network required
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 touches the network and 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. |
Six minutes, fully offline
The demo runs in mock mode, so nothing depends on the network or a model server. The fixture includes two deliberately risky hosts: an unknown device with Telnet and FTP open (risk 88) and an IoT camera exposing RTSP and Telnet (risk 78).
- 0:00The dashboard opens on 15 devices in a star topology: node color is risk, node size is open-port count, with live stat tiles for average risk and open alerts.
- 0:30A scan kicks off. Nodes pulse as WebSocket events land, and rogue device alerts fire for the untrusted hosts.
- 1:15Clicking the risky host shows the risk breakdown: dangerous ports, identity gaps, unknown OS, a CVE, and insecure protocols adding up to 88. Every point is traceable.
- 2:15Attack simulation walks a path from that host toward the database server on
3306. - 3:15Chat: "Which devices are riskiest and why?" gets a grounded answer with device citations from the local model.
- 4:15Compliance: an uploaded framework is assessed into a Compliant / Partial / Non-Compliant split tied to real findings, like Telnet failing an encryption control.
- 5:15The security report PDF generates: executive summary, inventory, vulnerability findings, and a remediation checklist.