讨论

依赖许可证审计技能

来自 Wikiprompt,自由的提示词百科全书

Fatih Kadir Akın

2026年10月10日

依赖许可证审计技能 一个完整的AI技能,用于审计依赖许可证,包括工作流程、参考资料、模板、示例和Python脚本。它帮助工程团队在发布前识别许可证风险。

提示词内容收藏

🌐
--- name: dependency-license-audit description: 审计项目依赖项的许可证,根据实际发布方式(SaaS、分发应用或库、内部工具)进行:将许可证名称规范化为 SPDX,解析 OR 和 AND 表达式,标记缺失、AGPL、GPL、源代码可用及不兼容的许可证,并生成排序的修复计划和通知列表。包含一个经过测试的 stdlib Python 审计器。 --- # 依赖许可证审计 你帮助工程团队在发布、收购审查或客户安全问卷发现问题之前,找出依赖项中的许可证问题。你将原始依赖列表转化为一份简短、排序的待修复事项清单,并用通俗语言解释每一项。你是一位细心的工程师,而非律师:你标记风险,并指出何时需要具备法律培训的人员做出决定。 ## 本技能中的文件 - `scripts/audit_licenses.py` - 规范化并分类每个依赖许可证,并根据所选分发模型评级(仅 Python 3 标准库) - `references/license-categories.md` - 宽松、弱 Copyleft、强 Copyleft、网络 Copyleft 和源代码可用在实际中的含义,附常见 SPDX ID - `references/remediation-playbook.md` - 如何修复每类发现,从替换包到添加通知 - `templates/license-audit-report.md` - 报告布局 - `examples/example-saas-audit.md` - 一个 Node.js SaaS 后端的完整审计示例 ## 工作流程 ### 1. 了解项目的发布方式 询问,或推断并声明为假设: - 项目许可证:专有(闭源)或开源许可证(SPDX ID)。 - 分发方式:`saas`(用户仅通过网络访问)、`distributed`(移动或桌面应用、本地安装、设备、SDK、发布的库、提供给客户的容器镜像)或 `internal`(仅员工使用)。 - 任何公司政策:始终允许和始终禁止的许可证。 同一个依赖项可能对一种模式没问题,对另一种模式却是阻碍,所以切勿跳过此步骤。 ### 2. 获取依赖清单 优先使用工具输出而非手工列表,并包含传递依赖: - Python:`pip-licenses --format=json --with-system`(或 `pip-licenses --format=csv`)。 - Node.js:`npx license-checker --json --production`(对开发工具再次运行,不带 `--production`)。 - 其他:从 SBOM 或包管理器导出的 CSV,包含 `name,version,license,scope`。 将构建、测试和 lint 工具标记为 `scope=dev`;它们不随产品发布,因此其发现降级为 INFO。 ### 3. 运行审计 ```bash python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas python3 scripts/audit_licenses.py licenses.json --distribution distributed --deny AGPL-3.0-only python3 scripts/audit_licenses.py deps.csv --json ``` 脚本输出严重性计数、每个包及其规范化许可证和类别的表格、带原因的发现、版本间许可证变更,以及需要在第三方通知中包含文本的许可证。退出码 1 表示至少有一个 HIGH 发现。 如果无法运行脚本,请使用 `references/license-categories.md` 手动分类每个许可证,并在报告中说明。 ### 4. 得出结论前进行验证 - 对每个 HIGH 或 WARN 发现,检查包在其仓库或包存档中的实际 LICENSE 文件;元数据常常错误或过时。 - 对于“缺失”或“SEE LICENSE IN”条目,打开引用的文件并根据其文本分类。 - 对于双重许可的包(OR),记录你选择的选项。 - 对于许可证在版本间变更的包,确认实际安装的版本(锁文件)。 ### 5. 建议和报告 使用 `references/remediation-playbook.md` 为每个发现提供一个具体行动:替换、隔离、固定到旧的宽松版本、购买商业许可证、合规(发布源代码或添加通知),或接受并记录原因。填写 `templates/license-audit-report.md`,如 `examples/example-saas-audit.md` 所示。 ## 规则 - 切勿以法律结论说某许可证“没问题”或“非法”。说明该许可证通常要求什么,以及何时应咨询法律顾问。 - 不要根据包名或流行度猜测许可证。未知保持未知,直到有人阅读许可证文本。 - 根据用户的分发模型评估风险,并指出如果模型改变会有什么变化(例如今天是 SaaS,明年是本地部署)。 - 保持报告简短:先列阻碍项,然后是义务(通知、源代码提供),最后是干净列表的计数。 - 将审计视为快照;建议在每次依赖变更时在 CI 中运行。 FILE:references/license-categories.md # 实际中的许可证类别 脚本中的严重性取决于类别和项目的发布方式。本页用通俗语言解释这些类别。这是工程总结,不是法律建议。 ## 公有领域及近似公有领域 SPDX:`0BSD`、`Unlicense`、`CC0-1.0`、`MIT-0`、`WTFPL` - 无实质性义务。保留通知仍是良好实践。 ## 宽松 SPDX:`MIT`、`ISC`、`BSD-2-Clause`、`BSD-3-Clause`、`Apache-2.0`、`Zlib`、`BSL-1.0`(Boost)、`PSF-2.0`、`Python-2.0`、`CC-BY-4.0`(数据,如 caniuse-lite) - 可在闭源产品中使用、修改和发布。 - 保留版权声明和许可证文本,通常在第三方通知文件或“关于 / 许可证”屏幕中。 - Apache-2.0 还要求保留任何 NOTICE 文件,声明对其文件的重大更改,并包含专利授权,若你就该代码中的专利提起诉讼则授权终止。 - BSD-3-Clause 禁止使用作者姓名推广你的产品。 ## 弱 Copyleft(文件或库级别) SPDX:`LGPL-2.1-*`、`LGPL-3.0-*`、`MPL-2.0`、`EPL-1.0`、`EPL-2.0`、`CDDL-1.0`、`CDDL-1.1`、带链接例外的 GPL(例如 `GPL-2.0-only WITH Classpath-exception-2.0`) - 你可以将其与闭源代码结合,但分发时对库自身文件的更改必须以相同许可证共享。 - LGPL:用户必须能够用自己的版本替换该库。动态链接(共享库、独立 jar、npm 包)通常使这变得容易;静态链接或捆绑到单个二进制文件需要谨慎。 - MPL-2.0:Copyleft 按文件适用。保持 MPL 文件独立,并发布对这些文件的更改。 ## 强 Copyleft SPDX:`GPL-2.0-only`、`GPL-2.0-or-later`、`GPL-3.0-only`、`GPL-3.0-or-later` - 如果你分发包含或链接 GPL 代码的软件,合并后的作品必须以 GPL 及其源代码提供。 - 在你自己的服务器上运行 GPL 软件并让用户通过网络访问不是分发,因此纯 SaaS 使用通常允许。向客户分发应用、本地安装程序、设备固件或容器镜像是分发。 - 版本很重要:`GPL-2.0-only` 代码不能与 GPL-3.0 或 Apache-2.0 代码合并为一个作品。`-or-later` 提供更多空间。 ## 网络 Copyleft SPDX:`AGPL-3.0-only`、`AGPL-3.0-or-later`、`SSPL-1.0`(未经 OSI 批准) - 类似 GPL,外加:如果用户通过网络与修改版本交互,你必须向其提供源代码。 - 对于闭源 SaaS 产品,这是最常见的阻碍。即使未修改的使用也会触发大多数公司的内部审查。 - SSPL 更进一步:将软件作为服务提供可能要求发布整个服务栈的源代码。 ## 源代码可用和非商业 SPDX 或名称:`BUSL-1.1`(商业源代码许可证)、`Elastic-2.0`、Commons Clause 附加条款、`CC-BY-NC-*`、`PolyForm-Noncommercial-1.0.0` - 源代码可见,但使用受限:通常禁止竞争性托管服务、超过限制的生产使用或任何商业使用,有时直到变更日期。 - 始终需要审查确切条款。许多此类许可证在几年后切换到开源许可证,因此旧版或新版可能不同。 ## 未知 - 空许可证字段、`UNKNOWN`、`NOASSERTION`、`UNLICENSED`、`SEE LICENSE IN <file>`、`LicenseRef-*` 或脚本无法识别的名称。 - “无许可证”意味着保留所有权利:你无权使用它。找到真实条款或替换包。 ## 表达式 - `MIT OR Apache-2.0`:你可以选择任一;选择适合你的并记录选择。 - `MIT AND BSD-3-Clause`:你必须遵守两者;最严格者设定类别。 - `GPL-2.0-only WITH <exception>`:例外缩小了 Copyleft 范围;请阅读。 FILE:references/remediation-playbook.md # 修复手册 为每个发现选择一个行动并记录负责人。优先选择消除风险的最便宜行动。 ## 工作顺序 1. 未知和缺失许可证(你可能根本无权使用该代码)。 2. 发布或托管代码中的网络 Copyleft 和源代码可用包。 3. 分发产品中的强 Copyleft,以及开源项目中的版本不兼容。 4. 弱 Copyleft 义务(链接、发布更改)。 5. 其他所有内容的通知。 ## 行动 ### 找到真实许可证 - 打开包的仓库和存档,阅读 LICENSE、COPYING 或主源文件头部。 - 检查元数据是否只是过时(旧包和分支的常见情况)。 - 如果确实没有许可证,请维护者添加(在请求中链接 choosealicense.com)或替换包。 ### 替换包 - 搜索具有宽松许可证和类似 API 的维护替代品。 - 以小时为单位估算工作量,并将其添加到积压中,注明其阻碍的发布。 - 对于传递依赖,查找引入它的直接依赖;不同版本或配置标志通常可以去掉它。 ### 固定到旧许可证下的版本 - 当项目从宽松许可证切换到源代码可用(或反之)时,旧版本保留旧许可证。 - 固定是短期修复:无安全更新。安排适当的替换。 ### 隔离它 - 将 Copyleft 工具作为独立进程或服务运行,通过命令行、HTTP 或队列与你的代码通信,而不是链接到你的代码中。 - 尽可能保持未修改。记录边界。 - 这是使用 GPL 命令行工具的常见方式,但 AGPL 和 SSPL 需法律顾问确认。 ### 购买商业许可证 - 许多双重许可项目(开源加商业)出售消除 Copyleft 义务的许可证。注意价格和续订条款。 ### 合规 - 弱 Copyleft:将库保持为独立、可替换的文件或包;发布对其自身文件的任何更改;包含其许可证。 - 分发产品中的 GPL:仅当整个产品可以在兼容条款下开源。这是商业决策,而非工程决策。 - 托管服务中的 AGPL:向服务用户提供相应源代码,包括你的更改。 ### 接受并记录原因 - 仅开发工具(linter、测试运行器、构建工具)从不发布通常没问题。记录“仅开发,不分发”。 - 仅内部使用 GPL 或 AGPL 工具通常没问题。记录谁使用以及不暴露给外部用户。 ## 第三方通知 - 从清单生成通知文件:包、版本、许可证、版权行、完整许可证文本(每个不同许可证文本一份副本)。 - 按原样包含 Apache NOTICE 文件。 - 在应用内(“设置 > 关于 > 许可证”)、安装程序和容器镜像中发布;对于 SaaS,从页脚或文档链接。 ## 保持干净 - 在每次锁文件变更时在 CI 中运行审计,并在 HIGH 时使构建失败。 - 与法律顾问商定允许列表(例如 MIT、BSD、ISC、Apache-2.0)和禁止列表(例如 AGPL-3.0、SSPL-1.0、BUSL-1.1),并通过 `--allow` 和 `--deny` 传递。 FILE:templates/license-audit-report.md # 依赖许可证审计:<项目名称> 日期:<YYYY-MM-DD> 项目许可证:<proprietary | SPDX ID> 分发方式:<saas | distributed | internal>(<一行:用户如何获得软件>) 清单来源:<工具和命令、锁文件提交> 审计包数:<n>(<运行时 n>、<开发 n>) ## 摘要 - 阻碍项(HIGH):<n> - <一行说明> - 义务(WARN):<n> - <一行说明> - 备注(INFO):<n> - 干净(OK):<n> - 结论:<可发布 | 修复后发布 | 受阻>,因为<原因>。 ## 阻碍项 | 包 | 版本 | 许可证(发现时) | 类别 | 对我们的重要性 | 行动 | 负责人 | 截止日期 | |---|---|---|---|---|---|---|---| | | | | | | | | | ## 义务 | 包 | 许可证 | 我们必须做什么 | 完成? | |---|---|---|---| | | | | | ## 人工验证 - <包>:元数据说 <x>,LICENSE 文件说 <y>。 ## 决策和假设 - 双重许可包及所选选项:<包:选项> - 接受的风险及原因:<包:原因,批准人> - 假设:<例如“管理仪表板仅内部使用”> ## 如果改变发布方式会有什么变化 - <例如:“如果我们发布本地版本,readline-sync(GPL-3.0)将成为阻碍项。”> ## 第三方通知 需要在通知文件中包含完整文本的许可证:<列表> 通知文件位置:<路径或 URL> ## 下次审计 在每次锁文件变更时在 CI 中运行;在<下次发布或日期>前进行全面审查。 _本报告是工程分类,不是法律建议。在依赖任何例外前,阻碍项必须由法律顾问确认。_ FILE:examples/example-saas-audit.md # 示例:Node.js SaaS 后端的许可证审计 ## 请求 “我们是闭源 B2B SaaS。企业客户不断发送关于开源许可证的安全问卷,明年我们可能销售本地版本。这是我们的依赖导出。有什么需要我们担心的吗?” 清单(`deps.csv`,从锁文件导出,18 行): ``` name,version,license,scope express,4.19.2,MIT,runtime lodash,4.17.21,MIT,runtime pg,8.12.0,MIT,runtime sharp,0.33.4,Apache-2.0,runtime dompurify,3.1.6,(MPL-2.0 OR Apache-2.0),runtime ghostscript-wrapper,2.1.0,AGPL-3.0-only,runtime chart-kit-pro,1.4.0,SEE LICENSE IN LICENSE.md,runtime readline-sync,1.4.10,GPL-3.0,runtime mariadb-connector,3.3.1,LGPL-2.1-or-later,runtime fast-csv-lite,0.9.0,,runtime redis-om,0.4.3,Business Source License 1.1,runtime jest,29.7.0,MIT,dev eslint-plugin-gpl-thing,1.0.2,GPL-2.0-only,dev argparse,2.0.1,Python-2.0,runtime tslib,2.6.3,0BSD,runtime lodash,3.10.1,MIT,runtime search-client,7.10.2,Apache-2.0,runtime search-client,8.1.0,Elastic License 2.0,runtime ``` ## 脚本运行 ``` $ python3 scripts/audit_licenses.py deps.csv --project-license proprietary --distribution saas Dependency license audit: 18 packages, project license proprietary, distribution saas HIGH 5 WARN 1 INFO 2 OK 10 ... HIGH chart-kit-pro@1.4.0 SEE LICENSE IN LICENSE.md unknown runtime HIGH fast-csv-lite@0.9.0 (missing) unknown runtime HIGH redis-om@0.4.3 BUSL-1.1 source-available runtime HIGH search-client@8.1.0 Elastic-2.0 source-available runtime HIGH ghostscript-wrapper@2.1.0 AGPL-3.0-only network-copyleft runtime WARN readline-sync@1.4.10 GPL-3.0-only strong-copyleft runtime INFO eslint-plugin-gpl-thing@1.0.2 GPL-2.0-only strong-copyleft dev INFO mariadb-connector@3.3.1 LGPL-2.1-or-later weak-copyleft runtime ... Notes: - search-client: different licenses across versions (Apache-2.0, Elastic-2.0); check which version you ship. ``` 退出码 1(有 HIGH 发现)。 ## 报告(填充模板,缩短) # 依赖许可证审计:acme-api 项目许可证:专有。分发方式:saas(客户使用 Web 应用和 REST API;今天没有安装在他们机器上的内容)。 审计包数:18(16 运行时,2 开发)。 ### 摘要 - 阻碍项(HIGH):5 - 两个无可用许可证的包,两个源代码可用包,一个 AGPL 包。 - 义务(WARN):1 - 一个 GPL-3.0 包,对 SaaS 没问题,但阻碍计划的本地版本。 - 结论:修复后发布。这里没有不寻常的,每项都有便宜的修复。 ### 阻碍项 | 包 | 许可证(发现时) | 对我们的重要性 | 行动 | |---|---|---|---| | chart-kit-pro 1.4.0 | SEE LICENSE IN LICENSE.md | LICENSE.md 是商业许可证,仅允许使用付费密钥。我们没有记录密钥。 | 与购买团队确认,或替换为宽松许可证的图表库。 | | fast-csv-lite 0.9.0 | (missing) | 无许可证意味着无权使用。 | 请维护者添加许可证;同时切换到 MIT 条款的维护 CSV 解析器。 | | redis-om 0.4.3 | BUSL-1.1 | 商业源代码许可证限制生产使用直到变更日期;条款因项目而异。 | 阅读确切的“附加使用授权”。如果禁止我们的使用,替换或购买许可证。 | | search-client 8.1.0 | Elastic-2.0 | Elastic License 2.0 禁止将软件作为托管服务提供。同一树中的版本 7.10.2 是 Apache-2.0。 | 检查锁文件:哪些模块导入 8.1.0?将两者固定到 Apache-2.0 行或迁移到 Apache 许可的分支。 | | ghostscript-wrapper 2.1.0 | AGPL-3.0-only | 我们在 API 内用它渲染客户 PDF;AGPL 网络使用要求向用户提供源代码。 | 将 PDF 渲染移到独立、未修改的工作进程,并让法律顾问确认,或购买商业许可证。 | ### 义务 | 包 | 许可证 | 我们必须做什么 | |---|---|---| | readline-sync 1.4.10 | GPL-3.0-only | 对 SaaS 没问题。如果本地版本随附它,则成为阻碍项。它仅由管理 CLI 脚本使用,因此从服务器镜像中移除。 | | mariadb-connector 3.3.1 | LGPL-2.1-or-later | 保持为正常 npm 依赖(可替换);如果我们修补它,发布更改。 | ### 人工验证 - dompurify:双重许可 `MPL-2.0 OR Apache-2.0`;我们选择 Apache-2.0。 - eslint-plugin-gpl-thing:仅开发,从不发布。已接受。 ### 如果发布本地版本会有什么变化 - readline-sync(GPL-3.0)变为 HIGH,mariadb-connector(LGPL)需要可替换性检查,Apache-2.0 包需要在安装程序中包含 NOTICE 文件。在该发布前使用 `--distribution distributed` 重新运行。 ### 第三方通知 需要完整文本的许可证:Apache-2.0、LGPL-2.1-or-later、MIT、Python-2.0。从应用页脚链接通知页面。 _工程分类,不是法律建议。_ ## 为什么这是好答案 - 它针对这家公司真实的分发模型评估每个包,并显示本地计划的变化。 - 它检查元数据无法显示的事实(商业 LICENSE.md、重复的 search-client 版本)。 - 每个阻碍项都有一个具体、便宜的行动。 FILE:scripts/audit_licenses.py #!/usr/bin/env python3 """根据项目的发布方式审计第三方依赖许可证。 用法: python3 audit_licenses.py deps.csv --project-license proprietary --distribution saas python3 audit_licenses.py licenses.json --distribution distributed [--json] cat deps.csv | python3 audit_licenses.py - [--allow LIC,LIC] [--deny LIC,LIC] 输入(自动检测): 带表头的 CSV:name,version,license[,scope] (scope:runtime | dev;默认 runtime) JSON 列表: [{"name": "...", "version": "...", "license": "...", "scope": "dev"}] pip-licenses --format=json: [{"Name": "...", "Version": "...", "License": "..."}] license-checker --json (npm): {"pkg@1.2.3": {"licenses": "MIT"}, ...} 选项: --project-license 你的项目许可证:proprietary(默认)或 SPDX ID,如 MIT、Apache-2.0、GPL-3.0-only --distribution saas(网络服务,默认)| distributed(应用、二进制、设备、库)| internal --allow / --deny 逗号分隔的 SPDX ID,你的政策始终允许或始终禁止 --json 机器可读输出 许可证名称规范化为 SPDX ID,OR 表达式取最宽松选项,AND 取最严格选项。每个包根据所选分发获得类别和严重性(OK、INFO、WARN、HIGH)。仅开发包不发布,因此其发现降级为 INFO。 退出码:0 无 HIGH 发现,1 至少一个 HIGH,2 用法或输入错误。 这是工程分类辅助,不是法律建议。仅标准库。 """ import argparse import csv import io import json import re import sys ALIASES = { # 小写常见名称 -> SPDX ID "mit": "MIT", "mit license": "MIT", "the mit license": "MIT", "expat": "MIT", "isc": "ISC", "isc license": "ISC", "isc license (iscl)": "ISC", "apache 2.0": "Apache-2.0", "apache-2": "Apache-2.0", "apache 2": "Apache-2.0", "apache2": "Apache-2.0", "apache license 2.0": "Apache-2.0", "apache license, version 2.0": "Apache-2.0", "apache software license": "Apache-2.0", "apache license version 2.0": "Apache-2.0", "bsd": "BSD-3-Clause", "bsd license": "BSD-3-Clause", "new bsd": "BSD-3-Clause", "bsd-3": "BSD-3-Clause", "3-clause bsd": "BSD-3-Clause", "bsd 3-clause": "BSD-3-Clause", "simplified bsd": "BSD-2-Clause", "bsd-2": "BSD-2-Clause", "2-clause bsd": "BSD-2-Clause", "python software foundation license": "PSF-2.0", "psf": "PSF-2.0", "psfl": "PSF-2.0", "mozilla public license 2.0 (mpl 2.0)": "MPL-2.0", "mpl 2.0": "MPL-2.0", "mpl-2": "MPL-2.0", "mpl2": "MPL-2.0", "gplv2": "GPL-2.0-only", "gpl-2.0": "GPL-2.0-only", "gpl v2": "GPL-2.0-only", "gpl2": "GPL-2.0-only", "gplv2+": "GPL-2.0-or-later", "gpl-2.0+": "GPL-2.0-or-later", "gplv3": "GPL-3.0-only", "gpl-3.0": "GPL-3.0-only", "gpl v3": "GPL-3.0-only", "gpl3": "GPL-3.0-only", "gplv3+": "GPL-3.0-or-later", "gpl-3.0+": "GPL-3.0-or-later", "gpl": "GPL-3.0-or-later", "gnu general public license v2 (gplv2)": "GPL-2.0-only", "gnu general public license v3 (gplv3)": "GPL-3.0-only", "gnu general public license v2 or later (gplv2+)": "GPL-2.0-or-later", "gnu general public license v3 or later (gplv3+)": "GPL-3.0-or-later", "lgpl": "LGPL-2.1-or-later", "lgplv2": "LGPL-2.1-only", "lgplv2+": "LGPL-2.1-or-later", "lgpl-2.1": "LGPL-2.1-only", "lgplv3": "LGPL-3.0-only", "lgplv3+": "LGPL-3.0-or-later", "lgpl-3.0": "LGPL-3.0-only", "gnu lesser general public license v2 (lgplv2)": "LGPL-2.1-only", "gnu lesser general public license v3 (lgplv3)": "LGPL-3.0-only", "gnu lesser general public license v2 or later (lgplv2+)": "LGPL-2.1-or-later", "agpl": "AGPL-3.0-only", "agplv3": "AGPL-3.0-only", "agpl-3.0": "AGPL-3.0-only", "gnu affero general public license v3": "AGPL-3.0-only", "gnu affero general public license v3 (agplv3)": "AGPL-3.0-only", "eclipse public license 2.0": "EPL-2.0", "epl 2.0": "EPL-2.0", "epl-2": "EPL-2.0", "public domain": "Unlicense", "unlicense": "Unlicense", "the unlicense": "Unlicense", "cc0": "CC0-1.0", "boost software license 1.0": "BSL-1.0", "zlib license": "Zlib", "zlib": "Zlib", "business source license 1.1": "BUSL-1.1", "elastic license 2.0": "Elastic-2.0", "server side public license": "SSPL-1.0", "sspl": "SSPL-1.0", } CATEGORY = { # SPDX ID 前缀 -> 类别 "MIT": "permissive", "MIT-0": "public-domain", "ISC": "permissive", "BSD-2-Clause": "permissive", "BSD-3-Clause": "permissive", "Apache-2.0": "permissive", "PSF-2.0": "permissive", "Python-2.0": "permissive", "Zlib": "permissive", "BSL-1.0": "permissive", "X11": "permissive", "BlueOak-1.0.0": "permissive", "CC-BY-4.0": "permissive", "CC-BY-3.0": "permissive", "Unicode-DFS-2016": "permissive", "Unicode-3.0": "permissive", "0BSD": "public-domain", "Unlicense": "public-domain", "CC0-1.0": "public-domain", "WTFPL": "public-domain", "MPL-2.0": "weak-copyleft", "EPL-1.0": "weak-copyleft", "EPL-2.0": "weak-copyleft", "CDDL-1.0": "weak-copyleft", "CDDL-1.1": "weak-copyleft", "LGPL-2.1": "weak-copyleft", "LGPL-3.0": "weak-copyleft", "CC-BY-SA-4.0": "weak-copyleft", "GPL-2.0": "strong-copyleft", "GPL-3.0": "strong-copyleft", "AGPL-3.0": "network-copyleft", "SSPL-1.0": "network-copyleft", "BUSL-1.1": "source-available", "Elastic-2.0": "source-available", "Commons-Clause": "source-available", "CC-BY-NC-4.0": "source-available", "CC-BY-NC-SA-4.0": "source-available", "PolyForm-Noncommercial-1.0.0": "source-available", } RANK = {"public-domain": 0, "permissive": 1, "weak-copyleft": 2, "strong-copyleft": 3, "network-copyleft": 4, "source-available": 5, "unknown": 6} SEV_ORDER = {"HIGH": 0, "WARN": 1, "INFO": 2, "OK": 3} NOTICE_LICENSES = {"Apache-2.0"} def unwrap(text): """移除包裹整个表达式的括号:'(MIT OR X)' -> 'MIT OR X'。""" t = text.strip() while t.startswith("(") and t.endswith(")"): depth = 0 for i, ch in enumerate(t): depth += ch == "(" depth -= ch == ")" if depth == 0 and i < len(t) - 1: return t t = t[1:-1].strip() return t def normalize(token): t = unwrap(token) if not t: return "" key = t.lower() if key in ALIASES: return ALIASES[key] for spdx in CATEGORY: if key == spdx.lower() or key.startswith(spdx.lower() + "-") or key == spdx.lower() + "+": return t if key.startswith(spdx.lower() + "-") else spdx return t def category_of(spdx): if spdx.upper().startswith("LICENSEREF") or spdx.upper().startswith("SEE LICENSE"): return "unknown" for prefix, cat in sorted(CATEGORY.items(), key=lambda kv: -len(kv[0])): if spdx == prefix or spdx.startswith(prefix + "-") or spdx.startswith(prefix + "+"): return cat return "unknown" def classify(expr): """返回(规范化表达式、所选许可证、类别、例外)用于许可证表达式。""" raw = (expr or "").strip() if not raw or raw.upper() in ("UNKNOWN", "NONE", "NOASSERTION", "UNLICENSED", "CUSTOM", "N/A"): return raw or "(missing)", raw or "(missing)", "unknown", None if raw.upper().startswith("SEE LICENSE"): return raw, raw, "unknown", None whole = unwrap(raw) if whole.lower() in ALIASES: # 长名称如 "... v2 or later (LGPLv2+)" 是单个许可证 spdx = ALIASES[whole.lower()] return spdx, spdx, category_of(spdx), None text = re.sub(r"\s+", " ", whole.replace("/", " OR ").replace(";", " OR ")) exception = None m = re.search(r"\bWITH\s+([\w.+-]+)", text, re.I) if m: exception = m.group(1) text = text[:m.start()] + text[m.end():] if re.search(r"\sor\s", text, re.I) and not re.search(r"\sand\s", text, re.I): parts, mode = re.split(r"\s+or\s+", text, flags=re.I), "or" elif re.search(r"\sand\s", text, re.I) and not re.search(r"\sor\s", text, re.I): parts, mode = re.split(r"\s+and\s+", text, flags=re.I), "and" elif re.search(r"\s(or|and)\s", text, re.I): parts, mode = re.split(r"\s+(?:or|and)\s+", text, flags=re.I), "and" # 混合:保守处理 else: parts, mode = [text], "single" lics = [normalize(p) for p in parts if normalize(p)] if not lics: return raw, raw, "unknown", exception scored = sorted(lics, key=lambda l: RANK[category_of(l)]) chosen = scored[0] if mode == "or" else scored[-1] cat = category_of(chosen) if exception and cat == "strong-copyleft" and re.search(r"classpath|linking|gcc|runtime|autoconf|bison|bootloader", exception, re.I): cat = "weak-copyleft" joiner = " OR " if mode == "or" else " AND " return joiner.join(lics) + (f" WITH {exception}" if exception else ""), chosen, cat, exception def assess(cat, chosen, distribution, project): proj_cat = "proprietary" if project.lower() == "proprietary" else category_of(normalize(project)) project_spdx = normalize(project) if cat == "unknown": return "HIGH", "许可证缺失或无法识别;发布前找到真实许可证文本。" if cat == "source-available": return "HIGH", "源代码可用或非商业条款通常限制商业使用或托管;需要许可证审查。" if project_spdx.startswith("GPL-2.0-only") and chosen.startswith("Apache-2.0"): return "HIGH", "Apache-2.0 通常被认为与 GPL-2.0-only 项目不兼容。" gpl3_family = re.match(r"(A|L)?GPL-3\.0", chosen) if project_spdx.startswith("GPL-2.0-only") and gpl3_family: return "HIGH", f"{chosen} 不能与 GPL-2.0-only 项目合并;项目需要 GPL-3.0 条款。" if re.match(r"GPL-3\.0|AGPL-3\.0", project_spdx) and chosen.startswith("GPL-2.0-only"): return "HIGH", "GPL-2.0-only 代码不能在 GPL-3.0 下重新许可;请上游提供“或更高版本”条款或替换它。" if proj_cat in ("strong-copyleft", "network-copyleft") and cat in ("strong-copyleft", "network-copyleft"): if cat == "network-copyleft" and proj_cat != "network-copyleft" and distribution == "saas": return "WARN", "GPL 项目中作为服务运行的 AGPL/SSPL:必须向用户提供整个服务的源代码。" return "OK", "Copyleft 项目中的 Copyleft 依赖;检查版本兼容性(例如 GPL-2.0-only 与 GPL-3.0)。" if cat == "network-copyleft": if distribution == "internal": return "WARN", "网络 Copyleft:纯内部使用没问题,但暴露给外部用户会触发源代码义务。" return "HIGH", "网络 Copyleft(AGPL/SSPL):提供服务或发布它要求发布相应源代码。" if cat == "strong-copyleft": if distribution == "distributed": return "HIGH", "GPL 代码随产品分发要求合并作品以 GPL 发布。" if distribution == "saas": return "WARN", "GPL 不因网络使用而触发,但任何未来分发(应用、本地、SDK)都会。" return "INFO", "仅内部使用的 GPL 无分发义务;保持它不进入任何发布内容。" if cat == "weak-copyleft": if distribution == "distributed": return "WARN", "弱 Copyleft:发布许可证,保持库可替换(LGPL 动态链接),并发布对其文件的更改。" return "INFO", "弱 Copyleft:如果稍后分发,发布你对这个库自身文件的任何更改。" if chosen in NOTICE_LICENSES and distribution == "distributed": return "INFO", "Apache-2.0:在你的分发中包含许可证和任何 NOTICE 文件。" return "OK", "宽松:在第三方通知中保留版权声明和许可证文本。" def load(text): stripped = text.lstrip() if not stripped: raise ValueError("输入为空") if stripped[0] in "[{": data = json.loads(text) rows = [] if isinstance(data, dict): # license-checker for key, val in data.items(): name, _, ver = key.rpartition("@") if not name: name, ver = key, "" lic = val.get("licenses", "") if isinstance(val, dict) else val if isinstance(lic, list): lic = " OR ".join(lic) rows.append({"name": name, "version": ver, "license": str(lic), "scope": "runtime"}) return rows for item in data: if not isinstance(item, dict): raise ValueError("JSON 列表项必须是对象") low = {k.lower(): v for k, v in item.items()} lic = low.get("license", low.get("licenses", "")) if isinstance(lic, list): lic = " OR ".join(lic) rows.append({"name": str(low.get("name", "")), "version": str(low.get("version", "")), "license": str(lic or ""), "scope": str(low.get("scope", "runtime") or "runtime").lower()}) return rows reader = csv.DictReader(io.StringIO(text)) fields = [f.strip().lower() for f in (reader.fieldnames or [])] if "name" not in fields or "license" not in fields: raise ValueError(f"CSV 需要至少包含 'name' 和 'license' 列的表头,得到:{', '.join(fields) or '(none)'}") rows = [] for r in reader: r = {(k or "").strip().lower(): (v or "").strip() for k, v in r.items()} if not r.get("name"): continue rows.append({"name": r["name"], "version": r.get("version", ""), "license": r.get("license", ""), "scope": (r.get("scope") or "runtime").lower()}) return rows def audit(rows, args): allow = {normalize(x) for x in args.allow.split(",") if x.strip()} if args.allow else set() deny = {normalize(x) for x in args.deny.split(",") if x.strip()} if args.deny else set() results, seen = [], {} for r in rows: expr, chosen, cat, exc = classify(r["license"]) sev, why = assess(cat, chosen, args.distribution, args.project_license) if exc: why += f" 许可证例外 {exc} 适用;阅读其确切范围。" if chosen in deny: sev, why = "HIGH", f"{chosen} 在你的禁止列表中。" elif chosen in allow and sev != "OK": sev, why = "OK", f"{chosen} 在你的允许列表中({why})" dev = r["scope"] in ("dev", "development", "test", "build") if dev and sev in ("HIGH", "WARN") and chosen not in deny: sev, why = "INFO", "仅开发(不发布):" + why results.append({"name": r["name"], "version": r["version"], "license_raw": r["license"], "license": expr, "effective": chosen, "category": cat, "scope": "dev" if dev else "runtime", "severity": sev, "reason": why}) seen.setdefault(r["name"].lower(), set()).add(expr) notes = [] for name, lics in seen.items(): if len(lics) > 1: notes.append(f"{name}:不同版本许可证不同({', '.join(sorted(lics))});检查你发布的版本。") results.sort(key=lambda x: (SEV_ORDER[x["severity"]], -RANK[x["category"]], x["name"].lower())) counts = {s: sum(1 for x in results if x["severity"] == s) for s in SEV_ORDER} cats = {} for x in results: cats[x["category"]] = cats.get(x["category"], 0) + 1 attribution = sorted({x["effective"] for x in results if x["scope"] == "runtime" and x["category"] in ("permissive", "weak-copyleft")}) return {"project_license": args.project_license, "distribution": args.distribution, "packages": len(results), "severity_counts": counts, "category_counts": cats, "attribution_licenses": attribution, "notes": notes, "results": results} def print_text(rep): print(f"依赖许可证审计:{rep['packages']} 个包,项目许可证 {rep['project_license']},分发方式 {rep['distribution']}") c = rep["severity_counts"] print(f"HIGH {c['HIGH']} WARN {c['WARN']} INFO {c['INFO']} OK {c['OK']}") print("类别:" + ", ".join(f"{k} {v}" for k, v in sorted(rep["category_counts"].items(), key=lambda kv: RANK[kv[0]]))) print() w = max([len(f"{x['name']}@{x['version']}") for x in rep["results"]] + [7]) print(f"{'SEV':<5} {'PACKAGE':<{w}} {'LICENSE':<28} {'CATEGORY':<17} SCOPE") for x in rep["results"]: pkg = f"{x['name']}@{x['version']}" if x["version"] else x["name"] print(f"{x['severity']:<5} {pkg:<{w}} {x['license'][:28]:<28} {x['category']:<17} {x['scope']}") findings = [x for x in rep["results"] if x["severity"] in ("HIGH", "WARN", "INFO")] if findings: print("\n发现:") for x in findings: print(f"- [{x['severity']}] {x['name']}({x['license_raw'] or '无许可证字段'}):{x['reason']}") if rep["notes"]: print("\n备注:") for n in rep["notes"]: print(f"- {n}") if rep["attribution_licenses"]: print("\n第三方通知必须包含许可证文本: " + ", ".join(rep["attribution_licenses"])) print("\n仅分类辅助,不是法律建议。") def main(): p = argparse.ArgumentParser(description="审计依赖许可证。", add_help=True) p.add_argument("path", help="CSV 或 JSON 文件,或 - 用于标准输入") p.add_argument("--project-license", default="proprietary") p.add_argument("--distribution", default="saas", choices=["saas", "distributed", "internal"]) p.add_argument("--allow", default="") p.add_argument("--deny", default="") p.add_argument("--json", action="store_true") args = p.parse_args() try: text = sys.stdin.read() if args.path == "-" else open(args.path, encoding="utf-8").read() rows = load(text) except FileNotFoundError: print(f"错误:文件未找到:{args.path}", file=sys.stderr) return 2 except (ValueError, json.JSONDecodeError) as e: print(f"错误:{e}", file=sys.stderr) return 2 if not rows: print("错误:输入中未找到依赖项", file=sys.stderr) return 2 rep = audit(rows, args) if args.json: print(json.dumps(rep, indent=2)) else: print_text(rep) return 1 if rep["severity_counts"]["HIGH"] else 0 if __name__ == "__main__": sys.exit(main())

登录以查看完整提示词

继续使用:

登录即表示你同意我们的 使用条款 和 隐私政策

用法

此提示词专为 coding 设计。复制上方内容并粘贴到你常用的 AI 工具中。

为获得最佳效果,可将占位符(方括号或大写字母标示)替换为你的具体需求。

参考资料

分类:coding| prompts.chat| license-audit| dependency-management

讨论