GUAC 供应链证据图谱:摄取、规范化与影响面查询
GUAC 解决的是证据分散后的关系查询问题。构建系统保存镜像摘要,SBOM 描述组件,Provenance 记录 builder,VEX 又对漏洞给出状态;这些文件分别成立,仍不等于团队能回答“某个构建器影响了哪些制品”或“一个组件最终进入了哪些部署候选”。GUAC 把多来源文档摄取为带来源的图关系,让这类影响面问题可以查询。
它不是 Registry、签名验证器或发布策略引擎。原始证据仍应保存在不可变位置,签名与身份由专门验证器核对,发布系统再依据策略采取动作。图中出现一条边,只说明 GUAC 根据某份输入建立了关系;生产门禁若把“能查到”直接解释成“可信”,会把聚合层错误提升为信任根。
GUAC 的责任停在关系与查询
GUAC 组件说明把完整运行链拆为 Collector、Ingestor、Assembler、消息服务、持久化后端与 GraphQL。Collector 从文件、OCI 或其他来源取得文档;Ingestor 解析格式并产生 GUAC 对象;Assembler 把对象写入后端;GraphQL 为人和系统提供关系查询。CollectSub 与 certifier 还可以根据已经发现的实体继续收集或补充信息。
这条链故意异步。上传文件成功,只能证明收集入口接受了输入;解析可能稍后失败,Assembler 可能积压,查询也可能因为规范化尚未完成而暂时看不到关系。验收因此不能只看一个 HTTP 状态,而要关联 source digest、消息批次、解析结果、写入水位与查询结果。原始文档应先写入受控对象存储或 Registry,再把不可变引用交给 GUAC,避免图数据库成为唯一副本。
GUAC 与 Grafeas可以协作,但两者不是同一个产品的两种部署方式。Grafeas 以 Note/Occurrence 提供制品元数据 API;GUAC 擅长汇聚多来源并遍历跨证据关系。需要记录面时选 Grafeas,需要影响面图查询时选 GUAC,也可以让 Grafeas 成为 GUAC 的一个来源。
安装先锁定发行物和后端
本地学习环境可以使用 GUAC 安装入口提供的 release 资产和 Compose 示例。二进制、Compose 文件与文档必须来自同一 release,下载后校验项目发布的 checksum 或签名;不要从默认分支取配置,再配一个旧版本 guacone。演示端口只绑定到本机或隔离网络,GraphQL 与 CollectSub 都不应直接暴露到公网。
docker compose -f guac-demo-compose.yaml -p guac up -d --force-recreate
docker compose -f guac-demo-compose.yaml -p guac ps
guacone version内存后端适合观察对象模型,重启后丢失,不能用于共享审计。PostgreSQL 部署说明适合需要持久化的团队环境,但数据库并不会替团队保存原始 SBOM 或 Attestation。投产前记录镜像 digest、启动参数、消息服务配置、后端 schema、数据库备份方式和恢复目标;这些项目共同定义实际版本,而不是某个容器 Tag。
安装账号与运行账号分开。安装者可能需要创建数据库、消息主题、Secret 和网络规则;Collector 只读取批准来源,Ingestor/Assembler 只访问队列与后端,查询身份只获得必要的只读入口。方便起见给所有组件共用云账号,会让一次 Collector 凭证泄露同时获得证据源、数据库和查询面权限。
身份规范化不能靠字符串相似
软件身份至少包含类型、命名空间、名称、版本和 qualifier,制品还需要不可变 digest。pkg:maven/example/lib@1.0?type=jar 与缺少 qualifier 的 purl 不一定等价;两个仓库中的同版本包也可能对应不同字节。GUAC 会把输入映射到统一 ontology,但关联仍可能依赖文档质量和启发式信息,不能把图节点 ID 当作跨实例业务主键。
每条关系要能回到 source URI 的受控引用、原始文档 digest、collector、观察时间、producer 和验证状态。GraphQL 返回的内部 id 只适合当前实例内翻页或继续查询;官方 GraphQL 说明明确提示 definitions 尚未稳定,实例迁移时应按开放身份与原始证据重新建立关系,而不是复制旧 ID。
冲突证据应并存。扫描器 A 在 T0 报告组件受影响,产品团队在 T1 发布 not_affected VEX,外部情报在 T2 修改受影响版本区间。正确模型保留三次观察及各自生产者,再让策略判断 VEX 发布者是否有资格、声明是否过期、subject digest 是否一致。用“最后写入获胜”压平冲突,会让事件调查失去时间与责任上下文。
正向摄取要观察每个中间状态
先准备一份有明确 namespace、package identity 和 document digest 的 SPDX 或 CycloneDX 样本,把原文与其摘要保存到实验目录。GUAC 支持的格式随发行版变化,输入前对照锁定版本的支持清单;解析失败不能通过改扩展名掩盖。执行收集后同时查看服务日志、消息积压和 GraphQL schema:
sha256sum evidence/app.spdx.json
guacone collect files ./evidence/
docker compose -f guac-demo-compose.yaml -p guac logs --no-color --since 3m
curl -fsS http://127.0.0.1:8080/query -H 'content-type: application/json' --data-binary @schema-query.json正向验证至少证明四件事:Collector 记录了准确 source;Ingestor 识别正确文档类型;Assembler 写入了与包身份、制品摘要对应的对象;GraphQL 查询能从 subject 找到组件或 builder,并保留原始来源。重复提交同一原文后,对象和关系数量不应无界翻倍;若发行版对相同内容仍创建新观察,也要能按 source digest 聚合,而不是把重复采集误报成多个独立证据。
保存查询时同时保存当时的 GraphQL SDL 或 introspection 结果。查询文件属于消费者合同,字段升级后必须在候选环境重放。只保存屏幕截图无法证明查询用了哪个 schema、过滤条件或数据水位,也无法在恢复演练里自动比对。
反向实验用身份冲突证明边界
复制正向样本,保持包名和版本不变,修改 qualifier 或 subject digest;再制作第二份 VEX,让它对相同 CVE 给出不同状态和 producer。预期不是 GUAC 替团队挑一个“正确答案”,而是图中能区分两个主体、两份来源和互相冲突的声明。若查询只返回一个结果,先检查输入映射和过滤条件,再检查是否有去重规则错误合并对象。
第二个反例是让 Ingestor 接受文档、却临时阻断 Assembler 到后端的连接。Collector 成功、队列增长、查询无结果可以同时成立。恢复连接后,积压应被处理且不会重复生成不可解释的边;如果直接重跑采集才能恢复,说明队列持久性或消费确认语义没有达到设计目标。
把这些反向 fixture 固定进升级验收:错 digest 不合并,冲突状态不覆盖,解析错误有死信或明确计数,后端故障不会回报端到端成功,过宽 GraphQL 查询能被超时或复杂度限制拦截。失败证据比一张“图中有节点”的截图更能证明系统边界。
项目接入分开生产与消费
生产侧在构建、SBOM、扫描和 Attestation 任务结束后,先保存原始文档与 digest,再提交 GUAC 可读取的引用。流水线成功条件是“权威原文已持久化、提交已接受、失败可重试”,不应同步等待所有 enrichment 完成。使用稳定幂等键关联 run、subject digest、document digest 与 source,重试时不生成另一套不可对账对象。
消费侧通过一组有版本的保存查询回答明确问题,例如“由 builder X 生成且含组件 Y 的制品集合”。查询结果进入策略前,再调用签名、声明和身份验证器;决策记录保存 query version、graph watermark、policy version 与匹配证据 digest。GUAC 暂时不可用时,选择阻断发布、使用有时效的上次快照或进入人工审批,必须在架构中写明,不能由调用端超时后默认放行。
交互式 GraphQL 适合调查,不适合无边界地开放给所有开发者。对常用影响面查询建立服务层,限定深度、分页、最大返回量和允许字段;调查账号与自动门禁账号分离。涉及内部包、仓库、builder 与漏洞关系的查询结果本身就是敏感资产,需要访问日志、导出审批和保留期限。
权限与敏感信息从 Collector 收敛
Collector 接触的来源最多,可能包含私有仓库地址、SBOM 内部组件、CI 元数据和短期凭证。凭证按 source 拆分,只给只读范围,通过 Secret 管理器注入;日志记录凭证引用和结果类别,不记录 Token、完整授权头或含密钥的 URL。对外部 enrichment 设置域名允许列表、代理、超时、配额与数据出境审查。
GraphQL 查询面采用身份认证、授权和网络隔离,禁止匿名 playground 出现在共享网络。数据库账号分读写,备份单独加密并限制恢复权限。若不同业务共享实例,不能只靠查询约定隔离 tenant;必须验证后端模型、API 授权与导出是否真正表达租户边界,否则选择独立实例或受控聚合服务。
图关系不应保存不必要的秘密原文。保留 document digest、受控引用和必要索引字段,敏感证据留在原始仓库按自身权限读取。删除请求也要区分原文、图索引、备份、导出和下游缓存;只删一个节点既不能证明数据消失,还可能破坏其他关系的可解释性。
容量、高可用与成本围绕异步积压
容量由原始文档大小、解析扇出、规范化基数、关系数、enrichment 外部调用和 GraphQL 查询宽度共同决定。压测要覆盖大 SBOM、重复摄取、冲突证据、宽影响面查询和后端恢复后的积压回放,记录 collector 吞吐、队列年龄、解析失败率、assembler 延迟、数据库 IOPS、索引体积、查询 P95/P99 与取消数。
高可用不是简单增加所有副本。Collector 可以按 source 分片;Ingestor 与 Assembler 只有在消息确认和写入幂等成立时才安全横向扩展;GraphQL 读副本是否允许陈旧,需要给消费者明确 watermark。数据库、消息服务和 blobstore 的 RPO/RTO 分别演练,恢复顺序保证队列不会把已经确认的批次重复放大。
成本包括对象存储、消息保留、数据库索引与备份、外部漏洞 API、跨区流量、日志和人工模型治理。先限制无用 enrichment 与宽查询,再讨论机器规格。个人实验可用 Compose 与内存后端;小团队把原文仓库、队列、GUAC 组件和 PostgreSQL 分开;组织级环境再按摄取面与查询面拆故障域。规模选择依据是每日关系增量、来源数、并发与恢复目标,不是副本数量。
排障沿首个停滞状态推进
“提交成功但查询为空”先核对原文 digest 和 Collector 记录,再看队列年龄、Ingestor 格式识别、Assembler 写入与 GraphQL watermark。不要先改查询字段碰运气。若只有某类文件失败,用最小原文在同版本解析器重放,并保留错误类别;若所有文件都积压,优先查消息服务、数据库连接和磁盘水位。
“同名组件被合并”按 purl qualifier、namespace、version、repository 与 digest 展开比较,定位是源文档缺字段、Ingestor 映射还是查询过滤过宽。修正规范化规则后,在隔离后端从原文重放并对比关系差异;直接在生产数据库手改节点会留下无法重建的隐形状态。
“升级后字段不存在”对候选实例执行 introspection,重放保存查询,并按 schema diff 修改消费者。错误处理必须区分 GraphQL transport error、field error、空结果和权限拒绝。把这些状态都转成空数组,会使门禁把系统故障解释成“没有风险”。
升级、迁移和退出都从原文重建
升级前保存镜像 digest、部署声明、GraphQL SDL、关键查询、后端备份和代表性原始证据。候选环境从空后端重放原文,比较规范化身份、关系集合、冲突保留、查询结果、吞吐与权限;通过后再切 Collector 和 consumer。回滚保持旧实例只读可用,新旧端不要同时消费同一 source,除非幂等与差异账本已经验证。
跨后端或跨实例迁移以开放身份、原始证据和关系导出为准,绝不复用 GraphQL 内部 id。先迁历史原文并建立水位,再短暂停止新摄取、迁尾差、切查询,保留差异报告。若新平台无法表达某类关系,记录为显式降级,在消费者确认替代查询前不得销毁旧系统。
退出时先冻结新 source,导出原始证据清单、schema、保存查询、关系和审计记录;让下游改读替代服务并完成正反验证;随后撤销 Collector 凭证、关闭查询入口、删除消息订阅和运行资源,按保留策略处理数据库、备份与导出。最后核对 DNS、云账号、对象存储、告警和持续费用。能停容器不等于能恢复或证明数据已经核销。
