TT Lab
Get started
Learn Learning paths Courses

Container Security

A Clean Scan Does Not Mean It Is Safe

Continue in TT Lab

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.