Cloudflare 开源 Security Audit Skill:让 Codex 像安全工程师一样审代码
拆解 Cloudflare security-audit-skill 的六阶段安全审计工作流,讲清侦察、覆盖率账本、独立验证、结构化报告与安全沙箱。

大家好,我是 Codex 中文网的站长宇哥。
本文依据 Cloudflare security-audit-skill 仓库在 2026 年 9 月的公开说明整理。命令、Skill 触发方式和运行环境会随工具版本变化,执行前请以项目 README 为准。

最近 Cloudflare 开源了一个很有意思的项目:
security-audit-skill
项目地址:
https://github.com/cloudflare/security-audit-skill
乍一看,它好像只是:
给 Codex、Claude Code 这类 Coding Agent 安装一个“安全审计 Skill”。
但真正把仓库翻一遍以后会发现,它远远不只是:
帮我检查一下代码有没有漏洞。
Cloudflare 把一次完整安全审计拆成了:
架构侦察
↓
覆盖率规划
↓
漏洞 Hunting
↓
候选问题验证
↓
独立 Agent 二次验证
↓
结构化报告
一共 6 个阶段。
而且一个 Agent 找到的问题,不能自己宣布“这是漏洞”。
必须交给另外一个没有参与发现过程的 Agent,重新阅读源码并尝试推翻这个结论。
这套 Skill 也是 Cloudflare 后来构建更大规模漏洞发现 Harness 的起点。官方仓库明确说明:最初的单仓库 Skill 后来演化成了多阶段、面向大量仓库运行的漏洞发现系统。
我觉得这个项目真正值得学习的不是“安全 Prompt 怎么写”。
而是:
怎么把安全工程师的一套工作流程,固化成 Agent 可以稳定执行的 SOP。
---
# 一、先安装这个 Skill
官方给出的安装方式很简单。
npx skills add https://github.com/cloudflare/security-audit-skill \
--skill security-audit
如果希望全局安装:
npx skills add https://github.com/cloudflare/security-audit-skill \
--skill security-audit \
--global
然后进入你自己的项目:
cd my-project
直接告诉 Agent:
security audit this codebase
或者:
find security vulnerabilities in ./src
也可以指定输出目录:
do a security review,
output to ~/audits/my-project
官方 Skill 会根据请求判断进入“指导模式”还是完整的六阶段 Audit;完整模式还会生成 architecture、coverage ledger、findings 和最终报告等文件。
---
# 二、它最不像普通 Prompt 的地方:先不找漏洞
普通人让 AI 做代码安全审计,通常直接:
帮我找这份代码里的安全漏洞。
然后模型开始:
这里可能 SQL 注入。
这里可能 XSS。
这里可能 CSRF。
这里建议增加鉴权。
最后看起来发现了一大堆问题。
但最大的问题是:
它到底把项目检查了多少?
不知道。
Cloudflare 的第一阶段反而不是找 Bug。
而是:
# Reconnaissance
也就是侦察。
它先要求多个 Research Agent 去回答:
这个系统是干什么的?
有哪些用户?
有哪些入口?
哪些数据来自低信任用户?
认证在哪里?
授权在哪里?
有哪些数据库?
有哪些队列?
有哪些外部接口?
哪些地方会写文件?
哪些地方会执行代码?
部署方式是什么?
官方甚至把侦察阶段拆成多个并行 Agent:
Agent 1a
产品、技术栈、运行方式
Agent 1b
身份、权限、信任边界
Agent 1c
输入入口、数据复制和 Sink
Agent 1d
本地执行和部署环境
每个 Agent 只返回源码事实,并要求给出仓库相对路径和代码位置。
这就比:
扫描一下漏洞。
严谨很多。
---
# 三、先看一个最简单的 Go Demo
我们自己做一个非常小的 API。
项目:
demo-security-app
├── main.go
├── user.go
├── handler.go
└── handler_test.go
假设这是一个用户资料接口。
---
# 四、定义 User
package main
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
模拟数据库:
package main
var users = map[int64]User{
1: {
ID: 1,
Name: "Alice",
Email: "[email protected]",
},
2: {
ID: 2,
Name: "Bob",
Email: "[email protected]",
},
}
---
# 五、写一个看起来完全正常的 API
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func getUser(c *gin.Context) {
idStr :=
c.Param("id")
id, err :=
strconv.ParseInt(
idStr,
10,
64,
)
if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"message": "invalid id",
},
)
return
}
user, ok :=
users[id]
if !ok {
c.JSON(
http.StatusNotFound,
gin.H{
"message":
"user not found",
},
)
return
}
c.JSON(
http.StatusOK,
user,
)
}
路由:
func main() {
router :=
gin.Default()
router.GET(
"/users/:id",
getUser,
)
router.Run(":8080")
}
从功能角度来看:
没有问题。
访问:
/users/1
得到 Alice。
访问:
/users/2
得到 Bob。
---
# 六、但是安全工程师会先问一个问题
当前登录用户是谁?
假设系统已经登录了:
Alice
Alice 应该只能查看:
/users/1
但是当前 Handler 完全没有检查:
当前用户
和:
:id
之间的关系。
这就是安全审计最关键的一件事情:
# 找信任边界
不是看到:
users[id]
就说:
这里有漏洞。
而是要建立完整路径:
低权限用户
↓
可以控制 URL 中的 id
↓
Handler 接收 id
↓
没有 Owner 校验
↓
查询另一个用户
↓
返回另一个用户的数据
只有这样,一个安全问题才真正成立。
Cloudflare 的 Skill 也明确要求 Candidate 必须包含一个真实的低信任主体、输入或动作、预期安全控制、跨越的边界以及具体结果;单纯“缺少最佳实践”不能直接算漏洞。
---
# 七、architecture.md 可以是什么样?
第一阶段结束以后,可以生成:
# Architecture
## Product
Simple user profile API.
## Principals
- authenticated user
- application administrator
## Protected resources
- user profile
- user email address
## Entry surfaces
### GET /users/:id
Source:
handler.go
User-controlled input:
:id
## Trust boundary
Authenticated user should only read
his or her own profile.
## Authorization
Authentication is performed before
the request reaches getUser.
No ownership validation was observed
inside getUser.
## Data flow
HTTP path parameter
→ strconv.ParseInt
→ users[id]
→ HTTP response
注意这里依然没有说:
“发现严重漏洞。”
只是记录:
系统事实。
这就是 Source First。
---
# 八、第二个核心设计:Coverage Ledger
这是我觉得整个项目里最值得学的东西之一。
很多 Agent 做完代码 Review 会说:
我已经全面检查了系统。
问题是:
你凭什么证明?
Cloudflare 为此设计了:
coverage-ledger.json
它把:
入口
×
信任边界
×
子系统
×
攻击类型
拆成一个个可以追踪的 Coverage Unit。
例如我们的 Demo:
[
{
"coverage_id": "http-get-users::owner-boundary::user-api::access-control",
"canonical_refs": {
"surface": "handler.go#GET /users/:id",
"boundary": "handler.go#user ownership",
"subsystem": "user-api",
"attack_class": "ATTACK-CLASSES.md#Access control"
},
"surface": "GET /users/:id",
"boundary": "user ownership",
"subsystem": "user-api",
"attack_class": "access control",
"starting_paths": [
"handler.go"
],
"ordinary_attack_class_block":
"ATTACK-CLASSES.md#Access control",
"selected_companion_blocks": [],
"excluded_blocks": [],
"prior_status": "none",
"attempts": [],
"wave": 1,
"status": "planned",
"agent_id": null,
"reviewed_paths": [],
"local_checks": [],
"result_fingerprints": [],
"unresolved": []
}
]
这个文件真正解决的问题是:
Agent 不再能用一句“我看过了”代替 Coverage。
---
# 九、状态也必须明确
Coverage Unit 不是只有:
完成
或者:
没完成
官方设计了多个状态:
planned
in_progress
covered
candidate
blocked
deferred
out_of_scope
比如:
{
"coverage_id":
"http-get-users::owner-boundary::user-api::access-control",
"status": "candidate",
"agent_id": "hunter-user-api-01",
"reviewed_paths": [
"handler.go",
"user.go"
],
"result_fingerprints": [
"user-profile-missing-owner-check"
]
}
意思是:
这里不是单纯“检查过了”,而是已经产生一个 Candidate。
---
# 十、Hunter Agent 真正应该怎么工作?
Cloudflare 的 Hunting 方法也非常值得抄。
它要求 Agent 每次调查都围绕一个:
# Security Invariant
例如:
登录用户只能读取自己的用户资料。
而不是:
找所有安全问题。
然后按照:
主体是谁?
他能控制什么?
哪个安全控制应该阻止它?
源码实际怎么走?
最终影响了谁?
最小修复是什么?
一路往下追。
官方还明确要求测试各种“异常状态”,例如空值、重复值、超限值、旧状态、撤销状态、并发状态和失败依赖等,但只能在输入接口确实接受这些状态时检查。
---
# 十一、Hunter 可以返回结构化结果
例如:
{
"units": [
{
"coverage_id":
"http-get-users::owner-boundary::user-api::access-control",
"disposition":
"candidate",
"reviewed_paths": [
"handler.go",
"user.go"
],
"checks": [
{
"agent_id":
"hunter-user-api-01",
"reviewed_paths": [
"handler.go"
],
"invariant":
"Authenticated users may only read their own profile",
"method":
"source",
"result":
"Route accepts arbitrary user id and no owner check is visible before lookup",
"artifact":
null
}
],
"candidate_fingerprints": [
"user-profile-missing-owner-check"
],
"unresolved": []
}
],
"candidates": [
{
"proposed_verdict":
"confirmed",
"fingerprint":
"user-profile-missing-owner-check",
"title":
"Missing ownership validation on user profile",
"description":
"Authenticated users can select another user id.",
"root_cause":
"getUser trusts the path id without binding it to the authenticated principal."
}
],
"hardening": [],
"uncovered": []
}
官方要求 Hunter 最终返回一个结构化 JSON,而不是一大段自然语言安全报告。
这非常重要。
因为:
Markdown
适合人看
JSON
适合系统继续处理
---
# 十二、但是这里还不能直接叫“Confirmed”
这是 Cloudflare 设计得很有意思的一点。
Hunter:
发现问题
不等于:
问题成立。
接下来必须进入:
# Phase 3:Independent Validation
官方明确规定:
Candidate 必须交给一个没有参与发现的 Fresh Verifier。
这个 Verifier 的任务甚至不是:
帮前一个 Agent 证明漏洞。
而是:
# 尝试推翻它
官方给 Verifier 的核心指令就是:
You did not write this candidate.
Try to refute it.
Verifier 必须重新读取引用的源码,并重新检查已有控制;能够安全本地复现时,再独立验证最小结果。
---
# 十三、我们也给 Demo 写一个安全的本地测试
注意:
这里完全不需要攻击任何真实系统。
使用:
httptest
只测试自己本地这个 Demo。
---
# 十四、先把当前用户放进 Context
模拟认证 Middleware:
func fakeAuth(
userID int64,
) gin.HandlerFunc {
return func(
c *gin.Context,
) {
c.Set(
"current_user_id",
userID,
)
c.Next()
}
}
---
# 十五、本地测试 Alice 读取自己的资料
func TestGetOwnUser(
t *testing.T,
) {
gin.SetMode(
gin.TestMode,
)
router :=
gin.New()
router.Use(
fakeAuth(1),
)
router.GET(
"/users/:id",
getUser,
)
req :=
httptest.NewRequest(
http.MethodGet,
"/users/1",
nil,
)
resp :=
httptest.NewRecorder()
router.ServeHTTP(
resp,
req,
)
if resp.Code !=
http.StatusOK {
t.Fatalf(
"expected 200, got %d",
resp.Code,
)
}
}
这个测试:
通过。
没有问题。
---
# 十六、再测试 Alice 请求 Bob
func TestCannotReadAnotherUser(
t *testing.T,
) {
gin.SetMode(
gin.TestMode,
)
router :=
gin.New()
router.Use(
fakeAuth(1),
)
router.GET(
"/users/:id",
getUser,
)
req :=
httptest.NewRequest(
http.MethodGet,
"/users/2",
nil,
)
resp :=
httptest.NewRecorder()
router.ServeHTTP(
resp,
req,
)
if resp.Code ==
http.StatusOK {
t.Fatalf(
"security invariant violated: user 1 read user 2",
)
}
}
运行:
go test ./...
如果当前代码返回:
200
测试就失败。
得到:
security invariant violated:
user 1 read user 2
这就是:
# Bounded Local Evidence
只用:
虚构用户
本地进程
本地数据
证明:
权限边界没有生效。
没有继续扩大影响。
这与项目强调的“本地、最小、受控验证”思路是一致的;官方要求目标程序执行时必须运行在严格隔离的 sandbox 中,不能访问外部网络、真实凭证、共享基础设施或生产数据。
---
# 十七、findings.json 才是最终安全记录
验证通过以后:
可以生成类似:
[
{
"verdict":
"confirmed",
"fingerprint":
"user-profile-missing-owner-check",
"title":
"User profile endpoint lacks ownership validation",
"description":
"An authenticated user may select another user identifier.",
"root_cause":
"The route uses the caller-controlled path id without comparing it to the authenticated principal.",
"intended_behavior":
"A normal user should only access the profile associated with the authenticated identity.",
"trace": [
{
"kind":
"entrypoint",
"file":
"handler.go",
"line":
12,
"scope":
"GET /users/:id",
"description":
"User controls the path id."
},
{
"kind":
"sink",
"file":
"handler.go",
"line":
31,
"scope":
"users[id]",
"description":
"Selected user record is returned without an ownership check."
}
],
"evidence": [
{
"file":
"handler_test.go",
"line":
38,
"description":
"Local dummy-user regression test demonstrates the missing ownership check."
}
],
"conditions": [
{
"kind":
"authentication_level",
"description":
"Caller is authenticated as a normal user."
}
],
"execution": {
"attacker_perspective":
"Authenticated dummy user 1",
"payloads": [
"GET /users/2"
],
"instructions": [
"Start only the local test router.",
"Authenticate as dummy user 1.",
"Request dummy user 2."
],
"observed_result":
"The handler returned dummy user 2."
},
"remediation": {
"strategy":
"Bind requested user id to the authenticated principal before loading the record."
},
"severity": {
"likelihood": {
"score":
"high",
"reason":
"The user identifier is directly selectable by an authenticated caller."
},
"impact": {
"score":
"medium",
"reason":
"The demonstrated effect is limited to another dummy user's profile data."
},
"overall_severity":
"medium"
},
"confidence": {
"score":
"high",
"reason":
"The source path and local dummy-data test both establish the result."
}
}
]
Cloudflare 官方 Schema 也明确区分:
confirmed
needs_validation
rejected
confirmed 必须包含 root cause、trace、evidence、conditions、execution、remediation、severity 和 confidence,而不是只有一个漏洞标题。
---
# 十八、怎么修这个 Demo?
真正的修复不是:
隐藏按钮。
也不是:
前端不让用户输入 ID。
而是:
# 后端强制 Ownership Check
例如:
func getUser(
c *gin.Context,
) {
currentUserValue,
exists :=
c.Get(
"current_user_id",
)
if !exists {
c.JSON(
http.StatusUnauthorized,
gin.H{
"message":
"unauthorized",
},
)
return
}
currentUserID,
ok :=
currentUserValue.(
int64,
)
if !ok {
c.JSON(
http.StatusUnauthorized,
gin.H{
"message":
"invalid identity",
},
)
return
}
requestedID,
err :=
strconv.ParseInt(
c.Param("id"),
10,
64,
)
if err != nil {
c.JSON(
http.StatusBadRequest,
gin.H{
"message":
"invalid id",
},
)
return
}
if requestedID !=
currentUserID {
c.JSON(
http.StatusForbidden,
gin.H{
"message":
"forbidden",
},
)
return
}
user, ok :=
users[
requestedID
]
if !ok {
c.JSON(
http.StatusNotFound,
gin.H{
"message":
"user not found",
},
)
return
}
c.JSON(
http.StatusOK,
user,
)
}
再次运行:
go test ./...
这时候:
Alice → Alice
200
Alice → Bob
403
安全边界才真正生效。
---
# 十九、继续加一个管理员场景
真实业务往往还允许管理员查看别人。
可以增加 Role。
type Identity struct {
UserID int64
Role string
}
Middleware:
func fakeIdentity(
identity Identity,
) gin.HandlerFunc {
return func(
c *gin.Context,
) {
c.Set(
"identity",
identity,
)
c.Next()
}
}
权限判断:
func canReadUser(
identity Identity,
targetUserID int64,
) bool {
if identity.Role ==
"admin" {
return true
}
return identity.UserID ==
targetUserID
}
Handler:
identityValue,
exists :=
c.Get(
"identity",
)
if !exists {
c.AbortWithStatus(
http.StatusUnauthorized,
)
return
}
identity :=
identityValue.(
Identity,
)
if !canReadUser(
identity,
requestedID,
) {
c.AbortWithStatus(
http.StatusForbidden,
)
return
}
这样安全规则就明确变成:
普通用户
→ 只能看自己
管理员
→ 可以看所有人
而不是散落在 Controller 里的:
if ...
---
# 二十、再写两个权限回归测试
普通用户:
func TestNormalUserCannotReadOtherUser(
t *testing.T,
) {
router :=
gin.New()
router.Use(
fakeIdentity(
Identity{
UserID: 1,
Role: "user",
},
),
)
router.GET(
"/users/:id",
getUser,
)
request :=
httptest.NewRequest(
http.MethodGet,
"/users/2",
nil,
)
response :=
httptest.NewRecorder()
router.ServeHTTP(
response,
request,
)
if response.Code !=
http.StatusForbidden {
t.Fatalf(
"expected 403, got %d",
response.Code,
)
}
}
管理员:
func TestAdminCanReadOtherUser(
t *testing.T,
) {
router :=
gin.New()
router.Use(
fakeIdentity(
Identity{
UserID: 99,
Role: "admin",
},
),
)
router.GET(
"/users/:id",
getUser,
)
request :=
httptest.NewRequest(
http.MethodGet,
"/users/2",
nil,
)
response :=
httptest.NewRecorder()
router.ServeHTTP(
response,
request,
)
if response.Code !=
http.StatusOK {
t.Fatalf(
"expected 200, got %d",
response.Code,
)
}
}
这就是非常标准的:
安全修复
+
Regression Test
---
# 二十一、为什么不能把所有可疑代码都叫漏洞?
Cloudflare 这个项目还有一个设计我特别喜欢:
# needs_validation
假设源码里看到:
func BuildCallbackURL(
host string,
) string {
return "https://" +
host +
"/callback"
}
Agent 可能会觉得:
host 可控,可能有安全问题。
但如果:
真实生产环境
前面有 Proxy
Proxy 已经严格重写 Host
那漏洞可能并不成立。
问题是:
仓库源码里没有 Proxy 配置。
这时候不能说:
Confirmed High Vulnerability
也不能说:
肯定没问题。
正确状态:
{
"verdict":
"needs_validation",
"fingerprint":
"callback-host-runtime-policy",
"title":
"Callback host trust depends on deployment policy",
"description":
"Source constructs callback URLs from the request host.",
"claimed_root_cause":
"The application relies on an externally supplied host value.",
"trace": [
{
"kind":
"entrypoint",
"file":
"callback.go",
"line":
12,
"scope":
"BuildCallbackURL",
"description":
"Host enters URL construction."
}
],
"evidence": [
{
"file":
"callback.go",
"line":
14,
"description":
"Host is concatenated into callback URL."
}
],
"blockers": [
"Repository does not establish whether the deployment proxy replaces or validates the Host value."
],
"validation_plan": {
"deployment":
"Have the deployment owner inspect the active proxy or ingress host validation policy."
}
}
这其实比:
AI 发现 47 个漏洞!
专业多了。
---
# 二十二、Rejected 也要保存
还有一种情况:
Agent A 说:
这里可能越权。
Verifier 重新检查以后发现:
if target.OwnerID !=
currentUser.ID {
return ErrForbidden
}
其实已经有完整授权。
那么结果:
{
"verdict":
"rejected",
"fingerprint":
"project-delete-owner-check",
"title":
"Suspected project deletion authorization bypass",
"description":
"Initial review suspected deletion lacked owner validation.",
"claimed_root_cause":
"Delete path appeared to trust project id directly.",
"trace": [
{
"kind":
"entrypoint",
"file":
"project.go",
"line":
60,
"scope":
"DeleteProject",
"description":
"Project id comes from caller."
}
],
"evidence": [
{
"file":
"project.go",
"line":
73,
"description":
"Ownership is checked before deletion."
}
],
"reason":
"Source-visible ownership validation blocks the proposed path."
}
为什么 Rejected 也要保存?
因为下一次 Audit:
Agent 不需要再从头浪费 Token 调查:
完全相同的错误猜测。
---
# 二十三、结构化结果还可以直接跑 Validator
项目提供:
validate-findings.cjs
以及:
validate-coverage-ledger.cjs
官方工作流会执行:
node <skill-dir>/validate-findings.cjs \
<output-dir>/findings.json
然后:
node <skill-dir>/validate-coverage-ledger.cjs \
<output-dir>/coverage-ledger.json
这一步解决的是:
AI 输出格式不稳定。
比如 Agent 少写:
"fingerprint"
或者:
"severity": "super-critical"
就应该直接拒绝。
官方 report-schema.json 使用严格的 JSON Schema 分支,并对 Confirmed、Needs Validation 和 Rejected 使用不同字段集合。
---
# 二十四、我们甚至可以再包一层 Validator
比如写:
import {
execFileSync
} from "node:child_process";
const skillDir =
process.env.SECURITY_SKILL_DIR;
const outputDir =
process.argv[2];
if (!skillDir) {
throw new Error(
"SECURITY_SKILL_DIR is required"
);
}
if (!outputDir) {
throw new Error(
"output directory is required"
);
}
execFileSync(
process.execPath,
[
`${skillDir}/validate-findings.cjs`,
`${outputDir}/findings.json`
],
{
stdio: "inherit"
}
);
execFileSync(
process.execPath,
[
`${skillDir}/validate-coverage-ledger.cjs`,
`${outputDir}/coverage-ledger.json`
],
{
stdio: "inherit"
}
);
console.log(
"security audit artifacts valid"
);
运行:
SECURITY_SKILL_DIR=./skills/security-audit \
node validate-audit.js \
./audit-result
以后甚至可以放进 CI。
---
# 二十五、GitHub Actions 可以校验报告格式
例如:
name: Security Audit Artifact Check
on:
pull_request:
jobs:
validate-security-audit:
runs-on:
ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Validate findings
run: |
node skills/security-audit/validate-findings.cjs \
audit/findings.json
- name: Validate coverage ledger
run: |
node skills/security-audit/validate-coverage-ledger.cjs \
audit/coverage-ledger.json
注意这里做的只是:
验证审计产物格式
不代表:
自动在 CI 里运行未知目标代码。
这两个概念一定要分开。
---
# 二十六、为什么 Cloudflare 对 Sandbox 要求特别严格?
因为:
安全审计的对象本身就是不可信代码。
你让 Agent 去执行:
npm install
或者:
make
谁能保证:
package.json
Makefile
测试脚本
里面没有副作用?
所以官方要求目标代码执行必须处于 OS 强制 Sandbox:
禁止外网
空环境变量
源码只读
只能写 scratch
限制 CPU
限制内存
限制进程数
限制文件大小
限制磁盘
限制运行时间
如果做不到:
不执行。
而是把问题标记为:
needs_validation
这也是这个 Skill 和普通:
AI 帮我跑一下项目。
最大的区别之一。
---
# 二十七、如果自己设计 Agent Sandbox,可以怎么想?
比如最简单的 Docker 思路:
docker run \
--rm \
--network none \
--memory 512m \
--cpus 1 \
--pids-limit 64 \
--read-only \
--tmpfs /tmp:size=64m \
-v "$PWD:/target:ro" \
my-audit-runner
这里几个关键点:
--network none
不给外网。
--read-only
容器根文件系统只读。
-v "$PWD:/target:ro"
目标源码只读。
--memory 512m
限制内存。
--pids-limit 64
限制进程数量。
--cpus 1
限制 CPU。
真实生产 Audit Sandbox 还应该进一步加强,但这个例子至少能说明:
Agent 执行代码本身,也必须进入威胁模型。
---
# 二十八、它甚至把 Agent 之间的写权限隔离了
完整 Audit 中:
Parent Agent 管理:
run-metadata.json
architecture.md
coverage-ledger.json
findings.json
REPORT.md
Hunter Agent:
agents/hunter-01/scratch/
Verifier Agent:
agents/verifier-01/scratch/
不能互相修改。
最终 retained artifact:
agents/hunter-01/artifacts/
也只有可信 Parent 负责提升。
也就是说:
Agent A
不能偷偷改 Agent B 的证据。
Agent B
不能偷偷改最终 findings.json。
这个设计已经很接近:
多 Agent 权限隔离。
---
# 二十九、如果自己实现一个简化 Parent,可以这样设计
示意代码:
type AuditAgent struct {
ID string
Role string
}
type AuditTask struct {
CoverageID string
AgentID string
Paths []string
}
type AuditResult struct {
CoverageID string
Disposition string
Fingerprint string
}
分配任务:
func assignHunter(
unit CoverageUnit,
agent AuditAgent,
) AuditTask {
return AuditTask{
CoverageID:
unit.ID,
AgentID:
agent.ID,
Paths:
unit.StartingPaths,
}
}
验证时:
func assignVerifier(
finding Finding,
hunter AuditAgent,
agents []AuditAgent,
) (
AuditAgent,
error,
) {
for _, agent :=
range agents {
if agent.ID ==
hunter.ID {
continue
}
if agent.Role !=
"verifier" {
continue
}
return agent, nil
}
return AuditAgent{},
errors.New(
"independent verifier unavailable",
)
}
这段代码虽然只是示意,但核心原则已经出来了:
Finder
!=
Verifier
---
# 三十、验证 Agent 还可以强制检查 Trace
比如:
func validateTrace(
trace []TraceEntry,
) error {
if len(trace) == 0 {
return errors.New(
"trace is empty",
)
}
if len(trace) == 1 {
if trace[0].Kind !=
"entrypoint" &&
trace[0].Kind !=
"sink" {
return errors.New(
"single trace must be entrypoint or sink",
)
}
return nil
}
if trace[0].Kind !=
"entrypoint" {
return errors.New(
"trace must start with entrypoint",
)
}
if trace[
len(trace)-1
].Kind !=
"sink" {
return errors.New(
"trace must end with sink",
)
}
for i := 1;
i < len(trace)-1;
i++ {
if trace[i].Kind !=
"propagation" {
return errors.New(
"middle trace entries must be propagation",
)
}
}
return nil
}
于是 Agent 说:
这里有越权。
不够。
必须给:
Entry
↓
Propagation
↓
Sink
完整路径。
---
# 三十一、Fingerprint 为什么重要?
假设第一次审计发现:
用户详情缺少 Ownership Check
给:
user-profile-missing-owner-check
下一次代码更新以后重新跑。
Agent 又发现:
用户详情缺少 Ownership Check
不能重新生成:
finding-2026-09-17-001
否则系统不知道:
是新漏洞,还是旧漏洞。
Fingerprint 要稳定:
同一个 Root Cause
↓
同一个 Fingerprint
这样才能:
追踪
去重
重新验证
比较历史
这也是为什么 Cloudflare 把:
安全扫描
逐渐做成:
持续的安全知识状态。
---
# 三十二、多次运行为什么会越来越有价值?
官方 README 特别提到:
多次运行是累加的。
新的 Audit 会读取之前的:
coverage-ledger.json
findings.json
然后判断:
哪些代码没有变化?
哪些漏洞已经验证?
哪些区域上次没有覆盖?
哪些代码发生变化?
哪些 needs_validation
现在可以重新验证?
官方也明确表示,在他们的测试中,单次运行大约只发现重复运行最终能发现漏洞的一半,所以项目并不把“一次扫描”包装成完全覆盖。
这个思路我非常喜欢。
因为:
安全 Audit 本来就不是一次性的。
---
# 三十三、最终目录可能长这样
security-audit/
│
├── run-metadata.json
│
├── architecture.md
│
├── coverage-ledger.json
│
├── findings.json
│
├── REPORT.md
│
├── FINDINGS-DETAIL.md
│
├── NEEDS-VALIDATION.md
│
│
└── agents/
│
├── hunter-auth-01/
│ ├── scratch/
│ └── artifacts/
│
├── hunter-api-01/
│ ├── scratch/
│ └── artifacts/
│
└── verifier-auth-01/
├── scratch/
└── artifacts/
这已经不是:
Prompt → Answer
而是:
# 一个真正的 Agent Workflow
---
# 三十四、最后的 REPORT.md 反而是最后一步
很多人使用 AI:
第一件事就想:
帮我生成报告。
Cloudflare 的顺序刚好相反:
源码事实
↓
架构
↓
Coverage
↓
Hunter
↓
Candidate
↓
Verifier
↓
结构化 Finding
↓
再次验证
↓
最后才生成 REPORT.md
也就是说:
报告只是数据库里的事实最后一种展示形式。
这其实也是非常值得 Agent 开发者学习的设计。
---
# 三十五、这个项目最值得学习的 5 个思想
如果让我把整个 Cloudflare Security Audit Skill 压缩成 5 点,我会留下:
1. Prompt 不等于 Workflow
不是写:
你是一名世界顶级安全专家。
Agent 就真的变成安全专家。
必须给它:
明确阶段
结构化状态
覆盖率
验证
退出条件
---
2. Agent 不能自己给自己证明
Finder
!=
Verifier
否则非常容易出现:
第一个 Agent 产生假设,第二轮自己继续给自己找证据。
---
3. 没证实就不要叫漏洞
三个状态:
confirmed
needs_validation
rejected
比:
high confidence
medium confidence
low confidence
更适合工程。
---
4. Coverage 必须可以计算
不能:
“我已经全面检查。”
而应该:
哪些入口检查了?
哪些边界检查了?
哪些攻击类别检查了?
哪些地方没检查?
为什么没检查?
全部进 Ledger。
---
5. Agent Harness 比模型更重要
你今天可能使用:
Codex
明天可能换:
Claude Code
后天可能换:
其他模型。
但:
Coverage
Workflow
Verifier
Schema
Regression
History
都可以留下来。
这才是真正的工程资产。
---
# 最后
我觉得 Cloudflare 这个项目最值得看的,不是:
AI 现在能不能发现漏洞。
而是:
Cloudflare 开始把安全工程师真实的工作方法,拆成机器可以持续执行的 Agent Workflow。
一个漏洞从出现到进入报告,中间必须经历:
Reconnaissance
↓
Coverage Planning
↓
Hunting
↓
Candidate
↓
Independent Verification
↓
Structured Finding
↓
Final Verification
↓
Report
这和我们现在很多:
把代码扔给 Codex
↓
让它 Review
↓
复制结果
完全不是一个东西。
如果你正在研究:
Codex
Claude Code
Agent
Skill
MCP
AI Coding
我觉得这个项目甚至比:
“它能发现几个安全漏洞”
更值得研究。
因为它展示了一件更重要的事情:
# Skill 到底应该怎么做?
一个真正有价值的 Skill,不应该只是:
一份更长的 Prompt。
它应该把:
经验
流程
状态
约束
检查标准
失败处理
结果格式
验证机制
全部固化下来。
最后让一个普通 Coding Agent,在这套约束下:
表现得越来越像一个专业团队。
这可能才是 Skill 真正开始进入工程化阶段的标志。
常见问题
这个 Skill 能直接替代人工安全审计吗?
不能。它把侦察、覆盖规划、验证和报告做成可重复执行的工作流,但业务规则、部署配置和最终风险接受仍需要熟悉系统的人判断。特别是依赖运行环境的线索,应保留为 needs_validation,不要把推测当作已确认漏洞。
可以在 CI 中直接运行完整审计吗?
只有当 CI 为目标代码提供了操作系统强制隔离的执行环境时才适合这样做。Cloudflare 的要求包括禁用外部网络、净化环境变量、限制资源,并将写入范围限制在分配的临时目录;没有这些条件时,优先验证已有审计产物的格式,而不是执行不受信任的构建或测试。
为什么还要保存被拒绝的候选问题?
被拒绝的结果记录了已经检查过的路径和能够阻断攻击的控制。后续审计可以将其与源码变更对照,减少重复调查,同时在控制被移除或修改时重新验证。
参考资料
相关文章
Codex Skill Installer 有哪些用法?批量安装、私有仓库和实验 Skill
围绕「Codex Skill Installer 有哪些用法」给出面向 Codex 用户的原理、配置、操作步骤、排查方法与常见问题,适合新手直接照着实践。
Codex 如何和 cc-switch 整合使用?本地路由配置教程
这篇文章是 Codex 中文网「API 教程」栏目里的完整教程,主题是 **Codex 如何和 cc-switch 整合使用?本地路由配置教程**。我会尽量用实战视角讲清楚:这个问题是什么、为什么会出现、应该怎么操作、遇到问题怎么排查,...
2026 年最强 Coding Agent Skills 推荐:让 AI 变成真正的软件工程师
盘点 Superpowers、OpenAI 官方 Skills、Claude Code 工作流、MCP 工具生态与 Code Review Skill,说明它们分别解决什么问题,并给出适合个人与团队的 AI 编程组合方案。
Codex + cc-switch 如何接入 DeepSeek?完整配置和避坑指南
这篇文章是 Codex 中文网「API 教程」栏目里的完整教程,主题是 **Codex + cc-switch 如何接入 DeepSeek?完整配置和避坑指南**。我会尽量用实战视角讲清楚:这个问题是什么、为什么会出现、应该怎么操作、遇...