A Clean Scan Does Not Mean It Is Safe
In one line
An image scanner does only two things. It unpacks the layers to build a list of installed packages, and it matches their names and versions against a vulnerability DB. It neither runs the image nor analyzes the code.
Why this was needed
If you treat a scanner as a magic box, you reach two wrong conclusions: "the scan is clean, so it is safe" and "1,247 findings came up, so we must fix them all". Both come from not knowing what the scanner does.
Once you know how it works, its limits follow automatically.
(a) Without package metadata, it is invisible. For Debian-family systems it reads
/var/lib/dpkg/status, for RPM-family systems the RPM DB, and for language runtimes the lock file.
So a binary dropped in with curl | tar xz in a Dockerfile does not appear in either the SBOM or the scan
results. This is the first reason that a clean scan result does not mean safe.
(b) If it is on the list, it is reported whether or not it is executed. Even a vulnerability in tar, which the application never calls, still shows up.
How it works
Real numbers make it easier to see. Scan payments:1.4.2 (debian 12.6) and you get
Total: 1247 (LOW 812, MEDIUM 289, HIGH 137, CRITICAL 9).
Apply --ignore-unfixed --severity CRITICAL,HIGH and it becomes Total: 4,
and the intersection with the Known Exploited Vulnerabilities list (KEV) is 1 item.
In summary: 1,247 findings became 4. Nothing became safer, but only the items you can act on right now remain.
And the real fix is not patching but replacing the base.
| Base | Size | Detected packages | Fixable CRITICAL | HIGH |
|---|---|---|---|---|
| node:22 | 1.12GB | 432 | 9 | 137 |
| node:22-slim | 231MB | 118 | 2 | 24 |
| node:22-alpine | 148MB | 41 | 0 | 3 |
| distroless/nodejs22 | 187MB | 19 | 0 | 1 |
| Static binary + scratch | 24MB | 0 | 0 | 0 |
Getting rid of 1,247 findings is done by replacing the base image, not by patching. And the criterion for judging is this one sentence — if the application actually links 8 shared libraries but the image contains 432 packages, the problem is not the vulnerabilities but the image composition.
What it looks like in the field
Gate policy starts from this dilemma. If you fail on every Critical, development
stops. If you never fail, nobody looks. The compromise that settles in practice is to
fail only on Critical/High findings that have a fix available. If you stop a build for something that cannot be fixed, people learn how to turn off the gate first. And every exception
must carry an expiry date (expired_at). An exception quietly becoming permanent is
the most common way a gate loses its force.
Priority is not decided by the CVSS score alone.
CVE-2021-44228 epss 0.9444
CVE-2023-45853 epss 0.00412
CVE-2024-5535 epss 0.00196
All three are rated Critical by CVSS. The real priority is decided by exploitability × exposure × reachability.
There are two families of SBOM formats. SPDX was created by the Linux Foundation, became the ISO/IEC 5962:2021 standard, and has deep roots in license compliance. CycloneDX started at OWASP, became ECMA-424, and is focused on security vulnerability management. But the real purpose of an SBOM is not document storage but the five minutes on the day of an incident. When a new vulnerability breaks, can you answer immediately which of your 40 services contain that package, and in which versions? That one thing decides the value of an SBOM.
Turning the scanner's numbers into real risk
The first scan often returns 200 CRITICAL findings. If you turn that list into tickets as it is, nobody will handle them. You need an order for reducing them.
First question: is that vulnerability actually executed in this image? Most are libraries that are in the base image but that our program does not call. Modern scanners have a feature that checks whether a binary actually references the function, and turning on just that shrinks the list greatly.
Second question: can an attacker reach that path? A vulnerability in the HTTP parser of an internet-facing service and a same-score vulnerability in a compression library used by an internal batch job carry different risk. A CVSS score is a value assigned without knowing the environment, so you must not rank by it as it is.
Third question: can it be fixed? If you turn on --ignore-unfixed, items without a patch yet drop out. This is not ignoring them. It is separating what you can do now from what you watch for. Keep a separate list of items without a patch, and handle them the day a patch appears.
trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:1.2.3
trivy image --format cyclonedx -o sbom.json myapp:1.2.3
Most of them disappear when you change the base image. Eighty percent of vulnerabilities come from OS packages that we do not even use. Switching to a slim image or distroless removes that 80% wholesale. Replacing the foundation is always cheaper than handling individual CVEs one by one.
Rebuilding regularly matters more than scanning. An image carries the packages of the day it was built as they are, so if you do not deploy, vulnerabilities keep piling up. If you have a pipeline that rebuilds once a week even when the code has not changed, most issues resolve themselves.
Record exceptions together with a deadline. If you put an item on an indefinite ignore list with "can't fix this", it stays forever. In .trivyignore, write the expiry date and the reason together, and make the build warn when an expired
item exists.
What you will do in the next lab
You build a package inventory yourself, compare the base and runtime images, and count the increased attack surface as a number. You build an SBOM by hand and write a policy gate script, then confirm that a token passed in with ENV gets baked into the image, and fix it by injecting at run time.