ttl-editor now carries scripts/check-supply-chain.mjs, a preflight that closes the two gaps SECURITY-PIPELINE.md has recorded since the pinning work landed. This repository should adopt it.
Upstream: sgort/ttl-editor#75 (merged) · sgort/ttl-editor#86 (a fix this repository found) · sgort/ttl-editor#76 (wiring it into the audit job) · sgort/ronl-business-api has the same issue open.
What it checks that zizmor cannot
zizmor validates pin format — that uses: names a 40-character SHA. It cannot say the SHA is the right one, so a wrong or hostile digest carrying a plausible # v4.4.0 comment passes zizmor, Prettier and review alike.
- Pin truth — each digest is resolved against the GitHub API and compared with the version its comment claims.
- Register agreement — the
Pinned table is compared with the workflows: digests, versions, (×N) multiplicities, and the totals headline where one exists.
This repository found two bugs in it
Worth leading with, because it is the reason adoption here is straightforward now and would not have been a week ago. Run against this repository's acc, the script reported two findings — and both were the script's fault, not yours:
actions/checkout is pinned at two digests here — 11d5960a… (v4.4.0) in three workflows and a37ce912… (v3.7.0) in four, mid-upgrade — and SECURITY-PIPELINE.md correctly records both rows. The script keyed register rows by action alone, so the second overwrote the first and every workflow on the other digest read as disagreeing.
Azure/static-web-apps-deploy is recorded as v1 (branch head), which is more informative than the bare v1 the workflow comment carries — the whole point of that pin being that v1 names both a 2021 tag and a 2024 branch head. Exact-string comparison called it a mismatch.
Neither could surface in ttl-editor, which pins every action once and annotates nothing — the same shape as the defect the script exists to catch, a check that only ever ran where its assumptions hold. Fixed in ttl-editor#86; rows are matched by digest and versions compared on the leading token.
It runs clean here now
check-supply-chain: 23 pinned reference(s) across 5 action(s) in .github/workflows/
note: Azure/static-web-apps-deploy@v1 verified against the branch, per its recorded exception
OK — digests, version comments and the register all agree.
Adoption lands green.
And this repository is why the register half matters
SECURITY-PIPELINE.md in ttl-editor cites what happened here: five keys used as JSON comments in renovate.json were rejected as invalid configuration and Renovate stopped opening pull requests as a precaution — and nothing in CI noticed. The repository looked green while half its supply-chain policy was inert.
That specific failure is caught by the renovate-config-validator --strict step, not by this script. They are the two halves of the same argument: pinning without working automated updates decays into an unpatched tree, and a register that has drifted from the workflows describes a policy nobody is actually running. Worth adopting both if the validator step is not here yet.
Adoption
Copy scripts/check-supply-chain.mjs verbatim — take it from acc after #86 merges, or the two false positives above come with it — and add:
"check-supply-chain": "node scripts/check-supply-chain.mjs"
No configuration is repo-specific. The script reads SECURITY-PIPELINE.md and .github/workflows/ relative to the working directory, so run it from the repository root.
Then decide whether it gates
Same decision as ttl-editor#76, and a real trade-off: adding it to the audit job puts a network dependency inside a blocking gate, so a required check could fail on an API outage or rate limit. --offline checks format and register agreement without resolving pins.
npm run check-supply-chain only — zero risk, catches drift where someone is already looking.
- A step in the existing
audit job with continue-on-error: true, flipped to blocking after a week or two — mirroring how the gate itself was introduced. Needs GITHUB_TOKEN: ${{ github.token }}; contents: read suffices for public refs.
- Blocking immediately — defensible since it is green today, but with no evidence yet about flakiness.
A step in the existing job rather than a new job: the ruleset requires the check named audit, so a step is covered automatically, while a new job would need adding to the ruleset first and would silently not block until it was.
One thing to decide alongside
The two checkout digests are a live upgrade, not a steady state — four workflows are still on v3.7.0. The script is happy either way, since the register records both. But once they converge, the second register row should go with them, and the script will say so as a note (SECURITY-PIPELINE.md lists actions/checkout at a37ce912… (v3.7.0), which no workflow currently uses) rather than a finding.
Acceptance
ttl-editornow carriesscripts/check-supply-chain.mjs, a preflight that closes the two gapsSECURITY-PIPELINE.mdhas recorded since the pinning work landed. This repository should adopt it.Upstream: sgort/ttl-editor#75 (merged) · sgort/ttl-editor#86 (a fix this repository found) · sgort/ttl-editor#76 (wiring it into the audit job) · sgort/ronl-business-api has the same issue open.
What it checks that zizmor cannot
zizmor validates pin format — that
uses:names a 40-character SHA. It cannot say the SHA is the right one, so a wrong or hostile digest carrying a plausible# v4.4.0comment passes zizmor, Prettier and review alike.Pinnedtable is compared with the workflows: digests, versions,(×N)multiplicities, and the totals headline where one exists.This repository found two bugs in it
Worth leading with, because it is the reason adoption here is straightforward now and would not have been a week ago. Run against this repository's
acc, the script reported two findings — and both were the script's fault, not yours:actions/checkoutis pinned at two digests here —11d5960a…(v4.4.0) in three workflows anda37ce912…(v3.7.0) in four, mid-upgrade — andSECURITY-PIPELINE.mdcorrectly records both rows. The script keyed register rows by action alone, so the second overwrote the first and every workflow on the other digest read as disagreeing.Azure/static-web-apps-deployis recorded asv1 (branch head), which is more informative than the barev1the workflow comment carries — the whole point of that pin being thatv1names both a 2021 tag and a 2024 branch head. Exact-string comparison called it a mismatch.Neither could surface in
ttl-editor, which pins every action once and annotates nothing — the same shape as the defect the script exists to catch, a check that only ever ran where its assumptions hold. Fixed in ttl-editor#86; rows are matched by digest and versions compared on the leading token.It runs clean here now
Adoption lands green.
And this repository is why the register half matters
SECURITY-PIPELINE.mdinttl-editorcites what happened here: five keys used as JSON comments inrenovate.jsonwere rejected as invalid configuration and Renovate stopped opening pull requests as a precaution — and nothing in CI noticed. The repository looked green while half its supply-chain policy was inert.That specific failure is caught by the
renovate-config-validator --strictstep, not by this script. They are the two halves of the same argument: pinning without working automated updates decays into an unpatched tree, and a register that has drifted from the workflows describes a policy nobody is actually running. Worth adopting both if the validator step is not here yet.Adoption
Copy
scripts/check-supply-chain.mjsverbatim — take it fromaccafter #86 merges, or the two false positives above come with it — and add:No configuration is repo-specific. The script reads
SECURITY-PIPELINE.mdand.github/workflows/relative to the working directory, so run it from the repository root.Then decide whether it gates
Same decision as ttl-editor#76, and a real trade-off: adding it to the
auditjob puts a network dependency inside a blocking gate, so a required check could fail on an API outage or rate limit.--offlinechecks format and register agreement without resolving pins.npm run check-supply-chainonly — zero risk, catches drift where someone is already looking.auditjob withcontinue-on-error: true, flipped to blocking after a week or two — mirroring how the gate itself was introduced. NeedsGITHUB_TOKEN: ${{ github.token }};contents: readsuffices for public refs.A step in the existing job rather than a new job: the ruleset requires the check named
audit, so a step is covered automatically, while a new job would need adding to the ruleset first and would silently not block until it was.One thing to decide alongside
The two
checkoutdigests are a live upgrade, not a steady state — four workflows are still on v3.7.0. The script is happy either way, since the register records both. But once they converge, the second register row should go with them, and the script will say so as a note (SECURITY-PIPELINE.md lists actions/checkout at a37ce912… (v3.7.0), which no workflow currently uses) rather than a finding.Acceptance
npm run check-supply-chainexits 0 onacc.SECURITY-PIPELINE.mdfails.