You scanned a runtime image — python, node, rust, openjdk — and got 100+ findings. This page tells you which of them are real, how to remove most of the rest in five steps, and how to document what's left so it passes a security review.
- • Most findings sit in OS packages your application never runs. Real, actionable issues are usually a handful.
- • The count drops fastest by upgrading the tag and shrinking the base — not by patching findings one by one.
- • What you don't fix, you prove: a VEX statement with evidence, not a silent ignore.
Why runtime images ship with 100+ CVEs
A runtime image is an OS layer (Debian/Ubuntu in default tags) + the language runtime + system libraries (OpenSSL, zlib, libxml2). The default tag installs hundreds of general-purpose OS packages — perl, git, gnutls, libsystemd — because the image is built for everyone, not for your app.
A scanner flags every known CVE in every installed package, whether or not your code ever loads it. So the number you see is an inventory of what's installed — not a measure of your risk.
How many findings are real
| Signal | Figure | Source |
|---|---|---|
| Critical CVSS that stays critical with runtime context | 18% | Datadog, State of DevSecOps 2025 |
| Critical count once filtered to running + exploitable + exposed | −92% | Datadog, State of DevSecOps 2025 |
| All published CVEs ever exploited in the wild | ~1% | CISA KEV: ~1,500 of 312,000+ |
| Open-source dependency vulns reachable at function level | <9.5% | Endor Labs |
| Dependency code actually used, on average | 12% | Endor Labs |
Function-level reachability cuts remediation work by roughly 90%. In practice a 150-finding report typically contains a few issues worth fixing today. The rest you remove structurally or document as not applicable.
Fix these first
A finding is real and urgent if any of these hold:
- It's in CISA/VulnCheck KEV — exploited in the wild right now.
- EPSS is elevated (above roughly 1–5%, versus the sub-0.5% typical for image findings).
- The package is one your app actually loads, on an internet-exposed path.
- A fixed version exists and upgrading is cheap.
Patch these directly: bump the affected package or move to the tag where it's fixed. Everything else — next section.
Five steps that cut the count
1. Upgrade the tag
Old tags sit on old OS bases and eventually stop receiving updates entirely — Python 3.9 reached end of life in October 2025, so its images no longer get security patches at all. Moving to a current version on the latest patch release rebases the whole OS layer onto patched packages. Typically the single biggest drop, for zero code changes.
2. Switch to a smaller variant
Fewer packages, fewer findings:
| Variant | Typical finding count |
|---|---|
| Full Ubuntu-based runtime image | 100–200+ |
-slim | a few dozen |
| distroless / Wolfi-based | near zero |
3. Multi-stage build
Compile and install in a builder stage, copy only artifacts into the runtime stage. Compilers, headers, and package managers — with all their CVEs — never ship.
FROM python:3.13-slim AS builder COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt FROM python:3.13-slim COPY --from=builder /install /usr/local COPY . /app USER 1000:1000 CMD ["python", "/app/main.py"]
4. Remove what you don't use
--no-install-recommends, clean caches, drop dev headers and docs.
5. Rebuild and re-scan on a schedule
New CVEs are published against packages that didn't change in your image. A stale minimal image is worse than a fresh slim one. Pin the base by digest, rebuild when it updates, re-scan daily.
Prove the rest doesn't apply
For findings you choose not to fix, record evidence — don't silently ignore:
- Build-only — the package exists in the builder stage and isn't present in the shipped image.
- Not reachable — the vulnerable function is never called on any code path your app executes.
- Not exposed — no network or input path reaches the vulnerable code.
- Already patched by the distro — Debian/Ubuntu backport security fixes without bumping the version string, and mark low-impact issues “no-dsa”; version-matching scanners still flag both. Check the distro security tracker before treating these as open.
Record each as a VEX statement: status not_affected plus a standard justification (for example vulnerable_code_not_in_execute_path). VEX is machine-readable — your scanner suppresses the finding, your auditor accepts the evidence, and the question doesn't come back next quarter.
The manual cost — and the alternative
Done by hand, this is roughly a day per image: upgrade, rebase, restructure the build, check distro trackers for backports, write VEX for the remainder — then repeat every time the base updates or a new CVE lands.
BaseImage ships hardened runtime images with this already done: noise removed, remainder documented, re-scanned daily so the one CVE that starts being exploited reaches you first.
Hardened images
Skip the day of manual work
Noise removed, the remainder documented with VEX, re-scanned daily.