SBOM for Java: Maven and Gradle under the CRA
On this page
The Cyber Resilience Act asks every manufacturer to keep a software bill of materials “in a commonly used and machine-readable format covering at the very least the top-level dependencies” of the product (Annex I, Part II, point 1). Java has a particular difficulty here: Maven has no lock file, and only the build tool knows the full dependency tree once it has resolved parents, BOMs and version conflicts. The SBOM therefore has to come out of the build itself.
For the regulation itself, see SBOM for the Cyber Resilience Act.
Maven: generate it in the build
The CycloneDX Maven plugin resolves the dependency tree exactly as Maven does and writes the SBOM. You can run it once without touching the POM:
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
Or bind it to the package phase so every build produces one:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>makeAggregateBom</goal></goals>
</execution>
</executions>
</plugin>
makeAggregateBom describes a multi-module build in one file; makeBom writes one per module. The files land in target/ (bom.json and bom.xml by default), and the 2.9 versions of the plugin write CycloneDX 1.6. By default the plugin keeps the compile, provided, runtime and system scopes and leaves test dependencies out.
Reading pom.xml alone is not enough. A version inherited from a parent POM or imported from a BOM such as spring-boot-dependencies lives outside your repository, and conflicts between transitive versions are settled by Maven’s “nearest wins” rule. Anything that reads the POM as text will miss part of the tree.
Gradle: lock it, then generate
Turn on dependency locking, so the versions are fixed and visible in the repository:
dependencyLocking {
lockAllConfigurations()
}
Apply it in every project (in allprojects { }, for example). Then ./gradlew dependencies --write-locks, run in each project, writes a gradle.lockfile per project, listing every resolved component for each configuration.
For the SBOM itself, apply the CycloneDX Gradle plugin:
plugins {
id("org.cyclonedx.bom") version "3.4.0"
}
Since version 3.0, ./gradlew cyclonedxDirectBom writes one SBOM per project (in build/reports/cyclonedx-direct/) and ./gradlew cyclonedxBom aggregates a multi-project build (in build/reports/cyclonedx/). Limit it to what ships, usually runtimeClasspath, with includeConfigs on the cyclonedxDirectBom task.
Spring Boot does part of it for you
From Spring Boot 3.3, adding the CycloneDX plugin is enough: Spring Boot configures it, places the SBOM inside the executable jar at META-INF/sbom/application.cdx.json, and records its location in the jar’s manifest. The SBOM then travels with every copy of the jar. The actuator can also serve it at /actuator/sbom; if you turn that on, keep the endpoint off the public internet, since the CRA does not ask you to publish your SBOM and attackers like to know your versions.
What the build tool does not show
- Fat and shaded jars. A shaded jar copies other libraries’ classes into yours, sometimes under a new package name. Log4Shell (CVE-2021-44228) showed the cost: many teams did not know
log4j-corewas inside a jar they shipped. Generate the SBOM from the build that produces the jar, and check what you shade in. - The Java runtime. If you ship a JRE with the product (a jlink image, an installer, a container image), its version is a component, with quarterly security updates from Oracle and the OpenJDK distributors.
- Application servers and agents. A WAR deployed on a Tomcat you ship, or an APM agent added at start-up, is part of the product too.
From SBOM to vulnerabilities
The plugins only list components; a scanner such as OWASP Dependency-Check, or any tool that reads CycloneDX, matches them to advisories. Look first at the vulnerabilities exploited in the wild, which can start the reporting clock, and record in a VEX statement why a vulnerable class your code never loads does not affect the product.
How Provetry helps
The most reliable input is the SBOM written by the CycloneDX Maven or Gradle plugin: upload it, or send it from your CI, and Provetry keeps it with the release, writes it again in SPDX on paid plans, and watches every component for new and exploited vulnerabilities, every day on paid plans and every week on Free. It also reads gradle.lockfile files directly, treating test, compile-only and annotation-processor configurations as development dependencies. From a bare pom.xml, it reads only the direct dependencies it can resolve inside your repository, says which versions it could not resolve, and marks the SBOM as incomplete rather than guessing. It shows where your code imports a vulnerable package, with the file and line, but never concludes that a Java package is unused: without a full dependency graph, it cannot rule it out.