Trivy RPM 归档(*.rpm)扫描实战:SBOM 生成、漏洞检测与 License 提取
导读:RPM(Red Hat Package Manager)归档文件是 RHEL/CentOS/Fedora 等发行版软件分发的标准载体,但单个
.rpm文件本身不携带操作系统信息,无法直接匹配系统漏洞库。Trivy 通过实验性的 RPM Archive 分析器(默认关闭,可用TRIVY_EXPERIMENTAL_RPM_ARCHIVE环境变量开启),让用户对任意目录下的.rpm文件生成 SBOM、提取 License,并结合"先补 OS 信息、再扫 SBOM"的两段式流程完成漏洞检测。读完本文,你将掌握该实验特性的启用方式、三种扫描器的实操命令、两段式漏洞扫描的原理,以及其底层分析器的源码实现细节。
功能总览与适用范围
本特性对应仓库文档 docs/guide/coverage/others/rpm.md,属于覆盖率文档(Coverage)中"Other"分类。该分类在 docs/guide/coverage/others/index.md 中统一索引。需要特别说明的是,该索引第 32 行明确标注:RPM Archive 特性仅在设置了 TRIVY_EXPERIMENTAL_RPM_ARCHIVE 环境变量时才生效。
Trivy 的 RPM 归档扫描支持以下三类 Scanner:
| Scanner | 是否支持 | 说明 |
|---|---|---|
| SBOM | ✓ | 直接对 *.rpm 文件生成 CycloneDX / SPDX 格式的 SBOM |
| Vulnerability | ✓ | 需先借助 SBOM 补充 OS 信息后才能匹配漏洞库(见下文"两段式工作流") |
| License | ✓ | 若 RPM 头中包含许可证元数据,则直接提取 |
在深入各扫描器的用法之前,先厘清它和仓库中其他 RPM 相关分析器(Analyzer)的区别,以免混淆:pkg/fanal/analyzer/const.go 中定义了三个不同角色——rpm(针对已安装系统的 RPM 数据库)、rpm-archive(本特性,针对独立的 .rpm 归档文件)与 rpmqa(RPM 查询输出)。本主题只涉及 rpm-archive。
实验特性说明与启用方式
本特性当前标记为 EXPERIMENTAL,文档明确警告:"This feature might change without preserving backwards compatibility",即后续版本可能在没有兼容性保障的前提下调整行为。生产使用前应关注版本升级带来的变化。
它默认处于禁用状态,可通过设置环境变量 TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true 来启用。
这一"默认禁用"的机制可以从源码得到直接印证。在 pkg/commands/artifact/run.go 中,构造扫描分析器列表的 disabledAnalyzers 相关逻辑如下:
// Disable RPM archive analyzer unless the environment variable is set
// TODO: add '--enable-analyzers' and delete this environment variable
if os.Getenv("TRIVY_EXPERIMENTAL_RPM_ARCHIVE") == "" {
analyzers = append(analyzers, analyzer.TypeRpmArchive)
}
也就是说:只有当环境变量非空时,rpm-archive 分析器才会被加入可用列表;环境变量为空时该分析器被禁用。代码注释中的 TODO 也提示,未来计划以 --enable-analyzers 命令行参数替代该环境变量,因此启用方式在后续版本中可能变化。
分析器通过 pkg/fanal/analyzer/all/import.go 中的空白导入完成注册(_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/rpm"),实际的归档解析实现位于 pkg/fanal/analyzer/pkg/rpm/archive.go。
场景一:从 RPM 归档目录生成 SBOM
SBOM 扫描是其余场景的基础。Trivy 会分析所有扩展名为 .rpm 的归档文件,对应分析器的 Required 判断逻辑(archive.go):
func (a *rpmArchiveAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return filepath.Ext(filePath) == ".rpm"
}
基本命令示例(以本地 ./rpms 目录为扫描目标):
TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true trivy fs ./rpms --format cyclonedx --output rpms.cdx.json
参数拆解:
TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true:临时启用实验特性(只对本次命令生效,不会写入配置文件)。trivy fs ./rpms:以 filesystem 模式扫描目录,属于"本地文件系统扫描"目标类型(相关的--format、--output等通用标志可参考 trivy_filesystem 命令参考)。--format cyclonedx:指定 SBOM 输出格式。--output rpms.cdx.json:结果写入文件而非标准输出。
输出格式限制:目前该特性只在以下三种格式下工作:
--format cyclonedx:CycloneDX JSON。--format spdx:SPDX 的 tag-value 文本格式。--format spdx-json:SPDX JSON 格式。
如果使用其他格式(例如默认的 table 或 json),RPM 归档分析器不会被触发,自然也不会产出对应组件。
需要提醒的是,生成的 SBOM 中每个组件(component)虽然携带包名、版本、架构等元素(见下节底层实现),但组件中不包含操作系统层面的信息,这正是下一场景必须做二次加工的原因。
场景二:两段式漏洞检测(SBOM + OS 信息补全)
单个 RPM 归档文件不携带 OS 信息,而 Trivy 的漏洞扫描器需要将软件包与特定发行版(如 Red Hat、CentOS)的 CVE 数据匹配——例如 pkg/detector/ospkg/redhat/redhat.go、pkg/detector/ospkg/centos 等 的实现都依赖 OS 的 family/version 去选择对应的漏洞数据库。
因此文档给出的漏洞检测方法是两段式工作流:
- 第一步:对 RPM 归档生成 CycloneDX SBOM;
- 第二步:用
jq手工向 SBOM 中的operating-system类型组件填入 OS 名称与版本; - 第三步:再让 Trivy 以 SBOM 作为输入做漏洞扫描。
完整命令如下:
$ TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true trivy fs ./rpms -f cyclonedx -o rpms.cdx.json
$ jq '(.components[] | select(.type == "operating-system")) |= (.name = "redhat" | .version = "7.9")' rpms.cdx.json > rpms-res.cdx.json
$ trivy sbom ./rpms-res.cdx.json
逐步解读:
-f cyclonedx -o rpms.cdx.json:分别是--format与--output的短写,与场景一命令等价。jq表达式:在 CycloneDX 的components数组中,找出type == "operating-system"的元素,将其name强制改写为redhat、version改写为7.9,其余字段保持不变,并把结果写入rpms-res.cdx.json。trivy sbom ./rpms-res.cdx.json:以补全后的 SBOM 文件为输入执行漏洞扫描(该子命令的完整参数见 trivy_sbom 命令参考)。
其中"OS 名称"必须与 Trivy 已支持的发行版命名一致(如 redhat、centos、fedora、amazon 等,可对照 pkg/detector/ospkg 下的各发行版目录),"OS 版本"则决定匹配该发行版的哪个版本系列数据。上面的例子将 OS 声明为 Red Hat 7.9,那么后续漏洞比对会以 Red Hat 7.9 对应的漏洞库为准。若名称或版本写错(如填了 Trivy 不认识的发行版),漏洞扫描将无法得到正确结果。这就是为什么文档脚注 [^1] 特别注明:漏洞检测"需要先生成 SBOM,并在 SBOM 中加入 OS 信息"。
为什么是
operating-system组件?CycloneDX 规范允许在components中以type: operating-system描述软件所依赖的操作系统。Trivy 在解析 SBOM 输入时会读取该组件来还原 OS 上下文,从而把包与 OS 级 CVE 关联起来。若直接扫描原始rpms.cdx.json(没有操作系统组件或 OS 字段为空),漏洞匹配将无从谈起。
场景三:从 RPM 归档提取 License
License 扫描相对简单。RPM 格式的标准头(header)中有一个 LICENSE 标签字段(由规范定义,tag 名即 LICENSE),记录了该软件包的许可证声明。Trivy 的 RPM 归档分析器在解析头时会读取该字段:
licenses, err := h.GetStrings(rpmutils.LICENSE)
(见 archive.go。)如果归档中包含许可证信息,Trivy 便会将其提取并写入包元数据的 Licenses 字段。换言之,License 扫描完全离线、直接可用,无需像漏洞扫描那样补充 OS 信息。
仓库测试夹具可以佐证这一点:archive_test.go 中针对 socat-1.7.3.2-2.el7.x86_64.rpm 的断言显示,解析结果包含 Licenses: []string{"GPLv2"} 以及 Maintainer: "Red Hat, Inc." 等字段——许可证正是从 RPM 头部直接读出的。
执行 License 扫描时,将 --scanners license 与文件系统目标结合即可(当需要输出 License 全文等更细粒度信息时,可配合 --license-full;从 run.go 的源码注释可以看出,扫描 license 文件头与全文是开销较高的操作,仅在 license 与 --license-full 同时指定时才执行):
TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true trivy fs ./rpms --scanners license
底层实现:RPM 归档分析器做了什么
理解了三个使用场景后,再来看底层实现可以让"为什么会得到这样的结果"更加清晰。完整的分析器实现在 pkg/fanal/analyzer/pkg/rpm/archive.go,核心逻辑可以分为四步。
1. 读取与解析 RPM 头(Analyze → parseHeader)
Analyze 方法(archive.go)调用第三方库 github.com/sassoftware/go-rpmutils 的 rpmutils.ReadRpm 读取归档,再解析其 RPM header。parseHeader(archive.go)提取的元数据包括:
- NEVRA(Name-Epoch-Version-Release-Architecture):通过
h.GetNEVRA()获取,拆分成Name、Version、Release、Epoch、Arch等字段; - License:来自 RPM 头
LICENSE标签; - Vendor / 发行方:来自
VENDOR标签,写入包的Maintainer字段; - Source RPM:
SOURCERPM标签(如socat-1.7.3.2-2.el7.src.rpm),会被拆分出源码包的名称、版本、发行号;若取值为(none)或空串则跳过; - Modularity Label:读取 tag 5096 对应的模块化标签(ModularityLabel),这是 RPM 4.16+ 定义的标签,源码注释中给出了其在 rpm 上游
rpmtag.h中的出处。
2. 生成 PURL
generatePURL(archive.go)把包信息转换成 Software Package URL(purl)。它根据 Vendor 名称(统一转小写后做子串匹配)推断命名空间(namespace):
| Vendor 包含关键字 | PURL namespace |
|---|---|
red hat |
redhat |
fedora |
fedora |
opensuse |
opensuse |
suse |
suse |
| 其他 | 空(未处理,源码中标注了 TODO: Handle more vendors) |
同时把架构作为 arch 限定符拼入 purl。结合测试断言(archive_test.go),socat-1.7.3.2-2.el7.x86_64.rpm 最终生成的 purl 形如 pkg:rpm/redhat/socat@1.7.3.2-2.el7?arch=x86_64。可以看到:版本号是 Version-Release 的拼接形式(1.7.3.2-2.el7),这一点对漏洞匹配很重要,因为 RPM 的修复版本通常以 release 粒度发布。
3. 处理缺失标签的容错
RPM 头并非总能包含上述全部标签。unexpectedError(archive.go)用于捕获 rpmutils.NoSuchTagError 这类"标签不存在"错误并以 Debug 日志记录、返回 nil,从而让分析继续而非中断;其他真正异常才向上返回。
4. 错误文件与版本管理
Analyze 处理损坏的 .rpm 文件时会返回错误(archive_test.go 中的 "broken" 用例即以 strings.NewReader("broken") 验证错误路径)。分析器还实现了 Version() 返回 archiveVersion = 1,与 rpm-archive 分析器类型常量(见 pkg/fanal/analyzer/const.go)共同服务于 Trivy 的分析器缓存与升级失效机制。
实操建议与注意事项
综合文档与源码,使用该特性时有几点值得注意:
- 启用前先确认版本行为:特性处于实验阶段且不保证向后兼容,升级 Trivy 后应重新验证命令与输出。
- 环境变量作用范围:
TRIVY_EXPERIMENTAL_RPM_ARCHIVE仅作用于单个命令进程,CI 中可放在步骤级 env 中,避免全局污染。 - 三种格式之外勿期待 SBOM 输出:仅
cyclonedx、spdx、spdx-json三种格式会触发归档解析。 - 漏洞扫描前务必补齐 OS 信息:直接扫描原始 SBOM 不会得到 OS 级 CVE 结果;OS name/version 的取值要与 Trivy 支持的发行版对齐。
- 该特性适用于独立
.rpm文件集合:如果你面对的是已安装系统(/var/lib/rpm数据库或镜像内的 rpm 数据库),应使用既有的系统包扫描路径(对应rpm/rpmqa分析器),而不是本实验特性。
对开发与测试人员而言,测试数据可用仓库的 mage 任务 mage rpm:fixtures 生成(见 archive_test.go 注释),而归档分析的入口与常量可在 pkg/fanal/analyzer/all/import.go、pkg/fanal/analyzer/const.go 中继续追溯。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00