Skip to content

ci(security): SHA-pin all actions, add TruffleHog, dependency-review, and a zizmor workflow audit - #97

Merged
ubercylon8 merged 1 commit into
mainfrom
ci/security-hardening
Aug 9, 2026
Merged

ci(security): SHA-pin all actions, add TruffleHog, dependency-review, and a zizmor workflow audit#97
ubercylon8 merged 1 commit into
mainfrom
ci/security-hardening

Conversation

@ubercylon8

Copy link
Copy Markdown
Owner

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-action reads this
repo's full git history; claude-code-action reads its diff. A tag
repointed 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:latest is now
digest-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-verified calls the provider's API to test whether a
candidate credential is live, so it reports real leaks rather than shapes.

Carries the --exclude-detectors=Lob workaround, and it is needed here: the Lob
detector's pattern is test_ plus exactly 35 characters, and 27 test names in
this repo
match it — test_503_maps_to_api_unavailable_finding,
test_base_render_finding_redacts_secrets, and 25 more. Without the exclusion
CI breaks on a naming coincidence.

3. dependency-review as a blocking PR gate

deps.yml previously had only pip-audit, which is continue-on-error: true
and has therefore never blocked anything. This is the gate that does.

4. zizmor as a blocking workflow audit

Run Findings
Before this PR 25: 17 unpinned-uses, 1 unpinned-images, 8 artipacked
After 0

So it lands blocking with no suppression file and no noise budget. There is
exactly one # zizmor: ignore[artipacked], in claude.yml, and it carries its
reason at the site: the @claude responder 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.yml only reads, so that
one got the real fix.

What was deliberately NOT ported

shellcheck. The sibling lints install.sh and verify-wall.sh; this repo
has zero .sh files, so that job would run and do nothing. PowerShell
likewise — no .ps1 files. Copying a job that cannot fail is worse than not
having it: it reads as coverage.

A latent bug the new job surfaced

secret-scan.yml granted pull-requests: write at workflow level, for
gitleaks' 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 gitleaks job.

No job gains contents: writegrep -rn "contents: write" .github/workflows/
returns nothing.

Offline guard

scripts/tests/test_workflow_pins.py enforces the same rules zizmor does — SHA
pins, version comments, digest-pinned images, no contents: write, and
version-pinned uvx tools — using stdlib and yaml only, because zizmor needs
the network and the suite must stay offline. This is the tightened version
of that guard: its is_pinned_spec() predicate rejects @latest, @main and
==1.* as well as a missing version, with a ten-case table stating the
contract. (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-review check

This PR edits claude-code-review.yml, and claude-code-action self-skips when
that 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)

  • Scope-first / Irreversible actions gated / Secrets never reach the
    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.
  • Generated trees rebuilt, not hand-edited — n/a.
  • No engagement data, credentials, or eval results staged.
  • uv run pytest (832 passed), uv run ruff check ., uv run mypy . pass.

Testing

uvx zizmor@1.29.0 .github/workflows/     # No findings to report
uv run pytest                            # 832 passed, 1 skipped (816 + 16 new)
uv run ruff check . && uv run mypy .     # clean, 60 source files
grep -rn "contents: write" .github/workflows/                       # no output
grep -rhoE "uses: \S+" .github/workflows/ | grep -vE "@[0-9a-f]{40}"  # no output

Mutation-checked: repointing lychee-action back to @v2 fails
test_every_action_is_pinned_to_a_commit_sha.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GWp9quiV9aBepvRyjA92Br

…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
@ubercylon8
ubercylon8 merged commit 51914b0 into main Aug 9, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant