Supply Chain Security: SBOMs for Java Applications

SBOMs identify software components in an application. Generate SBOMs for Java to enable vulnerability scanning, license checks and risk analysis.

An imaginative chemistry lab specialized in component analysis on coffee.

Software supply chain security has never been more critical, and protecting our systems from bad actors and vulnerabilities is a constant challenge. Do you have complete visibility and transparency for all the libraries and dependencies in your Java applications?

Knowing precisely every software component included in an application is paramount to performing operations such as vulnerability scanning and license compliance. How can you secure a software system if you don't know what is inside?

Software Bill of Materials (SBOM) is "a nested inventory for software, a list of ingredients that make up software components” (NTIA). In this article, I'll explain why we want to use SBOMs as part of our supply chain security strategy and what standards are available in the industry.

Then, I'll show you how to generate SBOMs for Java applications at different stages of the software lifecycle, explaining the pros and cons of each technique. You'll see how to create SBOMs for Java source code, JAR artifacts, GraalVM native executables, and container images. I'll also describe how to make SBOM generation part of your builds with Gradle or Maven. Let's get started!

πŸš€
The source code for the examples included in this article is available on GitHub.

Why SBOMs?

SBOMs are a critical piece of the puzzle when designing a supply chain security strategy. An SBOM "identifies and lists software components, information about those components, and supply chain relationships between them" (NTIA). For example, for each software component, an SBOM can include a version number, license descriptors, supplier details, hash digests, whether it's a transitive dependency, and more.

This inventory document enables scanners to check each component for integrity, known security vulnerabilities, compliance with license policies, and newer versions available. Data from SBOMs can also be fed into enterprise processes for asset management and procurement. Those use cases are some of the reasons why we want SBOMs.

Furthermore, SBOMs are recommended by our industry's main security frameworks and processes. They are mandatory for software supplied to US federal agencies per the US President’s Executive Order (EO) 14028 - Improving the Nation's Cybersecurity. And they will soon be required for software providers to comply with the EU Cyber Resilience Act.

In this article, I'll focus on how to generate high-quality SBOMs for Java applications. Future articles will dive deeper into how to use them to address several use cases in a supply chain security strategy. For example, I'll discuss vulnerability scanning with Trivy and continuous supply chain risk management with Dependency Track.

Data Formats for SBOMs

We want to structure an SBOM document so that automated systems can process its data. CycloneDX and SPDX are the two most widely used machine-readable formats for SBOMs.

CycloneDX is "a full-stack Bill of Materials (BOM) standard that provides advanced supply chain capabilities for cyber risk reduction". The specification supports SBOMs and other use cases such as VEX (Vulnerability Exploitability eXchange) and VDR (Vulnerability Disclosure Reports), which I will cover in a future article about vulnerability management. It also supports various types of BOMs, such as SaaSBOM (Software-as-a-Service Bill of Materials), KBOM (Kubernetes Bill of Materials), and ML-BOM (Machine Learning Bill of Materials). CycloneDX is an OWASP Flagship Project and is currently pursuing international Ecma standardization.

SPDX (Software Package Data Exchange) is "an open standard for communicating software bill of material information, including provenance, license, security, and other related information". SPDX 3.0, released in April 2024, extended support to more use cases besides SBOMs, such as VEX and other types of BOMs. The specification is backed by the Linux Foundation and is recognized as an international standard ISO/IEC 5962:2021.

Both formats are used in the industry and are extensively supported in the supply chain security ecosystem. I won't enter into the discussion about which format to adopt as it depends on the intended use case, the specific needs of your organization, and the integration possibilities with your current systems. Tools exist to convert from one format to the other.

In this article, I'll use CycloneDX as it's currently better supported in the Java ecosystem. Whenever possible, I'll include tips on how to convert the examples to using SPDX.

What makes a good SBOM?

After choosing a standard format for defining an SBOM, it's time to discuss what makes a good SBOM. Many factors can influence the quality of an SBOM.

The information included for each component in the inventory determines which use cases the SBOM can be used for. If you want to use SBOMs for license compliance, each component must include the details about its license. If you must comply with the US President’s Executive Order (EO) 14028 - Improving the Nation's Cybersecurity, a set of minimum elements must be present in each SBOM.

The depth of details included in the final SBOM for an application is influenced by when the SBOM is generated with respect to the software lifecycle. The tool used to create an SBOM also impacts the quality of the final results. In the following sections, I'll guide you through different scenarios to generate SBOMs at various stages (source code, build, executable) using a few different tools.

Evaluating the quality of an SBOM can be challenging because of the many ways it can be generated and used. The community behind the OWASP Software Component Verification Standard project published a BOM Maturity Model, which provides "a formalized structure in which bill of materials can be evaluated for a wide range of capabilities". The model also supports profiles to organize BOM information into categories related to a use case (such as application security, license, and procurement) and validate its compliance.

Furthermore, tools are emerging that can scan your SBOMs and score them based on specific criteria. For example, you can use sbomqs (SBOM Quality Score), an experimental tool part of the SBOM Benchmark project.

Generating SBOMs for Java Artifacts

Suppose you have already assembled a Java artifact (JAR or container image). In that case, you can generate an SBOM using a tool to scan the artifact and search for the software packages inside.

Creating an SBOM at this stage of the software development lifecycle should guarantee visibility into all the components of the final application artifact, including the dependencies needed at runtime, such as OS libraries and Java runtime (for container images). The result heavily depends on the tool used to generate the SBOM. How many components can it find?

Even though a good tool might identify (almost) all the software components included in the application artifact, the depth of information for each might be lacking. Is that bad? It depends. For example, SBOMs generated at this stage could be a good fit for vulnerability scanning purposes. In contrast, they might miss the necessary information for verifying license compliance or tracking the relationships among dependencies. Based on your needs, consider if it makes sense to move the SBOM generation to an earlier step in the software lifecycle (as I'll present later) to address use cases not possible at this stage.

For a third-party application, I recommend asking the vendor to ship the software with an SBOM rather than generating one yourself from their artifact. Based on your country and industry, that might even be (or soon become) a requirement by law. If it's an open source project, consider using the techniques learned from this article and submit a pull request to add SBOM generation.

Generating SBOMs for JAR files

Generating an SBOM for a Java application packaged as a JAR artifact requires a tool that supports finding and extracting information from JARs. Syft is a popular open-source tool for generating SBOMs and works with JAR artifacts.

You can check the documentation on how to install the tool on your machine. On macOS and Linux, you can use the Homebrew package manager.

brew install syft

Next, download the Git repository containing all the examples from this article and navigate to the application we'll use in this section. The demo is based on Spring Boot, but everything covered in this article equally applies to any Java application, independently from the framework.

git clone https://github.com/ThomasVitale/supply-chain-security-java
cd sbom/cyclonedx/gradle

Then, package the demo application as a JAR artifact. The project is configured with Java 21. If you don't have that installed, check out SDKMAN to do so.

./gradlew bootJar
πŸš€
Using SDKMAN, installing Java 21 is as simple as running 'sdk install java 21-tem'.

Finally, you can use Syft to generate an SBOM for the application starting from the JAR artifact.

syft build/libs/demo-sbom-cdx-gradle-1.0.jar -o cyclonedx-json --file bom-syft.cdx.json

Look at the generated SBOM (bom-syft.cdx.json) and familiarize yourself with its structure compliant with the CycloneDX format.

πŸ’‘
To generate an SBOM using the SPDX format, replace '-o cyclonedx-json' with '-o spdx-json'.

Generating SBOMs for container images

Generating an SBOM for a Java application packaged as a container image requires a tool that supports finding and extracting information from JARs and recognizes the OS packages and the Java runtime. Syft supports this use case, too.

Using the same application from the previous example, go ahead and package it as a container image. Make sure you have Podman or Docker running before proceeding. Under the hood, Spring Boot relies on Cloud Native Buildpacks for building a production-grade container image without needing any Dockerfile.

./gradlew bootBuildImage
πŸ’‘
If you work with Quarkus, you have the option to use the Cloud Native Buildpacks integration. First, configure the builder image in your properties file: 'quarkus.buildpack.jvm-builder-image=paketobuildpacks/builder-jammy-tiny'. Then, you can run './gradlew build -Dquarkus.container-image.build=true' command to build the image.

Then, you can use Syft to generate an SBOM for the application starting from the container image.

syft demo-sbom-cdx-gradle:1.0 -o cyclonedx-json --file bom-oci-syft.cdx.json
πŸ’‘
To generate an SBOM using the SPDX format, replace '-o cyclonedx-json' with '-o spdx-json'.

If you look at the generated SBOM (bom-oci-syft.cdx.json) you should see the same Java dependencies as before, but also a list of all the packages that Syft could find in the Ubuntu filesystem inside the image.

🧡
Scanners like Syft are trained to find software components following specific rules and patterns. If a component deviates from those, it will not be found. In August 2022, Chainguard founder Dan Lorenc discovered that when scanning the official container image for Node.js to generate an SBOM, no result was found for a Node.js component. The reason was that it was installed in a non-standard way, so scanners could not find it. Because of that, any vulnerability scanning operation on that image would not show any known security vulnerabilities associated with that specific version of Node.js.

SBOMs with Cloud Native Buildpacks

In the previous section, you used the bootBuildImage Gradle task from Spring Boot to containerize a Java application. Under the hood, Spring Boot uses Cloud Native Buildpacks, a specification to transform application source code into a container image without needing a Dockerfile

More specifically, Spring Boot uses the Paketo Buildpacks implementation, which generates an SBOM for each layer of the built image using Syft. When you need to create an SBOM for a container image built using Buildpacks, you can extract the autogenerated SBOMs from the image rather than doing it explicitly as you did earlier.

If you want to extract the SBOMs generated by Buildpacks, install the pack CLI. You can check the documentation on how to install the tool on your machine. On macOS and Linux, you can use the Homebrew package manager.

brew install buildpacks/tap/pack

Then, use pack to extract the SBOMs from each layer of the image.

pack sbom download demo-sbom-cdx-gradle:1.0 -o sbom-layers

You'll find the SBOMs in a sbom-layers directory. In the following example, you can see that one or more SBOMs are available in different formats for each layer.

$ tree sbom-layers

sbom-layers
└── layers
    └── sbom
        └── launch
            β”œβ”€β”€ buildpacksio_lifecycle
            β”‚   └── launcher
            β”‚       β”œβ”€β”€ sbom.cdx.json
            β”‚       β”œβ”€β”€ sbom.spdx.json
            β”‚       └── sbom.syft.json
            β”œβ”€β”€ paketo-buildpacks_bellsoft-liberica
            β”‚   └── helper
            β”‚       └── sbom.syft.json
            β”‚   └── jre
            β”‚       └── sbom.syft.json
            β”œβ”€β”€ paketo-buildpacks_ca-certificates
            β”‚   └── helper
            β”‚       └── sbom.syft.json
            β”œβ”€β”€ paketo-buildpacks_executable-jar
            β”‚   β”œβ”€β”€ sbom.cdx.json
            β”‚   └── sbom.syft.json
            β”œβ”€β”€ paketo-buildpacks_spring-boot
            β”‚   └── helper
            β”‚       └── sbom.syft.json
            β”‚   └── spring-cloud-bindings
            β”‚       └── sbom.syft.json

That is a convenient feature of Buildpacks that should lead to higher-quality SBOMs than those generated after the image build. However, extracting the SBOMs from the image is cumbersome and a bit challenging to integrate with other tools in the supply chain security ecosystem. Furthermore, its behavior could be more consistent. Different buildpacks produce SBOMs in different formats. The only constant in the example above is the Syft format, which would require an explicit conversion to CycloneDX or SPDX for integration scenarios.

For more information about SBOM support in Cloud Native Buildpacks, you can check the Buildpacks and Paketo documentation.

SBOMs with JReleaser

JReleaser is a powerful tool to orchestrate software releases and provides many useful features for Java projects. Among other things, JReleaser can orchestrate the generation of an SBOM for you. It does so based on the final application artifact (JAR or container image) and using either Syft or the CycloneDX CLI

You can check out the project document to learn how to configure this feature. Explaining JReleaser would deserve its own article. I won't say much here, but I recommend checking out this tool.

Generating SBOMs for Java Source Code

If you have access to the source code of a Java application, you can generate an SBOM using a tool that will scan the project and search for the software packages referenced inside.

Creating an SBOM at this stage of the software development lifecycle should guarantee visibility into all the components of the application project, including the dependencies needed for building and testing, such as annotation processors and test libraries. The result heavily depends on the tool used to generate the SBOM, but we expect it to be more detailed than the previous strategy because the tool has full access to the source code.

SBOMs generated at this stage can be a good fit for license compliance purposes and for finding known vulnerabilities affecting any components used within a software project. In contrast, they might miss the dependencies needed at runtime, which is usually the case for applications distributed as container images. Furthermore, these SBOMs typically include more than what is included in the final artifact (such as the test dependencies), making them less suitable for use cases such as procurement.

As mentioned earlier, SBOMs can be tailored to the specific use case. Based on the capability you need, consider creating SBOMs at different stages of the software lifecycle and using them to fulfill specific requirements.

The CycloneDX project provides a tool named cdxgen that can get complete visibility into all the dependencies used in a Java project, supporting both Gradle and Maven. That's what we're going to use.

Check the documentation on how to install cdxgen on your machine. On macOS and Linux, you can use the Homebrew package manager.

brew install cdxgen

You can try this strategy on the application included in the Git repository for this article (sbom/cyclonedx/gradle). Use cdxgen to generate an SBOM for the application starting from the project source code.

FETCH_LICENSE=true cdxgen -o bom-cdxgen.cdx.json --spec-version 1.5

Look at the generated SBOM (bom-cdxgen.cdx.json) and compare it with the one created previously with Syft. Do you notice any differences?

πŸ’‘
To generate an SBOM using the SPDX format, you can use Trivy with the command 'trivy fs --format spdx-json --output bom-trivy.spdx.json .'. For Maven projects, that will work out of the box. For Gradle projects, you'll need to enable the dependency version locking feature, or Trivy will not know how to find the Java dependencies. 

In the Git repository for this article, there is also a sample Java application using Maven (sbom/cyclonedx/maven) that you can use to test the SBOM generation with cdxgen. The result will be very similar to the Gradle example.

The cdxgen tool is quite powerful. It works with multiple languages and ecosystems, and even provides dedicated options via profiles to generate SBOMs based on the use case (application security, research, operations, threat modeling, and license compliance). You can find more information on the project's website.

πŸ’‘
With profiles, cdxgen can generate SBOMs tailored and optimized for a specific use case. For example, if you need the SBOM for research purposes, you can run this command: 'cdxgen -o bob-research.cdx.json --spec-version 1.5 --profile research', which will enable deep and evidence mode. Try to compare the results with the SBOMs you have generated so far. Do you notice any differences?

Generating SBOMs for Java Builds

After discussing options for generating SBOMs from application artifacts (JARs, container images) and source code, let's now investigate how to make the generation process part of the build lifecycle. For Java applications, you need a tool to hook into the build process and extract information about every component used to assemble the software.

Creating an SBOM at this stage of the software development lifecycle should guarantee visibility into all the components used to build the application. Since the generation process hooks into the build lifecycle, you can get complete control over which components to include and a more in-depth result than any other strategy.

SBOMs generated at this stage can be a good fit for most use cases, such as vulnerability scanning, license compliance, and procurement. You typically can tailor the operation to include only certain types of dependencies in the final SBOM (useful, for example, if you want to exclude the test dependencies). In contrast, these SBOMs might miss the dependencies needed at runtime, which is usually the case for applications distributed as container images.

recommend including the SBOM generation as part of the Java build lifecycle because it results in a higher-quality and more detailed document, enabling many capabilities needed in supply chain risk management. Then, based on your specific context and requirements, you might want to complement the resulting document with SBOMs generated in other stages and tailored to use cases not possible as part of the build lifecycle.

The CycloneDX project offers two convenient plugins for Gradle and Maven to make the SBOM generation process part of the build lifecycle. That's what we're going to use.

Generating SBOMs for Java with Gradle

Let's explore how to generate SBOM for Java applications using the CycloneDX Gradle plugin. You can try this strategy on the application included in the Git repository for this article (sbom/cyclonedx/gradle).

First, add the plugin to your build.gradle file.

plugins {
    id 'org.cyclonedx.bom' version '1.8.2'
}

The plugin adds a cyclonedxBom task to generate an SBOM for the application. Let's try it out.

./gradlew cyclonedxBom

By default, the SBOM is generated in build/reports/bom.json. You can compare the result with the SBOMs created previously in other stages. Do you notice any differences?

πŸ’‘
If you're using Spring Boot 3.3+, the SBOM will be generated in build/reports/application.cdx.json by default.

You can customize the plugin based on your needs. For example, let's configure it to use the CycloneDX schema version 1.5 with JSON. You can also make it part of the build step so that every time you build the application, an SBOM is generated automatically.

cyclonedxBom {
    projectType = "application"
    outputFormat = "json"
    schemaVersion = "1.5"
}
tasks.build.finalizedBy 'cyclonedxBom'
πŸ’‘
If you're using Spring Boot 3.3+, the SBOM will be generated automatically when you package the application as a JAR or container image. You don't need to configure the task explicitly.

The plugin provides various customizations, including which dependencies you want to include in the final SBOM (runtime, compile, test). You can also pass additional metadata about your application, such as contact details about your organization and the license used for the project. Check out the plugin documentation to explore all the available configurations.

πŸ’‘
To generate an SBOM using the SPDX format, you can use the SPDX Gradle Plugin.

Generating SBOMs for Java with Maven

Let's explore how to generate SBOM for Java applications using the CycloneDX Maven plugin. You can try this strategy on the application included in the Git repository for this article (sbom/cyclonedx/maven).

First, add the plugin to your pom.xml file.

<build>
    <plugins>
        <plugin>
            <groupId>org.cyclonedx</groupId>
            <artifactId>cyclonedx-maven-plugin</artifactId>
            <version>2.8.0</version>
        </plugin>
    </plugins>
</build>
πŸ’‘
If you're using Spring Boot 3.3+, you don't need to specify a version for the CycloneDX Maven Plugin because it's provided by Spring Boot.

The plugin adds a makeAggregateBom goal to generate an SBOM for the application. Let's try it out.

./mvnw cyclonedx:makeAggregateBom

By default, the SBOM is generated in target/bom.json. You can compare the result with the SBOMs created previously in other stages. Do you notice any differences?

πŸ’‘
If you're using Spring Boot 3.3+, the SBOM will be generated in target/classes/META-INF/sbom/application.cdx.json by default.

You can customize the plugin based on your needs. For example, let's configure it to use the CycloneDX schema version 1.5 with JSON. You can also make it part of the build step so that every time you build the application, an SBOM is generated automatically.

<build>
    <plugins>
        <plugin>
            <groupId>org.cyclonedx</groupId>
            <artifactId>cyclonedx-maven-plugin</artifactId>
            <version>2.8.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>makeAggregateBom</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <projectType>application</projectType>
                <outputFormat>json</outputFormat>
                <schemaVersion>1.5</schemaVersion>
            </configuration>
        </plugin>
    </plugins>
</build>
πŸ’‘
If you're using Spring Boot 3.3+, the SBOM will be generated automatically when you package the application as a JAR or container image. You don't need to configure the task explicitly.

The plugin provides various customizations, including which dependencies you want to include in the final SBOM (runtime, compile, test). You can also pass additional metadata about your application, such as contact details about your organization and the license used for the project. Check out the plugin documentation to explore all the available configurations.

πŸ’‘
To generate an SBOM using the SPDX format, you can use the SPDX Maven Plugin.

Generating SBOMs for GraalVM Native Executables

In this last section, I'll cover one additional option for generating SBOMs when compiling Java applications to native executables with GraalVM.

The image-native mode of GraalVM offers several benefits, such as instant startup time, instant peak performance, and reduced memory consumption. From a security standpoint, native executables compiled with GraalVM also have a reduced surface of attack compared to standard JVM applications. The reason is that GraalVM compiles only the achievable paths in the code starting from the main() method. Anything else is excluded from the final artifact.

I'll elaborate more on the additional security capabilities GraalVM offers in a future article about vulnerability management. For now, I want to point out that the final artifact will not have any JAR files, which means that scanners cannot find any component when analyzing the native executable using regular techniques.

When working with native executables, we don't have the option to scan the final artifact for SBOM generation directly. Instead, we need to move the generation step to an earlier stage of the software development lifecycle. Both the strategies I described before for source code and build lifecycle work, but they might include a superset of the actual dependencies that get compiled into the final executable.

For a more accurate result, the Oracle GraalVM distribution offers an experimental SBOM generation feature when compiling the application. This feature uses Syft under the hood and only supports the CycloneDX format.

The quality of the SBOM generated as part of the GraalVM compilation is comparable to the first strategy I addressed in this article. For example, it's suitable for vulnerability management scenarios but not for license compliance use cases because the necessary information is likely missing from the final result.

Before moving on to the example, ensure you have installed Oracle GraalVM and set it as your default Java version and distribution within your project. You can do so with SDKMAN.

sdk install java 21-graal
sdk use java 21-graal

Generating SBOMs for GraalVM with Gradle

Let's explore how to generate SBOMs for native applications using GraalVM with Gradle. You can try this strategy on the application included in the Git repository for this article (sbom/cyclonedx/gradle-native).

First, configure the GraalVM Gradle plugin in your build.gradle file to enable the SBOM generation feature.

graalvmNative {
    binaries {
        configureEach {
            buildArgs.add("--enable-sbom=cyclonedx,export")
        }
    }
}

Next, compile the application to a native executable.

./gradlew nativeCompile

As part of the native compilation process, an SBOM is generated using Syft and following the CycloneDX format. A copy of the SBOM is included in the artifact itself, and because we added the export flag in the configuration, it's also exported to build/native/nativeCompile/demo-sbom-cdx-gradle-native.sbom.json. You can inspect the exported SBOM or even extract the one embedded in the native executable using the Native Image Inspection Tool.

πŸ’‘
You cannot generate an SBOM using the SPDX format. Oracle GraalVM supports CycloneDX only.

Generating SBOMs for GraalVM with Maven

Let's now explore how to generate SBOMs for native applications using GraalVM with Maven. You can try this strategy on the application included in the Git repository for this article (sbom/cyclonedx/maven-native).

First, configure the GraalVM Maven plugin in your pom.xml file to enable the SBOM generation feature.

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <configuration>
                <buildArgs combine.children="append">
                    <buildArg>--enable-sbom=cyclonedx,export</buildArg>
                </buildArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

Next, compile the application to a native executable.

./mvnw -Pnative native:compile

As part of the native compilation process, an SBOM is generated using Syft and following the CycloneDX format. A copy of the SBOM is included in the artifact itself, and because we added the export flag in the configuration, it's also exported in target/demo-sbom-cdx-maven.sbom.json. You can inspect the exported SBOM or even extract the one embedded in the native executable using the Native Image Inspection Tool.

πŸ’‘
You cannot generate an SBOM using the SPDX format. Oracle GraalVM supports CycloneDX only.

Conclusion

This article introduced the concept of Software Bill of Materials (SBOM) and why it's essential for software supply chain security. I covered the main strategies for generating SBOMs for Java applications at three different stages of the software development lifecycle: source code, build, and artifacts. I also discussed dedicated integrations available in Gradle, Maven, and GraalVM.

Creating SBOMs is only the beginning. The real value comes from actively using SBOMs to manage our software's supply chain security risks. In future articles, I'll discuss how to distribute SBOMs as part of your build pipeline and how to use them for vulnerability scanning, license compliance, dependency management, and other use cases. Stay tuned!

References

Cover image generated with Stockimg.AI.

Latest update: April 25, 2024