Skip to content

Hands-on playbook · 60 min

Trivy Playbook: Scan Images and Code, Gate CI, Manage Exceptions and Run It Safely

Free playbookFacts checked Free overview: Trivy explained
Reading position
In this playbook

TL;DR: Trivy is easy to run and easy to misconfigure. Without --exit-code it never fails a build. Without --ignore-unfixed it blocks on findings nobody can fix. Exceptions without an expiry date become permanent. And since March 2026, everyone knows that a security scanner in CI is itself part of the supply chain. This playbook is free, as a sample of what the Premium playbooks cover. It takes you from a first local scan to a gate, an exception process and a hardened pipeline.

Under the hood

PLAYBOOK · ONE TRIVY SCAN
Inventory locally, match locally, filter, then decide

Target

image, folder, repo, SBOM

Analyse

OS and language packages

Match

local advisory DB

Filter

severity, status, ignores, VEX

Report

table, JSON, SARIF, exit code

Downloaded before the scan

Vulnerability DB

  • mirror.gcr.io first, then ghcr.io

Checks bundle

  • Misconfiguration checks, major version 2

Trivy works in two phases. First it analyses the target and builds an inventory: operating system packages, language dependencies from lock files and binaries, configuration files and anything that looks like a secret. Then it matches that inventory against a vulnerability database it downloaded beforehand, as an OCI artifact, from mirror.gcr.io and then ghcr.io. The matching happens on your machine. Misconfiguration checks come from a separate checks bundle, now major version 2. If the bundle cannot be downloaded, Trivy falls back to the checks built into the binary, which are only as new as your Trivy release.

Scanners decide what is looked for. The options are vuln, misconfig, secret, license and crypto; trivy image runs vuln and secret by default. Filters then decide what you see:

  • Severity limits findings to the levels you pass to --severity.
  • Status removes findings by fix status. --ignore-unfixed is shorthand for ignoring affected, will_not_fix, fix_deferred and end_of_life.
  • Ignore files and VEX remove findings you have accepted.

Finally, --exit-code decides whether anything that survives the filters fails the run. Its default is 0, so a scan without it reports findings and still passes.

Hands-on lab

You need Docker and Trivy installed locally. Everything below runs on your own machine against public images and files you create.

Step 1: scan an image and read the table.

trivy image alpine:3.15
trivy image --severity HIGH,CRITICAL --ignore-unfixed alpine:3.15

Compare the two runs. The first shows everything. The second shows only high and critical findings that have a fixed version, which is the list a developer can act on today. For each row, read the package, the installed version, the fixed version and the status.

Step 2: scan a folder for vulnerabilities, secrets and misconfigurations.

mkdir trivy-lab && cd trivy-lab
printf 'FROM alpine:3.15\nRUN apk add --no-cache curl\nCMD ["sh"]\n' > Dockerfile
trivy fs --scanners vuln,secret,misconfig .
trivy config .

The Dockerfile has no USER instruction, so the misconfiguration scan reports that the image runs as root. Add a non-root user, rescan, and watch the finding disappear.

Step 3: make it a gate. Add --exit-code 1 to the image scan and check the shell's exit status with echo $?. That number, not the table, is what a CI system reads.

Step 4: record an accepted risk. Pick one finding you have decided to accept for now. Write it to .trivyignore with a reason and an expiry date, then rerun:

# Accepted until the base image upgrade in sprint 42. Owner: platform team.
CVE-2019-14697 exp:2026-12-31

After the expiry date the entry stops suppressing, so the finding comes back by itself. That is the cheapest review reminder there is.

Step 5: generate an SBOM and rescan it later.

trivy image --format cyclonedx --output app.cdx.json alpine:3.15
trivy sbom app.cdx.json

Store SBOMs with each release. When a new advisory lands, rescanning the SBOM tells you which shipped releases are affected, without pulling any image.

Recipes

Recipe 1: put the scan policy in trivy.yaml. Trivy reads trivy.yaml from the working directory by default, and every key maps to a flag. That makes the file the reviewable home of your team's scan policy:

exit-code: 1
severity:
  - HIGH
  - CRITICAL
scan:
  scanners:
    - vuln
    - secret
    - misconfig
vulnerability:
  ignore-unfixed: true
ignorefile: ".trivyignore"
timeout: 10m0s

Recipe 2: path-scoped exceptions and VEX. .trivyignore ignores an ID everywhere. The YAML form, which is still marked experimental and must be passed with --ignorefile, can scope an exception to a path or a package URL and records a statement:

vulnerabilities:
  - id: CVE-2022-40897
    paths:
      - "usr/local/lib/python3.9/site-packages/setuptools-58.1.0.dist-info/METADATA"
    statement: Build-time only, removed in the final stage
    expired_at: 2026-12-31

For a statement other tools can also read, publish VEX. A VEX document says, for example, that a product is not_affected by a CVE because vulnerable_code_not_in_execute_path. Pass it with --vex, and add --show-suppressed to print what was hidden and by which source. VEX support is also marked experimental, so pin your Trivy version when you rely on it.

PLAYBOOK · FOUR WAYS TO ACCEPT A FINDING
From bluntest to most precise
  1. 1

    1 · --ignore-unfixed

    hide everything without a fix

    • Good default for gates
    • Still review unfixed criticals separately
  2. 2

    2 · .trivyignore

    an ID everywhere, optional expiry

    • Add exp: dates so exceptions come back
  3. 3

    3 · .trivyignore.yaml

    scoped by path or package

    • Records a statement
    • Experimental, pass with --ignorefile
  4. 4

    4 · VEX

    a standard, shareable claim

    • not_affected with a justification
    • Readable by other tools

Recipe 3: scan once, report many ways. Scanning is the slow part. Save JSON once and convert it for different audiences:

trivy image --format json -o result.json myapp:1.4
trivy convert --format sarif --output result.sarif result.json
trivy convert --format table --severity CRITICAL result.json

Only use output templates from sources you trust: Trivy's documentation warns that templates can read environment variables and put them in the report.

Recipe 4: a custom Rego check for a company rule. Suppose policy says no pod may use the host network:

# METADATA
# title: Host network not allowed
# description: Pods must not share the node's network namespace.
# schemas:
#   - input: schema["kubernetes"]
# custom:
#   id: ACME001
#   severity: HIGH
#   input:
#     selector:
#     - type: kubernetes
package user.kubernetes.ACME001

deny[res] {
    input.kind == "Pod"
    input.spec.hostNetwork == true
    msg := sprintf("Pod '%s' uses the host network", [input.metadata.name])
    res := result.new(msg, input.spec)
}
trivy config --config-check ./checks --check-namespaces user ./k8s

Custom packages only run when their namespace prefix is passed with --check-namespaces. The rule must be called deny or start with deny_. Returning result.new() lets Trivy show line numbers. Keep checks in a repository the pipeline owners control: since v0.76 the documentation warns that checks can read the process's environment variables and make HTTP requests, so the repository being scanned must never be able to supply or swap them.

Recipe 5: offline and rate-limit-proof. Trivy's databases come from free public infrastructure, and heavy CI use can hit rate limits. Options, in order of effort:

  • Keep the action's cache on.
  • List a mirror first with --db-repository. Setting the flag replaces the defaults, so list the official locations again if you still want them as fallbacks.
  • In air-gapped networks, copy the database into the cache directory by hand and scan with --skip-db-update --skip-java-db-update --skip-check-update.
trivy image --download-db-only            # on a connected machine
trivy image --skip-db-update --skip-java-db-update --skip-check-update myapp:1.4

Recipe 6: a hardened GitHub Actions job. Pin every action to a full commit SHA. To find the commit behind a release tag, ask Git directly and use the commit the ^{} line points to if the tag is annotated:

git ls-remote --tags https://github.com/aquasecurity/trivy-action v0.36.0
jobs:
  scan:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@<full-commit-sha>
      - run: docker build -t myapp:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@<commit-sha-of-v0.36.0>
        with:
          image-ref: myapp:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          limit-severities-for-sarif: true
          severity: HIGH,CRITICAL
      - uses: github/codeql-action/upload-sarif@<full-commit-sha>
        if: always()
        with:
          sarif_file: trivy-results.sarif

Two details from the action's README are easy to miss. By default, SARIF output includes every severity regardless of severity, unless you set limit-severities-for-sarif: true. And the upload step needs if: always(), so results still upload when the scan fails the job. Give this job no deployment credentials: it only needs to read the code and write security events.

Rollout runbook

PLAYBOOK · A 90-DAY TRIVY ROLLOUT
Report, gate the fixable, then pay down the rest

Days 1 to 30

report-only, SBOM per release

Days 31 to 60

gate fixable HIGH and CRITICAL

Days 61 to 90

base images, VEX, mirrors

What to report

Findings by base image

  • One upgrade often clears dozens

Gate failures and exceptions

  • Every exception has an expiry

Time to fix critical

  • From advisory to patched release

Days 1 to 30: report. Scan every image build and repository without failing anything. Generate and store an SBOM per release. Group findings by base image: most container findings come from a handful of base images, and upgrading one clears many.

Days 31 to 60: gate. Fail builds on fixable HIGH and CRITICAL findings, through a shared trivy.yaml. Publish the exception process: .trivyignore entries with an owner, a reason and an expiry, reviewed like code.

Days 61 to 90: reduce and harden. Move teams to maintained base images. Publish VEX for findings you have analysed as not affecting you. Set up a database mirror if CI hits rate limits. Audit every workflow for unpinned actions. Rescan stored SBOMs whenever a major advisory lands.

Troubleshooting

  • "context deadline exceeded" on large images. Raise --timeout, for example to 15 minutes.
  • Parallel jobs on one runner fail or hang. The cache allows one process at a time. Give each job its own --cache-dir.
  • "DENIED" when pulling the database from GHCR. A stale registry login or token is being sent. Log out of ghcr.io or unset GITHUB_TOKEN for the scan.
  • Database downloads fail under load. Use the cache, a mirror listed first in --db-repository, or a self-hosted copy. A GitHub token does not lift the database rate limits.
  • Custom template stopped working after an upgrade. v0.75 removed getHostByName from templates. Check the changelog before upgrading Trivy in CI.
  • trivy k8s reports permission errors or partial results. The scanner needs list access across the core, apps, batch, networking and RBAC API groups. Its node collector, on by default, also needs rights to create jobs and pods. If that is not acceptable in a cluster, add --disable-node-collector and accept the narrower view. --exclude-namespaces needs a cluster role, because Trivy must list every namespace first, and it cannot be combined with --include-namespaces.
  • Air-gapped misconfiguration results look dated. Without the checks bundle, Trivy uses checks embedded in the binary from its release date. Update Trivy, or mirror the bundle.

Interview drill

"Design an image scanning gate that does not block every release." Gate on fixable HIGH and CRITICAL findings, keep exceptions with owners and expiry dates, attack findings at the base image, and publish VEX for analysed false alarms. Practise it in the container image pipeline.

"What is the difference between an SBOM and a vulnerability scan?" An SBOM is an inventory. A scan matches an inventory against today's advisories, so the same SBOM gives a different answer next month. See SBOM vs AIBOM.

What if · interview follow-ups

Hold your answer as the constraints change

It is 22 March 2026. Your security team learns that aquasecurity/trivy-action tags were force-pushed on 19 March and that malicious code ran before the real scan. Your 300 repositories reference the action as @0.34.2 or @master.

  1. Start here

    What do you establish first?

Check yourself

Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.

  1. 1A pipeline runs trivy image with --severity HIGH,CRITICAL and never fails. Why?

  2. 2What does --ignore-unfixed actually ignore?

  3. 3Why add an exp: date to a .trivyignore entry?

  4. 4Why must custom Rego checks never come from the repository being scanned in CI?

Sources

Checked on 13 November 2026 against Trivy v0.76.2 documentation sources: the configuration file and CLI references, filtering, reporting, SBOM and VEX guides, databases and air-gapped use, cache and troubleshooting pages, and custom misconfiguration checks with their security considerations; the Trivy changelog for v0.69 to v0.76; the aquasecurity/trivy-action README and v0.36.0 release notes; GitHub advisory GHSA-69fq-xp46-6x23 (CVE-2026-33634).

An independent playbook for interview preparation and learning. AISecurityInterviews is not affiliated with or endorsed by Aqua Security (open source, Apache-2.0), and product names are trademarks of their owners. Use these procedures only on systems you own or are authorised to test, and check the vendor's documentation before changing production.