supply-chain-defense · diff
git:20260930.c3e2036 to git:20261005.8254c3e
116 added, 441 removed. Audit A to A.
---
name: supply-chain-defense
- description: "Behavioural-first defense against poisoned npm/PyPI packages in the publish-to-advisory window CVE tools miss. Use before every install or version bump: 7-day cooldown gate, Socket.dev behavioural score, stale-OIDC audit, publish-token rotation, and a worm-persistence self-scan."
+ description: "Behavioural-first defense against poisoned npm/PyPI/Composer/Cargo packages, malicious editor extensions and config-as-code repo poisoning, in the publish-to-advisory window CVE tools miss. Use when adding or bumping a dependency, when an advisory names a package you may run, when auditing CI OIDC trust or publish tokens, or when checking a machine or repo for worm persistence: 7-day cooldown gate, Socket.dev score, exposure and integrity scans."
license: MIT
allowed-tools: "Read Edit Write Bash Glob Grep Agent WebFetch"
metadata:
author: claude-mods
related-skills: security-ops, ci-cd-ops, github-ops, auth-ops
---
# Supply Chain Defense
- Proactive, behavioural-first defense against the 2026 software supply chain threat:
- self-propagating worms (Shai-Hulud / Mini Shai-Hulud) that poison popular npm and
- PyPI packages, steal credentials, republish from stolen tokens, and inject
- persistence hooks into **Claude Code and VS Code settings** specifically.
-
- ## Helps with
-
- Every install or version bump is a use case for this skill, not just suspected attacks — the routine cooldown gate + behavioural score is the whole point.
+ Behavioural-first defense against the 2026 supply-chain worms (Shai-Hulud / Mini
+ Shai-Hulud): they poison popular npm and PyPI packages, steal credentials, republish
+ from stolen tokens, and plant persistence hooks in **Claude Code and VS Code
+ settings**. `security-ops` is the reactive half (`npm audit` / `pip-audit` against
+ the CVE database); this skill is the proactive half and judges what a fresh package
+ *does*. The gap it closes is the 30-minute-to-6-hour window between "published" and
+ "advisory issued" - `references/threat-model.md` shows how lockfiles, `npm audit`,
+ 2FA and even Sigstore/SLSA provenance were each bypassed in 2026.
- Deciding whether a dependency you're about to add is safe — getting a behavioural
- verdict on an npm or PyPI package *before* `npm install` / `pip install`, not days
- later when a CVE drops. `socket package score`, the depscore MCP, or
- `scripts/preinstall-check.sh`.
+ Every install or version bump is a use case, not just a suspected attack: the
+ routine below is the point.
- A teammate or CI just pulled a freshly-published package version and you need to
- know if it's poisoned. The Shai-Hulud / Mini Shai-Hulud worm ships malicious
- versions that live for only hours (axios 1.14.1 / 0.30.4 were live ~3h).
+ ## The routine: every install or version bump
- `npm audit` / `pip-audit` come back clean but you're uneasy — those are
- CVE/advisory-driven and blind to malware that hasn't been reported yet. You want
- behavioural analysis (new `postinstall` hooks, unexpected network calls,
- obfuscated payloads), not a CVE lookup.
+ 1. **Score it** before you suggest or add it: the depscore MCP (free, no key) or
+ `socket package score <eco> <name> <version>`. Score the whole manifest, not just
+ the new package - depscore takes a list.
+ 2. **Age it**: `bash scripts/preinstall-check.sh <pkg>[@ver] ...` (`--pip`,
+ `--composer`, `--cargo`, `--go`); exit 10 = published inside the 7-day cooldown.
+ Never suggest a day-zero (`@latest`) release for a production path.
+ 3. **Install behind a gate**: `socket npm install <pkg>`, or no lifecycle scripts at
+ all (`npm config set ignore-scripts true`).
+ 4. **Pin it**: commit the lockfile; exact versions for anything in CI/prod.
+ 5. **Don't stop at a clean `npm audit`** - it only knows yesterday's CVEs.
- Setting up Socket.dev on a budget — the free `socket` CLI, the GitHub PR app, or
- the `depscore` MCP for Claude Code (`claude mcp add --transport http socket-mcp
- https://mcp.socket.dev/`, no API key). Deciding free vs paid tiers.
+ ## Which workflow?
- Auditing GitHub Actions for the stale-OIDC / `pull_request_target` misconfiguration
- that Mini Shai-Hulud abused to mint npm publish tokens from an orphaned workflow.
- `zizmor`, or `scripts/integrity-audit.sh`.
+ Step-by-step for each letter: `references/workflows.md`.
- Hardening installs against `postinstall` / `preinstall` lifecycle-script malware —
- `npm config set ignore-scripts true`, the `socket` wrapper, `lockfile-lint`, or the
- `pre-install-scan.sh` hook.
+ | Situation | Workflow | First move |
+ |---|---|---|
+ | About to add or bump a dependency | A, E | The routine above |
+ | A teammate or CI just pulled a fresh version (axios 1.14.1 was live ~3 h) | A, I | Score it, then `exposure-check.py` |
+ | `npm audit` / `pip-audit` clean but you're uneasy | A, K | Behavioural score + `postinstall-audit.py` |
+ | Rolling out Socket.dev on a budget | B | GitHub app + depscore MCP (both free) |
+ | Install commands need a gate | C | `socket` wrapper, `ignore-scripts`, `lockfile-lint` |
+ | CI publishes to npm/PyPI | D, F | `integrity-audit.sh` or `zizmor`; revoke stale OIDC trust; rotate to short-lived OIDC |
+ | Production deps auto-update | E | Renovate `minimumReleaseAge: 7 days` |
+ | Editor extensions, Claude plugins, skills | G | `exposure-check.py`, then `scan-extensions.sh [--deep]` |
+ | Is this machine already compromised? | H | `integrity-audit.sh` (hooks, MCP servers, shell rc, `.npmrc`) |
+ | An advisory names a package or extension | I | `exposure-check.py --root <dirs>` (exit 10 = exposed) |
+ | An unexplained UAC prompt or process you can't place; a post-compromise tripwire (Windows) | J | `phone-home-monitor.ps1` (stealers exfiltrate as outbound connections) |
+ | Is anything already on disk misbehaving? | K | `postinstall-audit.py --root <dirs>` |
+ | A repo you own is poisoned in place (build config, `tasks.json`) | L | `config-drift-check.py --staged` / `--root .` |
+ | Which scanner for which job? | - | `references/tooling-landscape.md` |
+ | Proof the skill covers a given attack | - | Coverage matrix in `references/threat-model.md` |
- Checking whether *this* machine is already compromised — detecting worm persistence
- hooks injected into `~/.claude/settings.json`, `~/.claude.json`, or VS Code
- `settings.json`. `scripts/integrity-audit.sh`.
+ A finding from H-L is an incident: isolate the machine, rotate every credential it
+ could read, then investigate. Removing the hook or package alone treats a symptom -
+ the credentials were stolen before the worm persisted.
- Choosing among supply-chain scanners — when to reach for Socket vs GuardDog vs
- OSV-Scanner vs zizmor vs Harden-Runner. See `references/tooling-landscape.md`.
+ ## Running the scripts
- Enforcing a release-age cooldown so production never pulls a day-zero version
- (Renovate `minimumReleaseAge`), and rotating long-lived npm/PyPI publish tokens to
- short-lived OIDC.
+ Paths are relative to this skill folder. Launch the `.py` scripts through
+ `scripts/run-python.sh`: it runs the first of `python3`, `python`, `py` that really
+ is Python 3.8+ (on Windows `python3` is often the Microsoft Store alias, which exits
+ 49 and runs nothing - the scripts' shebang hits it too). Shared exit codes: 0 ok,
+ 2 usage, 3 not found, 5 missing dependency, 7 unavailable, **10 = finding** (review
+ item / inside cooldown / exposed / behavioural hit). Every script takes `--json`;
+ stdout is data, stderr is progress.
- Responding to a fresh advisory — it names a poisoned package, version, or
- **malicious VS Code / Cursor extension** and you need to know whether any project or
- machine actually has it installed *right now*. `scripts/exposure-check.py` matches
- on-disk npm / PyPI / Composer / Cargo / Go / RubyGems lockfiles **and installed
- editor extensions** against an IOC catalog seeded with cited 2026 incidents (axios
- 1.14.1, Laravel-Lang tag rewrite, Nx Console 18.95.0 → the GitHub breach). For fleet-scale exposure response
- on macOS/Linux, see Bumblebee in `references/tooling-landscape.md`.
+ | Script | Answers | Run |
+ |---|---|---|
+ | `preinstall-check.sh` | Is this release inside the cooldown? | `bash scripts/preinstall-check.sh --pip requests fastapi@0.110.0` |
+ | `integrity-audit.sh` | Has a worm persisted in AI-tool, editor, shell or package-manager config? Live OIDC publish trust? | `bash scripts/integrity-audit.sh --json \| jq '.data.review[]'` |
+ | `exposure-check.py` | Is a named-bad package or extension installed? (`assets/exposure-catalog.json`) | `bash scripts/run-python.sh scripts/exposure-check.py --root ~/code` |
+ | `postinstall-audit.py` | Is any installed package *behaving* like malware? | `bash scripts/run-python.sh scripts/postinstall-audit.py --root . [--deep] [--live]` |
+ | `config-drift-check.py` | Does a build config or `tasks.json` carry an injected loader? | `bash scripts/run-python.sh scripts/config-drift-check.py --staged` |
+ | `scan-extensions.sh` | Which extensions, plugins and skills changed recently? | `bash scripts/scan-extensions.sh [--deep]` |
+ | `phone-home-monitor.ps1` | What connects out, from which process? (Windows) | `pwsh -NoProfile -File scripts/phone-home-monitor.ps1 [-Sysmon] [-Watch]` |
- Watching what already-running code **does on the network** — an unexplained UAC
- prompt, a process you can't place, or just wanting a tripwire for the
- post-compromise phase: a stealer that already landed exfiltrates as an *outbound
- connection* (Shai-Hulud phoned home to `webhook.site`). On Windows,
- `scripts/phone-home-monitor.ps1` maps every outbound TCP connection to its owning
- process + parent chain + signing status and flags interpreter/raw-IP/IOC/
- package-manager-child patterns. See `references/phone-home-monitoring.md`.
+ All are read-only except `phone-home-monitor.ps1 -InstallTask` (registers a logon
+ task - T3). Flags, dependencies and side effects per script:
+ `references/scripts-and-hooks.md`. After any edit run `bash tests/run.sh` (offline;
+ exit 0 = all pass).
- Defending a repo you *already own* against being poisoned in place — the
- **config-as-code / trusted-repo** class (PolinRider / EtherHiding, DPRK UNC5342)
- where the dependency tree stays clean and an obfuscated blockchain-C2 loader is
- appended to your own `vite.config.js` / `tailwind.config.js` / `.vscode/tasks.json`,
- then propagated by backdated false-flag force-pushes. The dependency scanners are
- blind to this; `scripts/config-drift-check.py` (pre-commit + CI) plus the
- branch-protection / signed-commits / hardware-key discipline in
- `references/repo-integrity.md` cover it. Workflow L.
+ ## Setup (one-time, all free)
- Wanting proof the skill covers a specific attack — the
- `references/threat-model.md` "Coverage" matrix maps every 2026 vector
- (maintainer compromise, OIDC theft, lifecycle scripts, persistence hooks, forged
- provenance, tag-rewrite, malicious extensions, MCP attacks, **config-as-code /
- trusted-repo poisoning**) to its control + caveat.
+ The scripts need no setup. What you switch on is the live tooling, in priority order:
- ## Overview
+ 1. **depscore MCP** - behavioural package scoring inside Claude Code, no API key:
+ `claude mcp add --transport http socket-mcp https://mcp.socket.dev/`
+ 2. **Install hooks** (optional; they ship with claude-mods, not in this folder):
+ `pre-install-scan.sh` + `manifest-dep-scan.sh`, advisory by default,
+ `SUPPLY_CHAIN_BLOCK=1` for a hard gate. Wiring: `references/scripts-and-hooks.md`.
+ 3. **Socket CLI wrapper** (zero-auth): `npm i -g socket`, then
+ `socket npm install <pkg>` or `socket wrapper on`. `socket login` is only needed
+ for `scan` / `score` / `ci`.
+ 4. **Behavioural engine (on demand)** for `--deep`: `uv tool install guarddog semgrep`.
+ Not installed by default; `--deep` says loudly when it is missing instead of
+ reporting a scan it never ran. On Windows GuardDog needs `PYTHONUTF8=1` (the
+ script sets it).
- This skill is the operational complement to two siblings:
+ The free Socket tier covers this whole campaign; paid tiers buy reachability and
+ seats, not detection (`references/socket-cli.md`). The minimum viable set is the
+ depscore MCP + the cooldown + `ignore-scripts`; OSV-Scanner, zizmor and Harden-Runner
+ are situational (`references/tooling-landscape.md`).
- - **`security-ops`** is *reactive* — it runs `npm audit` / `pip-audit` /
- `govulncheck` against the **CVE/advisory database**. Necessary, but blind to a
- malicious package that hasn't been reported yet.
- - **`supply-chain-defense`** (this skill) is *proactive* — it analyses what a
- freshly-published package actually **does** (new install scripts, network calls,
- obfuscation) within seconds of publication, before any CVE exists.
+ ## Portability
- > The defensive gap is the window between "package published" and "advisory
- > issued" — typically 30 minutes to 6 hours. A worm does real damage in that
- > window. Behavioural analysis is the only control that closes it. See
- > `references/threat-model.md` for why lockfiles, `npm audit`, 2FA, and even
- > Sigstore/SLSA provenance were each bypassed in the wild in 2026.
+ This folder runs when copied alone: scripts, `assets/` catalogs, references, the
+ launcher and `tests/run.sh` are all inside it. Two things are optional and live
+ outside it, in claude-mods: the `skills/_lib/term.sh` panels (without them the
+ scripts print plain ASCII framing, same data and exit codes) and the hooks above.
+ `tests/run.sh` copies the folder alone and proves it. Details:
+ `references/scripts-and-hooks.md`.
## The layers
- Layers 1–5 are **dependency-integrity** (is a package I pull malicious?). Layer 6 is
- a **separate detection surface — repo-integrity** (is a repo I already own being
- poisoned in place?). They are not the same problem; see the note below.
+ Layers 1-5 ask "is a package I pull malicious?". Layer 6 is a different axis - "is
+ a repo I already own being poisoned in place?" - where the payload never enters as a
+ package, so every dependency scanner is structurally blind to it.
| Layer | Control | What it stops |
|---|---|---|
- | 1. Detection | Behavioural scanner (Socket.dev) on every dependency change | Poisoned package merged via PR or pulled by an install |
- | 2. Interception | `socket` CLI wrapper + `pre-install-scan.sh` hook | Lifecycle scripts (`postinstall`, sdist `setup.py`) executing on install |
- | 3. Hygiene | Stale-OIDC audit, dep cooldown, token rotation, extension audit | The *entry points* worms use to mint publish access |
- | 4. Self-integrity + exposure | `integrity-audit.sh` (persistence hooks in AI-tool / editor configs) + `exposure-check.py` (am I running a named-bad package?) | Worm persistence on *this* machine; latent exposure to a fresh advisory |
- | 5. Post-install behavioural | `postinstall-audit.py` (what's already unpacked actually *does*) + `phone-home-monitor.ps1` (what it's *connecting to*) | A poisoned release that slipped past layers 1–2 and is now on disk / exfiltrating |
- | 6. **Repo-integrity** | `config-drift-check.py` (config-as-code injection in build configs / `tasks.json`) + branch-protection / signed-commits / hardware-key discipline (`references/repo-integrity.md`) | **Trusted-repo poisoning** (PolinRider / EtherHiding) — a build-config loader + backdated false-flag force-push that the dependency scanners never see |
-
- Layers 1–3 act before code runs; 4–5 assume something already got through and hunt
- for it on disk and on the wire. `exposure-check.py` answers "do I have a *named-bad*
- package?"; `postinstall-audit.py` answers "is *any* installed package *behaving* like
- malware?" — the unknown-bad case a fresh advisory hasn't caught up to yet.
-
- > **Layer 6 is a different axis, not a deeper layer.** Layers 1–5 all reason about
- > the **dependency tree** — Socket, depscore, the cooldown, `exposure-check.py`, and
- > `postinstall-audit.py` are *structurally blind* to a poisoned `vite.config.js` /
- > `tailwind.config.js` / `.vscode/tasks.json`, because that code never enters as a
- > package. The 2026 PolinRider / EtherHiding campaign (DPRK UNC5342) reached at least
- > one company via a clean dependency tree and a single shared deploy key. Layer 6 —
- > content-diffing first-party config files plus git-provenance discipline — is the
- > only thing that catches it. Workflow L and `references/repo-integrity.md`.
-
- ## Cost reality — free is enough to start
-
- **The Socket CLI is open-source and free. The free account tier defends against
- this exact campaign at $0.** Paid tiers buy noise-reduction and scale, not the core
- malware detection.
-
- | Capability | Free ($0) | Paid (Team $25/dev → Enterprise) |
- |---|---|---|
- | `socket` CLI (open source) | ✅ | ✅ |
- | Malware / behavioural blocking, 70+ risk types | ✅ | ✅ |
- | Private repos (unlimited) | ✅ | ✅ |
- | Scans / month | 1,000 | 5,000 → unlimited |
- | Members | 3 | 10 → unlimited |
- | **depscore MCP for Claude Code (no API key)** | ✅ | ✅ |
- | Reachability analysis (cuts CVE false positives) | ❌ | ✅ (Team+) |
- | SSO/SAML, SBOM, GitHub Actions + AI-model scanning | ❌ | ✅ (Business+) |
- | OSS projects | Free **Team** account on request | — |
-
- Start free. Move to Team only when CVE false-positive noise or seat count justifies
- it. Full breakdown + exact commands in `references/socket-cli.md`.
-
- ## Setup (one-time)
-
- All free, in priority order. The **scripts in this skill need no setup** — run them
- directly. What you switch on is the live tooling:
-
- 1. **depscore MCP** — behavioural package scoring inside Claude Code, no API key:
- `claude mcp add --transport http socket-mcp https://mcp.socket.dev/`
- 2. **Install-scan hook** — advisory on every dependency install. Wire
- `pre-install-scan.sh` into `~/.claude/settings.json` (see "Hook setup" below);
- set `SUPPLY_CHAIN_BLOCK=1` for a hard gate. Restart Claude Code after editing.
- 3. **Socket CLI wrapper** (optional, zero-auth): `npm i -g socket`, then
- `socket npm install <pkg>` or `socket wrapper on`. `socket login` is only needed
- for `scan` / `score` / `ci`, not the install wrapper.
- 4. **Behavioural engine (optional, on-demand)** for `scan-extensions.sh --deep`:
- `uv tool install guarddog semgrep`. **Not installed by default** — stay lean.
- `--deep` auto-detects it; if absent, that mode runs inventory + recency and
- loudly recommends installing rather than reporting a scan it didn't run. On
- Windows GuardDog needs `PYTHONUTF8=1` (the script sets it for you).
+ | 1. Detection | Socket.dev behavioural scan on every dependency change | Poisoned package merged via PR or pulled by an install |
+ | 2. Interception | `socket` wrapper, `ignore-scripts`, install hook | Lifecycle scripts (`postinstall`, sdist `setup.py`) running on install |
+ | 3. Hygiene | Stale-OIDC audit, cooldown, token rotation, extension audit | The entry points worms use to mint publish access |
+ | 4. Self-integrity + exposure | `integrity-audit.sh`, `exposure-check.py` | Persistence on *this* machine; exposure to a fresh advisory |
+ | 5. Post-install behaviour | `postinstall-audit.py`, `phone-home-monitor.ps1` | A poisoned release already on disk or exfiltrating |
+ | 6. Repo integrity | `config-drift-check.py` + signed commits, no force-push, no shared keys | Config-as-code poisoning (PolinRider / EtherHiding) behind a clean dependency tree |
- Situational extras — install only when the need arises
- (`references/tooling-landscape.md`): the behavioural engine above, OSV-Scanner (CVE
- breadth), zizmor + Harden-Runner (CI hardening). The minimum viable set is Socket's
- MCP + the cooldown + `ignore-scripts`; everything else is on-demand.
+ Layers 1-3 act before code runs; 4-5 assume something got through and hunt for it.
+ `exposure-check.py` answers "do I have a *named-bad* package?"; `postinstall-audit.py`
+ answers "is *any* installed package behaving like malware?" - the unknown-bad case.
+ Layer 6 playbook: `references/repo-integrity.md`.
## Safety tiers
| Operation | Tier | Execution |
|---|---|---|
| Score / scan a package before adding it | T1 | Inline (depscore MCP or `socket package score`) |
| Detect project stack + installed tools | T1 | Inline |
| Run `integrity-audit.sh` (read-only) | T1 | Inline |
| Run `preinstall-check.sh` on a package spec | T1 | Inline |
| Behavioural scan of full manifest (`socket scan`) | T2 | Inline / background |
| Audit GitHub Actions for stale OIDC trust | T2 | Inline (read workflows) |
| **Install / upgrade a dependency** | T3 | Confirm + scan first |
| **Rotate publish tokens / revoke OIDC trust** | T3 | Confirm — changes live infra |
| **Remove a flagged persistence hook from settings** | T3 | Confirm — edits user config |
- ## Workflows
-
- These map 1:1 to the briefing's recommended actions, ordered effort→value.
-
- ### A. Score a package before suggesting it (do this proactively)
-
- When considering adding a dependency, get a behavioural verdict *first*:
-
- - **With the depscore MCP** (free, no key): ask the `socket-mcp` server for the
- package score. Setup is a one-liner — see `references/socket-cli.md`.
- - **With the CLI:** `socket package score <ecosystem> <name> <version>`
- - **Cooldown check:** `scripts/preinstall-check.sh <pkg>[@version] …` flags any
- package published inside the 7-day cooldown window and routes to `socket` if
- installed.
-
- Never recommend a brand-new (`@latest`, day-zero) release for a production path.
-
- **Score the *whole* current project, not just one package** — the depscore MCP
- takes a list, so read every dependency from the manifest and score them in one
- call: parse `package.json` (`dependencies` + `devDependencies`), `requirements.txt`,
- `composer.json`, `Cargo.toml`, etc., then pass the full `{depname, ecosystem,
- version}` set to depscore. Triage anything with a low `supplyChain` / `quality`
- score before the next install or commit. This is the highest-value recurring local
- move — do it when opening a repo and after any dependency change.
-
- ### B. Trial Socket.dev on one repository (≈1 hour)
-
- 1. Pick the lowest-risk repo (small surface, low client exposure).
- 2. Install the **GitHub app** (free tier, private repos included) — it comments a
- risk report on any PR that adds/bumps a dependency.
- 3. Optionally `npm install -g socket && socket login` for terminal scanning.
- 4. Run for two weeks, review what it flags during PRs, then expand.
-
- ### C. Wrap installs at the terminal (layer 2)
-
- Route risky installs through Socket so they're intercepted before lifecycle
- scripts run:
-
- - One-off: `socket npm install <pkg>` / `socket npx <pkg>`
- - Workspace-wide: `socket wrapper on` (aliases `npm`/`npx` → routed through
- Socket; `socket wrapper off` to disable; `socket raw-npm` to bypass once).
- - Claude Code reinforcement: enable the `pre-install-scan.sh` hook (advisory by
- default) — see Hook setup below.
- - Cheapest possible mitigation — **disable lifecycle scripts entirely** where the
- project doesn't need them: `npm config set ignore-scripts true` (npm), or pnpm
- `enable-pre-post-scripts=false`. This neuters the `postinstall` vector outright.
- - Validate the lockfile itself with `lockfile-lint` — catches a lockfile whose
- resolved URLs point at a non-registry host (lockfile injection). See
- `references/tooling-landscape.md`.
-
- ### D. Audit GitHub Actions for stale OIDC trust (≈half a day)
-
- The Mini Shai-Hulud entry point was an **orphaned commit with live OIDC trust
- federation** to npm. No phished human. Audit and revoke:
-
- - Find workflows requesting an OIDC token: search for `id-token: write` and
- `permissions:` blocks, plus `npm publish` / `pypi` / `twine` / trusted-publisher
- steps. `scripts/integrity-audit.sh` flags these.
- - For each: is publish trust still needed? If not, revoke the trust relationship
- on the registry side (npm trusted publisher / PyPI publisher) **and** remove the
- workflow permission.
-
- ### E. Pin and freeze production dependencies
-
- Commit lockfiles. Pin exact versions for anything in CI/prod. Apply a **7-day
- cooldown**: don't auto-update production deps until a release has aged a week, so
- the ecosystem has time to detect and remediate a compromise. (Axios poisoned
- versions were live ~3 hours — a 7-day lag would have caught it.)
-
- ### F. Rotate publish tokens → short-lived OIDC
-
- Audit who holds standing npm/PyPI publish tokens. Prefer short-lived OIDC trusted
- publishing over long-lived tokens. Rotate any long-lived token; tighten the set of
- accounts with publish access. (T3 — confirm before rotating, it can break CI.)
-
- ### G. Editor extension / plugin audit (Nx Console / GitHub-breach vector)
-
- Three layers, in order — known-bad, then visibility, then behavioural:
-
- 1. **Known-bad (IOC):** `python scripts/exposure-check.py` matches installed
- extensions (VS Code/Cursor/Windsurf/VSCodium) against the catalog — e.g. Nx
- Console `nrwl.angular-console@18.95.0`, the backdoor behind the GitHub
- 3,800-repo breach. Catches what's already named in an advisory.
- 2. **Inventory + recency:** `bash scripts/scan-extensions.sh` lists every
- extension, Claude plugin (with pinned commit SHA), and skill, flagging what
- changed inside the recency window — the exact "no visibility into what's
- installed or how recently" gap the campaign exploits (Nx Console was live 11
- min). Zero-dependency, no false positives.
- 3. **Unknown-bad (behavioural):** `bash scripts/scan-extensions.sh --deep` runs
- GuardDog's semgrep rules against recently-changed extensions when `guarddog` +
- `semgrep` are present (`uv tool install guarddog semgrep`, on-demand — not kept
- installed). If absent it runs inventory only and recommends the install — never
- a false-clean. Best-effort on minified bundles — layers 1–2 stay the backbone for
- extensions; layer 3 is strongest on source (plugins/skills).
-
- Verified-publisher status is **not** sufficient — Nx Console was a verified
- publisher with 2.2M installs. Pause anything recently published by a non-verified
- publisher until it ages.
-
- ### H. Self-integrity scan (layer 4 — the one the briefing didn't have to worry about)
-
- Run `scripts/integrity-audit.sh`. It is **read-only** and reports:
-
- - New/unexpected `hooks` or `mcpServers` entries in `~/.claude/settings.json`,
- `~/.claude/settings.local.json`, `~/.claude.json`, and project `.claude/`.
- - Suspicious entries in VS Code `settings.json` (startup commands, task autoruns).
- - Workflows with live OIDC publish trust (feeds workflow D).
-
- A worm's persistence hook into Claude Code settings is the IOC from the briefing's
- most-quoted line. If the scan flags something you didn't add, treat it as an
- incident: isolate, rotate credentials, and investigate before continuing.
-
- ### I. Exposure response — "an advisory just dropped; are we running it?"
-
- When an advisory names a poisoned package + version, the urgent question is which
- projects/machines already have it. Match local state against an IOC catalog:
-
- ```bash
- python scripts/exposure-check.py --root ~/code --root ~/work
- python scripts/exposure-check.py --root . --json | jq '.data.findings[]'
- ```
-
- It reads npm lockfiles and Python installed metadata (no execution, no network),
- exits **10** if anything matches. The bundled `assets/exposure-catalog.json` is
- seeded with cited 2026 IOCs (axios 1.14.1 / 0.30.4) and is meant to be **extended
- from advisories** — add `{ecosystem, package, versions[]}` entries as incidents
- break. A match is an incident: isolate, rotate, remove the package.
-
- For **fleet-scale** exposure response across many macOS/Linux endpoints (with far
- broader ecosystem + extension + MCP coverage), use Perplexity's **Bumblebee** —
- whose catalog format this borrows. It does not run on Windows; `exposure-check.py`
- is the cross-platform local equivalent. See `references/tooling-landscape.md`.
-
- ### J. Outbound phone-home monitoring (Windows — the post-compromise tripwire)
-
- Layers 1–4 act at install time or at rest; nothing above watches what running code
- *does on the network*. `scripts/phone-home-monitor.ps1` closes that gap:
-
- ```powershell
- pwsh -NoProfile -File scripts/phone-home-monitor.ps1 # one snapshot, exit 10 on findings
- pwsh -NoProfile -File scripts/phone-home-monitor.ps1 -Status # which capture sources exist here?
- pwsh -NoProfile -File scripts/phone-home-monitor.ps1 -Watch -IntervalSeconds 30 # continuous, ring-buffer log
- pwsh -NoProfile -File scripts/phone-home-monitor.ps1 -InstallTask # logon daemon (T3 — confirm)
- ```
-
- Flags: IOC endpoints (`assets/network-ioc.json` — webhook.site is the cited
- Shai-Hulud exfil drop), binaries under `node_modules`/Temp, children of package
- managers (lifecycle-script behaviour), interpreters hitting raw public IPs,
- unsigned userland binaries. **Tool-first:** the preferred continuous source is
- Sysmon Event ID 3 with the SwiftOnSecurity config (`-Sysmon` consumes it; exit 5
- with the install one-liner when absent); TCP-table polling is the zero-install
- default. Full evaluation (Sysmon vs WFP 5156 vs polling vs tshark), wiring, and
- triage playbook: `references/phone-home-monitoring.md`. A finding routes back into
- H/I: `integrity-audit.sh` + `exposure-check.py` + credential rotation.
-
- ### K. Post-install behavioural sweep — "is anything already on disk misbehaving?"
-
- `exposure-check.py` (workflow I) answers the *known-bad* question; this answers the
- *unknown-bad* one. `scripts/postinstall-audit.py` walks installed `node_modules` and
- Python `site-packages` and flags what packages actually *do* — not what an advisory
- named:
-
- ```bash
- python scripts/postinstall-audit.py --root ~/code # exit 10 on a finding
- python scripts/postinstall-audit.py --root . --json | jq '.data.findings[]'
- python scripts/postinstall-audit.py --root . --deep # GuardDog confirms each flag
- python scripts/postinstall-audit.py --root . --live # is a flagged npm version still published?
- ```
-
- It flags shell/downloader lifecycle scripts, credential-path reads paired with exfil
- endpoints, env harvesting, obfuscation, persistence writes, and files modified after
- install. Findings need a **two-signal combo** (cred+net, env+net) so real `node_modules`
- trees don't false-alarm — the lesson from an earlier cut that lit up `three.js`/`vite`
- on `eval`+base64 alone. An **incremental fingerprint cache** makes it daily-runnable
- (only changed trees rescan), so wire it as a scheduled task — see
- `references/postinstall-audit.md` for the Task Scheduler / Claude Code cron recipes and
- the GuardDog/OSV/Socket tool evaluation. A high finding is an incident: isolate → read
- the flagged file → rotate credentials → confirm with `--deep` + `exposure-check.py`.
-
- ### L. Repo-integrity / config-drift — "is a repo I *own* being poisoned in place?"
-
- Workflows A–K all defend the **dependency tree** (or watch its network egress). This
- one defends a different surface: the **trusted-repo / config-as-code** class
- (PolinRider / EtherHiding, DPRK UNC5342), where the dependency tree stays clean and
- the payload is committed into your own build configs. The dependency scanners are
- structurally blind to it — see the Layer-6 note above and
- `references/threat-model.md` vector #12.
-
- `scripts/config-drift-check.py` is the on-disk detector. Run it two ways:
-
- ```bash
- python scripts/config-drift-check.py --root . # CI / full-repo sweep, exit 10 on finding
- python scripts/config-drift-check.py --staged # pre-commit: only staged config files
- python scripts/config-drift-check.py --root . --json | jq '.data.findings[]'
- ```
-
- It scans build configs (`vite/tailwind/webpack/next/rollup/postcss/svelte/astro.config.*`),
- `.vscode/tasks.json`, and `package.json` scripts for the Stage-2 injection signatures:
- blockchain explorer-API / RPC dead-drop endpoints (the EtherHiding payload read —
- `assets/network-ioc.json` `ETHERHIDING-BLOCKCHAIN-C2`), `eval` / `new Function` /
- shell-exec, Buffer-XOR decode loops, outbound network in a config that shouldn't have
- any, hex-var (`_0x..`) / long-escape obfuscation, an obfuscated appended blob, and
- `tasks.json` `runOn:folderOpen` auto-run. Zero-dependency, read-only.
-
- Wire it as a **pre-commit hook** (`--staged`, catches it before it's committed) **and**
- a **CI status check** (catches a force-pushed injection at the gate). A finding is an
- incident: read the flagged file, check the commit's **signature + server-side push
- timestamp** (a backdated author date lies; the push event doesn't), and rotate any
- credential the build could have touched.
-
- The detector is half the defense. The other half is **prevention + attribution** —
- no shared/standing keys, hardware-backed signing keys a RAT can't read, branch
- protection requiring **signed** commits and blocking force-push, server-side push-log
- as ground truth, build/env isolation, and VS Code Workspace Trust with auto-run tasks
- disabled. The full kill-chain-mapped playbook is `references/repo-integrity.md`.
-
- ## Hook setup — two checkpoints for the two ways a dep enters
-
- A dependency reaches a local machine two ways, and each gets an advisory hook:
-
- - **`pre-install-scan.sh`** (PreToolUse / `Bash`) — fires on install verbs
- (`npm/pnpm/yarn/bun install|add`, `pip install`, `uv add`, `composer
- require|install|update`, `gem install`, `cargo add`). Surfaces the cooldown +
- `socket` equivalent. Set
- `SUPPLY_CHAIN_BLOCK=1` for a hard gate; otherwise advisory.
- - **`manifest-dep-scan.sh`** (PostToolUse / `Write|Edit`) — fires when the agent
- *edits a manifest* (`package.json`, `requirements*.txt`, `composer.json`,
- `Cargo.toml`, `go.mod`, `Gemfile`, `pyproject.toml`) and the change adds a version
- spec — the Claude-Code path the install hook misses. Advises depscore + cooldown
- before install. High-signal: silent on version bumps / metadata edits.
-
- Both read the tool call as JSON on stdin (`.tool_input`), falling back to `$1`, and
- advise through one `additionalContext` JSON envelope on stdout - plain stdout from a
- tool hook goes to the debug log, never the model (`hooks/README.md`, "Output channels").
-
- ```json
- {
- "hooks": {
- "PreToolUse": [
- { "matcher": "Bash", "hooks": [
- { "type": "command", "command": "bash \"$HOME/.claude/hooks/pre-install-scan.sh\"", "timeout": 5 } ] }
- ],
- "PostToolUse": [
- { "matcher": "Write|Edit", "hooks": [
- { "type": "command", "command": "bash \"$HOME/.claude/hooks/manifest-dep-scan.sh\"", "timeout": 5 } ] }
- ]
- }
- }
- ```
-
## Anti-patterns
| Anti-pattern | Why it fails | Do instead |
|---|---|---|
| "We run `npm audit` in CI, we're covered." | Advisory-driven; blind to malware in the publish-to-CVE window — the exact gap the 2026 worms exploit. | Add a behavioural scan (Socket / GuardDog) gating the merge, not just a CVE check. |
| Trusting valid provenance / SLSA attestation as proof of safety. | Mini Shai-Hulud minted **valid Build L3 attestations** from stolen OIDC tokens. Valid ≠ safe. | Treat provenance as one signal; require behavioural verdict too. |
| Auto-updating production deps the day a release lands. | Poisoned versions live for hours; you become an early victim. | 7-day release-age cooldown (Renovate `minimumReleaseAge`). |
| Treating a verified-publisher VS Code extension as trustworthy. | Nx Console: verified publisher, 2.2M installs, backdoored. | Check publication recency; pause <7-day non-verified; audit on a schedule. |
| Leaving `id-token: write` on workflows that no longer publish. | The orphaned-OIDC entry point — a token minted from a stale workflow. | Revoke registry trust + drop the permission. Run `zizmor`. |
| Deleting a found persistence hook and moving on. | The worm stole credentials *before* it persisted; the hook is the symptom. | Treat as an incident: isolate, rotate every reachable credential, then investigate. |
## Verification checklist
- [ ] A behavioural verdict (not just `npm audit`) exists for every newly added/bumped dependency
- [ ] Production deps respect a release-age cooldown (≥7 days)
- [ ] Lockfiles committed; exact pins for anything in CI/prod
- [ ] No workflow carries `id-token: write` it doesn't need (`zizmor` clean)
- [ ] Long-lived publish tokens rotated or replaced with short-lived OIDC
- [ ] `scripts/integrity-audit.sh` exits 0 (no unexplained hooks/MCP servers in `.claude/` or VS Code settings)
- [ ] `ignore-scripts` enabled where lifecycle scripts aren't needed
- [ ] depscore MCP or `socket` CLI available so packages can be scored before they're suggested
- ## Scripts
-
- All seven follow the Axiom Tool Protocol: `--help` with EXAMPLES, `--json` for
- machine-readable output, stdout = data / stderr = progress, semantic exit codes
- (0 ok, 2 usage, 3 not-found, 4 invalid, 5 missing-dep, 7 unavailable, **10 = signal
- found** — review items / inside-cooldown / exposed / behavioural finding).
- Pipe-friendly: `--json | jq`.
-
- **Dependencies.** The skill is markdown + bash and every script's *default* mode is
- zero-dep (bash, coreutils, `curl`; `jq` only for `--json`). `scan-extensions.sh
- --deep` auto-detects `guarddog`+`semgrep` and uses them when present; when absent it
- runs inventory + recency and *loudly recommends* the on-demand install rather than
- reporting a behavioural scan it never ran (which would be the same false-clean
- GuardDog itself hits without semgrep). Nothing heavyweight is kept on the machine by
- default. All named tools (socket, guarddog, semgrep, zizmor, OSV-Scanner) are an
- optional *menu* — see `references/tooling-landscape.md` → "How the controls
- interact" for the minimum viable set.
-
- | Script | Purpose | Side effects |
- |---|---|---|
- | `scripts/integrity-audit.sh` | Scan AI-tool configs (Claude Code/Desktop, Gemini, MCP host JSON) + editor settings (VS Code, Cursor, Windsurf, VSCodium) for injected persistence hooks/MCP servers; flag workflows with live OIDC publish trust (uses `zizmor` if installed). Exit 10 if anything to review. | Read-only |
- | `scripts/preinstall-check.sh` | Given package specs, report registry publish age (npm/PyPI), flag any inside the cooldown window, route to `socket` if available. Exit 10 if any inside cooldown. | Read-only (queries registries) |
- | `scripts/exposure-check.py` | Match on-disk **npm (package-lock/pnpm/yarn) / PyPI / Composer / Cargo / Go / RubyGems** lockfiles **and installed editor extensions** against an IOC catalog (`assets/exposure-catalog.json`) — the "are we running a named-bad version/extension?" check. Supports a `*` wildcard for tag-rewrite attacks. Exit 10 if exposed. Catalog format borrowed from Bumblebee. | Read-only |
- | `scripts/phone-home-monitor.ps1` | **Windows outbound-connection tripwire** — map every outbound TCP connection to owning process + parent chain + signing status; flag IOC endpoints (`assets/network-ioc.json`), `node_modules`/Temp binaries, package-manager children, interpreter→raw-IP. Sources: Sysmon EID 3 (`-Sysmon`, preferred) or TCP-table polling (default). `-Watch`/`-InstallTask` for continuous capture with a ring-buffer JSONL log. Exit 10 on medium+ findings. | Read-only (except `-InstallTask`, which registers a logon scheduled task) |
- | `scripts/postinstall-audit.py` | **On-disk behavioural scan** — walks installed `node_modules` + Python `site-packages` under `--root` dirs and flags what already-unpacked packages *do*: shell/downloader lifecycle scripts, credential-path reads paired with exfil endpoints, env harvesting, obfuscation, persistence writes, files modified after install (tamper). Two-signal combos to avoid `node_modules` false-positives. Incremental per-package fingerprint cache (daily-runnable); `--deep` confirms flags with GuardDog; `--live` checks the registry still serves a flagged npm version (unpublished = IOC). Exit 10 on findings ≥ `--min-severity`, 7 if `--live` registry unreachable. See `references/postinstall-audit.md`. | Read-only |
- | `scripts/config-drift-check.py` | **Repo-integrity / config-as-code scanner** (layer 6) — scans build configs (`vite/tailwind/webpack/next/rollup/postcss/svelte/astro.config.*`), `.vscode/tasks.json`, and `package.json` scripts for PolinRider/EtherHiding injection: blockchain explorer-API / RPC dead-drop endpoints (extends from `assets/network-ioc.json`), `eval`/`new Function`/shell-exec, Buffer-XOR decode loops, outbound network in a config, `_0x..`/long-escape obfuscation, an obfuscated appended blob, and `tasks.json` `runOn:folderOpen` auto-run. `--staged` for pre-commit, `--root` for CI. Exit 10 on a finding. Zero-dep. See `references/repo-integrity.md`. | Read-only |
- | `scripts/scan-extensions.sh` | **Unknown-bad** triage of installed editor extensions / Claude plugins / skills. Default = zero-dep **inventory + recency** (no false positives). `--deep` auto-detects `guarddog`+`semgrep`: runs the behavioural scan if present (exit 10 on a finding), else runs inventory only and *loudly recommends* the on-demand install — never a false-clean. | Read-only |
-
- ```bash
- scripts/integrity-audit.sh --json | jq '.data.review[]'
- scripts/preinstall-check.sh --pip requests fastapi@0.110.0 --json | jq '.data[] | select(.inside_cooldown)'
- pwsh -NoProfile -File scripts/phone-home-monitor.ps1 -Json | jq '.data.findings[]'
- ```
-
- `tests/run.sh` is an offline-deterministic self-test (107 assertions) covering all
- seven scripts + the hooks against crafted fixtures — run it after any edit:
- `bash tests/run.sh` (exit 0 = all pass).
-
- ## Reference files
+ ## References
- | File | Contents |
+ | File | Load when |
|---|---|
- | `references/threat-model.md` | 2026 timeline (axios, Shai-Hulud, durabletask, Nx, GitHub breach), worm mechanics, IOCs, and why each legacy control failed |
- | `references/socket-cli.md` | Accurate Socket CLI + depscore MCP command surface, free-vs-paid table, Claude Code setup, source links, briefing corrections |
- | `references/tooling-landscape.md` | The wider (mostly free/OSS) defender ecosystem — GuardDog, OSV-Scanner, zizmor, Harden-Runner, lockfile-lint, `ignore-scripts` — mapped to the four layers, with a when-to-use-which matrix |
- | `references/phone-home-monitoring.md` | Outbound-monitoring tooling evaluation (Sysmon EID 3 + SwiftOnSecurity config vs WFP 5156 vs TCP-table polling vs tshark), Sysmon wiring, the monitor's rule/severity table, daemon tiers, triage playbook, honest limitations |
- | `references/postinstall-audit.md` | On-disk behavioural scan rationale — the post-install gap, the finding/severity table, the false-positive lesson (combos not singletons), incremental cache, `--deep`/`--live` modes, GuardDog/OSV/Socket tool evaluation, daily scheduling (Task Scheduler + Claude Code cron), incident response |
- | `references/repo-integrity.md` | **Repo-integrity / config-as-code defense** (PolinRider / EtherHiding) — the kill-chain-mapped playbook for trusted-repo poisoning: no shared/standing keys, hardware-backed signing keys, branch protection requiring signed commits + no force-push (why signing defeats the backdated false-flag), server-side push-log as ground truth, build/env isolation, VS Code Workspace Trust, and the `config-drift-check.py` pre-commit + CI gate |
- | `references/hardening-checklist.md` | Step-by-step OIDC audit, token rotation, dep cooldown policy, extension audit, persistence detection, client-proposal language |
+ | `references/workflows.md` | Running any workflow A-L step by step |
+ | `references/scripts-and-hooks.md` | Per-script flags and side effects, the launcher, hook wiring, what the folder needs when copied alone |
+ | `references/threat-model.md` | The 2026 timeline, worm mechanics, IOCs, why legacy controls failed, the coverage matrix |
+ | `references/socket-cli.md` | Socket CLI + depscore MCP commands; free vs paid tiers |
+ | `references/tooling-landscape.md` | Choosing among Socket, GuardDog, OSV-Scanner, zizmor, Harden-Runner, lockfile-lint; how they interact; the minimum viable set |
+ | `references/tooling-by-layer.md` | Per-tool commands and verified gotchas (GuardDog on Windows, `ignore-scripts` limits, Bumblebee) |
+ | `references/hardening-checklist.md` | A step-by-step hardening pass: OIDC audit, token rotation, cooldown policy, extension audit, client-facing language |
+ | `references/postinstall-audit.md` | Behavioural-scan findings and severities, the false-positive lesson, the cache, `--deep` / `--live`, daily scheduling |
+ | `references/phone-home-monitoring.md` | Sysmon vs WFP vs polling, wiring, the rule table, triage, limits |
+ | `references/repo-integrity.md` | Config-as-code kill chain, keys, branch protection + signed commits, audit log as ground truth, checklist |
+ | `references/repo-integrity-response.md` | Isolation, Workspace Trust, the `config-drift-check.py` gate, containment scope |
## See also
| Skill | When to combine |
|---|---|
| `security-ops` | Reactive CVE/SAST/auth audit — run alongside; they solve different problems |
| `ci-cd-ops` | Hardening GitHub Actions, OIDC trusted publishing setup |
| `github-ops` | Release flow, repo security settings |
| `auth-ops` | Credential/token handling patterns after a rotation |