Skip to content
Security Beginner Tutorial

Scan Your Containers and Dependencies for CVEs with Trivy in CI

Wire Trivy into GitHub Actions to automatically block pull requests that introduce critical or high vulnerabilities in your Docker images or dependency files.

Emeka Okafor
Emeka Okafor
Security Editor · Jun 11, 2026 · 5 min read

What You'll Build

You'll wire Trivy into a GitHub Actions workflow that scans both your built Docker image and project dependency files on every push and pull request. The job fails automatically when CRITICAL or HIGH CVEs are detected, so vulnerable code can't merge.

Prerequisites

  • A GitHub repository with a Dockerfile and/or a dependency manifest (package.json, requirements.txt, go.mod, etc.)
  • Docker installed locally for local testing (any recent version)
  • macOS (Homebrew) or Ubuntu 20.04+ for local Trivy install
  • No prior Trivy experience needed — the GitHub Actions runner installs it automatically

Step 1 — Install Trivy Locally

Validate your scan commands locally before committing them to CI.

macOS:

brew install trivy
trivy --version

Ubuntu / Debian:

sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key \
  | gpg --dearmor \
  | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" \
  | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy

Note: Aqua Security's Trivy APT repository only hosts a generic suite — it does not publish release-specific suites (e.g. focal, jammy). Using $(lsb_release -sc) here will cause a 404 error on apt-get update.

Step 2 — Run a Local Scan

Scan a public image to see Trivy's output:

trivy image --severity CRITICAL,HIGH nginx:latest

Add --exit-code 1 to confirm the failure gate works — Trivy exits with 1 when matching CVEs are found:

trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:latest
echo "Exit code: $?"

Scan your project's source tree (dependency lock files, IaC, and more):

trivy fs --exit-code 1 --severity CRITICAL,HIGH .

Step 3 — Add the GitHub Actions Workflow

Create .github/workflows/trivy.yml in your repository:

name: Trivy Security Scan

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  scan:
    name: Vulnerability Scan
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t app:${{ github.sha }} .

      - name: Scan image for CVEs
        uses: aquasecurity/trivy-action@0.20.0
        with:
          image-ref: app:${{ github.sha }}
          format: table
          exit-code: "1"
          ignore-unfixed: true
          severity: CRITICAL,HIGH

      - name: Scan filesystem / dependencies
        uses: aquasecurity/trivy-action@0.20.0
        with:
          scan-type: fs
          scan-ref: .
          format: table
          exit-code: "1"
          ignore-unfixed: true
          severity: CRITICAL,HIGH

Note: Check the trivy-action releases page and pin to the latest tag rather than using @master in production.

What each key option does:

Option Effect
exit-code: "1" Fails the CI job when matching CVEs are found
ignore-unfixed: true Skips CVEs with no available patch (reduces noise)
severity: CRITICAL,HIGH Only blocks on the most serious findings

Tip: If your repo has no Dockerfile, remove the build and image-scan steps and keep only the fs scan step.

Verify It Works

Push the workflow file, then open the Actions tab in your GitHub repo. A clean run prints:

Total: 0 (HIGH: 0, CRITICAL: 0)

To confirm the gate actually fails, temporarily change severity: CRITICAL,HIGH,MEDIUM,LOW — broadening the filter to include MEDIUM and LOW findings means almost any real-world image will trigger at least one result and fail the step. Revert the change before merging.

Troubleshooting

Scan always passes even on a vulnerable image ignore-unfixed: true hides CVEs that have no fix yet. Set it to false temporarily to see everything Trivy detects and verify the gate is wired correctly.

docker build fails with "Dockerfile not found" The build command looks in the current directory. If your Dockerfile is in a subdirectory, update the run step: docker build -t app:${{ github.sha }} ./path/to/subdir.

exit-code is ignored; the job always passes The value must be a quoted string ("1", not a bare 1) in the YAML. Also confirm the severity list has no typos — a misspelling like CRITIAL silently matches nothing.

First scan takes several minutes Trivy downloads its CVE database (~200 MB) on the first run per runner. Add a cache step pointing at ~/.cache/trivy to persist the database across workflow runs.

Next Steps

  • GitHub Security tab: Change format: sarif and upload results with github/codeql-action/upload-sarif@v3 to see CVEs annotated inline on pull requests (requires security-events: write permission).
  • Suppress false positives: Create a .trivyignore file listing CVE IDs you've reviewed and accepted, keeping the signal-to-noise ratio high.
  • Kubernetes clusters: Run trivy k8s --report summary cluster to extend scanning to live cluster configurations.
  • Trivy official docs cover SBOM generation, cloud account scanning, and policy-as-code configuration.
Emeka Okafor
Written by
Emeka Okafor · Security Editor

Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading