Add supplychain plugin with npm-harden skill#2
Open
jhuiting wants to merge 17 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New
supplychainplugin 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),packageManagerpin + CVE cross-check (pnpm <10.26.2), lockfile, exotic deps, trust policy, version ranges,only-allow-pnpmguard./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
actions/*,github/*) and self-org (viagit remote) exempt to preserve signal.installbypass risk is deferred to the CI skill's frozen-install check.Still deferred
/supplychain:*commands fire as expected once the plugin is installed.postinstall-scan,npmrc-secrets,aliases-overrides,typosquat-check,maintainer-drift,source-match.Test plan
/plugin install supplychain@eyesecuritysucceeds/supplychain:npm-hardenagainst a worst-case npm repo emits 🚨 on PM-1 + PM-4;📖line present/supplychain:npm-ci-auditflags external mutable-tag action + missing--frozen-lockfileon pnpm install/supplychain:npm-ci-auditon a repo without.github/workflowsemits➖ no workflows foundand stops✅ PASSINGin either skill🤖 Generated with Claude Code