Software Bill of Materials (SBOM) in Native Image

GraalVM Native Image can assemble a Software Bill of Materials (SBOM) at build time to detect any libraries that may be susceptible to known security vulnerabilities. Native Image provides the --enable-sbom option to embed an SBOM into a native executable (only available in Oracle GraalVM). In addition to being embedded, the SBOM can be added to the classpath or exported as a JSON file by using --enable-sbom=classpath,export.

The CycloneDX format is supported and is the default. To embed a CycloneDX SBOM into a native executable, pass the --enable-sbom option to the native-image command.

The implementation constructs the SBOM by recovering all version information observable in external library manifests for classes included in a native executable. The SBOM is compressed to limit the SBOM’s impact on the native executable size. The SBOM is stored in the gzip format with the exported sbom symbol referencing its start address and the sbom_length symbol referencing its size.

After embedding the compressed SBOM into the executable, the Native Image Inspect Tool is able to extract the compressed SBOM using an optional --sbom parameter accessible through $JAVA_HOME/bin/native-image-inspect --sbom <path_to_binary> from both executables and shared libraries. It outputs the SBOM in the following format:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "version": 1,
  "components": [
    {
      "bom-ref": "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final",
      "type": "library",
      "group": "io.netty",
      "name": "netty-codec-http2",
      "version": "4.1.104.Final",
      "purl": "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final",
      "properties": [
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:codec:codec:4.1.76.Final:*:*:*:*:*:*:*"
        },
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:codec:netty-codec-http2:4.1.76.Final:*:*:*:*:*:*:*"
        },
        {
          "name": "syft:cpe23",
          "value": "cpe:2.3:a:codec:netty_codec_http2:4.1.76.Final:*:*:*:*:*:*:*"
        },
        ...
      ]
    },
    ...
  ],
  "dependencies": [
    {
      "ref": "pkg:maven/io.netty/netty-codec-http2@4.1.104.Final",
      "dependsOn": [
        "pkg:maven/io.netty/netty-buffer@4.1.104.Final",
        "pkg:maven/io.netty/netty-codec-http@4.1.104.Final",
        "pkg:maven/io.netty/netty-codec@4.1.104.Final",
        "pkg:maven/io.netty/netty-common@4.1.104.Final",
        "pkg:maven/io.netty/netty-transport@4.1.104.Final"
      ]
    },
    ...
  ],
  "serialNumber": "urn:uuid:51ec305f-616e-4139-a033-a094bb94a17c"
}

Enabling Security Scanning #

You can leverage the generated SBOM to integrate with security scanning solutions. There are a variety of tools to help detect and mitigate security vulnerabilities in your application dependencies.

One example is Application Dependency Management (ADM) from Oracle. When submitting your SBOM to the ADM vulnerability scanner, it identifies application dependencies and flags those containing known security vulnerabilities. ADM relies on vulnerability reports from community sources, including the National Vulnerability Database (NVD). It also integrates with GitHub Actions, GitLab, and Jenkins Pipelines.

Another popular command-line scanner is grype, part of the Anchore software supply chain management platform. With grype, you can check whether the libraries listed in your SBOMs have known vulnerabilities documented in Anchore’s database. The output of the native-image-inspect tool can be fed directly into grype to scan for vulnerable libraries using the following command:

$JAVA_HOME/bin/native-image-inspect --sbom <path_to_binary> | grype

It produces the following output:

NAME                 INSTALLED      VULNERABILITY   SEVERITY
netty-codec-http2    4.1.76.Final   CVE-2022-24823  Medium

The generated report can then be used to update any vulnerable dependencies in your executable.

Automated Scanning #

Integrating security scanning into your CI/CD workflows has never been easier. With SBOM support available in the GraalVM GitHub Action, your generated SBOM can be automatically submitted and analyzed using GitHub’s dependency submission API. It enables:

  • Vulnerability tracking with GitHub’s Dependabot.
  • Dependency tracking with GitHub’s Dependency Graph.

This integration helps ensure that your application is continuously monitored for vulnerabilities throughout the development lifecycle.

Dependency Tree #

The SBOM provides information about component relationships through its dependencies field. This dependency information is derived from Native Image’s static analysis call graph. Analyzing the dependency graph can help you understand why specific components are included in your application. For example, discovering an unexpected component in the SBOM allows for tracing its inclusion through the dependency graph to identify which parts of the application are using it.

With the GraalVM GitHub Action, you get access to GitHub’s Dependency Graph feature.

More Accurate SBOMs with Maven #

To generate more accurate SBOMs, consider using the Maven plugin for GraalVM Native Image. This plugin integrates with Native Image to improve the SBOM creation.

The plugin creates a “baseline” SBOM by using the cyclonedx-maven-plugin. The baseline SBOM defines which package names belong to a component, helping Native Image associate classes with their respective components—a task that can be challenging for the native-image tool when shading or fat JARs are used. In this collaborative approach, Native Image is also able to prune components and dependencies more aggressively to produce a minimal SBOM.

These enhancements are available starting with plugin version 0.10.4 and are enabled by default when the --enable-sbom option is used.

Including Class-Level Metadata in the SBOM #

Using --enable-sbom=class-level adds class-level metadata to the SBOM components. This metadata includes Java modules, classes, interfaces, records, annotations, enums, constructors, fields, and methods that are part of the native executable. This information can be useful for:

  • Advanced vulnerability scanning: When the affected classes or methods of a vulnerability are published as part of a CVE, the class-level metadata can be checked to determine if a native executable with the affected SBOM component is actually vulnerable, thereby reducing the false positive rate of vulnerability scanning.
  • Understanding image contents: Quickly browse and search the class-level metadata to examine what is included in the native executable.

Including class-level metadata increases the SBOM size substantially. For this Micronaut Hello World Rest application, the SBOM size is 1.1 MB when embedded, and 13.7 MB when exported. The SBOM without class-level metadata is 3.5 kB when embedded, and 64 kB when exported. The size of the native image without an embedded SBOM is around 52 MB.

Data Format #

The CycloneDX specification allows the use of a hierarchical representation by nesting components that have a parent-child relationship. It is used to embed class-level information in SBOM components in the following way:

[component] SBOM Component
└── [component] Java Modules
    └── [component] Java Source Files
        ├── [property] Classes
        ├── [property] Interfaces
        ├── [property] Records
        ├── [property] Annotations
        ├── [property] Enums
        ├── [property] Fields
        ├── [property] Constructors
        └── [property] Methods

Each SBOM component lists its Java modules in the components field. Each module is identified by its name and lists its Java source files in the components field. Each source file is identified by its path relative to the component’s source directory and lists its classes, interfaces, records, annotations, enums, fields, constructors, and methods in the properties field.

Consider an example of a simple component containing one Java source file in mymodule:

// src/com/sbom/SBOMTestApplication.java
package com.sbom;

import org.apache.commons.validator.routines.RegexValidator;

public class SBOMTestApplication {
    private static final boolean IS_EMPTY_OR_BLANK = new RegexValidator("^[\\s]*$").isValid(" ");

    public static void main(String[] argv) {
        System.out.println(String.valueOf(IS_EMPTY_OR_BLANK));
        ClassInSameFile someClass = new ClassInSameFile("hello ", "world");
        someClass.doSomething();
    }
}

class ClassInSameFile {
    private final String value1;
    private final String value2;

    ClassInSameFile(String value1, String value2) {
        this.value1 = value1;
        this.value2 = value2;
    }

    String concatenate() {
        System.out.println(value1 + value2);
    }

    // This method is unreachable and will therefore not be included in the SBOM
    String unreachable() {
        return value;
    }
}

The class-level SBOM component would look like this:

{
    "type": "library",
    "group": "com.sbom",
    "name": "sbom-test-app",
    "version": "1.0.0",
    "purl": "pkg:maven/com.sbom/sbom-test-app@1.0.0",
    "bom-ref": "pkg:maven/com.sbom/sbom-test-app@1.0.0",
    "properties": [...],
    "components": [
        {
            "type": "library",
            "name": "mymodule",
            "components": [
                {
                    "type": "file",
                    "name": "com/sbom/SBOMTestApplication.java",
                    "properties": [
                        {
                            "name": "class",
                            "value": "com.sbom.ClassInSameFile"
                        },
                        {
                            "name": "class",
                            "value": "com.sbom.SBOMTestApplication"
                        },
                        {
                            "name": "field",
                            "value": "com.sbom.ClassInSameFile.value1:java.lang.String"
                        },
                        {
                            "name": "field",
                            "value": "com.sbom.ClassInSameFile.value2:java.lang.String"
                        },
                        {
                            "name": "field",
                            "value": "com.sbom.SBOMTestApplication.IS_EMPTY_OR_BLANK:boolean"
                        },
                        {
                            "name": "constructor",
                            "value": "com.sbom.ClassInSameFile(java.lang.String, java.lang.String)"
                        },
                        {
                            "name": "method",
                            "value": "com.sbom.ClassInSameFile.concatenate():java.lang.String"
                        },
                        {
                            "name": "method",
                            "value": "com.sbom.SBOMTestApplication.<clinit>():void"
                        },
                        {
                            "name": "method",
                            "value": "com.sbom.SBOMTestApplication.main(java.lang.String[]):void"
                        }
                    ]
                }
            ]
        }
    ]
}

The following table specifies the format of class-level metadata:

Kind CycloneDX Object type name value Notes
Module Component library module name - Unnamed module’s name is unnamed module
Source File Component file path relative to the src directory - Ends in .java, / separator, path derived from package name
Class Property - class fully qualified name Includes anonymous, inner, and sealed classes
Interface Property - interface fully qualified name -
Record Property - record fully qualified name -
Annotation Property - annotation fully qualified name -
Field Property - field className.fieldName:fieldType Field declaration
Constructor Property - constructor className(paramType1, paramType2) Parameter types comma-space separated
Method Property - method className.methodName(paramType1, paramType2):returnType Method with parameters and return type

Some additional notes:

  • Array types are suffixed with []. For example, an array of strings becomes java.lang.String[].
  • Synthetically generated lambda classes are not included.

When using shaded or fat JARs, the class-level metadata can sometimes not be accurately associated with a component. When this happens, all unresolved metadata gets collected in a placeholder component:

{
    "type": "data",
    "name": "class-level metadata that could not be associated with a component",
    "components": [
      ...
    ]
}

Connect with us