Trivy and Grype: making container scanning produce fixes instead of numbers

Every image scan returns hundreds of CVEs and almost none of them are reachable. How we cut the list down to what is exploitable, where each tool is stronger, and why the base image is the whole game.

Scan any production container image and you get several hundred vulnerabilities, of which a handful matter. If the pipeline blocks on "no criticals", the team ends up with a permanent exception list and no security benefit. If it does not block on anything, the report is decoration.

Getting value out of Trivy and Grype is mostly about deciding what to do with the output.

Two tools because the databases differ

Both scanners work the same way: build a software bill of materials from the image layers, match component versions against vulnerability databases, report. The difference is in the matching.

Trivy pulls from the distribution security trackers, the GitHub Advisory Database, and vendor feeds; it is broad, fast, and covers operating system packages, language dependencies, IaC and secrets in one binary. Grype uses the Anchore-curated database and pairs with Syft for SBOM generation, with matching that is often more conservative on language ecosystems.

Run both against the same image once and compare. In our experience the overlap is around 80 percent, and the differences are informative: something one reports and the other does not usually means a disputed CVE, a backported fix the other tracker knows about, or a component the other tool did not identify.

Pick one for the pipeline gate after that comparison. Running both on every build is friction with little marginal benefit; running both when you are evaluating or when a finding looks wrong is cheap.

Distribution matters more than the application

The typical breakdown of 400 findings in an image: 380 in operating system packages from the base image, 20 in application dependencies.

That ratio tells you where the effort goes. Switching from python:3.12 to python:3.12-slim removes most of the list. Switching to a distroless or Chainguard-style minimal base removes almost all of it, because the packages are not there — curl, git, bash, perl and the rest of the shell userland are the source of the majority of your CVE count and none of them are in your application's execution path.

This is the highest leverage change available and it is a one-line edit to a Dockerfile. Do it before tuning any scanner configuration.

Filter to what can be fixed and what is reachable

Two flags change the character of the output.

trivy image --ignore-unfixed --severity HIGH,CRITICAL \
  --scanners vuln,secret --format sarif -o out.sarif myimage:sha256-...

--ignore-unfixed drops the vulnerabilities with no available patch. They are still real, but they are not actionable this sprint and mixing them in stops anyone from acting on the ones that are. Track them separately and revisit when a fix lands.

Reachability is the next filter and the more powerful one. A CVE in a library that ships in the image but is never imported, or in a code path your application does not call, is not an exploitable vulnerability. Trivy does not do call-graph reachability, but you can approximate it well: a CVE in a transitive dependency of a build-time tool that is not in the runtime image is noise, and multi-stage builds remove that whole category automatically.

Scan the digest that runs, not the tag that built

The image your pipeline scanned last Tuesday and the image running in the cluster today are frequently different, because :latest moved or a rebuild reused a cached layer.

Pull the digests actually running:

kubectl get pods -A -o jsonpath='{range .items[*].status.containerStatuses[*]}{.imageID}{"\n"}{end}' \
  | sort -u > running.txt

and scan those. The gap between that list and the list of images your pipeline produced is often its own finding: images running that no current pipeline builds, which means nobody is patching them.

Generate the SBOM once, scan it repeatedly

Vulnerability data changes daily; the image does not. Generating an SBOM at build time and storing it alongside the image lets you re-evaluate every image you have ever shipped against today's database, in seconds, without pulling anything.

syft myimage:tag -o spdx-json > sbom.json
grype sbom:sbom.json

This is how you answer "are we affected?" within an hour of the next widely-exploited library bug, rather than within a week. It is also the artefact that satisfies the SBOM expectations now appearing in customer questionnaires and in regulation such as NIS2.

The gate that works

Block the build on: critical severity, with a fix available, in the runtime image, in a package the application actually loads. That is a small number, usually zero, and when it is not zero it is worth stopping for.

Everything else goes to a report with an owner and a date. The thing that keeps image hygiene good is not the gate; it is rebuilding every image weekly on a fresh base, automatically. A team that rebuilds weekly has a short vulnerability list without trying. A team that rebuilds when something breaks has a long one whatever the scanner is configured to do.

ConsultorIA

Want this done on your cloud?

A ten-day read-only assessment is free, and Skyline lets you see your estate on a map before you write to us.

Related articles