土法炼钢兴趣小组的算法知识备份

【开源许可与版权工程】闭源项目如何选择开源依赖:公司内部合规实操

文章导航

分类入口
architectureopensource
标签入口
#license#compliance#dependency#closed-source#sca#sbom#gpl#agpl#sspl#bsl#lgpl#copyleft#ospo

目录

你做了一个不开源的商业产品——国内 SaaS、私有化部署给大客户、出海的 B2B 软件,或者嵌入到硬件里卖的固件。你用了几十个开源库,README 上全都写着 MIT 或者 Apache 2.0。你觉得这没什么问题。

直到法务在投融资尽调时问你:项目里有没有 AGPL 或 GPL 组件?你打开 node_modules,第一次认真看里面的 LICENSE 文件。

这篇文章回答一个核心问题:我做一个不开源的商业产品 / 企业内部系统,我能用哪些开源库、怎么用、怎么证明没踩线?

如果你还没有建立对许可证分类的直觉,建议先看:

本文不重复协议的逐条解析,聚焦闭源商业产品在工程层面的落地操作

产品形态 × 许可证合规矩阵

一、闭源产品为什么要关心”开源依赖”

1.1 “我们的代码不公开”不等于许可证对你没有约束

开源许可证的约束条款不问你的源代码是否公开,它问的是:你有没有把受许可证约束的软件分发给第三方?

只要你把包含某个开源组件的二进制分发了出去——无论是给客户的安装包、Docker 镜像、App Store 上的 APK,还是烧录到硬件里的固件——许可证条款就对你生效。你自己的源代码公不公开是另一回事;你如何满足这些许可证条款(提供源码、书面要约、版权声明……)才是合规要做的事。

以 GPL-2.0 为例,FSF 的解释1明确指出:即使你卖的是硬件,只要硬件里烧了 GPL 软件,接收者就有权要求你提供该软件的完整源代码——你不公开”自家”代码没有任何意义,你必须提供 GPL 组件的源码。

1.2 三种触发场景

在实操中,闭源产品触发许可证义务的主要有三类场景:

场景 典型形态 说明
对外发布 binary 安装包、APK/IPA、EXE/MSI/DMG、固件镜像 软件离开了你的控制范围,交到了另一个法人/自然人手里,触发 GPL 分发义务
客户现场部署(on-prem) 私有化部署、Docker Compose 包、OVA/OVF 镜像、Helm Chart 推送给客户 即便是”客户自己安装”,你推送的是可执行形态,仍然构成 convey(参见 GPLv3 §0
SaaS 公网服务 面向公众的 Web/API 服务 GPLv2/v3 不触发(SaaS 豁免),但 AGPL-3.0 §13SSPL §13 明确把网络交互纳入 copyleft 触发条件

内部管理系统(只供本公司员工访问)通常不触发任何 copyleft 义务,但”本公司”的边界需要审慎界定,详见第七节的典型姿势

1.3 投融资 / 并购 / IPO 阶段的典型死法

开源合规在尽调中出现频率越来越高,常见的死亡模式有以下几种:

GPL / AGPL 污染关键模块:目标公司的核心组件(比如数据处理引擎)静态链接了 GPL 库,在没有 GPL Exception 的情况下,整个引擎从理论上讲应该以 GPL 发布——这意味着商业秘密荡然无存。买方法务一旦发现,轻则降低估值,重则放弃交易。

无 SBOM,无法自证清白:尽调要求提供软件成分清单,研发团队表示”从来没做过”,临时用工具扫出来有几十个 license unknown 和几个 AGPL。尽调时间窗口很短,临时补救通常来不及。关于 SBOM 工具链,参见 SCA、SBOM 与软件成分分析

协议改版被忽视:团队五年前引入了某数据库的旧版本(Apache 2.0),但从未升级,而该数据库已在 3.x 版本换成了 SSPL。尽调发现生产环境跑的是 SSPL 版本,但团队以为还是 Apache——技术债务与合规风险叠加。

子依赖传递陷阱:直接依赖全是 MIT,但某个间接依赖是 LGPL,而 Go 项目是以静态编译方式构建的,LGPL 静态链接需要额外操作才合规。这类问题在没有 SCA 工具的情况下很难被人工发现。

出海合规暴雷:准备进入欧美市场,才发现产品里有 BSL 的组件,而 BSL 对商业使用有限制;或者有 Commons Clause 叠加的库,竞争对手条款禁止以 SaaS 形式销售相同功能的服务。


二、闭源视角下的协议分类

本节的分类以”能不能把包含该协议软件的产品分发给客户/公众,而不公开自己的专有源码”为判断标准。

2.1 绝对安全——可自由用于闭源产品

以下许可证对闭源分发没有 copyleft 限制,只要保留版权声明(有的连这个也不要求):

SPDX 标识符 常见约束 典型代表
MIT 保留版权/许可证声明 React、Lodash、Vue 3
BSD-2-Clause 保留版权声明 NetBSD 工具链
BSD-3-Clause 同上 + 不得用原作者名称宣传 Flask、NumPy
ISC 等同于简化 BSD Node.js 部分模块
Apache-2.0 保留声明 + 专利授权 + NOTICE 文件 Kubernetes、Spring、Android
MPL-2.0 文件级 copyleft:修改过的 MPL 文件需保留 MPL Firefox、Servo
MulanPSL-2.0 类 Apache;明确中国法律适用 木兰宽松许可证(国内推荐)
0BSD 无需保留任何声明 极少见
Unlicense 等同公有领域 极少见
CC0-1.0 等同公有领域 通常用于数据/文档,不推荐用于代码

MPL-2.0 的文件级 copyleft 意思是:你修改了某个 MPL 文件,那个文件必须保持 MPL;你自己写的其他文件不受影响。只要做好”MPL 文件不混入专有代码”的隔离,在闭源产品里使用 MPL 库是安全的。参见 MIT、BSD、Apache 2.0 的真实区别

2.2 动态链接安全、静态链接危险——弱 Copyleft

SPDX 标识符 动态链接 静态链接 注意事项
LGPL-2.1-only ✓ 允许 ⚠ 需提供目标文件(.o)以便重链接 glibc、GTK 等
LGPL-2.1-or-later ✓ 允许 ⚠ 同上 多数 GNU 库
LGPL-3.0-only ✓ 允许(§4(d)(0)) ⚠ 需提供目标文件(§4(d)(1)) 少数项目
EPL-2.0 ✓ 允许 ⚠ 修改过的 EPL 文件须保留 EPL Eclipse 系列

Android 平台对 LGPL 的动态链接规则有额外限制:Android NDK 的构建系统默认静态链接,加上 Android App 不允许用户随意替换 .so 文件,部分 LGPL 库的版权方认为这不满足 LGPL §6(b) 的”用户可替换”要求。建议对移动 App 中 LGPL 库的使用单独评估。

详细分析见 Copyleft 的工程边界 第二节。

2.3 内部使用安全、对外分发危险——强 Copyleft

SPDX 标识符 内部使用 对外分发 说明
GPL-2.0-only ✓ 可用(未分发不触发) ✗ 分发必须开源 触发条件:distribute(物理传递)
GPL-2.0-or-later 同上
GPL-3.0-only 触发条件:convey(传递,更宽泛)
GPL-3.0-or-later 同上

“对外分发”包括:给客户的安装包、私有化部署包、公开下载的二进制、App Store 上架。SaaS(用户通过网络使用而不接收二进制) 下 GPL 不触发,这就是著名的”ASP 漏洞”(Application Service Provider loophole)。

2.4 SaaS 也不安全——网络 Copyleft

SPDX 标识符 触发条件 说明
AGPL-3.0-only 网络交互即触发(§13) 用户通过网络与程序交互就算触发
AGPL-3.0-or-later 同上 AGPL-3.0 的 “or later” 变体
SSPL-1.0 比 AGPL 更宽,提供相关服务基础设施都要开源 MongoDB、Elasticsearch(旧版)
OSL-3.0 公共展示(public display)即触发 较少见
CoCoL-1.0 类似 AGPL Copyfarleft,极少见

AGPL-3.0 §13 原文:

Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network … an opportunity to receive the Corresponding Source of your version …

这意味着:你用 AGPL 软件(比如某个版本的 Grafana、某版本的 Neo4j Community)提供了 SaaS 服务,任何通过网络访问服务的用户都有权要求你提供完整的修改版源代码,包括你自己的业务逻辑层。

SSPL-1.0 更进一步(§13):不仅要提供修改版本的源码,还要提供”让服务运行所必需的所有程序”的源码,包括管理软件、监控脚本、部署脚本等——这在实践中等于强制开源整个运维体系。

关于 AGPL 和 SSPL 的详细对比,参见 AGPL、SSPL、BSL:云厂商时代的”反云”许可证

2.5 “看似开源但商业受限”——Source Available

这类许可证在开源定义(OSD)意义上不算真正的开源,但代码是公开可读的:

许可证 特点 典型项目
BSL-1.1(Business Source License) 有 Change Date(通常 4 年后转为某开源协议);Change Date 之前,生产商业使用受限 CockroachDB、MariaDB MaxScale、HashiCorp Vault
ELv2(Elastic License v2) 不得提供与 Elastic 功能竞争的 SaaS;不得绕过许可证机制 Elasticsearch 7.11+、Kibana 7.11+
Confluent Community License 不得用于构建流处理平台产品销售 Confluent Platform 部分组件
RSAL(Redis Source Available License) 不得用于数据库/缓存类 SaaS Redis Stack(部分模块)
SSPL-1.0(MongoDB) 见上节,也属于此类 MongoDB 4.0+
Commons Clause(附加条款) 附在其他许可证上,禁止以销售形式提供该软件的功能 在原有 Apache/MIT 上叠加

注意:BSL 的 Change Date 到期后,源码按 Change License(通常是 Apache-2.0 或 GPL-2.0)发布,但已经分发的旧版二进制仍受 BSL 约束。

2.6 特殊协议注意事项

CC-NC(非商用):Creative Commons NonCommercial 变体(如 CC-BY-NC-4.0)明确禁止”以主要商业优势或金钱补偿为目的”的使用。商业产品不能用 CC-NC 代码,哪怕只是内部工具。

JSON License:JSON.org 的许可证基于 MIT,但附加了一条:“The Software shall be used for Good, not Evil.” 这句话的法律效力有争议,但企业合规团队通常会把它列为灰名单(因为”善”和”恶”无法确定范围)。IBM 曾专门联系 Douglas Crockford 请求豁免,最终得到了回复。Node.js 生态早期对此有很多讨论。

WTFPL(Do What The F**k You Want to Public License):字面上可自由使用,但因为字面内容,企业法律团队有时会拒绝接受,不能作为企业正式 License 使用。


三、不同产品形态下的协议红线矩阵

下表是本文核心。行为产品形态,列为许可证。每格含结论(✓ 允许 / ⚠ 有条件 / ✗ 禁用)与简要原因。

产品形态 MIT / BSD / Apache-2.0 LGPL 动态链接 LGPL 静态链接 MPL-2.0 GPL-2.0 / v3 AGPL-3.0 SSPL-1.0 BSL-1.1 Commons Clause
SaaS 公有云 ⚠ 需提供目标文件 ✓ ① ✗ §13 立即触发 ✗ §13 更严苛 ⚠ 看 Use Limitation 条款 ✗ 禁止商业 SaaS
私有化部署(on-prem) ⚠ 需提供目标文件 ✗ 分发触发 ⚠ 看是否允许商业 on-prem
桌面软件
移动 App(iOS/Android) ⚠ Android 静态链接复杂 ⚠ 需特殊操作
嵌入式固件 ⚠ 需确保用户可替换 ⚠ 提供目标文件 ✗ 需书面要约②
内部管理系统(本公司员工使用) ✓ 未分发不触发 ⚠ 内部是否算”用户” ⚠ 更宽泛 ✓ 内部使用通常豁免 ✓ 内部不出售
二次分发 SDK(嵌入客户产品) ✗ 通常禁止再分发

GPL SaaS 豁免:GPLv2/v3 只在”分发(distribute / convey)“时触发。SaaS 模式下用户使用网络服务、不接收可执行文件,GPL 义务不触发。这是业界普遍接受的立场(即”ASP loophole”)。AGPL-3.0 专门用 §13 堵掉了这个豁免。

3.1 矩阵逐行解读

SaaS 公有云:本质是你自己的服务器上运行软件、用户通过网络访问结果。GPLv2/v3 不触发(SaaS 豁免);LGPL 只要动态链接就安全;AGPL-3.0 和 SSPL-1.0 立即触发(§13);BSL 需要仔细阅读 Additional Use Grant 和 Use Limitation,许多 BSL 项目明确允许”Internal Use”但禁止”提供竞争性服务”;Commons Clause 明确禁止”以销售形式提供软件功能”,SaaS 正是典型的销售场景。

私有化部署(on-prem):你把包含软件的包交给客户,客户安装在自己的服务器上。这是最标准的”分发”场景,GPL 所有版本全部触发,AGPL 更不用说。LGPL 静态链接需要额外提供目标文件;动态链接是安全的。BSL 对 on-prem 的限制通常比 SaaS 宽松,但也要按需确认。

桌面软件:安装包(EXE / DMG / AppImage / .deb)是最传统的分发形态,每次安装都是”分发”。GPL / AGPL / SSPL 全部不可用(除非你愿意同步开源整个产品)。LGPL 的动态链接在 Windows 下是 DLL,在 Linux 下是 .so,在 macOS 下是 .dylib,都可以满足”用户可替换”要求。

移动 App(iOS / Android): - iOS:App Store 的分发机制使得用户无法替换 .dylib,LGPL 的”用户可替换”要求在技术上难以满足,FSF 明确认为 iOS App Store 的分发方式对 GPL/LGPL 不兼容。实践中,大多数公司直接避免在 iOS App 里引入任何 LGPL/GPL 组件。 - Android:Android NDK 的构建系统倾向于静态链接;虽然理论上可以动态链接 .so,但 Android 应用沙箱限制了用户替换 .so 的能力,使得 LGPL §6(b) 难以满足。部分 LGPL 版权方(如 Qt 的 The Qt Company)明确声明他们认为 Android 动态链接方案满足 LGPL——但这只是版权方的单方声明,不代表所有 LGPL 版权方的立场。

嵌入式固件:固件通常是静态编译的单一镜像,刷入设备后用户无法替换 .so 文件。LGPL 动态链接的”可替换性”在技术上难以实现(除非设备提供了刷新部分库的机制)。GPL 必须提供书面要约(Written Offer),设备必须附带纸质文档或设备屏幕上的信息告知用户如何获取源码。

内部管理系统:只供本公司同一法人实体员工使用,没有”分发”行为。GPL 可以自由使用;AGPL 在纯内部使用时是否触发 §13 有争议——FSF 的立场是:内部员工不是”公众”,内部工具不触发 §13;但如果公司规模极大(几万名员工),这一边界的解释可能会变得模糊。SSPL 对内部使用通常是安全的,因为 SSPL §13 的触发也需要”为第三方提供服务”。BSL 和 Commons Clause 的内部使用通常明确豁免(不属于”销售”)。

二次分发 SDK:你把 SDK 打包给客户,客户再把你的 SDK 嵌入他们的产品对终端用户分发。这个场景下,你的 SDK 会被二次分发,链式传染风险极高。GPL 组件会传染到你的 SDK,进而传染到客户的产品,引发客户的抗议和合同纠纷。BSL 通常明确禁止再分发,因为它限制的就是商业使用和再分发。这一形态下应严格执行白名单,只允许 MIT / BSD / Apache / MPL-2.0(文件级隔离)/ MulanPSL-2.0。

嵌入式固件含 GPL 组件时,必须向接收设备的客户提供书面要约(Written Offer),承诺在至少 3 年内按需提供完整源码,具体见 GPLv2 §3(b) 或 GPLv3 §6(b)。设备上必须有物理或数字手段(如随机文档、设备菜单)让用户知道如何索取源码。


四、黑名单、灰名单、白名单实操

4.1 大厂的公开参考

Google 的第三方许可证政策(Third-Party Licenses Policy)在多份公开演讲和 OSPO 材料中被提及: - 白名单(无限制使用):MIT、BSD-2/3-Clause、ISC、Apache-2.0、MulanPSL-2.0 - 灰名单(需 OSPO 审批):LGPL(需确认链接方式)、MPL-2.0、EPL-2.0 - 黑名单(默认禁止,需 VP 级审批方可例外):GPL-2.0、GPL-3.0、AGPL-3.0、SSPL-1.0

Microsoft OSPO 在其内部开源合规培训材料和多次 Open Source Summit 演讲中(可在 Linux Foundation 会议回放中找到)介绍了类似的三级体系,黑名单明确包含 AGPL、SSPL、Commons Clause 叠加协议、GPL-3.0(因为 GPL-3.0 包含了 Tivoization 禁止条款,对嵌入式/硬件业务影响大)。

Fedora 项目 维护了一份许可证 Good / OK / Bad 列表(Fedora License List),是判断协议是否符合 Fedora 自由软件原则的权威参考。闭源项目可以反向参考:Fedora 标注”OK”的协议基本都是宽松协议,可以安全用于闭源。

某头部国内大厂 OSPO 的公开实践(基于工程博客和行业交流): - 华为开放原子捐赠的项目(如鸿蒙、MindSpore)全部采用 Apache-2.0 或 MulanPSL-2.0,体现了对宽松协议的明确偏好。 - 国内多家大厂在内部建立了 OSS 准入机制,要求引入任何开源组件前在内部系统(有时叫”开源软件仓”或”OSS 中间台”)登记并通过自动扫描 + 人工审核双通道。 - 引入 LGPL 库通常需要注明链接方式;引入 GPL 库通常需要架构评审,确认是否存在隔离方案。

4.2 可抄的三级名单模板(YAML)

以下模板供中等规模公司直接参考,根据实际业务场景调整:

# license-policy.yaml
# 开源许可证准入三级名单模板 v1.0
# 适用范围:面向外部用户分发的商业产品(SaaS、私有化部署、客户端软件)
# 最后更新:2026-04

version: "1.0"
scope:
  - saas
  - on_prem
  - desktop_client
  - mobile_app
  - embedded_firmware
  - redistributed_sdk

# ——————————————————————————————————————
# 白名单:无需审批,可直接引入
# ——————————————————————————————————————
allow:
  # 宽松许可证
  - spdx: MIT
    note: "保留版权声明即可"
  - spdx: BSD-2-Clause
  - spdx: BSD-3-Clause
    note: "不得以原作者名义宣传"
  - spdx: ISC
  - spdx: Apache-2.0
    note: "保留 NOTICE 文件;注意专利防御性终止条款"
  - spdx: MulanPSL-2.0
    note: "国产推荐;适用中国法律"
  - spdx: MPL-2.0
    note: "文件级 copyleft;修改过的 MPL 文件须保留 MPL;不允许混入专有代码"
  - spdx: 0BSD
  - spdx: Unlicense
  - spdx: CC0-1.0
    note: "仅限数据/文档;不推荐用于代码"
  - spdx: PSF-2.0
    note: "Python 标准库"
  - spdx: Artistic-2.0
    note: "Perl CPAN 常见"
  - spdx: WTFPL
    condition: "需法务确认接受;部分大客户拒绝"

# ——————————————————————————————————————
# 灰名单:需架构委员会或 OSPO 审批
# 审批材料:产品形态、链接方式、分发范围、替代方案评估
# ——————————————————————————————————————
review:
  - spdx: LGPL-2.1-only
    conditions:
      - "动态链接:允许;需保留用户替换 .so 的能力"
      - "静态链接:需提供 .o 目标文件 + LGPL 源码 + 链接说明"
      - "Android App:需单独评估;buildroot/NDK 静态链接存在合规风险"
    approver: ospo
  - spdx: LGPL-2.1-or-later
    conditions:
      - "同 LGPL-2.1-only"
    approver: ospo
  - spdx: LGPL-3.0-only
    conditions:
      - "动态链接允许(GPLv3 §4(d)(0))"
      - "静态链接需 §4(d)(1) 目标文件"
    approver: ospo
  - spdx: EPL-2.0
    conditions:
      - "修改过的 EPL 文件须保留 EPL 并随产品分发源码"
      - "未修改使用可接受"
    approver: ospo
  - spdx: CDDL-1.0
    conditions:
      - "文件级 copyleft;与 GPL 不兼容;通常作为独立进程调用更安全"
    approver: ospo
  - spdx: BSL-1.1
    conditions:
      - "确认 Change Date 是否已过(过则按 Change License 对待)"
      - "确认 Additional Use Grant 是否覆盖本产品用例"
      - "生产商业使用需确认不违反 Use Limitation"
    approver: legal_and_ospo
  - spdx: LicenseRef-JSON
    note: "JSON License(含 Good/not Evil 条款);建议获得版权方书面豁免"
    approver: legal

# ——————————————————————————————————————
# 黑名单:默认禁止引入
# 例外:需 CTO / 法务联合审批,并提供完整合规方案
# ——————————————————————————————————————
deny:
  - spdx: GPL-2.0-only
    reason: "分发触发强 copyleft;除内部工具外禁止"
    exception_process: cto_legal_joint_approval
  - spdx: GPL-2.0-or-later
    reason: "同上"
    exception_process: cto_legal_joint_approval
  - spdx: GPL-3.0-only
    reason: "同上;Tivoization 禁止条款对嵌入式更不友好"
    exception_process: cto_legal_joint_approval
  - spdx: GPL-3.0-or-later
    reason: "同上"
    exception_process: cto_legal_joint_approval
  - spdx: AGPL-3.0-only
    reason: "网络 copyleft;SaaS 即触发;禁止用于任何对外服务"
    exception_process: cto_legal_joint_approval
  - spdx: AGPL-3.0-or-later
    reason: "同上"
    exception_process: cto_legal_joint_approval
  - spdx: SSPL-1.0
    reason: "超级 copyleft;连运维脚本都须开源;禁止引入"
    exception_process: no_exception
  - spdx: OSL-3.0
    reason: "公共展示即触发 copyleft"
    exception_process: cto_legal_joint_approval
  - spdx: CC-BY-NC-4.0
    reason: "明确禁止商业使用"
    exception_process: no_exception
  - spdx: CC-BY-NC-SA-4.0
    reason: "同上;附加 ShareAlike"
    exception_process: no_exception
  - spdx: LicenseRef-ELv2
    reason: "禁止提供竞争性 SaaS;禁止绕过许可证机制"
    exception_process: legal_only
  - spdx: LicenseRef-RSAL
    reason: "数据库/缓存类 SaaS 禁止"
    exception_process: legal_only
  - spdx: LicenseRef-CommonsClause
    reason: "销售/SaaS 禁止;附加在其他协议上的限制条款"
    exception_process: legal_only
  - spdx: Proprietary
    reason: "未知专有许可证;逐一评估"
    exception_process: legal_required

# ——————————————————————————————————————
# 例外审批流程说明
# ——————————————————————————————————————
exception_processes:
  ospo:
    description: "OSPO 负责人审批,在 PR 评论中留痕,归档到合规系统"
    turnaround: "3 个工作日"
  legal_and_ospo:
    description: "法务 + OSPO 联合审批"
    turnaround: "5 个工作日"
  legal_only:
    description: "法务审批"
    turnaround: "5 个工作日"
  cto_legal_joint_approval:
    description: "CTO + 法务联合审批;需提交架构隔离方案"
    turnaround: "10 个工作日"
  no_exception:
    description: "无例外通道;必须替换"
    turnaround: "N/A"

4.3 灰名单例外的审批流与留痕

例外审批必须留下可追溯的证据链,建议在内部合规系统(Jira、企业微信知识库、Confluence 均可)以下面的格式归档:

# exception-record-template.yaml
exception_id: "EX-2026-0042"
component:
  name: "libreadline"
  version: "8.2"
  spdx: "GPL-2.0-or-later"
  url: "https://tiswww.case.edu/php/chet/readline/rltop.html"
  sha256: "be42b703d28d94e2700ae5bf...(lockfile 中的 hash)"
product: "产品 A v2.3 私有化部署版"
requester: "张三(后端架构师)"
date_requested: "2026-04-10"
date_approved: "2026-04-15"
approvers:
  - "李四(OSPO)"
  - "王五(法务)"
  - "赵六(CTO)"
justification: |
  readline 仅用于内部 CLI 调试工具的命令行补全,不打包进客户分发包。
  客户分发包通过 Dockerfile 多阶段构建排除调试工具层(见 commit abc123)。
  已验证分发镜像不含 readline(shasum 列表见附件 A)。
mitigation: |
  如未来需要在分发版中引入命令行补全,替换方案为 linenoise(BSD-2-Clause)
  或 editline(BSD-3-Clause)。
sbom_evidence: "sbom/product-a-v2.3-distrib.cdx.json"
review_date: "2027-04-10"  # 每年复审

五、挑选库时的 10 条快检清单

在 PR 引入新依赖时,逐条过一遍下面的清单。每条需要”有明确答案”才能通过:

5.1 仓库根 LICENSE 文件是否清晰?

# 检查仓库根是否有 LICENSE 文件,且内容可识别
curl -sL "https://api.github.com/repos/<owner>/<repo>/license" \
  | jq '.license.spdx_id'
# 或本地检查
find . -maxdepth 2 -name 'LICENSE*' -o -name 'LICENCE*' | head -20

如果没有 LICENSE 文件,该项目默认为 All Rights Reserved——无任何授权,哪怕源码是公开的也不能使用。

5.2 是否有双重许可(dual license)?

双重许可(dual license)常见于商业开源模式:项目同时提供 GPL 版本(免费,但必须开源)和商业版本(付费,可闭源)。MySQL 是最典型的例子。

判断方法: - 查看 README 是否提到”Commercial License”或”Enterprise License” - 查看 SPDX 表达式:如 GPL-2.0-only OR LicenseRef-MySQL-Commercial

选择策略:如果需要闭源,向版权方购买商业许可,或者选择替代方案。

5.3 有没有贡献者许可证协议(CLA)?

贡献者许可证协议(Contributor License Agreement,CLA)不影响你作为用户使用该库的权利,但如果你计划向上游贡献代码,需要了解 CLA 条款,避免贡献内容的版权被以某种方式转移。参见 CLA、DCO 与贡献者协议

5.4 是否有专利授权(patent grant)?

Apache-2.0 明确包含了专利授权条款和防御性终止条款(§3)。MIT 和 BSD 不包含专利授权。对于涉及标准协议、编解码算法、特定数据结构的库,专利授权是否存在非常重要。

GPLv3 §11 包含了广泛的专利条款,这是部分公司拒绝 GPL-3.0 的原因之一。

5.5 上游是否还在维护?

一个已被放弃的库,不只是安全风险,也是许可证风险——如果未来出现版权纠纷或需要澄清许可条款,已经无法联系到版权方。

# 检查最后一次提交时间
git log --oneline -1
# 或通过 GitHub API
curl -sL "https://api.github.com/repos/<owner>/<repo>" | jq '.updated_at'

5.6 是否曾经改过许可证(历史提交记录)?

协议变更是高风险事件。MongoDB(Apache → SSPL)、Elasticsearch(Apache → Elastic License + SSPL)、HashiCorp(MPL-2.0 → BSL-1.1)、Redis 部分模块(BSD → RSAL → SSPL)都曾发生过。

检查方法:

# 查看 LICENSE 文件的变更历史
git log --follow --oneline LICENSE
# 查看 package.json 中 license 字段的变更
git log -p -- package.json | grep '^[+-].*license'

如果发现协议在你依赖的版本区间内有过变更,确认你锁定的版本号对应的协议。

5.7 是否有商标(trademark)限制?

有些项目的名称是商标,即便代码可以自由使用,使用该名称做商业宣传可能需要商标许可。典型例子:Firefox(火狐)品牌名由 Mozilla 持有商标,Debian 发行的是 Iceweasel;OpenStack、Kubernetes 等项目名称受相应基金会商标政策约束。

5.8 子目录 / vendored 目录是否协议不同?

许多仓库的根 LICENSE 是 MIT,但 vendor/third_party/contrib/ext/ 目录下的依赖可能是其他协议:

# 快速扫描所有 LICENSE 文件
find . -name 'LICENSE*' -o -name 'COPYING*' | sort | \
  xargs -I{} sh -c 'echo "=== {} ==="; head -3 {}'
# 用 syft 做深度扫描
syft dir:. --output spdx-json > sbom.spdx.json

5.9 包管理器声明的 SPDX 与实际 LICENSE 文件是否一致?

# Python:检查 setup.cfg / pyproject.toml 声明
grep -r 'license' pyproject.toml setup.cfg
# 与实际 LICENSE 文件内容比对

# npm:检查 package.json 声明
cat package.json | jq '.license'
# 与 LICENSE 文件比对

# Go:go.mod 不含 license 字段,用 syft 或 licensei 扫描
go-licenses check ./...

# Cargo:Cargo.toml
grep 'license' Cargo.toml

声明和实际不一致的情况确实存在:有些库在 package.json 里写 MIT,但 src/ 下混入了 LGPL 或 GPL 的片段(通常是早期维护者不严谨或者 vendored 了其他项目的代码)。

5.10 是否混入了 GPL 代码(误打包)?

这是最难检测的场景:某个声明 MIT 的库,内部 utils/ 目录下有一个文件是从 GPL 项目复制过来的。这类问题要靠 SCA 工具的片段匹配(snippet matching)功能来检测:

# FOSSA 片段扫描
fossa analyze --project myproject

# Black Duck 二进制片段分析
# 需要商业许可,但提供最高精度的片段匹配

# 开源工具:scancode-toolkit
scancode --license --copyright -n4 --json-pp scan-result.json ./src

详细的工具对比和流水线搭建方法见 SCA、SBOM 与软件成分分析


六、典型”毒依赖”与替代方案对照表

以下列出闭源产品最常遇到的许可证陷阱,以及可直接替换的合规方案。

6.1 数据库类

问题依赖 许可证 问题 替代方案 替代方案许可证
MySQL Server GPL-2.0-only 分发触发 copyleft MariaDB Server + mariadb-connector-c GPL-2.0-or-later(服务器端)+ LGPL-2.1-or-later(客户端)
MySQL Server GPL-2.0-only 同上 架构隔离:MySQL 作为独立进程,通过 TCP 协议访问 进程隔离消除 copyleft 传染
MySQL Server GPL-2.0-only 同上 购买 Oracle MySQL 商业许可证 商业许可,无 copyleft
MySQL Server GPL-2.0-only 同上 云托管 RDS / PolarDB / 阿里云 RDS MySQL 云服务使用,无需管理许可证
MongoDB 4.0+ SSPL-1.0 对外服务等同开源全栈 DocumentDB(AWS,兼容 MongoDB API) 云服务
MongoDB 4.0+ SSPL-1.0 同上 FerretDB(PostgreSQL 后端,MongoDB 协议) Apache-2.0
Elasticsearch 7.11+ ELv2 / SSPL-1.0 竞争性 SaaS 禁止 OpenSearch 2.x(AWS fork,Apache 2.0 基线) Apache-2.0
Elasticsearch 7.11+ ELv2 / SSPL-1.0 同上 MeiliSearch(商业友好,BSL → MIT 化路线) MIT(旧版)/ BUSL-1.1(新版,注意)
Redis 7.4+(Stack 模块) RSAL / SSPL-1.0 数据库类 SaaS 禁止 Valkey(Linux Foundation fork) BSD-3-Clause
Redis 7.4+(Stack 模块) RSAL / SSPL-1.0 同上 KeyDB BSD-3-Clause
Redis 7.4+(Stack 模块) RSAL / SSPL-1.0 同上 Dragonfly(注意:BSL-1.1,有 Change Date) BUSL-1.1Apache-2.0 4 年后
InfluxDB 2.x MIT(InfluxDB v1)/ MIT(仍安全) - 继续使用 v1 或迁移至 VictoriaMetrics Apache-2.0

6.2 终端输入与命令行类

问题依赖 许可证 替代方案 替代方案许可证
GNU Readline GPL-2.0-or-later linenoise(Salvatore Sanfilippo) BSD-2-Clause
GNU Readline GPL-2.0-or-later editline / libedit(NetBSD) BSD-3-Clause
GNU Readline GPL-2.0-or-later replxx BSD-3-Clause
GNU ncurses MIT-Modern-Variant 实际上也是 MIT 类,多数情况安全,但确认版本 -

6.3 多媒体处理类

问题依赖 许可证 替代方案 说明
FFmpeg(完整构建) LGPL-2.1-or-later + GPL 可选组件 使用 --disable-gpl 的纯 LGPL 构建 --enable-gpl 会引入 x264、x265 等 GPL 组件,使整个 FFmpeg 变为 GPL
GStreamer LGPL-2.1-or-later(核心)+ GPL(部分插件) 只使用 gstreamer-plugins-base(LGPL);禁止加载 bad/ugly 中的 GPL 插件 插件机制可在运行时按需加载,但加载 GPL 插件后运行时环境整体受 GPL 约束
libvpx BSD-3-Clause 安全,可直接使用 -
x264 GPL-2.0-or-later openh264(Cisco) BSD-2-Clause;需注意 H.264 专利
x265 GPL-2.0-or-later 商业编码器,或 AV1(libaom,BSD) BSD-2-Clause

6.4 可视化与监控类

问题依赖 许可证 替代方案 替代方案许可证
Grafana OSS 8.0+ AGPL-3.0-or-later Apache Superset Apache-2.0
Grafana OSS 8.0+ AGPL-3.0-or-later Redash BSD-2-Clause
Grafana OSS 8.0+ AGPL-3.0-or-later 购买 Grafana Enterprise 商业授权 商业许可
Metabase(社区版) AGPL-3.0-or-later Apache Superset、Redash Apache-2.0 / BSD-2-Clause

6.5 图形界面类

问题依赖 许可证 用法建议
Qt 5 / Qt 6 LGPL-3.0-or-later(LGPLv3 模块)/ GPL-2.0-or-later(某些模块) 动态链接 Qt 库(.so/DLL)满足 LGPL §4(d)(0);避免静态链接;避免使用 GPL 模块(如 Qt Charts 早期版本)
wxWidgets LicenseRef-wxWindows(含自定义例外) wxWidgets License 包含链接例外,允许闭源分发,安全

6.6 计算机视觉类

问题依赖 许可证 说明
OpenCV(主库) Apache-2.0 安全;核心模块 2020 年从 BSD-3 迁移至 Apache-2.0
OpenCV contrib 模块 混合:部分 Apache-2.0部分 GPL opencv_contrib 中的 non-free 目录含有专利算法实现(如 SIFT、SURF),同时许可证也可能包含 GPL 组件;生产环境禁止静态编译 contrib/non-free

七、典型被问到的五个姿势

7.1 内网工具能用 GPL 吗?

可以,未分发不触发。

GPLv2 的 distribute 和 GPLv3 的 convey 都要求软件离开你的控制范围传递给另一方。内网工具只供本公司员工使用,软件始终在本法人实体内,不构成分发,GPL copyleft 义务不触发。

但有两点需要注意: 1. 外包开发者:如果你雇了外部承包商帮你开发内网工具,并把代码交给了承包商,FSF 认为这不算分发(承包商是你的代理)。但一旦承包商把代码带走或共享给他人,分发就发生了。 2. 子公司 / 关联公司:同一母公司控股下的不同法人之间传递 GPL 软件,严格意义上构成分发。实践中通常被宽松对待,但若母子公司分拆或被收购,风险会浮现。

7.2 把 GPL 工具打到镜像里作为运行时工具,商业 App 容器里用了会出事吗?

这是一个常见的架构问题。典型场景:Docker 镜像中使用 Alpine 基础镜像,Alpine 自带 BusyBox(GPL-2.0),或者你在 Dockerfile 中 apk add git(GPL-2.0)作为运行时工具。

关键区分:exec 独立进程 vs 链接进地址空间

# syntax=docker/dockerfile:1.8
# ——————————————————————————————————————
# 构建阶段:允许使用各种工具,包括 GPL 工具链
# ——————————————————————————————————————
FROM golang:1.22-alpine AS builder
# apk add git 只在此阶段,不进入最终镜像
RUN apk add --no-cache git gcc musl-dev
COPY . /src
WORKDIR /src
RUN go build -o /out/myapp ./cmd/myapp

# ——————————————————————————————————————
# 运行阶段:distroless,不含任何 GPL 工具
# distroless/static-debian12 基于 Debian,核心组件 GPL-free
# ——————————————————————————————————————
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
COPY --from=builder /out/myapp /myapp
USER nonroot:nonroot
ENTRYPOINT ["/myapp"]

用 Syft 验证最终镜像不含 GPL 组件:

syft packages docker:myimage:latest \
  --output spdx-json \
  | jq '[.packages[] | select(.licenseConcluded | contains("GPL"))]'
# 结果应为空数组

7.3 修改过的 LGPL 库放到闭源 App 里

规则:参见第二节Copyleft 工程边界第二章。

动态链接:即使修改了 LGPL 库,只要以动态链接(.so / DLL / .dylib)形式使用,用户可以替换该共享库,满足 LGPL §6(b)。你必须公开修改后的 LGPL 库本身的源代码,但不需要公开你自己的主程序代码。

静态链接:必须提供修改后的 LGPL 库的源码,以及主程序的目标文件(.o)——不是源码,但用户可以用这些目标文件 + 修改后的 LGPL 库重新链接出可执行文件。主程序源码仍然可以保密。

操作清单:

静态链接 LGPL-2.1 的合规材料包:
├── modified-libfoo-2.1.3-patched.tar.gz   # 修改后的 LGPL 库源码
├── myapp-main.o                            # 主程序目标文件(不是源码)
├── myapp-utils.o
├── myapp-network.o
├── link-instructions.txt                   # 重链接说明:
│   # gcc -o myapp-rebuilt myapp-main.o myapp-utils.o myapp-network.o \
│   #   -L./libfoo-2.1.3-patched/build -lfoo -lssl -lz
└── NOTICE.txt                              # 版权声明

7.4 AGPL 数据库挂在 SaaS 后面只读使用

一定要怕。AGPL §13 立即生效。

不管是”只读”还是”只是用来存储”,只要你的 SaaS 产品的用户通过网络与你的服务交互,而你的服务是用修改版(或未修改版)AGPL 程序来提供的,用户就有权要求你提供该 AGPL 程序的 Corresponding Source——包括你在 AGPL 程序上的任何修改。

一个常见的误解是:我只是调用它的 API,没改它,应该没事。AGPL §13 的触发不要求你”修改”程序——你只要在对用户提供网络服务时”运行”了 AGPL 程序,§13 就适用。

Corresponding Source 的范围:包括你部署时使用的配置文件、patch、初始化脚本(只要这些是 AGPL 程序运行所必需的部分)。

典型陷阱:使用 Neo4j Community Edition(AGPL-3.0)作为知识图谱后端,即使前端是纯 Apache 的,只要这个 SaaS 服务通过 Bolt 协议让用户的请求到达 Neo4j,AGPL 就被触发。解决方案:换用 Neo4j AuraDB(商业云服务)或 TigerGraph(商业版)或 Apache AGE(PostgreSQL 扩展,Apache-2.0)。

7.5 用了 BSL 组件,4 年前 Change Date 已过但历史版本还在分发

BSL 的 Change Date 到期意味着该版本的源代码从那一天起按 Change License(通常是 Apache-2.0)可以使用。但这不代表历史行为被追溯豁免:

# 检查 BSL 文件中的 Change Date 和 Change License
cat LICENSE | grep -E 'Change Date|Change License|Additional Use Grant'
# 示例输出(CockroachDB v22.1 的 BSL):
# Change Date: 2026-05-24
# Change License: Apache License, Version 2.0
# Additional Use Grant: You may make use of the Licensed Work, provided...

八、内部审批流程与工具链

8.1 引入新依赖的 PR 流程

标准流程分四个环节:

开发者提交 PR(含新依赖)
        ↓
  [自动] SPDX 扫描(Syft / go-licenses / pip-licenses)
  ——生成增量 SBOM,检出新增许可证
        ↓
  [自动] 规则引擎(Dependency-Track / FOSSA / Snyk)
  ——对照 license-policy.yaml 分类:白名单通过 / 灰名单告警 / 黑名单阻断
        ↓
  [人工] 灰名单告警 → OSPO 审批(3 工作日)
  黑名单告警 → PR 被 Block,研发必须替换
        ↓
  [自动] 审批通过后合并;合规证据写入 PR 评论,同步归档

8.2 GitHub Actions CI 配置示例

# .github/workflows/license-check.yml
name: License Compliance Check

on:
  pull_request:
    branches: [main, release/*]
  push:
    branches: [main]

jobs:
  sbom-and-license:
    name: SBOM Generation & License Gate
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      pull-requests: write   # 允许在 PR 写评论

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # —— 生成 SBOM ——————————————————————————
      - name: Install Syft
        uses: anchore/sbom-action/download-syft@v0
        with:
          syft-version: "v1.3.0"

      - name: Generate SBOM (CycloneDX JSON)
        run: |
          syft dir:. \
            --output cyclonedx-json=sbom-$(git rev-parse --short HEAD).cdx.json \
            --output spdx-json=sbom-$(git rev-parse --short HEAD).spdx.json

      - name: Upload SBOM as artifact
        uses: actions/upload-artifact@v4
        with:
          name: sbom-${{ github.sha }}
          path: sbom-*.json
          retention-days: 1825   # 保留 5 年(合规存证)

      # —— 许可证规则检查 ———————————————————————
      - name: Install ORT (OSS Review Toolkit) CLI
        run: |
          curl -sSL \
            https://github.com/oss-review-toolkit/ort/releases/download/22.0.0/ort \
            -o /usr/local/bin/ort
          chmod +x /usr/local/bin/ort

      - name: Run ORT Analyzer
        run: |
          ort analyze \
            --input-dir . \
            --output-dir ort-results \
            --output-formats JSON

      - name: Run ORT Evaluator (license policy rules)
        run: |
          ort evaluate \
            --ort-file ort-results/analyzer-result.json \
            --package-curations-file .ort/curations.yml \
            --rules-file .ort/rules.kts \
            --output-dir ort-results \
            --output-formats JSON

      - name: Check for policy violations
        id: policy_check
        run: |
          python3 scripts/check-ort-violations.py \
            --ort-result ort-results/evaluation-result.json \
            --policy .ort/license-policy.yaml \
            --output-md violation-report.md
          echo "violations=$(cat violation-report.md | grep -c 'ERROR' || echo 0)" >> $GITHUB_OUTPUT

      - name: Comment PR with violation report
        if: github.event_name == 'pull_request' && steps.policy_check.outputs.violations != '0'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('violation-report.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## ⚠ 许可证合规检查发现问题\n\n${body}\n\n请联系 OSPO 处理灰名单,或替换黑名单组件。`
            });

      - name: Fail on black-list violations
        if: steps.policy_check.outputs.violations != '0'
        run: |
          echo "发现黑名单许可证,PR 被阻断。详见 violation-report.md"
          exit 1

8.3 GitLab CI 配置示例

# .gitlab-ci.yml(片段)
stages:
  - build
  - compliance

license-compliance:
  stage: compliance
  image: anchore/syft:v1.3.0
  script:
    # 生成 SBOM
    - syft dir:. --output cyclonedx-json=sbom.cdx.json
    # 用 python 脚本对照 policy 检查
    - pip install pyyaml --quiet
    - python3 scripts/license-gate.py --sbom sbom.cdx.json --policy license-policy.yaml
  artifacts:
    paths:
      - sbom.cdx.json
    expire_in: 5 years   # 合规存证 5 年
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

# 脚本:scripts/license-gate.py(示例逻辑)
# 读取 SBOM,对每个包的 licenseConcluded 比对 policy YAML
# deny 列表中的协议 → sys.exit(1)
# review 列表 → 打印告警,sys.exit(0)(不阻断,等人工审批)

8.4 ORT 规则文件示例

// .ort/rules.kts
// ORT Evaluator 规则,Kotlin DSL

val DENY_LIST = listOf(
    "AGPL-3.0-only", "AGPL-3.0-or-later",
    "GPL-2.0-only", "GPL-2.0-or-later",
    "GPL-3.0-only", "GPL-3.0-or-later",
    "SSPL-1.0",
    "OSL-3.0",
    "CC-BY-NC-4.0", "CC-BY-NC-SA-4.0"
)

val REVIEW_LIST = listOf(
    "LGPL-2.1-only", "LGPL-2.1-or-later",
    "LGPL-3.0-only", "LGPL-3.0-or-later",
    "EPL-2.0", "CDDL-1.0"
)

// 对每个包检查
ortResult.getPackages().forEach { pkg ->
    val spdxIds = pkg.pkg.declaredLicensesProcessed.spdxExpression?.licenses() ?: emptySet()

    spdxIds.forEach { license ->
        when {
            DENY_LIST.any { license.contains(it) } ->
                error("LICENSE_POLICY_VIOLATION",
                    "包 ${pkg.pkg.id} 使用了黑名单许可证 $license,禁止引入。")

            REVIEW_LIST.any { license.contains(it) } ->
                warning("LICENSE_REVIEW_REQUIRED",
                    "包 ${pkg.pkg.id} 使用了灰名单许可证 $license,需要 OSPO 审批。")
        }
    }
}

8.5 SBOM 作为证据链

合规存证的核心原则:每次对外发布的版本都必须有对应的 SBOM,保存 5 年以上(部分行业要求更长)。

# 发版时生成带版本号的 SBOM
VERSION=$(git describe --tags --always)
DATE=$(date -u +%Y%m%d)

# CycloneDX 格式(推荐用于许可证合规)
syft packages . \
  --output cyclonedx-json="release-sbom/${VERSION}/sbom-${DATE}.cdx.json"

# SPDX 格式(推荐用于供应链安全 / 出海合规)
syft packages . \
  --output spdx-json="release-sbom/${VERSION}/sbom-${DATE}.spdx.json"

# 生成哈希值用于存档完整性验证
sha256sum release-sbom/${VERSION}/sbom-*.json > \
  release-sbom/${VERSION}/sbom-checksums.sha256

# 提交归档(可以用 Git LFS 或对象存储)
git add release-sbom/${VERSION}/
git commit -m "chore: archive SBOM for release ${VERSION}"

CycloneDX JSON 的关键字段示例(片段):

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "serialNumber": "urn:uuid:f8e9d3b2-4c7a-4e0f-a1b2-c3d4e5f67890",
  "version": 1,
  "metadata": {
    "timestamp": "2026-04-22T08:00:00Z",
    "component": {
      "type": "application",
      "name": "myproduct",
      "version": "2.3.1",
      "purl": "pkg:generic/mycompany/myproduct@2.3.1"
    }
  },
  "components": [
    {
      "type": "library",
      "name": "spring-boot",
      "version": "3.2.5",
      "purl": "pkg:maven/org.springframework.boot/spring-boot@3.2.5",
      "licenses": [{ "license": { "id": "Apache-2.0" } }],
      "hashes": [{ "alg": "SHA-256", "content": "abc123..." }]
    }
  ]
}

关于 CycloneDX 和 SPDX 格式的完整对比,参见 SCA、SBOM 与软件成分分析 第三节。

8.6 出海产品的额外审计要求

面向欧美市场的出海产品,合规审计在 SBOM 之外还需要关注:


九、真实场景剧本(Q&A)

9.1 国内 SaaS 要不要怕 AGPL?

要怕,而且很怕。

一家国内 SaaS 公司使用 Neo4j Community Edition(AGPL-3.0)做知识图谱功能。该功能通过 Web 页面向企业客户提供——用户通过浏览器访问,浏览器请求后端 API,后端通过 Bolt 协议查询 Neo4j,查询结果返回给用户。

AGPL §13 在此场景下如何触发:Neo4j 进程在服务器上运行,通过 Bolt 协议与应用后端交互;应用后端再与用户的浏览器交互。整条链路中,用户在通过计算机网络与 AGPL 程序进行交互——尽管用户并不直接连接 Neo4j,但 Neo4j 是服务的”被修改版本”(即使你没改,运行的配置和部署也可以算作修改范围)。

AGPL 的 §13 要求:向所有通过网络与该程序交互的用户提供 Corresponding Source。

如果这家公司的业务代码(调用 Neo4j 的后端服务层)是其核心竞争力,强制开源会直接损害商业利益。

解决方案: 1. 替换为 Neo4j AuraDB(SaaS 托管,按用量付费,没有 copyleft 传染) 2. 使用 Apache AGE(PostgreSQL 扩展,Apache-2.0,支持 openCypher 语法) 3. 使用 JanusGraph + Apache Cassandra(全部 Apache-2.0) 4. 购买 Neo4j Enterprise 商业许可证

9.2 私有化部署客户现场的 GPL

某 CRM 软件公司为大客户提供私有化部署,客户在自己的服务器上安装运行。系统后端依赖了 GNU Readline(GPL-2.0-or-later)处理 CLI 界面。

问题:向客户交付安装包,Readline 随包分发,触发了 GPL。

处理方式:替换 Readline 为 linenoise(BSD-2-Clause)——功能完全覆盖,约 1000 行 C 代码,没有任何 copyleft。替换周期 1 周。

如果来不及替换(比如合同已经签了、版本已经发出去了): 1. 在随附文档中加入 GPL 版权声明和书面要约,承诺提供 Readline 源码 2. 提供完整的 Readline 版本源码(Readline 源码本身在 GNU FTP 公开,提供链接即可)

9.3 电商产品 Elasticsearch 改协议的迁移

某电商公司在 2021 年 Elasticsearch 宣布从 Apache-2.0 切换到 ELv2/SSPL 后,面临迁移决策。

情况:生产环境使用 Elasticsearch 7.10(Apache-2.0,最后一个 Apache 版本),有升级 7.11+ 的需求(BUG 修复、性能优化)。

判断过程: - Elasticsearch 7.11 → ELv2 + SSPL 双授权,ELv2 禁止提供与 Elastic 功能竞争的 SaaS;SSPL 对 SaaS 提供商有极其严苛的要求。 - 该电商的用例:自己的商品搜索系统,非 SaaS,自用,理论上 ELv2 的 Use Limitation 不直接触发,但法务评估后认为存在解释风险。

最终决策:迁移至 OpenSearch 2.x(AWS fork,Apache-2.0 基线,API 兼容 Elasticsearch 7.10)。迁移工作量约 2 周(主要是 mapping API 和一些插件的重新适配)。

参见 AGPL、SSPL、BSL:云厂商时代的”反云”许可证 第四节关于 Elasticsearch 改协议事件的详细分析。

9.4 自研嵌入式设备用 BusyBox

某 IoT 公司在嵌入式 Linux 设备中使用了 BusyBox(GPL-2.0-or-later)。设备通过线下渠道销售给终端用户。

BusyBox 是典型的对外分发触发 GPL 场景——设备交付给用户,用户接收了包含 GPL 软件的可执行形态。

GPL 义务:向设备接收者提供完整源码或书面要约。

合规操作: 1. 在设备随附手册(或固件菜单)中加入如下内容:

This device contains software licensed under the GNU General Public License v2.
Source code for BusyBox and other GPL-licensed components is available at:
https://www.example.com/opensource/device-model-v1.2-sources.tar.gz

You may also request source code by writing to:
[公司名称] 开源合规部门
[地址]
[邮箱]

This offer is valid for 3 years from the date of the device purchase.
  1. 在公司官网保留一个稳定的源码下载页面,至少维护 3 年(GPLv2 §3(b))。

  2. Software Freedom Conservancy 和 Harald Welte 的 gpl-violations.org 对嵌入式 GPL 违规有积极的执法历史(参见 中国 GPL 诉讼第一案系列),务必认真对待。

9.5 国产数据库内嵌到商业 ERP 里

某 ERP 厂商考虑将国产数据库嵌入到商业产品中一起销售。

以 TiDB 为例:TiDB 核心是 Apache-2.0,TiDB 的许多组件(TiKV、PD)也是 Apache-2.0。直接嵌入使用,Apache-2.0 对闭源产品完全安全。

以 OceanBase 为例:OceanBase 社区版(OceanBase CE)采用 MulanPSL-2.0,也是宽松许可证,允许闭源嵌入,只需保留声明。

以 openGauss 为例:openGauss 核心是 MulanPSL-2.0,安全。

注意:部分数据库的管理工具、前端界面可能使用了不同的许可证(如 AGPL)。嵌入时要分别确认每个组件的许可证,不能只看主库。

参见 OceanBase、TiDB、Apache Doris:中国开源数据库的协议选择

9.6 AI 产品训练时用到了 GPL 数据处理工具

某 AI 公司在训练流水线中使用了 GPL 工具(如某 GPL 的文本预处理库)来清洗训练数据。最终产出的模型权重(.safetensors 文件)对外提供推理服务。

关键判断:GPL 对代码的 copyleft 效力不延伸到数据。GPL 工具处理数据,产出的数据和模型权重不受 GPL 约束——GPL 不是一种”数据传染”的协议,它约束的是代码的衍生作品,而模型权重不是 GPL 工具代码的衍生作品。

训练流水线:
[原始数据] → [GPL 预处理工具(进程独立)] → [清洗后数据] → [训练脚本(Apache-2.0)] → [模型权重]

GPL 边界:仅影响 GPL 预处理工具本身的分发,不影响清洗后数据和模型权重

需要注意的是: 1. 如果你把 GPL 训练工具和模型权重一起打包分发,GPL 工具的分发义务仍然存在(需要提供 GPL 工具的源码或书面要约)。 2. 如果训练数据本身有 CC-NC(非商用)许可证,那是另一个层面的问题,与 GPL 无关。参见 文档、数据、模型的许可。 3. 模型权重的许可证问题(如 LLaMA 2 License、OpenRAIL)是独立话题,不在本文范围内。


十、与海外母公司 / 合资公司的治理

10.1 跨实体的许可证合规一致性

国内研发团队 + 海外母公司或合资公司的常见架构,会产生以下合规冲突:

海外标准更严:欧美大公司的 OSPO 通常已经建立了成熟的 deny 名单(GPL、AGPL 全部禁止)。国内子团队因为历史原因或信息不对称,存在大量灰色依赖。合并后的代码审查往往会暴露这些问题。

法律适用多重化:GPL 是美国版权法框架下的协议,在中国法院如何被认定、执行范围如何,有一定不确定性(参见 中国 GPL 诉讼第一案系列)。但海外业务受海外法律管辖,跨境业务的合规底线应按较严格的一方执行。

10.2 GitHub Enterprise 内部仓库的合规扫描

企业使用 GitHub Enterprise Server(GHES)或 GitHub Enterprise Cloud(GHEC)时,可以配置组织级别的 Dependabot 策略和 Code Scanning:

# .github/dependabot.yml(组织级别模板)
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "license-check-required"
    # 自动忽略已知黑名单包的升级(需配合自定义 Action)
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-major"]

10.3 双重 OSPO 体系

规模较大的跨国公司通常需要建立双重 OSPO 体系:

┌─────────────────────────────────┐
│         全球 OSPO(总部)         │
│  • 制定全局许可证政策              │
│  • 维护 deny/review/allow 名单    │
│  • 对外开源战略与对外贡献管理        │
└──────────────┬──────────────────┘
               │ 下行:策略、工具链、培训
               │ 上行:例外审批请求、风险上报
┌──────────────▼──────────────────┐
│         区域 OSPO(中国)         │
│  • 执行全局策略                   │
│  • 处理国内特殊许可证(木兰等)      │
│  • 国内法律合规(著作权法、等保 2.0)│
│  • 与开放原子基金会 / 国内开源社区对接│
└─────────────────────────────────┘

区域 OSPO 的独特工作内容: - 处理木兰许可证(MulanPSL-2.0、MulanCopyleft-1.0)的引入评估——总部 OSPO 可能对这些协议不熟悉 - 对接国内等保 2.0 合规要求中的”开源组件安全”条款 - 处理国内客户的合规文件要求(国内大型央企客户有时要求提供开源组件清单和著作权证明)

10.4 出海时协议更严格,倒挤国内流程升级

出海业务往往成为国内合规流程升级的催化剂:

建议:如果公司有出海计划,从产品设计阶段就按出海标准建立合规流程,而不是先用国内宽松标准做好产品,上线前再突击整改——突击整改的成本通常是从头建设的 5 倍以上。


十一、工程坑点(真实踩坑)

11.1 MongoDB Go 驱动误以为整个 MongoDB SSPL 传染

:开发者引入了 go.mongodb.org/mongo-driver,看到 MongoDB 已经是 SSPL,误以为驱动也是 SSPL,因此认为整个后端服务被 SSPL 感染,需要开源。

实际情况:MongoDB Go 驱动(mongo-driver)的许可证是 Apache-2.0,不是 SSPL。SSPL 是 MongoDB 服务器本身的许可证。驱动作为客户端库,使用 Apache-2.0 是 MongoDB 公司有意为之——如果驱动也用 SSPL,没有人会用它。

教训:服务器端许可证与客户端/驱动库许可证是分开的。检查时要区分你依赖的是服务器软件本体还是客户端驱动库

# 检查 Go 模块实际的许可证
go-licenses check go.mongodb.org/mongo-driver@v1.14.0
# 输出:go.mongodb.org/mongo-driver, Apache-2.0, ...

11.2 Alpine 基础镜像含 GPL 工具但 MIT 库被拷出来用

:Alpine 镜像中有 BusyBox(GPL-2.0),开发者从 Alpine 镜像里的某个工具目录提取了一个静态链接的 MIT 二进制,打包进自己的产品。但这个 MIT 二进制在编译时静态链接了 musl(MIT)和 zlib(zlib/libpng License),都没问题。

等等——但如果从 Alpine 里取出来的是一个 静态链接了 OpenSSL(Apache-2.0) 的工具,而那个工具的构建过程是由 Alpine 的构建系统管理的,从非官方渠道取出的二进制无法验证确切的构建依赖,存在不确定性。

教训:不要直接从基础镜像里”挖出”二进制复用,应当从已知干净的源码重新构建,并生成 SBOM 作为证据。

11.3 NPM 包声明 MIT,实际 src/ 混入了 LGPL

:某前端 UI 组件库在 package.json 中声明 "license": "MIT",但 src/utils/ 目录下有几个文件的文件头包含:

/*
 * Adapted from jQuery UI 1.12.0 (LGPL-2.1-or-later)
 * Copyright jQuery Foundation and other contributors
 */

使用 SCA 工具扫描时,这些文件被检出为 LGPL,与顶层声明的 MIT 不符。

实际影响:这个组件库通过 webpack 被打包进闭源产品,webpack 会把所有模块混合打包成一个 bundle.js。在闭源桌面应用(Electron)中,这个 bundle.js 被分发给客户,构成对 LGPL 代码的静态链接式分发——需要满足 LGPL 义务。

发现方式

# scancode-toolkit 扫描
scancode --license --copyright -n4 \
  --json-pp scan-result.json \
  node_modules/ui-component-lib/src/

# 检查是否有 LGPL
cat scan-result.json | jq '[.files[].licenses[]? | select(.spdx_license_key | contains("LGPL"))]'

教训package.jsonlicense 字段是自报,不可信。必须用 SCA 工具扫描实际文件内容。

11.4 Vendored Go 模块没有同步 LICENSE

:Go 项目使用 vendor/ 目录锁定依赖,早期 go mod vendor 默认不复制 LICENSE 文件(Go 1.14 之前)。上线后 SCA 扫描发现大量 license: unknown——不是真的 unknown,是 LICENSE 文件没被复制进 vendor。

解决方案

# Go 1.14+ 的 go mod vendor 默认包含 LICENSE 文件
go mod vendor

# 验证 vendor 目录下是否有 LICENSE 文件
find vendor -name 'LICENSE*' | wc -l
# 应该和 go.sum 中的模块数量接近

# 对比 go.sum 中的模块数
grep -c '^' go.sum

现代做法:在 CI 中使用 go-licenses 生成许可证清单,直接从模块缓存读取,不依赖 vendor 目录:

go install github.com/google/go-licenses@latest
go-licenses report ./... \
  --template=csv.tmpl \
  > licenses.csv

11.5 Android AOSP Fork 的 GPL 内核与闭源 HAL

:某 Android 设备厂商 fork 了 AOSP(Apache-2.0),在内核(Linux,GPL-2.0-only)上做了大量定制化修改,同时有闭源的硬件抽象层(HAL)。

问题: 1. Linux 内核是 GPL-2.0-only(没有 or-later)。Linus Torvalds 的立场是:用户态程序通过 system call 调用内核不构成 GPL 传染(Linux COPYING 中有注明)。但内核模块(.ko)如果与内核代码链接,通常需要是 GPL 或兼容协议。 2. 闭源 HAL 如果以内核模块形式实现(如驱动程序),则可能受 GPL 约束;如果通过 HIDL/AIDL 以用户态进程形式与内核交互,则不受约束。 3. AOSP 的 Fork 中,厂商经常不上传修改后的内核源码,这直接违反了 GPL 分发义务。

典型案例:多个 Android 设备厂商曾因未提供内核源码而被 Software Freedom Conservancy 发出合规警告。

正确做法:在设备官网维护一个”内核源码下载”页面,及时更新每个固件版本对应的内核源码。

11.6 Go 的 replace 指令隐藏了替换后的许可证

:Go 项目的 go.mod 中使用 replace 指令把某个官方模块替换为内部 fork 版本。官方模块是 Apache-2.0,但内部 fork 混入了一个 GPL 文件(从另一个开源项目复制用于 bug fix)。go-licenses 默认只报告替换后的模块路径,如果 fork 放在内部仓库且没有标准 LICENSE 文件,工具可能报 unknown

// go.mod(示例)
replace github.com/some/lib => ../internal/forked-lib
# 发现 replace 指令
grep 'replace' go.mod

# 检查替换后的实际许可证
find ../internal/forked-lib -name 'LICENSE*' -o -name 'COPYING*'
# 用 scancode 扫描实际文件内容
scancode --license ../internal/forked-lib/ --json-pp fork-scan.json

教训:对所有 replace 指令必须单独核查;内部 fork 的 LICENSE 管理要和公共依赖同等对待,fork 后立刻在根目录放置准确的 LICENSE 文件。

11.7 Python wheel 内嵌了静态链接的 LGPL C 扩展

:某 Python 数据处理库发布了带预编译 C 扩展的 wheel 包(xxx-1.0-cp311-manylinux_2_17_x86_64.whl)。该 C 扩展在编译时静态链接了一个 LGPL-2.1 的 C 库。wheel 文件里只有编译好的 .so,没有 LGPL 库的源码或书面要约。

引入这个 Python 库后,你分发产品时实际上分发了一个静态链接了 LGPL 代码的 .so——违反了 LGPL §6 的合规要求(需要提供目标文件或源码供用户重链接)。

这类问题很难被标准 SCA 工具发现,因为工具通常只扫描包的顶层许可证声明,不分析二进制内嵌的静态符号。Black Duck 和 FOSSA 的二进制扫描功能可以检测此类情况。

# 检查 .so 文件的静态链接情况
# 动态链接库会在 ldd 输出中可见;静态链接则不会
ldd path/to/extension.so

# 用 nm 检查是否包含 LGPL 库的特征符号
nm --defined-only path/to/extension.so | grep -i "readline\|libzip\|gmp_"

# 用 strings 检查版权声明字符串
strings path/to/extension.so | grep -iE "LGPL|GNU Lesser|Copyright.*Free Software"

# 用 readelf 检查 .comment 节(GCC 通常会在此嵌入构建信息)
readelf -p .comment path/to/extension.so 2>/dev/null || true

十二、选型建议与决策清单

12.1 依赖选型的优先原则

在选择开源依赖时,按以下优先级排序:

  1. 同等功能下,优先选 MIT / BSD / Apache-2.0 / MulanPSL-2.0。这类许可证约束最少,合规成本最低。
  2. LGPL 动态链接是可以接受的,但要在构建系统、打包脚本中明确保证动态链接方式,禁止静态链接。
  3. MPL-2.0 可以接受,但要做好文件级隔离——不要把修改后的 MPL 文件和专有代码混合在同一文件里。
  4. GPL / AGPL / SSPL 应当进入黑名单,除非有明确的架构隔离方案(进程隔离、IPC 接口、独立服务),且已经过 OSPO 和法务审核。
  5. BSL / ELv2 / Commons Clause 需要逐条读 Use Limitation,确认你的用例不在限制范围内。

12.2 每次引入新依赖的快速判断流程

新依赖引入
    │
    ├─ Step 1: 找 LICENSE 文件,识别 SPDX 标识符
    │
    ├─ Step 2: 比对 license-policy.yaml
    │     ├─ 白名单 → 直接通过
    │     ├─ 灰名单 → 提交 OSPO 审批
    │     └─ 黑名单 → 找替代方案 / 提交例外申请
    │
    ├─ Step 3: 检查子目录 / vendored 依赖
    │     └─ 用 syft 扫描,确认无隐藏 GPL/AGPL 组件
    │
    ├─ Step 4: 确认链接方式(动态 vs 静态)
    │     └─ LGPL 动态链接:通过;LGPL 静态链接:提交审批
    │
    ├─ Step 5: 检查版本锁定,确认所用版本的协议
    │     └─ 版本号写入 go.sum / package-lock.json / Pipfile.lock
    │
    └─ Step 6: 更新 SBOM,提交 PR,CI 自动验证

12.3 可下载的合规 Checklist(Markdown 版)

## 开源依赖引入合规 Checklist

组件名称:_______________
版本号:_______________
SPDX 标识符:_______________
引入场景(SaaS/私有化/桌面/嵌入式/内部/SDK):_______________

### 基础核查(每条必须勾选)

- [ ] 仓库根目录存在 LICENSE 文件,且 SPDX 可识别
- [ ] SPDX 标识符已与 license-policy.yaml 比对,结果为:[ ] 白名单 / [ ] 灰名单(已提交审批)/ [ ] 黑名单(已提供替代方案)
- [ ] 是否有 dual license?[ ] 是(已确认使用哪个) / [ ]
- [ ] 子目录 / vendored 代码已扫描,无隐藏 copyleft 组件
- [ ] 已确认链接方式:[ ] 动态链接 / [ ] 静态链接(LGPL 静态链接已提交审批)
- [ ] 版本号已锁定(lockfile 已更新)
- [ ] 已检查 LICENSE 文件的变更历史,所用版本协议无变化
- [ ] syft / scancode 扫描结果已通过,无 unknown license

### 出海产品额外核查

- [ ] 是否含加密功能?[ ] 是(已检查 EAR/ECCN)/ [ ]
- [ ] 是否在美国出口管制实体清单范围内有依赖?[ ]
- [ ] SBOM 已生成并归档(CycloneDX JSON + SPDX JSON)

### 审批记录

- 申请人:_______________
- 申请日期:_______________
- OSPO 审批人:_______________(灰名单时必填)
- 法务确认:_______________(BSL / ELv2 / Commons Clause 时必填)
- 归档位置:_______________

十三、参考资料

以下资料为本文关键论断的来源与延伸阅读:

协议原文

FSF 官方解释

工具与规范

案例与延伸

本系列相关篇目


本文为工程参考,不构成法律意见。涉及具体法律风险请咨询专业法律顾问。


上一篇开源许可证实操手册:从选型到发布

下一篇开源许可与版权工程


  1. GNU GPL FAQ: https://www.gnu.org/licenses/gpl-faq.html↩︎

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / opensource

开源许可与版权工程

面向中国工程团队的开源许可、版权与合规系列。从 GPL、AGPL、Apache、木兰协议到中国真实案例、SCA/SBOM 工具链与出海合规,讲清楚开源在工程落地中的坑与方法。


By .