Safeguard
Supply Chain•

How do I make one SBOM for a multi-module Maven build?

Use the CycloneDX Maven plugin's makeAggregateBom goal for a root SBOM across modules, and learn the defaults for scope, schema version and output files.

Safeguard Security Team
3 min read

Bind the CycloneDX Maven plugin's makeAggregateBom goal at the root of the build. It writes one aggregate SBOM covering every module, and can also emit one per module. Check the scope defaults first, because test scope is excluded unless you turn it on.

Definitions

An SBOM is a machine readable list of the components in a piece of software. CycloneDX is one standard format for it. In a multi-module Maven build, you can describe each module separately, or the whole product at once.

The goals

GoalProduces
makeBomA BOM for each module
makeAggregateBomAn aggregate BOM at the build root, optionally with each module's BOM
makePackageBomA BOM for each module with war or ear packaging

Any goal can be skipped with cyclonedx.skip=true.

Configuration sketch

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

Pin the plugin version your team has approved. The plugin's own page binds the aggregate goal to package in its usage example.

Defaults worth knowing

  • Output format defaults to all, so you get XML and JSON, named bom by default in the build directory.
  • The schema version is 1.7 in current releases. The plugin version decides it, with older plugin lines producing older schemas.
  • Compile, provided, runtime and system scopes are included. Test scope is not, since includeTestScope is false.
  • Attached BOMs get the cyclonedx classifier during install or deploy. Set cyclonedx.skipAttach=true to stop that.
  • You can exclude modules, for example with excludeArtifactId or excludeTestProject.

Which one should you ship?

For a service, ship the aggregate or the BOM for the deployable module. For a library, ship each published module's BOM, since consumers get modules, not your whole reactor. Match the SBOM to what is actually delivered.

Common pitfalls

  • Generating the SBOM in a pipeline stage that runs before dependencies are resolved for every module.
  • Forgetting that excluded modules, such as test projects, will not appear.
  • Shipping a root SBOM for a library whose consumers only receive one module.
  • Leaving the generated file in the build directory and never storing it. An SBOM nobody can find during an incident does not help.

Check the result

  1. Open the JSON and count components.
  2. Confirm a few known dependencies are present.
  3. Confirm each component has a package URL, see how to write a purl.
  4. Store it with the build artifact.
  5. Remember that a shaded jar needs extra attention, see shaded JARs.

Try it yourself

Click through it at your own pace, or open it full screen.

Look at the component list and its versions. That is the same shape of data an SBOM carries.

Where Safeguard fits

Safeguard ingests and analyses SBOMs and scans dependencies directly. The free scan is a quick comparison against your generated file.

Quick check

Is test scope in the SBOM?

Not by default. IncludeTestScope is false.

Where does the file end up?

By default in the build directory as bom.xml and bom.json, and attached as an additional artifact with the cyclonedx classifier on install or deploy.

Sources

Last reviewed: 19 October 2026.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.