Skip to content

ci(workflows): harden with zizmor, refresh pins, retire eol runtimes - #359

Open
ps-bnayah wants to merge 4 commits into
mainfrom
ci/harden-workflows
Open

ci(workflows): harden with zizmor, refresh pins, retire eol runtimes#359
ps-bnayah wants to merge 4 commits into
mainfrom
ci/harden-workflows

Conversation

@ps-bnayah

@ps-bnayah ps-bnayah commented Sep 3, 2026

Copy link
Copy Markdown

User description

Opener Type

  • Agent (automated)

Summary

A maintenance pass over all 13 workflows plus the composite action. zizmor went
from 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

Permissionspermissions: read-all replaced with contents: read in the
eight workflows that used it. Write scopes pushed down to the single job that
needs them: pages/id-token to deploy-pages' deploy job, contents: write
to the archive-traffic and wiki-sync jobs.

Template injection (33 sites) — every ${{ }} expansion moved out of run:
and github-script bodies into step env: / process.env. One subtlety: the
NVD PR body used $'...', which does not expand parameters, so a naive
${VAR} swap would have silently broken it. Rewritten with printf and
byte-compared against the original.

$GITHUB_ENV → step output — deploy-pages derived the suite release URL into
$GITHUB_ENV, which can set NODE_OPTIONS. A step output cannot.

Third-party action droppedsoftprops/action-gh-release replaced with
gh release, which is preinstalled and pre-authenticated. Re-pushing a tag
re-runs the job, so the action's upsert behaviour is preserved explicitly
(viewedit + upload --clobber, else create).

Cache kept out of the release trust pathsetup-node's
package-manager-cache input defaults to true, so the six release-job steps
now 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-pages v5.0.0→v5.0.1, trufflehog v3.97.1→v3.97.2,
setup-python v6.2.0→v7.0.0 (one stale straggler).

codeql-action repinned from codeql-bundle-v2.26.4 to v4.37.9. The bundle
tag is unparseable by Dependabot (Version.correct? rejects it), and no v4.x.y
tag 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. Verified
by replicating Dependabot's VersionCommenter logic against live tag data: all
72 pins are recognised, so bump PRs rewrite both the SHA and the comment.

Alsopersist-credentials: false on 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 to
v20.20.2, while react-router@8.3.0 declares engines.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, so
engine-strict is off and npm downgrades EBADENGINE to a warning. Every
engines.node range in package-lock.json admits Node 24.

Python 3.10 → 3.12 in the two advisory jobs. CI lints utils/ on 3.12, but
the 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's
requires-python = ">=3.10" is deliberately left alone: utils/ is documented
for users to run on their own Python across the READMEs, wiki and four
SKILL.md files. Run on 3.12, stay source-compatible with 3.10.

The one suppression

deploy-pages' workflow_run trigger. 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 to pull_request would break
the release→deploy chain, so the trigger stays with a documented ignore.

Related Issues

None.


Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • Documentation update

Testing

  • zizmor . → clean (1 documented ignore). --persona=pedantic → 0 errors/warnings.
  • All 14 workflow/action YAML files parse; bash -n passes on all 154 run: blocks.
  • All scripts/test-*.mjs pass. Two files needed assertion updates because they
    match on workflow text; both are in the hardening commit so tests are green at
    every commit (verified per-commit for bisectability).
  • Python paths exercised on 3.12: analyzer self-tests 10/10, validate_skill.py
    against a real skill, and the enrichment path end-to-end.
  • gitleaks and trufflehog run manually over main..HEAD (hooks are disabled
    in this checkout): no findings.

Not verified: npm ci && npm run build on Node 24. The authoring environment
has no route to registry.npmjs.org and no Node version manager, so the Node
bump rests on the declared engines ranges rather than a green build. First CI
run here is the real check — please don't merge before it passes.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my changes
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally

Follow-ups found but deliberately not fixed here

  1. CI's linters are stale and Dependabot cannot reach them. ci.yml hardcodes
    ruff==0.6.9 / bandit==1.7.9 via pipx run --spec, while
    .github/requirements-lint-python.txt declares ruff==0.15.13 /
    bandit==1.9.4. Nothing installs that file — its only reference in the repo is
    a paths: filter in scorecard.yml. So the pip entry in dependabot.yml
    produces PRs that change nothing.
  2. 13 of 16 Python files are never linted. CI checks utils/ only. Unlinted:
    scripts/i18n/*.py (which i18n-qa.yml executes), scripts/ci/verify_skill_release_import_closure.py,
    and skills/soul-guardian/scripts/*.py.
  3. No single source of truth for runtime versions. Node appears as a literal in
    19 places, which is why 20 rotted past EOL unnoticed. An engines field plus
    .nvmrc and node-version-file: would collapse it to one number.
  4. Nothing executes analyze_exploitability.py --test-cases in CI, despite its
    output being written into the signed advisory feed.
  5. clawsec-scanner/SKILL.md self-contradicts — line 109 says Python 3.10+,
    line 334 says 3.8+.
  6. GitHub secret-scanning push protection is still disabled on this repo
    (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.

TopicDetails
Workflow hardening Harden workflow execution by moving expressions into env, restricting permissions, disabling credential persistence and release caching, replacing the third-party release action with gh release, and documenting the sole zizmor exception.
Modified files (15)
  • .github/workflows/archive-traffic.yml
  • .github/workflows/ci.yml
  • .github/workflows/codeql.yml
  • .github/workflows/community-advisory.yml
  • .github/workflows/deploy-pages.yml
  • .github/workflows/i18n-qa.yml
  • .github/workflows/pages-verify.yml
  • .github/workflows/poll-ghsa-without-cve.yml
  • .github/workflows/poll-nvd-cves.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/skill-release.yml
  • .github/workflows/wiki-export-verify.yml
  • .github/workflows/wiki-sync.yml
  • scripts/test-nvd-ghsa-consolidation-workflow.mjs
  • scripts/test-skill-release-workflow.mjs
Latest Contributors(2)
UserCommitDate
bnaya.harel@sentinelon...ci(workflows): replace...September 03, 2026
david.a@prompt.securityfeat(security): add Tr...August 30, 2026
Runtime and pin refresh Refresh pinned action references and upgrade CI runtimes to Node 24 and Python 3.12 across build, advisory, release, deployment, and publishing paths, while adding Dependabot cooldowns and updating developer requirements.
Modified files (14)
  • .github/dependabot.yml
  • .github/workflows/archive-traffic.yml
  • .github/workflows/ci.yml
  • .github/workflows/codeql.yml
  • .github/workflows/community-advisory.yml
  • .github/workflows/deploy-pages.yml
  • .github/workflows/i18n-qa.yml
  • .github/workflows/pages-verify.yml
  • .github/workflows/poll-nvd-cves.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/skill-release.yml
  • .github/workflows/wiki-export-verify.yml
  • .github/workflows/wiki-sync.yml
  • CLAUDE.md
Latest Contributors(2)
UserCommitDate
bnaya.harel@sentinelon...ci(workflows): replace...September 03, 2026
david.a@prompt.securityfeat(security): add Tr...August 30, 2026
Review this PR on Baz
Customize your next review

ps-bnayah and others added 4 commits September 3, 2026 18:40
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>
@baz-reviewer

baz-reviewer Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Merger

Needs 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.

Review this PR on Baz

Customize your next review

Comment on lines 1946 to +1949
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 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

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.

@davida-ps davida-ps self-assigned this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants