The differ is FANGS’ core comparison engine. It doesn’t classify behavior as malicious — it asks a narrower question: is this run different from what this package has done before? Each completed run produces a set ofDocumentation 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.
(category, normalized_value) fingerprints. Those fingerprints are compared against the package’s rolling baseline_fingerprints table. Any pair not in the baseline becomes a Deviation row for an operator to review.
Analysis flow
Run completes
The runner posts a final
ScanResult to POST /v1/runs/{run_id}/result. The orchestrator marks the run RunStateDone and calls Differ.AnalyzeRun(ctx, runID).Load events and build the allowlist filter
AnalyzeRun calls store.ListEventsByRun to retrieve every EventRow for the run (stored as JSON in the events table). It also calls store.ListAllowEntries to load the union of global and package-scoped operator allowlist rules. A Filter is constructed from those entries plus the hardcoded CDN CIDR list.Extract fingerprints
ExtractFingerprintsWith(events, filter) walks every event row, dispatches per type to a category-specific extractor, applies normalization, and deduplicates into a map[string]*Fingerprint. Each unique (category, value) key becomes one Fingerprint with a Count (number of events that collapsed to it) and a FirstEvtID (lowest events.id, used as an evidence pointer).Load baseline
store.LoadBaseline(packageName) returns the current baseline_fingerprints rows for the package. If the slice is empty this is the package’s first run.First run — seed baseline
If no baseline exists,
seedBaseline writes every run fingerprint as a BaselineRow and marks the run is_baseline=true. AnalyzeRun returns 0 deviations. The first run always becomes the baseline; future runs are compared against it.Subsequent runs — diff and write deviations
A
known map is built from the baseline rows. Each run fingerprint is checked:- In
known→ bumpoccurrence_countviaMergeBaseline. - Not in
known→ emit aDeviationRowwith category, value, evidence event ID, and severity.
DeleteDeviationsForRun) so re-analysis is idempotent — the orchestrator may call AnalyzeRun multiple times per run (once per debounced event-batch POST, once more on final result).Six fingerprint categories
Each category maps to one event type and one field extracted from the event’s JSON payload. Severity is the default assigned when a deviation is first written; operators can tune it via allowlist rules.net_new_destination
Severity: warnSource:
net_connect events. Value: "ip:port" string (e.g. "1.2.3.4:443"). IPs in any CDN allowlist CIDR or operator-defined CIDR are suppressed before fingerprinting — their identity is canonically captured by SNI/DNS.net_new_dns
Severity: warnSource:
dns_query events. Value: the normalized QName (e.g. "registry.npmjs.org"). Helps catch exfiltration via DNS even when the IP side is allowlisted.net_new_https_host
Severity: warnSource:
tls_sni events. Value: the normalized SNI hostname (e.g. "malicious-c2.example.com"). Catches HTTPS connections to new hosts even when routed through a known CDN IP range.fs_new_path_read
Severity: infoSource:
file_access events where the openat flags do NOT include O_WRONLY (1), O_RDWR (2), or O_CREAT (64). Value: the normalized path.fs_new_path_write
Severity: infoSource:
file_access events where flags & (O_WRONLY | O_RDWR | O_CREAT) != 0. Value: the normalized path. Write-intent opens to credential paths (/root/.ssh/, /etc/shadow, etc.) are the highest-signal file deviations.proc_new_exec
Severity: warnSource:
exec events. Value: the normalized binary path (e.g. /usr/bin/curl). A package that suddenly spawns a shell or network binary for the first time is a strong signal.A seventh category
syscall_rare_category appears in the differ comments as a planned addition (D18) but is not yet implemented. Only the six categories above are written to the deviations table in the current release.Normalization
Normalization collapses volatile path segments and host representations so a baseline established on one run remains stable across future runs of the same package. All normalization is applied before the fingerprint map lookup — two events that normalize to the same value produce a single fingerprint.- Paths
- DNS
- SNI
- Destinations
NormalizePath applies a sequence of regex rules in priority order. Earlier rules are more specific:| Pattern | Replacement | Example |
|---|---|---|
/proc/<number>/… | /proc/<PID>/… | /proc/12345/status → /proc/<PID>/status |
/tmp/<word>-<3+ digits>… | /tmp/<RAND> | /tmp/npm-2156-abc/foo → /tmp/<RAND> |
| ISO datetime npm debug logs | /<TIMESTAMP>-debug-<N>.log | 2026-05-22T10_00_00_000Z-debug-0.log → /<TIMESTAMP>-debug-<N>.log |
| Generic ISO-date filenames | /<DATE>.<ext> | /var/log/2026-05-22.log → /var/log/<DATE>.log |
| cacache content-addressed paths | …/<SHA256> | .npm/_cacache/content-v2/sha256/AA/BB/<hex> → …/<SHA256> |
| cacache index paths | …/<HASH> | .npm/_cacache/index-v5/AA/BB/<hex> → …/<HASH> |
cacache /tmp/<hex> staging | …/<HEX> | .npm/_cacache/tmp/<hex> → …/<HEX> |
| Long hex string path components | /<HEX>/ | /12abcdef34567890/ → /<HEX>/ |
NormalizeBinaryPath (used for proc_new_exec) strips null bytes and surrounding whitespace only — deliberately kept minimal to avoid hiding PATH-traversal indicators.Allowlist filter
Before any fingerprint is written, three suppression checks are applied. TheFilter struct is built per-run from the hardcoded CDN CIDR list plus any operator-defined allow_entries rows (global and package-scoped).
Hardcoded CDN allowlist
TheDefaultCDNAllowlist covers IP ranges for large CDNs that round-robin their edge IPs per DNS query. Without this allowlist, every npm install would produce net_new_destination deviations as DNS returns different IPs on each run.
| Provider | CIDR examples | Covers |
|---|---|---|
| Cloudflare | 104.16.0.0/13, 172.64.0.0/13, 13 more ranges | npm registry CDN, Discord CDN, etc. |
| GitHub | 140.82.112.0/20, 185.199.108.0/22, 2 more | raw.githubusercontent.com etc. |
142.250.0.0/15, 172.217.0.0/16, 3 more | gstatic, googleapis CDN edges | |
| Fastly | 151.101.0.0/16, 199.232.0.0/16 | npm registry origin |
| Amazon CloudFront | 13.32.0.0/15, 52.84.0.0/15, 6 more ranges | AWS CDN |
fangs allow add --kind cidr --value 10.0.0.0/8 --note "internal", or suppress specific SNIs and path prefixes similarly.
Severity defaults
| Category | Default severity | Rationale |
|---|---|---|
net_new_destination | warn | New IP:port is medium-confidence signal |
net_new_dns | warn | New DNS lookup often precedes exfil |
net_new_https_host | warn | New TLS hostname is the strongest network signal |
fs_new_path_read | info | Read of new path is lower confidence |
fs_new_path_write | info | Write to new path is higher confidence |
proc_new_exec | warn | Unexpected subprocess is high signal |
/root/.ssh/, /etc/shadow, etc.) receive EventTagCredAccess in the sensor and are rendered with a red badge in the UI regardless of the differ category severity.