In late 2025, a maintainer's npm account got compromised and a couple of extremely popular packages, chalk and debug, got a malicious version published straight to the registry. It sat live for roughly two hours before it was pulled. Two hours is nothing and everything: nothing in absolute terms, everything if your CI happened to run an install in that window.
That incident is the reason most supply-chain-security advice you'll read boils down to one instruction: pin everything to an immutable hash. Not a version tag, not a branch, a commit SHA. It's good advice. I follow it for every third-party action in CVE Lite CLI's workflows.
I don't follow it for my own action, anywhere it's referenced, including in the README examples other people copy. Getting to a version of that answer I'd actually defend took a real second look.
The reviewer who caught it
A contributor opened a PR pinning actions/checkout to a SHA in our README's example workflow. Good catch, obviously correct. Same PR also pinned our own action, OWASP/cve-lite-cli@v1, to a SHA.
I asked him to revert that second part, and my first-draft reasoning for it was too thin: "v1 is a floating tag I move forward on every release, so pinning it would mean re-pinning it everywhere on every release." True, but that's a maintenance argument, not a security one, and it skates past the actual question a security-literate reader will ask: what happens if my account, a release workflow's token, or the OWASP org itself gets compromised badly enough to force-move v1 to a malicious commit? That's a real risk category, the same one SHA-pinning defends against for actions/checkout, and I don't get to wave it away just because it's inconvenient to my own docs.
So here's the actual case for floating v1, stated so it survives that question instead of dodging it. OWASP/cve-lite-cli@v1 isn't a random third-party action with an unknown publisher and no visibility into how it's maintained - it's a verified OWASP Foundation project with branch protection, required review, and its own independent release-integrity stack (the three chains below) protecting the actual artifact a compromised tag would need to point at. That's a materially different trust position than an arbitrary action you've never audited, and it's exactly the position the rest of the ecosystem already treats this way: almost nobody pins actions/checkout itself to a SHA either, because GitHub's own actions get the same "verified, governed publisher" treatment. For a security tool specifically, floating also has a real upside that a pinned SHA gives up: everyone referencing @v1 gets fixes and detection improvements the moment they ship, not whenever someone remembers to bump a hash.
None of that makes the residual risk zero. It's a real tradeoff, weighed against a publisher I'm choosing to trust more than an arbitrary third party - which is also exactly why I don't extend the same leniency to actions/checkout in our own workflows. I don't get to unilaterally decide GitHub's own supply chain is trustworthy enough to skip pinning; I can decide that about the project I run, with the controls I can actually see.
What I do instead: three chains that don't share a failure mode
Where I care a lot about redundancy, no exceptions, is verifying the release artifact itself - because that's exactly where the chalk/debug attack landed. If you install CVE Lite CLI from npm today, there are three independent ways to check it hasn't been tampered with, and none of them depend on the others holding up.
A GPG-signed git tag, on every release:
git tag -s v1.29.0 -m "v1.29.0"
A Sigstore build attestation on the release tarball, minted in CI:
permissions:
id-token: write
attestations: write
jobs:
attest-and-upload:
runs-on: ubuntu-latest
steps:
- run: npm ci && npm run build && npm pack
- uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
with:
subject-path: 'cve-lite-cli-*.tgz'
No long-lived private key sits in a secret anywhere for this one. The certificate is minted via OIDC, scoped to that one workflow run, and expires. Anyone can check it themselves:
gh attestation verify cve-lite-cli-1.29.0.tgz --owner OWASP
npm's own automatic package signing, applied the moment I publish, no config required.
If my GPG key leaked, the Sigstore attestation is unaffected. If my npm account got compromised, the signed tag is unaffected. An attacker has to beat all three independently, not find the one I forgot to set up. That's the actual point of redundancy: not "more security controls," but controls that fail differently from each other - which is a different property than "more pins," and it's why the pinning question above needed its own answer instead of borrowing this one.
Findings where the people who'd act on them already look
Separately from artifact integrity, CVE Lite CLI outputs SARIF, so scan findings land in a repo's Security tab next to CodeQL alerts, not in a report nobody opens:
- uses: OWASP/cve-lite-cli@v1
with:
fail-on: high
sarif: "true"
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: ${{ github.workspace }}/cve-lite-cli.sarif
That's the whole integration. No separate dashboard to check, no export step, no second login. If a team already reviews Code Scanning alerts as part of how they work, dependency findings show up in the same place instead of asking them to add a new habit.
The takeaway that isn't "turn on more settings"
It would be easy to end this with a checklist: enable attestations, output SARIF, pin your actions, done. But the pinning section is the actual point. My first-draft answer for not pinning v1 was true but incomplete - it explained the maintenance cost without ever naming the risk being traded off against it, which is the version of "trust me" that a security-literate reader should reject on sight. The version that survives scrutiny doesn't remove the risk, it names it and says specifically why a governed publisher's own release-integrity controls make it a reasonable trade, in a way that doesn't generalize to trusting some other third party's action the same way. That's the actual test for any control you're choosing to skip: not "can I justify this," but "does my justification survive being asked what threat it leaves standing."
Top comments (0)