Hands-on playbook · 60 min
Trivy Playbook: Scan Images and Code, Gate CI, Manage Exceptions and Run It Safely
In this playbook
TL;DR: Trivy is easy to run and easy to misconfigure. Without
--exit-codeit never fails a build. Without--ignore-unfixedit 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
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-unfixedis shorthand for ignoringaffected,will_not_fix,fix_deferredandend_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.
- 1
1 · --ignore-unfixed
hide everything without a fix
- Good default for gates
- Still review unfixed criticals separately
- 2
2 · .trivyignore
an ID everywhere, optional expiry
- Add exp: dates so exceptions come back
- 3
3 · .trivyignore.yaml
scoped by path or package
- Records a statement
- Experimental, pass with --ignorefile
- 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
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.ioor unsetGITHUB_TOKENfor 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
getHostByNamefrom templates. Check the changelog before upgrading Trivy in CI. trivy k8sreports permission errors or partial results. The scanner needslistaccess 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-collectorand accept the narrower view.--exclude-namespacesneeds 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.
Start here
What do you establish first?
Which runs, in which window, with which secrets?
List every workflow run that used the action by tag during the exposure window, and for each one the secrets and tokens the job could read. The advisory says the injected code read secrets from the runner's memory, so anything reachable from those jobs is treated as exposed.
Carry it forwardScope by runs and reachable secrets, not by repositories.
Weak answerAssuming only repositories with failed scans were affected.
Constraint changes
What do you do about the exposed secrets?
Rotation, then evidence.
Rotate every credential the affected jobs could reach, starting with cloud and package registry tokens. Search the organisation for repositories named tpcp-docs-*, which the advisory lists as a fallback exfiltration route, and review cloud and registry logs for use of the old credentials.
Carry it forwardRotate first, then hunt for use.
Weak answerUpgrading the action and considering the incident closed.
Constraint changes
What changes so this cannot recur?
References, permissions and separation.
Pin every third-party action to a full commit SHA, with a bot proposing reviewed updates. Give scan jobs read-only permissions and no deploy credentials. Verify images by digest and signature where publishers sign them.
Carry it forwardA scanner runs with your secrets; treat it as supply chain.
Weak answerBanning the tool while leaving every other action unpinned.
Now defend it
How do you summarise the lesson of the 2026 trivy-action compromise?
Mutable references let an attacker change the code your pipeline runs. Scope exposure by runs and reachable secrets, rotate everything those jobs could read and hunt for its use, then pin actions to commits and keep scanners away from credentials they do not need.
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.
1A pipeline runs trivy image with --severity HIGH,CRITICAL and never fails. Why?
2What does --ignore-unfixed actually ignore?
3Why add an exp: date to a .trivyignore entry?
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.