ci(security): pin the TruffleHog scanner, not just its wrapper - #15
Conversation
`uses: trufflesecurity/trufflehog@v3.96.0` pins the composite action. It does not pin the scanner. The action's `version` input defaults to `latest`, so the wrapper pulled ghcr.io/trufflesecurity/trufflehog:latest on every run — the binary that decides whether the secret-scan gate passes was a mutable ref. The comment above the pin argued that a mutable ref is a remote-code-execution path into CI. That argument applies with more force to a container running with the repository checked out at fetch-depth 0, and the pin never closed it. The second-order problem is worse: the gate could stop firing without anyone noticing. Green is ambiguous here — it is what both "no verified secrets" and "scanner no longer checks" look like. This is how an upstream exit-code change in v3.90.9 reached this workflow with no pull request. Setting `version` explicitly makes the two pins equal and the step reproducible. Note that Dependabot maintains the `uses:` ref and has no knowledge of the `version` input, so the two must be moved together; the comment says so. This closes the drift, not the whole gap. A positive control — a fixture a working scanner must fail on — is not straightforward under --only-verified, which by design only fails on credentials it can confirm are live. Article VIII wants this control verified by test rather than asserted; it still is not. Closes #14
The container image is tagged 3.96.0; only the action is tagged v3.96.0. Passing the v form failed with 'manifest unknown' and exit 125. That failure is itself the evidence the pin now works: before this change the version input defaulted to latest and a bad value could not have surfaced, because the input was never reaching the image reference.
|
Correction pushed, and the round trip is worth recording because it is the closest thing to a test this change has. The first attempt used The container image is tagged That failure is the evidence the pin is real. Before this PR, Current run, all four checks green:
Note the failure mode is fail-closed: a wrong version stops the job rather than silently skipping the scan. That is the right direction for a gate, and it is the opposite of the behaviour this PR fixes. |
Closes #14.
What
Adds
version: v3.96.0to the secret-scan step in.github/workflows/ci.yml, and rewrites the comment above it to describe what is actually pinned.Why
uses: trufflesecurity/trufflehog@v3.96.0pins the composite wrapper. It does not pin the scanner. The action'sversioninput defaults tolatest, so the wrapper pulledghcr.io/trufflesecurity/trufflehog:lateston every run — meaning the binary that decides whether this gate passes was a mutable ref. The run log on #6 shows it:Downloaded newer image for ...:latest.The comment above that pin argued a mutable ref is a remote-code-execution path into CI. That argument applies with more force to a container running with the repository checked out at
fetch-depth: 0, and the pin never closed it.The second-order problem is the one that actually matters: the gate can stop firing without anyone noticing. Green is ambiguous here — it is what both "no verified secrets" and "the scanner no longer checks" look like. This is how an upstream exit-code change in v3.90.9 reached this workflow with no pull request.
Note for whoever bumps this next
Dependabot maintains the
uses:ref (.github/dependabot.yml, github-actions ecosystem). It has no knowledge of theversion:input. So a future Dependabot bump will move one pin and leave the other behind, and the step will silently go back to running a different scanner than the tag says. The comment in the workflow says this; it is worth remembering when reviewing the next bump.What this does not fix
The drift is closed. The gap is not.
Article VIII requires security controls be verified by test rather than asserted in prose, and this control still is not. A positive control — a fixture a working scanner must fail on — is not straightforward here, because
--only-verifiedby design only fails on credentials it can actively confirm are live, and a live credential cannot be committed to test it.I have not resolved that, and I would rather say so than close #14 implying the control is now proven. If you want it pursued, the plausible directions are a scoped throwaway credential minted and revoked inside the job, or a separate job that runs the scanner without
--only-verifiedagainst a known fixture and asserts a non-zero exit. Both deserve their own issue and their own thought.