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 persists all scan state — package watches, run lifecycle, captured events, deviation findings, webhook targets, and operator allowlists — in a single SQLite database. The schema is applied through four numbered migration files; each migration is idempotent and additive. The Backend interface in the storage package is the only consumer of this schema, meaning a future PostgreSQL backend would implement the same interface against an equivalent schema without changing any orchestrator code. Foreign keys are enforced at connection time via PRAGMA foreign_keys = ON.
Migration history
| Migration | Tables added / changed |
|---|
0001_init | packages, releases, runs, events, baseline_fingerprints, deviations, notifications (v1, per-deviation) |
0002_notifiers | Drops notifications v1; adds notifiers, notifications v2 (per-run, per-notifier) |
0003_run_result | Adds events_emitted, events_dropped, duration_ns columns to runs |
0004_allowlists | Adds allowlists table |
packages
Tracks the set of npm packages the Watcher polls on every cycle. Adding a package to this table causes FANGS to check the npm registry for new releases every 5 minutes (default interval) and automatically queue a sandbox scan when a new version appears.
CREATE TABLE packages (
name TEXT PRIMARY KEY,
added_at TEXT NOT NULL,
last_checked_at TEXT,
last_seen_version TEXT
);
| Column | Type | Description |
|---|
name | TEXT PK | npm package name, e.g. "axios". Primary key. |
added_at | TEXT NOT NULL | RFC 3339 timestamp when the package was added to the watch list. |
last_checked_at | TEXT | RFC 3339 timestamp of the most recent successful registry poll. NULL until the first poll completes. |
last_seen_version | TEXT | The latest version string observed during the last poll. When a new poll returns a different value, the Watcher creates a releases row and queues a scan. |
releases
An append-only record of every package version discovered by the Watcher. One row per (package_name, version) pair. Inserting a duplicate is a no-op, so a Watcher restart after a crash cannot create duplicate release rows.
CREATE TABLE releases (
package_name TEXT NOT NULL REFERENCES packages(name) ON DELETE CASCADE,
version TEXT NOT NULL,
tarball_sha256 TEXT NOT NULL,
npm_integrity TEXT NOT NULL,
published_at TEXT NOT NULL,
discovered_at TEXT NOT NULL,
PRIMARY KEY (package_name, version)
);
| Column | Type | Description |
|---|
package_name | TEXT NOT NULL FK | References packages(name). Cascades on delete. |
version | TEXT NOT NULL | npm version string, e.g. "1.7.9". |
tarball_sha256 | TEXT NOT NULL | SHA-256 hex digest of the package tarball. Used for integrity verification. |
npm_integrity | TEXT NOT NULL | The integrity field from the npm registry manifest (SRI format, e.g. sha512-...). |
published_at | TEXT NOT NULL | RFC 3339 timestamp when the version was published to npm. |
discovered_at | TEXT NOT NULL | RFC 3339 timestamp when FANGS first observed this release. |
runs
The central table: one row per scan execution. Tracks lifecycle state from pending through done or failed, along with run metadata and final scan statistics.
CREATE TABLE runs (
id TEXT PRIMARY KEY,
package_name TEXT NOT NULL DEFAULT '',
version TEXT NOT NULL DEFAULT '',
tarball_sha256 TEXT NOT NULL DEFAULT '',
lockfile_sha256 TEXT NOT NULL DEFAULT '',
node_version TEXT NOT NULL DEFAULT '',
npm_version TEXT NOT NULL DEFAULT '',
state TEXT NOT NULL,
attempt INTEGER NOT NULL DEFAULT 1,
is_baseline INTEGER NOT NULL DEFAULT 0,
started_at TEXT,
finished_at TEXT,
failure_reason TEXT NOT NULL DEFAULT '',
-- Added in migration 0003:
events_emitted INTEGER NOT NULL DEFAULT 0,
events_dropped INTEGER NOT NULL DEFAULT 0,
duration_ns INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX runs_by_pkg_state ON runs (package_name, state);
CREATE INDEX runs_by_finished ON runs (finished_at);
| Column | Type | Description |
|---|
id | TEXT PK | Hex-encoded 16-byte run identifier generated by the orchestrator at dispatch time. |
package_name | TEXT NOT NULL | npm package name. Defaults to the sandbox image name for ad-hoc runs without an explicit package. |
version | TEXT NOT NULL | Package version string. |
tarball_sha256 | TEXT NOT NULL | Expected tarball digest (populated when available from the registry). |
lockfile_sha256 | TEXT NOT NULL | SHA-256 of the package-lock.json generated during the install (populated by the runner when available). |
node_version | TEXT NOT NULL | Node.js version inside the sandbox container. |
npm_version | TEXT NOT NULL | npm version inside the sandbox container. |
state | TEXT NOT NULL | Current lifecycle state. See the state machine below. |
attempt | INTEGER | Retry counter, starting at 1. Incremented on re-queue after failure. |
is_baseline | INTEGER | 1 when this run has been promoted to baseline (zero deviations, or manually approved). 0 otherwise. |
started_at | TEXT | RFC 3339 timestamp when the runner began execution. NULL until the run leaves pending. |
finished_at | TEXT | RFC 3339 timestamp when the run reached a terminal state. NULL until done or failed. |
failure_reason | TEXT | Human-readable description of why the run failed. Empty on success. |
events_emitted | INTEGER | Total events the sensor emitted (from POST /v1/runs/{id}/result). Added in migration 0003. |
events_dropped | INTEGER | Events dropped due to ring-buffer or queue overflow. Non-zero indicates incomplete coverage. Added in migration 0003. |
duration_ns | INTEGER | Actual wall-clock scan duration in nanoseconds. Added in migration 0003. |
Run state machine
pending → building → sandboxed → analyzed → done
└──────────────────────────────────────→ failed
| State | Meaning |
|---|
pending | Queued, awaiting runner pickup |
building | Runner is pulling the container image |
sandboxed | Container is running; sensor is attached |
analyzed | Differ has completed deviation analysis |
done | Run completed successfully |
failed | Run terminated with an error |
Indexes
runs_by_pkg_state (package_name, state) — used by the Differ and CLI package-view queries.
runs_by_finished (finished_at) — used for time-range queries and event retention pruning.
events
Append-only log of every eBPF event captured during a run. Rows are inserted in batches by AppendEvents as the runner streams data via POST /v1/runs/{run_id}/events. The data column holds the full JSON-serialized payload for the event type.
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
ts_ns INTEGER NOT NULL,
type TEXT NOT NULL,
data TEXT NOT NULL
);
CREATE INDEX events_by_run ON events (run_id, ts_ns);
| Column | Type | Description |
|---|
id | INTEGER PK AUTOINCREMENT | Auto-incrementing row ID. Used as evidence_event_id in the deviations table. |
run_id | TEXT NOT NULL FK | References runs(id). Cascades on delete. |
ts_ns | INTEGER NOT NULL | Nanosecond-precision wall-clock timestamp recorded by the orchestrator on receipt (not the eBPF kernel timestamp). |
type | TEXT NOT NULL | Event type string: file_access, exec, net_connect, dns_query, or tls_sni. |
data | TEXT NOT NULL | JSON-serialized event payload. Schema depends on type; see the Events API for payload field details. |
Index
events_by_run (run_id, ts_ns) — used by ListEventsByRun to stream events in chronological order.
Raw events are retained for approximately 90 days by default (configurable). Events referenced as evidence_event_id by an existing deviations row are excluded from pruning — the “click to see evidence” link on a deviation remains valid indefinitely even after the raw retention window expires.
baseline_fingerprints
Rolling memory of “what this package legitimately does.” Each row is a (package_name, category, value) triple observed across all baseline-promoted runs. The Differ compares each new run’s observations against this table to determine whether a behavior is novel (a deviation) or known-good.
CREATE TABLE baseline_fingerprints (
package_name TEXT NOT NULL,
category TEXT NOT NULL,
value TEXT NOT NULL,
first_seen_run_id TEXT NOT NULL,
last_seen_run_id TEXT NOT NULL,
occurrence_count INTEGER NOT NULL,
PRIMARY KEY (package_name, category, value)
);
| Column | Type | Description |
|---|
package_name | TEXT NOT NULL | npm package name. Part of the composite primary key. |
category | TEXT NOT NULL | Deviation category, matching the values in deviations.category. |
value | TEXT NOT NULL | The normalized value, e.g. an IP address, hostname, or file path. |
first_seen_run_id | TEXT NOT NULL | Run ID of the first run that produced this fingerprint. |
last_seen_run_id | TEXT NOT NULL | Run ID of the most recent run that produced this fingerprint. Updated on every MergeBaseline call. |
occurrence_count | INTEGER NOT NULL | Number of baseline-promoted runs that observed this (category, value) pair. Incremented on each merge. |
deviations
Findings produced by the Differ after each run. A deviation row represents a (category, value) pair observed in the run that was not present in the baseline_fingerprints table for that package. The Differ deletes and re-inserts all deviations for a run on each analysis pass (debounced at 2 seconds) to avoid duplicates.
CREATE TABLE deviations (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
category TEXT NOT NULL,
value TEXT NOT NULL,
evidence_event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
severity TEXT NOT NULL,
detected_at TEXT NOT NULL,
notified_at TEXT,
suppressed INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX deviations_by_run ON deviations (run_id);
| Column | Type | Description |
|---|
id | TEXT PK | UUID assigned by the storage layer. Supports git-style prefix lookups via fangs deviation show <short>. |
run_id | TEXT NOT NULL FK | References runs(id). Cascades on delete. |
category | TEXT NOT NULL | The class of deviation detected. See the category table below. |
value | TEXT NOT NULL | The specific value that triggered the deviation, e.g. an IP address string or file path. |
evidence_event_id | INTEGER NOT NULL FK | References events(id). The specific event row that produced the finding. Cascades on delete (see retention note above). |
severity | TEXT NOT NULL | Risk classification: info, warn, or crit. |
detected_at | TEXT NOT NULL | RFC 3339 timestamp when the Differ wrote this row. |
notified_at | TEXT | RFC 3339 timestamp when a webhook was successfully delivered. NULL until notified. |
suppressed | INTEGER | 1 when the operator has suppressed this deviation (e.g. via an allowlist match). 0 by default. |
Deviation categories
| Category | Triggered by | Typical severity |
|---|
net_new_destination | New outbound IP address not in baseline | warn |
net_new_dns | New DNS hostname queried, not in baseline | warn |
net_new_https_host | New TLS SNI hostname not in baseline | warn |
fs_new_path_read | File read at a path not in baseline | info |
fs_new_path_write | File write at a path not in baseline | warn |
proc_new_exec | Binary execution not seen in baseline | crit |
notifiers
Webhook targets the orchestrator fires after the Differ finishes analyzing a run with one or more deviations. Each notifier is a named, reusable webhook configuration. Added in migration 0002; replaces the per-deviation webhook model from migration 0001.
CREATE TABLE notifiers (
name TEXT PRIMARY KEY,
url TEXT NOT NULL,
template TEXT NOT NULL,
secret_env TEXT,
headers TEXT,
min_severity TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
| Column | Type | Description |
|---|
name | TEXT PK | Operator-assigned identifier, e.g. "slack-security". Primary key. |
url | TEXT NOT NULL | Webhook target URL. |
template | TEXT NOT NULL | Payload template: slack, discord, or generic. Controls the JSON shape posted to the webhook. |
secret_env | TEXT | Name of an environment variable holding the HMAC signing secret. NULL disables request signing. |
headers | TEXT | JSON object of extra HTTP headers to include in every webhook POST. NULL or "" means no extra headers. |
min_severity | TEXT | Minimum deviation severity required to fire: low, medium, high, or critical. Empty fires on any severity. |
enabled | INTEGER | 1 to fire; 0 to silence without deleting the configuration. |
created_at | TEXT NOT NULL | RFC 3339 creation timestamp. |
updated_at | TEXT NOT NULL | RFC 3339 last-modification timestamp. |
notifications
Per-run webhook delivery log. One row per (run_id, notifier_name, attempt) triple. The orchestrator appends a new row for each delivery attempt; retries are tracked here without modifying existing rows. Added in migration 0002 with per-run semantics (the migration 0001 version tracked per-deviation deliveries and was dropped).
CREATE TABLE notifications (
id TEXT PRIMARY KEY,
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
notifier_name TEXT NOT NULL REFERENCES notifiers(name) ON DELETE CASCADE,
attempt INTEGER NOT NULL,
status TEXT NOT NULL,
last_attempted_at TEXT,
next_attempt_at TEXT,
response_code INTEGER,
response_body TEXT,
error_msg TEXT,
deviation_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE INDEX notifications_by_status ON notifications (status, next_attempt_at);
CREATE INDEX notifications_by_run ON notifications (run_id);
| Column | Type | Description |
|---|
id | TEXT PK | UUID for this delivery record. |
run_id | TEXT NOT NULL FK | References runs(id). Cascades on delete. |
notifier_name | TEXT NOT NULL FK | References notifiers(name). Cascades on delete. |
attempt | INTEGER NOT NULL | Attempt number, starting at 1. Incremented on retry. |
status | TEXT NOT NULL | Delivery status: queued, sent, failed, or permanent (permanent failure, no more retries). |
last_attempted_at | TEXT | RFC 3339 timestamp of the most recent delivery attempt. NULL when still queued. |
next_attempt_at | TEXT | RFC 3339 timestamp for the next scheduled retry. NULL when sent or permanent. |
response_code | INTEGER | HTTP status code received from the webhook target. NULL when the request could not be sent. |
response_body | TEXT | First portion of the webhook target’s HTTP response body. Useful for debugging rejected payloads. |
error_msg | TEXT | Transport-level error message when response_code is NULL. |
deviation_count | INTEGER | Number of deviation rows included in the webhook payload for this run. |
created_at | TEXT NOT NULL | RFC 3339 timestamp when this delivery record was created (immediately after the Differ finished). |
Indexes
notifications_by_status (status, next_attempt_at) — used by the retry worker to find queued deliveries that are due.
notifications_by_run (run_id) — used by ListNotificationsByRun.
allowlists
Operator-managed suppression rules. When the Differ evaluates a run, it checks each candidate deviation against the allowlist before writing a row to deviations. Entries are either global (apply to every run) or package-scoped (apply only to runs of a specific package). Added in migration 0004.
CREATE TABLE allowlists (
id TEXT PRIMARY KEY,
scope TEXT NOT NULL,
package_name TEXT,
kind TEXT NOT NULL,
value TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
CHECK (scope IN ('global','package')),
CHECK (kind IN ('cidr','path','sni')),
CHECK ((scope='global' AND package_name IS NULL) OR
(scope='package' AND package_name IS NOT NULL))
);
CREATE INDEX allowlists_by_package ON allowlists (package_name);
CREATE INDEX allowlists_by_scope ON allowlists (scope);
| Column | Type | Description |
|---|
id | TEXT PK | UUID. Supports git-style prefix lookups via fangs allowlist remove <short>. |
scope | TEXT NOT NULL | global — applies to every run. package — applies only to runs of package_name. |
package_name | TEXT | Non-NULL iff scope = 'package'. NULL for global entries. Enforced by a CHECK constraint. |
kind | TEXT NOT NULL | Allowlist type. Controls which Differ category the entry suppresses. See the kind table below. |
value | TEXT NOT NULL | The value to allow. Interpretation depends on kind. |
note | TEXT NOT NULL | Optional human-readable comment explaining why this entry exists. Defaults to empty string. |
created_at | TEXT NOT NULL | RFC 3339 creation timestamp. |
Allowlist kinds
| Kind | Suppresses | Value format | Example |
|---|
cidr | net_new_destination | IPv4 or IPv6 CIDR block | "10.0.0.0/8" |
path | fs_new_path_read, fs_new_path_write | Path prefix string | "/srv/data/" |
sni | net_new_https_host | SNI hostname (case-insensitive exact match) | "telemetry.example.com" |
Constraints
The table enforces three CHECK constraints:
scope must be 'global' or 'package'.
kind must be 'cidr', 'path', or 'sni'.
package_name must be NULL when scope = 'global' and non-NULL when scope = 'package'.
Indexes
allowlists_by_package (package_name) — used when the Differ loads the per-package allowlist for a run.
allowlists_by_scope (scope) — used when loading all global entries.
The Differ combines the database allowlist with a hardcoded DefaultCDNAllowlist (covering major CDN address ranges). A destination is allowed if it matches either source — the database rules or the built-in CDN list. This means common npm registry and CDN traffic is suppressed by default without requiring manual allowlist entries.