Apache CycloneDX Ant Library 0.2 进阶实战详解

基于 Ivy 多配置工程实测 Apache CycloneDX Ant Library 0.2 的进阶能力:conf/optionalconf/externalconf 与 templateComponent 的依赖解析行为、驱逐模块空指针的零驱逐配方、SOURCE_DATE_EPOCH 逐字节可复现构建、离线 schema 校验与质量门禁脚本、CI 接入示例,以及与 cyclonedx-maven-plugin 2.9.3 的同依赖集对比,命令与回显均实测通过。

Apache CycloneDX Ant LibraryApache CycloneDX Ant Library2026/10/15

Apache CycloneDX Ant Library 0.2 进阶实战详解

目标:在真实多配置 Ivy 工程上把 Apache CycloneDX Ant Library 0.2 的依赖解析、可复现构建、schema 校验门禁与 CI 接入逐一跑通,并用同一份依赖集与 cyclonedx-maven-plugin 2.9.3 正面对比;本文全部命令与回显均实测通过。

前置阅读:《Ubuntu 22.04 安装 Apache CycloneDX Ant Library 指南》《Apache CycloneDX Ant Library 0.2 快速入门与集成示例》《Apache CycloneDX Ant Library 0.2 常用 API 与任务详解》。本文不重复任务属性的默认值速查,聚焦工程化问题。

一、环境信息

主环境(与专栏前几篇一致):

  • 操作系统:Ubuntu 22.04.5 LTS,x86_64
  • JDK:OpenJDK 17.0.20.1
  • 构建工具:Apache Ant 1.10.12、Apache Ivy 2.6.0(位于 antlib 依赖目录)
  • 组件版本:ant-cyclonedx 0.2(org.apache.ant:ant-cyclonedx:0.2),依赖目录 /opt/cyclonedx-antlib/lib
  • 主实验工程:/root/cdx-advanced(Ivy 多配置工程 notes-service)
  • 辅助实验工程:/root/cdx-lab(第二章 2.6 节的复现实验在此进行)

对比环境(第六章使用,一台独立的 Maven 环境):

  • 操作系统:Ubuntu 24.04.5 LTS
  • Maven:3.9.16,插件 cyclonedx-maven-plugin 2.9.3
  • JDK:17.0.20.1(Eclipse Adoptium)
  • 对比工程:/root/mvn-app(com.example:notes-service-mvn:1.0.0)

对比环境确认(实时回显):

head -2 /etc/os-release
PRETTY_NAME="Ubuntu 24.04.5 LTS"
NAME="Ubuntu"
mvn -version | head -3
Apache Maven 3.9.16 (2bdd9fddda4b155ebf8000e807eb73fd829a51d5)
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Eclipse Adoptium, runtime: /opt/java/openjdk

校验工具链(第四、五章使用):

  • Python 3.10.12 + jsonschema 3.2.0(实测 python3 -c "import jsonschema;print(jsonschema.__version__)" 输出 jsonschema 3.2.0)
  • CycloneDX 1.7 官方 schema 三件套,存于 /root/cdx-advanced/schemas/:bom-1.7.schema.json、spdx.schema.json、jsf-0.82.schema.json

💡 说明:jsonschema 版本建议锁定在 3.x。实测脚本使用了 RefResolver(3.x 的标准 API),4.x 起它被标记为弃用,锁定版本可以避免 CI 环境里脚本行为漂移。

二、Ivy 依赖解析深度

本章的主工程是一份三配置 Ivy 描述符(/root/cdx-advanced/ivy.xml,完整内容):

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="com.example" module="notes-service" revision="1.0.0"/>
  <configurations>
    <conf name="compile" description="compile classpath"/>
    <conf name="test" description="test only"/>
    <conf name="optional" description="optional extras"/>
  </configurations>
  <dependencies>
    <dependency org="org.apache.commons" name="commons-configuration2" rev="2.10.1" conf="compile->default">
      <exclude org="org.apache.commons" module="commons-text" conf="compile"/>
    </dependency>
    <dependency org="org.apache.commons" name="commons-text" rev="1.12.0" conf="compile->default"/>
    <dependency org="com.google.code.gson" name="gson" rev="2.10.1" conf="optional->default"/>
    <dependency org="junit" name="junit" rev="4.13.2" conf="test->default"/>
  </dependencies>
</ivy-module>

注意 commons-configuration2 上的 <exclude ... commons-text> 与显式声明的 commons-text 1.12.0——这是 2.6 节「零驱逐配方」的一部分,先记住它的位置。解析 target(build.xml):

<target name="resolve">
  <ivy:resolve/>
  <ivy:retrieve pattern="lib/[artifact]-[revision].[ext]" conf="*"/>
</target>

执行后 lib/ 目录落盘 7 个 jar(实测清单):

ls lib/
commons-configuration2-2.10.1.jar
commons-lang3-3.14.0.jar
commons-logging-1.3.0.jar
commons-text-1.12.0.jar
gson-2.10.1.jar
hamcrest-core-1.3.jar
junit-4.13.2.jar

2.1 conf 选取决定组件范围

同一个 ivyModule,conf="compile" 与 conf="*" 的产物组件数实测对比:

ivyModule conf产物components 数组件清单
conf="compile"notes-service-compile.json4commons-logging 1.3.0、commons-configuration2 2.10.1、commons-lang3 3.14.0、commons-text 1.12.0
conf="*"notes-service-all.json7上表 4 个 + gson 2.10.1、junit 4.13.2、hamcrest-core 1.3

结论:ivyModule 严格按所选 Ivy 配置取组件,测试依赖(test 配置)与可选依赖(optional 配置)只有把 conf 放宽到 * 才会进入 SBOM。SBOM 的完整性由 conf 决定,生产工程至少要把 compile 与运行期配置并进去,是否包含 test 依赖按审计口径决策。

2.2 optionalconf:给可选依赖打 scope 标记

sbom-optional target 使用 conf="*" optionalconf="optional":

<cdx:ivyModule conf="*" optionalconf="optional"/>

产物中 gson 组件实测带上了 "scope": "optional":

{"type": "library", "bom-ref": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "group": "com.google.code.gson", "name": "gson", "version": "2.10.1", "scope": "optional", "licenses": [{"license": {"id": "Apache-2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}}], "purl": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "externalReferences": [{"type": "website", "url": ""}, {"type": "license", "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}]}

其余 6 个组件不带 scope。optionalconf 的语义就是「这个 Ivy 配置里的依赖按 CycloneDX 的 optional scope 落盘」。

2.3 externalconf:外部配置的并入行为

sbom-external target 改用 conf="*" externalconf="test"。产物 components 数同样是 7,清单与 optionalconf 版本完全一致,唯一差异是 gson 不带 scope(实测组件列表中 gson ... scope=None,junit 与 hamcrest 照样进入 SBOM)。

实测对比两个产物文件大小:21042 字节(all,含 "scope": "optional")对 21016 字节(ext,无 scope),差值即一行 scope 字段。externalconf 只决定「并入谁」,不改变 scope 标记;想同时拿到「并入 + 打标」两个效果,conf="*" optionalconf="optional" 即可(optional 配置自然被并入)。

2.4 templateComponent:富化元数据而不动哈希

sbom-template target 给 gson 挂了模板:

<cdx:ivyModule conf="*">
  <cdx:templateComponent name="gson" group="com.google.code.gson">
    <cdx:license licenseId="MIT"/>
    <cdx:externalReference type="website" url="https://github.com/google/gson"/>
  </cdx:templateComponent>
</cdx:ivyModule>

产物中 gson 组件实测(哈希 8 个全部保留,此处省略):license 变为 MIT、website 外部引用变为真实地址,同时 2.2 节那个空 website 引用被替换掉了:

{"type": "library", "bom-ref": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "group": "com.google.code.gson", "name": "gson", "version": "2.10.1", "licenses": [{"license": {"id": "MIT"}}], "purl": "pkg:maven/com.google.code.gson/gson@2.10.1?type=jar", "externalReferences": [{"type": "website", "url": "https://github.com/google/gson"}]}

用法判断:Ivy 元数据缺什么就补什么(自定义 fork 版本的许可证、内部镜像地址等),哈希与版本信息仍由解析结果保证,模板不会覆盖它们。

2.5 哈希来源:retrieve 目录与 Ivy 缓存等价

sbom-noretrieve target 只 ivy:resolve 不 ivy:retrieve,直接生成 SBOM。把它的产物与 retrieve 版本逐组件比对(含全部哈希字节),实测结果:

compile==noretrieve components: True

即不 retrieve 也有哈希——antlib 回退到 Ivy 缓存目录(~/.ivy2/cache/)里取构件计算。工程含义:CI 上即使跳过 retrieve,SBOM 的哈希依然完整;但要注意 retrieve 目录与缓存不一致的罕见场景(手工换过 jar)时,两处来源可能给出不同哈希,保持构建流程单一来源最稳。

2.6 驱逐触发 NPE:复现与零驱逐配方

这是 Ivy 集成最深的坑。构造一个会发生版本仲裁驱逐的场景(/root/cdx-lab/ivy-cfg2.xml,只依赖 commons-configuration2 2.10.1,其传递依赖与显式依赖的 commons-text 版本在仲裁中互相驱逐),运行 ivyModule 时实测报错:

BUILD FAILED
/root/cdx-lab/build-cfg2.xml:80: java.lang.NullPointerException: Cannot invoke "org.apache.ivy.core.module.descriptor.ModuleDescriptor.getModuleRevisionId()" because "md" is null

只要 Ivy 解析报告里存在被驱逐(evicted)的模块,ivyModule 就会触发这个空指针。 排查时先确认是不是驱逐问题:把 Ivy 的 resolve 报告打开(ivy:resolve 后的 report 产物)看有没有 evicted 标记。

两种修复思路的实测结果:

  • 思路一(失败):在描述符里显式声明与传递依赖一致的 commons-text 版本(ivy-cfg2fix.xml,configuration2 2.10.1 + commons-text 1.12.0)。实测仍然同样报错——显式声明统一了最终版本,但被驱逐的旧候选仍留在解析报告里,NPE 照旧:
BUILD FAILED
/root/cdx-lab/build-cfg2fix.xml:80: java.lang.NullPointerException: Cannot invoke "org.apache.ivy.core.module.descriptor.ModuleDescriptor.getModuleRevisionId()" because "md" is null
  • 思路二(成功):在引入传递冲突源的依赖上用 <exclude> 把旧候选挡在解析报告之外,再显式声明需要的版本——即本章开头 ivy.xml 的写法(configuration2 上 exclude commons-text + 顶层显式 commons-text 1.12.0)。实测 sbom-compile 构建成功,产物 4 个组件、全部 8 哈希。

配方模板(可直接抄):

<dependency org="org.apache.commons" name="commons-configuration2" rev="2.10.1" conf="compile->default">
  <!-- 关键:把会引发版本仲裁驱逐的传递依赖挡在报告外 -->
  <exclude org="org.apache.commons" module="commons-text" conf="compile"/>
</dependency>
<dependency org="org.apache.commons" name="commons-text" rev="1.12.0" conf="compile->default"/>

💡 技术拓展:这与第一章备忘的另一个 Ivy 集成坑同源——taskdef 各带 classpath 会把 Ivy 类加载两份导致 ClassCastException(原文与解法见《Apache CycloneDX Ant Library 0.2 常用 API 与任务详解》第七章 Q1)。Ivy 深度集成的两条铁律:全部 taskdef 去内嵌 classpath 改用 ant -lib;解析报告里不许有驱逐模块。

三、可复现构建 SOURCE_DATE_EPOCH

CycloneDX SBOM 有两处「每次构建必变」的字段:随机 serialNumber 与当前时间 metadata.timestamp。antlib 0.2 支持通过环境变量 SOURCE_DATE_EPOCH(Unix 秒)把两者同时固定,这是 reproducible-builds.org 的通用约定。五组实测(工程 /root/cdx-advanced,target sbom-repro,注意按第一章惯例带 -lib):

对照组——不设 SDE:serial 随机(v4 形态)、timestamp 为当前时间:

urn:uuid:00a17322-4951-4838-afc3-1d21194b4555 2026-10-15T08:20:34Z

实验一——SOURCE_DATE_EPOCH=1760486400:

SOURCE_DATE_EPOCH=1760486400 ant -lib /opt/cyclonedx-antlib/lib sbom-repro 2>&1 | grep BUILD
BUILD SUCCESSFUL
python3 -c "import json;d=json.load(open('bom/notes-service-repro.json'));print(d['serialNumber'],d['metadata']['timestamp'])"
urn:uuid:e52aa000-0199-1000-b4d0-663096d8c180 2025-10-15T00:00:00Z

1760486400 正是 2025-10-15T00:00:00Z:timestamp 被固定为 SDE 时刻,serialNumber 变成 v1 形态(第 13 位是 1,时间信息编码进了 UUID 前段)。

实验二——同一 SDE 重复构建,逐字节比对:

md5sum bom/notes-service-repro.json
SOURCE_DATE_EPOCH=1760486400 ant -lib /opt/cyclonedx-antlib/lib sbom-repro >/dev/null 2>&1
md5sum bom/notes-service-repro.json
363269f734898944b2f04370c29dd2ad  bom/notes-service-repro.json
363269f734898944b2f04370c29dd2ad  bom/notes-service-repro.json

两次构建 md5 完全一致——SBOM 达到逐字节可复现(前提是构建输入不变,包括第二章的依赖解析结果)。

实验三——换 SDE 为 1760572800:

urn:uuid:ea50fc00-0199-1000-b4d0-663096d8c180 2025-10-16T00:00:00Z

timestamp 精确推进一天,serial 随之变化。结论汇总:

场景serialNumbertimestamp
不设 SDEv4 随机当前时间
仅固定 serialNumber 属性(API 篇 3.4 节)固定当前时间
设置 SDE由 SDE 派生固定SDE 时刻

💡 技术拓展:发布流水线里推荐 SDE 取「本次发布的时间戳常量」并入库(如 .sde-file 或 CI 环境变量),配合固定 serialNumber 可以让同一版本源码在任何机器上产出同一份 SBOM,审计侧即可对 SBOM 做差分比对。

四、Schema 校验与质量门禁

生成 SBOM 之后,工程化流程必须回答两个问题:这份文件合法吗?内容达标吗?本章用两个脚本闭环:validate-sbom.py 管合法性,check-sbom.sh 管门禁。

4.1 离线校验脚本

先下载官方 1.7 schema 三件套(bom-1.7.schema.json 内部相对引用 spdx.schema.json 与 jsf-0.82.schema.json,三个都必须就位;jsf-0.82.schema.json 首次下载偶发得到 0 字节文件,下载后用 ls -la 核对大小,异常就重下):

cd /root/cdx-advanced && mkdir -p schemas && cd schemas
curl -fsSO https://cyclonedx.org/schema/bom-1.7.schema.json
curl -fsSO https://cyclonedx.org/schema/spdx.schema.json
curl -fsSO https://cyclonedx.org/schema/jsf-0.82.schema.json

/root/cdx-advanced/validate-sbom.py(完整内容):

#!/usr/bin/env python3
"""Validate a CycloneDX SBOM JSON file against the official 1.7 schema.

Usage: python3 validate-sbom.py <bom.json>
Exit code: 0 = valid, 1 = invalid, 2 = usage error.
"""
import json
import sys

from jsonschema import Draft7Validator, RefResolver

BASE = "http://cyclonedx.org/schema/"
FILES = ["bom-1.7.schema.json", "spdx.schema.json", "jsf-0.82.schema.json"]


def main():
    if len(sys.argv) != 2:
        print("usage: validate-sbom.py <bom.json>", file=sys.stderr)
        return 2
    path = sys.argv[1]

    store = {}
    for name in FILES:
        schema = json.load(open("schemas/" + name))
        store["file:///root/cdx-advanced/schemas/" + name] = schema
        store[BASE + name] = schema

    schema = store[BASE + "bom-1.7.schema.json"]
    resolver = RefResolver(base_uri=BASE + "bom-1.7.schema.json",
                           referrer=schema, store=store)
    validator = Draft7Validator(schema, resolver=resolver)
    instance = json.load(open(path))

    errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path))
    if errors:
        for err in errors:
            where = "/".join(str(p) for p in err.path) or "<root>"
            print("INVALID %s: %s" % (where, err.message))
        return 1
    print("%s: valid CycloneDX 1.7 SBOM" % path)
    return 0


if __name__ == "__main__":
    sys.exit(main())

一个必须注意的实现细节:bom-1.7.schema.json 声明的 $id 是 http://cyclonedx.org/schema/...,直接校验时相对引用会尝试走网络,离线环境实测会长时间挂起。脚本因此把三个 schema 预载进 RefResolver 的 store,并同时挂 file:// 与 http://cyclonedx.org/schema/ 两种键,实现完全离线校验。

4.2 通过与篡改实测

四个场景的实测结果(含退出码):

python3 validate-sbom.py bom/notes-service-compile.json; echo exit=$?
bom/notes-service-compile.json: valid CycloneDX 1.7 SBOM
exit=0

篡改一:把 bomFormat 改成 CycloneDX2:

INVALID bomFormat: 'CycloneDX2' is not one of ['CycloneDX']
exit=1

篡改二:追加未知字段 unknownField:

INVALID <root>: Additional properties are not allowed ('unknownField' was unexpected)
exit=1

篡改三(意外发现):把 specVersion 改成 "9.9":

/tmp/tamper2.json: valid CycloneDX 1.7 SBOM
exit=0

specVersion 篡改能通过——实测证明 1.7 schema 并未把 specVersion 做成枚举约束。schema 合法不等于内容正确,这正是 4.3 节门禁脚本要叠加业务断言(组件数下限)的原因:schema 管结构,门禁管内容。

4.3 门禁脚本

/root/cdx-advanced/check-sbom.sh(完整内容):

#!/bin/sh
# SBOM 质量门禁:生成 -> schema 校验 -> 组件数断言
# 用法: BOM_MIN=3 ./check-sbom.sh
set -e

BOM_MIN="${BOM_MIN:-3}"

ant -lib /opt/cyclonedx-antlib/lib sbom-compile

python3 validate-sbom.py bom/notes-service-compile.json

COUNT=$(python3 -c 'import json;print(len(json.load(open("bom/notes-service-compile.json"))["components"]))')

if [ "$COUNT" -lt "$BOM_MIN" ]; then
    echo "SBOM 门禁不通过:组件数 $COUNT 低于阈值 $BOM_MIN" >&2
    exit 1
fi
echo "SBOM 门禁通过:schema 合法,组件数 $COUNT(阈值 $BOM_MIN)"

两场景实测(退出码直接取脚本自身,不经过管道):

./check-sbom.sh > /tmp/gate-pass.log 2>&1; echo pass-exit=$?; tail -1 /tmp/gate-pass.log
pass-exit=0
SBOM 门禁通过:schema 合法,组件数 4(阈值 3)
BOM_MIN=99 ./check-sbom.sh > /tmp/gate-fail.log 2>&1; echo fail-exit=$?; tail -2 /tmp/gate-fail.log
fail-exit=1
SBOM 门禁不通过:组件数 4 低于阈值 99

退出码 0/1 齐备,可以直接被任何 CI 的失败判定消费。

五、CI 流水线接入

5.1 通用 Shell 门禁

把 4.3 节脚本搬上 CI 前,先盘点前置条件(全部为本文实测环境事实):

  1. JDK 17 + Ant(JDK 8 会因 SHA3 缺失失败,见安装指南 FAQ)。
  2. /opt/cyclonedx-antlib/lib 下的 antlib 与依赖 jar(安装指南方案 A 的拉取命令可原样进 CI)。
  3. Python 3 + jsonschema(实测版本 3.2.0)+ schemas/ 三件套随仓库提交。
  4. 工程内 ivy.xml 已套用零驱逐配方(第二章 2.6 节),否则 Ivy 集成会 NPE。

本地全流程一条命令即可复跑:

cd /root/cdx-advanced && ./check-sbom.sh
SBOM 门禁通过:schema 合法,组件数 4(阈值 3)

5.2 GitHub Actions 配置示例

以下为配置示例(YAML 托管结构按 GitHub 官方文档写法组织,其中的安装与校验命令均来自本章与安装指南的实测命令):

name: sbom-gate
on: [push]

jobs:
  sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - name: Provision Antlib and dependencies
        run: |
          mkdir -p /opt/cyclonedx-antlib/fetch /opt/cyclonedx-antlib/lib
          cp ci/fetch-pom.xml /opt/cyclonedx-antlib/fetch/pom.xml
          cd /opt/cyclonedx-antlib/fetch
          mvn -q -B dependency:copy-dependencies -DoutputDirectory=/opt/cyclonedx-antlib/lib

      - name: Install validator dependency
        run: pip3 install jsonschema==3.2.0

      - name: SBOM quality gate
        run: |
          cd cdx-advanced
          ./check-sbom.sh

要点:setup-java 固定 temurin 17 满足 SHA3 要求;jsonschema==3.2.0 与实测版本对齐;门禁脚本自身的退出码即 GitHub Actions 的步骤失败判定,无需额外包装。

5.3 Jenkins 声明式流水线配置示例

pipeline {
    agent any
    tools {
        jdk 'jdk-17'
    }
    stages {
        stage('SBOM quality gate') {
            steps {
                sh 'cd cdx-advanced && ./check-sbom.sh'
            }
        }
    }
    post {
        always {
            archiveArtifacts artifacts: 'cdx-advanced/bom/*.json', allowEmptyArchive: true
        }
    }
}

sh 步骤同样消费脚本的 0/1 退出码;post 段把 SBOM 产物归档,供审计平台拉取。

六、与 cyclonedx-maven-plugin 对比实测

同一份业务依赖(commons-configuration2 2.10.1 + commons-text 1.12.0),在 Maven 环境用 cyclonedx-maven-plugin 2.9.3 生成 SBOM,与本文 Ivy 工程的 antlib 产物正面对比。对比工程的 pom.xml 不内嵌插件配置,直接用完整坐标调用(便于固定插件版本做可复现实验):

cd /root/mvn-app
mvn -B org.cyclonedx:cyclonedx-maven-plugin:2.9.3:makeBom
[INFO] BUILD SUCCESS

产物 target/bom.json 里的组件名清单(实测):

grep -o '"name" : "[^"]*"' target/bom.json | head -12
"name" : "cyclonedx-maven-plugin"
"name" : "notes-service-mvn"
"name" : "maven.goal"
"name" : "maven.scopes"
"name" : "commons-configuration2"
"name" : "commons-lang3"
"name" : "commons-logging"
"name" : "commons-text"

两侧对比结果(全部字段均实测):

维度Antlib 0.2(Ant + Ivy)cyclonedx-maven-plugin 2.9.3(Maven)
默认 specVersion1.71.6
尝试升到 1.7默认即是-Dcyclonedx.schemaVersion=1.7 不生效,产物仍为 1.6(实测,无告警静默回退)
同一依赖集组件数4(commons-logging/configuration2/lang3/text)4(同上四个)
工具组件ant-cyclonedx 0.2,publisher 为 The Apache Software Foundation,11 条外部引用cyclonedx-maven-plugin 2.9.3,author 为 OWASP Foundation
额外元数据lifecycleslifecycles + metadata.properties(maven.goal、maven.scopes)
serialNumberv4 随机;可用属性固定或 SDE 派生(第三章)v3 形态;两次构建实测值不同(a1582a49-3048-… 对 ab8fa3aa-3709-…),不能直接当可复现依据
bom-ref 习惯Ivy 集成场景带 ?type=jar 限定符与 Maven 坐标一致

补充实测两个细节:其一,对 Maven 侧尝试 -Dcyclonedx.schemaVersion=1.7,构建成功但产物 specVersion 仍是 1.6——在 2.9.3 上该属性对 1.7 的设定不生效;而 antlib 默认就输出 1.7,这是当前两代工具最直接的规范版本差异。其二,Maven 侧的 serialNumber 虽是 v3(名字派生)形态,但两次构建值不同,若需要可复现 SBOM,antlib 的 SDE 机制是目前实测下来更直接的路。

选型建议:构建体系已经是 Maven,选 maven-plugin(零集成成本、maven.goal/scopes 审计信息丰富);构建体系是 Ant/Ivy,或者需要 CycloneDX 1.7、SDE 可复现 SBOM,选本文的 antlib。两者并存的多模块仓库也可以并存使用——产物格式一致,审计平台无感。

七、参考链接