3、SPDX标准详解:SPDX文档结构、SPDX字段、SPDX 2.2 vs 2.3 差异、使用spdx-tools生成SPDX

聊到SBOM,就绕不开SPDX。这个标准在软件供应链安全领域,基本就是“通用语言”般的存在。我记得刚接触SBOM那会儿,面对一堆XML和JSON格式的文档,说实话有点懵。但后来摸透了SPDX的结构,发现它其实设计得非常清晰——说白了,就是一张标准化的“物料清单”,只不过记录的是软件组件。

3.1 SPDX文档结构:一张清单长什么样?

一个标准的SPDX文档,你可以把它想象成一个三层结构的文件夹:

  • 文档头(Document Creation Info):谁创建的?什么时候?用什么工具?版本号多少?
  • 包信息(Packages):清单里到底有哪些组件?每个组件的名称、版本、下载地址、校验和。
  • 关系(Relationships):这些组件之间是什么关系?A依赖B?B是A的补丁?

嗯,这里要注意:SPDX文档本身也是一个“包”,它有自己的SPDX ID。这个设计我一开始觉得有点绕,但后来发现它让文档可以互相引用,非常灵活。

核心概念:SPDX文档的根节点是 SPDXRef-DOCUMENT,所有包、文件、片段都通过 SPDXRef-xxx 格式的ID进行引用。这是整个文档的“锚点”。

下面这张图,是我个人习惯用来给团队新人讲解SPDX结构的,你可以看看它怎么把零散的信息串起来:

SPDX文档结构图 SPDX 文档结构 Document Creation Info Packages Relationships Package A (v1.0) Package B (v2.3) Package C (v1.5) A DEPENDS_ON B B CONTAINS C A GENERATED_FROM D 每个节点都有唯一的 SPDXRef 标识符

3.2 核心字段详解:creationInfo、packages、relationships

你想想看,一个SBOM如果只有组件列表,没有创建者信息,也没有组件间的关系,那它就是个半成品。SPDX用三个核心字段解决了这个问题。

3.2.1 creationInfo:文档的“身份证”

这个字段记录了文档本身的元数据。我在项目中遇到过好几次,因为creationInfo里没写清楚工具版本,导致审计时被质疑SBOM的可信度。所以,我建议你养成习惯,把下面这些信息填完整:

字段 说明 示例
created 文档创建时间(UTC) 2025-03-15T10:30:00Z
creators 创建者(人/组织/工具) Tool: spdx-tools-0.8.2
licenseListVersion 使用的许可证列表版本 3.21
comment 可选备注 Generated for release v2.0

我的习惯:在creators里同时写上“Person: 你的名字”和“Tool: 生成工具名及版本”。这样既有人工审核痕迹,又有自动化工具背书,审计时少很多麻烦。

3.2.2 packages:清单里的“主角”

每个包都是一个独立的实体。SPDX 2.2里,一个包必须包含以下关键信息:

  • SPDXID:唯一标识,比如 SPDXRef-Package-A
  • name:包名,比如 log4j-core
  • versionInfo:版本号,比如 2.17.1
  • downloadLocation:下载地址,没有就用 NOASSERTION
  • checksums:校验和(SHA1、SHA256等),这是安全验证的关键
  • licenseConcluded:你最终认定的许可证
  • copyrightText:版权声明,没有就用 NOASSERTION

嗯,这里有个坑:licenseConcluded 和 licenseDeclared 的区别。前者是你分析后得出的结论,后者是包本身声明的。我曾经见过一个项目直接把 licenseDeclared 当结论用,结果漏掉了包内嵌的其他许可证——这问题在审计时被揪出来,挺尴尬的。

3.2.3 relationships:组件间的“关系网”

光有包列表还不够,你得告诉别人这些包是怎么关联的。SPDX定义了几十种关系类型,常用的有:

关系类型 含义 示例
DEPENDS_ON A依赖B my-app DEPENDS_ON log4j
CONTAINS A包含B docker-image CONTAINS nginx
GENERATED_FROM A由B生成 binary GENERATED_FROM source
BUILD_TOOL_OF A是B的构建工具 maven BUILD_TOOL_OF my-app
PATCH_FOR A是B的补丁 hotfix PATCH_FOR log4j

避坑指南:我曾经在生成SBOM时,把所有依赖都写成了 DEPENDS_ON,结果审计方追问“这个测试依赖也是运行时依赖吗?”——后来我学会了区分 DEPENDS_ON、TEST_DEPENDENCY_OF 和 DEV_DEPENDENCY_OF。关系越精确,SBOM的价值越高。

3.3 SPDX 2.2 vs 2.3:差异在哪里?

SPDX 2.3是2022年发布的,相比2.2,它主要补了几个关键短板。我直接给你列个对比表,这样更清楚:

特性 SPDX 2.2 SPDX 2.3
安全参考 不支持 新增 externalRef 安全类别,可引用CVE、CVSS
包验证 仅支持SHA1/SHA256 新增SHA512、BLAKE2b、BLAKE3等算法
关系类型 约20种 新增 DATA_OF、ANCESTOR_OF、VARIANT_OF 等
生命周期 无明确支持 新增 lifecycle 字段,支持构建、测试、部署阶段
AI/ML模型 不支持 新增 AI、ML 包类型

我个人觉得,2.3最大的亮点是安全参考。以前你要在SBOM里关联CVE,得自己加注释,现在直接用 externalRef 就能搞定。举个例子:

{
  "externalRefs": [
    {
      "referenceCategory": "SECURITY",
      "referenceType": "http://spdx.org/rdf/references/cve",
      "referenceLocator": "CVE-2021-44228"
    }
  ]
}

你想想看,有了这个,安全团队扫描SBOM时,直接就能知道哪个组件有已知漏洞,效率提升不是一点半点。

3.4 使用spdx-tools生成SPDX

理论说完了,咱们来点实际的。spdx-tools是SPDX官方维护的Java工具,支持2.2和2.3。我一般用它来做两件事:一是从现有项目生成SBOM,二是验证别人给的SBOM格式对不对。

3.4.1 安装与基本用法

spdx-tools是Java写的,所以你得先装JDK 8+。然后直接下载jar包:

wget https://github.com/spdx/tools-java/releases/download/v1.1.6/spdx-tools-1.1.6-jar-with-dependencies.jar

生成一个简单的SPDX 2.3文档,命令如下:

java -jar spdx-tools-1.1.6-jar-with-dependencies.jar \
  -in /path/to/your/project \
  -out output.spdx.json \
  -format JSON \
  -specVersion 2.3

嗯,这里要注意:-in 参数指向的是项目根目录,工具会自动扫描常见的包管理文件(pom.xml、package.json、go.mod等)。

3.4.2 验证SBOM

我经常收到合作方发来的SBOM,但格式对不对心里没底。这时候就用spdx-tools的验证功能:

java -jar spdx-tools-1.1.6-jar-with-dependencies.jar \
  -verify \
  -in output.spdx.json

如果输出 SPDX document is valid,那就放心了。如果有错误,它会告诉你具体哪个字段有问题。

我的经验:在CI/CD流水线里,我会加一步spdx-tools验证。只要SBOM验证不通过,构建就失败。这招逼着团队把SBOM质量提上来了,审计时基本没出过问题。

3.4.3 从代码生成更精确的SBOM

如果你需要更精细的控制,可以用spdx-tools的Java API。下面是一个生成单个包的例子:

SpdxDocument document = new SpdxDocumentImpl(
    new SpdxDocument("SPDXRef-DOCUMENT"));
document.setCreationInfo(new SpdxCreationInfo(
    Arrays.asList(new SpdxCreator("Tool: spdx-tools-1.1.6")),
    new DateTime(new Date())));
document.setSpecVersion("SPDX-2.3");

SpdxPackage pkg = new SpdxPackageImpl(
    new SpdxPackage("SPDXRef-Package-A"));
pkg.setName("log4j-core");
pkg.setVersionInfo("2.17.1");
pkg.setDownloadLocation("https://repo1.maven.org/...");
pkg.addChecksum(new SpdxChecksum(
    SpdxChecksumAlgorithm.SHA256,
    "a1b2c3d4e5f6..."));

document.getPackages().add(pkg);
document.getRelationships().add(
    new SpdxRelationship(
        document.getSpdxDocument(),
        pkg,
        SpdxRelationshipType.DESCRIBES));

// 输出为JSON
SpdxWriterFactory.getInstance()
    .getWriter(SpdxWriterFactory.SpdxWriterType.JSON)
    .write(document, new FileOutputStream("output.spdx.json"));

这段代码虽然看着长,但逻辑很清晰:创建文档、加包、加关系、输出。我在自动化生成SBOM的脚本里,就是用这套API批量处理几百个组件的。

避坑指南:生成SBOM时,downloadLocation 和 checksums 一定要填。我曾经见过一个SBOM,所有包的下载地址都是 NOASSERTION,校验和也是空的——这种SBOM基本等于废纸,因为没人能验证你用的到底是哪个版本的包。

好了,SPDX的核心内容就这些。从文档结构到字段细节,再到版本差异和工具实操,你照着这个思路去落地,基本不会跑偏。记住一点:SBOM不是生成出来就完事了,它是要给别人看的、要经得起审计的。所以,每个字段都认真对待,别留坑。