pnpm sbom 仓库 URL 规范化修复:让 CycloneDX externalReferences 与 SPDX homepage 输出合法可用的地址
2026/9/19 16:09:19 网站建设 项目流程

pnpm sbom 仓库 URL 规范化修复:让 CycloneDX externalReferences 与 SPDX homepage 输出合法可用的地址

【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm

导读

本文围绕 pnpm 的 SBOM(软件物料清单)生成能力,深入讲解一项针对仓库地址输出的补丁修复:pnpm sbom现在会在 CycloneDX 的externalReferences[].url与 SPDX 的homepage字段中发布合法、可被下游消费的 URL,而不是像过去那样直接透传 package.json 中的原始值。读完本文,你将掌握 pnpm 是如何把 npm shorthand(如vercel/ms)、scp 风格 git 远程(如git@github.com:vercel/ms.git)展开为完整地址、如何去除 URL 中内嵌的凭据、以及如何在生成 SBOM 时对齐 npm 的仓库地址语义。

背景:SBOM 中的仓库地址为什么会被拒收

SBOM(Software Bill of Materials)是一份描述软件组成成分(组件、依赖、许可证、来源地址)的结构化清单。pnpm 通过pnpm sbom命令为项目生成两种主流格式:

  • CycloneDX:每个组件通过externalReferences[]描述外部引用,其中vcs类型的引用指向源码仓库;
  • SPDX:每个包通过homepage字段声明主页/仓库地址。

问题在于:package.json 的repository字段允许多种写法——完整的 HTTPS URL、npm shorthand(如vercel/msgitlab:group/subgroup/project)、scp 风格 git 远程(如git@github.com:vercel/ms.git)等。CycloneDX 规范要求 URL 必须是合法的iri-reference,而 npm shorthand 或 scp 远程并非合法 URL。pnpm 早期版本直接把 manifest 中的原始值写入 SBOM,导致生成的文档被 Dependency-Track 等严格校验的消费方拒收(对应上游问题 pnpm/pnpm#14773)。

本次补丁(记录于 .changeset/fix-sbom-repository-url.md,影响@pnpm/deps.compliance.sbom@pnpm/deps.compliance.commandspacquetpnpm四个包)的核心目标就是:让 SBOM 输出的仓库地址与 npm 解析出的地址保持一致,且不泄露任何内嵌凭据

修复规则一览:五种输入,五条明确出路

根据 changeset 的表述并结合源码实现(getPkgMetadata.ts 中的repositoryFromFieldurlWithoutCredentials),仓库地址的输出规则可以归纳为下表:

输入形态示例输出行为
npm shorthand(owner/repo)vercel/ms展开为git+https://github.com/vercel/ms.git这类 npm 推导出的git+httpsURL
带前缀的 shorthandgitlab:group/subgroup/project同样展开为对应的git+httpsURL
scp 风格远程git@github.com:vercel/ms.git展开为与 npm 一致的 HTTPS 地址
其他普通 URLhttps://example.com/repo.git以规范化(normalized)形式输出,去除内嵌凭据(userinfo 中的用户名/密码/令牌)
不指向仓库的值邮箱地址、mailto:file:、无 owner 的gist:直接省略该字段,不写入 SBOM

其中最后一条尤为关键:任何无法确定指向一个仓库的值,都不会被写入 SBOM。例如repository字段填了一个邮箱地址,那么在 CycloneDX 中就不会出现对应的vcs引用,SPDX 中也不会出现对应的homepage

源码级拆解:npm shorthand 如何被展开

仓库 URL 规范化的核心逻辑位于 getPkgMetadata.ts 的repositoryFromField函数:

export function repositoryFromField (field: unknown): string | undefined { const raw = urlFieldValue(field) if (!raw) return undefined const absolute = absoluteUrl(raw) if (absolute) return absolute const hosted = HostedGit.fromUrl(raw) // 无 owner 的 shorthand 会推导出 owner 为 null 的 URL; // gist:<id> 同样无 owner,且 pnpm v12 的解析器不支持 gist, // 因此这里直接丢弃,保证两个版本行为一致。 if (!hosted?.user) return undefined const expanded = hosted.https() return expanded ? urlWithoutCredentials(expanded)?.href : undefined }

其处理流程是:

  1. 先按绝对 URL 解析absoluteUrl调用urlWithoutCredentials,只有带 host 的完整 URL 才被接受。mailto:file:这类值因为 "没有 host" 而不构成可访问的仓库地址,直接返回undefined
  2. 再走 hosted-git-info 展开:对于 shorthand 和 scp 远程,借助hosted-git-info库(见 getPkgMetadata.ts 第 7 行import HostedGit from 'hosted-git-info')解析出托管平台信息,调用hosted.https()得到与 npm 一致的git+https地址。
  3. 无 owner 直接放弃vercel/ms有 owner(vercel)所以可以展开;而gist:<id>这类无 owner 的 shorthand 会被丢弃——这正是注释中强调的、与 pnpm v12 解析器行为保持一致的关键决策。

值得注意:repository字段本身也可能是对象形态{ type, url }urlFieldValue会统一提取其中的url字符串再处理,兼容{ "type": "git", "url": "..." }的常见写法。

源码级拆解:凭据剥离与非法字符净化

SBOM 文档会被分发给第三方(审计平台、合规工具),绝不能把私有仓库地址中的用户名/密码/令牌写进去urlWithoutCredentials承担了这一职责:

function urlWithoutCredentials (raw: string): URL | undefined { let url: URL try { url = new URL(raw) } catch { return undefined } // 解析器会保留不以 %XX 转义序列开头的 %,而 iri-reference 不允许这种字符。 if (/%(?![0-9a-f]{2})/i.test(url.href)) return undefined // ssh: 与 git+ssh: 协议的 host 本身形如 git@github.com, // 此时无密码的用户名属于地址的一部分;其他协议下用户名可能就是密钥本身。 const sshLogin = !url.password && (url.protocol === 'ssh:' || url.protocol === 'git+ssh:') if (!sshLogin) { url.username = '' url.password = '' } return url }

这里有两层语义:

  • 合法化:WHATWG URL 解析器会做百分号编码等规范化处理(例如把空白和控制字符转义),这恰好满足了 CycloneDXiri-reference的语法要求;同时,%后跟非十六进制字符(不是合法的%XX转义)的值会被直接拒绝,因为iri-reference不允许这类裸百分号。
  • 去凭据:除非是ssh:/git+ssh:协议下无密码的用户名(此时git@github.com中的git是登录用户而非秘密),否则一律清空usernamepassword。这样https://user:token@example.com/repo.git会被输出为https://example.com/repo.git

此外,bugs字段的 URL(bugsUrlFromField)同样只接受http:/https:协议且剥离凭据后的地址,用于 CycloneDX 的issue-tracker引用。

源码级拆解:git 依赖的下载地址如何规范化

除了 manifest 中的repository字段,lockfile 中git 依赖resolution.repo也需要规范化为合法的下载 URL。这由 collectComponents.ts 中的gitDownloadUrl完成:

export function gitDownloadUrl (resolution: Resolution): string | undefined { if (resolution.type !== 'git') return undefined const needsGitPlusPrefix = resolution.repo.includes('://') && !resolution.repo.startsWith('git+') const prefix = needsGitPlusPrefix ? 'git+' : '' return `${prefix}${resolution.repo}#${resolution.commit}` }

规则非常直接:只有包含://的协议式地址才需要补git+前缀,且不会重复加前缀;git@github.com:user/repo.git这类 scp 风格地址保持原样;非 git 类型(如 tarball 解析)返回undefined。测试 gitDownloadUrl.test.ts 覆盖了五种情形:

  • https://github.com/stevemao/left-pad.gitgit+https://github.com/stevemao/left-pad.git#<commit>
  • ssh://git@github.com/user/repo.gitgit+ssh://git@github.com/user/repo.git#<commit>
  • git@github.com:user/repo.git(scp 风格)→ 不加git+前缀,原样追加#<commit>
  • git+ssh://git@github.com/user/repo.git(已有前缀)→ 不重复加前缀
  • tarball 解析 → 返回undefined

修复落点:字段在两种格式中如何呈现

规范化后的仓库地址最终写入两个位置,分别对应两种 SBOM 格式:

CycloneDX(serializeCycloneDx.ts):组件级和根组件级的externalReferences数组中,仓库地址以vcs类型出现(homepage作为website类型、bugs作为issue-tracker类型一并输出):

if (comp.repository) { externalRefs.push({ type: 'vcs', url: comp.repository, }) }

SPDX(serializeSpdx.ts):仓库地址写入包的homepage字段:

if (rootComponent.repository) { rootPackage.homepage = rootComponent.repository }

由于修复后的repository值要么是合法 URL,要么是undefined(被省略),SPDX 中不再会出现homepage被填充成非法值、或者downloadLocation退化的情况——下游工具校验时自然不会再把文档判为非法。

如何验证:命令行与测试

生成 SBOM 的命令如下(--sbom-format必选,指定cyclonedxspdx):

# 生成 CycloneDX 格式 pnpm sbom --sbom-format cyclonedx # 生成 SPDX 格式 pnpm sbom --sbom-format spdx # 仅基于 lockfile 数据生成(跳过 store 元数据读取) pnpm sbom --sbom-format cyclonedx --lockfile-only

相关的其余选项(见 sbom.ts 的cliOptionsTypes)还包括:--sbom-type <library|application>(根组件类型,默认library)、--sbom-spec-version <1.5|1.6|1.7>(CycloneDX 规范版本,默认1.7)、--sbom-authors--sbom-supplier--split--exclude-peers等。

仓库内的测试用例可以直接印证本次修复的行为:

  • gitDownloadUrl.test.ts:验证 git 解析地址的前缀处理与 scp 风格原样输出;
  • getPkgMetadata.test.ts:覆盖repositoryFromField对 shorthand、scp 远程、凭据剥离、无 owner 值的处理;
  • serializeCycloneDx.test.ts 与 serializeSpdx.test.ts:验证两种格式序列化后字段的正确性;
  • 命令级集成测试位于 commands/test/sbom/ 下,其fixtures/中包含sbom-repositorysbom-component-repository等专门用于仓库地址场景的 fixture。

兼容性与行为一致性

本次修复不仅是"把值改对",还刻意保持了与 npm 及 pnpm 其他版本的语义对齐:

  • 与 npm 对齐vercel/msgitlab:group/subgroup/project展开出的地址,正是 npm 自身推导的git+httpsURL;
  • 与 pnpm v12 对齐:对gist:<id>这类无 owner 的 shorthand 直接丢弃,避免产出 owner 为null的畸形 URL;
  • 跨格式一致:同一份规范化后的地址既用于 CycloneDX 的vcsexternalReference,也用于 SPDX 的homepage,保证两份 SBOM 对仓库来源的描述一致;
  • 安全默认:任何协议下的 URL 都经过凭据剥离(ssh 登录用户除外),SBOM 不会成为凭据泄露通道。

小结

pnpm sbom的仓库 URL 修复看似是一处小改动,实则是"合规性文档必须输出合法、可消费、安全数据"这一原则的典型落地:通过 hosted-git-info 展开 npm shorthand 与 scp 远程、通过 WHATWG URL 规范化并剥离凭据、对不指向仓库的值整体省略,pnpm 让生成的 CycloneDX 与 SPDX 文档能被 Dependency-Track 等严格校验工具顺利接受。对任何使用pnpm sbom做供应链合规审计的团队来说,理解这套规范化规则,就能预判生成的 SBOM 中每个仓库地址的最终形态。

【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询