SBOM (软件物料清单) 是什么 如何上传和解析CycloneDX/SPDX格式的XML

SBOM是软件供应链必需的成分表,CycloneDX/SPDX XML解析失败主因是非标准扩展、缺失命名空间或版本混用;应使用cyclonedx-python-lib安全解析并严格遵循v1.4/v1.5规范。

SBOM 不是附加功能,而是现代软件供应链里必须有的“成分表”——它明确列出项目所用的开源组件、版本、许可证、已知漏洞等信息。CycloneDX 和 SPDX 是当前主流的两种 SBOM 格式,XML 是它们都支持的序列化方式,但解析和上传时容易因命名空间、schema 版本或元素嵌套层级出错。

为什么 CycloneDX/SPDX 的 XML 解析常失败

根本原因不是格式本身复杂,而是实际生成的 XML 常含非标准扩展、缺失必要命名空间声明,或混用不同 spec 版本(如 CycloneDX v1.4 与 v1.5 的 bom 根元素属性不兼容)。常见报错包括:

  • Element 'bom' does not carry attribute 'serialNumber'(v1.4 要求该属性,v1.5 已移除)
  • cvssScore is not a valid value for cvssV3Score(字段名大小写或命名空间前缀错位)
  • Python xml.etree.ElementTree 读取时直接抛 ParseError: no element found(因 XML 声明后多空行或 BOM 字节)

用 Python 安全解析 CycloneDX XML

别直接用原生 xml.etree,优先用官方库 cyclonedx-python-lib,它内置 schema 校验和版本适配逻辑:

from cyclonedx.parser import XmlParser
from cyclonedx.model.bom import Bom

确保文件无 BOM,且是 UTF-8 编码

with open('sbom.xml', 'rb') as f: bom = XmlParser().parse(f)

获取所有组件名称和版本

for component in bom.components: print(f"{component.name} @{component.version}")

关键点:

  • 必须用 open(..., 'rb') 二进制模式读取,避免编码干扰
  • 若遇到 UnsupportedVersionException,检查 XML 中 version 属性是否为整数(如 version="1.4" 合法,version="1.4.0" 非法)
  • 自定义字段(如 backend)需通过 component.get_property_value('internal:team')

    访问,不能硬编码找子节点

上传 CycloneDX/SPDX XML 到依赖扫描平台

GitHub Dependabot、GitLab Dependency Scanning、Syft + Grype 都接受 XML,但上传路径和参数差异大:

  • GitHub:把 sbom.xml 提交到仓库根目录,再在 .github/workflows/sbom-scan.yml 中调用 actions/upload-artifact@v4,**不是**直接 POST 到 API
  • GitLab:需在 .gitlab-ci.yml 中用 artifacts:reports:dependency_scanning 指向 XML 路径,且文件名必须为 gl-dependency-scanning-report.xml
  • 本地工具链(Syft + Grype):syft -o cyclonedx-xml dir:/path > sbom.xml 生成后,grype sbom:./sbom.xml 可直接解析——注意 sbom: 前缀不能省略

SPDX XML 上传更受限:目前 GitHub/GitLab 原生不支持 SPDX XML 扫描,需先用 spdx-tools 转成 JSON-LD 或 tag-value 格式再传。

手动生成合规 CycloneDX XML 的最小要点

不要手动拼 XML 字符串。用 cyclonedx-python-lib 构建对象再序列化:

from cyclonedx.model import LicenseChoice, Component, Property
from cyclonedx.output import XmlOutput

c = Component( name='requests', version='2.31.0', purl='pkg:pypi/requests@2.31.0' ) c.licenses = LicenseChoice(license_expression='Apache-2.0')

bom = Bom() bom.metadata.component = c bom.components.add(c)

输出严格符合 v1.4 规范的 XML

output = XmlOutput(bom, schema_version=14) with open('sbom.xml', 'wb') as f: f.write(output.output_as_string().encode('utf-8'))

重点:

  • schema_version=14 必须是整数,对应 CycloneDX v1.4;写 1.4 会报错
  • purl 字段必须完整(含 pkg: 前缀),否则生成的 XML 缺失 节点,被多数扫描器忽略
  • 生成后务必用 curl -X POST https://demo.cyclonedx.org/api/v1/bom -F 'file=@sbom.xml' 测试能否被在线校验器接受——这是最简验证手段

真正难的不是生成或上传,而是让每个构建环节(CI、打包、发布)自动产出带完整许可证和哈希值的 SBOM,并确保不同工具链之间 component.bom_refpurl 保持一致。一旦某处用了缩写 pkg:npm/express,另一处用了全量 pkg:npm/express@4.18.2,关联分析就断了。