Skip to content

Add supplychain plugin with npm-harden skill#2

Open
jhuiting wants to merge 17 commits into
mainfrom
feature/security-skills
Open

Add supplychain plugin with npm-harden skill#2
jhuiting wants to merge 17 commits into
mainfrom
feature/security-skills

Conversation

@jhuiting
Copy link
Copy Markdown
Collaborator

@jhuiting jhuiting commented Apr 23, 2026

Summary

New supplychain plugin with two shipped skills that protect the npm supply chain at two surfaces:

  • /supplychain:npm-harden [path] — local project config: lifecycle scripts, release-age gate (with pnpm global fallback), packageManager pin + CVE cross-check (pnpm <10.26.2), lockfile, exotic deps, trust policy, version ranges, only-allow-pnpm guard.
  • /supplychain:npm-ci-audit [path] — how the manager is invoked in GitHub Actions. NPM-first: install flags (--frozen-lockfile / --immutable / --ignore-scripts), registry integrity, manager version consistency, OIDC + provenance publish flow, uncontrolled surface (npx, global installs), Dependabot npm scope. Plus generic Actions hardening (external-only SHA pinning, permissions, pull_request_target).

Both skills share one output grammar: 🚨/🔶/⚡/✅/➖ icons, └─ <file>: <exact value> fix lines, HARD-STOP-after-PASSING, 📖 Hardening guide: link pointing at the manager's official docs.

Design choices worth flagging

  • CI-1 SHA-pinning scoped to external actions only; first-party (actions/*, github/*) and self-org (via git remote) exempt to preserve signal.
  • PM-7 version ranges WARN when lockfile holds — the acute install bypass risk is deferred to the CI skill's frozen-install check.
  • Incident examples (Axios 1.14.1, @bitwarden/cli 2026.4.0, Shai-Hulud, chalk/debug, tj-actions CVE-2025-30066, s1ngularity, CVE-2025-69263/69264) are verbatim-copied from their Step 3 definition into output — no paraphrasing, single source.

Still deferred

  • Trigger-surface test: confirm the namespaced /supplychain:* commands fire as expected once the plugin is installed.
  • Planned roadmap skills: postinstall-scan, npmrc-secrets, aliases-overrides, typosquat-check, maintainer-drift, source-match.

Test plan

  • /plugin install supplychain@eyesecurity succeeds
  • /supplychain:npm-harden against a worst-case npm repo emits 🚨 on PM-1 + PM-4; 📖 line present
  • /supplychain:npm-ci-audit flags external mutable-tag action + missing --frozen-lockfile on pnpm install
  • /supplychain:npm-ci-audit on a repo without .github/workflows emits ➖ no workflows found and stops
  • No output after ✅ PASSING in either skill

🤖 Generated with Claude Code

joshuiting-eye-security and others added 17 commits April 22, 2026 22:57
Lifts the standalone npm-harden skill into plugins/supplychain/ as a
new marketplace plugin. Skill content is copied verbatim; ship-blocker
fixes and incident verification deferred to a follow-up pass.

Plugin scope is designed to grow to cover CI pipeline audits,
postinstall scanning, and committed-secret detection — see the
roadmap table in plugins/supplychain/README.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous scaffold commit added the plugin files but forgot the
marketplace manifest, so `/plugin install supplychain` failed with
"Plugin not found in any marketplace". Adding the entry here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ship-blocker fixes identified in the plugin-lift review:

- Add Step 1.5 deriving CVE_FLAG, PKG_MGR_CVE, YARN_SCRIPTS_OFF from
  Step 1 output. Previously PNPM_VERSION and YARN_VERSION were captured
  but never compared, so CVE and Yarn-v4.14 branches silently never
  fired. Step 3 now references the flags by name instead of re-running
  the compare in prose.

- Rewrite PM-3 packageManager CVE cross-check and PM-5 exotic-deps
  verdicts to consume the new flags. PM-5 also gets an UNKNOWN branch
  when pnpm isn't on PATH.

- Extend DANGEROUSLY grep to also scan .npmrc (pnpm reads the setting
  from both) and accept the kebab-case form. A project hiding
  dangerouslyAllowAllBuilds in .npmrc previously passed silently.

- Replace the illustrative incident example in Step 4 with an abstract
  placeholder template, and strengthen the rule to "Step 3 is sole
  source; copy first clause and e.g. line verbatim". Removes the drift
  risk between two different incident strings across the skill.

- Drop Bun entirely: remove bun.lock from Step 1 detection, remove the
  Bun PM-1 WARN, remove the Bun PM-2 unit conversion, and drop the
  bun-harden planned row from the plugin README. Bun support will
  land as a separate skill when it does.

- Align plugin.json description to the marketplace.json wording so
  descriptions don't drift across manifests.

Still deferred: incident-claim web-verification, trigger-surface test,
minor .npmrc/env-var grep scope polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Berry" is the Yarn team's internal nickname for v2+ and reads as
jargon to most users. Using the version range directly is clearer.
Plain "Yarn v2" would be wrong since the scope actually covers v2,
v3, and v4, so "Yarn v2+" is the compromise.

Touches all manifests, READMEs, badges, and the npm-harden skill
prose. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the one-line summary + top fix, the skill now emits a single
line pointing at the official supply-chain hardening guide for the
detected package manager. Gives the user a single authoritative
next-step read without having to hunt for it.

Links:
- pnpm  → https://pnpm.io/supply-chain-security
- yarn  → https://yarnpkg.com/features/security
- npm   → https://github.blog/security/supply-chain-security/our-plan-for-a-more-secure-npm-supply-chain/

(npm has no single docs.npmjs.com hardening hub; GitHub-as-npm-owner
blog post from the Shai-Hulud response is the closest to first-party.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
npm v11.10.0 (Feb 2026) added --min-release-age / .npmrc
minimumReleaseAge, using seconds as the unit (604800 = 7 days) to
mirror bun's --minimum-release-age. The skill was previously
declaring the npm value was "already in days", which would have
off-by-a-factor-of-86400 every npm release-age verdict.

Two fixes:
- PM-2 unit conversion now divides npm values by 86400.
- .npmrc grep extended to also match minimum-release-age and
  camelCase minimumReleaseAge; previously only the kebab
  min-release-age form was captured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR R4 mandates exact version pinning — no ^/~ ranges — because
automatic range resolution during unfrozen installs is how several
2025-2026 compromises (s1ngularity, Shai-Hulud, Bitwarden CLI) reached
downstream projects.

Release-age and lockfile checks carry the critical-severity weight;
ranges are belt-and-suspenders, capping out at 🔶 FAIL. A range alone
doesn't mean a compromise, but it removes the last static barrier
against a malicious version that clears the release-age gate.

Implementation:
- All three manager blocks (pnpm, npm, Yarn) now emit RANGE_TOTAL,
  PKG_NAME, and a sample of RANGES from package.json.
- New PM-7 in Step 3 classifies each range as internal (scope match)
  vs external, with verdicts: 0 ranges = PASS; all internal = WARN;
  1-5 external = FAIL with list; 6+ external = FAIL with count.
- Finding qualifies its severity against PM-4: if the lockfile is
  frozen, note the residual risk lives in manual installs/updates.
- Config tie-ins suggest save-exact (npm) and
  defaultSemverRangePrefix (Yarn) as the forward-looking fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New skill `ci-supplychain-audit` (trigger `/ci-supplychain`) audits
GitHub Actions workflows for supply-chain hardening across all package
managers covered by the plugin (npm, pnpm, Yarn v1/v2+).

Checks:
- CI-1 Action pinning — branch refs → CRITICAL, mutable tags → FAIL,
  first-party tags → WARN, 40-char SHA → PASS.
  Anchors to tj-actions/changed-files (CVE-2025-30066, Mar 2025).
- CI-2 Token permissions — `write-all` → CRITICAL, missing block → WARN,
  unused `id-token: write` → WARN.
- CI-3 pull_request_target — Pwn Request pattern (PRT + PR-head checkout)
  → CRITICAL.
- CI-4 Install-script flags per manager:
  - pnpm: `--frozen-lockfile`, `--ignore-scripts`
  - npm: `npm ci` (not `install`), `--ignore-scripts`
  - yarn v1: `--frozen-lockfile`; yarn v2+: `--immutable`
- CI-5 Dependabot config presence.
- CI-6 npm OIDC trusted publishing — OIDC-only PASS, mixed
  OIDC+NODE_AUTH_TOKEN FAIL.
- CI-7 Harden-Runner runtime egress monitoring.

Output grammar, icons, fix-line format, and HARD-STOP-after-PASSING
rule all shared with `npm-harden`. Step 3 is sole source for incident
wording; Step 4 example uses abstract placeholders.

Also cite @bitwarden/cli 2026.4.0 (Apr 2026) as a fresh incident in
both CI-4 (preinstall hook) and CI-7 (exfiltration to
audit.checkmarx.cx), plus npm-harden PM-1 npm branch. Research source:
https://research.jfrog.com/post/bitwarden-cli-hijack/

Plugin README, plugin.json description, and root marketplace.json
updated to reflect CI coverage shipping. ci-supplychain-audit moved
from "planned" to "shipped" row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With ci-supplychain-audit now checking --frozen-lockfile / --immutable
enforcement in CI, the "external ^/~ ranges resolve to latest on any
unfrozen install" scenario is covered by that skill. The acute risk
only fires when PM-4 also fails (lockfile absent or gitignored).

New PM-7 verdicts:
- All internal ranges → WARN (reproducibility)
- External + PM-4=PASS → WARN, refers user to /ci-supplychain to verify
  CI frozen-install enforcement. Acute risk contained by lockfile.
- External + PM-4=FAIL → FAIL (unchanged — lockfile gap + ranges
  compound to an immediate supply-chain risk).

Icon-system summary in Step 4 updated to match: "external ranges +
lockfile gap" replaces the blanket "external ^/~ ranges" example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the skill name and slash command in line with the sibling
npm-harden:

- Skill name: ci-supplychain-audit → npm-ci-audit
- Slash trigger: /ci-supplychain → /npm-ci-audit
- Directory: plugins/supplychain/skills/ci-supplychain-audit/ →
  plugins/supplychain/skills/npm-ci-audit/ (git mv, history preserved)

Updates the frontmatter, Trigger section, Step 4 output header,
plugin README (skills table + "What to expect" + structure diagram),
and the npm-harden PM-7 cross-reference that points users at the CI
audit to verify frozen-install enforcement.

Plugin name `supplychain` is unchanged; no marketplace.json, plugin.json,
or repo-root changes needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…capture

Closes the last three ship-blocker gaps carried over from the ADR
review:

- PM-2 now falls back to pnpm's `GLOBAL_RELEASE_AGE` when the project
  has no `minimumReleaseAge` set. A team with pnpm configured
  globally to 7d was previously flagged as 🚨 CRITICAL despite being
  protected. Verdicts split: global ≥7d → PASS with nudge to commit
  the setting; global 1-6d → WARN; both NOT_SET → CRITICAL (unchanged).

- Pnpm Step 1 now also captures `GLOBAL_IGNORE_SCRIPTS` via
  `pnpm config get ignore-scripts` and greps `.npmrc` for
  `ignore-scripts` / `ignoreScripts`. PM-1 pnpm gives a ✅ credit when
  either is set — defence-in-depth on top of the v10 scripts-off
  default.

- New PM-8 Manager enforcement (pnpm only): reads `ONLY_ALLOW` for a
  `preinstall: npx only-allow pnpm` guard in package.json. Present → ✅;
  absent → ⚡ WARN. Catches the "teammate ran `npm install` out of habit
  and silently bypassed pnpm" scenario — the one where `package-lock.json`
  appears, release-age gate doesn't fire, and npm defaults apply.

No changes to PM-3/4/5/6/7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The skill's name promises an npm-in-CI audit, but the previous layout
was 80% generic GitHub Actions hardening (action pinning, permissions,
pull_request_target, Dependabot, Harden-Runner) with npm install flags
as a single CI-4 afterthought. This commit inverts that.

New structure:

Primary group (NPM supply chain in CI):
- NPM-1 Install command + flags (was CI-4; kept content, renumbered)
- NPM-2 Registry integrity (NEW) — .npmrc writes in workflow,
  NPM_CONFIG_REGISTRY overrides, setup-node registry-url
- NPM-3 Manager setup + version consistency (NEW) — setup-node /
  setup-pnpm pinning, corepack enable, drift between workflow version
  and project `packageManager` field
- NPM-4 Publish flow (was CI-6) — OIDC trusted publishing, provenance
  auto-emit under trusted publishing, mixed-auth FAIL, classic-token
  migration. Cites Axios 1.14.1 (Mar 2026) as the stolen-token scenario
  OIDC would have prevented.
- NPM-5 Uncontrolled install surface (NEW) — `npx` resolves fresh with
  no lockfile / release-age; `npm install -g` / `pnpm add -g` /
  `yarn global add` run lifecycle scripts outside the lockfile.
- NPM-6 Dependabot npm coverage (was CI-5) — now explicitly checks
  that the `package-ecosystem: npm` entry exists, not just that any
  dependabot config is present.

Secondary group (generic Actions hardening that compounds npm risk):
- CI-1 Action pinning (unchanged content, reframed — s1ngularity cited
  as the npm-token-theft flavour of the same class)
- CI-2 Token permissions (unchanged)
- CI-3 pull_request_target (unchanged — reframed as classic npm token
  theft vector)
- CI-4 Harden-Runner (was CI-7, renumbered)

Step 1 signal set extended for the new checks: NPX, GLOBAL_INSTALL,
REGISTRY_ENV, NPMRC_WRITES, SETUP_NODE, SETUP_NODE_VERSION, SETUP_PNPM,
SETUP_PNPM_VERSION, COREPACK_ENABLE, PROVENANCE, NPM_TOKEN in NODE_AUTH.

Output renders NPM group before CI group; top-fix line biases toward
NPM findings over CI findings when both fire, matching the skill's
primary mandate.

Plugin.json, marketplace.json, and plugin README updated to reframe
the plugin as "hardening at the project level and in GitHub Actions
CI" rather than "hardening plus CI audit" — less sibling-and-add-on,
more two-surfaces-same-mandate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First-party (actions/*, github/*) and self-org (any action owned by
the repo's own GitHub org, derived from git remote) are now exempt
from the SHA-pin requirement. The check fires only on external
third-party actions, where the tj-actions/changed-files attack class
actually lives.

Rationale: demanding a SHA pin on actions/checkout@v4 is noise — the
project already trusts GitHub for the runner, the token minting, and
the OIDC issuer. Same logic for self-org actions: if an attacker
controls your org's action repo, they control your project's repo
too, so the SHA pin adds no defence. Scoping to external restores
signal-to-noise on CI-1.

Step 1 adds REPO_ORG signal via `git config remote.origin.url`. Step 2
classification splits USES_ALL into first-party / self-org / external
and derives EXT_TOTAL / EXT_SHA / EXT_BRANCH / EXT_TAG over the
external subset only. CI-1 verdicts rewritten:

- EXT_BRANCH > 0    → CRITICAL (external branch ref)
- EXT_TAG    > 0    → FAIL     (external mutable tag)
- all external SHAs → PASS
- no external at all→ PASS     (first-party / self-org only)

Previous ⚡ WARN for first-party tags is removed — that was the same
bar as external and contradicted the stated policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
step-security/harden-runner is a commercial external product and a
dependency the skill shouldn't push users toward. Removing the check
and its signal keeps the skill vendor-neutral — runtime egress
monitoring is a real concern but out of scope for a plain Actions
audit.

Removes:
- Step 1 HARDEN_RUNNER signal
- CI-4 section and verdicts
- Harden-Runner mentions in the skill description and plugin README
- Skill line-2 check count now "NPM-1..NPM-6 plus CI-1..CI-3"

The @bitwarden/cli 2026.4.0 / tj-actions egress scenarios are still
cited under NPM-1 (install flags) where they belong as motivation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two copy changes applied consistently across manifests, READMEs, and
both skills:

- Slash triggers now use the plugin-namespaced form:
  `/npm-harden` → `/supplychain:npm-harden`
  `/npm-ci-audit` → `/supplychain:npm-ci-audit`
  (Inside the frontmatter descriptions, Trigger sections, output
  headers, and cross-references.)

- Marketing copy no longer stamps "against 2025-2026 attack patterns".
  Replaced with evergreen "protects the npm supply chain" phrasing.
  Specific incidents still appear inline in each check's `e.g.` line,
  which is where they actually matter.

New one-liners:
- plugin.json / marketplace.json: "Protects the npm supply chain —
  hardens npm, pnpm, and Yarn v2+ projects locally and in GitHub
  Actions CI."
- Plugin README tagline: "Protects the npm supply chain across npm,
  pnpm, and Yarn v2+ — at the project level and in GitHub Actions CI."
- npm-harden description: drops the dated clause, keeps the
  functional summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants