DevOps Peach

Security (DevSecOps) · Vulnerability scanning with Trivy in the pipeline · Lesson 4 of 5

Putting Trivy into a CI/CD pipeline

Run Trivy after the image build and before push or deploy, use --exit-code to fail on a threshold, export SARIF or JUnit to surface results, and keep time-limited exceptions in .trivyignore.

Practice interactively →

Where it goes in the pipeline

flowchart LR
  C["Checkout"] --> FS["trivy fs + config<br/>(fast, on PRs)"]
  FS --> B["docker build"]
  B --> IM{"trivy image<br/>--exit-code 1"}
  IM -- "HIGH/CRITICAL found" --> X["Fail, do not push"]
  IM -- "Clean" --> P["Push to registry"] --> D["Deploy"]

Scan the image before pushing so a vulnerable image never reaches the shared registry. By default Trivy exits 0 even with findings; to block, set --exit-code 1.

trivy image --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed example-org/myapp:1.0

A common pattern is two runs: one full report for reading (exit 0) and one with only the blocking threshold (exit 1).

GitHub Actions

- name: Build
  run: docker build -t example-org/myapp:${{ github.sha }} .
- name: Trivy scan
  uses: aquasecurity/trivy-action@0.28.0
  with:
    image-ref: example-org/myapp:${{ github.sha }}
    severity: HIGH,CRITICAL
    ignore-unfixed: true
    exit-code: '1'
    format: sarif
    output: trivy.sarif
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with: { sarif_file: trivy.sarif }

Uploading SARIF shows results under Security → Code scanning and on the PR. Pin actions to a commit SHA: in March 2025 the tags of the popular tj-actions/changed-files action were moved to malicious code that printed secrets into logs, a textbook supply-chain attack.

Jenkins and GitLab

stage('Trivy') {
  steps {
    sh 'trivy image --exit-code 1 --severity CRITICAL --no-progress example-org/myapp:${BUILD_NUMBER}'
  }
}

GitLab's Container Scanning template uses Trivy under the hood; Azure DevOps runs the trivy command in a script step. --format template --template "@contrib/junit.tpl" produces a JUnit report that Jenkins and GitLab can display.

Time-limited exceptions

# .trivyignore — ID, reason, expiry
# No fix yet and the package is never called. Review before 2026-12-31
CVE-2024-EXAMPLE exp:2026-12-31

Exceptions need a reason, an approver and an expiry date so they do not become a permanent hiding place. The YAML format .trivyignore.yaml can also record paths and statements.

Traps: scanning after push or deploy; forgetting --exit-code so the pipeline is always green; blocking on every severity so developers ignore it; not caching the DB so jobs are slow.

Practice interactively →