纸上得来终觉浅,绝知此事要躬行。——陆游

漏洞报告里最贵的部分,是它敢不敢说「不确定」

一个很熟悉的场景

你让一个 agent 去审代码,它读了一遍,回来给你一份报告:这里有 SQL 注入,那里有越权,某处还有硬编码密钥。格式漂亮,措辞笃定,每一条都附了行号。

然后你逐条去核。第一条,参数其实走了预处理。第二条,那个接口前面挂着一层鉴权,agent 没看见。第三条,密钥是个测试夹具里的假值。

一份报告的可信度,不是被错误本身摧毁的,是被错误的确定性摧毁的。当一份报告里既有真问题也有假问题时,你没法只信一半——你只能开始怀疑全部,然后退回人工复审,工具的价值归零。

Cloudflare 开源的这份 security-audit,就是冲这件事来的。它是一份给编码 agent 用的技能,描述写得很克制:多阶段安全审计,结论经过独立核验,且机器可读。

项目地址:https://github.com/cloudflare/security-audit-skill

拆开看,三个关键词:多阶段、独立核验、机器可读。这三个词其实对应三种失败模式——流程走不全、自己验自己、结论没法被程序检查。

它是什么

一句话:把 agent 变成安全审计员。

它不让一个 agent 从头干到尾,而是编排一群相互隔离的 agent,走完六个环节:侦察、以覆盖表为线索的猎取、候选验证、结构化输出、独立的记录核验、目标中立地出报告。

注意「隔离」这个词。整个设计的骨架就架在它上面:找漏洞的人、验漏洞的人、核源的人,是三批不同的人。这不是流程洁癖,这是唯一能让「自查」失效的办法。

出身:从一个 harness 里长出来

这份技能不是凭空设计的,它是 Cloudflare 那套漏洞发现 harness 的种子。那套东西后来长成了一个多阶段、面向整个机队的系统,而这份仓库是它演化之前的单仓库起点。

这件事值得多想一秒。通常开源出来的工具,是「我们内部用的简化版」;而这一份是「我们内部那套东西最开始的样子」。它保留了原初的骨架,也保留了原初的克制——没有为规模做过的妥协,也没有为演示做过的美化。

六个阶段,一条不接受含糊的流水线

一、侦察:先把地形画出来

第一阶段不找漏洞,只画地图。

要画的东西有五样:架构、信任边界、输入面、既有证据、确定性覆盖。产出两份文件,一份是 architecture.md,一份是 coverage-ledger.json。

这一步是整个流程里最容易被跳过、也最不该跳过的。因为「审计」这件事的本质不是搜索,而是知道该搜哪里。一份没有地图的审计,最后一定会退化成「把常见的几个模式 grep 一遍」。

而 coverage-ledger.json 这个命名,暴露了设计者的真实意图:覆盖不是感觉,是账本。每一块被审过的区域都要记一笔,没记的就是没审。

二、按覆盖表打猎

第二阶段从账本出发,把审计单元分派给相互隔离的猎手,记录他们查了什么,再交给「覆盖批评者」去找缺口。

这里有两个角色。猎手负责打,批评者负责问「你漏了什么」。批评者的存在,等于在流程里内置了一个专门唱反调的人——不是等他自然出现,而是设一个岗位。

三、让另一个 agent 来推翻它

第三阶段是整套设计里最狠的一环:每一个候选漏洞,都交给一个全新的验证者,而验证者的任务是证伪。

不是「确认一下这个是不是真的」,是「试着把它推翻」。这两种提问方式会导向完全不同的行为。前者倾向于找支持证据,后者倾向于找反例。

一个 agent 找出来的东西,交给另一个 agent 去杀。杀不掉,才留下。

四、把结论写成能被机器检查的东西

第四阶段开始产出结论,写进 findings.json,三种状态:confirmed、needs_validation、rejected。

写完不算完,还要过 report-schema.json 的校验。

这一步的意义在于把「判断」变成「数据」。一份自然语言的报告,你只能读;一份受 schema 约束的 JSON,你可以查、可以统计、可以拿去和上一轮对比、可以接进 CI。

五、再验一次源

第五阶段,新一批 agent 去核验最终的源码论断。如果某条记录发生了实质性替换,替换后的版本还要再过一位独立验证者。

核验之后还有核验,替换之后还有替换——这个递归没有停在「我觉得对了」,而是停在「另一个人也觉得对了」。

六、报告是中性的

最后阶段从核验过的记录和覆盖账本里派生出三份文档:REPORT.md、FINDINGS-DETAIL.md、NEEDS-VALIDATION.md。

「目标中立」这个说法很讲究。报告不是写给某个特定技术栈、某个特定框架、某个特定部署形态的,它从记录里长出来,记录怎么写,它就怎么呈现。同一套流程审一个 Rust 服务、审一个浏览器扩展、审一个 LLM 应用,产出的报告结构是一致的。

三种判决,三种不同的诚实

这套流程真正的骨头,是那三个状态。

confirmed:有完整的源码追溯,有边界清晰的观测结果。注意这两个条件——追溯和观测缺一不可。只有追溯,那是代码阅读;只有观测,那是现象罗列。

needs_validation:有一个确切但未解决的事实,而且不带严重性等级。这一条最值得鼓掌。一个还没搞清的问题,凭什么给它打「高危」?定级本身就是一种断言,而断言需要证据。

rejected:记录一个被证伪的候选。注意它不是「删掉」,是「记录」。被推翻的东西也进账本,因为下一次审计需要知道这块地方已经被试过了。

三个状态合起来,构成了一个很罕见的品质:这份工具允许自己不确定,并且把不确定标注出来。

校验器不是装饰

流程里有两支校验脚本,出现的位置很说明问题。

validate-coverage-ledger.cjs 在账本刚建立时跑一次,之后每一次账本更新都要再跑一次。

validate-findings.cjs 在第四阶段跑,然后在第五阶段每一次替换之后再跑。

这两句话读起来像流水账,但它们是整个体系的执法环节。没有校验,schema 就只是一份愿望;有了校验,每一次「我觉得改好了」都得先过机器这一关。更关键的是频率——不是最后统一检查,是每次变更都检查。

跑第二遍不是重复劳动

文档里有一句话,轻描淡写,但改变了对这类工具的使用方式:对同一个仓库的多次运行是累加的。

它会拿之前的账本和结论去找缺口,重新验证改动过的源码,把当前源码的证据继续往前带。同时它守了一条线:不把过期的工作或未解决的工作当成已覆盖。

这句话背后是一种很现实的工程观。代码是活的,昨天审过的文件今天可能已经改了。一次性的审计报告,有效期可能只有几天;而一个能把「上一轮审到哪、这一轮该补哪」记住的流程,才配得上「持续」这个词。

那一堆文件在分工什么

仓库里最多的不是代码,是一批按攻击面分类的提示词文件。

  • SKILL.md 讲安装、核心原则、平台术语、流程总览,以及审计里的反模式
  • RECONNAISSANCE.md 是第一阶段
  • HUNTING.md 是第二阶段的编排与方法论
  • ATTACK-CLASSES.md 装核心、通配、以及那些「太明显反而没人看」的攻击提示
  • MEMORY-SAFETY-AND-BINARY.md 管内存安全、二进制、内核
  • AI-AND-LLM.md 管提示注入、agent 与工具、输出处理
  • WEB-PROTOCOL-AND-AUTH.md 管 HTTP 请求构造、缓存、认证协议
  • CLIENT-SIDE.md 管 DOM 注入、消息信任、UI 覆盖、原型污染
  • SUPPLY-CHAIN-AND-RELEASE.md 管依赖、CI、发布、签名、更新、插件与扩展
  • CLOUD-AND-DEPLOYMENT.md 管 IAM、基础设施即代码、容器、无服务器、入口、运行时配置
  • PROTOCOLS-RPC-AND-MESSAGING.md 管 RPC、序列化、队列、消息代理、webhook、流式协议
  • RESOURCE-EXHAUSTION-AND-AVAILABILITY.md 管共享资源、配额、队列、工作进程、运营成本
  • DATA-ISOLATION-AND-LIFECYCLE.md 管租户隔离、缓存、搜索、导出、备份、迁移、删除、恢复
  • DESKTOP-MOBILE-AND-LOCAL-IPC.md 管原生应用、深链、webview、导出组件、辅助进程、守护进程、本地 IPC
  • VALIDATION-AND-REPORTING.md 管第三到第六阶段

这份目录本身就是一份攻击面清单。它承认了一件事:安全审计不是一种技能,是十几种技能,而每一种都需要自己的词汇表。把「内存安全」和「租户隔离」塞进同一段提示词里,结果一定是两边都泛泛而谈。

装与用

安装走 Skills CLI:一条添加命令加上仓库地址,用 --skill security-audit 指定要装的技能;加 --global 可以装到用户级;npx skills --help 能看到 agent 选择与非交互模式的参数。

用起来更像是在跟同事说话。在你要审的代码库里启动 agent,然后直接提要求:

1
security audit this codebase
1
find security vulnerabilities in ./src
1
do a security review, output to ~/audits/my-project

技能的触发是自动的:只要请求落在「安全审计」「找漏洞」「对这个代码做渗透测试」这类语义上,它就会接管。这是 skill 这种形态的好处——它不需要你记住一个新命令,它改变的是 agent 对某类请求的反应方式。

它还分了两档。直接的代码库审计或渗透测试请求,走完整审计模式;而安全提问和聚焦某个具体漏洞的工作,走引导模式,除非你明确要报告产物。

输出目录也有一套默认规则:完整审计模式下没指定目录,默认落到 ~/security-audit-skill/<仓库名>/run-<N>。而「写进目标仓库内部」这件事,只有在你明确选择一个被版本控制忽略的目录时才会发生。

最后这一条是分寸感的体现。安全审计的产物里可能包含未修复漏洞的位置和利用思路,把它随手写进仓库根目录,等于把一份地图留在门口。

没有沙箱,就不执行

需求那一节,值得整段抄下来。

你需要一个支持工具调用与并行子 agent 的编码 agent。需要 Node.js 来跑那两个零依赖校验器。而最硬的一条是:需要一个由操作系统强制隔离的沙箱,用来跑目标代码触发的构建、测试、进程、浏览器、模拟器、fuzzer 和夹具。

这个沙箱要满足四件事:禁用外部网络、使用净化过的白名单环境、强制资源上限、只允许写入指定的临时路径。

然后是一句让整套流程立住的话:如果这些控制不成立,流程会把这个线索保持为 needs_validation,而不去执行目标代码。

也就是说,缺沙箱的代价不是「跑得慢一点」,而是「这条线索只能停在待确认」。它宁可交一份不完整的报告,也不肯在没有围栏的地方跑一段来路不明的代码。

这是安全工具该有的优先级。

五条设计原则

文档末尾列了五条,每一条都在纠正一种常见的坏习惯。

只确认已成立的边界失效。 一个有源码依据但被挡住的可疑线索,保留为 needs_validation,并写清那个确切未解决的事实。

对抗式验证。 检查一个结论的 agent,永远不是发现它的那个 agent。

严重性必须有影响面。 是可能性乘以影响,不是「偏离了某份检查表几条」。

纵深防御的缺口不是漏洞。 如果 A 层已经挡住了这次攻击,B 层缺失只算加固建议。这一条能砍掉报告里一大半的噪音——那些「没有 CSP」「没有 rate limit」「没有二次确认」的条目,多数属于这一类。

多跑几轮能提高覆盖。 文档给了一个来自他们自己测试的数字:单轮运行发现的漏洞,大约是反复运行所发现总量的一半。

这个数字很刺眼。它等于承认:一次审计的覆盖率大概只有五成,无论你多认真。承认这一点,才谈得上设计「多轮累加」的机制;不承认这一点,就会把第一份报告当成全集。

最后

回到最开始那个场景。

一份报告的价值,不在于它说了多少条,而在于它说出的每一条,你敢不敢直接拿去修。要让这件事成立,工具必须做三件不讨喜的事:把找和验分开,把不确定的标出来,把被推翻的也记上。

security-audit 把这三点写进了流程本身——不是写进「使用建议」,是写进文件格式、写进校验脚本、写进 agent 的分工。你没法绕过它,因为绕过之后 JSON 校验就过不去。

安全这件事最难的地方,从来不是发现问题的能力,而是克制地报告问题的能力。一个总想给你多报几条的工具,最后只会让你不再打开它的报告。