A software bill of materials lists every component an application is built from, and for a Java project built with Maven or Gradle it takes one plugin to produce. The Cyber Resilience Act makes an SBOM mandatory for software sold in the EU from December 2027. It also makes the manufacturer fix what the SBOM lists, for at least five years, and that second duty is where old Java stacks fail.

What is an SBOM?

The Cyber Resilience Act defines an SBOM as a formal record containing details and supply chain relationships of components included in the software elements of a product with digital elements (Regulation (EU) 2024/2847, Article 3(39)).

For a Java application that means one entry per resolved artifact, transitive ones included, with its Maven coordinates, version, a package URL (purl) that scanners match on, hashes and licenses, plus the dependency graph that says which component pulled in which. This is one component from a CycloneDX SBOM, trimmed:

{
  "type": "library",
  "group": "log4j",
  "name": "log4j",
  "version": "1.2.17",
  "purl": "pkg:maven/log4j/log4j@1.2.17?type=jar",
  "bom-ref": "pkg:maven/log4j/log4j@1.2.17?type=jar"
}

The US NTIA minimum elements of July 2021 set seven data fields for every entry: supplier, component name, version, other unique identifiers, dependency relationship, author of the SBOM data and a timestamp. A CISA draft from August 2025 adds the component hash, license, the name of the tool that generated the file and the generation context (whether it was produced from source, at build time or from a binary). It was published for comment and is not final.

CycloneDX or SPDX

Two formats cover almost every tool a Java team will meet:

CycloneDXSPDX
Maintained byOWASP FoundationLinux Foundation
StandardECMA-424, 2nd edition, December 2025ISO/IEC 5962:2021 (SPDX 2.2.1)
Current version1.7, October 20253.0
Java build toolingMaven and Gradle plugins, built into Spring Boot 3.3 and laterA Maven plugin; more often generated from binaries by Syft or Trivy

The CRA asks only for a commonly used and machine-readable format, and the technical guideline Germany's BSI has published for CRA SBOMs (TR-03183-2, version 2.1.0) names both. For a Maven or Gradle project, CycloneDX is the shorter path: the generator runs inside the build that already resolved the dependency tree, so the SBOM describes exactly what was compiled and packaged.

Generating an SBOM for Maven and Gradle with CycloneDX

Generate the SBOM in the release build, in CI, and store it next to the artifact it describes. An SBOM belongs to one version; a file regenerated later from a branch describes something that was never shipped.

Maven

The cyclonedx-maven-plugin (2.9.3, July 2026) has three goals: makeBom for one module, makeAggregateBom for a multi-module reactor, and makePackageBom for each module that produces a WAR or EAR. For an application, bind the aggregate goal to package:

<plugin>
  <groupId>org.cyclonedx</groupId>
  <artifactId>cyclonedx-maven-plugin</artifactId>
  <version>2.9.3</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>makeAggregateBom</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <projectType>application</projectType>
  </configuration>
</plugin>

mvn package then writes target/bom.json and target/bom.xml. The default projectType is library, which is wrong for a deployable product, so set it. Compile, provided, runtime and system scopes are included by default and test scope is not, which matches what ships; scan the test dependencies separately if your threat model covers the build servers.

Gradle

The Gradle plugin (3.5.1, October 2026) is applied to the root project:

plugins {
    id("org.cyclonedx.bom") version "3.5.1"
}

./gradlew cyclonedxBom writes the aggregate SBOM to build/reports/cyclonedx/bom.json, and a direct SBOM per project to build/reports/cyclonedx-direct/. Settings such as projectType go on the cyclonedxDirectBom task, and the plugin's README shows the Kotlin and Groovy forms.

Spring Boot 3.3 and later: an SBOM inside the jar

Spring Boot 3.3 added CycloneDX support. With spring-boot-starter-parent, declaring the plugin without a version or configuration is enough; the build packages the SBOM into the executable jar at META-INF/sbom/application.cdx.json and records it in the manifest as Sbom-Location. The sbom Actuator endpoint serves it, once exposed:

# application.properties
management.endpoints.web.exposure.include=health,sbom

$ curl http://localhost:8080/actuator/sbom
{"ids":["application"]}
$ curl http://localhost:8080/actuator/sbom/application > running-bom.json

This answers a different question from the CI file: what is running on this server right now. During an incident, that is the question that matters. Put the endpoint behind the same access control as the rest of Actuator, since the list of every library and version is useful to an attacker too.

Reading an SBOM to find vulnerable and end-of-life dependencies

The SBOM workflow for a Java application in five steps: 1, Maven or Gradle resolves the dependency tree; 2, the CycloneDX plugin writes bom.json on every build; 3, a scanner looks up each package URL in advisory data such as OSV or NVD; 4, component versions are compared with their support end dates; 5, each finding is triaged: is the code reachable, is there a fixed version, then upgrade or record the decision.FROM BUILD TO FINDINGS1. ResolveMaven or Gradleresolves thedependency tree2. GenerateThe CycloneDXplugin writesbom.json3. MatchA scanner looksup each purl inOSV or NVD data4. Check EOLVersions vssupport enddates5. TriageReachable?Fixed version?Upgrade or note
The SBOM workflow used in this guide, from the build to a decision on each finding.

Java vulnerability scanners that read a CycloneDX file

A Java vulnerability scanner matches each purl in the SBOM against advisory databases. Five open-source options cover most setups:

ToolCommandNotes
OSV-Scannerosv-scanner scan source -L target/bom.jsonGoogle's OSV.dev advisory database. The file must be named bom.json or *.cdx.json.
Grypegrype sbom:target/bom.jsonAlso scans container images and directories directly.
Trivytrivy sbom target/bom.jsonTrivy's documentation warns that SBOMs from other tools may give less accurate results.
OWASP Dependency-Checkdependency-check --scan target/app.war --format HTML --failOnCVSS 7Scans the jars rather than an SBOM, against NVD data; pass an NVD API key with --nvdApiKey.
OWASP Dependency-TrackA POST of the file to /api/v1/bomA server that keeps every uploaded SBOM and re-checks it as new advisories appear.

The first three suit a CI gate. Dependency-Track is the one that matters after release: when the next Log4Shell is published, it shows which of the versions you shipped contain the component, without rebuilding any of them. That is the question the CRA's reporting deadlines put to a manufacturer.

Spring4Shell: a finding that depends on how the application is deployed

A scanner result is a match on name and version. Whether the vulnerability is exploitable depends on facts the SBOM does not hold, and Spring4Shell is the clearest case in the Java ecosystem.

CVE-2022-22965, disclosed by Spring on 31 March 2022 and rated 9.8, allows remote code execution through data binding. CISA added it to its catalog of known exploited vulnerabilities on 4 April. According to Spring's advisory, the known exploit needs all four of these:

  • JDK 9 or later;
  • Apache Tomcat as the servlet container;
  • packaging as a WAR;
  • a dependency on spring-webmvc or spring-webflux.

A Spring Boot executable jar, the default packaging, is not vulnerable to that exploit, though the advisory adds that the nature of the vulnerability is more general. So the same SBOM line, spring-beans 5.3.17, is a release blocker for a WAR on Tomcat and Java 11, and a lower-priority upgrade for a Boot jar. Record that decision where the next scan can read it: CycloneDX carries it as a VEX statement, with a status such as "not affected" and a justification, so the finding does not come back for re-triage every week.

The advisory has a second lesson. It lists 5.2.19.RELEASE and earlier as affected and ships fixes only in 5.3.18 and 5.2.20. A system on Spring Framework 4.3, whose open-source support ended on 31 December 2020, had no open-source patch to take. Its fix was a major-version upgrade, carried out under the pressure of an exploited vulnerability.

End-of-life components: findings a CVE scan reports late

A vulnerability scanner reports known CVEs. It does not say that a component has stopped receiving fixes, and for a manufacturer with a support period to honor, that is the finding that decides the next two years of work.

Log4j 1.x shows the difference. Apache declared it end of life in August 2015 (as quoted in the NVD record for CVE-2021-4104). Since then CVE-2019-17571 and CVE-2022-23305, both rated 9.8, have been published against versions up to 1.2.17, and no 1.x release fixes either one. A team that removed log4j 1.x when it reached end of life never had those findings to deal with.

Spring publishes its own support dates through spring.io. As of October 2026, several recent generations are already past open-source support:

GenerationOpen-source support ended or endsPaid commercial support until
Spring Boot 2.730 June 202330 June 2029
Spring Boot 3.530 June 202630 June 2032
Spring Boot 4.031 December 202631 December 2027
Spring Framework 5.331 August 202430 June 2029
Spring Framework 6.230 June 202630 June 2032
Spring Framework 7.031 July 202731 July 2028

Read this table with a CRA support period in mind. A product placed on the market in 2028 with a five-year support period needs fixes until 2033, and no current Spring generation has free support that long. The plan therefore has to include either framework upgrades inside the support period or a paid support contract, and the CRA expects that choice to be made when the support period is set.

The end-of-life check is easy to automate. This script reads a CycloneDX SBOM and looks each mapped component up in the endoflife.date API, a community-maintained dataset; extend PRODUCTS with the libraries your systems use:

import functools
import json
import sys
import urllib.request

# Maven group:artifact -> product name on endoflife.date
PRODUCTS = {
    "org.springframework.boot:spring-boot": "spring-boot",
    "org.springframework:spring-core": "spring-framework",
    "log4j:log4j": "log4j",
    "org.apache.logging.log4j:log4j-core": "log4j",
    "org.hibernate:hibernate-core": "hibernate",
    "org.hibernate.orm:hibernate-core": "hibernate",
    "org.apache.tomcat.embed:tomcat-embed-core": "tomcat",
}


@functools.cache
def releases(product):
    url = f"https://endoflife.date/api/v1/products/{product}/"
    with urllib.request.urlopen(url) as response:
        return json.load(response)["result"]["releases"]


with open(sys.argv[1]) as f:
    bom = json.load(f)
for c in bom.get("components", []):
    key = f'{c.get("group")}:{c["name"]}'
    if key not in PRODUCTS:
        continue
    version = c["version"]
    cycles = [r for r in releases(PRODUCTS[key])
              if version == r["name"] or version.startswith(r["name"] + ".")]
    if not cycles:
        print(f"{key} {version}: no release cycle found, check by hand")
        continue
    cycle = max(cycles, key=lambda r: len(r["name"]))  # 2.12.4 matches 2.12, not 2
    if cycle["isEol"]:
        print(f'{key} {version}: end of life since {cycle["eolFrom"]}')

Run against an SBOM from a Spring Boot 2.7 application, it prints:

$ python3 eol_check.py target/bom.json
org.springframework.boot:spring-boot 2.7.18: end of life since 2023-06-30
org.springframework:spring-core 5.3.39: end of life since 2024-08-31
log4j:log4j 1.2.17: end of life since 2015-10-15
org.hibernate:hibernate-core 5.6.15.Final: end of life since 2023-02-06

Dates come from endoflife.date, so the log4j line shows its date of 15 October 2015 rather than Apache's August announcement. For a contract or an audit, confirm each date on the vendor's own page.

What the Cyber Resilience Act requires from software manufacturers

The Cyber Resilience Act applies to products with digital elements made available on the market whose use includes a data connection to a device or network (Article 2). The definition covers software sold on its own, including components placed on the market separately, together with the product's remote data processing: the backend a product needs in order to work. Software offered only as a service is outside it; recital 12 points SaaS providers to the NIS 2 Directive instead. Medical devices, vehicles, aviation and marine equipment stay under their own regulations.

So an on-premises Java product, a desktop or mobile client, an appliance and a commercial library are in scope when sold in the EU, wherever the manufacturer is based. A web application that customers use only through the browser, on your servers, generally is not.

The SBOM requirement in Annex I

The obligation is one sentence in the vulnerability handling requirements, Annex I, Part II, point (1). Manufacturers shall:

identify and document vulnerabilities and components contained in products with digital elements, including by drawing up a software bill of materials in a commonly used and machine-readable format covering at the very least the top-level dependencies of the products

Cyber Resilience Act, Annex I, Part II (1)

Three details follow from the rest of the text:

  • The SBOM does not have to be published. It is part of the technical documentation (Annex VII, point 2(b)), and the user information needs to say where to find it only if the manufacturer decides to make available the software bill of materials to the user (Annex II, point 9).
  • A market surveillance authority can ask for it, with a reasoned request, to check compliance (Annex VII, point 8).
  • The Commission may set the format and required fields by implementing act (Article 13(24)). Until it does, the BSI guideline above is the most specific reference.

A list of top-level dependencies meets the wording and little else. The same annex requires products to be placed on the market without known exploitable vulnerabilities (Part I, point (2)(a)), and Article 13(5) requires due diligence on components from third parties, free and open-source ones included. Log4Shell sat in log4j-core, which many applications pulled in through another library rather than declaring it, so a top-level list would not have shown it. The Maven and Gradle plugins record the full tree at no extra cost, and a manufacturer who keeps only the first level cannot answer whether a product contains the next one.

Vulnerability handling for the whole support period

Article 13(8) makes the manufacturer handle vulnerabilities in the product, including its components, for a support period of at least five years, unless the product is expected to be in use for less. Each security update must then stay available for at least ten years after it is issued, or for the rest of the support period if that is longer (Article 13(9)). Annex I, Part II adds the working rules: remediate without delay, ship security fixes separately from feature updates where technically feasible, have a coordinated vulnerability disclosure policy with a contact address, and distribute security updates free of charge, unless a business customer agreed otherwise for a tailor-made product. A paid support contract for an old framework is therefore a cost the manufacturer carries; it cannot be passed on as a paid security update.

When the vulnerability is in a component, Article 13(6) asks the manufacturer to report it to whoever maintains that component, and to share its own fix if it wrote one.

Cyber Resilience Act dates

Cyber Resilience Act timeline. Published in the Official Journal on 20 November 2024 and in force since 10 December 2024. Chapter IV, the rules for notified bodies, applies from 11 June 2026. Article 14, the duty to report actively exploited vulnerabilities and severe incidents, applies from 11 September 2026, including to products placed on the market before December 2027. All other obligations, including the software bill of materials, apply from 11 December 2027 to products placed on the market from then on, and to older products only after a substantial modification. Today is October 2026, so the reporting duty already applies.CYBER RESILIENCE ACT: WHEN EACH PART APPLIES2025202620272028Today: October 202610 Dec 2024In force11 Jun 2026Rules for notifiedbodies (Chapter IV)11 Sep 2026Reporting of exploitedvulnerabilities (Art. 14)11 Dec 2027All obligations,SBOM included,for new productsmilestonereporting duty in force, also for products already on the EU market
Dates from Articles 69 and 71 of Regulation (EU) 2024/2847; the Commission's CRA page gives the same.

The regulation was published on 20 November 2024 and entered into force on 10 December 2024. Most obligations, the SBOM among them, apply from 11 December 2027. Two parts start earlier: the rules for conformity assessment bodies from 11 June 2026, and Article 14, the reporting duty, from 11 September 2026.

The transitional rules in Article 69 matter for older systems. A product placed on the market before 11 December 2027 falls under the regulation only if it is substantially modified after that date, meaning a change that affects its compliance with the essential requirements or its intended purpose. Article 14 is the exception: it applies to every product in scope, whenever it was placed on the market. A Java product your customers in the EU have run since 2015 is covered by the reporting duty today.

Reporting actively exploited vulnerabilities

Under Article 14, a manufacturer that becomes aware of an actively exploited vulnerability in its product notifies the coordinating CSIRT and ENISA through the single reporting platform, in three stages:

  1. an early warning within 24 hours of becoming aware;
  2. a vulnerability notification within 72 hours, with what is known about the product, the exploit and the mitigations users can apply;
  3. a final report no later than 14 days after a corrective or mitigating measure is available.

Severe incidents that affect the security of the product follow the same 24- and 72-hour stages. The 24 hours run from awareness of exploitation in your product, and the SBOM is how you find out, within hours rather than days, whether a newly exploited library is in the product at all.

Fines for breaching the essential requirements or Articles 13 and 14 reach EUR 15 million or 2.5% of worldwide annual turnover, whichever is higher (Article 64(2)). Supplying incorrect or incomplete information to an authority carries up to EUR 5 million or 1%.

How old Java stacks fail SBOM and CRA checks

The Java security vulnerabilities a scan finds on a ten-year-old system fall into three groups, and one problem comes before any scanner runs.

What a build-time SBOM lists compared with what a legacy Java server runs. The SBOM lists the dependencies declared in pom.xml, their transitive dependencies and the versions Maven resolved. The production WAR on an application server runs all of that plus jars copied by hand into WEB-INF/lib, classes shaded into other jars, libraries the application server provides, and the JDK with its own version. Those extra components are missing from the SBOM unless a binary scan adds them.WHAT THE BUILD-TIME SBOM LISTSDependencies declared in pom.xmlTheir transitive dependenciesVersions as Maven resolved themWHAT THE LEGACY SERVER RUNSProduction WAR on an app serverEverything on the left, plus:Jars copied into WEB-INF/libClasses shaded into other jarsLibraries the app server providesThe JDK and its own version
Components a legacy deployment runs that a build-time SBOM does not list.

The build does not know everything that ships. Jars copied by hand into WEB-INF/lib or a lib/ folder, as Ant-era builds did, have no coordinates in any POM, so the Maven plugin never sees them. Dependencies in provided scope come from the application server, at whatever version that server carries. Shaded jars hide other libraries inside their own classes. A binary scan of the deployable closes part of the gap: syft app.war -o cyclonedx-json=app.cdx.json looks inside nested jars, shaded ones included, for the Maven metadata they carry (Syft's Java documentation). Comparing that file with the build SBOM shows what the build never declared; jars with no metadata at all can still be identified by checksum against Maven Central.

The fix exists only in a newer major version. Spring4Shell on Spring 4.3 is one example. The same pattern repeats with Hibernate 5, old Jackson lines and application servers: the advisory names a fixed version, and reaching it means moving the framework, the javax to jakarta namespace and often the JDK at the same time.

There is no fix at all. Log4j 1.x, abandoned upload and templating libraries, and in-house forks of open-source projects that stopped tracking upstream. Each one has to be replaced, and the replacement is application work, with tests around it.

The fix exists and nobody took it. This is the most common case. In 2025, according to Sonatype, Log4j was downloaded from Maven Central nearly 300 million times, and roughly 13% of those downloads, about 40 million, still contained Log4Shell four years after the fix. Across the ecosystem, about 95% of vulnerable components downloaded already had a safer version available. Sonatype sells software composition analysis tools, so read the framing accordingly; the download counts come from Maven Central, which Sonatype operates.

We saw all four on a fifteen-year-old online store and back office for a wine merchant. An OSV scan found 12 critical and 35 high-severity advisories, log4j 1.x among them, many with no fix on the versions in use, and several libraries had been abandoned by their authors with no release for Spring 6. We moved both applications to Java 25 and Spring 6.2 in tested steps, in about four weeks for the core upgrade, and the re-scan found none.

SBOM and CRA readiness checklist for a Java application

The order matters: the first steps are cheap and tell you how large the later ones are.

  1. Add the CycloneDX plugin to the release build (mvn package or ./gradlew cyclonedxBom) and archive bom.json with every released artifact.
  2. Scan the deployable as well (syft app.war -o cyclonedx-json=app.cdx.json) and give every component that appears only in the binary scan an owner and a Maven coordinate, or a replacement.
  3. Gate CI on known vulnerabilities: osv-scanner scan source -L target/bom.json, or --failOnCVSS 7 in Dependency-Check, with the threshold written down.
  4. Upload the SBOM of every version still under support to Dependency-Track, so a new advisory is matched against what customers run as well as against main.
  5. Run the end-of-life check above and list every component past open-source support, with its date and the version that replaces it.
  6. Set the support period for each product, then check it against the support dates of the JDK, the framework and the application server under it. Where they end first, plan the upgrade or the paid support now.
  7. Record triage decisions as VEX statements, with the reason, so "not affected" survives the next scan and the next audit.
  8. Write the Article 14 procedure: who decides that a vulnerability is actively exploited, who files the 24-hour warning, and which CSIRT coordinates for your company.
  9. Publish a vulnerability disclosure policy and a security contact address.

In our experience, steps 1 to 5 take a day or two per application. What they find decides the rest, and on systems older than Spring Boot 3 it is usually upgrade work.

If the scan lists more findings than your team can schedule, our Java vulnerability remediation work starts from the same place: an SBOM of what is in the build, notes on which findings apply, and upgrades in priority order, each tested and followed by a re-scan you can hand to an auditor.