DEV Community

Daniel Westgaard
Daniel Westgaard

Posted on • Originally published at riftmap.dev

A CVE just hit your base image. Your scanner won't tell you which repos to fix.

In January 2026, CVE-2026-0861 landed in glibc. An integer overflow in the memalign family, rated high, present in every glibc from 2.30 to 2.42. Which is to say: present in debian:bookworm-slim, and in the default python, node, and golang tags, all of which are Debian underneath. The -alpine variants dodged this one, because musl is not glibc. Everything else inherited it.

If you ran a scanner across your registry, you knew within the hour. The dashboard went red. Trivy, Grype, Docker Scout, whichever one you use, they are good at this now. The CVE is high. It is in your base. Forty images flagged.

And then you sit there with the one question the dashboard does not answer. Which repositories do I open a pull request in.

Those feel like the same question. A scanner found the vulnerable image, so surely it can point me at the fix. They are not the same question, and the gap between them is the whole reason a base-image CVE takes three days instead of an afternoon.

Detecting a vulnerable image and knowing where to fix it are two different jobs. The first is an inventory of what is wrong. It is computed from the image, on the registry side or the runtime side, by reading the layers and matching package versions against an advisory feed. The second is a map of where the edit goes. And the edit does not go into the image. It goes into a Dockerfile, in a repository, that a person owns. Those repositories are a different list, derived from a different source, and your scanner never saw them.

This post is about that second list, and why the tools that produce the first one structurally cannot produce it.

What the scanner actually knows

I want to be fair to the scanners, because they are genuinely excellent and the criticism here is narrow.

Take Docker Scout, the most capable of them at the remediation end. Point it at an image and it builds an SBOM, matches every package against CVE feeds, and shows you the vulnerabilities ranked by severity. Run docker scout recommendations and it will tell you the base is out of date and which newer tag clears the most CVEs, sometimes as specific as "this tag fixes three". It ships an Up-to-Date Base Images policy that flags images still sitting on a stale base. With provenance attestations it identifies the exact base image and digest you built from. And with the GitHub integration wired up, it can open the remediation pull request for you, straight from the dashboard. That is real, and it is good.

Trivy and Grype sit a little further back, by design. You point them at a target. An image, a filesystem, an SBOM, a running cluster. They tell you what is vulnerable in that target. Same shape. The unit of work is a thing you hand them, and the output is the verdict on that thing.

And before someone says Renovate already handles this: partly, and it is worth being precise about which part. Renovate and Dependabot will open a base-image bump in each repo they are configured on, one repo at a time. That is genuinely useful, and it is the other half of remediation, the mechanical edit. But they operate per repo and tell you nothing about the consumer set as a whole. They will not tell you that forty repos share this base, which of them are on which tag, who owns each, or that the real first move is a shared internal base two hops up. They keep versions current. They do not give you the blast radius. An SBOM has the same problem in the other direction: it is an inventory of what is inside one image, the contents of an artefact rather than the consumers of it.

Now notice what the unit is in every one of these. It is an image. Or it is one repository, the one that built a given image, reached from the image through its provenance. Scout's image hierarchy is the ancestry of the image in front of you: what it was built FROM, going up. That is a real and useful relationship, and it runs in exactly the wrong direction for the question you are now asking.

The relationship you need runs the other way

A base-image CVE does not ask what this image was built from. It asks who is built on top of this base. That is the inverse relationship, and it is not one a scanner can give you, because it is not visible from any single image.

Scout, at its best, maps an image to the one repository that produced it. One image, one source repo, via the attestation. That is a one-to-one link, and it answers "where did this image come from".

What a base-image CVE forces is a one-to-many link. One base image, every repository in the organisation whose Dockerfile declares FROM it. Twelve repos, forty, a hundred and ten. Each pinning a different tag. Each owned by a different team. Some built on the base directly, some built on an internal image that is itself built on the base. That fan-out is the remediation topology, and it lives in FROM lines spread across every repo you have. Not in the registry. Not in the runtime. Not in any one image's SBOM. In source.

"What's running" is the wrong index for "what to change"

The reflex is to reach for the registry or the cluster, because that is where the scanner already looks. Both are the wrong index, and it is worth being precise about why, because the reasons are not edge cases.

The registry knows which images exist and, with provenance, what each was built from. The runtime knows what is deployed right now. Neither is the set of FROM lines in your repositories, and the divergence shows up immediately.

A repo whose image is not currently deployed still has a vulnerable Dockerfile, and it will rebuild the vulnerable base on its next merge. The runtime cannot see it. The tag a Dockerfile pins is frequently not a literal: it is FROM ${REGISTRY}/base:${BASE_VERSION}, resolved at build time from an ARG or a CI variable, so the registry's record of what was built and the repo's record of what is requested are two different strings. Internal mirrors and pull-through caches rewrite the name, so the image in your registry is harbor.internal/library/python and the thing you actually have to find across your repos is python. And the base you care about is often two hops up: your teams build FROM acme/runtime-base, which is built FROM debian, so the glibc fix has to propagate from debian to runtime-base to the forty leaf repos, and the scanner that flagged forty leaf images cannot tell you that the real first move is one pull request against runtime-base.

Every one of those is a case where the inventory of what is vulnerable and the map of what to edit pull apart. The edit lands in source. So the index has to be built from source.

The fix is a graph query, not a scan

Strip the panic away and the thing you need at the moment a base-image CVE drops is small and specific. Every repository that declares a dependency on this base, directly or transitively. The tag or digest each one pins, so you can tell who is already on a patched base and who is not. The team that owns each repo, so you know who to route the pull request to. And the order, so you fix runtime-base before you fix the forty repos that sit on it.

That is a query against a graph of your FROM edges. And the only honest way to build that graph is to parse it. Read the FROM lines in every repository, resolve the ARG defaults and the multi-stage AS aliases and the Compose image: references, normalise the internal-mirror names back to the base they point at, and connect the edges. Parsed, not inferred. Not guessed from image names that happen to look similar. Not reconstructed from a catalogue someone updated last quarter. Not pieced together from a Slack thread. Read from the files that already declare the dependency, because those files are the source of truth, and they are also exactly where your fix is going to land.

The enumeration has more sharp edges than it looks: ARG-templated tags, multi-stage builds where only one stage matters, Compose files that reference the image with no Dockerfile in sight, repos that produce the base as well as consume it. I wrote up the full mechanics of parsing all of that in how to find every consumer of your Docker base image. This post is the layer above it. Not how to build the list, but why the scanner that found the CVE was never going to be the thing that hands it to you.

Even "we can wait on this one" needs the list

There is a version of this where the CVE turns out not to be urgent, and it is worth following through, because it makes the same point from the other side.

CVE-2026-0861 is a good example. It is rated high, but exploiting it requires an attacker to control both the size and the alignment passed to memalign, with the alignment pushed into a range no ordinary program ever reaches. In most services it is not practically reachable. A reasonable platform team might decide to let it ride to the next routine base bump rather than scramble at midnight.

But that is a per-consumer decision, and you cannot make it without the per-consumer list. "Is this reachable in our usage" has a different answer in the one repo that does its own aligned allocation than in the forty that never call memalign directly. To triage at all, to say these three we patch tonight and the rest wait for the monthly rebuild, you first have to know which repos those are and how each one uses the base. Deprioritising safely is not the absence of the graph. It is one of the things the graph is for. The scanner's per-image severity score tells you the CVE is high. It does not tell you it is high here, in this repo, given how this repo uses the base, and that last clause is the only one that decides whether anyone loses sleep.

The two lists

So here is the shape of it, stripped down.

Your scanner produced a list: the images that are vulnerable. That list is real and you need it. But it is an inventory of what is wrong, indexed by image, computed from the registry and the runtime.

The list you actually act on is a different one: the repositories that declare FROM this base, with their tags and their owners and their order. That list is a map of where to go, indexed by repository, and it can only be computed from source, because source is the one place the FROM edge is written down and the one place the fix can land.

A scanner is very good at telling you the building has a problem. It is just not the thing that hands you the keys to the rooms you have to walk into. Those are different artefacts, and on the morning a base-image CVE drops, the second one is the only one that shortens the day.

This is the query Riftmap is built to answer. Point it at your GitLab or GitHub organisation with one read-only token and it parses the FROM edges across every repo, resolving the ARG defaults, the multi-stage stages, and the internal-mirror names, and builds the consumer graph. When a base-image CVE drops you select the base, and you get the list: every repository on it, direct and transitive, the tag each one pins, the team that owns it. The scanner tells you the image is vulnerable. Riftmap tells you where the fix goes.

About Riftmap

Riftmap maps cross-repo dependencies across your entire GitLab or GitHub organisation — Terraform, Docker, CI templates, Helm, and more. One read-only token. No YAML to maintain.

Top comments (0)