Development
How to build, change and extend the platform. Read ARCHITECTURE.md first if you haven't — this doc assumes you already know the components.
Workspace layout
backend/ Rust sensor + Go services + Python control tools
sensor/ Rust 1.95 — AF_PACKET, Tier 1/2/3, NATS client
correlator/ Go — JetStream consumer + entity graph + mitigation controller + latency probe
api/ Go — REST + SSE + RBAC; embeds the SPA via go:embed static/*
control/ Python stdlib — baseline_sync, approve, dhcp_lease, siem_connector, ml_*
generator/ Python (Scapy) — lab attack tooling, dataset/eval/fuzz/load
tools/ Python — nats_sub.py and friends
Makefile backend-local commands; the repo-root Makefile re-exports the same targets
frontend/ Vite + React 18 + TS — operator console; builds into backend/api/static
infra/ docker-compose · SQL schema · Grafana · Prometheus · Ansible · systemd
tests/ Playwright E2E
docs/ what you're reading
marketing/ presentation deck + landing-site content (not part of the platform)
Build & run
The repo root and backend/ both expose make help to list targets. Most common:
# data plane
make stack # docker compose up: NATS + PG + Redis + Prometheus + Grafana
make db-migrate # apply infra/sql/00*.sql in order
# sensor (Rust)
make sensor # cargo build --release
make sensor-test # cargo test --release
make sensor-bench # ./target/release/arpg-sensor bench 2000000 (proves <2ms SLA)
make caps # setcap cap_net_raw,cap_net_admin+eip ← re-run after every rebuild
make sensor-selftest # synthetic Tier 1/2/3 verdicts; no privileges needed
make sensor-baseline # live sensor with PG-driven baseline → NATS
# correlator + mitigation (Go)
make correlator MODE=guarded # monitor | guarded | enforce
# API + SPA (Go, embeds frontend build)
make sync-dashboard # frontend npm run build → backend/api/static → go build
make api # serves :8080
# dashboard hot reload (Vite)
cd frontend && npm install && npm run dev # :5173, proxies /api and /events to :8080
# evaluation
make eval # labeled-dataset precision/recall/FP
make fuzz # adversarial harness
make latency-probe # detect→mitigate p95
Detection JSON contract (sensor → NATS → correlator)
{
"ts": "2026-05-23T10:42:13.412Z",
"site": "lab",
"vlan": 10,
"sensor_id": "rust-sensor-managed-1",
"rule_id": "BIND-FLIP", // FORGED-GW | MAC-MISMATCH | BCAST-SHA | UNSOL-REPLY | GARP-STORM | BIND-FLIP | CARD-MULTI-IP | ...
"severity": "CRITICAL", // CRITICAL | HIGH | MEDIUM | LOW
"confidence": 0.97,
"mitre": "T1557.002",
"eth_src": "aa:bb:cc:00:00:01",
"sha": "aa:bb:cc:00:00:01", // ARP sender hardware address
"spa": "192.168.10.1",
"tpa": "192.168.10.50",
"gratuitous": false,
"note": "unknown-mac-for-protected-ip",
"pcap_ref": "console/aa:bb:cc:00:00:01/12345.pcap"
}
Bus subject pattern: arp.<site>.<vlan> — lab subject is arp.lab.10. Keep the field set minimal; the correlator and API tolerate unknown keys, but adding required fields needs a backwards‑compatible plan.
Database schema
Source of truth: infra/sql/001..006. Apply in order; idempotent (IF NOT EXISTS). Key trap to remember: vmac_allowlist.vmac (not mac) — an early script silently skipped rows by using the wrong name. Add migrations as new numbered files; never edit 001_baseline.sql once shipped.
Adding a new detection rule
- Implement the check in
backend/sensor/src/detect.rs(oranomaly.rsfor Tier 3). - Emit a new
rule_idin the NDJSON output — keepseverityhonest (Tier‑3 rules - Add a unit test in the same module covering positive + a near‑miss.
- If the rule should be auto‑mitigation‑eligible, also add a positive case in the
- Update
docs/SECURITY.mdif the rule changes the safety invariants.
typically stay at MEDIUM and only the correlator escalates).
correlator's gate (backend/correlator/mitigate.go) — Tier‑1/2 only.
Adding a new API endpoint
- Handler in
backend/api/main.go. Use therows()helper for read‑only SQL. - If it modifies state, gate it through
requiredRole()inauth.go(analyst+ for - Add the corresponding method on
APIinfrontend/src/lib/api.ts. - Document the shape in API.md.
incident actions, responder+ for binding writes, admin for users/policies/settings).
Adding a new dashboard page
- New file under
frontend/src/pages/exporting a single React component. - Add a
ViewKeyentry infrontend/src/types.ts. - Add the sidebar entry in
frontend/src/components/Sidebar.tsx. - Wire it in
frontend/src/App.tsx'sShellview === '…'switch. - Reuse existing primitives (
KPICard,Modal, severity / badge utility classes from
index.css). Do not introduce new vendor scripts via <script src> — bundle through npm.
Style & conventions
- Rust:
rustfmtdefaults; the sensor is libc‑only by design (nopcap, no Tokio). Keep - Go:
gofmt; small handlers next to each other inmain.go; helpers in same package. - TypeScript: strict; prefer concrete interfaces in
types.tsoverany. CSS variables - Python: stdlib only (
control/). Scapy is fine ingenerator/because it's lab tooling. - Docs:
[MEASURED]for numbers you ran,[VERIFY]for plausible-but-unconfirmed,
hot‑path allocations out of the per‑frame loop.
for colors — no hex literals in components.
[ASSUMPTION] / [ESTIMATE] for derived values. Never publish projected numbers as measured ones. (See `../CLAUDE.md`.)
Common gotchas
- Caps drop after every cargo build. Re‑run
make caps. - `pkill -f ml_shadow.py` matches itself — split kill/start across separate shell
- JetStream durable consumers get stuck on stale state. The correlator handles this on
- Sensor NATS client used to go silent on idle. PING/PONG keepalive + reconnect‑on‑error
- Baseline accuracy is the FP battleground. The lab gateway MAC must reflect reality
- Dashboard 403s = external CDN was reintroduced. All assets live inside the Vite
commands or use the [m]l_shadow.py regex trick.
startup with DeleteConsumer + DeliverNew. Don't remove that bootstrap path.
in sensor/src/nats.rs fix it; don't strip the background reader.
(see backend/control/baseline_sync.py).
bundle. If you see one, find the offending <script src=…> and replace with an npm import.