FANGS is a delta detector, not a classifier. It watches npm packages, runs each new release inside a Docker sandbox, and flags behavior that differs from the package’s rolling baseline. The system is built from two long-running processes —Documentation Index
Fetch the complete documentation index at: https://mintlify.com/irchaosclub/FANGS/llms.txt
Use this file to discover all available pages before exploring further.
fangs-orchestrator and fangs-runner — plus a CLI (fangs) and an optional web dashboard. Communication between the orchestrator and runner happens over HTTP (opt-in mTLS for production); storage defaults to SQLite with opt-in PostgreSQL.
System diagram
Components
fangs-orchestrator
Hosts the HTTP API, watcher, differ, and notifier. Owns the storage backend. Receives event batches from runners and triggers analysis when a run completes.
fangs-runner
Loads the eBPF sensor at startup, polls for jobs, spawns Docker sandboxes, streams captured events back to the orchestrator. Requires
CAP_BPF and the Docker socket.fangs CLI
Operator interface: add packages, submit one-off scans, review deviations, manage the allowlist, promote baselines. Talks to the orchestrator’s HTTP API.
eBPF sensor
Seven tracepoints, two kprobes, and one libssl uprobe attached at runner startup. No agent inside the container — kernel-level observation only.
End-to-end scan flow
Watcher polls npm registry
The watcher runs inside
fangs-orchestrator and polls registry.npmjs.org every 5 minutes (configurable via --watcher-interval). For each watched package it calls the registry’s metadata endpoint and compares dist-tags.latest against the last-seen version stored in the database.New release → sandbox_scan job queued
When a new version is detected the orchestrator records a
releases row (package name, version, tarball SHA-256, npm integrity, published timestamp) and enqueues a sandbox_scan job. The sandbox spec targets node:20-slim and runs npm install <pkg>@<version> followed by a 2-second grace period.Runner polls for work
The runner long-polls
GET /v1/runners/{id}/jobs. A 204 No Content response means the queue is empty; a 200 body carries a Job struct with the RunID, SandboxSpec, and WatchedPaths list.Runner creates cgroup, registers sensor, starts container
Before calling
docker start, the runner creates a parent cgroupv2 cgroup and calls Sensor.AddCgroup with the cgroup’s inode ID and the job’s WatchedPaths. Registering before the container starts closes the race window — events from the container’s very first syscall are captured. The eBPF probes are already attached globally; only processes whose cgroup is in CGMAP produce events.Sensor captures events, runner streams to orchestrator
While the container runs, the sensor emits typed events (file access, exec, net connect, DNS query, TLS SNI) into a 64 MiB kernel ringbuf. The runner reads the ringbuf, batches events into
EventBatch JSON objects, and streams them to POST /v1/runs/{run_id}/events. Each batch carries a monotonically increasing seq number; the orchestrator deduplicates on (run_id, seq).Orchestrator debounces, then runs Differ.AnalyzeRun
Each event-batch POST triggers a 2-second debounce timer. When the timer fires (no new batches for 2 s), the orchestrator calls
Differ.AnalyzeRun(ctx, runID). The differ is also called once more when the runner posts the final ScanResult.Differ extracts fingerprints, compares to baseline
AnalyzeRun loads the run’s events from the events table, applies the operator allowlist filter, and calls ExtractFingerprintsWith to produce a deduplicated set of (category, normalized_value) pairs. It then loads the package’s baseline_fingerprints. Any pair not present in the baseline becomes a Deviation row.Zero deviations → auto-promote; deviations → hold for review
If the run produces zero deviations, it is automatically marked
is_baseline=true and its fingerprints are merged into baseline_fingerprints (D38 auto-promotion). If there are deviations the run lands in fangs pending and waits for a human decision.Notifier fires webhooks
When deviations are written, the notifier dispatches webhooks to any configured targets (Slack, Discord, generic SIEM webhook). Targets are added with
fangs notifier add.Process startup order
The runner must start after the orchestrator is reachable. On first start it
POST /v1/runners/register, receives a RegistrationAck containing job_poll_interval and heartbeat_interval, then begins polling. The orchestrator restart auto-expires stale runner registrations after approximately 3× the heartbeat interval.Wire protocol summary
All orchestrator ↔ runner traffic is JSON over HTTPS. The protocol version is negotiated at registration (proto_version: 1). Key endpoints:
| Method | Path | Purpose |
|---|---|---|
POST | /v1/runners/register | Runner introduces itself at startup |
GET | /v1/runners/{id}/jobs | Runner long-polls for the next job |
POST | /v1/runs/{run_id}/events | Runner streams EventBatch objects |
POST | /v1/runs/{run_id}/result | Runner posts final ScanResult |
POST | /v1/scans | Operator submits an on-demand scan |
GET | /v1/health | Liveness probe |
Storage
The orchestrator owns all persistent state. SQLite is the default; PostgreSQL is an opt-in via--db-url. Core tables: watched_packages, releases, runs, events, baseline_fingerprints, deviations, allow_entries, runners, notifiers. Prometheus metrics are exposed at /metrics and the dashboard at /ui/.