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:
| CycloneDX | SPDX | |
|---|---|---|
| Maintained by | OWASP Foundation | Linux Foundation |
| Standard | ECMA-424, 2nd edition, December 2025 | ISO/IEC 5962:2021 (SPDX 2.2.1) |
| Current version | 1.7, October 2025 | 3.0 |
| Java build tooling | Maven and Gradle plugins, built into Spring Boot 3.3 and later | A 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
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:
| Tool | Command | Notes |
|---|---|---|
| OSV-Scanner | osv-scanner scan source -L target/bom.json | Google's OSV.dev advisory database. The file must be named bom.json or *.cdx.json. |
| Grype | grype sbom:target/bom.json | Also scans container images and directories directly. |
| Trivy | trivy sbom target/bom.json | Trivy's documentation warns that SBOMs from other tools may give less accurate results. |
| OWASP Dependency-Check | dependency-check --scan target/app.war --format HTML --failOnCVSS 7 | Scans the jars rather than an SBOM, against NVD data; pass an NVD API key with --nvdApiKey. |
| OWASP Dependency-Track | A POST of the file to /api/v1/bom | A 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-webmvcorspring-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:
| Generation | Open-source support ended or ends | Paid commercial support until |
|---|---|---|
| Spring Boot 2.7 | 30 June 2023 | 30 June 2029 |
| Spring Boot 3.5 | 30 June 2026 | 30 June 2032 |
| Spring Boot 4.0 | 31 December 2026 | 31 December 2027 |
| Spring Framework 5.3 | 31 August 2024 | 30 June 2029 |
| Spring Framework 6.2 | 30 June 2026 | 30 June 2032 |
| Spring Framework 7.0 | 31 July 2027 | 31 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
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:
- an early warning within 24 hours of becoming aware;
- a vulnerability notification within 72 hours, with what is known about the product, the exploit and the mitigations users can apply;
- 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.
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.
- Add the CycloneDX plugin to the release build (
mvn packageor./gradlew cyclonedxBom) and archivebom.jsonwith every released artifact. - 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. - Gate CI on known vulnerabilities:
osv-scanner scan source -L target/bom.json, or--failOnCVSS 7in Dependency-Check, with the threshold written down. - 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. - Run the end-of-life check above and list every component past open-source support, with its date and the version that replaces it.
- 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.
- Record triage decisions as VEX statements, with the reason, so "not affected" survives the next scan and the next audit.
- 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.
- 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.