Codex 中文站Codex 中文站
工具推荐2026-09-17 16:0042 分钟阅读

Cloudflare 开源 Security Audit Skill:让 Codex 像安全工程师一样审代码

拆解 Cloudflare security-audit-skill 的六阶段安全审计工作流,讲清侦察、覆盖率账本、独立验证、结构化报告与安全沙箱。

Cloudflare 开源 Security Audit Skill:让 Codex 像安全工程师一样审代码

大家好,我是 Codex 中文网的站长宇哥。

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

Cloudflare Security Audit Skill 工作流

最近 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中文网,如若转载,请注明出处:https://codex-zh.com/posts/cloudflare-security-audit-skill/

相关文章

工具推荐2026-08-12 11:399 分钟阅读

Codex 如何和 cc-switch 整合使用?本地路由配置教程

这篇文章是 Codex 中文网「API 教程」栏目里的完整教程,主题是 **Codex 如何和 cc-switch 整合使用?本地路由配置教程**。我会尽量用实战视角讲清楚:这个问题是什么、为什么会出现、应该怎么操作、遇到问题怎么排查,...