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 组件的补充属性,而不是全局主键。
二、直接依赖与传递依赖的区分
直接依赖是项目清单文件中显式声明的组件,例如 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"}
]
}
构建完成后应做三项校验:是否存在环、是否存在孤立节点、直接依赖是否全部出现在图中。环通常来自生态中的循环依赖,需标记但不阻断;孤立节点可能是解析失败或未声明依赖,应回溯清单文件确认。
四、从依赖图到风险传播判断
依赖图建成后,风险分析才有可靠基础。当某个传递依赖被披露漏洞时,可通过反向遍历入边找到所有引入路径,判断哪些直接依赖需要升级。例如 qs@6.11.0 出现漏洞,反向路径为 express → body-parser → qs,则修复策略可以是升级 express 或直接锁定 body-parser 的版本。若同一组件存在多条引入路径,必须全部处理,否则残留路径仍会引入风险。
工程上建议为每个节点维护 depth(距根节点最短距离)与 paths(引入路径数)。depth 越小说明越接近直接依赖,修复影响面越大;paths 越多说明被多个组件复用,升级时需重点回归。这两个指标可直接用于修复优先级排序。
综上,组件标识解决“这个组件是谁”,依赖关系建模解决“它从哪里来、影响谁”。PURL 作为主键保证唯一性,CPE 与 GAV 作为补充保证漏洞匹配与生态兼容,直接/传递依赖与 scope 保证风险传播路径真实,依赖图则把三者组织成可查询、可遍历、可修复的结构。完成本节建模后,后续的漏洞关联、影响面分析和修复验证才有稳定的数据基础。