Create Your First Quarkus SBOM with CycloneDX
Learn where Quarkus writes SBOM files, what is inside them, and how to validate the JSON before you hand it to a scanner.
After ./mvnw package, the file I care about is not the JAR. It is this one:
target/quarkus-run-cyclonedx.jsonThat file is a software bill of materials (SBOM) for the Quarkus application distribution you just built. It is the thing you hand to a scanner, a platform team, a customer, or an auditor when they ask what is inside the service. That conversation is much less fun when all you have is a copied Maven dependency tree and a confident shrug.
Quarkus makes this more interesting than plain Maven because a Quarkus application is not only the dependencies you declared in pom.xml. Extensions have runtime and build-time artifacts. The build augments the application model. The final package has a Quarkus-specific layout. A raw dependency:tree is useful, but it does not fully describe what Quarkus used to build the application or what landed in the final distribution. The Quarkus CycloneDX guide is explicit about that split.
quarkus-cyclonedx fills the gap from inside the Quarkus build. The extension became stable in Quarkus 3.32.2 and sits in the security category. The runnable sbom-demo tree uses Quarkus 3.34.3, because I prefer the article and the code to speak about the same current line.
We build a tiny REST service, generate the distribution SBOM during ./mvnw package, generate a separate dependency SBOM with quarkus:dependency-sbom, inspect the Quarkus-specific metadata, validate both files with the CycloneDX CLI, and archive the result in GitHub Actions. Small app, useful artifact. That is a good trade.
Prerequisites
You need a normal Quarkus setup and enough Maven comfort to read a pom.xml and run build goals. I keep SBOM theory short here; the point is what Quarkus writes and how to prove the files are usable.
Java 21 or newer
A JDK that matches the repository sample if you run the finished
sbom-demotree, currently Java 25Quarkus CLI
Maven, or the Maven wrapper generated by Quarkus
jqfor inspecting JSONCycloneDX CLI for validation
Basic understanding of Quarkus REST applications
Project Setup
Create the project:
quarkus create app com.example:sbom-demo \
--extension='rest,quarkus-cyclonedx' \
--no-code
cd sbom-demoExtensions:
rest- a minimal REST endpoint so the build produces a normal servicequarkus-cyclonedx- CycloneDX SBOM generation from the Quarkus build and application model
If you already have a Quarkus project, add only the SBOM extension:
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-cyclonedx"Your pom.xml should then include:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cyclonedx</artifactId>
</dependency>There is no separate CycloneDX Maven plugin to wire for the distribution path. The extension joins the Quarkus build and writes SBOM output when the application is packaged.
Give Quarkus Something to Package
Create the source packages:
mkdir -p src/main/java/com/example/sbom
mkdir -p src/test/java/com/example/sbomAdd src/main/java/com/example/sbom/GreetingResource.java:
package com.example.sbom;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello sbom";
}
}The endpoint is intentionally boring. We only need a real application artifact so Quarkus can create its standard quarkus-app layout. Adding databases, messaging clients, or other extensions would make the first SBOM bigger, not clearer.
Now add src/main/resources/application.properties:
quarkus.cyclonedx.format=allall gives us JSON and XML for the distribution SBOM. Some tools want JSON, some vendor workflows still want XML, and generating both is cheaper than rebuilding because somebody remembered their importer too late.
Build the application:
./mvnw packageLook at the top of the packaged application:
find target/quarkus-app -maxdepth 2 -type f | sortYou should see output like this:
target/quarkus-app/app/sbom-demo-1.0.0-SNAPSHOT.jar
target/quarkus-app/quarkus-app-dependencies.txt
target/quarkus-app/quarkus-run.jar
target/quarkus-app/quarkus/generated-bytecode.jar
target/quarkus-app/quarkus/quarkus-application.dat
target/quarkus-app/quarkus/transformed-bytecode.jarRuntime libraries sit deeper under lib/boot/ and lib/main/, so that short find command does not print them. Typical paths look like this:
target/quarkus-app/lib/boot/io.quarkus.quarkus-bootstrap-runner-<version>.jar
target/quarkus-app/lib/main/...With quarkus.cyclonedx.format=all, Quarkus also writes these files beside quarkus-app/:
target/quarkus-run-cyclonedx.json
target/quarkus-run-cyclonedx.xmlThat is the first artifact we inspect.
Read the Distribution SBOM
Start with the BOM root:
jq '.metadata.component | {type, name, version, purl}' target/quarkus-run-cyclonedx.jsonFor the default fast-jar layout in current Quarkus 3.34.x, the root is the runnable distribution:
{
"type": "application",
"name": "quarkus-run.jar",
"version": "1.0.0-SNAPSHOT",
"purl": "pkg:generic/quarkus-run.jar@1.0.0-SNAPSHOT"
}That detail is easy to miss. The root of this SBOM is not your Maven artifact by itself. It is the executable distribution Quarkus produced.
Your project JAR is still listed as a component:
jq '.components[] | select(.name == "sbom-demo") | {type, group, name, version, purl}' \
target/quarkus-run-cyclonedx.jsonExpected shape:
{
"type": "library",
"group": "com.example",
"name": "sbom-demo",
"version": "1.0.0-SNAPSHOT",
"purl": "pkg:maven/com.example/sbom-demo@1.0.0-SNAPSHOT?type=jar"
}It is modeled as a library because, in the distribution, it is an on-disk JAR under quarkus-app/app/. The SBOM root tells consumers what launches. The component list still gives scanners Maven coordinates for your code and for the other JARs.
Now inspect a few components:
jq '.components[:5]' target/quarkus-run-cyclonedx.jsonQuarkus adds properties that are more useful than a flat inventory. The one I check first is quarkus:component:scope:
jq '.components[]
| {name, version, properties}
| select(.properties != null)' \
target/quarkus-run-cyclonedx.jsonComponents can be marked as runtime or development. Runtime means the component is part of what you ship. Development means Quarkus used it for build-time work, augmentation, or similar tasks. That distinction is important when a scanner flags something and everyone suddenly becomes very interested in the word “reachable.”
For runtime components in a fast-jar distribution, Quarkus can also record where the file appears:
jq '.components[]
| select(.evidence.occurrences != null)
| {name, locations: [.evidence.occurrences[].location]}' \
target/quarkus-run-cyclonedx.jsonCycloneDX 1.5 introduced evidence.occurrences.location, and Quarkus uses it for distribution locations where applicable. If a component is marked runtime and points to lib/main/..., you have a concrete answer for “is this actually in the thing we deploy?”
Finally, look at the dependency graph:
jq '.dependencies[:10]' target/quarkus-run-cyclonedx.jsonThis is the difference between “we listed some files” and “we can explain the chain.” The components array is the inventory. The dependencies array is the relationship map.
Generate the Dependency SBOM
The distribution SBOM is about the packaged output. Quarkus also gives you a dependency SBOM for the application model before packaging:
./mvnw quarkus:dependency-sbomBy default, the Maven goal writes JSON to:
target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.jsonThis goal has its own user properties. quarkus.cyclonedx.format=all only affects distribution SBOMs. For XML from the dependency goal, use the goal property:
./mvnw quarkus:dependency-sbom -Dquarkus.dependency.sbom.format=xmlThe complete parameter list comes from Maven itself:
./mvnw help:describe -Dcmd=quarkus:dependency-sbom -DdetailCheck what was written:
find target -maxdepth 1 \( -name '*dependency-cyclonedx.json' -o -name '*dependency-cyclonedx.xml' \)The root now describes the Maven application model, not the fast-jar executable:
jq '.metadata.component | {type, group, name, version, purl}' \
target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.jsonExpected shape:
{
"type": "application",
"group": "com.example",
"name": "sbom-demo",
"version": "1.0.0-SNAPSHOT",
"purl": "pkg:maven/com.example/sbom-demo@1.0.0-SNAPSHOT?type=pom"
}That gives you two honest views:
Dependency SBOM - what entered the Quarkus build model
Distribution SBOM - what came out as the packaged application
I use both when supply-chain review is more than a checkbox. The input view is good for pull request review and early scanning. The output view is what I attach to a release.
You can also ask for dev or test mode dependency graphs:
./mvnw quarkus:dependency-sbom -Dquarkus.dependency.sbom.mode=dev
./mvnw quarkus:dependency-sbom -Dquarkus.dependency.sbom.mode=testThose write ...-dev-dependency-cyclonedx.json or ...-test-dependency-cyclonedx.json. Useful when the build behaves differently under test support or dev services and you want the graph to say so instead of guessing later.
Configure Output Formats
For distribution SBOMs, keep the setting in src/main/resources/application.properties:
quarkus.cyclonedx.format=allThe supported values are json, xml, and all. JSON is the default. (Quarkus)
For dependency SBOMs, use the Maven goal parameter or its user property:
./mvnw quarkus:dependency-sbom -Dquarkus.dependency.sbom.format=xmlThat split is slightly annoying, but it is at least explicit: one setting belongs to the build-time extension output, the other belongs to the Maven goal.
Everything under target/ disappears when you run ./mvnw clean. If your CI validates fixed paths, generate the dependency SBOM after every clean build. Do not let yesterday’s file pass today’s build. It is a small lie, but still a lie.
Fast-jar is the default shape used above. If you build an uber-jar:
./mvnw package -Dquarkus.package.jar.type=uber-jarQuarkus writes:
target/<finalName>-runner-cyclonedx.json
target/<finalName>-runner-cyclonedx.xmlFor fast-jar packaging, the files are:
target/quarkus-run-cyclonedx.json
target/quarkus-run-cyclonedx.xmlQuarkus can attach SBOMs as secondary Maven artifacts for package types where that makes sense, and the guide documents the attachSboms flag when you need to disable it.
Validate Before You Archive
Validation is boring in the best way. It catches broken metadata before you store it, publish it, or send it to someone with a spreadsheet and a deadline.
Make sure both JSON files exist:
./mvnw package
./mvnw quarkus:dependency-sbomThe CycloneDX CLI supports schema validation and a few other BOM operations. Install it on macOS:
brew install cyclonedx/cyclonedx/cyclonedx-cliValidate the distribution SBOM:
cyclonedx validate \
--input-file target/quarkus-run-cyclonedx.json \
--input-format json \
--input-version v1_6Validate the dependency SBOM:
cyclonedx validate \
--input-file target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.json \
--input-format json \
--input-version v1_6Expected output:
Validating JSON BOM...
BOM validated successfully.That proves schema validity for the declared CycloneDX version. It does not prove that your dependency choices are good, safe, licensed correctly, or approved by procurement. Sadly, no CLI flag makes all of that go away.
Wire It into CI
Build the application, generate both SBOM views, validate them, and archive them as job artifacts:
name: Build and Generate SBOM
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
cache: maven
- name: Build application and distribution SBOM
run: ./mvnw -B package -DskipTests
- name: Generate dependency SBOM
run: ./mvnw -B quarkus:dependency-sbom
- name: Install CycloneDX CLI
run: |
curl -Lo cyclonedx \
https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-x64
chmod +x cyclonedx
sudo mv cyclonedx /usr/local/bin/
- name: Validate distribution SBOM
run: |
cyclonedx validate \
--input-file target/quarkus-run-cyclonedx.json \
--input-format json \
--input-version v1_6
- name: Validate dependency SBOM
run: |
cyclonedx validate \
--input-file target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.json \
--input-format json \
--input-version v1_6
- name: Archive SBOMs
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: |
target/quarkus-run-cyclonedx.json
target/quarkus-run-cyclonedx.xml
target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.json
retention-days: 90For a stricter pipeline, pin the CycloneDX CLI version instead of downloading latest, pin GitHub Actions by SHA, and publish SBOMs next to your release artifact instead of only storing them as ephemeral CI artifacts. The short workflow above keeps the moving parts visible.
Production Notes
Drift is the first problem
SBOM generation is easy to forget because the file appears under target/ and then quietly disappears on clean. Generate it every build. Archive it with the commit SHA or release version. When an extension changes the graph, you want history, not archaeology.
Scanner findings need scope
When a CVE lands, the first useful question is usually scope. Is the component present? Is it runtime? Is it build-time only? Does it appear under lib/main/ in the distribution? The Quarkus quarkus:component:scope property and evidence.occurrences.location field give you a better starting point than “the string appears somewhere in a report.”
Scope does not excuse everything. A build-time dependency can still matter if your build system is part of the trust boundary. But it is not the same risk as a runtime JAR shipped into production traffic.
Compliance is not a magic sticker
SBOM pressure is real. U.S. Executive Order 14028 pushed federal software supply-chain transparency work and led to NTIA’s SBOM minimum elements. The EU Cyber Resilience Act, Regulation (EU) 2024/2847, also moves product security obligations closer to normal engineering work. (NTIA) (EU)
This Quarkus extension does not make you compliant by itself. It gives you a repeatable artifact that belongs in those conversations. That is already better than manually assembling evidence during an audit. Manual evidence collection is where optimism goes to become a ticket queue.
Attach artifacts where consumers already look
If you publish services or internal libraries through Maven repositories, attaching SBOMs as related artifacts is cleaner than sending files around in chat. Consumers already know how to fetch classifiers. Security tooling already expects files next to builds. Use the repository as the distribution channel when you can.
Final Check
Start the service:
./mvnw quarkus:devIn another terminal:
curl http://localhost:8080/helloExpected output:
hello sbomStop dev mode, then run the build and SBOM checks:
./mvnw package
ls -1 target/quarkus-run-cyclonedx.*
./mvnw quarkus:dependency-sbom
ls -1 target/*dependency-cyclonedx*Expected SBOM files:
target/quarkus-run-cyclonedx.json
target/quarkus-run-cyclonedx.xml
target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.jsonValidate both JSON files:
cyclonedx validate \
--input-file target/quarkus-run-cyclonedx.json \
--input-format json \
--input-version v1_6
cyclonedx validate \
--input-file target/sbom-demo-1.0.0-SNAPSHOT-dependency-cyclonedx.json \
--input-format json \
--input-version v1_6At that point you have a runnable Quarkus app, a distribution SBOM tied to the packaged output, a dependency SBOM tied to the build input model, and validation that both files are structurally valid CycloneDX.
Conclusion
quarkus-cyclonedx is useful because it sits where the Quarkus-specific knowledge lives: inside the build. It can describe the fast-jar distribution, mark runtime and development components, and point to occurrence locations where the format supports it. Pair that distribution view with quarkus:dependency-sbom, validate both, and archive them with the build. The result is not glamorous. It is just evidence you can regenerate. I like that kind of boring.
The runnable code lives in the Main Thread repository: https://github.com/myfear/the-main-thread/tree/main/sbom-demo


