Java SBOM Guide for Maven, Gradle, and Spring Boot
Quick Answer
For Java and JVM applications, generate the SBOM from the build tool first: use the CycloneDX Maven plugin for Maven projects, the CycloneDX Gradle plugin for Gradle projects, and container scanning for the final deployed image. Validate the generated file with the SBOM Validator before sending it to vulnerability management, procurement, or customer review workflows.Introduction: The Java Dependency Ecosystem
Java applications, with their mature and extensive ecosystem spanning over 25 years, present unique challenges for Software Bill of Materials generation. The Java Virtual Machine (JVM) ecosystem includes not just Java but also Kotlin, Scala, Groovy, and Clojure, each potentially contributing dependencies to your application. A typical enterprise Java application might include 200-500 direct and transitive dependencies from Maven Central, corporate repositories, and third-party vendors.
The complexity of Java dependency management stems from several factors. First, the transitive dependency resolution in tools like Maven and Gradle can pull in hundreds of JAR files, each with their own dependencies. Second, Java's binary compatibility means applications often run with different library versions than they were compiled against. Third, the prevalence of application servers and runtime containers adds another layer of dependencies that may not be captured in build files.
Generating accurate SBOMs for Java applications is now a practical requirement for enterprise vulnerability management, customer reviews, and regulated delivery workflows. Incidents like Log4Shell showed how one dependency deep in the tree can create broad exposure. This guide focuses on generating high-quality Java SBOMs, validating them with the SBOM Validator, and fitting them into normal build and release processes.Why SBOMs Matter for Java Applications
Understanding Java's Complex Dependency Landscape
Java's dependency management is more complex than most ecosystems due to its enterprise nature and long history. Unlike interpreted languages where dependencies are typically loaded at runtime, Java applications are compiled and packaged with specific dependency versions, creating a complex web of compile-time and runtime requirements.
Java applications typically include multiple dependency scopes that serve different purposes:
- Direct dependencies: Libraries explicitly declared in
pom.xml,build.gradle, orbuild.sbt. These are the libraries your code directly imports and uses. - Transitive dependencies: Dependencies of your dependencies, often making up 80-90% of your total dependencies. Maven and Gradle automatically resolve these, but conflicts can arise when different libraries require different versions of the same dependency.
- Test dependencies: JUnit, Mockito, TestNG, and other testing frameworks that shouldn't be included in production deployments but could introduce vulnerabilities during the build process.
- Provided dependencies: Application server libraries (Tomcat, Jetty, JBoss) that are present at runtime but not packaged with your application. These are often overlooked in SBOM generation but can contain critical vulnerabilities.
- Runtime dependencies: JDBC drivers, logging implementations, and other libraries loaded dynamically at runtime. These might not be detected by static analysis.
- Plugin dependencies: Build-time tools and annotation processors that run during compilation. While not deployed, they can introduce supply chain risks.
This creates one of the most complex software supply chains in modern development, where a single Spring Boot project can easily have 200+ dependencies. The challenge is compounded by Java's culture of backward compatibility, which means applications often run with library versions released years apart, each with different security profiles.
Build Tool Support
Understanding Java Build Tools
The Java ecosystem offers several build tools, each with different approaches to dependency management and SBOM generation. Maven, with its declarative XML configuration, has been the standard for over a decade. Gradle, with its flexible Groovy/Kotlin DSL, has gained popularity for its performance and flexibility. Understanding these tools' different approaches is crucial for accurate SBOM generation.
Maven Projects
Maven is the most popular build tool for Java projects, managing over 10 million artifacts in Maven Central. Its declarative approach and strict dependency scoping make it ideal for SBOM generation. Maven's dependency mediation and conflict resolution rules are well-defined, allowing SBOM tools to accurately predict which versions will be used at runtime.
Maven's strength for SBOM generation lies in its comprehensive dependency information. Each artifact in Maven Central includes not just the JAR file but also metadata about licenses, developers, and source repositories. This rich metadata enables detailed SBOMs that go beyond simple package lists:
CycloneDX Maven Plugin
The CycloneDX Maven plugin has become the de facto standard for Java SBOM generation, with deep integration into Maven's build lifecycle. Unlike generic SBOM tools, it understands Maven's dependency resolution, scope management, and multi-module structures. The plugin can generate SBOMs at different build phases, capturing different aspects of your application:
The plugin integrates seamlessly into your existing Maven workflow, generating SBOMs as part of your normal build process:
<!-- Add to pom.xml -->
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<projectType>application</projectType>
<schemaVersion>1.7</schemaVersion>
<includeBomSerialNumber>true</includeBomSerialNumber>
<includeCompileScope>true</includeCompileScope>
<includeProvidedScope>true</includeProvidedScope>
<includeRuntimeScope>true</includeRuntimeScope>
<includeSystemScope>true</includeSystemScope>
<includeTestScope>false</includeTestScope>
<includeLicenseText>false</includeLicenseText>
<outputReactorProjects>true</outputReactorProjects>
<outputFormat>all</outputFormat>
<outputName>bom</outputName>
</configuration>
</plugin># Generate SBOM
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# Generate with all scopes including test
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DincludeTestScope=true
# Generate only for specific module
mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom -pl my-module
# Generate with license text
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DincludeLicenseText=trueAdvanced Maven Configuration
Beyond basic SBOM generation, Maven projects often require advanced configuration to handle enterprise requirements. This includes filtering certain dependencies, adding organizational metadata, and controlling output formats for different consumers. The CycloneDX plugin provides extensive configuration options to meet these needs:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.2</version>
<configuration>
<!-- Project metadata -->
<projectType>application</projectType>
<schemaVersion>1.7</schemaVersion>
<!-- Output options -->
<outputDirectory>${project.build.directory}</outputDirectory>
<outputName>sbom</outputName>
<outputFormat>all</outputFormat>
<!-- Scope inclusion -->
<includeCompileScope>true</includeCompileScope>
<includeProvidedScope>true</includeProvidedScope>
<includeRuntimeScope>true</includeRuntimeScope>
<includeSystemScope>true</includeSystemScope>
<includeTestScope>false</includeTestScope>
<!-- Enhanced metadata -->
<includeBomSerialNumber>true</includeBomSerialNumber>
<includeLicenseText>true</includeLicenseText>
<includeCompileScope>true</includeCompileScope>
<!-- Multi-module support -->
<outputReactorProjects>true</outputReactorProjects>
<!-- Component filtering -->
<excludeTypes>
<excludeType>pom</excludeType>
</excludeTypes>
<!-- Custom metadata -->
<metadata>
<supplier>
<name>Your Company</name>
<url>https://yourcompany.com</url>
</supplier>
</metadata>
</configuration>
</plugin>Maven Multi-Module Projects
Multi-module Maven projects, common in enterprise Java development, present unique challenges for SBOM generation. Each module might have its own dependencies, and modules often depend on each other. The challenge is deciding whether to generate individual SBOMs per module or an aggregate SBOM for the entire project. The answer depends on how your modules are deployed:
For microservices deployed independently, generate separate SBOMs. For monolithic applications, use aggregate SBOMs:
<!-- Parent pom.xml -->
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.2</version>
<inherited>false</inherited>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
<configuration>
<outputReactorProjects>true</outputReactorProjects>
<outputName>aggregate-bom</outputName>
</configuration>
</plugin># Generate aggregate SBOM for all modules
mvn clean verify
# Generate individual SBOMs for each module
mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom -DoutputReactorProjects=falseGradle Projects
Gradle is increasingly popular, especially for Android and modern Java projects.
CycloneDX Gradle Plugin
// build.gradle
plugins {
id 'org.cyclonedx.bom' version '3.2.4'
}
cyclonedxBom {
// Skip configurations that don't make sense to include in the BOM
skipConfigs = ["compileClasspath", "testCompileClasspath"]
// Include only these configurations
includeConfigs = ["runtimeClasspath"]
// Skip these projects in a multi-project build
skipProjects = [":some-project", ":some-other-project"]
// Destination for the BOM
destination = file("build/reports")
// Name of the BOM files (without extension)
outputName = "bom"
// Format of the BOM (XML, JSON, or ALL)
outputFormat = "all"
// Schema version
schemaVersion = "1.7"
// Include license text
includeLicenseText = false
// Include BOM serial number
includeBomSerialNumber = true
}# Generate SBOM
./gradlew cyclonedxBom
# Generate with specific format
./gradlew cyclonedxBom -PoutputFormat=json
# Generate for specific subproject
./gradlew :my-module:cyclonedxBomGradle Kotlin DSL
// build.gradle.kts
plugins {
id("org.cyclonedx.bom") version "3.2.4"
}
cyclonedxBom {
skipConfigs.set(listOf("compileClasspath", "testCompileClasspath"))
includeConfigs.set(listOf("runtimeClasspath"))
destination.set(file("build/reports"))
outputName.set("sbom")
outputFormat.set("all")
schemaVersion.set("1.7")
includeLicenseText.set(true)
includeBomSerialNumber.set(true)
}Gradle Multi-Project Build
// Root build.gradle
subprojects {
apply plugin: 'org.cyclonedx.bom'
cyclonedxBom {
includeConfigs = ["runtimeClasspath"]
outputName = "${project.name}-bom"
destination = file("${rootProject.buildDir}/sboms")
}
}
// Task to aggregate all SBOMs
task aggregateSboms {
dependsOn subprojects.collect { it.tasks.cyclonedxBom }
doLast {
println "All SBOMs generated in ${buildDir}/sboms"
}
}SBT (Scala Build Tool)
For Scala and mixed Scala/Java projects.
SBT CycloneDX Plugin
// project/plugins.sbt
addSbtPlugin("org.cyclonedx" % "sbt-cyclonedx" % "2.7.4")// build.sbt
ThisBuild / cyclonedxSchemaVersion := "1.7"
ThisBuild / cyclonedxIncludeBomSerialNumber := true
ThisBuild / cyclonedxIncludeLicenseText := true
ThisBuild / cyclonedxProjectType := "application"
lazy val root = (project in file("."))
.settings(
name := "my-scala-app",
version := "1.0.0",
scalaVersion := "2.13.11",
cyclonedxBomTarget := target.value / "cyclonedx",
cyclonedxBomFilename := "sbom"
)# Generate SBOM
sbt cyclonedxBom
# Generate with specific configuration
sbt "set cyclonedxIncludeTestScope := true" cyclonedxBomFramework-Specific Configurations
Spring Boot Applications
Spring Boot applications often have complex dependency trees with starters and auto-configuration.
Maven with Spring Boot
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.2</version>
<configuration>
<projectType>application</projectType>
<includeProvidedScope>true</includeProvidedScope>
<includeSystemScope>true</includeSystemScope>
<!-- Spring Boot specific -->
<excludeTypes>
<excludeType>pom</excludeType>
</excludeTypes>
<!-- Include Spring Boot fat JAR analysis -->
<analyzers>
<analyzer>jar</analyzer>
</analyzers>
</configuration>
</plugin>Gradle with Spring Boot
plugins {
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
id 'org.cyclonedx.bom' version '3.2.4'
}
cyclonedxBom {
// Include Spring Boot's runtime classpath
includeConfigs = ["runtimeClasspath"]
// Skip Spring Boot development tools
skipConfigs = ["developmentOnly"]
outputName = "spring-boot-sbom"
}
// Task to generate SBOM after building fat JAR
task bootSbom {
dependsOn bootJar, cyclonedxBom
doLast {
// Optionally analyze the fat JAR
exec {
commandLine 'syft', 'packages',
"jar:build/libs/${jar.archiveFileName.get()}",
'-o', 'cyclonedx-json=build/reports/fat-jar-sbom.json'
}
}
}Spring Boot Actuator Integration
// SbomEndpoint.java
@Component
@Endpoint(id = "sbom")
public class SbomEndpoint {
private final ObjectMapper objectMapper = new ObjectMapper();
@ReadOperation(produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Object> sbom() {
try {
// Load SBOM from classpath or file system
InputStream sbomStream = getClass().getResourceAsStream("/sbom.json");
if (sbomStream != null) {
return objectMapper.readValue(sbomStream, Map.class);
}
} catch (IOException e) {
// Handle error
}
return Map.of("error", "SBOM not available");
}
}# application.yml
management:
endpoints:
web:
exposure:
include: health,info,sbom
endpoint:
sbom:
enabled: trueJakarta EE Applications
<!-- Jakarta EE project pom.xml -->
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<configuration>
<projectType>application</projectType>
<!-- Include provided scope for Jakarta EE APIs -->
<includeProvidedScope>true</includeProvidedScope>
<!-- Exclude test scope -->
<includeTestScope>false</includeTestScope>
<!-- Jakarta EE specific metadata -->
<metadata>
<component>
<type>application</type>
<supplier>
<name>Your Organization</name>
</supplier>
<description>Jakarta EE Application</description>
</component>
</metadata>
</configuration>
</plugin>Android Applications
// Android app build.gradle
android {
// ... existing configuration
applicationVariants.all { variant ->
variant.outputs.all { output ->
def variantName = variant.name.capitalize()
task "cyclonedxBom${variantName}" {
dependsOn "assemble${variantName}"
doLast {
// Generate SBOM for specific variant
exec {
commandLine './gradlew', 'cyclonedxBom',
"-PvariantName=${variant.name}"
}
}
}
}
}
}
cyclonedxBom {
// Android-specific configurations
includeConfigs = ["releaseRuntimeClasspath"]
outputName = "android-app-sbom"
// Filter out Android SDK components if needed
excludeTypes = ["aar"]
}Universal Tools
Syft - Universal SBOM Generator
Syft works excellently with Java projects:
# Install Syft
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
# Generate SBOM from JAR file
syft jar:target/myapp-1.0.0.jar -o cyclonedx-json=jar-sbom.json
# Generate from directory (analyzes pom.xml, build.gradle)
syft dir:. -o cyclonedx-json=project-sbom.json
# Generate from fat JAR (Spring Boot, etc.)
syft jar:target/myapp-1.0.0-SNAPSHOT.jar -o spdx-json=fat-jar-sbom.json
# Generate from WAR file
syft java-archive:target/myapp.war -o cyclonedx-json=war-sbom.json
# Generate from running container
syft docker:my-java-app:latest -o cyclonedx-json=container-sbom.jsonSPDX Tools for Java
# Using SPDX Maven plugin
mvn org.spdx:spdx-maven-plugin:createSPDX
# Convert CycloneDX to SPDX
cyclonedx convert --input-file bom.json --output-file sbom.spdx.json --output-format spdx-jsonCI/CD Integration
Jenkins Pipeline
pipeline {
agent any
tools {
maven 'Maven-3.9.4'
jdk 'JDK-17'
}
environment {
MAVEN_OPTS = '-Xmx1024m'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean compile'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
publishTestResults testResultsPattern: 'target/surefire-reports/*.xml'
}
}
}
stage('Package') {
steps {
sh 'mvn package -DskipTests'
}
}
stage('Generate SBOM') {
parallel {
stage('CycloneDX SBOM') {
steps {
sh 'mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom'
}
}
stage('Syft SBOM') {
steps {
script {
// Install Syft if not available
sh '''
if ! command -v syft &> /dev/null; then
curl -sSfL https://get.anchore.io/syft | sh -s -- -b ${WORKSPACE}/bin
export PATH="${WORKSPACE}/bin:$PATH"
fi
# Generate SBOM from built JAR
syft jar:target/*.jar -o cyclonedx-json=target/syft-sbom.json
'''
}
}
}
}
}
stage('SBOM Validation') {
steps {
script {
// Validate generated SBOMs
sh '''
# Check if SBOM files exist and are valid JSON
if [ -f target/bom.json ]; then
echo "Validating CycloneDX SBOM..."
jq empty target/bom.json && echo "✅ CycloneDX SBOM is valid JSON"
# Count components
COMPONENT_COUNT=$(jq '.components | length' target/bom.json)
echo "📊 Found $COMPONENT_COUNT components in CycloneDX SBOM"
fi
if [ -f target/syft-sbom.json ]; then
echo "Validating Syft SBOM..."
jq empty target/syft-sbom.json && echo "✅ Syft SBOM is valid JSON"
fi
'''
}
}
}
stage('Security Scan') {
steps {
script {
// Security scanning with various tools
sh '''
# OWASP Dependency Check
mvn org.owasp:dependency-check-maven:check
# Grype vulnerability scanning
if command -v grype &> /dev/null; then
grype jar:target/*.jar -o table
grype target/bom.json -o json --file target/grype-results.json || true
fi
# OSV Scanner
if command -v osv-scanner &> /dev/null; then
osv-scanner --sbom target/bom.json --format json --output target/osv-results.json || true
fi
'''
}
}
post {
always {
// Archive security reports
archiveArtifacts artifacts: 'target/dependency-check-report.html', allowEmptyArchive: true
archiveArtifacts artifacts: 'target/*-results.json', allowEmptyArchive: true
}
}
}
}
post {
always {
// Archive SBOM files
archiveArtifacts artifacts: 'target/bom.*', fingerprint: true
archiveArtifacts artifacts: 'target/*-sbom.json', fingerprint: true
// Publish HTML reports
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target',
reportFiles: 'bom.json',
reportName: 'SBOM Report'
])
}
success {
script {
if (env.BRANCH_NAME == 'main') {
// Upload SBOM to security platform
sh '''
curl -X POST \
-H "Authorization: Bearer ${SECURITY_API_TOKEN}" \
-F "sbom=@target/bom.json" \
-F "project=${JOB_NAME}" \
-F "version=${BUILD_NUMBER}" \
https://your-security-platform.com/api/v1/sbom
'''
}
}
}
}
}GitHub Actions
name: Java SBOM Generation
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-sbom:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: [11, 17, 21]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn clean compile test package -DskipTests
- name: Generate CycloneDX SBOM
run: |
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# Generate both JSON and XML formats
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DoutputFormat=xml
- name: Install Syft
run: |
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
- name: Generate Syft SBOM
run: |
# Generate from built JAR
syft jar:target/*.jar -o cyclonedx-json=target/syft-sbom.json
# Generate from project directory
syft dir:. -o spdx-json=target/project-spdx-sbom.json
- name: Validate SBOMs
run: |
# Validate JSON format
jq empty target/bom.json
jq empty target/syft-sbom.json
# Basic validation
echo "CycloneDX SBOM components: $(jq '.components | length' target/bom.json)"
echo "Syft SBOM components: $(jq '.artifacts | length' target/syft-sbom.json)"
- name: Security Scanning
run: |
# Install security tools
pip install cyclonedx-cli
# Validate against CycloneDX schema
cyclonedx-cli validate --input-file target/bom.json
# OWASP Dependency Check
mvn org.owasp:dependency-check-maven:check
- name: Upload SBOM Artifacts
uses: actions/upload-artifact@v4
with:
name: sbom-java-${{ matrix.java-version }}
path: |
target/bom.*
target/*-sbom.json
target/dependency-check-report.html
retention-days: 30
- name: Upload to Security Platform
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.SECURITY_API_TOKEN }}" \
-F "sbom=@target/bom.json" \
-F "project=${{ github.repository }}" \
-F "version=${{ github.sha }}" \
https://your-security-platform.com/api/v1/sbomGitLab CI
stages:
- build
- test
- sbom
- security
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
cache:
paths:
- .m2/repository/
- target/
build:
stage: build
image: maven:3.9.4-openjdk-17
script:
- mvn $MAVEN_CLI_OPTS clean compile
artifacts:
expire_in: 1 hour
paths:
- target/
test:
stage: test
image: maven:3.9.4-openjdk-17
dependencies:
- build
script:
- mvn $MAVEN_CLI_OPTS test
artifacts:
reports:
junit:
- target/surefire-reports/TEST-*.xml
expire_in: 1 hour
paths:
- target/
package:
stage: build
image: maven:3.9.4-openjdk-17
dependencies:
- build
- test
script:
- mvn $MAVEN_CLI_OPTS package -DskipTests
artifacts:
expire_in: 1 day
paths:
- target/*.jar
- target/
generate-sbom:
stage: sbom
image: maven:3.9.4-openjdk-17
dependencies:
- package
before_script:
# Install additional tools
- apt-get update && apt-get install -y curl jq
- curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
script:
# Generate CycloneDX SBOM
- mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
- mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DoutputFormat=xml
# Generate Syft SBOMs
- syft jar:target/*.jar -o cyclonedx-json=target/syft-jar-sbom.json
- syft dir:. -o spdx-json=target/project-spdx-sbom.json
# Validate SBOMs
- jq empty target/bom.json
- echo "Generated SBOM with $(jq '.components | length' target/bom.json) components"
artifacts:
expire_in: 1 week
paths:
- target/bom.*
- target/*-sbom.json
reports:
cyclonedx: target/bom.json
security-scan:
stage: security
image: maven:3.9.4-openjdk-17
dependencies:
- generate-sbom
before_script:
- apt-get update && apt-get install -y curl jq python3 pip
- pip install cyclonedx-cli
- curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
script:
# Validate SBOM schema
- cyclonedx-cli validate --input-file target/bom.json
# OWASP Dependency Check
- mvn org.owasp:dependency-check-maven:check
# Vulnerability scanning
- grype target/bom.json -o table
- grype target/bom.json -o json --file target/grype-results.json || true
# License analysis
- jq '.components[] | {name: .name, version: .version, licenses: .licenses}' target/bom.json > target/license-report.json
artifacts:
expire_in: 1 week
paths:
- target/dependency-check-report.html
- target/grype-results.json
- target/license-report.json
allow_failure: trueDocker Integration
Multi-stage Docker Build
# Multi-stage Java build with SBOM generation
FROM maven:3.9.4-openjdk-17 AS builder
WORKDIR /app
# Copy POM files for dependency resolution
COPY pom.xml .
COPY src ./src
# Build application
RUN mvn clean package -DskipTests
# Generate SBOM
RUN mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# Runtime stage
FROM openjdk:17-jre-slim AS runtime
WORKDIR /app
# Install tools for SBOM analysis
RUN apt-get update && apt-get install -y curl && \
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy application JAR and SBOM
COPY --from=builder /app/target/*.jar app.jar
COPY --from=builder /app/target/bom.json sbom.json
# Generate additional container SBOM
RUN syft jar:app.jar -o cyclonedx-json=jar-sbom.json
# Health check that includes SBOM availability
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD test -f sbom.json && test -f jar-sbom.json || exit 1
EXPOSE 8080
# Run application
CMD ["java", "-jar", "app.jar"]Spring Boot Docker Example
# Spring Boot with comprehensive SBOM
FROM maven:3.9.4-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Build and generate SBOM
RUN mvn clean package -DskipTests && \
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
# Runtime stage
FROM openjdk:17-jre-slim
WORKDIR /app
# Install SBOM tools
RUN apt-get update && \
apt-get install -y curl jq && \
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy application
COPY --from=build /app/target/*.jar app.jar
COPY --from=build /app/target/bom.* ./
# Generate runtime SBOM
RUN syft jar:app.jar -o cyclonedx-json=runtime-sbom.json
# Spring Boot health endpoint will include SBOM info
ENV MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,sbom
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Best Practices
Essential Best Practices for Java SBOM Generation
Successful SBOM generation in Java projects requires understanding the nuances of the JVM ecosystem and establishing consistent processes across your development lifecycle. These best practices, developed through experience with enterprise Java applications, ensure accurate, maintainable, and useful SBOMs.
1. Dependency Management
<!-- Use dependency management to control versions -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.1.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Explicitly declare direct dependencies -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Security-critical dependencies with explicit versions -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
</dependency>
</dependencies>2. SBOM Validation
// SbomValidator.java
public class SbomValidator {
private final ObjectMapper objectMapper = new ObjectMapper();
public ValidationResult validateCycloneDxSbom(Path sbomFile) {
try {
JsonNode sbom = objectMapper.readTree(sbomFile.toFile());
ValidationResult result = new ValidationResult();
// Check required fields
result.addCheck("specVersion", sbom.has("specVersion"));
result.addCheck("version", sbom.has("version"));
result.addCheck("serialNumber", sbom.has("serialNumber"));
// Check components
JsonNode components = sbom.get("components");
if (components != null && components.isArray()) {
result.addCheck("hasComponents", components.size() > 0);
// Validate each component
for (JsonNode component : components) {
validateComponent(component, result);
}
}
return result;
} catch (IOException e) {
return ValidationResult.failure("Failed to parse SBOM: " + e.getMessage());
}
}
private void validateComponent(JsonNode component, ValidationResult result) {
String name = component.path("name").asText();
result.addCheck("component_" + name + "_has_type", component.has("type"));
result.addCheck("component_" + name + "_has_version", component.has("version"));
result.addCheck("component_" + name + "_has_purl", component.has("purl"));
}
}3. License Analysis
# Extract license information from SBOM
jq '
.components[] |
select(.licenses != null) |
{
name: .name,
version: .version,
licenses: [.licenses[]?.license?.name // .licenses[]?.license?.id // "Unknown"]
}
' target/bom.json > license-analysis.json
# Check for problematic licenses
jq '
.components[] |
select(.licenses != null) |
select(.licenses[]?.license?.id | test("GPL|AGPL|SSPL"))
' target/bom.json > problematic-licenses.json4. Vulnerability Tracking
// VulnerabilityTracker.java
@Component
public class VulnerabilityTracker {
@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // Daily
public void checkVulnerabilities() {
try {
Path sbomFile = Paths.get("target/bom.json");
if (Files.exists(sbomFile)) {
// Use OSV API to check for vulnerabilities
checkWithOsv(sbomFile);
// Use NVD API
checkWithNvd(sbomFile);
// Send alerts if critical vulnerabilities found
notifySecurityTeam();
}
} catch (Exception e) {
log.error("Failed to check vulnerabilities", e);
}
}
private void checkWithOsv(Path sbomFile) {
// Implementation for OSV vulnerability checking
}
private void checkWithNvd(Path sbomFile) {
// Implementation for NVD vulnerability checking
}
}Frequently Asked Questions (FAQ)
General Java SBOM Questions
Q: Should I use Maven or Gradle plugins for SBOM generation?A: Use the plugin that matches your build tool. The CycloneDX Maven plugin is more mature and feature-complete for Maven projects, while the Gradle plugin offers better integration with Gradle's flexible build system. Both produce compatible SBOMs, but using the native plugin ensures accurate dependency resolution. If you need to generate SBOMs for JARs without source code, use universal tools like Syft.
Q: How do I handle shaded/fat JARs in SBOM generation?A: Shaded JARs (created with Maven Shade Plugin or Gradle Shadow) bundle dependencies directly into the JAR, making standard SBOM generation miss embedded libraries. Solutions:
- Generate SBOM before shading to capture all dependencies
- Use Syft to scan the shaded JAR:
syft jar:my-app-shaded.jar - Configure shade plugin to generate a dependency-reduced POM
- Consider using application packaging (Spring Boot, Quarkus) instead of shading
A: Private repositories (Nexus, Artifactory, GitHub Packages) require authentication configuration. The SBOM plugins respect Maven/Gradle authentication settings, but you may need to:
- Configure settings.xml (Maven) or gradle.properties (Gradle) with credentials
- Use environment variables for CI/CD:
MAVEN_USERNAME,MAVEN_PASSWORD - Include repository metadata in SBOM for traceability
- Consider mirroring public dependencies to your private repository for consistency
A: Legacy Java projects present challenges but can still generate SBOMs:
- Use older plugin versions that support your Java version
- Run SBOM generation with newer JDK but target older version
- Use Syft or other scanners that analyze JARs directly
- For Ant projects, consider migrating to Maven/Gradle or use filesystem scanning
- Document known limitations in SBOM metadata
Tool-Specific Questions
Q: Why does my multi-module Maven project generate duplicate entries? A: This usually occurs when usingmakeBom instead of makeAggregateBom or incorrect configuration. Solutions:
- Use
makeAggregateBomgoal for multi-module projects - Configure plugin only in parent POM with
false - Set
true - Check for dependency management vs dependencies confusion
A: Yes, but it requires special handling:
# For WAR files
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DprojectType=application
# Scan deployed WAR
syft jar:my-app.war -o cyclonedx-json=war-sbom.json
# For EAR files, extract and scan components
jar xf my-app.ear
find . -name "*.jar" -exec syft jar:{} \; > ear-components.txtA: Provided scope dependencies are tricky because they're present at compile time but not packaged:
- Include them in SBOM:
-DincludeProvidedScope=true - Document runtime environment separately
- For containers, scan the base image to capture provided dependencies
- Consider runtime SBOM generation for complete picture
Security and Compliance Questions
Q: How do I identify vulnerable dependencies in my Java SBOM?A: Use multiple tools for comprehensive coverage:
- OWASP Dependency Check:
mvn org.owasp:dependency-check-maven:check - Snyk:
snyk test --file=pom.xml - GitHub Security: Automatic scanning if SBOM in repository
- Grype:
grype sbom:bom.json - Commercial tools: Sonatype Nexus IQ, JFrog Xray
A: Some vulnerabilities hide in shaded JARs or transitive dependencies:
- Use deep scanning:
syft dir:. --scope all-layers - Search for specific vulnerable classes
- Use specialized scanners like log4j-detector
- Regularly regenerate and scan SBOMs
- Implement runtime application security monitoring
A: Java projects often mix various licenses requiring careful management:
- Generate license report:
mvn license:license-list - Include license text in SBOM:
-DincludeLicenseText=true - Check for incompatible licenses (GPL vs proprietary)
- Use license scanning tools like FOSSA or Black Duck
- Document license exceptions and waivers
Troubleshooting
Common Issues and Solutions
Java SBOM generation can encounter various issues due to the complexity of build systems, repository configurations, and dependency resolution. These solutions address the most common problems:
Common Issues
- Large dependency trees causing memory issues
# Increase Maven memory
export MAVEN_OPTS="-Xmx2g -XX:MaxMetaspaceSize=512m"
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom- Missing transitive dependencies
# Analyze dependency tree
mvn dependency:tree -Dverbose
# Force dependency resolution
mvn dependency:resolve-sources
mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom- Plugin version conflicts
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.2</version>
<configuration>
<skip>false</skip>
<failOnError>true</failOnError>
</configuration>
</plugin>- Gradle build issues
# Clear Gradle cache
./gradlew clean build --refresh-dependencies
# Generate with debug info
./gradlew cyclonedxBom --debugConclusion
Generating accurate SBOMs for Java applications is essential for managing the complex dependency ecosystems inherent in JVM-based projects. The mature tooling available for Java - particularly the CycloneDX plugins for Maven and Gradle - provides enterprise-grade SBOM generation capabilities that integrate seamlessly with existing build processes.
The key to successful Java SBOM generation is choosing the right tool for your build system and configuring it to capture all relevant dependencies across different scopes. Whether you're working with legacy Java EE applications, modern Spring Boot microservices, or Android applications, the approaches in this guide provide comprehensive coverage.
As the Java ecosystem continues to evolve with new frameworks like Quarkus and GraalVM native images, SBOM generation practices must adapt. The investment in establishing robust SBOM generation now will pay dividends as supply chain security becomes increasingly critical for enterprise software.
Next Steps
After establishing Java SBOM generation in your projects, consider these advanced steps:
- Integrate with security scanning: Combine SBOM generation with OWASP Dependency Check, Snyk, or Grype for continuous vulnerability monitoring
- Automate dependency updates: Use Dependabot or Renovate to automatically update vulnerable dependencies
- Implement policy gates: Create automated checks that fail builds when critical vulnerabilities or banned licenses are detected
- Monitor runtime behavior: Use tools like JFrog Xray or Sonatype Nexus IQ for runtime dependency analysis
- Share with stakeholders: Provide SBOMs to customers, security teams, and compliance officers as part of your software delivery
Related Resources
- CI/CD SBOM Integration Guide - Automate Java SBOM generation in your pipeline
- Docker SBOM Guide - Generate SBOMs for containerized Java applications
- Kubernetes SBOM Guide - Manage SBOMs for Java microservices in Kubernetes
---
Last updated: July 2, 2026 Reading time: 20 minutes Expertise level: Beginner to Advanced