feat(supply-chain): add PR-time release-age, install-script, and build-hook checks#2865
Conversation
…d-hook checks Adds three new supply-chain CI checks to defend against common npm and PyPI supply-chain attack patterns (event-stream, ua-parser-js, node-ipc, xz-utils class). * release-age: fails PRs that pin a dep version published <7 days ago * install-scripts: fails PRs that add an npm dep with preinstall/install/postinstall * build-hooks: warns on new top-level setup.py or build.rs (strict in CI) * scanner-trip-wire: fails PRs that edit both scanner code and dep manifests Scanners parse manifests structurally (git show base:path -> tomllib/json) rather than diffing raw lines, closing a class of injection and parser-evasion bugs surfaced during a multi-model red-team review. Pure stdlib, no new deps. Tests: 175 new (71 + 53 + 43 + 8); 344/344 in scripts/tests/ pass. Co-authored-by: Copilot <[email protected]> Signed-off-by: Jack Batzner <[email protected]>
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
Adds 21 legitimate vocabulary terms used by the new supply-chain scanners to the cspell dictionary (pathspec, basenames, packument, typosquats, Birsan, rustdecimal, esbuild, deasync, etc.) and syncs the workflow generator manifest with actions/checkout v6.0.3 (which Dependabot PR microsoft#2843 already applied to .github/workflows but not to .github/ci/actions.toml). The generator now produces output matching the committed YAML, so the 'Check generated workflows' and 'inline-script-tests' jobs pass. Co-authored-by: Copilot <[email protected]> Signed-off-by: Jack Batzner <[email protected]>
|
CI follow-up (commit
Happy to split the actions.toml fix into its own PR if a maintainer prefers; flagged here for transparency since it touches a file outside the supply-chain scanner scope. |
imran-siddique
left a comment
There was a problem hiding this comment.
Good implementation. The critical design decisions are right: structural manifest parsing instead of diff-line regex, fail-closed on transient registry failures, lockfile hint treated as informational only (never as a skip signal), and nested entries correctly unwrapped.\n\nOne minor issue in : detects when quoting changed something unexpected, logs a comment saying "something is wrong with the input" -- and then es instead of raising. The upstream / checks make this a defense-in-depth gap rather than a live bypass, but it should raise to be consistent with the fail-closed contract documented throughout.\n\nThe trip-wire design is correct and the self-referential note (this PR adds scanner code but no manifests, so trip-wire won't fire) is accurate.\n\nCI is green. Approve pending the silent-pass fix, which can land as a follow-up given the upstream guards already cover it.
…-chain-scans Signed-off-by: Jack Batzner <[email protected]> # Conflicts: # .cspell-repo-terms.txt
|
The core design is sound. One structural note: the |
imran-siddique
left a comment
There was a problem hiding this comment.
Well-engineered PR. The three checks are correctly hardened — fail-closed on registry failures, size-bounded HTTP reads, strict regexes, deadline budgets, no shell interpolation of user-controlled data, and least-privilege workflow permissions. 175 tests with explicit red-team regression labels is excellent. Approving with two optional suggestions.
Optional (worth fixing before merge)
-
PYTHONPATHnot set in workflow (.github/workflows/supply-chain-check.yml,release-ageandinstall-scriptsjob steps): The scripts doimport _supply_chain_common as commonwhich works because the runner cwd is the repo root — but this is an implicit assumption. If a checkout step ever uses apath:key, the import silently fails. Addenv: PYTHONPATH: ${{ github.workspace }}/scriptsto those steps (the test files already do this viasys.pathmanipulation, which confirms the risk is known). -
SAFE_NAME_REallows/in non-scoped names (scripts/_supply_chain_common.py,safe_url_path): The regex\A@?[A-Za-z0-9][A-Za-z0-9._/-]*\Zacceptssome/pathas a valid package name. The downstreamsafe_url_pathpercent-encodes the/to%2F, so there's no injection risk, but the false acceptance would produce a confusingHARD FAIL: 404instead ofREJECTED: unsafe name syntax. Consider narrowing to disallow/in non-scoped names.
Merge blocker (mechanical): Resolve the conflict — the PR is DIRTY against main.
Resolve conflicts in .cspell-repo-terms.txt (kept main's structure, added only the PR's new terms in sorted position) and .github/workflows/supply-chain-check.yml (kept both main's lockfile-integrity job and the PR's release-age, install-scripts, and build-hooks jobs). Signed-off-by: Imran Siddique <[email protected]> Co-Authored-By: Claude Opus 4.8 <[email protected]>
Description
Adds three new supply-chain CI checks plus a shared helpers module to defend the repo (and downstream consumers of its packages) against the most common npm and PyPI supply-chain attack patterns: the
event-stream,ua-parser-js,node-ipc, andxz-utilsclass of incidents.This extends the existing
dependency-confusion-checkjob in.github/workflows/supply-chain-check.ymlwith three siblings and a trip-wire:preinstall/install/postinstalllifecycle hooks. These run with full shell privileges the momentnpm installruns, and are the single most-abused attack surface in npm history.--strict, which CI uses) when a PR adds a top-levelsetup.pyorbuild.rs— the Python / Rust equivalents of npm install scripts.Scanners parse manifests structurally (
git show base:path→tomllib/json) rather than diffing raw lines, which closes a class of diff-injection and parser-evasion bugs surfaced during a multi-model red-team review (17 findings, all resolved before this PR). Pure stdlib — no new third-party deps, no network egress beyond the three official registries.Value to the repo and the community
copilot-instructions.mdalready documents the 7-day rule as policy; this PR is the missing enforcement layer.Security benefits in concrete terms
release-agewith timestamp + remediationrelease-ageat PR timepostinstall(event-stream class)npm installinstall-scriptswith the exact command quotedsetup.py/build.rsquietly--strictfailure in CIscanner-trip-wirehasInstallScriptLOCKFILE LIEDType of Change
Package(s) Affected
scripts/and.github/workflows/supply-chain-check.ymlChecklist
ruff check --select E,F,W --ignore E501clean on all 8 new files)pytest scripts/tests/shows 344/344 passAttribution & Prior Art
Prior art / related projects:
The 7-day release-age policy and lifecycle-script flagging are well-established defences. Commercial scanners (Socket, Snyk Advisor, Phylum, JFrog Xray) and OpenSSF Scorecard all implement variants. This PR is original code, not a wrapper around any of those — pure stdlib, scoped to PR-time only, intentionally narrow surface so the entire scanner is auditable in one sitting. The threat model is grounded in well-documented public incidents (event-stream 2018, ua-parser-js 2021, node-ipc 2022, xz-utils 2024) referenced in module docstrings.
The repo's
copilot-instructions.mdalready codifies the 7-day rule as repo policy under "Supply Chain Security (Anti-Poisoning)" — this PR is the enforcement implementation of policy that already exists.AI Assistance
Authored with Copilot CLI (Claude Opus 4.7). The implementation went through a multi-model red-team review (Claude Opus 4.6 + GPT 5.4 fanned across 6 reviewer dimensions = 12 parallel agents) which produced 17 findings (5 critical, 5 high, 5 medium, 2 process). All 17 are resolved in this commit; the architectural rewrite to structural manifest parsing is a direct response to the critical/high findings about diff-line injection and parser evasion. Tests include explicit regression coverage for each fixed finding.
IP, Patents, and Licensing
Related Issues
None. Defensive hardening; no incident or filed issue triggered this.
Reviewer notes
scanner-trip-wirejob will be visible on this PR's CI run but will not fire — the trip-wire fails only when scanner code and dep manifests change together, and this PR touches no manifests..github/change: repo policy normally asks for separate PRs for.github/changes. The workflow file change here is inseparable from the scanner code it invokes (the new jobs reference the new scripts), so they ship together. Workflow changes are line-by-line: 3 new jobs + 1 trip-wire job, all SHA-pinned actions, allpersist-credentials: false, all top-levelcontents: read.install-scripts: legitimate native-binary packages (sqlite3,node-canvas,bcrypt) usepostinstallto build C extensions and will fail this check by default. That is intentional — if any future PR needs such a dep, a maintainer should land it in a separate, reviewed PR and we can add a narrow allow-list. Prebuilt binaries (prebuild-install) are the preferred modern path.