From 2d3ac905f1294ac4ea933697d88beab186d2dcbe Mon Sep 17 00:00:00 2001 From: auroracapital Date: Wed, 12 Aug 2026 10:11:33 +0200 Subject: [PATCH 1/2] ci(security): add reusable pnpm-overrides sanity workflow pnpm 10 no longer reads the `pnpm` field from package.json. It warns "The pnpm field in package.json is no longer read by pnpm" and then IGNORES pnpm.overrides / pnpm.patchedDependencies. The failure mode is silent and security-relevant: a security override declared in package.json under pnpm 10 looks committed and reviewed, resolves nothing, and the advisory stays open. That is exactly how critical tar GHSA-23hp-3jrh-7fpw survived repeated dependency bumps in hypest-intelligence-hub and hypest-dashboard (found 2026-08-12, both had tar 7.5.1 pinned "to >=7.5.19"). This reusable workflow: 1. fails when packageManager pins pnpm >= 10 while package.json still declares overrides / patchedDependencies / peerDependencyRules / packageExtensions, naming the exact dead keys and where to move them; 2. parses the overrides block in pnpm-workspace.yaml and asserts each one is actually reflected in pnpm-lock.yaml, so a declared-but-unapplied override (stale lockfile) also fails. Verified both directions before commit: - positive: real hypest-intelligence-hub lockfile -> tar 7.5.22 vs override >=7.5.19 and tailwindcss>nanoid 3.3.11 vs 3.3.7 both read correctly - negative control: synthetic workspace pinning >=7.5.19 against a lockfile holding tar@7.5.1 -> checker correctly fails (it is not a no-op that always passes) --- workflow-templates/pnpm-overrides-sanity.yml | 146 +++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 workflow-templates/pnpm-overrides-sanity.yml diff --git a/workflow-templates/pnpm-overrides-sanity.yml b/workflow-templates/pnpm-overrides-sanity.yml new file mode 100644 index 0000000..bd48c36 --- /dev/null +++ b/workflow-templates/pnpm-overrides-sanity.yml @@ -0,0 +1,146 @@ +name: pnpm overrides sanity + +# Why this exists +# +# pnpm 10 STOPPED reading the `pnpm` field from package.json. It prints +# "The pnpm field in package.json is no longer read by pnpm" +# and then IGNORES `pnpm.overrides` / `pnpm.patchedDependencies` entirely. +# +# The failure is silent and security-relevant: a security override declared in +# package.json under pnpm 10 looks committed and reviewed, resolves nothing, and +# the advisory stays open. This is exactly how critical `tar` GHSA-23hp-3jrh-7fpw +# survived multiple dependency bumps in two repos (2026-08-12). +# +# This reusable workflow fails when a repo pins pnpm >= 10 and still declares +# overrides/patchedDependencies in package.json, and it verifies that every +# override actually took effect in the lockfile. + +on: + workflow_call: + inputs: + working-directory: + description: Directory containing package.json + required: false + default: "." + type: string + +jobs: + pnpm-overrides-sanity: + name: pnpm overrides are actually read + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + + - name: Check overrides live where pnpm 10 reads them + working-directory: ${{ inputs.working-directory }} + run: | + set -euo pipefail + + if [ ! -f package.json ]; then + echo "no package.json in ${{ inputs.working-directory }} — nothing to check" + exit 0 + fi + + pm=$(node -p "require('./package.json').packageManager || ''" 2>/dev/null || echo "") + echo "packageManager: ${pm:-}" + + # Only pnpm repos are in scope + case "$pm" in + pnpm@*) ;; + *) echo "not a pnpm-pinned repo — skipping"; exit 0 ;; + esac + + major=$(printf '%s' "$pm" | sed -E 's/^pnpm@([0-9]+).*/\1/') + echo "pnpm major: $major" + + has_field=$(node -p "const p=require('./package.json'); (p.pnpm && Object.keys(p.pnpm).length) ? 'yes' : 'no'") + # Keys pnpm 10 still honours in package.json vs the ones it drops + dropped=$(node -p " + const p=require('./package.json').pnpm || {}; + const dead=['overrides','patchedDependencies','peerDependencyRules','packageExtensions']; + Object.keys(p).filter(k=>dead.includes(k)).join(',') || 'none' + ") + + if [ "$major" -ge 10 ] && [ "$dropped" != "none" ]; then + echo "::error title=pnpm 10 ignores these::package.json has pnpm.{$dropped} but pnpm $major does NOT read them." + echo "" + echo "Move them to pnpm-workspace.yaml, e.g.:" + echo "" + echo " overrides:" + echo " some-pkg: '>=1.2.3'" + echo "" + echo "Leaving them in package.json makes security overrides SILENTLY INERT:" + echo "the advisory stays open while the diff looks correct." + exit 1 + fi + + echo "OK: no silently-ignored pnpm config (field present: $has_field, dropped keys: $dropped)" + + - name: Verify declared overrides took effect in the lockfile + working-directory: ${{ inputs.working-directory }} + run: | + set -euo pipefail + + if [ ! -f pnpm-workspace.yaml ] || [ ! -f pnpm-lock.yaml ]; then + echo "no pnpm-workspace.yaml + pnpm-lock.yaml pair — skipping" + exit 0 + fi + + python3 - <<'PY' + import re, sys, pathlib + + ws = pathlib.Path("pnpm-workspace.yaml").read_text() + lock = pathlib.Path("pnpm-lock.yaml").read_text() + + # crude but dependency-free: read the overrides: block + m = re.search(r"(?m)^overrides:\s*$((?:\n[ \t]+.*|\n\s*)*)", ws) + if not m: + print("no overrides block in pnpm-workspace.yaml — nothing to verify") + sys.exit(0) + + entries = [] + for line in m.group(1).splitlines(): + s = line.strip() + if not s or s.startswith("#"): + continue + if ":" not in s: + continue + name, spec = s.split(":", 1) + entries.append((name.strip().strip("'\""), spec.strip().strip("'\""))) + + if not entries: + print("overrides block empty — nothing to verify") + sys.exit(0) + + failures = [] + for name, spec in entries: + # scoped selectors like tailwindcss>nanoid: verify the target package + target = name.split(">")[-1] + m2 = re.search(r"(?m)^\s{2}%s@([^\s:(]+):" % re.escape(target), lock) + if not m2: + print(f" ? {name} -> {spec}: {target} not present in lockfile (may be optional)") + continue + resolved = m2.group(1) + print(f" {target} resolved to {resolved} (override {spec})") + + mv = re.match(r">=\s*([0-9][0-9.]*)", spec) + if mv: + want = [int(x) for x in mv.group(1).split(".")] + got = [] + for part in re.split(r"[.\-+]", resolved): + if part.isdigit(): + got.append(int(part)) + else: + break + if got and got < want: + failures.append(f"{target}: lockfile has {resolved}, override requires {spec}") + + if failures: + print("::error title=override declared but not applied::" + "; ".join(failures)) + print("Run: pnpm install --lockfile-only (and commit pnpm-lock.yaml)") + sys.exit(1) + + print("OK: all declared overrides are reflected in pnpm-lock.yaml") + PY From b536eadaa54790406fe5e137a84655e40ee61104 Mon Sep 17 00:00:00 2001 From: auroracapital Date: Wed, 12 Aug 2026 16:02:37 +0200 Subject: [PATCH 2/2] ci(security): compare exact/caret/tilde override specs, handle quoted scoped lockfile keys, fail when nothing verifiable --- workflow-templates/pnpm-overrides-sanity.yml | 67 ++++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/workflow-templates/pnpm-overrides-sanity.yml b/workflow-templates/pnpm-overrides-sanity.yml index bd48c36..a6b7baa 100644 --- a/workflow-templates/pnpm-overrides-sanity.yml +++ b/workflow-templates/pnpm-overrides-sanity.yml @@ -114,28 +114,69 @@ jobs: print("overrides block empty — nothing to verify") sys.exit(0) + def nums(v): + out = [] + for part in re.split(r"[.\-+]", v): + if part.isdigit(): + out.append(int(part)) + else: + break + while len(out) < 3: + out.append(0) + return out[:3] + failures = [] + verified = 0 for name, spec in entries: - # scoped selectors like tailwindcss>nanoid: verify the target package - target = name.split(">")[-1] - m2 = re.search(r"(?m)^\s{2}%s@([^\s:(]+):" % re.escape(target), lock) + # selectors like tailwindcss>nanoid or tailwindcss>@scope/pkg + target = name.split(">")[-1].strip() + # pnpm-lock v9 quotes scoped keys: '@babel/core@7.24.0': + m2 = re.search( + r"(?m)^\s{2}'?%s@([^\s:'(]+)'?:" % re.escape(target), lock + ) if not m2: - print(f" ? {name} -> {spec}: {target} not present in lockfile (may be optional)") + print(f" SKIP {name} -> {spec}: {target} not present in pnpm-lock.yaml, override NOT verified") continue resolved = m2.group(1) print(f" {target} resolved to {resolved} (override {spec})") - mv = re.match(r">=\s*([0-9][0-9.]*)", spec) + s = spec.strip() + mv = re.match(r"^>=\s*([0-9][0-9.]*)$", s) + exact = re.match(r"^=?\s*([0-9]+(?:\.[0-9]+){0,2}(?:[-+][0-9A-Za-z.\-]+)?)$", s) + caret = re.match(r"^\^\s*([0-9]+(?:\.[0-9]+){0,2})$", s) + tilde = re.match(r"^~\s*([0-9]+(?:\.[0-9]+){0,2})$", s) if mv: - want = [int(x) for x in mv.group(1).split(".")] - got = [] - for part in re.split(r"[.\-+]", resolved): - if part.isdigit(): - got.append(int(part)) - else: - break - if got and got < want: + if nums(resolved) < nums(mv.group(1)): + failures.append(f"{target}: lockfile has {resolved}, override requires {spec}") + else: + verified += 1 + elif exact: + want = exact.group(1) + if resolved != want and nums(resolved) != nums(want): + failures.append(f"{target}: lockfile has {resolved}, override pins exactly {want}") + else: + verified += 1 + elif caret or tilde: + base = (caret or tilde).group(1) + lo = nums(base) + got = nums(resolved) + if caret: + hi = [lo[0] + 1, 0, 0] if lo[0] > 0 else ([0, lo[1] + 1, 0] if lo[1] > 0 else [0, 0, lo[2] + 1]) + else: + hi = [lo[0], lo[1] + 1, 0] + if not (lo <= got < hi): failures.append(f"{target}: lockfile has {resolved}, override requires {spec}") + else: + verified += 1 + else: + failures.append( + f"{target}: override spec {spec!r} is not a comparable version range " + f"(alias/catalog/file/tag) — this check cannot confirm it applied" + ) + + if entries and verified == 0 and not failures: + print("::error title=override guard verified nothing::none of the declared overrides could be checked against pnpm-lock.yaml") + sys.exit(1) if failures: print("::error title=override declared but not applied::" + "; ".join(failures))