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.
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
Dockerfileand/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
genericsuite — it does not publish release-specific suites (e.g.focal,jammy). Using$(lsb_release -sc)here will cause a 404 error onapt-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
@masterin 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 thefsscan 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: sarifand upload results withgithub/codeql-action/upload-sarif@v3to see CVEs annotated inline on pull requests (requiressecurity-events: writepermission). - Suppress false positives: Create a
.trivyignorefile listing CVE IDs you've reviewed and accepted, keeping the signal-to-noise ratio high. - Kubernetes clusters: Run
trivy k8s --report summary clusterto extend scanning to live cluster configurations. - Trivy official docs cover SBOM generation, cloud account scanning, and policy-as-code configuration.
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
No comments yet
Be the first to weigh in.