Grafeas 制品元数据 API:Note、Occurrence 与权限治理
Grafeas 提供一套制品元数据 API,让漏洞扫描器、构建系统和证明服务用共同模型记录“某类元数据是什么”以及“它怎样出现在某个具体资源上”。Note 是可复用的定义或权威记录,Occurrence 是 Note 在资源上的实例。两者分开后,漏洞提供者可以维护 Note,扫描器只创建指向它的 Occurrence,不必获得修改漏洞定义的权限。
这个模型的价值在所有权,而不在把所有安全事实塞进一张表。资源 URI、Note 名称、kind、producer、创建时间与原始证据引用必须稳定;签名是否可信、漏洞是否可利用、制品能否发布,仍由对应验证器和策略决定。若客户端只看“存在 Occurrence”就放行,它会把元数据记录误当成授权结论。
Note 与 Occurrence 表达两种所有权
Grafeas 概念文档把 Note 描述为可以被多个资源引用的高层元数据,把 Occurrence 描述为某个 Note 在具体资源上的发生记录。漏洞 Note 可以定义 CVE、严重度和受影响版本;Occurrence 再记录某个镜像被扫描后与这条 Note 的关系。构建 Note 可以声明 builder 或证明类型,Occurrence 绑定实际产物。
Note 与 Occurrence 适合放在不同项目或权限域。Note owner 负责内容、版本和撤销,consumer 获得读取权限;Occurrence producer 只能为自己有资格观察的资源创建记录。这样既防止扫描器篡改权威漏洞定义,也防止普通消费者伪造“某资源已经通过构建证明”的记录。权限设计若只按 API 方法分读写、没有按项目和名称约束,模型上的所有权仍会在实现中失效。
Grafeas 不是任意属性图数据库。它提供按资源、Note、kind 等字段查询元数据,但不替代复杂影响面遍历。需要跨 SBOM、builder、VEX 和部署关系做图查询时,可以把 Grafeas 作为 GUAC 的记录来源;需要稳定的 Note/Occurrence API 与 producer 权限时,则直接消费 Grafeas。
权威接口从 proto v1 开始
Grafeas 仓库把 proto/v1声明为权威 API。旧的运行文档仍可能出现 v1beta1 路径,因此实现前先锁定目标 release 或 commit,用对应 protobuf 生成客户端,并核对服务器真正暴露的路由、kind oneof、过滤语法和存储后端。不能从一个旧 curl 示例推断当前生产合同。
git clone https://github.com/grafeas/grafeas.git
cd grafeas
git checkout <reviewed-release-or-commit>
buf lint proto/v1
go test ./...下载源码或容器后记录 commit、镜像 digest、proto descriptor、数据库迁移和启动参数。若组织使用 Google Cloud Artifact Analysis,应以该托管服务的 API、资源名、IAM 与区域约束为准;它实现 Grafeas API 的部分能力,不意味着开源服务器的所有部署和运维结论都可直接套用。
客户端生成物要进入仓库或可重复构建流程。语言 SDK、proto descriptor 和服务器版本共同组成契约;只更新服务器镜像、不重新执行兼容测试,会让 oneof 新增、字段行为或过滤表达式变化直到运行时才暴露。
部署先明确持久化与项目边界
本地实验可参考仓库的 运行说明,但其中的 beta 路径只作为历史实现线索。启动前创建独立数据库和最小权限账号,固定 schema migration,监听地址只开放给实验网络。生产环境还要在 API 前建立身份认证、TLS、方法授权、限流和审计;开源服务的默认启动不等于已经具备完整多租户边界。
建议把 Note owner 与 Occurrence producer 分成不同身份和项目。例如安全情报服务维护 projects/advisories/notes/...,构建与扫描系统只能读取这些 Notes,并在 projects/workloads/occurrences/... 下创建 Occurrences。消费者只读两个项目,通过过滤条件获得目标资源的记录。管理员账号不进入日常流水线。
持久化验收包括数据库重启、备份恢复、名称唯一性和引用完整性。服务返回创建成功后,重新读取 Note/Occurrence 并核对资源名、kind、create/update time 与原始证据 digest;仅凭数据库中有一行记录,不能证明 API 序列化与权限结果正确。
资源身份必须固定到不可变对象
Occurrence 的 resource_uri 决定记录指向谁。对于 OCI 制品,优先使用带 digest 的规范引用,并在扩展字段或关联存储中保留 media type、仓库和平台信息;不要只写可移动 Tag。文件制品使用可解析 URI 与内容 digest,避免把本地临时路径当成组织级身份。
同名、同版本不等于同一资源。多架构镜像的 index digest 与平台 manifest digest承担不同边界;重新打包的 Release 文件即使文件名不变,摘要也已经改变。producer 在创建 Occurrence 前计算或取得权威 digest,API 网关校验 URI 形态,consumer 再按自身策略确认目标摘要。字符串小写、去空格或截断 qualifier 都可能造成错误合并。
Note 名称同样属于长期身份。不要通过删除并重建同名 Note 来表达语义完全不同的定义;变更 kind 或权威生产者时创建有版本的新 Note,旧 Note 进入弃用和撤销流程。Occurrence 引用的 Note 若不可读,客户端应显示“引用无法解析”或权限拒绝,不能把它解释成安全检查不存在。
正向实验建立可追踪引用
先创建一个测试 Note,由专用 owner 身份写入,再用另一个 producer 身份为固定 digest 的实验镜像创建 Occurrence。请求结构以锁定的 proto/v1 生成客户端为准,下面只表达对象关系,不复制可能漂移的 REST 路径:
{
"note": "projects/advisories/notes/CVE-EXAMPLE",
"resourceUri": "https://registry.example.test/app@sha256:0123456789abcdef",
"kind": "VULNERABILITY",
"evidenceDigest": "sha256:fedcba9876543210",
"producer": "scanner-ci"
}正向验证先以 Note owner 读取 Note,再以 producer 创建 Occurrence,最后以只读 consumer 按 resource URI 与 Note 名称查询。断言 Occurrence 名称稳定、引用可解析、kind 与 oneof 匹配、create time 可审计、原始扫描文档能按 digest 取回。重复提交使用业务幂等键或先查后建,不能为同一次扫描无限新增记录。
查询结果进入项目时保留过滤表达式、分页 token、服务版本和读取时间。一个资源可能有多个不同 producer 的 Occurrences;客户端按名称和 producer 展示,不自行覆盖。需要得出门禁结论时,将这些记录交给漏洞、签名或 Provenance 策略求值,并保存 policy version。
反向实验验证伪造写入会被拒绝
用 Occurrence producer 尝试修改 Note,预期得到明确权限拒绝;再让无关团队为不属于自己的资源创建 Occurrence,检查项目边界、资源前缀或上层授权是否阻断。若两个动作都能成功,Note/Occurrence 的模型分离只停留在文档里,不能承担生产信任。
第二组反例创建指向不存在或不可读 Note 的 Occurrence。不同实现可能在写入时拒绝,也可能保留引用;无论采用哪种语义,consumer 都必须区分“没有记录”“引用损坏”“Note 不可读”和“Note 已撤销”。把这些状态统一转成空数组,会让权限故障表现为没有风险。
再以相同 Tag、不同 digest 创建两条记录,确认查询不会按 Tag 合并;构造 kind 与 oneof 不匹配的请求,确认 protobuf 或服务器拒绝;用过宽 filter 和超大分页测试限流。反向 fixture 在升级时重放,防止新版本放宽字段校验或权限中间件绕过。
项目接入围绕 producer 责任
构建器、扫描器和证明服务分别拥有 producer 身份。每个任务先把完整原文写入不可变存储,计算 evidence digest,再创建 Occurrence 引用。任务结果区分原文保存失败、API 写入失败、幂等重复和权限拒绝;只有可重试错误进入重试队列,语义错误进入人工修复,不用无限重试掩盖坏数据。
consumer 建立小型适配层,把 Grafeas 资源名、分页、过滤和错误码转换成业务状态。适配层不返回一个含糊布尔值,而是返回 records、unresolved references、service watermark 和 query version。策略层据此判断缺证据、坏证据与系统故障;服务不可用时默认行为由环境策略决定,生产发布通常应拒绝或进入有审计的 break-glass。
跨工具协作时不复制整份业务数据库到 Grafeas。高频订单状态、实时运行指标和临时会话不适合作为制品元数据。Grafeas 保存能够长期关联制品和生产者的记录,原文仓库存放大对象,图谱或分析平台通过名称和 digest 继续建立关系。
权限、敏感数据与审计共同设计
API 身份至少分 Note owner、Occurrence producer、只读 consumer、备份恢复和平台管理员。按项目、名称前缀、方法和资源来源限制权限,定期核对实际调用;producer 凭证使用短期工作负载身份,不能共享管理员密钥。数据库账号只对服务进程开放,备份身份独立并带恢复审计。
Note/Occurrence 可能暴露内部仓库、包名、builder、漏洞和组织结构。公开漏洞 Note 与私有资源 Occurrence 可以使用不同项目和访问策略;日志记录调用身份、方法、资源名摘要、结果类别与 latency,不记录访问令牌、完整私有 URI 查询参数或原始证明正文。导出和批量查询设置审批、速率和保留期。
多租户不能只依赖 producer 自报项目名。网关从已认证身份派生允许项目,服务器再次授权,数据库行级或独立实例承担最后隔离。恢复演练要用不同 tenant 身份验证不可越界读取;只测试管理员全量恢复,无法证明租户边界在灾后仍成立。
容量和高可用取决于写入与过滤模式
容量主要来自 Note/Occurrence 写入、resource URI 与 producer 的高基数、过滤查询、索引、审计日志和备份。压测样本覆盖批量扫描峰值、同一资源多 producer、多 kind 查询、深分页和 Note 热点读取,报告写入吞吐、冲突率、查询 P95/P99、数据库连接、索引体积和积压年龄。
索引围绕真实 filter 设计,不能把每个 protobuf 字段都建索引。先从慢查询日志和计划中确认 resource URI、note name、kind、project 与时间窗口的组合,再评估写放大。大对象留在外部证据仓库,Occurrence 只存必要元数据和 digest,降低数据库备份与复制成本。
高可用服务节点必须共享一致数据库和名称生成策略。写请求重试要有幂等键,负载均衡不能把超时后的重复请求变成两个 Occurrence。数据库故障切换后比较最近写入与审计水位;只看 API Pod Ready 会遗漏复制延迟和索引未恢复。RPO/RTO 同时覆盖 Notes、Occurrences、IAM 配置和原始证据仓库。
排障从资源名和权限开始
“Occurrence 存在但 Note 读不到”先以同一 consumer 身份读取 Note,区分名称拼错、项目不存在、权限拒绝和 Note 已删除;再检查缓存是否保留过期内容。服务不应在 Note 不可读时继续使用旧语义给出通过结论,也不应把 403 改写成 404 后丢失审计原因。
“同一镜像出现多条记录”核对 digest、producer、扫描 revision、幂等键与创建时间。不同 producer 的独立观察应保留,同一任务重试产生的重复记录应由业务键合并或标记。不要按 CVE 编号全局去重,否则不同资源和不同 Note owner 会被错误压缩。
“升级后客户端解析失败”比较 proto descriptor、oneof、服务路由和生成 SDK,先在候选环境重放代表请求。beta 到 v1 迁移以 protobuf 为源显式映射名称和 kind;禁止在生产请求中临时猜字段。过滤行为变化用 golden query 对比,不只验证 HTTP 200。
升级、迁移与退出保持引用完整
升级前导出 Notes、Occurrences、引用关系、owner/producer 映射、proto descriptor、数据库 schema 和代表查询。候选环境恢复备份后,用原 producer 与只读 consumer 分别执行正反验证,比较对象计数、名称、resource URI、kind、权限和过滤结果。写入保持单一权威端;需要双写时设置幂等键、差异队列和明确回滚方向。
迁移到新实现时按不可变 resource URI、Note 名称和 Occurrence 名称对齐,不以数据库自增 ID 作为合同。先迁 Notes 与 owner 权限,再迁 Occurrences 与 producer 映射,最后迁 consumer;否则会出现存在引用却无法解释的半状态。新平台不能表达的 kind 或 oneof 记录进入降级清单,在下游确认前保留旧只读服务。
安全退出先停止新 producer,导出并校验 Notes、Occurrences、原始证据引用和审计;让 consumer 完成替代 API 的正反测试;随后撤销工作负载身份、关闭入口、删除运行资源,并按保留策略处理数据库、备份、日志和导出。最后核对 DNS、Secret、云数据库、对象存储与持续费用。删除服务不等于引用已迁移,保留数据库也不等于仍能解释 protobuf 语义。
