On March 19, 2026 at 17:43 UTC, a threat actor tracked as TeamPCP compromised the release pipeline of Trivy, the open-source scanner millions of CI jobs use to fail builds on vulnerable container images. The attacker published a malicious trivy v0.69.4 release and force-pushed 76 of 77 version tags in aquasecurity/trivy-action, plus all 7 tags in aquasecurity/setup-trivy, to commits carrying a Python infostealer that harvested SSH keys, cloud and database credentials, Docker and Kubernetes configs, and crypto wallets from CI runners. Any pipeline referencing aquasecurity/trivy-action@v0.28 — or any other mutable tag — pulled the backdoored commit automatically on its next run, no code change required. The incident, tracked as GHSA-69fq-xp46-6x23, is a pointed reminder that the same automation making container scanning effortless can just as easily automate a compromise if you trust a tag instead of a commit. This tutorial walks through wiring image scanning into GitHub Actions with severity-based fail-the-build thresholds, SARIF upload to the Security tab, and — given what happened in March — the pinning discipline that would have kept a pipeline safe through the incident. The result, whichever container image tool your team standardizes on, is a repeatable container image audit built into every pull request instead of a manual, periodic check.
What does a minimal fail-the-build scan actually look like?
A minimal fail-the-build scan is a single Trivy Action step with two inputs: severity and exit-code. Running aquasecurity/trivy-action with severity: CRITICAL,HIGH and exit-code: 1 tells Trivy to scan the built image against its vulnerability database and return a non-zero exit code — which GitHub Actions treats as a failed step — the moment it finds a match in either severity band. Leave exit-code at its default of 0 and Trivy still prints every finding to the job log, but the workflow goes green regardless, which is a common misconfiguration teams don't notice until an incident review. The scan targets the image reference you just built and tagged in an earlier step (for example myapp:${{ github.sha }} ), not a registry URL, so it needs to run after docker build and before docker push. Grype, Anchore's equivalent scanner, uses the same shape of control with a --fail-on high CLI flag instead of an Action input, producing an identical pass/fail contract for teams that prefer it. Snyk's own GitHub Action follows the same pattern too, with a --severity-threshold flag playing the role exit-code plays for Trivy — so wiring up Snyk in GitHub Actions for a fail-the-build gate looks nearly identical regardless of which of these container image solutions a team has already standardized on.
How do you tune severity thresholds without drowning the team in noise?
You tune thresholds by separating "block the merge" from "log for later," because a threshold set too low turns every PR red and trains engineers to ignore the check entirely. The common pattern is two jobs: a strict gate on CRITICAL,HIGH that fails the build, and a separate non-blocking scan on MEDIUM,LOW that uploads results without an exit-code override, so lower-severity findings stay visible without stopping deploys. Trivy also supports ignore-unfixed: true, which drops findings for vulnerabilities with no available patch — useful because failing a build on a CVE with no fix forces developers into either waiting or building an image exception, neither of which improves security posture. A .trivyignore file lets a team suppress specific CVE IDs with a documented, reviewable exception rather than lowering the severity bar globally, which is the difference between a targeted waiver and a policy that quietly stops catching real criticals.
How do you get findings into GitHub's Security tab instead of just the job log?
You get findings into the Security tab by having Trivy emit SARIF format and uploading it with github/codeql-action/upload-sarif, a GitHub-maintained Action that ingests the file and renders each finding as a code scanning alert. This requires two Trivy Action runs in the same job — one with format: sarif and exit-code: 0 purely to produce the report, then the upload step, then a second Trivy invocation with exit-code: 1 to actually gate the merge — because a step that fails the job before the SARIF upload runs will bury the very evidence a reviewer needs to see why the check failed. SARIF alerts persist across commits and let a security team track a CVE's status — open, dismissed, fixed — as a first-class GitHub object instead of scrolling through historical Actions logs to find when a finding first appeared.
What actually happened in the March 2026 Trivy supply chain compromise?
TeamPCP gained access to Aqua Security's release infrastructure and used it to force-push nearly every version tag across aquasecurity/trivy-action and aquasecurity/setup-trivy to malicious commits, alongside a poisoned trivy binary release itself, according to reporting by The Hacker News and BleepingComputer and the GitHub Security Advisory GHSA-69fq-xp46-6x23. Because GitHub Actions resolves a tag like @v0.28.0 to whatever commit it currently points at — not the commit it pointed at when a workflow author first wrote the YAML — every pipeline using a floating tag silently ran the malicious code on its next execution. Legit Security's incident writeup identified trivy-action v0.35.0 (commit 57a97c7) and setup-trivy v0.2.6 (commit 3fb12ec) as verified clean versions predating the compromise. Teams that had already pinned to a specific commit SHA rather than a tag were unaffected regardless of when the attack occurred, because a SHA reference cannot be silently redirected.
What should the pinning step in a real workflow look like?
The pinning step should reference the scanning Action by its full 40-character commit SHA, with the human-readable version as a trailing comment for maintainability — for example uses: aquasecurity/trivy-action@<commit-sha> # v0.35.0, not uses: aquasecurity/trivy-action@v0.35.0. Dependabot and Renovate both support version-comment syntax that lets them still propose SHA-to-SHA updates when a new release ships, so pinning does not mean freezing forever, only removing the window where a maintainer's compromised account can rewrite what your pipeline executes. The same logic applies to actions/checkout, docker/build-push-action, and every other third-party Action in the job — GitHub's own security hardening guidance for Actions has recommended SHA pinning for years, and the March 2026 incident is the clearest evidence yet of why that advice exists rather than an abstract best practice.
How Safeguard fits into this pipeline
Safeguard's esscm container image integration scans layers from ECR, Docker Hub, GCP, GitHub Container Registry (GHCR), and OCI registries continuously, and its own guidance mirrors the tutorial above: scan specific tags rather than latest, and integrate scanning into CI/CD before deployment rather than relying on periodic registry sweeps. For teams standardizing on Safeguard directly in the pipeline, safeguard-sh/gate-action@v1 takes the same policy and fail-on: critical inputs shown here, emits SARIF and JUnit output for the Security tab and test reporting respectively, and returns documented exit codes — 1 for a policy violation, 2 for an auth error, 3 for misconfiguration — so a failing gate step is unambiguous about why it failed. Because Safeguard ingests and correlates SBOMs across the whole supply chain, a compromised scanning Action upstream (as in the Trivy incident) would surface as an anomalous SBOM diff on the next scan, not just a silent tag change nobody reviewed.