ci(security): SHA-pin all actions, add TruffleHog, dependency-review, and a zizmor workflow audit - #97
Merged
Merged
Conversation
…eview, zizmor Ports the hardening landed in f0_pentest (#54, #56, #57), adapted to what this repo actually is. Every number below came from running the tool here. - SHA-pin all 17 action references and the semgrep container digest, each with a version comment so Dependabot can still update them. A tag is a mutable pointer owned by a third party, and gitleaks-action reads this repo's full history while claude-code-action reads its diff. - TruffleHog alongside gitleaks. Not a duplicate: gitleaks is regex plus entropy with no verification, TruffleHog --only-verified calls the provider to test whether a candidate credential is live. Carries the Lob detector exclusion, which is needed here too — 27 test names match its `test_` + 35 char pattern and would break CI on a coincidence. - dependency-review as a blocking PR gate. deps.yml previously had only pip-audit, which is continue-on-error and has therefore never blocked. - zizmor as a blocking workflow audit. It found 25 issues on first run here (17 unpinned actions, 1 unpinned image, 8 credential-persisting checkouts) and reports 0 after this change, so it lands with no suppression file. The single ignore, in claude.yml, carries its reason at the site: the @claude responder can commit, so its checkout credential is load-bearing. - Pin `uvx pip-audit` to 2.10.1. Pinning the tool does not freeze what it knows; the advisory database is fetched at run time. shellcheck was deliberately NOT ported: this repo has zero .sh files, so that job would do nothing. PowerShell likewise — no .ps1 files. Adding the TruffleHog job surfaced a latent over-permission: secret-scan.yml granted pull-requests: write at workflow level for gitleaks' PR comment, which the new job would have inherited. Now scoped to the gitleaks job. scripts/tests/test_workflow_pins.py keeps the suite offline while enforcing the same rules zizmor does; mutation-checked against an unpinned action. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GWp9quiV9aBepvRyjA92Br
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Ports the CI hardening landed in
f0_pentest(#54, #56, #57) to this repo,adapted to what it actually is rather than copied wholesale. Every figure below
came from running the tool here, not from the sibling.
1. SHA-pin every action (17 refs) and the semgrep container
A tag is a mutable pointer owned by a third party.
gitleaks-actionreads thisrepo's full git history;
claude-code-actionreads its diff. A tagrepointed upstream changes what runs here with no commit on this side. Each pin
carries a trailing version comment — Dependabot updates SHA pins from that
comment, so this costs nothing in maintenance.
semgrep/semgrep:latestis nowdigest-pinned for the same reason: that job is a blocking gate, and a silent
image swap changes what the gate enforces.
2. TruffleHog alongside gitleaks
Not a duplicate. gitleaks is regex plus entropy — high recall, no verification.
TruffleHog with
--only-verifiedcalls the provider's API to test whether acandidate credential is live, so it reports real leaks rather than shapes.
Carries the
--exclude-detectors=Lobworkaround, and it is needed here: the Lobdetector's pattern is
test_plus exactly 35 characters, and 27 test names inthis repo match it —
test_503_maps_to_api_unavailable_finding,test_base_render_finding_redacts_secrets, and 25 more. Without the exclusionCI breaks on a naming coincidence.
3.
dependency-reviewas a blocking PR gatedeps.ymlpreviously had onlypip-audit, which iscontinue-on-error: trueand has therefore never blocked anything. This is the gate that does.
4. zizmor as a blocking workflow audit
unpinned-uses, 1unpinned-images, 8artipackedSo it lands blocking with no suppression file and no noise budget. There is
exactly one
# zizmor: ignore[artipacked], inclaude.yml, and it carries itsreason at the site: the
@clauderesponder can commit in response to a mention,so its checkout credential is load-bearing — removing it would be a behaviour
change wearing hardening's clothes.
claude-code-review.ymlonly reads, so thatone got the real fix.
What was deliberately NOT ported
shellcheck. The sibling lints
install.shandverify-wall.sh; this repohas zero
.shfiles, so that job would run and do nothing. PowerShelllikewise — no
.ps1files. Copying a job that cannot fail is worse than nothaving it: it reads as coverage.
A latent bug the new job surfaced
secret-scan.ymlgrantedpull-requests: writeat workflow level, forgitleaks' PR comment. Correct when gitleaks was the only job; adding TruffleHog
meant a job that posts nothing would have inherited write access to pull
requests. Now scoped to the
gitleaksjob.No job gains
contents: write—grep -rn "contents: write" .github/workflows/returns nothing.
Offline guard
scripts/tests/test_workflow_pins.pyenforces the same rules zizmor does — SHApins, version comments, digest-pinned images, no
contents: write, andversion-pinned
uvxtools — using stdlib andyamlonly, because zizmor needsthe network and the suite must stay offline. This is the tightened version
of that guard: its
is_pinned_spec()predicate rejects@latest,@mainand==1.*as well as a missing version, with a ten-case table stating thecontract. (The sibling's guard needed three passes to get there; porting the
earlier one would have shipped a known-permissive check.)
Note on the
claude-reviewcheckThis PR edits
claude-code-review.yml, andclaude-code-actionself-skips whenthat file differs from the default branch — the job then goes green in ~20s
having reviewed nothing. That behaviour is now documented at the step. Judge
this PR's review by duration: a real one takes 2-3 minutes.
Checklist (mirrors the Critical Rules in CLAUDE.md)
model / Returns Finding(s) / Safety logic stays in
core//Small-model-safe — n/a: CI configuration and one test, no server or
core/code touched. The TruffleHog job strengthens secret detection.uv run pytest(832 passed),uv run ruff check .,uv run mypy .pass.Testing
Mutation-checked: repointing
lychee-actionback to@v2failstest_every_action_is_pinned_to_a_commit_sha.🤖 Generated with Claude Code
https://claude.ai/code/session_01GWp9quiV9aBepvRyjA92Br