Why two scanners disagree
Scan the same image with Grype and Trivy and you can get two different numbers. Neither tool is broken. A CVE count is the result of matching an inventory against a database, and both halves of that differ between tools.
This page is about how to read a finding. It is not an argument that findings can be ignored — some of ours are real, and the ones that are get fixed rather than explained. What follows is how to tell which is which, for our images or anyone else's.
What a finding actually claims
A scanner does not examine your image and conclude that it is exploitable. It performs two steps:
- Inventory. Catalogue what is installed — apk database, Go build metadata, jars, gems, crates, npm trees.
- Match. Look each entry up in a vulnerability database, by name and version.
So a finding claims: a component with this name and version is present, and an advisory exists against that name and version. That is a useful claim. It is not the same as “this image is exploitable”, and the gap between the two is where the disagreements live.
Why the numbers differ
- Different databases. Tools aggregate different sources and normalise them differently. An advisory present in one feed and absent from another produces a finding in one tool and not the other.
- Different cataloguers. If one tool reads a language manifest that another skips, it sees dependencies the other never inventories — so it can only report more.
- Different matching rules. Version-range and CPE matching are heuristics. How strictly a tool matches decides whether a near-miss becomes a finding.
- Different defaults. Severity floors, whether unfixed findings are shown, and whether dev dependencies are included all change the headline number without changing the image.
Comparing two vendors' published CVE counts is only meaningful if the same tool, the same database version and the same flags were used on the same day. Otherwise the comparison measures the tooling, not the images.
When a finding is not an exposure
These are the recurring causes, roughly in order of how often they come up:
- A backported fix. Distributions patch a vulnerability without changing the upstream version number. The version string still looks old to a matcher keyed on version alone. Our own version of this problem — a distro rebuild counter compared across distributions — is described in Reading scan results.
- Unreachable code. The vulnerable function exists in a linked dependency but nothing in the image can call it. Present, not reachable.
- Over-broad matching. An advisory recorded against a product name that collides with a different project, or a version range wider than the code it describes.
- Not in the image's role. A client library flagged for a server-side flaw the container never performs.
The honest counterweight: most findings in a container image are not in these categories. Old software with published advisories is the common case, and the fix is to update it. Treating “probably a false positive” as a default is how images stay vulnerable while looking managed.
The opposite failure, which is worse
A scanner that cannot identify the software in an image reports nothing, and an empty report looks exactly like a clean one. We measured this: two builds of HAProxy at an identical version, differing only in the apk package name, reported 0 and 14 findings. Nobody escalates a green scan.
Before trusting any clean report, confirm the scanner listed the software you expected. The commands for that are in Why your scanner reports zero CVEs.
Triaging a finding yourself
- Is the component really there?
syft <image> -o json | jq -r '.artifacts[] | "\(.name) \(.version)"' | grep -i <pkg> - What does the advisory actually say? Read the upstream advisory, not the summary line. It states the affected versions, the conditions, and often that a configuration is required.
- Is a fix available? A finding with no fixed version cannot be resolved by updating, and rebuilding will not clear it.
- Can the code be reached? The hardest question and the one worth the time. For Go, govulncheck answers it directly by analysing call paths rather than version strings.
What we publish
Every production image is scanned and both numbers are published: raw, what the scanner reported, and effective, what remains after OpenVEX statements. Both are shown on purpose — a single “0 CVEs” headline hides whether it was reached by patching or by suppression.
Suppression is evidence-based and auditable: the VEX files are in the repository, withheld counts are displayed next to the totals, and a finding that later gains corroboration returns to the count on its own. The rules, including the cases we deliberately never withhold, are in Reading scan results.
We would rather publish a number that needs explaining than a zero that needs trusting.