引言
许可证合规不是「法务读一遍 LICENSE 文件」这么简单。一个中等规模的产品,直接依赖加传递依赖动辄几百到几千个组件,其中任何一个组件的许可证义务没履行,都可能在被收购、被客户尽调、被上游维权时暴露出来。合规的本质是把法律义务翻译成可执行、可验证、可留痕的工程动作:谁在什么时候引入了什么组件、这个组件带来了哪些义务、我们在哪个环节履行了它、证据存在哪。
工程上真正的难点有三个。第一是识别准确性:依赖树里既有 MIT OR Apache-2.0 这种表达式,也有仓库里根本没有 LICENSE 文件、或者元数据写着 MIT 但实际混入了 GPL 代码的组件,扫描工具只能给出置信度,最终判断要人来做。第二是义务的分布性:GPL 的源码提供义务发生在「分发」那一刻,而分发可能发生在客户私有部署、Docker 镜像交付、SaaS 对外服务等多个出口,义务必须在每个出口都被覆盖。第三是证据的时效性:审计往往发生在产品发布后一两年,那时构建环境早已销毁,如果当时没有把 SBOM、扫描报告、例外审批单归档到不可变存储,事后根本补不出来。
还有一个常被低估的难点:合规的成本曲线是非线性的。在产品早期,几十个依赖靠人工检查完全可行;一旦依赖数破千、发布节奏变成每周多次,人工流程必然崩掉,此时再补自动化,历史版本的证据已经丢失。所以合规要尽早做成流水线的固定环节,而不是等被尽调时才立项。合规失败的代价通常以三种形式出现:被要求开源(GPL/AGPL 义务未履行,被上游或维权方要求公开衍生作品源码,极端情况下产品被迫下架或重写)、交易受阻(收购尽调时发现许可证瑕疵,导致估值折价、交易延期甚至终止)、客户流失(企业客户的安全与合规问卷要求提供 SBOM 与无 GPL 证明,拿不出就进不了供应商名单)。三种后果都不是纯技术问题,但都源于工程环节没有留下证据。这也是为什么合规必须由工程团队主导,而不是把一份政策文档丢给法务。
本文按「义务速查 → 四阶段流程 → 清单生成 → 识别准确性 → 义务履行 → 例外审批 → CI 门禁 → 审计存证 → 运营」的顺序展开。重心放在治理与流程:扫描工具只作为手段点到为止,扫描工具的详细用法可以看本站 devops 专题;许可证之间的兼容性推导(比如 Apache-2.0 与 GPL-2.0-only 为何不兼容)另有专文,本文只处理「怎么把结论落进流水线」。
目录
- 主流许可证义务速查
- 合规流程的四个阶段
- 依赖清单与 SBOM 生成
- 许可证识别与文本匹配的准确性问题
- 义务履行:版权声明、LICENSE、NOTICE、源码提供
- 例外审批与法务介入时机
- 在 CI 中嵌入合规门禁
- 审计准备与存证归档
- 合规运营:责任人、培训与持续改进
1. 主流许可证义务速查
判断一个组件能不能用,先看它属于哪一档:宽松(permissive)、弱 copyleft、强 copyleft、非开源许可。区别不在「能不能商用」,而在「传染范围」和「附加义务」。很多团队把「开源 = 免费 = 随便用」当成默认前提,恰恰是返工的源头。
| 许可证 | 档位 | 传染范围 | 核心义务 | 闭源分发可行性 |
|---|---|---|---|---|
| MIT | 宽松 | 无 | 保留版权声明与许可全文 | 可以 |
| BSD-2-Clause | 宽松 | 无 | 保留版权声明与免责声明 | 可以 |
| BSD-3-Clause | 宽松 | 无 | 同上,另加「不得用作者名背书」条款 | 可以 |
| Apache-2.0 | 宽松 | 无(对独立作品) | 保留声明、提供 NOTICE、标注修改、专利授权 | 可以 |
| MPL-2.0 | 弱 copyleft | 文件级 | 修改过的 MPL 文件源码须公开,可与其他许可文件共存 | 可以(不修改即无源码义务) |
| LGPL-2.1/3.0 | 弱 copyleft | 库级 | 允许动态链接闭源,须提供库源码并允许替换 | 可以(须满足可替换) |
| GPL-2.0-only / GPL-3.0-only | 强 copyleft | 衍生作品整体 | 整体以 GPL 发布并附完整对应源码 | 不可以 |
| AGPL-3.0-only | 强 copyleft + 网络条款 | 衍生作品整体 | 通过网络提供服务即触发源码提供义务 | 不可以(SaaS 也触发) |
| EPL-2.0 | 弱 copyleft | 模块级 | 修改模块源码须公开,允许链接闭源 | 可以(不改模块即可) |
| BSL-1.1 | 源码可得但受限 | 按授权方定义 | 生产使用受限,Change Date 后转 Apache-2.0 | 看具体授权条款 |
几个容易踩的细节:Apache-2.0 的 NOTICE 义务只在上游仓库确实提供了 NOTICE 文件时才触发,不是每个 Apache 组件都要造一个 NOTICE;GPL-3.0 额外要求「安装信息」(Installation Information),对消费类硬件产品意味着不能让设备拒绝用户刷入修改版固件;AGPL 的 §13 是 SaaS 场景下最常被忽视的条款,只要用户通过网络与修改版交互就要提供源码。BSD-3-Clause 的第三条(no endorsement)在宣传材料里引用上游项目名时会踩线。
还要区分许可证版本号后缀:GPL-2.0-only 与 GPL-2.0-or-later 是两个不同的 SPDX 标识,前者不允许升到 GPL-3.0,后者允许。同样 LGPL-2.1-only 与 LGPL-2.1-or-later 在混用 GPL-3.0 时结论完全不同。扫描工具常把它们归一成 GPL-2.0,这个字段必须保留完整。
2. 合规流程的四个阶段
把合规拆成四个阶段,每个阶段有明确的输入、动作、产出和责任人,流程才能被审计。四阶段的划分原则是让每个法律义务都能对应到一个具体的工程动作,而不是停留在文档里。
阶段一:引入(intake)。 开发者在 PR 里新增依赖时,自动化检查该组件的许可证是否在允许列表内。不在列表内的,触发例外审批。产出:依赖引入记录、例外审批单(如有)。这一阶段的关键是「阻断在合并前」,一旦依赖进了主干再清理,成本会高一个数量级。
阶段二:构建(build)。 每次构建生成 SBOM,与上一版做 diff,识别新增/移除/版本变更的组件,重点标记许可证发生变化的组件。产出:带版本与提交哈希的 SBOM 快照。这一阶段是单一事实源的建立点。
阶段三:发布(release)。 在制品打包时组装义务履行物:聚合的 LICENSE/NOTICE 文件、需要提供源码的组件的源码包、书面要约(written offer,若采用该方式履行)。产出:发布物 + 义务履行清单,两者绑定同一版本号。
阶段四:审计(audit)。 归档 SBOM、扫描报告、审批记录、发布物哈希。在被尽调或被质疑时,能在小时级内定位到「某组件在某版本中的使用情况与义务履行证据」。
四阶段的关键是以构建产出的 SBOM 为准,而不是以开发者手工维护的依赖清单为准。手工清单必然漂移:有人本地 go get 了没提交,有人改了 package.json 却忘了更新清单,半年后没人说得清生产环境到底跑了什么。SBOM 从制品里生成才不会骗人。
每个阶段都要有明确的失败行为:引入阶段失败则 PR 不能合并;构建阶段失败则流水线红;发布阶段失败则制品不能发布;审计阶段发现问题则触发追溯与补救。没有失败行为的流程等于没有流程。
四阶段的责任矩阵
| 阶段 | 主要执行方 | 审批方 | 失败行为 |
|---|---|---|---|
| 引入 | 开发者 + 自动化门禁 | 合规负责人(例外场景) | PR 不可合并 |
| 构建 | 流水线 | — | 流水线失败,制品不产出 |
| 发布 | 发布工程师 | 合规负责人 | 制品不可发布 |
| 审计 | 合规负责人 | 法务 | 触发追溯与补救 |
责任矩阵的意义在于每个环节都有明确的人。合规最怕的状态是「人人都觉得有人在管」,实际没人管;等出事时既找不到证据,也找不到责任人。
3. 依赖清单与 SBOM 生成
SBOM 有两种主流格式:SPDX(2.3 / 3.0,Linux 基金会,法律与合规场景更常见)和 CycloneDX(1.5 / 1.6,OWASP,偏向安全与 VEX)。合规场景建议至少保留 SPDX JSON,因为它对许可证字段(licenseConcluded / licenseDeclared / licenseInfoInFiles)的表达最完整。实践上两个都出,成本很低。
容器与文件系统镜像用 syft,一次产出两种格式:
syft registry.example.com/app:1.2.3 \
-o spdx-json=sbom.spdx.json \
-o cyclonedx-json=sbom.cdx.json \
--scope all-layers \
--source-name app --source-version 1.2.3
--scope all-layers 不能省:默认只扫镜像最上层,会把基础镜像里打进底层的组件全部漏掉,而基础镜像恰恰是 GPL 组件的高发区。对本地源码目录做快照,用于「引入阶段」比对:
syft dir:. --exclude './vendor/**/testdata/**' -o spdx-json=intake.spdx.json
Maven 多模块项目用 cyclonedx-maven-plugin,聚合所有子模块:
mvn -B org.cyclonedx:cyclonedx-maven-plugin:2.8.0:makeAggregateBom \
-DoutputFormat=json \
-DoutputName=bom \
-DincludeTestScope=false \
-DschemaVersion=1.5
-DincludeTestScope=false 排除测试作用域依赖,否则 SBOM 里会混进大量只在构建期存在的组件,增加噪音。Gradle 项目同理:
./gradlew cyclonedxBom -PoutputFormat=json
Node.js 用内置命令(npm 10+)或 cdxgen:
npm sbom --sbom-format cyclonedx > sbom.cdx.json
cdxgen -t js -o sbom.cdx.json . # 也支持 -t java / -t go / -t python
Go 项目用 go-licenses 直接产出「组件 → 许可证」的 CSV,便于人工复核:
go-licenses csv ./... > licenses.csv
go-licenses check ./... --allowed_licenses=MIT,Apache-2.0,BSD-3-Clause
生成只是第一步,归档格式要固定:文件命名 <product>-<version>-<commit7>-sbom.spdx.json,与制品同目录、同生命周期。SBOM 里必须写入 --source-name / --source-version,否则半年后一堆 sbom.json 根本对不上版本。
最后是覆盖度检查:SBOM 里的组件数应当与包管理器锁文件(package-lock.json / go.sum / pom.xml 解析结果)的条目数在同一量级,差一个数量级说明扫描方式有漏。这条自检比任何工具报告都可靠。
SPDX 与 CycloneDX 怎么选
| 维度 | SPDX | CycloneDX |
|---|---|---|
| 主导方 | Linux 基金会 | OWASP |
| 强项 | 许可证与法律元数据 | 漏洞、VEX、依赖关系图 |
| 许可证字段 | licenseConcluded / Declared / InfoInFiles 三层 | licenses[].license.id 单层 |
| 典型消费方 | 法务、尽调问卷 | 安全团队、SCA 工具 |
| 归档建议 | 合规主档,必须保留 | 安全侧副产品,一并保留 |
结论:合规以 SPDX 为主档,CycloneDX 作为安全侧的副产品一并生成。两者可以从同一份扫描结果转换,成本只有一条命令,没必要二选一。
4. 许可证识别与文本匹配的准确性问题
扫描工具输出的许可证字段,准确率在 85%~95% 之间,剩下的是需要人判的硬骨头。工具内部的匹配逻辑基于 SPDX 许可证列表的匹配指南(忽略版权行、空白、标点差异),但文本相似不等于法律等价。常见问题与处理方式:
- 相似许可证混淆。 MIT 与 ISC 文本高度相似,BSD-2 与 BSD-3 只差一条,Apache-1.1 与 Apache-2.0 义务天差地别。工具给低置信度(如 < 90%)时必须人工比对原文。
- 多许可证表达式。
MIT OR Apache-2.0是「二选一」,履行其一即可;MIT AND GPL-2.0-only是「都要满足」,实际不可闭源分发。工具常把表达式截断成第一个,务必保留完整 SPDX 表达式字段。 - 双许可(dual license)。 上游同时提供 GPL 与商业许可时,工具只会报 GPL,需要人工确认我们走的是哪条授权路径,并留存购买商业许可的凭证。
- 无 LICENSE 文件。 默认「保留所有权利」,即便代码公开在平台上也不能直接用。这类组件必须走例外审批或替换。
- 元数据与文件不一致。
package.json写 MIT,仓库根目录的 LICENSE 是 AGPL。以仓库内 LICENSE 文本为准,元数据仅作参考。 - vendored 与生成代码。 第三方代码被复制进
vendor/、third_party/或单文件头注释里,依赖树里根本看不到。要用文件级扫描(syft dir:或 ScanCode)补盲区。 - SPDX-License-Identifier 缺失。 源文件没有 SPDX 头时,工具只能整包推断,粒度不足。可在自己仓库强制要求每个文件带
SPDX-License-Identifier: Apache-2.0,配合 lint 卡住。
工程化对策:把「置信度低于阈值」「出现 AND 表达式」「出现未在允许列表中的许可证」三类情况统一标记为需人工复核,而不是直接放行或直接阻断。复核结论回写进豁免文件(带过期时间),下次遇到同组件同版本自动放行,避免重复劳动。
复核阈值可以写进策略,避免每次都靠人记:
confidence_threshold: 90
review_triggers:
- license_expression_contains: " AND "
- confidence_below: 90
- license_not_in: [MIT, Apache-2.0, BSD-3-Clause]
- no_license_file: true
命中任一条件即打上 needs-review 标签并指派给合规负责人,未复核前门禁不放行。这套阈值规则本身也要纳入版本控制,审计时可以证明「复核标准是事先定义且一致执行的」,而不是临时拍脑袋。许可证之间的兼容性判断(例如把 Apache-2.0 代码并入 GPL-2.0-only 项目是否可行)不是扫描器能回答的,需查许可证兼容性
矩阵并留法务确认记录。
5. 义务履行:版权声明、LICENSE、NOTICE、源码提供
识别只是起点,履行才是合规的实际交付物。四类义务按出现频率排列:
版权声明保留。 几乎每个许可证都要求保留原始版权声明和许可文本。做法是构建时聚合所有依赖的 LICENSE 文件到制品中,例如发布一个 THIRD-PARTY-LICENSES.txt:
go-licenses save ./... --save_path=third_party/licenses
find third_party/licenses -name 'LICENSE*' -exec sh -c \
'printf "\n===== %s =====\n" "$1"; cat "$1"' _ {} \; > THIRD-PARTY-LICENSES.txt
LICENSE 文件。 自己的项目根目录要有明确的 LICENSE,且与源文件 SPDX 头一致。多许可项目要写清「哪个目录适用哪个许可」,例如 docs/ 用 CC-BY-4.0、src/ 用 Apache-2.0,并在 README 里说明。
NOTICE 文件。 Apache-2.0 §4(d):若上游提供了 NOTICE,分发时必须一并提供,且不得修改其中的归属声明。做法是在制品里放 NOTICE,内容为各上游 NOTICE 的拼接:
Product: app 1.2.3
This product includes software developed at:
- The Apache Software Foundation
Portions of this software are derived from:
- foo/bar (Apache-2.0), Copyright 2021 Foo Authors
源码提供。 GPL/AGPL/LGPL 要求提供「对应源码」(Corresponding Source)。GPL-2.0 §3 给了三种方式:随二进制附带源码、附带书面要约(written offer,有效期至少三年)、或转发他人要约(仅限非商业)。SaaS 场景走 AGPL §13,必须在用户可访问处提供下载入口。工程做法是把源码归档成 tar 并计算哈希,随发布物一起入库:
git -C vendor/foo archive --format=tar.gz -o dist/src/foo-2.4.1.tar.gz HEAD
sha256sum dist/src/foo-2.4.1.tar.gz >> dist/src/SOURCES.sha256
LGPL 的动态链接场景还需保证「可替换」:不能静态链接、不能把库改到无法替换、要提供用于重新链接的目标文件。这一条在嵌入式与移动端最容易失守——为了减小包体做静态链接,等于把 LGPL 用成了 GPL。
书面要约(written offer)适用于「不随包附带源码、由我方按请求提供」的场景,模板要点如下:
Written Offer of Source Code
Product: app 1.2.3 Release date: 2026-10-07
This product includes software licensed under GPL-2.0-only / LGPL-3.0-only.
For a period of three (3) years from the date of distribution, we will
provide the complete corresponding source code on a medium customarily
used for software interchange, at no charge beyond the cost of distribution.
Request address: oss-compliance@example.com Ref: SOURCES-1.2.3
要约必须写明有效期(至少三年)、索取方式、产品版本号与引用编号,三要素缺一不可,否则审计时无法证明要约有效。引用编号要与内部归档的源码包对应,否则收到请求时找不到对应版本。
义务履行的自动化原则:LICENSE/NOTICE/源码包的生成应当是构建流水线的固定产物,而不是发布时人工准备。人工准备必然在某个紧急发版时被跳过,而漏掉的那一次恰恰会成为审计时的缺口。
6. 例外审批与法务介入时机
不是所有不合规的依赖都要立刻移除,但每一次放行都必须有记录。例外审批单至少包含:组件名与版本、许可证、使用方式(直接/传递、动态/静态链接、是否修改)、分发形态(私有部署/镜像交付/SaaS)、风险描述与缓解措施、有效期、审批人、关联的工单号。
审批矩阵可以简化为三档:
| 风险等级 | 典型情形 | 审批人 | 有效期 |
|---|---|---|---|
| 低 | 宽松许可,仅缺 NOTICE | 研发负责人 | 随组件生命周期 |
| 中 | 弱 copyleft、来源不明、无 LICENSE | 合规负责人 + 研发负责人 | 6 个月,需复审 |
| 高 | 强 copyleft 用于闭源分发、AGPL 用于 SaaS | 法务 + 管理层 | 按版本,逐次审批 |
法务必须介入的信号:引入 AGPL 或 GPL 且产品闭源分发;上游主张专利或涉及标准必要专利;组件与客户合同中的知识产权保证条款冲突;客户要求我方提供 indemnification(赔偿担保);许可证发生变更(如某组件从 Apache-2.0 切到 BSL);被要求提供「无 GPL 证明」的尽调问卷。这些场景下工程团队不应自行判断,因为风险已经从技术问题变成合同责任问题。
例外必须带过期时间。永久豁免等于永久风险,半年后没人记得为什么放行。到期自动在工单系统里生成复审任务。复审时问三个问题:上游是否已换更宽松的许可?我们是否已找到替代组件?缓解措施是否仍然成立?三个都否,就应当升级为技术债清理项。
审批流程还要防审批疲劳:如果每周有几十个例外申请,审批人会退化成盖章。解决办法是把高频重复的例外沉淀成「预批准清单」(例如「LGPL-3.0 动态链接、未修改、已提供库源码」整类预批准),只把真正新颖的情形送上去。
7. 在 CI 中嵌入合规门禁
门禁的目标是「让不合规的变更进不了主干」,而不是「事后扫描出一堆报告没人看」。策略即代码(policy as code)是主流做法:把允许列表、禁止列表、例外文件都放进仓库,用统一的策略引擎评估 SBOM。策略引擎的选型与落地方式可参考策略即代码与治理 。
策略文件示例(Conftest/Rego 风格,读 SBOM 判断):
# policy/license.yaml:声明式允许列表
allowed:
- MIT
- Apache-2.0
- BSD-2-Clause
- BSD-3-Clause
- ISC
- MPL-2.0
denied:
- AGPL-3.0-only
- AGPL-3.0-or-later
- SSPL-1.0
- BUSL-1.1
review_required:
- GPL-2.0-only
- GPL-3.0-only
- LGPL-3.0-only
- EPL-2.0
waivers_file: .compliance/waivers.yaml
# .compliance/waivers.yaml:例外文件,带过期时间
- component: foo/bar
version: "2.4.1"
license: LGPL-3.0-only
reason: "动态链接,未修改,已提供库源码"
ticket: COMPL-1234
approver: legal@example.com
expires: "2027-04-01"
门禁流水线的关键步骤:
syft dir:. -o spdx-json=sbom.spdx.json --source-name app --source-version "$GIT_SHA"
syft convert sbom.spdx.json -o cyclonedx-json | cyclonedx-diff baseline.cdx.json - -o diff.json
conftest test --policy policy/ --all-namespaces sbom.spdx.json
conftest test --policy policy/expiry.rego .compliance/waivers.yaml
四步分别是:生成 SBOM、与基线 diff(只对新增/变更组件做策略评估)、策略评估(非零退出即阻断 PR)、例外过期检查(过期即失败)。
门禁的工程细节:只对增量做阻断。如果对存量依赖一刀切阻断,团队第一件事就是加 continue-on-error,门禁形同虚设。正确姿势是冻结基线(baseline),新增依赖严格卡,存量组件通过「合规债清单」逐步清理。
# .github/workflows/compliance.yml 片段
- name: License gate
run: conftest test --policy policy/ sbom.spdx.json
# 失败时把违规组件写进 PR 评论,而不是只丢一行报错
conftest test --policy policy/ --output json sbom.spdx.json > result.json || \
./scripts/comment-pr.sh result.json # 在 PR 里列出违规组件与建议替代
另外,依赖自动升级机器人(如 Dependabot)提交的 PR 也要过同一套门禁,否则机器人会成为绕过合规的通道。相关配置可参考依赖安全与自动升级 。
门禁的可观测性同样重要:记录每次阻断的原因分布(新引入 GPL?还是置信度不足?),每月回看一次,能发现「哪个团队在反复引入同一类违规」,从而针对性地做培训或提供替代组件。
门禁的三层结构
| 层 | 触发时机 | 检查内容 | 阻断强度 |
|---|---|---|---|
| 提交层 | pre-commit / PR | 依赖清单变更是否带许可证字段 | 警告 |
| 构建层 | CI build | SBOM 策略评估 + 例外过期 | 阻断 |
| 发布层 | release job | LICENSE/NOTICE/源码包是否齐全 | 阻断 |
分层的价值是把快的检查放前面:提交层只做轻量校验,秒级反馈;重的 SBOM 扫描放在构建层,避免拖慢每次保存。发布层做最后一道闸,确保制品里真的带了义务履行物——很多团队在构建层做了策略评估,却忘了验证制品里到底有没有 NOTICE,结果门禁全绿但制品不合规。
8. 审计准备与存证归档
审计(尽调、客户问卷、收购)来时,对方要的是证据链,不是结论。提前准备好材料清单:
- 各版本 SBOM(SPDX + CycloneDX),与制品哈希绑定
- 依赖扫描报告与人工复核记录
- 例外审批单全集(含已过期与已关闭的)
- 聚合的 LICENSE / NOTICE 归档
- 需提供源码的组件的源码包与哈希
- CLA / DCO 签署记录(贡献者授权链条),参见贡献者许可协议与 DCO
- 培训记录、合规政策文档、责任人清单
留存年限:一般取「产品生命周期 + 3 年」或「最后一次分发后 3~7 年」,以较长为宜;GPL-2.0 的书面要约要求三年有效期,因此源码要约相关材料至少留三年。金融、医疗等行业客户合同可能要求更长,按最严的要求统一口径。
存证方式:写入不可变存储(对象存储开启版本控制 + 对象锁 / WORM),每个归档物附 SHA-256 与时间戳。SBOM 是「快照」,不要在同一个路径上反复覆盖:
aws s3 cp sbom.spdx.json \
"s3://compliance-archive/app/1.2.3/$GIT_SHA/sbom.spdx.json" \
--object-lock-mode COMPLIANCE \
--object-lock-retain-until-date "2033-10-07T00:00:00Z"
sha256sum sbom.spdx.json THIRD-PARTY-LICENSES.txt > MANIFEST.sha256
建议的归档目录结构(按产品/版本/提交组织,便于按版本回溯):
compliance-archive/
app/
1.2.3/
a1b2c3d/
sbom.spdx.json
sbom.cdx.json
THIRD-PARTY-LICENSES.txt
NOTICE
SOURCES.sha256
waivers.yaml
MANIFEST.sha256
路径里带 commit 短哈希,是为了区分「同一版本号、不同构建」的情况——这在热修复场景下很常见,只按版本号归档会导致后一次构建覆盖前一次的证据。
审计前的自检清单:抽查三个历史版本的 SBOM 是否可获取;随机挑一个 GPL 组件,验证源码包可下载且哈希匹配;确认例外文件中没有已过期但仍在生效的条目;确认所有分发出口(私有部署包、镜像仓库、SaaS 下载页)都能找到对应的 LICENSE/NOTICE。
还有一条容易被忽略的:问卷与答复的归档。客户尽调时提交的合规问卷、我方给出的书面答复,本身就是承诺,后续审计会拿它对照实际。这些答复要版本化留存,避免前后矛盾。
9. 合规运营:责任人、培训与持续改进
流程建好只是开始,能长期运转靠运营。三件事必须落地。
责任人。 设立开源项目办公室(OSPO)或至少指定一名合规负责人,职责是维护允许列表与例外台账、主持复审、对接法务、在引入高风险组件时把关。责任人要有否决权,否则审批会退化成盖章。
培训。 新员工入职培训覆盖「如何判断一个依赖能不能引入」「去哪里申请例外」;每季度给研发团队同步一次近期新增的禁止许可证与典型返工案例。培训记录本身也是审计材料,别只做不留痕。
指标与持续改进。 跟踪四个指标:扫描覆盖率(有多少制品出了 SBOM)、例外存量与平均存续时长(越长说明技术债越重)、门禁阻断率与误报率、从「引入组件」到「义务履行完成」的端到端时长。指标恶化时先查流程而不是查人。
持续改进的常见方向:把高频出现的例外(比如某个 LGPL 组件反复被申请)转化为「预批准清单」并附标准缓解措施;把义务履行物(LICENSE/NOTICE/源码包)的生成从「发布时人工准备」变成「构建流水线的固定产物」;把许可证变更监控接进依赖升级流程,让上游改许可时第一时间被发现而不是等审计。
最后是定期演练:每年做一次「模拟尽调」,按第 8 节的清单走一遍,看看材料是否齐全、能否在 48 小时内交付。演练暴露的缺口远比事后补救便宜。
权衡取舍
| 维度 | 严格卡口(阻断式) | 宽松放行(记录式) | 建议 |
|---|---|---|---|
| 门禁策略 | 新增即阻断,存量冻结 | 全部放行,只记台账 | 增量阻断 + 存量清债 |
| 识别置信度 | 一律人工复核 | 全信工具输出 | 低于阈值才人工 |
| 例外有效期 | 逐版本审批 | 永久豁免 | 6 个月,到期复审 |
| SBOM 格式 | 只留 SPDX | 只留 CycloneDX | 两种都出,SPDX 为主 |
| 源码提供 | 每版本打全量源码包 | 只给书面要约 | 常用组件随包,长尾用要约 |
| 归档 | 不可变存储 + 时间戳 | 放在 CI 制品里 | 不可变存储,生命周期绑定 |
| 培训频次 | 只在入职时 | 无固定安排 | 入职 + 每季度同步 |
核心取舍是成本与风险的平衡:阻断太严会拖慢交付并催生绕过,放行太松则风险敞口不可控。工程上最优解通常是「增量严、存量宽、例外有期限、证据自动化」。
另一个取舍是人工复核的粒度:全量人工复核准确率最高但不可持续;全自动化跑得快但会漏掉 AND 表达式这类陷阱。折中做法是用置信度阈值把人工精力集中在真正的模糊地带,并让复核结论可复用。
常见坑清单
- 只扫直接依赖:传递依赖占风险的多数,必须扫全依赖树,
syft的--scope all-layers别省。 - SBOM 不带版本标识:一堆同名
sbom.json事后无法对账,生成时必须写入 source-name/source-version。 - 把工具输出的许可证当结论:
MIT AND GPL-2.0-only被截断成 MIT 放行,是典型的静默错误,表达式字段要完整保留。 - 忽略 NOTICE 的「仅在上游提供时」前提:给所有 Apache 组件凭空造 NOTICE 会造成归属声明错误,反而制造新问题。
- AGPL 只在「分发」时才检查:SaaS 不产生二进制分发,但 AGPL §13 照样触发源码义务,SaaS 出口要单独设卡。
- 静态链接 LGPL:静态链接破坏「可替换」前提,要么改动态链接,要么提供目标文件与重链接方式。
- 例外永久有效:没有过期时间的豁免会在人员更替后变成无人负责的灰色地带。
- 门禁一刀切阻断存量:团队会立刻加
continue-on-error绕过,正确做法是冻结基线只卡增量。 - 归档放 CI 制品里:CI 制品有保留期,默认 30~90 天就清空,审计时补不回来。
- 忽略 vendored 与生成代码:复制进来的第三方代码不在依赖树里,需文件级扫描补盲。
- 许可证变更无人监控:上游从 Apache-2.0 切到 BSL 不会通知你,要在升级流程里做 diff。
- 义务履行与制品脱钩:LICENSE/NOTICE 靠发布时人工准备,迟早漏掉某个出口,应作为构建固定产物。
小结
许可证合规的工程化本质是三件事:看得见(SBOM 覆盖全依赖、全制品)、管得住(策略即代码的增量门禁、有期限的例外审批)、拿得出(义务履行物自动生成、证据归档到不可变存储)。工具负责第一件,流程负责第二件和第三件,缺一不可。把这三件事做成流水线的固定环节之后,合规就不再是发布前的一次性突击,而是每次提交都在自动执行的日常。
落地顺序建议从 SBOM 开始:先让每个制品都有带版本标识的 SBOM,再接增量门禁,最后补义务履行物的自动化与归档。跳过第一步直接上阻断式门禁,通常以误报太多、被团队关掉告终。
衡量是否做到位的标准很朴素:被问「你们 1.2.3 版本里那个 GPL 组件的源码在哪」时,能不能在半小时内给出带哈希的下载链接和当时的审批记录。能,就说明流程真的在运转。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。