Practical guide to generating SBOMs for Java and JVM applications using Maven, Gradle, Spring Boot, and common security workflows.

Updated:

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, or build.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=true

Advanced 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=false

Gradle 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:cyclonedxBom

Gradle 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" cyclonedxBom

Framework-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: true

Jakarta 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.json

SPDX 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-json

CI/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/sbom

GitLab 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: true

Docker 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.json

4. 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:

  1. Generate SBOM before shading to capture all dependencies
  2. Use Syft to scan the shaded JAR: syft jar:my-app-shaded.jar
  3. Configure shade plugin to generate a dependency-reduced POM
  4. Consider using application packaging (Spring Boot, Quarkus) instead of shading
Q: What about dependencies from private Maven repositories?

A: Private repositories (Nexus, Artifactory, GitHub Packages) require authentication configuration. The SBOM plugins respect Maven/Gradle authentication settings, but you may need to:

  1. Configure settings.xml (Maven) or gradle.properties (Gradle) with credentials
  2. Use environment variables for CI/CD: MAVEN_USERNAME, MAVEN_PASSWORD
  3. Include repository metadata in SBOM for traceability
  4. Consider mirroring public dependencies to your private repository for consistency
Q: How do I generate SBOMs for legacy Java projects (Java 6/7)?

A: Legacy Java projects present challenges but can still generate SBOMs:

  1. Use older plugin versions that support your Java version
  2. Run SBOM generation with newer JDK but target older version
  3. Use Syft or other scanners that analyze JARs directly
  4. For Ant projects, consider migrating to Maven/Gradle or use filesystem scanning
  5. 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 using makeBom instead of makeAggregateBom or incorrect configuration. Solutions:
  1. Use makeAggregateBom goal for multi-module projects
  2. Configure plugin only in parent POM with false
  3. Set true
  4. Check for dependency management vs dependencies confusion
Q: Can I generate SBOMs for WAR/EAR files?

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.txt
Q: How do I handle provided scope dependencies (servlet-api, lombok)?

A: Provided scope dependencies are tricky because they're present at compile time but not packaged:

  1. Include them in SBOM: -DincludeProvidedScope=true
  2. Document runtime environment separately
  3. For containers, scan the base image to capture provided dependencies
  4. 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:

  1. OWASP Dependency Check: mvn org.owasp:dependency-check-maven:check
  2. Snyk: snyk test --file=pom.xml
  3. GitHub Security: Automatic scanning if SBOM in repository
  4. Grype: grype sbom:bom.json
  5. Commercial tools: Sonatype Nexus IQ, JFrog Xray
Q: What about Log4j and similar embedded vulnerabilities?

A: Some vulnerabilities hide in shaded JARs or transitive dependencies:

  1. Use deep scanning: syft dir:. --scope all-layers
  2. Search for specific vulnerable classes
  3. Use specialized scanners like log4j-detector
  4. Regularly regenerate and scan SBOMs
  5. Implement runtime application security monitoring
Q: How do I handle license compliance for Java dependencies?

A: Java projects often mix various licenses requiring careful management:

  1. Generate license report: mvn license:license-list
  2. Include license text in SBOM: -DincludeLicenseText=true
  3. Check for incompatible licenses (GPL vs proprietary)
  4. Use license scanning tools like FOSSA or Black Duck
  5. 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

  1. Large dependency trees causing memory issues
# Increase Maven memory
   export MAVEN_OPTS="-Xmx2g -XX:MaxMetaspaceSize=512m"
   mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
  1. Missing transitive dependencies
# Analyze dependency tree
   mvn dependency:tree -Dverbose

   # Force dependency resolution
   mvn dependency:resolve-sources
   mvn org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
  1. 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>
  1. Gradle build issues
# Clear Gradle cache
   ./gradlew clean build --refresh-dependencies

   # Generate with debug info
   ./gradlew cyclonedxBom --debug

Conclusion

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:

  1. Integrate with security scanning: Combine SBOM generation with OWASP Dependency Check, Snyk, or Grype for continuous vulnerability monitoring
  2. Automate dependency updates: Use Dependabot or Renovate to automatically update vulnerable dependencies
  3. Implement policy gates: Create automated checks that fail builds when critical vulnerabilities or banned licenses are detected
  4. Monitor runtime behavior: Use tools like JFrog Xray or Sonatype Nexus IQ for runtime dependency analysis
  5. Share with stakeholders: Provide SBOMs to customers, security teams, and compliance officers as part of your software delivery

---

Last updated: July 2, 2026 Reading time: 20 minutes Expertise level: Beginner to Advanced