Skip to content
SRE & DevOpsDeep Dive Published Updated 6 min readViews unavailable

Trivy Image and Filesystem Scanning: Reproducible CI, Triage, and Evidence

Build a Trivy workflow that scans dependencies and final images, records scanner inputs, and triages CVEs without treating findings as proof of exploitability.

Trivy is an open-source scanner maintained by Aqua Security. The commands below use the Trivy project directly; they do not imply that every Aqua Security commercial product has the same features or workflow. Trivy can scan different targets with different scanners, so a filesystem scan and an image scan answer related but non-identical questions. A production-grade process preserves the target identity and scanner inputs, keeps findings visible, and requires human triage before claiming that a vulnerability is exploitable or harmless.

Scan the dependency tree and the artifact that will run

trivy fs inspects a local project for supported lockfile vulnerabilities and, depending on enabled scanners, secrets, misconfigurations, and licenses. It can reveal issues before a container build, but it does not prove that the final image contains the same dependency set. Build steps may install operating-system packages, copy only selected files, compile code, or introduce generated assets. Conversely, the repository scan can find risky files that are not copied into the runtime image.

trivy image inspects image contents and metadata without starting the container. It can identify supported OS and language packages and other configured findings in the artifact that you intend to deploy. Scan by immutable digest rather than a mutable tag so that the report is tied to a specific image:

set -eu

# Run from the repository root with the approved Trivy binary or image.
trivy fs --scanners vuln,misconfig,secret \
  --format json --output trivy-filesystem.json .

# IMAGE must resolve to the digest produced by this build, not a moving tag.
trivy image --scanners vuln \
  --format json --output trivy-image.json "$IMAGE"

The exact scanner set is a policy choice. Trivy’s filesystem documentation says vulnerability and secret scanning are enabled by default while misconfiguration scanning is opt-in; container-image scanning also has scanner-specific defaults. Declare the scanners you need rather than assuming that one command checks every category. A filesystem scan of a Dockerfile or Terraform file is not a substitute for scanning the resulting image, and an image vulnerability scan does not exercise application behavior at runtime.

Pin the target and record the scanner’s knowledge inputs

Vulnerability results can change even when the image digest does not, because the vulnerability database changes as advisories are added or corrected. Tool versions and configuration also affect detection and reporting. For each CI result, preserve at least the repository commit, image digest, Trivy version, enabled scanners, severity source and filter settings, database source, scan time, and raw JSON report. If the final artifact is rebuilt, scan the pushed digest that deployment will pull rather than only a local tag.

Pin the scanner runtime to an approved release and immutable distribution reference, such as a reviewed binary checksum or container image digest. Do not install an unreviewed latest scanner into every build. Keep scanner updates in a deliberate process that reviews upstream releases and verifies the chosen artifact. A version pin is for repeatability, not a reason to leave a scanner vulnerable or indefinitely stale.

Trivy automatically downloads and maintains databases as needed. Its documentation describes separate vulnerability, Java artifact, and checks databases for different scanner functions. Record where each database came from and when it was refreshed. If strict reproducibility is required for an investigation or release attestation, mirror and preserve the relevant database artifact or cache under an approved process and record its immutable identity. Only use --skip-db-update when the job intentionally consumes a verified preloaded snapshot; skipping an update without verifying age and content can produce stale results, not reproducibility with useful security coverage.

For ordinary CI, prefer a fresh, controlled database update and save the resulting inputs alongside the report. For historical comparison, rerun against a separately preserved snapshot, clearly label it as historical, and also perform a current-database scan. This separates the question “what did the scanner know at release time?” from “what do we know today about that same image?”

Make findings actionable without calling them exploit proofs

A vulnerability match means that the scanner found a component/version or package condition associated with an advisory under its detection rules. Severity communicates an impact classification from a data source; it does not by itself establish reachability, exploitability in this application, compromise, or business priority. Validate the installed package identity and vendor advisory, including distribution-specific backports or version conventions. Then assess whether affected code is present, enabled, reachable with attacker-controlled input, and exposed in the deployed context.

Look for corroborating evidence such as a vendor security advisory, a proof-of-concept or exploitation record, exposure and privilege context, runtime telemetry, and whether a supported fix is available. CISA’s Known Exploited Vulnerabilities catalog is one useful source for known exploitation in the wild, but absence from that catalog is not proof that exploitation is impossible. Likewise, a static scan cannot establish that an application never reaches the affected function merely because no finding describes a call path.

Use a CI gate that is explicit and proportionate. For example, a team may fail a build for new High or Critical findings while still retaining a complete report for all severities:

trivy image --severity HIGH,CRITICAL --exit-code 1 "$IMAGE"

Do not hide the rest of the scan behind that gate. Upload the full report as a retained artifact, track accepted risk with an owner and expiry date, and rescan deployed digests as new advisories arrive. A reviewed exception should include the affected image digest, advisory identifier, reason the exposure is limited or the package cannot yet be fixed, compensating controls, approver, and next review date. Avoid broad ignore rules that silence an entire package or directory without preserving the underlying finding.

Separate detection, remediation, and release provenance

When a fix exists, update the dependency or base image, rebuild from reviewed inputs, and scan the new digest. Verify that the finding disappeared for the expected reason; a missing report can also mean the scanner stopped detecting a package or a different image was scanned. Compare package inventories and image digests, not only summary counts.

When no fix exists, decide whether removing the component, disabling the affected feature, isolating the workload, limiting network exposure, or accepting a time-bounded exception is appropriate. A scanner’s --ignore-unfixed option changes what it displays or gates on; it does not mitigate the vulnerability. Keep unresolved items in the inventory and ensure their risk owner receives updates when vendor guidance changes.

Finally, preserve the chain from source commit to image digest and scan report. A clean scan is not proof that an image was built from reviewed source or that it was not altered after scanning. Publish SBOM and provenance evidence where the release process requires them, verify the exact digest at deployment, and rerun policy checks when an artifact changes. The useful security claim is precise: this digest was inspected with this scanner version, these enabled scanners, and this database snapshot, and these findings received the documented disposition.

Related:

Sources:

Comments