Safeguard
DevSecOps

Running Trivy on GitHub: A Practical Security Guide

Wiring Trivy into GitHub Actions gives you free container, filesystem, and IaC scanning with results in the Security tab. Here's a working setup and the pinning mistake to avoid.

Marcus Chen
DevSecOps Engineer
6 min read

Wiring Trivy into GitHub gets you free, in-pipeline scanning of container images, filesystems, and infrastructure-as-code, with results landing directly in your repository's Security tab. Trivy is the open-source scanner from Aqua Security, and the official aquasecurity/trivy-action makes it a few lines of YAML to add a security gate to any GitHub Actions workflow. The catch — and it's a real one — is that a scanner running in CI is also a supply-chain dependency of your build, so the way you reference the action matters as much as the scan itself.

This guide gives you a working Trivy GitHub setup, explains the scan types, and covers the pinning discipline that keeps the scanner from becoming the attack.

What Trivy scans

Trivy is a single binary that covers several targets, which is why it shows up so often in GitHub pipelines. It scans:

  • Container images for known CVEs in OS packages and language dependencies.
  • Filesystems and repositories for vulnerable dependencies and hardcoded secrets.
  • Infrastructure-as-code (Terraform, CloudFormation, Kubernetes manifests, Dockerfiles) for misconfigurations.
  • SBOMs, which it can both generate and scan.

That breadth means one action can cover image scanning, dependency scanning, secret detection, and IaC checks, rather than stitching together four tools.

A working Trivy GitHub Actions workflow

Here's a filesystem scan that fails the build on critical and high findings and uploads results to GitHub code scanning so they appear in the Security tab:

name: trivy-scan
on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  security-events: write   # required to upload SARIF

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy filesystem scan
        uses: aquasecurity/trivy-action@0.28.0
        with:
          scan-type: fs
          scan-ref: .
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH

      - name: Upload results to Security tab
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

The security-events: write permission is what lets the SARIF upload succeed; without it the last step fails. Uploading SARIF is what puts findings in the Security tab with code annotations, rather than leaving them buried in log output nobody reads.

To scan a built image instead of the source tree, switch the inputs:

      - name: Run Trivy image scan
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: ghcr.io/acme/api:${{ github.sha }}
          exit-code: '1'          # fail the job on findings
          severity: CRITICAL,HIGH
          ignore-unfixed: true    # skip CVEs with no available fix

ignore-unfixed: true is a pragmatic choice — it hides CVEs where no patched version exists yet, so you don't fail builds on things you can't act on. Turn it off when you want full visibility for reporting.

Failing the build vs. reporting only

There are two ways Trivy interacts with your pipeline, and mixing them up causes friction. exit-code: '1' makes Trivy return non-zero on findings, which fails the job and blocks the merge. format: sarif with a code-scanning upload reports findings without necessarily failing.

A sensible pattern: on pull requests, upload SARIF for visibility but don't hard-fail on high findings while the team tunes the baseline; once the noise is under control, flip on exit-code: '1' to enforce a gate. Failing a build the day you introduce a scanner, before anyone has triaged the existing findings, just trains developers to skip the check.

The pinning mistake that turns your scanner into a risk

Here's the part most Trivy GitHub tutorials skip. A GitHub Action you reference by a mutable tag like @master or @v1 executes whatever code lives at that tag at run time, with access to your workflow's token and secrets. If the action's repository is compromised, your pipeline runs the attacker's code.

This is not hypothetical. The tj-actions/changed-files action was compromised in March 2025, and reports through 2026 describe further campaigns targeting popular actions, including tampering with the Trivy action's own tags. The lesson is the same regardless of which action: pin to a full commit SHA, not a tag.

# Fragile: a moved tag runs new code
- uses: aquasecurity/trivy-action@master

# Better: a version tag, still mutable
- uses: aquasecurity/trivy-action@0.28.0

# Best: an immutable commit SHA
- uses: aquasecurity/trivy-action@<full-40-char-sha>  # v0.28.0

A SHA can't be moved to point at malicious code the way a tag can. Add a comment noting which version the SHA corresponds to so upgrades stay legible, and use Dependabot's github-actions ecosystem to propose SHA bumps you can review. The scanner protecting your build shouldn't be the least-protected thing in it.

Tuning the noise

Trivy will find a lot on first run, especially in base images. Two levers keep it usable. The .trivyignore file lets you suppress specific CVE IDs you've assessed as not-applicable, with a comment explaining why:

# CVE-2023-XXXXX: only reachable via a CLI flag we don't ship
CVE-2023-XXXXX

And severity filtering keeps the gate focused on what you'll actually fix. Start at CRITICAL,HIGH, tighten later. For the dependency findings Trivy surfaces, reachability is the question it can't fully answer — knowing a CVE is present isn't the same as knowing your code calls it. An SCA tool such as Safeguard adds that reachability layer, and our Snyk comparison discusses where open scanners like Trivy fit against commercial platforms. For hardening the images Trivy scans, see our Docker image security best practices.

FAQ

How do I add Trivy to GitHub Actions?

Use the official aquasecurity/trivy-action in a workflow step, set scan-type (fs, image, config, or repo), and either output SARIF to upload to the Security tab or set exit-code: '1' to fail the build. Grant security-events: write permission for the SARIF upload to work.

How do I make Trivy fail the build on vulnerabilities?

Set exit-code: '1' on the trivy-action step, combined with a severity filter like CRITICAL,HIGH. Add ignore-unfixed: true to avoid failing on CVEs that have no patched version available yet.

Should I pin the Trivy GitHub action to a version or a SHA?

Pin to a full commit SHA, not a mutable tag. Actions referenced by tag run whatever code is at that tag when the workflow runs, and popular actions — including Trivy's — have been targeted by tag-tampering supply-chain attacks. A SHA is immutable; use Dependabot to review upgrades.

Does Trivy tell me whether a vulnerability is exploitable?

Not fully. Trivy reports that a vulnerable package is present, but it doesn't determine whether your code actually reaches the vulnerable function. Pair it with reachability analysis to prioritize the findings that represent real exposure rather than triaging every CVE equally.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.