ci(workflows): harden with zizmor, refresh pins, retire eol runtimes - #359
ci(workflows): harden with zizmor, refresh pins, retire eol runtimes#359ps-bnayah wants to merge 4 commits into
Conversation
A freshly published release is the highest-risk window for a compromised or yanked package. A 7-day cooldown lets upstream yank or patch before the bump ever reaches a PR here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zizmor reported 31 findings across the 13 workflows. All are addressed
on the merits rather than suppressed, with one documented exception.
Permissions: replace `permissions: read-all` with `contents: read` in
the eight workflows that used it, and push write scopes down to the one
job that needs them -- pages/id-token to deploy-pages' `deploy` job,
contents:write to the archive-traffic and wiki-sync jobs.
Template injection: move every `${{ }}` expansion out of `run:` and
`github-script` bodies into step `env:` / `process.env`. The NVD PR
body used `$'...'`, which does not expand parameters, so that one
became `printf` -- a naive substitution would have silently broken it.
GITHUB_ENV: deploy-pages derived the suite release URL into $GITHUB_ENV,
which can set NODE_OPTIONS. It now uses a step output, which cannot.
Releases: drop softprops/action-gh-release for `gh release`, which is
preinstalled and already authenticated. Re-pushing a tag re-runs the
job, so the upsert behaviour is kept explicitly.
Caching: setup-node's `package-manager-cache` defaults to true, so the
six release-job steps set it to false -- a signed release must not
restore a cache into its trust path.
Also: `persist-credentials: false` on checkouts that do not push,
`$/...` self-repository syntax for in-repo action refs, and
`codeql-action` repinned from the `codeql-bundle-*` tag to `v4.37.9`,
because Dependabot cannot parse the bundle tag and so could not compute
updates for it at all.
The one suppression is deploy-pages' `workflow_run` trigger, which is
safe here: it checks out no PR head, consumes no artifact from the
triggering run, and every job guards on the event not being a PR.
Test assertions that match on workflow text are updated to follow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci.yml lints utils/ on 3.12 and i18n-qa runs on 3.12, but the two jobs that actually execute that code against live advisory data and commit a signed feed ran it on 3.10. The version writing the signed feed was the one version no gate exercised. 3.10 also reaches end-of-life this October. utils/analyze_exploitability.py is stdlib-only and needs 3.9+, so this is a runtime change only. pyproject's `requires-python = ">=3.10"` and ruff's `target-version = "py310"` stay as they are: utils/ is documented for users to run on their own Python across the READMEs, the wiki and four SKILL.md files, and clawsec-scanner states 3.10+ as its prerequisite. Run on 3.12, stay source-compatible with 3.10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Node 20 reached end-of-life on 2026-04-30. Node 24 is the active LTS, supported until 2028-04-30. This was not only a stale version. `node-version: '20'` resolves to the highest 20.x ever released, v20.20.2, and react-router@8.3.0 declares `engines.node: ">=22.22.0"` -- unsatisfiable by any Node 20. The deployed site was being built on a runtime its own router declares unsupported. It never failed because there is no .npmrc, so engine-strict is off and npm downgrades EBADENGINE to a warning. Every `engines.node` range in package-lock.json admits Node 24. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MergerNeeds Review The NVD polling workflow disables checkout credential persistence but still performs authenticated git fetch/push operations, leaving changed-feed runs unable to update advisory branches. This diff-confirmed regression requires reviewer attention; a separate low-severity test-gap thread is also unresolved. |
| bash scripts/ci/guard_clawhub_slug_owner.sh \ | ||
| "${{ needs.release-tag.outputs.clawhub_slug }}" | ||
| "${NEEDS_RELEASE_TAG_OUTPUTS_CLAWHUB_SLUG}" | ||
| env: | ||
| NEEDS_RELEASE_TAG_OUTPUTS_CLAWHUB_SLUG: ${{ needs.release-tag.outputs.clawhub_slug }} |
There was a problem hiding this comment.
Slug hardening test can pass falsely
Both workflow-text assertions still match the old GitHub expression in env:, so they pass without verifying ${NEEDS_RELEASE_TAG_OUTPUTS_CLAWHUB_SLUG} reaches the shell argument — should we require the env variable names and mappings in both assertions?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`.github/workflows/skill-release.yml` around lines 1946-1949, update the workflow-text
assertions for the automatic ClawHub guard so they verify the shell argument uses
`NEEDS_RELEASE_TAG_OUTPUTS_CLAWHUB_SLUG`, not merely the old GitHub expression in the
`env:` mapping. Apply the same correction to the analogous manual-republish assertion,
requiring both the expected environment variable name and its `${{ ... }}` mapping so
template-injection hardening is actually regression-tested.
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| fetch-depth: 1 | ||
| persist-credentials: false |
There was a problem hiding this comment.
NVD PR upsert cannot push changes
With checkout credential persistence disabled, this job runs raw git fetch/git push against origin without authentication, so changed-feed runs fail at git fetch origin main and never create or update the automation branch or advisory PR — should we configure an authenticated Git remote or explicit Git credentials first?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`.github/workflows/poll-nvd-cves.yml` around line 44, the checkout step disables
credential persistence, leaving the later `upsert-pr` git fetch/push logic
unauthenticated. Update the workflow to configure a securely authenticated `origin`
remote or Git credential helper using the job’s GitHub token before `git fetch origin
main` and `git push`, while avoiding token leakage in logs.
User description
Opener Type
Summary
A maintenance pass over all 13 workflows plus the composite action.
zizmorwentfrom 31 findings (10 high, 2 medium) to zero, every action pin was
verified against its real tag, and two end-of-life runtimes were retired.
Findings are fixed on the merits rather than suppressed. There is exactly one
zizmor: ignore, and it carries its justification inline.Changes Made
Permissions —
permissions: read-allreplaced withcontents: readin theeight workflows that used it. Write scopes pushed down to the single job that
needs them:
pages/id-tokento deploy-pages'deployjob,contents: writeto the archive-traffic and wiki-sync jobs.
Template injection (33 sites) — every
${{ }}expansion moved out ofrun:and
github-scriptbodies into stepenv:/process.env. One subtlety: theNVD PR body used
$'...', which does not expand parameters, so a naive${VAR}swap would have silently broken it. Rewritten withprintfandbyte-compared against the original.
$GITHUB_ENV→ step output — deploy-pages derived the suite release URL into$GITHUB_ENV, which can setNODE_OPTIONS. A step output cannot.Third-party action dropped —
softprops/action-gh-releasereplaced withgh release, which is preinstalled and pre-authenticated. Re-pushing a tagre-runs the job, so the action's upsert behaviour is preserved explicitly
(
view→edit+upload --clobber, elsecreate).Cache kept out of the release trust path —
setup-node'spackage-manager-cacheinput defaults totrue, so the six release-job stepsnow set it to
false. Not a false positive.Pins — all 72 refs SHA-pinned and each SHA verified to resolve to its claimed
tag. Updated:
deploy-pagesv5.0.0→v5.0.1,trufflehogv3.97.1→v3.97.2,setup-pythonv6.2.0→v7.0.0 (one stale straggler).codeql-actionrepinned fromcodeql-bundle-v2.26.4to v4.37.9. The bundletag is unparseable by Dependabot (
Version.correct?rejects it), and nov4.x.ytag points at that SHA, so Dependabot could not compute updates for it at all.
v4.37.9 is 5 commits ahead of the bundle commit.
Pin comments normalised to Dependabot's own
@<sha> # <tag>form. Verifiedby replicating Dependabot's
VersionCommenterlogic against live tag data: all72 pins are recognised, so bump PRs rewrite both the SHA and the comment.
Also —
persist-credentials: falseon checkouts that do not push;$/...self-repository syntax for in-repo action refs (15 sites).Retired end-of-life runtimes
Node 20 → 24. Node 20 hit EOL on 2026-04-30; 24 is active LTS through
2028-04-30. This was worse than staleness:
node-version: '20'resolves tov20.20.2, while
react-router@8.3.0declaresengines.node: ">=22.22.0"—unsatisfiable by any Node 20. The deployed site was built on a runtime its own
router declares unsupported. It never failed because there is no
.npmrc, soengine-strictis off and npm downgradesEBADENGINEto a warning. Everyengines.noderange inpackage-lock.jsonadmits Node 24.Python 3.10 → 3.12 in the two advisory jobs. CI lints
utils/on 3.12, butthe jobs that execute that code against live NVD data and commit a signed feed
ran it on 3.10 — the one version no gate exercised.
pyproject'srequires-python = ">=3.10"is deliberately left alone:utils/is documentedfor users to run on their own Python across the READMEs, wiki and four
SKILL.mdfiles. Run on 3.12, stay source-compatible with 3.10.The one suppression
deploy-pages'
workflow_runtrigger. Safe here: it checks out no PR head,consumes no artifact from the triggering run, and every job guards on
workflow_run.event != 'pull_request'. Switching topull_requestwould breakthe release→deploy chain, so the trigger stays with a documented ignore.
Related Issues
None.
Type of Change
Testing
zizmor .→ clean (1 documented ignore).--persona=pedantic→ 0 errors/warnings.bash -npasses on all 154run:blocks.scripts/test-*.mjspass. Two files needed assertion updates because theymatch on workflow text; both are in the hardening commit so tests are green at
every commit (verified per-commit for bisectability).
validate_skill.pyagainst a real skill, and the enrichment path end-to-end.
gitleaksandtrufflehogrun manually overmain..HEAD(hooks are disabledin this checkout): no findings.
Not verified:
npm ci && npm run buildon Node 24. The authoring environmenthas no route to
registry.npmjs.organd no Node version manager, so the Nodebump rests on the declared
enginesranges rather than a green build. First CIrun here is the real check — please don't merge before it passes.
Checklist
Follow-ups found but deliberately not fixed here
ci.ymlhardcodesruff==0.6.9/bandit==1.7.9viapipx run --spec, while.github/requirements-lint-python.txtdeclaresruff==0.15.13/bandit==1.9.4. Nothing installs that file — its only reference in the repo isa
paths:filter inscorecard.yml. So thepipentry independabot.ymlproduces PRs that change nothing.
utils/only. Unlinted:scripts/i18n/*.py(whichi18n-qa.ymlexecutes),scripts/ci/verify_skill_release_import_closure.py,and
skills/soul-guardian/scripts/*.py.19 places, which is why 20 rotted past EOL unnoticed. An
enginesfield plus.nvmrcandnode-version-file:would collapse it to one number.analyze_exploitability.py --test-casesin CI, despite itsoutput being written into the signed advisory feed.
clawsec-scanner/SKILL.mdself-contradicts — line 109 says Python 3.10+,line 334 says 3.8+.
(noted in CLAUDE.md as the control worth more than all the local gates).
🤖 Generated with Claude Code
Generated description
Below is a concise technical summary of the changes proposed in this PR:
Harden GitHub Actions workflows and the composite signing action by eliminating unsafe interpolations, narrowing permissions, pinning verified action SHAs, and removing unnecessary credential persistence. Retire Node 20 and Python 3.10 in CI and update release, advisory, deployment, and publishing flows to use safer runtime and artifact handling.
env, restricting permissions, disabling credential persistence and release caching, replacing the third-party release action withgh release, and documenting the solezizmorexception.Modified files (15)
Latest Contributors(2)
Modified files (14)
Latest Contributors(2)
Customize your next review