The base image you pinned six months ago is a frozen snapshot of that day's vulnerabilities - shipped to production on every deploy.
A pinned base image is two things at once. It is reproducible, which is why you pinned it - the same digest builds the same bytes on every machine. And it is frozen, which is the part nobody puts on the slide: the moment you pin it, the image becomes a snapshot of that day's known vulnerabilities, and it keeps shipping that snapshot to production on every deploy until someone rebuilds. The CVE disclosed last Tuesday does not care that your build is reproducible. It is in there, reachable, on the next push.
The dangerous word is silent. Nothing in the pipeline goes red when a new CVE lands against a package that has been sitting in your base layer for six months. The image that passed its build-time scan in January is full of February and March disclosures by spring, and the green checkmark from January is still the last thing the pipeline said about it.
Scanning only at first build measures the image against the advisory data that existed at build time. That is a point-in-time photograph, and vulnerabilities are not a point-in-time fact. A CVE is disclosed against a package version; the version in your layer did not move, but the set of known-bad versions grew underneath it. The honest question is not "was this image clean when we built it" but "is this image clean against what we know today", and those two questions diverge a little more every day the image stays pinned.
So the unit of work is the built image as it exists now - its actual installed packages, checked against a current advisory feed - not the Dockerfile's intent and not a one-time verdict cached at build. apPosture re-reads the image's own package database every scan, so a re-scan of an unchanged image surfaces every CVE disclosed since the last run, with no rebuild required to learn the truth.
The scan never phones home. apPosture inspects a local Docker-daemon image through the already-mounted socket, creates a throwaway container without running it, and exports the flattened filesystem to a temporary tar that is always deleted in a finally block. From a single streaming pass over that tar it lifts three things: the OS package database, the release files that name the base OS, and any application dependency manifests. No layer is pulled from a registry, no advisory data is fetched over the network. An air-gapped pipeline still learns about a CVE disclosed yesterday, because the advisory feed is curated and shipped offline, not queried live.
The package database is the ground truth. On Debian and Ubuntu images that is var/lib/dpkg/status, parsed stanza by stanza with only genuinely installed packages kept. On Alpine it is lib/apk/db/installed, read by its P: and V: records. The base OS version is taken from the image's own /etc/os-release or /etc/alpine-release, read from inside the layers - evidence from the filesystem, never a guess parsed off the FROM line. Below is where the staleness lives and how the read works:
Matching a package name to a CVE is the easy half and the half that produces noise. The package is only vulnerable inside a version range, and distro versions are a minefield of epochs, dfsg suffixes, +really markers and -revision tails. apPosture normalizes a distro version to its true upstream version before any range comparison, so a string like 1:1.3.dfsg+really1.3.1-1+b1 is compared as 1.3.1, not mis-flagged or missed on a naive string match. A finding is raised only when the installed version actually satisfies the advisory's vulnerable range. Base images that have reached end of life are flagged on an exact distro-and-version match against a curated feed, never a heuristic guess, so an EOL call is a fact, not a hunch.
Alongside the CVE matching, apPosture runs CIS-style image configuration checks against the image config and history - fully offline, no execution. These are the hardening defaults that turn one OS-package CVE into a worse incident: a container running as root, a mutable :latest tag, a secret baked into an ENV layer, a missing healthcheck. Two kinds of evidence, one scan:
| Check | What it inspects | Why it matters | Severity |
|---|---|---|---|
| OS package CVE | Installed version in dpkg / apk DB vs vulnerable range (OWASP A06) | Known-fixed flaw still shipping in the layer | per advisory |
| Base image EOL | distro + version from the image's release file vs curated feed | No upstream security fixes are coming at all | medium |
| Runs as root | Config User is empty, 0 or root | No least-privilege boundary if the process is popped | medium |
| Secret in ENV | Env keys matching PASSWORD / TOKEN / API_KEY with a real value | Credential baked into a shareable layer | high |
| :latest tag | Image reference pins a mutable tag, not a digest | Not reproducible; today's :latest is not yesterday's | low |
| No healthcheck | Config has no HEALTHCHECK test | Orchestrators cannot detect an unhealthy container | info |
Each row is grounded in something read from the image, and the finding records the package, the version, the fixed version where one exists, and the file the evidence came from. When the optional Trivy binary is present and its database has been pre-seeded offline, apPosture runs it as an augmenting passthrough and de-duplicates the overlap; the built-in engine stays the default so a clean install needs no extra binaries and no network.
A container CVE is rarely a container-only problem. The same vulnerable library can show up in your source dependency scan and in the built image, and a naive tool would report it twice and let you fix it zero times. apPosture collapses both into a single unit of work: OS and application-package findings dedup on the CVE-and-component fingerprint, which is the same key the SCA engine uses, so an image finding and a dependency finding for the same CVE merge instead of multiplying. The same fingerprint also merges an imported Trivy report into the record you already have.
All of it lands in the unified Vulnerability model as one source among DAST, SAST, SCA and IaC - not a fourth dashboard you have to remember to open. The container CVE inherits the same risk scoring, the same SLA clock counted from first-seen, and the same posture grade as everything else, and each finding ties back to the package and layer that introduced it so the remediation is concrete: bump this package, or rebuild on this newer base.
The shape of the fix is almost always the same and almost always boring: rebuild. The value is in being told, on a schedule, against today's advisory data, and without shipping your image anywhere - that the frozen snapshot you deployed months ago has quietly drifted into a list of known, fixed, still-present CVEs. The image never leaves your host; only the verdict moves.