DEV Community

Daniel Westgaard
Daniel Westgaard

Posted on • Originally published at riftmap.dev

The CRA's 24-hour clock is a cross-repo question. Your SBOM answers a different one.

It is a Tuesday in late September 2026. A maintainer publishes a fix for an actively exploited vulnerability in a base image your platform team maintains: company/base-runtime. Somewhere in a Slack channel a security engineer asks the question the next twenty-four hours turn on. Not "what is inside base-runtime". Your SBOM scanner answered that months ago and the component is right there in the inventory. The question is the other one: "which of the products we have placed on the EU market actually ship this image, and at which tag?" That question is not in any SBOM you currently generate. It is a cross-repo question, and the clock is already running.


A note on what this post is, and isn't

This is not a compliance guide, and I am not a lawyer. There are good CRA compliance guides written by people who are, and I link to several below. This is an engineering post about a structural mismatch: the shape of the artifact the regulation asks you to keep, versus the shape of the question the regulation's timelines force you to answer under pressure.

The argument is narrow. A Software Bill of Materials is a vertical inventory: the components inside one product, down through its dependency tree. The Cyber Resilience Act's reporting clock, when it starts ticking, asks a horizontal question: across every repository in your organisation, which products ship the affected component, and at which version. Those are different graphs. The SBOM is necessary and the regulation is right to mandate it. It is just not sufficient for the question the 24-hour deadline actually asks, and the missing piece is not a better SBOM. It is the cross-repo dependency graph that tells you where each SBOM entry propagates.

Riftmap does not generate CRA-format SBOMs today. That work is on the roadmap, and I will be explicit about where the line currently sits rather than imply the product does something it does not. What Riftmap builds right now is the horizontal graph, the part that answers "which repos ship this", and that turns out to be the part the SBOM tooling category structurally does not produce.

If you are a platform lead or a CISO at a European manufacturer reading about September 2026 reporting deadlines, the practical takeaway is this: budget for SBOM generation, yes, but understand that generating SBOMs and answering "where is this component deployed across our estate" are two projects, not one.


The two questions the regulation forces together

The Cyber Resilience Act entered into force on 10 December 2024. Two dates matter for engineering planning. From 11 September 2026, manufacturers must report actively exploited vulnerabilities and severe incidents through ENISA's single reporting platform, with an early warning inside 24 hours, a full notification inside 72 hours, and a final report no later than 14 days after a corrective measure is available. From 11 December 2027, the full set of obligations applies, including the SBOM requirement that sits in the technical documentation.

Most of the public attention has gone to the SBOM mandate, and the mandate itself is modest in scope. Annex I, Part II(1) requires manufacturers to draw up a software bill of materials "in a commonly used and machine-readable format covering at least the top-level dependencies of the product." That is the legal floor: top-level dependencies, machine-readable, retained as documentation a market surveillance authority may request. The SBOM tooling category already clears this floor comfortably. Syft, Trivy, cdxgen, and the rest produce CycloneDX or SPDX output that lists components and their transitive trees inside an artifact. The SBOM is, for most teams, a solved generation problem.

The reporting clock is the part that is not solved, and it asks a different question.

When an actively exploited vulnerability lands in a component you ship, the 24-hour early warning does not ask "what is inside product X." It assumes you already know that. It asks, in effect, "which of your products with digital elements are affected, and what is the scope of the exposure." For a single product with one SBOM, that is a lookup. For a manufacturer shipping dozens of products that share internal base images, shared Terraform modules, common Helm charts, and reusable CI workflows, it is a fan-out problem across the whole estate. The exploited component is one node. The set of products that ship it is the answer, and that set is spread across every repository that consumes the node directly or transitively.

That is a cross-repo dependency question. It is the question this blog has been about since the first post. The CRA simply attached a 24-hour deadline and a fine of up to €15 million or 2.5% of global turnover to getting it wrong.

Why the SBOM does not answer it

Here is the part that trips up the budgeting conversation. A team hears "the CRA wants SBOMs" and "the CRA has a 24-hour reporting clock" and assumes the SBOM is the thing that answers the clock. It is not, and the reason is in the shape of the artifact.

An SBOM is scoped to a product. It is the inventory of components that go into one shippable thing: one container image, one application build, one firmware blob. CycloneDX and SPDX both model this as a tree: the product at the root, direct dependencies below it, transitive dependencies below those. The tooling that generates it is build-time or filesystem-scoped on purpose. Syft scans an image or a directory. Trivy scans a target. The output is faithful to one artifact and says nothing about the others.

So when the exploited component is in a shared base image, the SBOM of that base image tells you what is inside the base image. It does not tell you which application images were built FROM it, in which repositories, at which tags. Each of those downstream products has its own SBOM, and the component appears in each of those too, but only if those SBOMs were generated, retained, indexed, and queryable as a set, with the base-image relationship preserved as a resolved edge rather than a string. In practice that index does not exist as a by-product of running an SBOM generator. The SBOM generators do not build it, and they are explicit that infrastructure relationships are out of scope.

The tooling draws the line itself

You do not have to take my word for the scope boundary. The SBOM and scanning tools say it themselves. Grype's own documentation is blunt about it: it is a vulnerability scanner and nothing more, and if you need Terraform, CloudFormation, or Kubernetes manifest analysis, you need a separate tool. Trivy does scan IaC, but for misconfigurations: it tells you a security group is too open, not which repositories consume the module that defines it. Checkov runs on infrastructure code to catch policy violations within a configuration. None of these tools resolves the cross-repo artifact relationship: this base image is consumed by those eight application repos, four of which float to the new tag on next build and four of which are pinned behind.

The category split is the same one I wrote about in Symbol graphs and artifact graphs. SBOM generators inventory what is inside an artifact. They do not build the graph of which artifacts consume which other artifacts across an organisation, because that graph requires a parser estate that understands FROM company/base:${TAG}, source = "git::...?ref=v3.2.0", uses: company/actions/deploy@v2, and the registry and git resolution behind each of them. That is artifact-graph work, and it is structurally outside what a component inventory produces.

What the 24-hour clock actually asks for

Walk through the incident concretely, because the gap is clearest under time pressure.

A CVE is published for company/base-runtime and there is evidence of active exploitation in the wild. The CRA clock starts when you become aware. Inside 24 hours you owe ENISA an early warning. To write it, you need to know the scope: which products with digital elements that you have placed on the EU market are affected.

With the standard SBOM pipeline, you have an inventory per product, somewhere: in a registry, in an artifact store, in Dependency-Track if you run it. To answer the scope question you would need every product's SBOM, indexed together, with the base-image edge resolved so that "ships base-runtime" is a query rather than a grep. Most teams do not have this. What they have is the base image's own SBOM, a CI system that built the downstreams, and a frantic afternoon of grep -r "base-runtime" across repositories followed by manual reading of each Dockerfile to work out whether the tag in question is actually the one in production after build-arg substitution.

The grep finds the files. It does not resolve the answer. Which FROM lines pin the affected tag directly, which use ${BASE_TAG} resolved from a build arg in a separate workflow file, which inherit it transitively through an intermediate internal base image that is itself built FROM company/base-runtime. None of that comes out of a text search. It comes out of a parser that reads the Dockerfile, finds the default, reads the build invocation to see if it is overridden, and follows the intermediate-image chain. That is exactly the resolution work a cross-repo artifact graph does once, ahead of time, so that during the incident the scope query is a lookup instead of an investigation.

The shape of the answer you need

The early-warning notification needs the scope of affected products. The 14-day final report needs the remediation status: which products have been patched, which are pending, which are out of scope because the affected code path is not reachable. Both are queries against the same horizontal graph:

  • Which repositories consume company/base-runtime directly?
  • Which consume it transitively through an internal wrapper image?
  • Of those, which pin the affected tag versus float to it on next build?
  • For each affected product, what is the remediation state once the fixed tag is published?

A component inventory does not have these answers because it was never scoped to ask them. A cross-repo dependency graph is built to. This is not a knock on SBOMs. It is the observation that the regulation mandates one artifact (the vertical inventory) and its reporting clock demands a different one (the horizontal graph), and teams that conflate the two will discover the gap at the worst possible moment, with a 24-hour deadline running.

Where NIS2 and DORA fit, honestly

It is tempting to stack all three EU regimes into one regulatory wall and imply they all mandate the same thing. They do not, and a compliance-literate reader will catch the overstatement, so here is the honest version.

The CRA is the one with an explicit, named SBOM mandate in the legal text. NIS2 raises expectations for software supply chain security. Article 21 requires in-scope entities to manage security-related aspects of the relationships with their direct suppliers, but it does not mandate SBOMs by name. DORA, which has applied to financial entities since January 2025, emphasises ICT third-party risk management and a Register of Information covering third-party providers, rather than an SBOM requirement as such.

So the accurate framing is not "three laws all demand IaC SBOMs." It is that three overlapping EU regimes are pushing the same direction (software supply chain transparency and the ability to answer "what are we exposed to, and where" on a deadline), and the CRA is the one that makes the SBOM explicit and attaches the sharpest clock. The cross-repo graph is useful under all three for the same underlying reason: every one of them, in its own language, eventually asks a manufacturer or an essential entity to know where a given component or supplier sits across its estate. But the CRA's 24-hour reporting obligation is the concrete, dated forcing function, and it is the one to plan against first.

What this means for how you budget the work

If you are scoping CRA readiness for an engineering organisation, the practical decomposition is two projects, not one.

The first project is SBOM generation and retention: wire Syft or cdxgen or your build-tool's native CycloneDX plugin into CI, produce a machine-readable SBOM per product covering at least top-level dependencies, retain it as technical documentation, and ideally manage it in something like Dependency-Track so vulnerability correlation is continuous rather than incident-time. This is well-trodden ground with mature tools and it clears the Annex I floor.

The second project is the horizontal graph: the ability to answer, across every repository, which products ship a given component (base image, shared module, chart, workflow) and at which version, with the resolution work done ahead of the incident rather than during it. This is the project most teams have not separated out, because the SBOM conversation absorbs it. It is also the project that determines whether the 24-hour clock is a lookup or a fire drill.

The two projects share inputs. The same parser estate that resolves "which repos consume base-runtime" is reading the same Dockerfiles, Terraform sources, and Helm charts that feed component inventories. There is a real convergence here, and over time the artifact graph and the per-product SBOM become two views of one resolved dataset. That convergence is on Riftmap's roadmap and it is the subject of a future post once the SBOM-export work ships. For now the honest statement is narrower: Riftmap builds the horizontal graph today, and the horizontal graph is the half of CRA-readiness the SBOM tooling category does not cover.

What Riftmap returns for the cross-repo half

Concretely, the incident question ("which repositories ship company/base-runtime, and at which tag") is a single call against the artifact graph rather than an afternoon of grep and manual Dockerfile reading.

GET /api/v1/artifacts/{artifact_id}/consumers
Enter fullscreen mode Exit fullscreen mode
{
  "artifact": {
    "id": "a17c4f02-8b9d-4e51-9c2a-1f7e6d3b8a90",
    "artifact_type": "docker_image",
    "name": "base-runtime",
    "source_repository_id": "c2d8e1f4-3a6b-4c9d-8e2f-7b1a9d4c6e30",
    "registry_url": "registry.company.com/platform/base-runtime",
    "version": "3.4.1",
    "consumer_count": 6,
    "is_orphan": false
  },
  "consumers": [
    {
      "repository": {
        "id": "f1a2b3c4-d5e6-4f70-8a91-0b2c3d4e5f60",
        "name": "checkout-api",
        "full_path": "polaris-works/payments/checkout-api"
      },
      "version_constraint": "3.4.1",
      "source_file": "Dockerfile", "source_line": 1,
      "is_latest": true,
      "import_count": 1
    },
    {
      "repository": {
        "id": "a9b8c7d6-e5f4-4039-8271-6a5b4c3d2e10",
        "name": "ledger-worker",
        "full_path": "polaris-works/payments/ledger-worker"
      },
      "version_constraint": "3.4.1",
      "source_file": "Dockerfile", "source_line": 2,
      "is_latest": true,
      "import_count": 1
    },
    {
      "repository": {
        "id": "12340000-5678-4abc-9def-000011112222",
        "name": "invoicing-svc",
        "full_path": "polaris-works/finance/invoicing-svc"
      },
      "version_constraint": "3.3.0",
      "source_file": "Dockerfile", "source_line": 1,
      "is_latest": false,
      "import_count": 1
    },
    {
      "repository": {
        "id": "33334444-5555-4666-8777-888899990000",
        "name": "internal-base-python",
        "full_path": "polaris-works/platform/internal-base-python"
      },
      "version_constraint": "3.4.1",
      "source_file": "Dockerfile", "source_line": 1,
      "is_latest": true,
      "import_count": 1
    }
  ],
  "total_consumers": 6,
  "consumers_on_latest": 5,
  "consumers_lagging": 1,
  "latest_version": "3.4.1"
}
Enter fullscreen mode Exit fullscreen mode

The consumer table is the scope of the early-warning notification, already resolved. Each row carries the version constraint the consumer pins, the source file and line where the FROM lives, and whether that pin is on the latest published version. consumers_lagging: 1 is invoicing-svc, still on 3.3.0, evaluated against the published version list rather than left for you to work out by hand. That is the count you reason about for the report: which products ship the affected image, and which are behind.

Two of the resolution problems are worth calling out because they are exactly where a grep over FROM lines goes wrong, and they are resolved before these rows are produced rather than surfaced as separate fields. The first is build-arg substitution: a FROM company/base-runtime:${BASE_TAG} line only resolves to a real tag once the build argument is evaluated, so the consumer relationship has to be recorded against the actual base image rather than left as a literal ${BASE_TAG} string a text search would skip over. The second is the intermediate image: internal-base-python is itself built FROM base-runtime and is in turn consumed by other application repos, so the products that inherit the affected base through that wrapper are reachable by walking the graph one hop further, with a second call against internal-base-python as the artifact. Neither of those is a field you parse out of the response. They are resolution work the graph did so the response is already correct.

This does not generate the CRA SBOM. It answers the question the SBOM does not: where, across the estate, the affected component actually ships. The two halves are complementary, and the second half is the one with no existing category occupying it.

The short version

The CRA mandates a software bill of materials covering at least the top-level dependencies of each product. That is a vertical inventory, scoped to one artifact, and the SBOM tooling category (Syft, Trivy, cdxgen, Dependency-Track) produces it well.

The CRA's reporting clock, which starts on 11 September 2026, asks a different question. When an actively exploited component lands in something you ship, you owe a scope assessment inside 24 hours: which of your products with digital elements are affected, across every repository in your estate. That is a horizontal, cross-repo question. It is not what an SBOM is shaped to answer, and the SBOM tools say as much themselves: infrastructure relationships across repositories are explicitly out of their scope.

The gap between the two is a cross-repo IaC dependency graph: the resolved set of which products consume which shared components, across Docker base images, Terraform modules, Helm charts, and reusable workflows, with build-arg substitution evaluated and intermediate-image chains followed. Budget CRA readiness as two projects: SBOM generation, which is solved, and the horizontal graph, which is the half that turns the 24-hour clock from a fire drill into a lookup.

Riftmap builds that horizontal graph today. It does not yet emit CRA-format SBOMs. That convergence is coming, and it is a post for the day it ships. For now the claim is the narrow, true one: the part of CRA readiness that the SBOM category structurally does not cover is the part Riftmap exists to build.


Riftmap scans your GitHub or GitLab organisation with a read-only token, parses Terraform, Docker, Helm, Kustomize, Kubernetes, GitHub Actions, GitLab CI, Ansible, Go modules, and npm, and builds the cross-repo artifact graph as a queryable surface, for engineers in the UI and for agents over MCP. The "which products ship this component" query is one call. Five minutes to first graph. The free tier is here.

For the per-ecosystem parsing detail behind the consumer queries, the Find Every Consumer series goes one ecosystem at a time, starting with Docker base images.

Top comments (0)