Skip to main content

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

MigrationTables added / changed
0001_initpackages, releases, runs, events, baseline_fingerprints, deviations, notifications (v1, per-deviation)
0002_notifiersDrops notifications v1; adds notifiers, notifications v2 (per-run, per-notifier)
0003_run_resultAdds events_emitted, events_dropped, duration_ns columns to runs
0004_allowlistsAdds 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
);
ColumnTypeDescription
nameTEXT PKnpm package name, e.g. "axios". Primary key.
added_atTEXT NOT NULLRFC 3339 timestamp when the package was added to the watch list.
last_checked_atTEXTRFC 3339 timestamp of the most recent successful registry poll. NULL until the first poll completes.
last_seen_versionTEXTThe 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)
);
ColumnTypeDescription
package_nameTEXT NOT NULL FKReferences packages(name). Cascades on delete.
versionTEXT NOT NULLnpm version string, e.g. "1.7.9".
tarball_sha256TEXT NOT NULLSHA-256 hex digest of the package tarball. Used for integrity verification.
npm_integrityTEXT NOT NULLThe integrity field from the npm registry manifest (SRI format, e.g. sha512-...).
published_atTEXT NOT NULLRFC 3339 timestamp when the version was published to npm.
discovered_atTEXT NOT NULLRFC 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);
ColumnTypeDescription
idTEXT PKHex-encoded 16-byte run identifier generated by the orchestrator at dispatch time.
package_nameTEXT NOT NULLnpm package name. Defaults to the sandbox image name for ad-hoc runs without an explicit package.
versionTEXT NOT NULLPackage version string.
tarball_sha256TEXT NOT NULLExpected tarball digest (populated when available from the registry).
lockfile_sha256TEXT NOT NULLSHA-256 of the package-lock.json generated during the install (populated by the runner when available).
node_versionTEXT NOT NULLNode.js version inside the sandbox container.
npm_versionTEXT NOT NULLnpm version inside the sandbox container.
stateTEXT NOT NULLCurrent lifecycle state. See the state machine below.
attemptINTEGERRetry counter, starting at 1. Incremented on re-queue after failure.
is_baselineINTEGER1 when this run has been promoted to baseline (zero deviations, or manually approved). 0 otherwise.
started_atTEXTRFC 3339 timestamp when the runner began execution. NULL until the run leaves pending.
finished_atTEXTRFC 3339 timestamp when the run reached a terminal state. NULL until done or failed.
failure_reasonTEXTHuman-readable description of why the run failed. Empty on success.
events_emittedINTEGERTotal events the sensor emitted (from POST /v1/runs/{id}/result). Added in migration 0003.
events_droppedINTEGEREvents dropped due to ring-buffer or queue overflow. Non-zero indicates incomplete coverage. Added in migration 0003.
duration_nsINTEGERActual wall-clock scan duration in nanoseconds. Added in migration 0003.
Run state machine
pending → building → sandboxed → analyzed → done
   └──────────────────────────────────────→ failed
StateMeaning
pendingQueued, awaiting runner pickup
buildingRunner is pulling the container image
sandboxedContainer is running; sensor is attached
analyzedDiffer has completed deviation analysis
doneRun completed successfully
failedRun 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);
ColumnTypeDescription
idINTEGER PK AUTOINCREMENTAuto-incrementing row ID. Used as evidence_event_id in the deviations table.
run_idTEXT NOT NULL FKReferences runs(id). Cascades on delete.
ts_nsINTEGER NOT NULLNanosecond-precision wall-clock timestamp recorded by the orchestrator on receipt (not the eBPF kernel timestamp).
typeTEXT NOT NULLEvent type string: file_access, exec, net_connect, dns_query, or tls_sni.
dataTEXT NOT NULLJSON-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)
);
ColumnTypeDescription
package_nameTEXT NOT NULLnpm package name. Part of the composite primary key.
categoryTEXT NOT NULLDeviation category, matching the values in deviations.category.
valueTEXT NOT NULLThe normalized value, e.g. an IP address, hostname, or file path.
first_seen_run_idTEXT NOT NULLRun ID of the first run that produced this fingerprint.
last_seen_run_idTEXT NOT NULLRun ID of the most recent run that produced this fingerprint. Updated on every MergeBaseline call.
occurrence_countINTEGER NOT NULLNumber 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);
ColumnTypeDescription
idTEXT PKUUID assigned by the storage layer. Supports git-style prefix lookups via fangs deviation show <short>.
run_idTEXT NOT NULL FKReferences runs(id). Cascades on delete.
categoryTEXT NOT NULLThe class of deviation detected. See the category table below.
valueTEXT NOT NULLThe specific value that triggered the deviation, e.g. an IP address string or file path.
evidence_event_idINTEGER NOT NULL FKReferences events(id). The specific event row that produced the finding. Cascades on delete (see retention note above).
severityTEXT NOT NULLRisk classification: info, warn, or crit.
detected_atTEXT NOT NULLRFC 3339 timestamp when the Differ wrote this row.
notified_atTEXTRFC 3339 timestamp when a webhook was successfully delivered. NULL until notified.
suppressedINTEGER1 when the operator has suppressed this deviation (e.g. via an allowlist match). 0 by default.
Deviation categories
CategoryTriggered byTypical severity
net_new_destinationNew outbound IP address not in baselinewarn
net_new_dnsNew DNS hostname queried, not in baselinewarn
net_new_https_hostNew TLS SNI hostname not in baselinewarn
fs_new_path_readFile read at a path not in baselineinfo
fs_new_path_writeFile write at a path not in baselinewarn
proc_new_execBinary execution not seen in baselinecrit

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
);
ColumnTypeDescription
nameTEXT PKOperator-assigned identifier, e.g. "slack-security". Primary key.
urlTEXT NOT NULLWebhook target URL.
templateTEXT NOT NULLPayload template: slack, discord, or generic. Controls the JSON shape posted to the webhook.
secret_envTEXTName of an environment variable holding the HMAC signing secret. NULL disables request signing.
headersTEXTJSON object of extra HTTP headers to include in every webhook POST. NULL or "" means no extra headers.
min_severityTEXTMinimum deviation severity required to fire: low, medium, high, or critical. Empty fires on any severity.
enabledINTEGER1 to fire; 0 to silence without deleting the configuration.
created_atTEXT NOT NULLRFC 3339 creation timestamp.
updated_atTEXT NOT NULLRFC 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);
ColumnTypeDescription
idTEXT PKUUID for this delivery record.
run_idTEXT NOT NULL FKReferences runs(id). Cascades on delete.
notifier_nameTEXT NOT NULL FKReferences notifiers(name). Cascades on delete.
attemptINTEGER NOT NULLAttempt number, starting at 1. Incremented on retry.
statusTEXT NOT NULLDelivery status: queued, sent, failed, or permanent (permanent failure, no more retries).
last_attempted_atTEXTRFC 3339 timestamp of the most recent delivery attempt. NULL when still queued.
next_attempt_atTEXTRFC 3339 timestamp for the next scheduled retry. NULL when sent or permanent.
response_codeINTEGERHTTP status code received from the webhook target. NULL when the request could not be sent.
response_bodyTEXTFirst portion of the webhook target’s HTTP response body. Useful for debugging rejected payloads.
error_msgTEXTTransport-level error message when response_code is NULL.
deviation_countINTEGERNumber of deviation rows included in the webhook payload for this run.
created_atTEXT NOT NULLRFC 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);
ColumnTypeDescription
idTEXT PKUUID. Supports git-style prefix lookups via fangs allowlist remove <short>.
scopeTEXT NOT NULLglobal — applies to every run. package — applies only to runs of package_name.
package_nameTEXTNon-NULL iff scope = 'package'. NULL for global entries. Enforced by a CHECK constraint.
kindTEXT NOT NULLAllowlist type. Controls which Differ category the entry suppresses. See the kind table below.
valueTEXT NOT NULLThe value to allow. Interpretation depends on kind.
noteTEXT NOT NULLOptional human-readable comment explaining why this entry exists. Defaults to empty string.
created_atTEXT NOT NULLRFC 3339 creation timestamp.
Allowlist kinds
KindSuppressesValue formatExample
cidrnet_new_destinationIPv4 or IPv6 CIDR block"10.0.0.0/8"
pathfs_new_path_read, fs_new_path_writePath prefix string"/srv/data/"
sninet_new_https_hostSNI 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.

Build docs developers (and LLMs) love