Supply-chain file-integrity monitoring for package managers and the developers who use them.
DireC is a small, hardened, single-binary daemon that watches the
directories where package managers stage and install code (npm,
pip, gem, cargo, go, composer, plus the OS-level package
managers), and emits a structured event the moment something on disk
matches a known-bad package, dropper artefact, install-time payload,
exfiltration domain, lifecycle hook, lockfile-tampering pattern, or
tampered package-manager binary.
It is the defensive output of the YurilLAB Security Team's multi-year study of supply-chain compromises, and is built to be deployed as a "double-edged sword" — equally useful to individual developers protecting their workstation, and to package-manager hosts and CI runners protecting end users from a tainted publish.
Package-manager supply-chain compromise is now the dominant initial access vector for code-execution attacks against developers and CI infrastructure. The incidents bundled into the IoC database of this tree, all from 2022–2026:
event-stream / flatmap-stream • ua-parser-js / coa / rc • colors / faker maintainer sabotage • node-ipc / peacenotwar • ctx / phpass • Solana web3.js • Lottie • xz-utils (CVE-2024-3094) • Ledger ConnectKit • rspack • the qix chalk/debug phishing wave • Shai-Hulud worm waves 1, 2, 3 (Bitwarden CLI) • the SAP
@cap-jsTeamPCP campaign • the Sapphire Sleet axios compromise • the LiteLLM / Telnyx PyPI backdoor • tj-actions/changed-files secrets-leak
Every one of these had on-disk artefacts that would have been visible
the instant the malicious package landed in node_modules/ or
site-packages/, before its first require() or import ever ran.
DireC is the watcher that surfaces those artefacts.
The companion research document
YurilLAB Supply Chain.docx
covers the same incidents in depth; the IoC database in
src/iocs.c is kept in sync with that research; and
the per-incident write-ups in docs/ trace each artefact
to the detection that catches it.
In scope.
- Tampering with the package-manager binary itself (
npm,pip,gem,cargo,go,composer,dpkg,apt,rpm,dnf,pacman,brew,snap,flatpak) — including the case where the canonical path is a symlink, which is what every distro actually ships. - Tampering with manifests and lockfiles (
package.json,package-lock.json,npm-shrinkwrap.json,yarn.lock,pnpm-lock.yaml,requirements.txt,Pipfile.lock,poetry.lock,pyproject.toml,Cargo.toml,Cargo.lock,Gemfile.lock,composer.json,composer.lock,go.mod,go.sum). - Lockfile structural attacks: integrity-hash stripping, non-default
resolvedURLs,git+/file:non-registry sources. - All sixteen npm lifecycle hooks (
preinstall,install,postinstall,prepare,prepublish,prepublishOnly,preprepare,postprepare,prerestore,postrestore,prepack,postpack,preversion,postversion,predeploy,postdeploy). - Worm-class auto-republication patterns (Shai-Hulud / TeamPCP):
npm publish/npm view/npm access/npm versiondriven from inside a lifecycle hook, plus the dead-man's-switch wiper variants (--no-preserve-root,rm -rf /family). - Bun-runtime evasion (Shai-Hulud 2.0):
bun install/bun run/bun xinvoked from a lifecycle hook to bypass Node.js-tuned sandboxes. - AI-weaponised supply-chain attacks: AI CLI guardrail-bypass flags
(
--dangerously-skip-permissions,--trust-all-tools,--yolo) inside an installed package — Nx/s1ngularity (Aug 2025); rogue Model Context Protocol server artefacts in AI-tool directories — SANDWORM_MODE (Feb 2026); local AI service port-probing (Ollama:11434, generic127.0.0.1:1234/5000/8000/8080). - CI/CD trigger and runner abuse:
pull_request_targetco-occurring withactions/checkoutandsecrets.*(HackerBot-Claw, Feb–Mar 2026); expression-injection of untrusted PR fields intorun:steps (Ultralytics, Dec 2024); self-hosted runner registration inside a workflow (Shai-Hulud 2.0 wave-2SHA1HULUDpersistence). - PhantomRaven Remote Dynamic Dependencies: a
package.jsondependency specifier that is anhttp(s)://URL, bypassing every registry-side static-analysis control. - Blockchain command-and-control: the GlassWorm primary Solana C2 address as a literal in package source.
- Python
setup.pyarbitrary-code (the single biggest difference from npm —pip installruns full Python) and.pthimport-hijack files (the TeamPCPlitellm_init.pthshape). - Cargo
build.rsarbitrary-code attacks. - Per-victim credential exfiltration patterns: GitHub PAT, AWS access key, Stripe live key, Slack token, Discord webhook URL, Telegram bot API URL.
- Configuration redirection:
.npmrc/.yarnrcregistry=override,package.jsonpublishConfig.registryoverride, npmbinfield path traversal. - GitHub Actions workflow tampering (the
tj-actions/changed-filesMarch-2025 secrets-print pattern). - CVE-2021-42574 "Trojan Source" Bidi/RLO Unicode in source.
- Cross-ecosystem pollution (Python files inside
node_modules/, JS/EXE insidesite-packages/). - OS-level dropper artefacts (
liblzma.so.5.6.0,litellm_init.pth,6202033.vbs,bun_environment.js,bw_setup.js,bw1.js,discussion.yaml, ...). - Metadata-only tampering: SUID/SGID bit flips,
chown,setcap cap_net_admin+ep, SELinux relabels, xattr injection. - Kernel-level page-cache writes (CVE-2026-31431 "Copy Fail",
Apr 2026, every Linux distro since 2017). The hash-changes-but-
mtime-doesn't signature is detected as
system.page-cache-tamperat SEV_ERR with the CVE id and upstream patch reference; the exploit script's literal strings (authencesn(,socket(AF_ALG, the CVE id itself) are also IoC-tagged. - High-value system regions outside package-manager directories:
PAM modules, sudoers, sshd configs,
/etc/passwd//etc/shadow, systemd unit dirs, cron drop-ins,/var/spool/cron, kernel modules, modprobe / sysctl / udev configs, GRUB config, profile scripts, polkit / D-Bus, CA cert bundles, firewall rules. macOS: LaunchDaemons / LaunchAgents / kexts. Windows: Scheduled Tasks / drivers / registry hives / Startup folder / Group Policy. Each registered region fires its own semantic event (system.pam- modified,system.cron-modified, etc.) at the appropriate severity. - Detection accuracy: every
supply.*event runs through the 7-identifier weighted scorer (src/score.c) and carriesscore=N tier=LABEL verify=Nin its note so SIEM rules can route by confidence band. Below-thresholdSEV_WARNevents are suppressed (logged at INFO so the audit trail still exists);SEV_ERRevents always fire, scoring can never hide a critical alert. - Mid-scan race-edits and TOCTOU:
read_cappedrecords the buffer's(inode, mtime, size)before scanning; at every emit the path is re-lstat'd and the result feedsI_VERIFY. A race- edit, decoy swap, or attacker rewriting the file between read and emit shows up asverify=70(mtime drift),verify=60(size drift), orverify=30(inode swap / vanished). - Daemon survivability against malware: the daemon hardens
itself in-process (Linux:
PR_SET_DUMPABLE=0,PR_SET_NO_NEW_PRIVS, OOM exemption; Windows: process DACL blocksPROCESS_TERMINATEfor non-admin SIDs, mitigation policies block code injection / extension-point hijacking). Optional--supervisormode auto-restarts the worker on abnormal exit; under systemd the unit shipsRestart=always.
Out of scope.
- Process-level attribution. DireC is filesystem-resident and does not see which process wrote a file. Pair with auditd, eBPF, or Falco for pid attribution.
- Browser-side wallet drainer behaviour (the qix payload activates only inside a browser). DireC catches the on-disk artefact and the exfil domain; the runtime hook is detectable only by an instrumented browser.
- Network-level exfil interception. DireC notes the exfil domain in the file content; blocking the connection is a job for a host firewall or egress proxy.
- Cryptographic authenticity of upstream tarballs. npm itself does not require signed tarballs; DireC sees the result on disk and tells you it changed. End-to-end authenticity is complementary (Sigstore, npm provenance, pinned commit SHAs).
| Event kind | What it catches |
|---|---|
supply.package |
Known-compromised <package>@<version> in any lockfile or manifest |
supply.install-script |
Suspicious token or OOB domain inside any of the 16 npm lifecycle hooks |
supply.lockfile-integrity-strip |
package-lock.json / npm-shrinkwrap.json with many resolved URLs but few/zero integrity SRI hashes |
supply.lockfile-non-registry |
git+, file:, git@github.com: source in lockfile |
supply.registry |
Lockfile resolved URLs not pointing to registry.npmjs.org |
supply.npmrc-registry |
.npmrc / .yarnrc / .yarnrc.yml registry= (or :registry= / npmRegistryServer:) line redirected away from registry.npmjs.org. The check confirms the literal assignment-to-default is present, so a comment mentioning registry.npmjs.org cannot mask an actual hostile-mirror redirect on a different line |
supply.pipconf-index |
pip.conf / pip.ini index-url (or extra-index-url) line redirected away from pypi.org — the Python-side equivalent of the npmrc primitive |
supply.composer-script |
composer.json references an OOB exfil domain, suspicious token, embedded credential, or bidi/RLO Unicode — composer's lifecycle event keys (pre-install-cmd / post-install-cmd / pre-update-cmd / etc.) are the analogue of npm's lifecycle hooks |
supply.cargo-non-registry |
Cargo.toml [source.crates-io] replace-with redirect, OR Cargo.lock source = "registry+... not pointing to the canonical crates.io-index, OR source = "git+..." dependency. The Rust-side equivalent of the npm/lockfile non-registry detection |
supply.gemfile-source |
Bundler Gemfile source directive not pointing to rubygems.org — the Ruby-side equivalent of supply.npmrc-registry and supply.pipconf-index |
fim.watch-path-missing |
One-shot warning when a configured top-level watch = path becomes unreadable (operator-visibility fail-safe — without this the daemon would silently produce zero events from that tree) |
fim.watch-path-recovered |
Info-level signal when a previously-missing watch path returns to readable |
daemon.backend-error |
The active backend's run loop returned an error; daemon is shutting down with non-clean status (was previously a silent exit) |
supply.publish-config |
package.json publishConfig.registry redirect |
supply.bin-traversal |
package.json bin field contains ../ |
supply.token-leak |
npm _authToken= reference outside its only legitimate home (.npmrc / .yarnrc) |
supply.credential-leak |
High-confidence credential prefix (GitHub PAT, AWS, Stripe, Slack, Discord, Telegram) embedded in package code |
supply.setup-py |
setup.py references OOB domain, suspicious token, credential, or bidi char |
supply.build-rs |
Cargo build.rs references OOB domain, suspicious token, credential, or bidi char |
supply.pth-payload |
Python .pth file containing exec/eval/__import__/socket/subprocess/... (TeamPCP litellm_init.pth) |
supply.cross-ecosystem |
.py inside node_modules/ or .js/.exe inside site-packages/ |
supply.ai-bypass-flag (SEV ERROR) |
AI CLI guardrail-bypass flag (--dangerously-skip-permissions, --trust-all-tools, --yolo) inside an installed package — Nx/s1ngularity (Aug 2025), 2,349 secrets exfiltrated by driving the locally-installed Claude Code / Gemini CLI / Amazon Q into recursive enumeration |
supply.mcp-server (SEV ERROR) |
Rogue Model Context Protocol server filename (mcp-server-*, .mcp.json, mcp_config*, claude_mcp_*) appearing in an AI-tool directory (~/.dev-utils/, ~/.cursor/mcp/, ~/.config/claude/mcp/, ~/.codeium/, ~/.local/share/mcp/) — SANDWORM_MODE (Feb 2026) |
supply.remote-dynamic-dependency |
package.json declares a dependency whose version specifier is an http:// or https:// URL — PhantomRaven Remote Dynamic Dependency shape |
supply.workflow-prt-secrets (SEV ERROR) |
.github/workflows/*.yml combines pull_request_target + actions/checkout@v + secrets.* access — HackerBot-Claw (Feb–Mar 2026), exploited in 6–7 major repos in 37 hours |
supply.workflow-inject |
Workflow interpolates an untrusted PR field (github.head_ref, github.event.pull_request.title/body, github.event.issue/discussion.title) into a run: step — Ultralytics (Dec 2024) |
supply.self-hosted-runner (SEV ERROR) |
Workflow registers a self-hosted runner (runs-on: self-hosted + config.sh/./run.sh/actions-runner/SHA1HULUD) — Shai-Hulud 2.0 wave-2 persistence |
supply.workflow |
.github/workflows/*.yml references OOB domain, pipes remote shell, or prints ${{ secrets.* }} through base64 (tj-actions style) |
supply.os-file |
OS-level dropper filename match (xz-utils, TeamPCP, Shai-Hulud, ...) |
supply.oob-domain |
Out-of-band exfil domain in any textual file inside a package-manager tree |
supply.token |
Suspicious code substring (eval(Buffer.from(, curl -fsSL, | bash, ...) |
supply.bidi |
CVE-2021-42574 Bidi/RLO Unicode control characters in source |
supply.unicode-injection |
Invisible-Unicode steganography — variation selectors U+FE00–U+FE0F, Variation Selectors Supplement, Tag characters, Private Use Area BMP+Supp. Anchor: GlassWorm (Oct 2025–Mar 2026, 35,800+ VS Code installations affected) |
supply.sri-missing |
HTML/JSX/Vue template loads multiple external resources (<script src="https://..."> / <link href="...">) without paired integrity= attributes. Anchor: Bybit/Safe{Wallet} $1.5 B heist (Feb 2025) |
supply.typosquat |
Bounded Levenshtein hit against the popular npm / PyPI name list, dotfile-internals (.bin, .cache, ...) skipped |
pm.binary-baseline |
INFO, once at startup per detected PM binary |
pm.binary-changed |
ERROR, on any change to a fingerprinted PM binary; symlinks are followed and the readlink target is mixed into the hash so a symlink swap also flips the fingerprint |
pm.binary-deleted |
ERROR, the PM binary disappeared between cycles |
pm.binary-restored |
INFO, the PM binary returned to disk after an absence (paired with the prior pm.binary-deleted) |
pm.update-detected |
INFO, per-cycle summary fired when many creates/modifies land under one package manager's state directories or its binary changed in the same cycle — flags "this looks like an npm install / apt upgrade happened" so downstream supply.* hits can be correlated with the install context |
system.page-cache-tamper (SEV ERROR) |
Hash differs but mtime, size, and mode are all unchanged — the unique footprint of a kernel-level page-cache write primitive (CVE-2026-31431 "Copy Fail" and equivalents). The note carries the CVE id and upstream patch reference; requires paranoid = yes to detect because the default fast-path skips re-hashing when stat is unchanged |
system.pam-modified, system.sudoers-modified, system.sshd-config-modified, system.passwd-modified, system.shadow-modified, system.security-config-modified, system.systemd-unit-modified, system.cron-modified, system.kernel-modules-modified, system.modprobe-config-modified, system.sysctl-modified, system.udev-modified, system.hosts-modified, system.resolv-modified, system.nsswitch-modified, system.ca-certs-modified, system.firewall-modified, system.grub-modified, system.profile-modified, system.polkit-modified, system.dbus-modified, system.launchdaemon-modified, system.launchagent-modified, system.kext-modified, system.scheduled-task-modified, system.driver-modified, system.registry-hive-modified, system.startup-modified, system.gpo-modified |
Semantic file-integrity events fired when a fim.create / fim.modify lands on a path registered in src/protect.c. CRITICAL-tier paths fire SEV_ERROR; SENSITIVE / NORMAL fire SEV_WARN. The generic fim.* event is preserved alongside, so existing SIEM rules keep working |
forensics.profile |
INFO, paired with each supply.* hit. Carries SHA-256 (full file or sha256-prefix= if file > 1 MiB), size, line count, magic-bytes hex, Shannon entropy, hit count, co-occurrence score, and the first triggered kind — everything an IR responder needs to triage off-host |
beige.campaign-detected |
INFO/WARN/ERROR depending on the matched signature, fired by the BEIGE temporal-correlation engine when a sliding-window event cluster matches one of the 8 hard-coded multi-stage attack patterns (Shai-Hulud propagation, TeamPCP cascade, GlassWorm staging, Bybit SRI strip + supply hit, etc.). Single events score in isolation; BEIGE turns "this package.json mentioned chalk" + "this index.js leaked a token" + "this .npmrc redirected the registry" — all within 5 minutes on the same package root — into one named campaign |
daemon.start / daemon.stop |
Lifecycle. The start event embeds the host fingerprint (host=… kernel=… arch=… init=…) so the JSONL sink "remembers" which host each event came from |
daemon.backend-error |
The active backend's run loop returned an error; daemon is shutting down with non-clean status (was previously a silent exit) |
fim.create / fim.modify / fim.metadata / fim.delete |
Generic file-integrity events from the active backend |
fim.watch-path-missing / fim.watch-path-recovered |
One-shot warnings when a configured top-level watch = path becomes / returns from unreadable (operator-visibility fail-safe — without this the daemon would silently produce zero events from that tree) |
A stage-by-stage map of every npm install lifecycle event to the
DireC detection that catches it lives in
docs/npm-protection.md.
┌──────────────┐ ┌──────────────────┐
│ direc.conf │ │ --supervisor │
│ (parsed, │ │ (optional │
│ bounded, │ │ wrapper proc.; │
│ validated) │ │ restarts on │
└──────┬───────┘ │ abnormal exit) │
│ └──────┬───────────┘
▼ │ fork+execvp
│ ▼
┌────────────────────────────────────────────────────────────┐
│ direcd (single C11 binary) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ harden.c self-hardening applied EARLY: │ │
│ │ Linux: PR_SET_NO_NEW_PRIVS, PR_SET_DUMPABLE=0, │ │
│ │ oom_score_adj=-1000, SIGHUP ignored, │ │
│ │ mlockall (opt-in) │ │
│ │ Win: SetProcessMitigationPolicy(Dynamic|Image| │ │
│ │ ExtensionPoint), SetSecurityInfo (DACL) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ Backend │───▶│ Supply │───▶│ score.c │ │
│ │ fanotify│ │ scanner │ │ 7 identifiers │ │
│ │ inotify │ │ ┌───────┐ │ │ ┌─────────────┐ │ │
│ │ rdc │ │ │ scan_*│ │ │ │ I_LOC │ │ │
│ │ usn │ │ │ + IoC │ │ │ │ I_NAME │ │ │
│ │ poll │ │ │ tables│ │ │ │ I_COOC │◀─┐ │ │
│ │ (auto- │ │ └───────┘ │ │ │ I_CONTEXT │ │ │ │
│ │ fallba │ │ capture + │ │ │ I_PROV │ │ │ │
│ │ -ck) │ │ re-stat -> │ │ │ I_FRESH │ │ │ │
│ │ │ │ I_VERIFY │ │ │ I_VERIFY │ │ │ │
│ └─────────┘ └──────┬──────┘ │ └─────────────┘ │ │ │
│ │ │ │ weighted 0..100 │ │ │
│ │ ▼ │ threshold │ │ │
│ │ ┌─────────────┐ │ suppression │ │ │
│ │ │ forensics.c │ │ for SEV_WARN │ │ │
│ │ │ SHA-256 + │ └────────┬─────────┘ │ │
│ │ │ entropy + │ │ │ │
│ │ │ magic + │ ▼ │ │
│ │ │ lines + │ ┌───────────────────┐│ │
│ │ │ hits + │ │ Events emitter ││ │
│ │ │ cooc │────▶│ - JSONL sink ││ │
│ │ └─────────────┘ │ - syslog ││ │
│ │ │ - alert hook ││ │
│ │ │ (fork+execve) ││ │
│ │ └───────┬───────────┘│ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ beige.c BEIGE │ │ │
│ │ │ (Behavioral Event Intel. │ │ │
│ │ │ & Graph Engine) │ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ beige_ring.c sliding │ │ │ │
│ │ │ │ event window (4096) │ │ │ │
│ │ │ ├─────────────────────────┤ │ │ │
│ │ │ │ beige_graph.c path │ │ │ │
│ │ │ │ co-occurrence graph │ │ │ │
│ │ │ │ (1024 nodes, 4096 ed) │ │ │ │
│ │ │ ├─────────────────────────┤ │ │ │
│ │ │ │ beige_patterns.c 8 │ │ │ │
│ │ │ │ campaign signatures │ │ │ │
│ │ │ ├─────────────────────────┤ │ │ │
│ │ │ │ beige_stats.c 1-hour │ │ │ │
│ │ │ │ density + entropy + │ │ │ │
│ │ │ │ anomaly boost (0..40) │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ │ feeds I_COOC boost back ─────┼───┘ │
│ │ │ to scoring next emit │ │
│ │ │ emits beige.campaign-detected│ │
│ │ └────────────────┬──────────────┘ │
│ │ │ │
│ │ ▼ (back to sink) │
│ │ │
│ ┌───────────────────────┐ │
│ │ pkgmgr.c PM binary │ │ │
│ │ baseline + recovery │──────────────────────┤ │
│ │ (pm.binary-changed, │ │ │
│ │ pm.binary-deleted, │ │ │
│ │ pm.binary-restored, │ │ │
│ │ pm.update-detected) │ │ │
│ └───────────────────────┘ │ │
│ ┌───────────────────────┐ │ │
│ │ protect.c curated │ │ │
│ │ system regions per │──────────────────────┤ │
│ │ platform; semantic │ │ │
│ │ event kinds: │ │ │
│ │ system.pam-modified, │ │ │
│ │ system.cron-modified, │ │ │
│ │ system.sudoers-..., │ │ │
│ │ system.kext-..., etc. │ │ │
│ └───────────────────────┘ │ │
│ ┌───────────────────────┐ │ │
│ │ verify.c -D / -V │ │ │
│ │ baseline + diff │ │ │
│ └───────────────────────┘ │ │
└─────────────────────────────────────────────────┼──────────┘
│
▼
┌───────────────────────┐
│ SIEM / Tauri GUI / │
│ on_alert hook / │
│ direcd -T / direcd -S │
└───────────────────────┘
Every emitted event note ends with [score=N tier=LABEL verify=N],
where score is the 7-identifier weighted confidence (0–100), tier
is low|medium|high|critical, and verify is the at-emit-time
re-stat agreement (100=identical, 70=mtime drift, 60=size drift,
30=inode swap or vanished). SEV_WARN events whose score falls below
detection_threshold are suppressed (logged at INFO so an audit
trail still exists); SEV_ERR events always fire.
Three Linux backends with automatic fallback.
fanotify— kernel-level, filesystem-wide. RequiresCAP_SYS_ADMIN.inotify— per-directory, recursive. No special privileges beyond read access to the watched paths. Watch-descriptor lifecycle is managed (auto-drop onIN_IGNORED/DELETE_SELF/MOVE_SELF, dedup on duplicatewd).poll— pure POSIXlstat+ SHA-256 + xattr-hash. Works anywhere as the last resort. The richest fingerprint, the only one that reliably catches xattr / file-capability injection, the only one that catches CVE-2026-31431-style page-cache tamper, and the one used by-Dand-V. Hash table grows with load factor (no O(N²) cliff on big trees). Pinned PM binaries support absent → present recovery viapm.binary-restored.
Three Windows backends with the same fallback chain.
rdc—ReadDirectoryChangesW+ IOCP. Bounds-checks everyFILE_NOTIFY_INFORMATIONrecord; fires a watch-silent warning if rearm fails.usn— NTFS USN journal. Bounds-checksRecordLength/FileNameOffset/FileNameLengthagainst the batch end.poll.
Selection is automatic by default; backend = inotify (or -b inotify on the command line) forces a specific one.
Every supply.* event runs through a small weighted-sum scorer
implemented in src/score.c. Each identifier is a
0–100 unsigned value capturing one orthogonal source of evidence.
The default weights sum to exactly 100 so the result is itself a
0–100 number.
| Identifier | Weight | What it captures |
|---|---|---|
I_NAME |
20 | Match strength: exact-IoC > prefix > substring > fuzzy/Levenshtein |
I_VERIFY |
18 | Re-stat agreement at emit time (race-edit / TOCTOU detection) |
I_COOC |
18 | Independent IoCs hitting the same file (1=30, 2=60, 3=80, 4+=100) |
I_LOC |
15 | Path-tier (node_modules//etc high, /tmp lower) |
I_CONTEXT |
12 | Extension × event-kind fit (eval in .js high, eval in .md low) |
I_PROV |
10 | Provenance (PM-managed > OS-managed > user tree) |
I_FRESH |
7 | mtime-age curve (just-modified = 100, > 30 days = 20) |
Tiers are bucketed: 0–30 low, 31–50 medium, 51–70 high,
71–100 critical. detection_threshold (default 30) is the
suppression cutoff for SEV_WARN events; SEV_ERR events always
fire regardless. Severity is never raised by scoring, only
suppression-eligible.
Single events score in isolation. Real supply-chain compromises are
multi-stage: a package.json mentions chalk @ a known-bad version;
an index.js in the same package dumps a registry token; the same
tree's .npmrc redirects the registry to an attacker mirror — three
distinct file events that, taken together, are a campaign.
src/beige.c — Behavioral Event Intelligence &
Graph Engine — keeps a 5-minute sliding window of every emitted
event, builds a lightweight co-occurrence graph of package-root path
prefixes, scores per-window density + path-entropy anomalies, and
matches the cluster against eight hard-coded multi-stage signatures
grounded in real 2022–2026 incidents (Shai-Hulud propagation,
TeamPCP cascade, GlassWorm staging, Bybit SRI strip + supply hit,
CanisterWorm wiper chain, etc.). When a match fires, BEIGE emits a
beige.campaign-detected event AND boosts the I_COOC identifier
of the next-scored event so that hits landing inside a detected
campaign tier higher than the same hits in isolation.
| Sub-module | Role | Bound |
|---|---|---|
beige_ring.c |
Sliding-window event store (FIFO eviction) | 4096 events |
beige_graph.c |
Path-prefix co-occurrence graph (LRU eviction) | 1024 nodes / 4096 edges |
beige_patterns.c |
8 built-in campaign signatures + external file loader | 32 patterns × 8 stages |
beige_stats.c |
1-hour rolling density + path-entropy + 0..40 boost | 12 buckets × 256 paths |
Memory is bounded at init (~200 KiB total); no allocator failure
path at runtime. If beige_init fails (OOM, unreasonable config),
the daemon continues without correlation — fail-safe by design.
--beige-dump prints the current window, graph, and stats. Full
documentation in docs/BEIGE.md.
For each scan, read_capped calls capture_verify(fd) which
remembers (inode, size, mtime) of the buffer the scanner is about
to read. At every emit_supply call the path is re-lstat'd and
the result feeds I_VERIFY:
| State at re-stat | I_VERIFY |
|---|---|
| Identical (inode, size, mtime all match) | 100 |
| mtime changed only | 70 |
| size changed | 60 |
| Inode swap / file vanished | 30 |
| No capture (nested non-buffered scan) | 50 |
forensic_save_t saves and restores the verify state across nested
scans so scan_install_code calling scan_credentials doesn't
clobber the parent's capture.
src/protect.c ships a curated registry of ~50
high-value system paths, tagged CRITICAL / SENSITIVE / NORMAL
and platform-masked. When a fim.create or fim.modify event lands
on one of these, an additional semantic event with the
registry-defined kind is emitted at the appropriate severity:
- Linux CRITICAL (raises
SEV_ERR):/etc/pam.d,/etc/sudoers(.d),/etc/ssh/sshd_config(.d),/etc/passwd,/etc/shadow,/etc/security,/etc/systemd/{system,user},/usr/lib/systemd/system,/etc/cron{tab,.d},/var/spool/cron,/lib/modules,/etc/default/grub,/etc/grub.d - Linux SENSITIVE: profile.d, modprobe.d, sysctl.d, udev rules,
/etc/hosts,/etc/resolv.conf,/etc/nsswitch.conf, CA cert bundles, firewall rules, polkit rules, init.d, rc.local - macOS:
/Library/LaunchDaemons(CRITICAL), LaunchAgents,/Library/Extensions(kexts),/etc/sudoers,/etc/ssh/sshd_config - Windows:
System32\Tasks,System32\drivers,System32\config(registry hives), Startup folder, GroupPolicy,drivers\etc\hosts
direcd --check (-K) lists every CRITICAL region present on the
host that isn't under any configured watch = line and prints a
final coverage summary; direcd --wizard prints all registry
entries as watch = candidates (active or commented based on
existing coverage) tagged with their tier and a one-line note.
harden.c applies in-process self-hardening early, before any
subsystem touches the filesystem:
- Linux:
PR_SET_NO_NEW_PRIVS=1,PR_SET_DUMPABLE=0(blocks ptrace by non-root, disables core dumps),oom_score_adj=-1000(kernel never picks DireC as an OOM-victim; the systemd unit also sets this so the protection is in place even beforeharden_apply_self()runs),SIGHUPignored, optionalmlockall(MCL_CURRENT|MCL_FUTURE)viaharden_lock_memory = yes. - Windows:
SetProcessMitigationPolicy(DynamicCode | ImageLoad | ExtensionPointDisable)blocks runtime code injection, AppInit_DLLs, IME hooks, and remote-share DLL loads;SetSecurityInfoon the process handle restrictsPROCESS_TERMINATE/VM_WRITE/VM_OPERATION/DUP_HANDLEto Local System and Administrators only;SetPriorityClass(HIGH_PRIORITY_CLASS). All Windows APIs resolved viaGetProcAddressso Windows 7 builds gracefully degrade.
A harden status: … syslog line records exactly what got applied
("on", "n/a", or "FAILED" per identifier).
The April-2026 Linux kernel flaw in the AF_ALG / authencesn
in-place crypto path lets an unprivileged local user write 4
attacker-chosen bytes into the page cache of any readable file
without going through write() — so mtime, size, and ctime stay
unchanged. DireC catches this three ways:
- Pre-attack:
--wizardand--checkwarn if the vulnerablealgif_aeadkernel module is loaded, with a concretemodprobe -r+ blacklist mitigation. - During staging: IoC strings (
authencesn(,socket(AF_ALG, the literal CVE id,copy-fail) hit if the exploit script lands in a watched directory. - Post-exploit: the poll backend fires
system.page-cache-tamper(SEV_ERR) whenfp_diffreturnsFP_DIFF_HASHalone with no other bits set — the unique filesystem footprint of a kernel-level page-cache write primitive. Requiresparanoid = yesbecause the default fast-path skips re-hashing when stat is unchanged (which is exactly the state the CVE leaves behind).
| Kind | Binary paths fingerprinted | Lockfiles / manifests scanned |
|---|---|---|
| npm | /usr/bin/npm, /usr/bin/npx, /usr/local/bin/npm, /usr/local/bin/npx, /opt/homebrew/bin/npm, /opt/homebrew/bin/npx |
package.json, package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, .npmrc |
| pip | /usr/bin/pip, /usr/bin/pip3, /usr/local/bin/pip, /usr/local/bin/pip3, /opt/homebrew/bin/pip3 |
requirements.txt, Pipfile, Pipfile.lock, pyproject.toml, poetry.lock, uv.lock, setup.py, pip.conf, pip.ini |
| gem | /usr/bin/gem, /usr/bin/bundle, /usr/local/bin/gem, /usr/local/bin/bundle |
Gemfile, Gemfile.lock, .gemrc |
| cargo | /usr/bin/cargo, /usr/local/bin/cargo |
Cargo.toml, Cargo.lock, build.rs |
| go | /usr/bin/go, /usr/local/bin/go, /usr/local/go/bin/go |
go.mod, go.sum |
| composer | /usr/bin/composer, /usr/local/bin/composer |
composer.json (full scripts/lifecycle scan), composer.lock |
| rpm / dnf / yum | /usr/bin/rpm, /usr/bin/dnf, /usr/bin/yum, /usr/bin/microdnf |
(no project lockfiles) |
| dpkg / apt | /usr/bin/dpkg, /usr/bin/apt, /usr/bin/apt-get, /usr/bin/aptitude |
(no project lockfiles) |
| pacman | /usr/bin/pacman |
(no project lockfiles) |
| brew | /opt/homebrew/bin/brew, /usr/local/bin/brew |
(no project lockfiles) |
| snap | /usr/bin/snap |
(no project lockfiles) |
| flatpak | /usr/bin/flatpak |
(no project lockfiles) |
Adding a new package manager is an append-only edit to
src/pkgmgr.c; the rest of the daemon picks it up automatically.
sudo ./setup.shsetup.sh builds, installs, runs direcd -K to
validate the resulting config, and (if systemd is present) enables
and starts the unit. It does NOT overwrite an existing
/etc/direc/direc.conf, so it is safe to re-run after editing.
sudo direcd --wizard-W / --wizard runs a non-interactive first-time setup tour. It
auto-detects your host's OS (Ubuntu/Debian/Arch/Fedora/Alpine on
Linux, macOS by version, Windows 10/11 by build, BSDs via uname) —
sourced from /etc/os-release + uname() on POSIX, from
RtlGetVersion + GetNativeSystemInfo on Windows — and prints a
configuration tailored to that specific OS. Arch's Python paths
differ from Ubuntu's; macOS ships package managers under
/opt/homebrew/...; Windows uses C:\Program Files\nodejs\...; the
wizard knows.
It also detects which package managers are actually installed on
this host (using the same pkgmgr_for_each_existing_bin path the
daemon's pm_monitor uses), prints watch = lines targeting their
state directories, and explains the highest-impact tunables
(paranoid, pm_monitor, on_alert) so an operator new to DireC
can choose intelligently rather than running on defaults that may
or may not match their threat model. The output is plain stdout —
copy what applies into /etc/direc/direc.conf, then validate with
direcd --check (which itself shows the detected host on a Host:
line).
The detected OS fingerprint is also embedded in every daemon.start
event written to the JSONL sink, so the sink "remembers" which host
each event came from across restarts and across kernel / distro
upgrades:
{"ts":"2026-05-02T03:25:45Z","sev":"info","kind":"daemon.start",
"note":"backend=poll paths=1 host=Ubuntu 24.04 LTS kernel=6.8.0
arch=x86_64 init=systemd"}sudo direcd --stats-S / --stats aggregates the JSONL events sink and prints
counts by severity, by event kind (top 20), and by path (top 10).
It is strictly local: it opens one file (the sink the daemon
already writes to), produces output, and exits. No network calls,
no telemetry export, no third-party dependencies — everything an
operator needs to refine detection thresholds and find the noisy
event kinds, without DireC ever leaving the host.
sudo direcd -T-T follows the JSONL event sink and pretty-prints each line with
colour-coded severity:
2026-05-02T01:23:45Z WARN supply.package /home/dev/proj/package.json chalk [5.6.1] (qix phishing wallet drainer)
2026-05-02T01:23:46Z ERROR pm.binary-changed /usr/bin/npm pm=npm diff=content sha256=...
Color is auto-detected: enabled on a TTY (kitty, alacritty, foot,
wezterm, gnome-terminal, iTerm2, Konsole, Terminal.app, Windows
Terminal, PowerShell, conhost on Windows 10+), disabled when the
output is piped or redirected. The detection honours
NO_COLOR, FORCE_COLOR, and TERM=dumb,
so it integrates cleanly with less, grep, jq, and CI logs that
do or don't interpret ANSI. Tailing also survives log rotation:
when the sink file is replaced (different inode) the new file is
re-opened from its start.
# Build
make
sudo make install
# Edit config (see `Configuration` below)
sudoedit /etc/direc/direc.conf
# Validate the config before launching
sudo direcd -K -c /etc/direc/direc.conf
# Run
sudo systemctl daemon-reload
sudo systemctl enable --now direcd
# Watch detections live
sudo journalctl -fu direcd
sudo tail -f /var/log/direcd-events.jsonl | jq .The bundled systemd unit ships with NoNewPrivileges, full
ProtectSystem, MemoryDenyWriteExecute, a tight
SystemCallFilter=@system-service, and a CapabilityBoundingSet of
just CAP_SYS_ADMIN and CAP_DAC_READ_SEARCH (needed for fanotify
and for traversing unreadable directories during the baseline scan).
The daemon binary is built with -D_FORTIFY_SOURCE=2,
-fstack-protector-strong, -fstack-clash-protection, full RELRO,
immediate binding (-z now), a non-executable stack, and PIE.
mingw32-make -f Makefile.winOr cross-compile from Linux:
make -f Makefile.win CC=x86_64-w64-mingw32-gcccd packaging/arch
makepkg -siThe defaults in conf/direc.conf are the
recommended baseline. The fields most worth tuning:
# Which paths to watch. Add one `watch =` line per path.
watch = /etc
watch = /usr/bin
watch = /usr/sbin
watch = /boot
watch = /usr/lib/node_modules
watch = /usr/lib/python3/dist-packages
# Always rehash, never trust size+mtime. Recommended on hosts that
# hold production secrets or signing keys; mtime is trivially
# forgeable by any process that already wrote the file.
paranoid = yes
# Auto-baseline package-manager binaries. Catches "the package
# manager itself was tampered with" attacks. Symlinked PM paths
# (npm typically resolves to /usr/lib/node_modules/npm/bin/npm-cli.js)
# are followed; the readlink target is mixed into the content hash
# so a symlink swap also flips the fingerprint.
pm_monitor = yes
# JSONL event sink. The bundled GUI tails this; any SIEM / log
# shipper can ingest it.
events_jsonl = /var/log/direcd-events.jsonl
# Optional alert hook. Invoked via fork+execve (never /bin/sh) with
# argv: <severity> <kind> <path> <note>. DireC refuses to register a
# hook that isn't an absolute path, isn't a regular file, or is
# world-writable.
on_alert = /usr/local/sbin/direcd-alert.sh
# Site-specific IoC overrides (extra exfil domains, custom suspicious
# tokens). Format: `substring=...` or `domain=...`, one per line.
supply_iocs = /etc/direc/supply-iocs.conf
# Detection threshold (0-100). Every supply.* event runs through the
# 7-identifier weighted score; SEV_WARN events whose score falls
# below this threshold are suppressed (logged at INFO, not emitted).
# SEV_ERR events ALWAYS fire regardless. 0 = fire everything;
# 30 = default (suppresses prose / docs / test-fixture noise);
# 50 = moderate; 70 = strict.
detection_threshold = 30
# Pin the daemon's working set in RAM (Linux only; ignored elsewhere).
# Prevents an attacker with raw-disk access from reading fingerprints,
# IoC matches, or cached file content off the swap partition.
# Off by default because pinning all current+future pages can cause
# memory pressure on small (< 1 GiB RAM) hosts. Under systemd the
# unit ships LimitMEMLOCK=infinity so just turning this on works.
harden_lock_memory = noPer-PM coverage details and the full lockfile / manifest table live
in docs/package-managers.md. The
lifecycle-by-lifecycle map of npm install to DireC events lives in
docs/npm-protection.md.
DireC can dump a fingerprint of every watched file and PM binary to JSONL on stdout, ship that out of band, and later verify any host against it. This is how you wire DireC into a CI gate or an out-of-band tamper check.
# 1. On a known-good host, dump a baseline of every watched file plus
# every supported package-manager binary.
sudo direcd -c /etc/direc/direc.conf -D > golden.jsonl
# 2. Move golden.jsonl out of band. Sign it, store on a TPM, ship to
# your build artefact registry, whatever.
# 3. Verify any host (now or later) against that baseline. Exit code
# is 0 = clean, 1 = drift detected, 2 = I/O error.
sudo direcd -c /etc/direc/direc.conf -V golden.jsonl
echo $?The dump format is line-oriented JSON whose schema matches the
daemon's events_jsonl sink, so the same tooling (jq, grep, SIEM
ingest) works for both modes.
- All file reads use
O_NOFOLLOWplus aS_ISREGcheck, so a hostile actor under a watched user-writable tree cannot redirect the daemon (running as root) into reading/etc/shadow,/proc/self/mem, a FIFO, or/dev/zero. - Trusted-symlink mode for hardcoded PM-binary paths only: the
fp_compute_trustedvariant follows the symlink to the realnpm-cli.js(or equivalent), but mixes thereadlinktarget into the content hash so a symlink swap is itself tamper-evident. - The pidfile and the JSONL events file are opened with
O_NOFOLLOWand restrictive permissions (0644/0640). - Recursive directory walks are bounded so a poisoned filesystem layout cannot blow the stack.
inotifywatches useIN_DONT_FOLLOWso a symlink planted at a watched name cannot redirect the watch elsewhere.- The JSONL emitter escapes every byte ≥
0x80as\u00xx, so a path containing invalid UTF-8 still produces strictly valid JSON. - The supply-chain scanner's read is sized to the actual file (cap
1 MiB), so a giant minified bundle cannot stall the daemon and a
100-byte
package.jsondoesn't pull a 1 MiB allocation. - Watch paths from
direc.confmust be absolute; relative or empty paths are rejected with a stderr warning, never silently resolved againstchdir("/"). - The poll interval is range-checked (1 .. 86400 s) so a negative or garbage value cannot turn the loop into a busy-spin.
SIGCHLDis configuredSA_NOCLDWAIT, andhooks_dispatchdrains zombies on each call, so a flood of alerts cannot exhaust the process table.- The systemd unit drops every capability except those genuinely
needed and applies a
@system-servicesyscall filter.
src/ daemon (C11, no third-party runtime deps)
main.c POSIX entry point, signals, daemonize, pidfile,
--check / --tail / --stats / --wizard /
--supervisor dispatch, CVE-2026-31431 +
protect-region coverage warnings
main_win.c Windows entry point
config.c direc.conf parser; bounded ints, abs-path
check, line-truncation detection, empty-value
rejection, detection_threshold +
harden_lock_memory parsing
backend.c backend selection + dispatch
backends/ fanotify, inotify, posix_poll (with hash-table
resize, page-cache-tamper detection,
pm.update-detected burst summary, pm.binary-
restored), win_rdc, win_usn, win_poll
fingerprint.c SHA-256 + size + mode + uid/gid + xattr-hash
(with alloc-failure propagation so a partial
fingerprint never gets recorded as
"no xattrs"); trusted-symlink variant for PM
binaries; lstat.st_size disambiguation for
readlink truncation
forensics.c post-detection profiler: SHA-256 + size +
lines + Shannon entropy + magic-bytes hex,
paired with each supply.* event so an IR
responder gets one structured fingerprint
per file investigated
harden.c Linux: PR_SET_NO_NEW_PRIVS / DUMPABLE / oom_
score_adj / SIGHUP / mlockall.
Windows: SetProcessMitigationPolicy + process
DACL.
Cross-platform --supervisor (POSIX
fork+execvp, Windows CreateProcess) with
capped exponential backoff
hooks.c fork+execve alert hook (no shell), zombie reaper
events.c JSONL event sink with strict-UTF-8 escaping;
vsnprintf truncation marker; fclose-error log
iocs.c curated IoC database (packages, popular names,
suspicious substrings, OOB domains, lifecycle
keys, credential prefixes, bidi sequences,
.pth payload tokens, CVE-2026-31431 exploit
signatures)
pkgmgr.c package-manager registry + symlink-aware
known-bin lookup; pkgmgr_state_kind_for_path
(used by pm.update-detected)
protect.c curated registry of ~50 high-value system
regions per platform (PAM, sshd, sudoers,
systemd, cron, kernel modules, udev, GRUB,
LaunchDaemons, kexts, Tasks, drivers, registry
hives, ...); each entry has a semantic event
kind + tier
score.c 7-identifier confidence-weighted scorer:
I_LOC, I_NAME, I_COOC, I_CONTEXT, I_PROV,
I_FRESH, I_VERIFY -> 0..100 score + tier
label; threshold suppression for SEV_WARN
sha256.c portable SHA-256
supply.c supply-chain scanner: lockfile / setup.py /
build.rs / .npmrc / package.json / .pth /
workflow / cross-ecosystem / typosquat /
credential / OOB-domain / bidi /
unicode-injection / SRI-gap / Cargo /
Gemfile / pip.conf / composer / MCP /
AI-bypass / self-hosted-runner /
PRT-secrets-leak / remote-dynamic-dependency.
Stat-capture for I_VERIFY; per-emit scoring
and threshold suppression; AWS-EXAMPLE +
FAKEACCESS dummy-credential FP suppression
verify.c -D / -V offline baseline + compare; bounded
json_str_field cap to prevent integer-overflow
heap overflow on a malicious baseline
osinfo.c cross-platform host detection (Linux distro,
macOS version, Windows 10/11 by build, BSDs)
stats.c local JSONL aggregator (-S); strictly local,
no network
tail.c -T pretty-printed event tail with colour;
NO_COLOR / FORCE_COLOR / TERM=dumb honoured
term.c cross-platform terminal-palette autodetect
(kitty / iTerm2 / Windows Terminal /
PowerShell / conhost on Win10+)
wizard.c --wizard first-time setup; OS-aware watch
recommendations + critical-region section +
CVE-2026-31431 mitigation guidance
xmem.c xmalloc / xrealloc / xstrdup
beige.c BEIGE Behavioral Event Intelligence & Graph
Engine: bounded sliding-window of every
emitted event, lightweight path-prefix co-
occurrence graph, multi-stage campaign
signature matcher, anomaly-density boost.
All memory bounded at init (~200 KiB);
fail-safe init disables BEIGE on OOM
without taking the daemon down. Feeds the
I_COOC scoring identifier.
beige_ring.c Bounded circular event store (4096 events,
FIFO eviction). Foundation data structure
every other BEIGE module reads from
beige_graph.c Path-prefix co-occurrence graph: 1024 nodes,
4096 directed edges, LRU-by-timestamp
eviction, periodic decay sweep
beige_patterns.c Eight hard-coded campaign signatures +
optional external pattern-file loader; up to
32 patterns × 8 stages each
beige_stats.c Rolling 1-hour window split into 12 five-
minute buckets; density + log2 path-entropy
=> 0..40 boost folded into I_COOC
gui/ Tauri 2 + React + TypeScript read-only observer
UI; tails events_jsonl in real time
tests/ Standalone unit tests for the BEIGE modules
(ring, graph, patterns, stats, core API,
integration, validation, safe_strlen).
`make -C tests run` is part of CI
conf/ direc.conf and supply-iocs.conf templates
systemd/ sandboxed direcd.service unit (Restart=always,
OOMScoreAdjust=-1000, LimitMEMLOCK=infinity,
full @system-service syscall filter)
packaging/arch/ PKGBUILD for Arch Linux
docs/ ecosystem references, npm install protection
map, per-incident write-ups, supported PM
table, BEIGE.md (full BEIGE design + operator
playbook)
YurilLAB Supply Chain.docx YurilLAB research, 2022-2026
The work below is sequenced from the YurilLAB DireC Implementation
Document (May 2026), which maps documented 2022–2026 supply-chain
incidents to concrete DireC detections. Each item is grounded in a
specific incident; see docs/roadmap.md for the
full incident-to-detection map.
Near-term (architectural extensions, no external dependencies).
- Slopsquatting Bloom-filter v1 (USENIX Security 2025 hallucination corpus, ~205,474 names, ~300 KB filter at 1% FPR).
- Maintainer behavioural anomaly detection (registry API polling, ctx hijack and XZ-Utils trust-building shape).
- Tag rollback / retroactive tag poisoning (GITREF backend watching
.git/refs/tags/; tj-actions / TeamPCP imposter commits).
Medium-term (new parsing capabilities).
- Phantom dependency detection (Axios
plain-crypto-js, Mar 2026): cross-reference packages innode_modules/and the lockfile againstrequire()/importstatements in the application source tree. - Nested
.pthencoding decoder (TeamPCPlitellm_init.pth, Mar 2026): iterative base64 / hex / ROT13 unwrap before applying the existingioc_pth_payload_tokens[]matching. - Build-to-source divergence (XZ-Utils CVE-2024-3094;
SolarWinds SUNBURST): extend
direcd -Vwith a--build-verifymode that diffs build scripts, M4 macros, test-fixture binaries, and shipped shared objects between a Git checkout and a release artefact.
Research (high-effort, narrow blast radius).
- ELF IFUNC resolver hijacking (XZ-Utils CVE-2024-3094 binary layer): lightweight in-process ELF parser, IFUNC symbol classification, known-benign IFUNC resolver allow-list.
- Per-PM known-good-version fingerprint database, so
pm.binary-changedcan be down-graded to INFO when the new content hash matches a published upstream release. - Cross-ecosystem coverage:
.NETextension method interception (shanhai666 NuGet PLC sabotage, 2023–2024), Java classpath shadowing (MavenGate, 2024), Go module-proxy cache poisoning (boltdb-go/bolt, 2021–2024).
DireC's IoC database is the easiest meaningful contribution: append a
new entry to src/iocs.c (ioc_packages[], ioc_oob_domains[],
ioc_suspicious_substr[]) with a short reference, run the e2e tests
in .github/workflows/ci.yml locally, and open a PR.
For new detection classes, the per-detection layout in supply.c
keeps additions self-contained — see scan_setup_py, scan_npmrc,
or the .pth payload scan block as templates.
If DireC catches something we should know about, please open an issue or contact the YurilLAB Security Team. The research document is updated as new incidents are publicly disclosed.
See the in-tree LICENSE file once published. Until then, all code is provided for defensive supply-chain monitoring use only.