Native Image 中的软件物料清单 (SBOM)

GraalVM Native Image 可以在构建时组装软件物料清单 (SBOM),以检测可能容易受到已知安全漏洞影响的任何库。Native Image 提供了 --enable-sbom 选项,用于将 SBOM 嵌入到原生可执行文件中(仅在 Oracle GraalVM 中可用)。除了嵌入之外,SBOM 还可以添加到类路径中或通过使用 --enable-sbom=classpath,export 导出为 JSON 文件。

支持 CycloneDX 格式,并且它是默认格式。要将 CycloneDX SBOM 嵌入到原生可执行文件中,请将 --enable-sbom 选项传递给 native-image 命令。

该实现通过恢复原生可执行文件中包含的类的外部库清单中可观察到的所有版本信息来构建 SBOM。SBOM 会被压缩以限制其对原生可执行文件大小的影响。SBOM 以 gzip 格式存储,其中导出的 sbom 符号引用其起始地址,sbom_length 符号引用其大小。

提取 SBOM 内容 #

将压缩的 SBOM 嵌入到映像中后,有两种可能的方法来提取 SBOM 内容

Native Image Inspect 工具 #

Native Image Inspect 工具 能够使用 --sbom 参数提取压缩的 SBOM,该参数可从可执行文件和共享库中访问

native-image-inspect --sbom <path_to_binary>

它以 JSON 格式输出内容

{
  "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"
}

Syft #

Syft 是由 Anchore 开发的一个开源工具,用于为容器映像和文件系统生成 SBOM。此外,它还可以提取嵌入式 SBOM,并以其原生 Syft 格式和 CycloneDX 格式呈现。得益于 GraalVM 团队的贡献,syft 可以从为 Linux、macOS 或 Windows 构建的原生映像中提取嵌入式 SBOM。

在原生可执行文件上运行 syft scan 以提取整个 SBOM 内容

syft scan <path_to_binary> -o cyclonedx-json

要仅列出其中包含的 Java 库,请运行

syft <path_to_binary>

它会输出类似以下内容的列表

NAME               VERSION       TYPE
Oracle GraalVM     25+12-LTS     graalvm-native-image
collections        25+12-LTS     java-archive
commons-validator  1.9.0         java-archive
json               20211205      java-archive
...

启用安全扫描 #

您可以利用生成的 SBOM 与安全扫描解决方案集成。有各种工具可以帮助检测和缓解应用程序依赖项中的安全漏洞。

一个例子是 Oracle 的 应用程序依赖管理 (ADM)。将您的 SBOM 提交到 ADM 漏洞扫描器时,它会识别应用程序依赖项并标记包含已知安全漏洞的依赖项。ADM 依赖于社区来源的漏洞报告,包括国家漏洞数据库 (NVD)。它还集成了 GitHub Actions、GitLab 和 Jenkins Pipelines。

另一个流行的命令行扫描器是 grype,它是 Anchore 软件供应链管理平台 的一部分。使用 grype,您可以检查 SBOM 中列出的库是否具有 Anchore 数据库中记录的已知漏洞。native-image-inspect 工具的输出可以直接馈送到 grype,以使用以下命令扫描有漏洞的库

native-image-inspect --sbom <path_to_binary> | grype

它会产生以下输出

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

生成的报告随后可用于更新可执行文件中的任何有漏洞的依赖项。

自动化扫描 #

将安全扫描集成到您的 CI/CD 工作流中从未如此简单。随着 GraalVM GitHub Action 中提供了 SBOM 支持,您生成的 SBOM 可以使用 GitHub 的依赖项提交 API 自动提交和分析。它支持

  • 使用 GitHub 的 Dependabot 进行漏洞跟踪。
  • 使用 GitHub 的依赖项图进行依赖项跟踪。

此集成有助于确保您的应用程序在整个开发生命周期中持续监控漏洞。

依赖树 #

SBOM 通过其 dependencies 字段提供组件关系信息。此依赖信息来源于 Native Image 的静态分析调用图。分析依赖图可以帮助您理解为什么应用程序中包含特定组件。例如,在 SBOM 中发现意外组件后,可以通过依赖图追溯其包含情况,以识别应用程序的哪些部分正在使用它。

借助 GraalVM GitHub Action,您可以访问 GitHub 的依赖图功能。

使用 Maven 获取更准确的 SBOM #

为了生成更准确的 SBOM,请考虑使用 GraalVM Native Image 的 Maven 插件。此插件与 Native Image 集成,以改进 SBOM 的创建。

该插件通过使用 cyclonedx-maven-plugin 创建一个“基线”SBOM。基线 SBOM 定义了哪些包名称属于一个组件,这有助于 Native Image 将类与其各自的组件关联起来——当使用混淆或胖 JAR 时,这对 native-image 工具来说可能是一项挑战。在这种协作方法中,Native Image 还能更积极地修剪组件和依赖项,以生成最小化的 SBOM。

这些增强功能从插件版本 0.10.4 开始可用,并且在使用 --enable-sbom 选项时默认启用。

在 SBOM 中包含类级别元数据 #

使用 --enable-sbom=class-level 会向 SBOM 组件添加类级别元数据。此元数据包括作为原生可执行文件一部分的 Java 模块、类、接口、记录、注解、枚举、构造函数、字段和方法。此信息对于以下方面很有用

  • 高级漏洞扫描:当某个漏洞受影响的类或方法作为 CVE 的一部分发布时,可以检查类级别元数据以确定具有受影响 SBOM 组件的原生可执行文件是否确实存在漏洞,从而降低漏洞扫描的误报率。
  • 理解映像内容:快速浏览和搜索类级别元数据,以检查原生可执行文件中包含的内容。

包含类级别元数据会大幅增加 SBOM 大小。对于此 Micronaut Hello World Rest 应用程序,嵌入时 SBOM 大小为 1.1 MB,导出时为 13.7 MB。不含类级别元数据的 SBOM 嵌入时为 3.5 kB,导出时为 64 kB。不含嵌入式 SBOM 的原生映像大小约为 52 MB。

请注意,Syft 不支持包含类级别元数据,因为包含此元数据的嵌套组件字段会从提取的 SBOM 中删除。此限制仅影响提取的 SBOM 中元数据的可见性;它不影响漏洞扫描功能。

数据格式 #

CycloneDX 规范允许通过嵌套具有父子关系的组件来使用分层表示。它以以下方式在 SBOM 组件中嵌入类级别信息

[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

每个 SBOM 组件都在 components 字段中列出其 Java 模块。每个模块都由其名称标识,并在 components 字段中列出其 Java 源文件。每个源文件都由其相对于组件源目录的路径标识,并在 properties 字段中列出其类、接口、记录、注解、枚举、字段、构造函数和方法。

考虑一个包含 mymodule 中一个 Java 源文件的简单组件示例

// 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;
    }
}

类级别 SBOM 组件将如下所示

{
    "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"
                        }
                    ]
                }
            ]
        }
    ]
}

下表指定了类级别元数据的格式

种类 CycloneDX 对象 type name value 备注
模块 组件 模块名称 - 未命名模块的 nameunnamed module
源文件 组件 文件 相对于 src 目录的路径 - .java 结尾,/ 分隔符,路径来源于包名
属性 - class 完全限定名称 包括匿名类、内部类和密封类
接口 属性 - interface 完全限定名称 -
记录 属性 - record 完全限定名称 -
注解 属性 - annotation 完全限定名称 -
字段 属性 - field className.fieldName:fieldType 字段声明
构造函数 属性 - constructor className(paramType1, paramType2) 参数类型以逗号加空格分隔
方法 属性 - method className.methodName(paramType1, paramType2):returnType 带有参数和返回类型的方法

一些补充说明

  • 数组类型以后缀 [] 结尾。例如,字符串数组变为 java.lang.String[]
  • 不包括合成生成的 Lambda 类。

当使用混淆或胖 JAR 时,类级别元数据有时无法准确地与组件关联。发生这种情况时,所有未解析的元数据将收集到一个占位符组件中

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

联系我们