security-audit-skill
纸上得来终觉浅,绝知此事要躬行。——陆游
漏洞报告里最贵的部分,是它敢不敢说「不确定」
一个很熟悉的场景
你让一个 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、导出组件、辅助进程、守护进程、本地 IPCVALIDATION-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 校验就过不去。
安全这件事最难的地方,从来不是发现问题的能力,而是克制地报告问题的能力。一个总想给你多报几条的工具,最后只会让你不再打开它的报告。
