PEP 770 – 通过软件物料清单提高 Python 软件包的可测量性
- 作者:
- Seth Larson <seth at python.org>
- 发起人:
- Brett Cannon <brett at python.org>
- PEP 代理人:
- Brett Cannon <brett at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 已接受
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2025年1月2日
- 发布历史:
- 2024年11月5日, 2025年1月6日
- 决议:
- 2025年4月11日
摘要
今天几乎所有的 Python 软件包都可以通过软件成分分析 (SCA) 工具准确测量。对于无法准确测量的项目,目前没有现有机制来为 Python 软件包添加成分数据以提高可测量性。
软件物料清单 (SBOM) 是一种与技术和生态系统无关的方法,用于描述软件成分、来源、继承等。SBOM 被用作 SCA 工具(例如漏洞和许可证扫描器)的输入,并且在全球软件法规和框架中越来越受到关注。
本 PEP 建议使用包含在 Python 软件包中的 SBOM 文档,作为提高 Python 软件包自动化软件可测量性的一种手段。
动机
可测量性与幽灵依赖
Python 软件包尤其受到“幽灵依赖”问题的影响,即非 Python 编写的软件组件由于多种原因(例如易于安装和与标准兼容)而被包含在 Python 软件包中。
- Python 服务于科学、数据、网络和机器学习用例,这些用例使用 Rust、C、C++、Fortran、JavaScript 等编译或非 Python 语言。
- Python wheel 格式因其易于安装而受到用户青睐。在安装步骤中不执行任何代码,仅解压归档。
- Python wheel 格式要求捆绑共享编译库,但没有一种方法可以编码这些库的元数据。
- 与 Python 打包相关的软件包有时需要解决“引导”问题,因此在其源代码中包含纯 Python 项目。
这些软件组件无法使用 Python 软件包元数据进行描述,因此很可能会被软件成分分析 (SCA) 软件遗漏,这可能导致易受攻击的软件组件无法准确报告。
例如,Python 软件包 Pillow 在 wheel 中包含了 16 个共享对象库,这些库是由 auditwheel 作为构建的一部分捆绑的。使用 Syft 和 Grype 等常用 SCA 工具时,这些共享对象库均未被检测到。如果包含一个 SBOM 文档,并标注所有包含的共享库,那么 SCA 工具就可以可靠地识别这些包含的软件。
构建工具、环境和可复现性
除了软件包的运行时依赖项之外:SBOM 还可以记录用于构建软件包的工具和环境。记录用于构建软件包的精确工具和版本通常是建立可复现构建所必需的。可复现构建是软件的一种特性,可用于在与上游源代码进行比较时检测不正确或恶意修改的软件组件。如果没有记录构建工具和版本的列表,第三方将很难甚至无法验证构建的可复现性。
法规
最近的软件安全法规要求 SBOM,例如安全软件开发框架 (SSDF) 和网络弹性法案 (CRA)。由于它们被纳入这些法规,对开源项目 SBOM 文档的需求预计会很高。一个目标是通过让需要 SBOM 的开源用户使用现有工具自给自足,最大限度地减少对开源项目维护者的需求。
另一个目标是让需要 SBOM 的用户能够贡献他们所依赖项目的 SBOM 信息。目前没有机制来传播这些贡献的结果,因此用户没有动力进行此类工作。
基本原理
使用 SBOM 标准而非核心元数据字段
试图将 SBOM 标准提供的所有字段添加到 Python 软件包核心元数据中,将导致核心元数据字段数量激增,包括需要不断更新以适应 SBOM 领域新需求。
相反,本提案将 SBOM 特定元数据委托给包含在 Python 软件包中、位于 dist-info 下的命名目录中的 SBOM 文档。
本标准也不旨在用 SBOM 取代核心元数据,而是侧重于 SBOM 信息作为核心元数据的补充。包含的 SBOM 仅包含有关软件包归档中包含的依赖项的信息,或有关软件包中顶层软件的信息,这些信息无法编码到核心元数据中,但与 SBOM 用例相关(“软件标识符”、“用途”、“支持级别”等)。
零个或多个不透明的 SBOM 文档
本 PEP 并非要求每个 Python 软件包最多包含一个 SBOM 文档,而是提议一个 Python 软件包中可以包含一个或多个 SBOM 文档。这意味着,尝试使用 SBOM 数据标注 Python 软件包的代码可以这样做,而不必担心损坏已包含在其他 SBOM 文档中的数据。
此外,本 PEP 不透明地处理 SBOM 文档数据,而是依赖 SBOM 数据的最终用户来处理其中包含的 SBOM 数据。这一选择承认 SBOM 标准是一个活跃的开发领域,目前还没有(也可能永远不会有)一个单一的明确的 SBOM 标准,并且 SBOM 标准可以独立于 Python 打包标准继续发展。现有消耗 SBOM 文档的工具已经支持多种 SBOM 标准来应对这一现实。
这些决定意味着本 PEP 能够支持任何 SBOM 标准,并且不偏袒任何一个,而是将决定权交给生产项目和工具以及消费者用户工具。
在不更改元数据版本的情况下向 Python 软件包添加数据
新元数据版本和字段的推出需要许多不同的项目和团队按顺序采用该元数据版本,以避免广泛的破坏。这种影响通常意味着用户和工具能够多快开始使用新打包功能会大幅延迟。
例如,一个元数据版本升级需要更新 PyPI、各种 pyproject.toml
解析和 schema 项目、packaging
库,等待发布,然后 pip
和其他安装程序需要捆绑 packaging
的更改并发布,然后构建后端才能开始发出新的元数据版本,再次等待发布,只有这样项目才能开始使用新功能。即使采用这种谨慎的方法,也不能保证工具不会在新元数据版本和字段上崩溃。
为了避免这种延迟,简化整体上如何包含 SBOM,并为构建后端和工具提供灵活性,本 PEP 提议在 .dist-info
下使用子目录来安全地向 Python 软件包添加数据,同时避免需要新的元数据字段和版本。这种机制允许构建后端和工具在 PEP 被接受后立即开始使用本 PEP 中描述的功能,而不会因为其他项目采用 PEP 而造成“队头阻塞”。
将文件存储在 .dist-info
或 .data
目录中
在二进制分发版中,除了软件本身之外,还有两个顶层目录可以存储文件:.dist-info
和 .data
。本规范选择使用 .dist-info
目录来存储子目录和文件。
首先,.data
目录在已安装的软件包中没有相应的位置,而 .dist-info
则保留了二进制分发版与环境中已安装软件包之间的链接。.data
目录的所有内容在环境中所有已安装的软件包之间合并,这可能导致命名相似的文件发生冲突。
其次,.data
目录下的子目录需要对 Python sysconfig 模块进行新的定义。这意味着定义额外的目录需要等待 Python 的更改,而_使用_该目录需要等待用户采用新的 Python 版本。.dist-info
下的子目录没有这些要求,它们可以在新子目录名称注册后立即被任何用户、构建后端和安装程序使用,无论 Python 或元数据版本如何。
PEP 770 与 PEP 725 有何不同?
PEP 725(“在 pyproject.toml 中指定外部依赖项”)是一个与 PEP 770 有些相似的 PEP,例如都试图在 Python 打包元数据中描述非 Python 软件。本节旨在展示这两个 PEP 正在跟踪不同的信息并服务于不同的用例
- PEP 725 描述的是**抽象依赖项**,例如需要“一个 C 编译器”作为构建时依赖项(
virtual:compiler/c
)或需要在构建时链接“OpenSSL 库”(pkg:generic/openssl
)。PEP 770 描述的是**具体依赖项**,更类似于“锁定文件”中的依赖项,例如通过 AlmaLinux 发行版分发的软件库的精确名称、版本、架构和哈希(pkg:rpm/almalinux/libssl3@3.2.0
)。对于构建依赖项等情况,这可能导致通过 PEP 725 请求依赖项,然后在构建后使用 PEP 770 在 SBOM 中具体记录。 - PEP 725 用于描述**外部依赖项**,由用于构建或运行软件的系统提供。PEP 770 用于描述**Python 软件包归档中捆绑的软件**,SBOM 文档不描述系统上的软件。
- **PEP 725 主要关乎标识**,使用软件标识符列表。PEP 770 提供**SBOM 标准的完整功能**,用于描述各种软件属性,例如许可证、校验和、下载位置等。
- **PEP 725 和 PEP 770 具有不同的用户和用例**。PEP 725 主要供手动在
pyproject.toml
中编写依赖项的人类使用。信息的用户是构建后端和想要从源代码构建软件的用户。PEP 770 主要供能够生成 SBOM 文档以包含在 Python 软件包归档中的工具,以及想要使用有关已安装软件的 SBOM 文档来执行其他任务(例如漏洞扫描或软件分析)的 SBOM/SCA 工具使用。
规范
实施本 PEP 所需的更改包括
- 明确预留子目录
.dist-info/sboms
。 - 构建分发 (wheel) 和已安装项目规范的补充
除了上述内容,还将创建一个信息性 PEP,用于指导工具如何使用已包含的 SBOM 文档和其他 Python 软件包元数据来生成完整的 Python 软件包 SBOM 文档。
预留 .dist-info/sboms
目录
本 PEP 引入了一个新的注册表,用于在 分发归档 和 已安装项目 项目类型的 .dist-info
目录中允许的保留子目录名称。该注册表的未来新增将通过 PEP 流程进行。该注册表的初始值如下:
子目录名称 | PEP / 标准 |
---|---|
licenses |
PEP 639 |
license_files |
PEP 639 (仅草案) |
LICENSES |
REUSE 许可框架 |
sboms |
PEP 770 |
有关选择此目录名称以避免向后不兼容的完整方法,请参阅向后兼容性。
项目格式中的 SBOM 文件
现有的规范将进行一些补充。
- 已构建分发版 (wheels)
- wheel 规范将更新,以添加保留目录名称的新注册表,并反映如果指定了
.dist-info/sboms
子目录,则该目录包含 SBOM 文件。 - 已安装项目
- “记录已安装项目”规范将更新,以反映如果指定了
.dist-info/sboms
子目录,则该目录包含 SBOM 文件,并且安装工具必须从 wheels 复制此目录中的任何文件。
SBOM 数据互操作性
本 PEP 将 SBOM 文档中包含的数据视为不透明的,承认 SBOM 标准是一个活跃的开发领域。然而,对于 SBOM 数据生产者来说,有一些考虑事项,遵循这些考虑事项将提高 Python 软件包中 SBOM 数据的互操作性和可用性。
- SBOM 文档应该使用广为接受的 SBOM 标准,例如 CycloneDX 或 SPDX。
- SBOM 文档在可用的 SBOM 标准中应使用 UTF-8 编码的 JSON(RFC 8259)。
- SBOM 文档应包含所用 SBOM 标准的所有必需字段。
- SBOM 文档应包含所用 SBOM 标准的“创建时间”和“创建工具”字段。此信息对于尝试重构 Python 软件包构建的不同阶段的用户非常重要。
- SBOM 文档描述的主要组件应为 Python 软件包中的顶层软件(例如,Pillow 软件包的“pkg:pypi/pillow”)。
- 所有非主要组件都应在关系图中包含一个或多个路径,以显示组件之间的关系。如果未包含此信息,SCA 工具可能会排除关系图之外的组件。
- 所有软件组件都应具有名称、版本和一个或多个软件标识符(PURL、CPE、下载 URL)。
PyPI 和其他索引可以验证本 PEP 指定的 SBOM 文档内容,但不得验证或拒绝未知 SBOM 标准、版本或字段的数据。
向后兼容性
预留 .dist-info/sboms
子目录
新的保留子目录 .dist-info/sboms
代表了一个以前未被记录的新保留,因此有可能破坏现有工具所做的假设。
为了检查目前 .dist-info
子目录名称的使用情况,已对 PyPI 上软件包归档中的所有文件进行了查询
SELECT (
regexp_extract(archive_path, '.*\.dist-info/([^/]+)/', 1) AS dirname,
COUNT(DISTINCT project_name) AS projects
)
FROM '*.parquet'
WHERE archive_path LIKE '%.dist-info/%/%'
GROUP BY dirname ORDER BY projects DESC;
请注意,这仅包含_文件_的记录,因此不会返回空目录的结果。空目录被普遍使用并以某种方式承载负载的可能性不大,因此是使用此方法可接受的风险。此查询产生了以下结果:
子目录 | 独立项目 |
---|---|
licenses |
22,026 |
license_files |
1,828 |
LICENSES |
170 |
.ipynb_checkpoints |
85 |
license |
18 |
.wex |
9 |
dist |
8 |
include |
6 |
build |
5 |
tmp |
4 |
src |
3 |
calmjs_artifacts |
3 |
.idea |
2 |
上面未显示的大约有 50 个其他子目录名称,它们仅在一个项目中使用。从这些结果我们可以看到
.dist-info
下的大多数子目录都与许可有关,其中一个(licenses
)由 PEP 639 指定,而其他(license_files
、LICENSES
)则来自 PEP 639 的草案实现。sboms
子目录与现有用途不冲突。.dist-info
下的其他子目录名称似乎要么不广泛,要么是意外。
由于此查询,我们可以看到已经有一些项目将目录放置在 .dist-info
下,因此我们不能要求构建前端对未注册的子目录发出错误。相反,建议是构建前端可以在这种情况下警告用户或引发错误。
安全隐患
SBOM 文档的有用性仅取决于其中编码的信息。如果 SBOM 文档包含不正确的信息,这可能导致 SCA 工具进行不正确的下游分析。因此,将 SBOM 数据包含到 Python 软件包中的工具必须对它们正在记录的信息充满信心。SBOM 除了已知数据外,还能够记录“已知未知”。当对正在记录的数据不确定时,建议采用这种做法,以便用户进行进一步分析。
因为 SBOM 文档可以编码有关 Python 软件包构建的原始系统的信息(例如,操作系统名称和版本,不太常见的是路径名称)。这些信息有可能通过 Python 软件包通过 SBOM 泄露给安装程序。如果此信息敏感,则可能构成安全风险。
如何教授此内容
大多数 Python 和 Python 软件包的典型用户不需要了解本标准的细节。本标准的细节对于 Python 软件包的维护者和 SBOM 生成工具和漏洞扫描器等 SCA 工具的开发者来说最为重要。
Python 软件包维护者需要了解什么?
Python 软件包元数据已经可以描述软件包归档中包含的顶层软件,但如果软件包归档中包含顶层软件之外的其他软件组件怎么办?例如,Pillow 的 Python wheel 包含了一堆捆绑在其中的其他软件库,如 libjpeg
、libpng
、libwebp
等等。这种情况是本 PEP 最有用的地方,用于向 Python 软件包添加有关捆绑软件的元数据。
一些构建工具可能能够自动标注捆绑的依赖项。通常,当这些依赖项来自“打包生态系统”(例如 PyPI、Linux 发行版、Crates.io、NPM 等)时,工具可以自动标注捆绑的依赖项。
SBOM 文档用户需要了解什么?
本 PEP 的许多用户可能不知道它的存在,相反,他们的软件成分分析工具、SBOM 工具或漏洞扫描器在升级后将开始提供更全面的信息。对于对这些新信息来源感兴趣的用户,SBOM 元数据的“工具”字段已经提供了指向生成其 SBOM 的项目的链接。
对于需要描述其开源依赖项的 SBOM 文档的用户,第一步应该始终是“自己创建它们”。使用上述基准,可以记录并向用户推荐一份已知对 Python 软件包准确的工具列表。对于需要额外手动 SBOM 标注的项目:可以推荐贡献此数据和维护数据的工具的技巧。
请注意,由于依赖项、Python 版本、平台、架构等方面的差异,SBOM 文档在不同的 Python 软件包归档中可能有所不同。因此,用户只应使用实际下载和安装的 Python 软件包归档中包含的 SBOM 文档,不要假设给定软件包版本中所有归档的 SBOM 文档都相同。
参考实现
Auditwheel fork 生成 CycloneDX SBOM 文档,以包含在 wheels 中,描述捆绑的共享库文件。这些 SBOM 文档对于 Syft 和 Grype SBOM 和漏洞扫描器按预期工作。
被拒绝的想法
要求单一的 SBOM 标准
目前没有普遍接受的 SBOM 标准,这个领域仍在快速发展(例如,SPDX 在 2024 年 4 月发布了其标准的新主要版本)。目前关于 SBOM 的大部分讨论和开发都集中在两个 SBOM 标准上:CycloneDX 和 SPDX。
为了避免在明确的赢家出现之前将 Python 生态系统锁定在特定的标准中,本 PEP 将 SBOM 文档视为不透明的,并且仅提出建议以促进与 SBOM 文档数据的下游消费者的兼容性。
本 PEP 中的任何决定都不会限制未来的 PEP 选择单一的 SBOM 标准。今天使用 SBOM 数据的工具已经需要支持多种格式来处理这种情况,因此未来更新为只要求一个标准的标准不会对下游 SBOM 工具产生任何影响。
使用元数据字段在归档中指定 SBOM 文件
本规范的早期版本使用 Sbom-File
元数据字段来指定源或二进制分发归档中的 SBOM 文件。这将使实现类似于 PEP 639,后者使用 License-File
字段来枚举归档中的许可证文件。
这种方法的主要问题是 SBOM 文件可以来自静态和动态来源:例如版本控制的源代码、构建后端,或者在构建完成后由工具添加 SBOM 文件(例如 auditwheel)。
元数据字段必须是静态的或动态的,不能两者兼顾。这与 SBOM 数据的最佳情况直接冲突:SBOM 文件由工具在 Python 软件包构建过程中自动添加,无需用户参与或知情。将这种情况与几乎总是静态的许可证文件进行比较。
639 样式的方法最终被放弃,转而通过 SBOM 文件在 .dist-info/sboms
目录中的存在来定义 SBOM。这种方法允许构建后端和工具添加自己的 SBOM 数据,而无需静态/动态冲突。
未来的 PEP 将定义静态定义要添加到 .dist-info/sboms
目录的 SBOM 文件的过程。
参考资料
- 可视化 Python 软件包 SBOM 数据流。这是一张图表,展示了本 PEP 如何融入 Python 软件包 SBOM 数据故事的更大图景。
- 使用 auditwheel 为 Python wheels 添加 SBOM。这是 auditwheel 的一个分支的早期结果,用于向 wheel 添加 SBOM 数据,然后使用 SBOM 生成工具 Syft 检测已安装软件包中的 SBOM。
- 查询 PyPI 上每个版本中的每个文件。使用了 Tom Forbes 在 py-code.org 上提供的数据集来检查
.dist-info
文件中的子目录使用情况。
致谢
感谢 Karolina Surma 撰写并领导 PEP 639 获得通过。本 PEP 的初始设计深受 PEP 639 的启发,并采用了在 .dist-info
下使用子目录来存储文件的类似方法。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0770.rst