← All articles

Most of your container CVEs aren't yours

August 7, 2026 · Wilson Santos
On this page

A repository built to be scanned. The application is 35 lines of Python. Scanning it produces 292 findings, and three quarters of them come from the container image. The code somebody actually wrote, reviewed and is responsible for accounts for fifteen.

Where the findings come from A single bar divided by control. The images control accounts for 219 of 292 findings, the Debian userland inside python:3.8-slim, while iac, sca, sast and secrets together account for 73. 292 findings from one 35-line Python application images · 219 iac · 40 everything else · 73 iac 40 · sca 17 · sast 15 · secrets 1 the base image you chose in one line not fixable by changing the application
Three quarters of the list is one decision, not three quarters of the work.

Those 219 are real, published CVEs in real packages. The image is python:3.8-slim, and what the scanner found is the Debian userland inside it: the C library, the TLS stack, the compression libraries, the certificate bundle. Every one of them is vulnerable to something, and not one is fixable by changing the application.

Why this shape is normal

A container image is an operating system with your code on top, and the ratio is not close: a slim Python base is several hundred packages and your service is one. So "the image has 219 vulnerabilities" is mostly a report on a Linux distribution somebody else assembled and you chose in one line of a Dockerfile.

This is why container scanning has a reputation for noise it does not deserve. The findings are accurate. What is wrong is the implied call to action, because 219 CVEs sorted by severity reads as 219 pieces of work and it is one decision plus a short list.

The move that changes the number

Not triage, but a different base. It is the one action that moves this number by an order of magnitude rather than a few percent, and the mechanism is boring: fewer packages means fewer things to be vulnerable, and a maintained tag means the ones left are patched. In order of how much they remove:

The moveWhat it removes, and what it costs
Update the tag you already usepython:3.8-slim is old, and much of that 219 is the passage of time. A newer patch tag of the same family removes a large share for one line and a test run
Go smallerdistroless and minimal-by-design images drop the shell, the package manager and everything else nobody needs in production
Build from a maintained minimal basedistributions built to carry near-zero known CVEs exist, and are worth evaluating on their own terms rather than because a vendor said so

The lever is one line of the Dockerfile, and no amount of finding-by-finding triage substitutes for pulling it.

What is left after you pull it

A smaller number and a more interesting one, because this is what remains after the base image stops dominating.

What is leftWhose problem it is
Packages you installedyours. They behave like dependency findings because that is what they are
Base packages with no fixnobody's yet. Decide, record it, revisit: risk acceptance rather than triage. Some publishers ship a VEX document saying which of these never applied to their image, which is the same decision made once by the people who built the thing
Code that never runsthe certificate bundle you do not parse, the compression format you never decompress. Present, real, and irrelevant to your image

That last row is the one nobody can close for you. Reachability analysis answers it for language dependencies and, for the operating-system packages inside an image, mostly does not exist yet. Anyone claiming to have solved it across a whole Linux userland is worth questioning closely.

That third category is the one the industry is still working out how to express, and it has two answers worth knowing.

What to do on Monday

Do thisBecause
1Separate image findings from code findings before you look at eitherdifferent owners, different fixes, and merging them buries the fifteen that are yours under the 219 that are not
2Treat the base image as a dependency with a versionit is one. Update it on a schedule rather than when a scan gets loud
3Rank by exposure, not by count219 on an internal batch job and 219 on your public gateway are not the same problem, and that context is not in the image
4Record what you acceptthe no-fix ones will still be there next quarter, and if nobody wrote down that they were considered, somebody considers them again from scratch

Draugr keeps images as its own control for exactly this reason, and acts on the distinction: mark an image builtBy: upstream and its 219 findings stop arriving as 219 libraries to upgrade and become the one thing you can actually do, which is take a newer image. Packages your own build installed stay listed as yours, because those you can fix.

The container image scanning guide covers what the scanners see. The numbers above came from the demo sandbox, which you can re-run.