3. 组件标识与依赖关系建模

在SBOM工程中,组件标识与依赖关系建模是决定整份清单能否被机器正确解析、比对和追溯的基础。一个组件如果只有名称和版本,几乎无法在跨生态、跨工具的场景中稳定匹配;而如果只记录依赖列表却不区分直接依赖与传递依赖,风险传播路径就会失真。本节围绕四类标识坐标与依赖图构建方法展开,目标是让读者能够为真实项目建立可解析、可追溯的组件依赖模型。

一、四类组件标识坐标的适用边界

PURL(Package URL)是当前SBOM中最通用的开源组件标识方式,其结构为 pkg:type/namespace/name@version?qualifiers#subpath。例如一个 npm 包可表示为 pkg:npm/lodash@4.17.21,一个 Maven 构件可表示为 pkg:maven/org.apache.commons/commons-lang3@3.12.0。PURL 的优势在于跨生态统一,type 字段区分 npm、maven、pypi、golang、cargo 等,qualifiers 可补充 arch、os、repository_url 等限定条件。它适合作为 SBOM 中组件的主标识。

CPE(Common Platform Enumeration)是 NVD 漏洞库长期使用的命名规范,格式如 cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*。CPE 的强项是与 CVE 数据直接关联,弱项是版本区间表达和生态覆盖不如 PURL 精细。实际工程中常采用 PURL 为主、CPE 为辅的双标识策略:用 PURL 做组件唯一性判定,用 CPE 做漏洞库匹配。

GAV 坐标是 Maven 生态的经典三元组 groupId:artifactId:version,例如 org.springframework:spring-core:5.3.20。它天然适合 Java 构件,但无法直接表达 npm、Go 等生态。因此 GAV 通常作为 Maven 组件的补充属性,而不是全局主键。

选择原则:SBOM 内部去重与依赖图节点使用 PURL;漏洞匹配阶段将 PURL 映射为 CPE 或直接使用生态专用漏洞源;Maven 组件保留 GAV 作为可读性补充。

二、直接依赖与传递依赖的区分

直接依赖是项目清单文件中显式声明的组件,例如 package.json 的 dependencies、pom.xml 的 <dependency>。传递依赖是直接依赖自身引入的依赖,例如项目直接依赖 A,A 依赖 B,B 依赖 C,则 B、C 均为传递依赖。风险治理中,传递依赖往往占据组件总量的 70% 以上,也是漏洞引入的主要通道。

建模时必须记录依赖类型与作用域。以 Maven 为例,compile、runtime、provided、test 对运行时风险的影响不同;npm 的 dependencies 与 devDependencies 也应区分。SBOM 中可通过 scope 字段或关系类型表达,避免把测试依赖误判为生产风险。

三、依赖图构建方法

依赖图是有向图,节点为组件标识,边为依赖关系。构建过程通常分三步:解析清单文件得到直接依赖;递归解析每个依赖的元数据得到传递依赖;对相同 PURL 的节点做合并,并保留多条入边以表达多路径引入。合并时需注意版本冲突:同一组件不同版本应作为不同节点,或按解析后的实际版本归一。

以 npm 为例,执行 npm ls --all --json 可得到嵌套依赖树,再将其转换为扁平节点与边集合。Maven 可使用 mvn dependency:tree -DoutputType=json 获取依赖树。转换后的图结构建议采用邻接表存储,节点属性包含 PURL、版本、scope、是否直接依赖,边属性包含依赖类型与解析路径。

{
  "nodes": [
    {"id": "pkg:npm/express@4.18.2", "direct": true, "scope": "runtime"},
    {"id": "pkg:npm/body-parser@1.20.1", "direct": false, "scope": "runtime"},
    {"id": "pkg:npm/qs@6.11.0", "direct": false, "scope": "runtime"}
  ],
  "edges": [
    {"from": "pkg:npm/express@4.18.2", "to": "pkg:npm/body-parser@1.20.1", "type": "transitive"},
    {"from": "pkg:npm/body-parser@1.20.1", "to": "pkg:npm/qs@6.11.0", "type": "transitive"}
  ]
}

构建完成后应做三项校验:是否存在环、是否存在孤立节点、直接依赖是否全部出现在图中。环通常来自生态中的循环依赖,需标记但不阻断;孤立节点可能是解析失败或未声明依赖,应回溯清单文件确认。

组件标识与依赖图构建示意 标识坐标层 PURL(主标识) CPE(漏洞匹配) GAV(Maven补充) scope / 依赖类型 依赖树(直接 + 传递) express@4.18.2 直接依赖 body-parser cookie qs@6.11 debug@2.6 蓝色=直接依赖 浅蓝=一级传递 橙色=深层传递 依赖图(节点合并 + 多入边) express 4.18.2 body-parser 1.20.1 cookie 0.5.0 qs 6.11.0 debug 2.6.9 相同 PURL 合并为单节点,保留多条入边表达多路径引入

四、从依赖图到风险传播判断

依赖图建成后,风险分析才有可靠基础。当某个传递依赖被披露漏洞时,可通过反向遍历入边找到所有引入路径,判断哪些直接依赖需要升级。例如 qs@6.11.0 出现漏洞,反向路径为 express → body-parser → qs,则修复策略可以是升级 express 或直接锁定 body-parser 的版本。若同一组件存在多条引入路径,必须全部处理,否则残留路径仍会引入风险。

工程上建议为每个节点维护 depth(距根节点最短距离)与 paths(引入路径数)。depth 越小说明越接近直接依赖,修复影响面越大;paths 越多说明被多个组件复用,升级时需重点回归。这两个指标可直接用于修复优先级排序。

常见误区:只记录直接依赖而忽略传递依赖,会导致 SBOM 覆盖率不足;只记录组件名称不记录 PURL,会导致跨工具比对时大量误匹配;不区分 scope,会把测试依赖计入生产风险,造成修复资源浪费。

综上,组件标识解决“这个组件是谁”,依赖关系建模解决“它从哪里来、影响谁”。PURL 作为主键保证唯一性,CPE 与 GAV 作为补充保证漏洞匹配与生态兼容,直接/传递依赖与 scope 保证风险传播路径真实,依赖图则把三者组织成可查询、可遍历、可修复的结构。完成本节建模后,后续的漏洞关联、影响面分析和修复验证才有稳定的数据基础。