漏洞扫描报了 CVE,不等于制品真的有风险:用 vexctl 把“不受影响”结论做成可签名证据
漏洞扫描的职责是发现“组件版本与已知漏洞相交”的可能性;它通常不知道你的程序是否走得到那条代码路径、发行版是否已在上游打了补丁,或某个可选功能是否根本没有启用。于是,安全团队会遇到一种尴尬的重复劳动:扫描报告一直亮红灯,研发在工单、聊天记录或某份 ignore 文件里写下“当前不受影响”,下一次镜像构建又从头解释一遍。
真正缺少的不是再找一个扫描器,而是把影响判断做成能被下游读取、审查和迁移的证据。OpenVEX 是用于表达漏洞可利用性与影响状态的开放格式;vexctl 则是 OpenVEX 项目提供的 Go 工具,用于创建、转换和证明(attest)VEX 元数据。它采用 Apache-2.0 许可证。对已经在做镜像扫描、SBOM 或制品签名的团队而言,它能补上“发现告警”与“接受例外”之间缺失的一环。
不要把“发现漏洞”和“受影响”混为一谈
假设扫描结果显示基础镜像中的某个包关联到 CVE。这个结果值得被认真处理,但它不是自动放行或自动阻断的判决。团队至少要回答:受影响的是哪个制品版本?漏洞在当前构建中是否可触达?是否已有缓解措施或修复?这个结论由谁复核,何时必须重新评估?
把答案写成随意的忽略规则有三个问题。第一,它往往只能被某一个扫描器或仓库理解;第二,它容易跟着浮动 tag 漂移,无法说明结论对应的是哪个镜像内容;第三,理由、状态和更新历史难以被安全审计消费。VEX 的价值是把“我们评估过且当前不受影响”转为面向具体产品标识和漏洞标识的机器可读声明,而不是把告警悄悄删掉。
这也是使用 VEX 时最重要的边界:它不修复漏洞,不改变上游 CVE,也不能替代补丁升级。它记录的是某个产品版本在特定时点的影响评估。只要基础镜像、依赖版本、编译选项或暴露面变化,原先的判断就可能需要复审。
从一条评估开始:PURL、状态与理由
vexctl 的最小工作流是为产品和漏洞创建一条声明。上游 README 的示例使用 Package URL(PURL)定位产品,并以 not_affected 表示“不受影响”;命令中的 CVE 编号是演示值,不能当作真实漏洞结论复制到生产环境。
vexctl create --product="pkg:apk/wolfi/git@2.38.1-r0?arch=x86_64" \ --vuln="CVE-2014-123456" \ --status="not_affected" \ --justification="inline_mitigations_already_exist"
这段命令的关键不在于记住字段,而在于先建立评估输入。--product 应精确对应你正在交付的组件版本和平台;--vuln 对应扫描器报告中的漏洞;--status 与 --justification 则应反映已经过人工验证的事实。不要为了让 CI 变绿而把“尚未看完”写成 not_affected。当团队仍在判断影响时,上游示例展示的 under_investigation 更符合真实状态;确认修复后则可记录 fixed。让中间状态可见,比制造一个看似确定的结论更有治理价值。
实际落地时,建议把 VEX JSON 与评估依据一起纳入受保护的仓库路径:PR 中说明触达分析、测试证据、适用版本与复核人;为每条例外设置复查触发条件,例如依赖升级、镜像基线变更或漏洞记录更新。这样,VEX 不再是扫描器的“消音器”,而是有生命周期的安全决策记录。
还应把“谁能发布何种状态”写进流程。提出 not_affected 的人可以是熟悉组件的开发者,但合并到交付分支前,安全负责人或组件所有者应确认理由能解释扫描命中为何不构成实际影响。对于 under_investigation,不要把它从报表中隐藏:它表示团队已知晓问题,却还没有足够证据下结论。为这类状态设置到期时间和负责人,避免临时判断长期滞留。若之后组件升级或修复,更新声明时也应保留前后状态变化,便于回答“当时为什么允许发布、后来又为何撤销或替换”的审计问题。
多方结论如何汇合,而不是互相覆盖
软件供应链里不只一个角色会发出影响判断:上游组件维护者、Linux 发行版、镜像维护者和你的应用团队都可能掌握不同事实。vexctl merge 可以将针对同一产品的多份 VEX 文档合并,形成包含时间演进的视图:先是 under_investigation,后来变为 fixed,而不是让后一份文件把前一份的上下文完全覆盖。
vexctl merge --product=pkg:apk/wolfi/bash@1.0.0 \ examples/openvex/document1.vex.json \ examples/openvex/document2.vex.json
合并不等于盲目信任。它只是保留和整理声明的工具;生产流程仍要规定优先级。例如,供应商对其发行包的评估可以作为重要输入,但你的镜像是否额外启用了受影响功能,仍应由自己的构建与运行环境证据来回答。对冲突声明应进入人工复核队列,而不是取“最后写入”的结果。
把证据绑定到不可变的镜像内容
若 VEX 文件只留在某个 Git 分支,部署系统拿到镜像后仍可能无法确认“这份判断对应的到底是哪一个制品”。vexctl 支持把 VEX 文档包装为签名证明并附着到容器镜像。上游命令采用带 digest 的镜像引用,这比 latest 或业务 tag 更可靠:digest 指向不可变内容,tag 则可能在稍后被重新指向另一份镜像。
vexctl attest --attach --sign mydata.vex.json \ cgr.dev/image@sha256:e4cf37d568d195b4..
这里的镜像地址只是 README 的截断示例。生产中必须替换为完整 digest,并让签名身份、密钥保管与验证策略符合现有制品签名体系。尤其不要把“附着了 VEX”误读为“镜像天然可信”:VEX 证明的是影响判断的发布与签名关系,镜像来源、SBOM 完整性、运行时权限和恶意篡改防护仍需要各自的控制措施。
让扫描结果消费已审查的结论
vexctl filter 目前以 SARIF 结果文件为输入。它可以读取本地 VEX 文件,或从已存储在镜像上的 VEX 证明读取数据,再移除那些已被声明为不可利用的结果项。
vexctl filter scan_results.sarif.json vex_data.csaf
vexctl filter scan_results.sarif.json \ cgr.dev/image@sha256:e4cf37d568d195b4b5af4c36a...
这一步最适合放在“扫描完成之后、质量门禁之前”。保留原始 SARIF 作为发现记录,同时保存过滤后的结果和所消费的 VEX 版本。门禁策略可以明确区分三类项:没有 VEX 的高风险告警仍阻断;under_investigation 保持可见并设定处理时限;只有具备充分理由、有效签名与匹配制品身份的 not_affected 声明才可不计入阻断数。这样 CI 的绿色状态仍然可解释,而不是依赖不可追溯的排除列表。
适用场景与上线清单
vexctl 最适合已有漏洞扫描流程、但例外判断经常重复出现的团队:例如使用固定基础镜像、维护长生命周期产品线,或需要向客户交付可审计安全材料的组织。它不适合拿来替代快速修复;如果漏洞可利用且有可行升级路径,更新组件通常比新增一份 VEX 声明更直接。
上线前可以检查五件事:产品标识是否精确到版本与平台;状态是否由实际证据支持;VEX 是否随代码和镜像变更触发复审;证明是否绑定完整 digest 并能被验证;过滤前后的 SARIF 是否都被保存。把这五项做扎实,扫描器发现的“可能有问题”才能被转换为可复查、可携带、也可被撤销的工程结论。
还有一个容易忽略的失败模式:把同一份 VEX 当作永久豁免。即使漏洞编号不变,重新构建后的依赖树、基础镜像、编译参数和暴露的协议接口也可能已经不同。因此,自动化流程应把制品 digest 与声明的产品标识进行匹配,并在两者不一致时拒绝沿用旧结论。对于缺少理由、签名无法验证或状态已经过期的 VEX,最稳妥的处理不是继续过滤,而是将对应扫描项重新送回人工评估。安全例外只有能被重新质疑,才真正具备可信度。