SBOM & Compliance

How to Generate SBOMs From Maven Projects

Produce accurate CycloneDX SBOMs from Maven builds using the official plugin, handle multi-module reactors, and ship attested SBOMs alongside your JARs.

Nayan Dey
Senior Security Engineer
4 min read

An SBOM without dev-time dependency context is almost useless for vulnerability triage, and Maven's transitive resolution makes a correct SBOM harder than it looks. This tutorial shows you how to generate a CycloneDX 1.4 SBOM from a Maven project using the official cyclonedx-maven-plugin 2.7.x, handle multi-module reactors so you get one aggregate SBOM instead of thirty fragments, exclude test-only dependencies, and publish the SBOM as a Maven classifier artifact so downstream consumers can fetch it with the JAR. Prerequisites: JDK 17, Maven 3.9, and push access to your artifact repository. Expect around 30 minutes including a clean build of a medium-sized reactor.

What plugin should I use?

Use the official cyclonedx-maven-plugin from the CycloneDX organization, not third-party forks. Version 2.7.9 supports CycloneDX 1.4 with full BOM-Ref and pedigree data, and integrates with the verify phase so a broken SBOM fails your build.

<plugin>
  <groupId>org.cyclonedx</groupId>
  <artifactId>cyclonedx-maven-plugin</artifactId>
  <version>2.7.9</version>
  <executions>
    <execution>
      <phase>verify</phase>
      <goals><goal>makeAggregateBom</goal></goals>
    </execution>
  </executions>
</plugin>

Add this to the parent POM's <build><plugins> section, not each module. makeAggregateBom walks the reactor and produces a single target/bom.json at the root with a component for every module plus all transitive deps.

How do I run it from the CLI?

Invoke the plugin goal directly without modifying the POM if you just want a one-off SBOM. This is the right approach for ad hoc audits or when you cannot edit the project.

mvn org.cyclonedx:cyclonedx-maven-plugin:2.7.9:makeAggregateBom \
  -DoutputFormat=json \
  -DschemaVersion=1.4 \
  -DincludeBomSerialNumber=true \
  -DincludeLicenseText=false
ls target/
# bom.json  bom.xml  classes  ...

The outputFormat accepts json, xml, or all. Prefer JSON for machine consumption — most scanners parse JSON faster and the diffs are human-readable in pull requests.

How do I exclude test and provided scopes?

Set includeTestScope=false and includeProvidedScope=false to keep the SBOM focused on runtime dependencies. Including test scope bloats the SBOM with JUnit, Mockito, and AssertJ — none of which ship in your artifact.

mvn verify \
  -DincludeTestScope=false \
  -DincludeProvidedScope=false \
  -DincludeRuntimeScope=true \
  -DincludeCompileScope=true

Be careful with provided: Servlet API and Lombok live there. If your artifact runs inside Tomcat, Tomcat provides the Servlet API at runtime, so you do want it in a runtime-truth SBOM but not in a "what's in my JAR" SBOM. Pick one semantic and document it in your security policy.

How do I handle multi-module reactors correctly?

Use makeAggregateBom at the reactor root instead of makeBom per module. A per-module SBOM is correct for that module but misses cross-module dependencies and produces N files for downstream tools to reconcile.

mvn -pl . -am verify
# [INFO] CycloneDX: Resolving Dependencies
# [INFO] CycloneDX: Writing and validating BOM (JSON): target/bom.json
# [INFO] CycloneDX: Writing and validating BOM (XML): target/bom.xml

For a reactor with parent + ten modules, you should see exactly one target/bom.json at the parent after the build completes. Inspect it with jq '.components | length' — for a Spring Boot reactor you typically see 180–250 components.

How do I publish the SBOM as a Maven artifact?

Attach the SBOM as a classifier on the main artifact so it is resolvable at the same GAV coordinates. Use build-helper-maven-plugin to attach bom.json with classifier cyclonedx and type json.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <version>3.4.0</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals><goal>attach-artifact</goal></goals>
      <configuration>
        <artifacts>
          <artifact>
            <file>${project.build.directory}/bom.json</file>
            <type>json</type>
            <classifier>cyclonedx</classifier>
          </artifact>
        </artifacts>
      </configuration>
    </execution>
  </executions>
</plugin>

Downstream consumers can then resolve com.acme:api:1.2.3:json:cyclonedx and always get the matching SBOM. This is the convention deps.dev and OSS Review Toolkit expect.

How do I validate the output?

Use cyclonedx-cli validate or jq for smoke tests. A malformed SBOM breaks downstream ingestion silently, so always validate in CI.

cyclonedx-cli validate --input-file target/bom.json \
  --input-format json --input-version v1_4
# BOM is valid

jq '{format, specVersion, components: (.components | length)}' target/bom.json
# { "format": "CycloneDX", "specVersion": "1.4", "components": 214 }

If cyclonedx-cli is not installed, npm i -g @cyclonedx/cyclonedx-cli gets you a 12 MB binary in under a minute. Run validation on every CI build, not just releases.

How Safeguard Helps

Safeguard ingests CycloneDX SBOMs directly from your Maven pipeline and enriches them with exploit intelligence, reachability data, and license risk in one pass. Griffin AI cross-references each Maven coordinate against the OSV and GitHub Advisory databases and highlights which transitive deps in your reactor are actually reachable from your application's entry points — cutting false positives dramatically when you have 250+ components. The platform stores every SBOM version so you can diff 1.2.3 against 1.2.4 and see exactly which libraries moved. Policy gates can block a release if a new critical CVE appears in a scope you care about, and SBOM attestations are signed and stored for audit. Ship your JAR with an SBOM, and Safeguard turns it into live supply chain intelligence.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.