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

  1. Implement the check in backend/sensor/src/detect.rs (or anomaly.rs for Tier 3).
  2. Emit a new rule_id in the NDJSON output — keep severity honest (Tier‑3 rules
  3. typically stay at MEDIUM and only the correlator escalates).

  4. Add a unit test in the same module covering positive + a near‑miss.
  5. If the rule should be auto‑mitigation‑eligible, also add a positive case in the
  6. correlator's gate (backend/correlator/mitigate.go) — Tier‑1/2 only.

  7. Update docs/SECURITY.md if the rule changes the safety invariants.

Adding a new API endpoint

  1. Handler in backend/api/main.go. Use the rows() helper for read‑only SQL.
  2. If it modifies state, gate it through requiredRole() in auth.go (analyst+ for
  3. incident actions, responder+ for binding writes, admin for users/policies/settings).

  4. Add the corresponding method on API in frontend/src/lib/api.ts.
  5. Document the shape in API.md.

Adding a new dashboard page

  1. New file under frontend/src/pages/ exporting a single React component.
  2. Add a ViewKey entry in frontend/src/types.ts.
  3. Add the sidebar entry in frontend/src/components/Sidebar.tsx.
  4. Wire it in frontend/src/App.tsx's Shell view === '…' switch.
  5. Reuse existing primitives (KPICard, Modal, severity / badge utility classes from
  6. index.css). Do not introduce new vendor scripts via <script src> — bundle through npm.

Style & conventions

  • Rust: rustfmt defaults; the sensor is libc‑only by design (no pcap, no Tokio). Keep
  • hot‑path allocations out of the per‑frame loop.

  • Go: gofmt; small handlers next to each other in main.go; helpers in same package.
  • TypeScript: strict; prefer concrete interfaces in types.ts over any. CSS variables
  • for colors — no hex literals in components.

  • Python: stdlib only (control/). Scapy is fine in generator/ because it's lab tooling.
  • Docs: [MEASURED] for numbers you ran, [VERIFY] for plausible-but-unconfirmed,
  • [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
  • commands or use the [m]l_shadow.py regex trick.

  • JetStream durable consumers get stuck on stale state. The correlator handles this on
  • startup with DeleteConsumer + DeliverNew. Don't remove that bootstrap path.

  • Sensor NATS client used to go silent on idle. PING/PONG keepalive + reconnect‑on‑error
  • in sensor/src/nats.rs fix it; don't strip the background reader.

  • Baseline accuracy is the FP battleground. The lab gateway MAC must reflect reality
  • (see backend/control/baseline_sync.py).

  • Dashboard 403s = external CDN was reintroduced. All assets live inside the Vite
  • bundle. If you see one, find the offending <script src=…> and replace with an npm import.