VEX、OpenVEX 与 CSAF:让漏洞影响声明可验证、可撤回
扫描平台发现某基础库漏洞,门禁却因为一份 not_affected 声明自动放行。事故发生后,团队才看见声明中的 purl 少了架构 qualifier,它原本只适用于不加载易受攻击模块的 amd64 构建,却被扩展到 arm64 镜像;缓存又只按 CVE 编号索引,把旧状态套给了所有同名产品。文件结构合法,产品和易受攻击子组件却匹配错位。
另一次故障中,供应商发布了修订版 VEX,把状态从 under_investigation 改为 affected,内部镜像仍保留旧文档。消费者按“时间更新者获胜”选择了一个更晚但未授权的第三方声明,并把 CSAF 的 known_not_affected、OpenVEX 的 not_affected 和 CycloneDX 的 false_positive 当成同义字符串。结果不是信息不足,而是格式转换、发布者信任和冲突消解共同制造了一张永久豁免票。
VEX 是语义而不是某一种 JSON 文件
VEX 表达特定发布者在特定时点对“产品或组件 × 漏洞”的影响判断。CISA 的 VEX 最小要求定义了格式无关的协调语义,而不是唯一 serialization,也不是强制合规标准。OpenVEX、CSAF VEX profile、CycloneDX vulnerabilities 和 SPDX 3 Security profile 都能承载部分或完整 VEX 语义,但对象模型并不一一对应。
VEX 不能证明漏洞不存在,也不替代 SCA、镜像扫描、可达性分析、补丁验证或风险接受。扫描器回答“组件或制品可能与漏洞匹配”,VEX 回答“有资格的发布者对这一精确产品作出什么影响判断,理由和处置是什么”。消费端仍要保存原始扫描证据;VEX 只改变告警处置或策略状态,不删除观察事实。
四个协调状态是 not_affected、affected、fixed、under_investigation。它们需要产品、漏洞、发布者、时间和状态依据共同成立。not_affected 没有精确身份与 justification 时不能自动抑制,under_investigation 不能当作 pass,fixed 也要说明修复适用于哪个版本或制品。
产品、漏洞和局部引用必须各有身份
产品身份优先落到不可变 artifact digest,再用完整 purl、CPE、SWID 或供应商产品 ID连接不同生态。purl 是包坐标,qualifier、version、subpath 缺失会改变匹配集合;CPE 是平台命名和逻辑匹配,不是文件摘要;OpenVEX @id、CSAF product_id、CycloneDX bom-ref 和 SPDX 元素 ID首先是文档内引用,不能直接当跨组织主键。
产品与易受攻击子组件也不能颠倒。一条声明可以说“产品 P 包含组件 C,C 对漏洞 V 有风险,但 P 的特定构建不可达”;如果转换时只留下 C 或只留下 P,消费策略会扩大或缩小豁免。每个 statement 至少保留 product identity、vulnerability identity、status、author/publisher、版本或时间、justification/impact 与 action。
漏洞身份通常使用 CVE 等命名空间标识,但同名并不自动代表情报语义完全一致。内部未公开漏洞、供应商 advisory 和别名需要明确 namespace 与映射来源。缓存键至少包含规范化产品身份、漏洞身份、发布者和 statement 版本;只按 CVE 或产品名缓存必然串线。
选择 OpenVEX 或 CSAF 取决于发布责任
OpenVEX 0.2.0是 tagged draft,项目尚未达到 1.0。它用紧凑 JSON-LD 表达 author、timestamp、products、vulnerability、status、justification、impact/action 等信息,适合与 SBOM、扫描和 attestation 工具链组合。draft 身份意味着升级时要准备 breaking change,不应把 0.2.0 写成稳定行业标准。
CSAF 2.0连同 Approved Errata 01是稳定 OASIS Standard。它不仅能表达 VEX,还包含 product tree、branches、relationships、vulnerability notes、remediations、scores、tracking/revision history 和 provider/aggregator 分发角色,适合供应商正式安全公告。CSAF 2.1 仍处于 Committee Specification Draft 02,不能作为无风险稳定升级目标。
OpenVEX 更小、更容易嵌入 attestation;CSAF 更适合复杂产品族、正式公告、修订历史和受信 provider 分发。两者可以协作,但不要把 OpenVEX 当“简化 CSAF”做机械字段映射。状态集合、产品树、版本范围、remediation 和 revision 都可能损失。
安装工具并建立受控发布目录
OpenVEX 官方旗舰 CLI 是 vexctl。从 release 获取与平台匹配的二进制,验证发布校验和或供应链证据后放入受控工具目录;也可按官方模块路径从源码构建固定 tag。执行前由工具管理员设置 VEXCTL_VERSION 和 CHECK_JSONSCHEMA_VERSION,仓库中记录实际二进制与 schema digest。
test -n "$VEXCTL_VERSION" && test -n "$CHECK_JSONSCHEMA_VERSION"
go install "github.com/openvex/vexctl@$VEXCTL_VERSION"
vexctl version
vexctl --help
sha256sum "$(command -v vexctl)" > vexctl.binary.sha256
pipx install "check-jsonschema==$CHECK_JSONSCHEMA_VERSION"
curl -fsSLo openvex-0.2.0.schema.json \
https://raw.githubusercontent.com/openvex/spec/v0.2.0/openvex_json_schema.json
sha256sum openvex-0.2.0.schema.json > openvex-schema.sha256CSAF 的开源工具入口位于 gocsaf/csaf,包含 validator、checker、downloader、provider、aggregator 和 uploader。选择与 CSAF 2.0 + Errata 01 配套且通过团队样本集的 release,分别授予 validator 只读、provider 写发布目录、downloader 读远端 provider 的权限。不要让验证器同时修改文档,也不要让公共 provider 进程读取 restricted 草稿目录。
受信 CSAF provider 不只是一个 JSON 静态目录。稳定基线要求围绕 TLS、hash sidecar、OpenPGP detached signature、公钥发布、索引/变更发现和镜像角色建立分发链。消费者镜像时保存文档、hash、.asc、provider metadata 和获取记录,缺任一层都标记为证据不完整。
第一组正反实验:schema 通过后仍要验证身份与理由
先创建一条 OpenVEX not_affected 声明,产品使用完整 purl 和目标 digest 的外部绑定,理由选择与事实一致的 vulnerable_code_not_in_execute_path,并写明易受攻击子组件和复测触发器。实际创建参数以固定版本的 vexctl create --help 为准;生成后同时跑 schema 和策略验证。
# 正向:结构合法,产品精确,not_affected 有理由和证据引用
vexctl create \
--product='pkg:oci/app@sha256:<SUBJECT_DIGEST>?repository_url=registry.example.test/team/app&arch=amd64' \
--vuln=CVE-20XX-0001 --status=not_affected \
--justification=vulnerable_code_not_in_execute_path \
--impact-statement='module-x is excluded; retest when build profile changes' \
> openvex.json
check-jsonschema --schemafile openvex-0.2.0.schema.json openvex.json
./scripts/verify-vex-policy openvex.json expected-subject.json trusted-publishers.json预期结构验证退出 0,策略输出命中的规范化产品、漏洞、发布者、justification、statement digest 与复测触发器。只有 subject digest 命中、发布者受信、理由允许且没有更新冲突时,策略才可对该产品抑制这一个告警。
反向样本删除 justification,把 purl 的 arch qualifier 去掉,同时保留合法字段类型。schema 可能因格式要求失败,也可能仍接受某些组合;无论如何,业务策略必须失败,且不能退化为人工不可见的 warning。
# 反向:扩大产品集合并移除 not_affected 的技术依据
jq '(.statements[0].products[0]["@id"]) |= sub("&arch=amd64"; "")
| del(.statements[0].justification)
| .statements[0].impact_statement = ""' \
openvex.json > openvex.invalid-policy.json
check-jsonschema --schemafile openvex-0.2.0.schema.json openvex.invalid-policy.json
./scripts/verify-vex-policy openvex.invalid-policy.json \
expected-subject.json trusted-publishers.json预期策略退出非零,证据包含 statement index、缺失理由、实际 purl 和候选产品数。若 schema 通过,正好证明结构合法不等于可以抑制;若 schema 已失败,策略仍应返回明确分类,便于区分生产者错误与消费政策错误。
第二组正反实验:状态更新、冲突和发布者授权
准备同一产品与漏洞的三次声明:under_investigation、受信发布者更新的 affected、同一受信发布者最终给出的 fixed。每次都生成新的 statement/document ID,保留 predecessor digest、时间顺序和 action。消费者选取最新有效状态,但历史记录不可覆盖删除。
# 正向:同一发布者的受控状态迁移
./scripts/ingest-vex vex-under-investigation.json
./scripts/ingest-vex vex-affected.json
./scripts/ingest-vex vex-fixed.json
./scripts/query-vex-state \
--product 'registry.example.test/team/app@sha256:<SUBJECT_DIGEST>' \
--vulnerability CVE-20XX-0001 --publisher vendor-psirt预期当前状态为 fixed,并可回链到三份原文 digest、发布者身份、状态理由和修复版本。若缺少中间 affected,消费仍可理解当前结论,但审计不能伪造不存在的迁移。
再加入一份发布时间更晚、密码学签名有效但发布者不在该产品授权集合中的 not_affected,以及一份同一受信发布者但 subject digest 不同的声明。消费者不能用“最新时间”或“有签名”静默胜出。
# 反向:未授权发布者与错误 subject 制造冲突
./scripts/ingest-vex third-party-not-affected.json
./scripts/ingest-vex trusted-wrong-subject.json
./scripts/query-vex-state \
--product 'registry.example.test/team/app@sha256:<SUBJECT_DIGEST>' \
--vulnerability CVE-20XX-0001 --require-unambiguous预期未授权文档被保留为观察但不参与放行,错误 subject 被隔离,当前状态仍来自 vendor-psirt;如果两个受信且精确匹配的当前声明互相冲突,查询结果必须是 conflict/unknown 并阻止自动抑制。证据至少包含候选 statement digest、授权判断、subject 比对和冲突策略版本。
四状态不能机械映射到每一种格式
OpenVEX 使用四个协调状态。CSAF VEX profile 的核心产品状态是 known_not_affected、known_affected、fixed、under_investigation,同时完整 CSAF 还可表达 first_affected、first_fixed、last_affected、recommended 等集合。CycloneDX analysis.state 包含 resolved、resolved_with_pedigree、exploitable、in_triage、false_positive、not_affected。SPDX 3 Security profile 则通过 typed relationship 与 assessed element 组织影响语义。
因此 known_affected 不总能无条件转换为 CycloneDX exploitable,fixed 也不等于 resolved_with_pedigree,因为后者强调 pedigree 证据。CycloneDX false_positive 不能简单降成 not_affected:前者可能表示检测结果错误,后者表示产品不受影响,两者治理含义不同。CSAF recommended/first_fixed 也没有 OpenVEX 四状态中的精确单字段位置。
转换器输出 exact、approximate、unmapped 三类结果,并逐字段记录产品树、版本范围、时间、remediation、justification、impact、action、publisher 和签名的处理方式。只有 exact 且目标格式满足本地策略的声明可进入自动抑制;approximate 和 unmapped 保留给人工处置或信息展示。
转换、裁剪和重新验签是新的发布事件
CSAF 产品树可能把产品族、版本、模块和关系拆成多个节点,OpenVEX 更偏向产品标识列表;转换时必须生成显式 product mapping。自由文本可以原样携带,却不能自动恢复成目标格式的机器可读 justification。revision history、statement timestamp、firstIssued/lastUpdated 也不能互相替代,否则冲突消解顺序会变化。
转换任务要先验证源文档与源签名,再生成目标 payload 和 loss report,随后验证目标 schema 与语义,最后用目标发布者身份重新签名。原签名状态记录为“已验证后转换”,不能贴到新文件上假装仍然有效。
./scripts/verify-source-vex advisory.csaf.json advisory.csaf.json.asc
./scripts/convert-vex --from csaf-2.0 --to openvex-0.2.0 \
advisory.csaf.json --output advisory.openvex.json --loss conversion-loss.json
./scripts/verify-vex-policy advisory.openvex.json expected-subject.json trusted-publishers.json
jq -e 'all(.mappings[]; .quality == "exact")' conversion-loss.json
./scripts/sign-derived-vex advisory.openvex.json conversion-loss.json裁剪 restricted 缓解信息同样产生新文档。公开版可能只给状态和外部 advisory,客户版给修复版本,受限版才包含补偿控制、不可达路径证明、未公开 CVE 和修复窗口。每一版使用新 ID、版本、签名和可审计的源 digest,避免公共文档的签名被错误用于内部全文。
流水线和 Registry 应保存声明历史而不是最终缓存值
生产侧在扫描、可达性分析或供应商评估后创建 VEX,先写不可变对象存储或 OCI Registry,再登记 statement digest、subject digest、publisher、格式版本和 predecessor。OpenVEX 可作为 in-toto predicate 封装,CSAF 通常通过 provider 目录和签名 sidecar 分发;无论哪条路径,都不能只把解析后的状态写进数据库而丢弃原文。
消费侧先解析待晋级制品 digest,再收集 SBOM 与所有候选 VEX,完成 schema、引用、签名、发布者授权、产品匹配、状态必需字段、时效/触发器和冲突检查,最后才交给门禁。一个可审计决策至少记录:subject、vulnerability、原始扫描结论、采用的 statement digest、publisher、状态、理由、策略版本、决定和复测触发器。
vex_policy:
accept_formats: [openvex-0.2.0, csaf-2.0-errata01]
suppress_only_when:
- subject_digest_exact
- publisher_authorized_for_product
- signature_and_distribution_chain_valid
- status_is_not_affected
- justification_allowed
- no_current_conflict
unknown_when: [under_investigation, unavailable, approximate_mapping]Registry 中用 subject digest 关联 attestation/referrer;复制时保留 VEX、签名、loss report 和 predecessor 链。缓存以 statement digest 和策略版本为键,产品变化、SBOM 变化、新 VEX、撤回、发布者授权变化或复测触发器命中时失效。缓存 TTL 只能作为性能保护,不能替代业务失效条件。
权限与分类分发保护私有依赖和缓解细节
VEX 可能暴露未公开漏洞、受影响客户构建、私有组件、精确旧版本、补偿控制、不可达路径、修复计划、联系人和 PSIRT 工作流。尤其是“为什么不受影响”的技术论证,可能直接告诉攻击者哪些部署拓扑例外存在。发布者要把 public、customer、internal、restricted 四类视图放在不同路径和访问策略下。
CSAF 可结合 TLP、sharing group、provider 与镜像机制分发;受限文档的文件名、索引记录和 changes feed 也可能泄露产品与漏洞关系,不能只保护 JSON 正文。OpenVEX 通过对象存储、Registry 或 attestation 分发时,同样需要仓库级读权限、短期凭据、审计和最小披露。
角色至少分 author、reviewer、signer、provider publisher、mirror/downloader、policy consumer 和 auditor。author 不直接掌握发布签名,consumer 不修改声明,第三方 aggregator 不能凭聚合身份替代供应商授权。日志记录谁读取了哪个 statement digest 和分类,不记录完整 impact/action 文本或访问 token。
清理、撤回和故障恢复必须保留否定证据
实验结束按“消费缓存→索引→Registry/目录文档→签名 sidecar→测试密钥与身份→本地临时文件”的顺序清理。删除前保存测试对象 digest 清单,删除后证明查询不到且旧身份无法重新发布。共享 provider 中不要按 CVE 模糊删除,因为同一漏洞可能关联多个产品和历史 revision。
rm -f openvex.invalid-policy.json conversion-loss.json
rm -f vexctl.binary.sha256 openvex-schema.sha256 openvex-0.2.0.schema.json
rm -f expected-subject.json
# provider/Registry 中按文档 digest 撤回测试对象,再刷新索引和消费缓存真实声明出错时优先发布带 revision/predecessor 的纠正文档或撤回标记,而不是静默覆盖原文件。消费者遇到撤回、签名缺失、provider 不可信或冲突时,把已有抑制状态改为 unknown 并重新评估扫描结果。恢复完成的证据是旧 statement 不再影响决策、历史仍可审计、新声明能精确命中目标产品。
排障从产品身份开始,再看漏洞 ID、schema/profile、justification、签名、发布者授权、revision 顺序、缓存与策略。一个 VEX “没有生效”可能是正确拒绝;先读取结构化 rejection code,不要直接放宽 purl 匹配或信任所有签名者。
容量、成本和高可用由产品漏洞笛卡尔积驱动
VEX 容量不是文档数量。一个供应商产品树可包含大量版本、模块和关系,每个漏洞又有多次 revision、多个分发视图、签名与转换衍生物。估算时记录产品节点、产品×漏洞 statement、revision 深度、文档与 sidecar 字节、索引基数、下载频率、缓存命中和复审队列。
成本主要落在对象存储、provider/Registry、签名或 HSM、镜像带宽、索引数据库、审计日志和人工复审。把所有状态都压成数据库最终值会节省短期存储,却失去冲突、撤回和策略重放能力。原文采用低成本不可变留存,索引可重建,热缓存只保存当前决策及其依赖 digest。
高可用覆盖 provider、Registry、对象存储、签名服务、信任根、索引和策略引擎。下载端保留最后一次完整验证的快照和水位,但上游不可达时不冒充“最新”。监控发布失败、签名/sidecar 缺失、镜像延迟、冲突数、under_investigation 年龄、抑制命中、触发器失效、未授权发布者和 approximate 转换。生产阈值由发布节奏、威胁模型与 SLO 决定。
升级、迁移和退出以可重放决策为门槛
升级 OpenVEX draft、CSAF 工具或 validator 前,保存工具 digest、schema、受信发布者、产品映射、状态映射、签名材料公钥侧、代表性原文和期望决策。候选环境重放 not_affected、affected、fixed、under_investigation、冲突、错误 subject 和未授权发布者样本,比较结构错误、语义拒绝与最终决策。
CSAF 2.0 + Errata 01 迁往 2.1 时,只有在 2.1 达到组织接受的批准级别且生产者、consumer、provider 和签名链都通过兼容测试后再切换;CSD02 作为预演目标,不替换稳定权威源。OpenVEX 0.2.0 升级也要假设对象结构可能变化,双写期间保留原始语义模型和 loss report。
格式迁移按 statement 集合比对,不按文件数比对。产品身份、漏洞、状态、理由、action、时间、发布者、revision 和签名状态逐项比较;approximate 不进入自动抑制。回滚切回旧 consumer 和旧映射策略,新格式原文继续留存,避免双写结果丢失。
退出平台时暂停新发布,等待镜像和索引队列排空,导出原文、签名、hash sidecar、provider metadata、产品映射、发布者授权、撤回记录、loss report、策略与审计决策。在空环境中重建当前状态并重放历史冲突后,撤销 author、signer、publisher、mirror 和 consumer 凭据,清理缓存与临时草稿。最终检查是旧身份发布失败、受限 URL 不可读、归档可离线验签,并能解释每一次漏洞抑制为何生效或失效。
CISA VEX 最小要求。CISA VEX 状态与 justification
