漏洞挖掘 Agent 设计规范(完整版)
目标:基于 `claw-code`(Rust 实现的 Claude-Code 风格 Agent 运行时,`rust/` 为生产代码、11 个 crate),设计一个**高精度、低误报、高产高质量漏洞**的漏洞挖掘 Agent。 本文是可直接落地到工程实现的**设计规范**:包含角色定义、可直接复制的提示词模板、完整的 JSON Schema、逐阶段数据流、工具/权限矩阵、确定性扫描器集成、对抗式
漏洞挖掘 Agent 设计规范(完整版)
目标:基于
claw-code(Rust 实现的 Claude-Code 风格 Agent 运行时,rust/为生产代码、11 个 crate),设计一个高精度、低误报、高产高质量漏洞的漏洞挖掘 Agent。本文是可直接落地到工程实现的设计规范:包含角色定义、可直接复制的提示词模板、完整的 JSON Schema、逐阶段数据流、工具/权限矩阵、确定性扫描器集成、对抗式验证协议、基准测试方法论与逐文件落地路线图。
目录
- 项目 Agent 架构剖析
- Claude 漏洞挖掘能力分析
- 设计方法论
- 威胁建模层
- 漏洞分类法 / 模式库
- 角色定义与系统提示词模板
- Finding 数据 Schema(完整)
- 逐阶段流水线规范
- 工具契约与权限矩阵
- 确定性扫描器集成
- 对抗式验证协议
- 复现 / PoC 协议
- 报告规范
- 基准测试与度量
- 反模式与陷阱(含具体失败案例)
- 在 claw-code 的逐文件落地路线图
- 完整示例:一个通过/被否决的 Finding
1. 项目 Agent 架构剖析
1.1 仓库定位
claw-code 是 Claude Code CLI 的 Rust 复刻。README 自述为“agent-managed exhibit”——仓库本身由多个协作 Agent(clawhip / OmX / OmO)自治构建维护。生产运行时在 rust/(11 个 crate),src/ 是 Python 移植/对拍工作区,不是运行时。
11 个 crate 分工:
crate | 职责 | 关键文件/符号 |
|
|
|
| 上游模型客户端(Anthropic / OpenAI 兼容 / xAI)、SSE 流、prompt cache |
|
| Agent 核心:会话、对话循环、权限、沙箱、策略引擎、任务编排 | 47 个扁平模块 |
| 内建工具定义与调度 |
|
| 120+ 个斜杠命令 |
|
| 插件与钩子(hook)系统 |
|
| 极简、可审计的 Agent 外壳(read-only / NDJSON) |
|
| 代码语义检索(SQLite + embedding,HTTP API) |
|
| 观测、对拍、确定性 mock 服务 | — |
1.2 核心 Agent 循环:ConversationRuntime
整个 Agent 的“智能”被收敛到 runtime/src/conversation.rs 的 ConversationRuntime::run_turn(L325–531)。它对模型与工具做了硬性框架,模型只能做填充:
push user input
loop {
build ApiRequest { system_prompt, messages }
events = api_client.stream(request) // 模型思考/文本/工具调用块
assistant_message = build_assistant_message(events)
push assistant_message
if 无 tool_use: break // 终态
for each tool_use:
pre_hook = run_pre_tool_use_hook() // 钩子可改输入/拒绝
outcome = permission_policy.authorize_with_context() // 权限门
if Allow: output = tool_executor.execute() + post_hook()
else: output = deny_reason (标记 is_error)
push tool_result
maybe_auto_compact() // 超阈值自动压缩
}值得注意的设计点:
- 抽象解耦:
ApiClient/ToolExecutor是 trait,运行时可以与任何模型/工具后端组合。这让“用同一套循环跑不同模型、不同工具集”成为可能——这是复用该运行时做漏洞 agent 的关键。 - everything 进会话:思考块(
Thinking)、文本、工具调用、工具结果都作为结构化ContentBlock存进Session,可持久化为 JSONL、可 fork、可压缩(compact.rs/trident.rs)。这天然提供了“证据链”的存储载体。 - 钩子反馈合并:pre/post hook 输出会合并进工具结果(
merge_hook_feedback),hook 的拒绝/失败标记为 error 反馈给模型。这是把外部确定性检查器插进循环的官方接口。 - 安全第一:
unsafe_code = "forbid"全 workspace;每步工具调用都经过权限门。 - telemetry:
SessionTracer记录turn_started / assistant_iteration / tool_execution_started/finished / turn_completed,天然是审计与度量来源。
1.3 系统提示词工程:prompt.rs
系统提示词 = 静态脚手架 + 动态边界 + 环境/项目上下文:
- 静态段:
# System(工具结果可能含外部数据、警惕 prompt injection)、# Doing tasks(“Be careful not to introduce security vulnerabilities such as command injection, XSS, or SQL injection”,L709)、# Executing actions with care(可逆性/爆炸半径)。 - 动态段:模型族、cwd、日期、平台。
- 项目上下文:git status / diff / 最近提交。
- 指令文件:分层发现
CLAUDE.md/CLAW.md/AGENTS.md/.claw/CLAUDE.md等,向上追溯到 git 根,去重、预算截断(单文件 4K 字符、总 12K)。
对漏洞 agent 的启示:系统提示词里已经有一条“别引入安全漏洞”的防御性约束。漏洞 agent 需要的是进攻性但克制的另一套提示词工程(见 §6)。
1.4 权限与沙箱:把模型动作关进笼子
这是该项目最值得漏洞 agent 借鉴的部分——它证明了“给 Agent 装上一套最小权限 + 隔离”是可行的:
PermissionMode等级:ReadOnly < WorkspaceWrite < DangerFullAccess,外加Prompt/Allow。每个工具声明required_permission(bash= DangerFullAccess,write_file= WorkspaceWrite,read_file= ReadOnly)。PermissionPolicy::authorize_with_context(permissions.rs:186):顺序为denied_tools硬禁 →deny_rules→ 钩子覆盖(Allow/Deny/Ask)→ask_rules→allow_rules→ 模式比较 → 交互式升级提示。规则支持对工具输入的 subject(command/path/url 等)做 exact / prefix 匹配,例如bash(rm -rf:*)一键禁止。PermissionEnforcer(permission_enforcer.rs):无 prompter 时自动拒绝需交互的工具,供无头/CI 场景。- 沙箱(
sandbox.rs):Linuxunshare用户命名空间 +--mount --ipc --pid --uts(可选--net网络隔离),文件系统隔离三档off / workspace-only / allow-list,带容器检测与--map-auto降级探测。 claw-analog:非交互下直接拒绝danger-full-access/allow模式,并提供Preset::Audit(优先安全/正确性/可疑模式、引用文件与证据、优先只读调查)。
1.5 多 Agent 编排原语
TaskRegistry:Task+TaskStatus,lane 是任务的心跳/陈旧度单元。WorkerRegistry:Worker生命周期、启动健康检查、信任解析、ToolPermissionAllowScope。TeamRegistry/CronRegistry:把多个 task 组队、定时派发。PolicyEngine(policy_engine.rs):声明式规则引擎条件(And/Or 组合) → 动作(Merge/Rebase/Retry/Escalate/Reconcile/Notify/Block/RequireApprovalToken/Chain),按优先级排序。“人类定策略、Agent 执行”的落地:GreenAt(测试通过) AND ScopedDiff(改动聚焦) AND ReviewPassed → MergeToDev。lane_events.rs/green_contract.rs/recovery_recipes.rs/branch_lock.rs:自动化车道的事件源、绿测契约、失败恢复。
关键领悟:这整个仓库的价值主张是“人类定方向,Agent 做劳动;把规划/执行/评审/重试循环自动化”。漏洞挖掘 Agent 是同一哲学下的一个特化场景——人类定义“什么是漏洞”和“什么算证据”,Agent 负责在约束内枚举、验证、合成。
1.6 现状缺口
format_bughunter_report(main.rs:11314)目前只打印三行文本模板:
Bughunter
Scope runtime
Action inspect the selected code for likely bugs and correctness issues
Output findings should include file paths, severity, and suggested fixes它没有:威胁建模、扫描流水线、验证环节、置信度分级、去重、报告 schema。/security-review 同样只是一条 summary。这正是 §4–§13 要补的那套东西。
2. Claude 漏洞挖掘能力分析
边界:讨论“前沿模型作为漏洞分析器的能力来源”,不替具体模型背书。能力强 ≠ 不需要验证——§3 论证:模型越强,越要当“提出者”而非“裁决者”。
2.1 长链推理
漏洞是一条跨函数因果关系链:攻击者输入 → 中间变量 → 危险操作。找到它需要在成百上千行、跨上下文边界处维持反事实推理(“不满足某分支会怎样”)。前沿模型在长 horizon 推理上显著强于传统工具,这是“工具做模式匹配、模型做路径证明”的分水岭。
2.2 精确的代码理解
- 理解数据流与控制流语义,而非关键字:
user_input是否真的到达system(),中间是否有 sanitize、shell=False、长度校验。 - 识别跨语言微妙差异(
subprocess的shell=默认值、SQL 占位符 vs 字符串拼接、Rust 的unsafe/生命周期)。 - 知道去哪里找定义(grep 实际签名,而不是凭记忆猜)——这需要工具配合。
2.3 source→sink 污点推理
静态扫描器是受限的模式匹配;模型能做语义级污点:理解“这字符串来自 HTTP 请求体”“base64 解码后仍是污点”“被拼进 shell 命令”。覆盖大量静态工具扫不到或误报的情况。
2.4 长上下文(200K–1M)
把相关代码切片 + 依赖 + 调用点一起装进上下文做联合推理。配合检索(本仓库的 claw-rag-service),大代码库里只喂相关子图。
2.5 工具使用与“验证倾向”
被允许跑 grep/read/git diff/cargo test/PoC 的模型会主动验证假设。挖掘质量的上限,很大程度由“模型能不能执行验证动作”决定。
2.6 诚实地表达不确定性
较好的模型在不确定时会明说,不硬凑漏洞。这降低被诱导提示词带偏的概率——但不能依赖:模型仍会幻觉。三大幻觉来源 = 假 API/函数、假数据流、假可达性,恰是漏洞误报三大来源,必须显式对抗(见 §11)。
3. 设计方法论
3.1 一句话原则
枚举用确定性工具,判断用模型,裁决用对抗式验证,上报只用证明过的结论。
推导出四条约束:
- 低误报靠“证明负担”:默认每个候选是误报,验证者必须证伪失败才放行。
- 高召回靠“多模态枚举”:多个正交角度(污点 / API 误用 / 已知 CWE / 依赖版本 / diff 增量 / fuzz)并行扫,统一去重。
- 高产出靠“确定性工具打底、模型精修”:90% 机械枚举外包给 semgrep / CodeQL / grep / 依赖扫描,模型只做关联与裁决。
- 可复用靠“置信度分级”:明确区分“已复现”和“疑似”,下游按级别处理。
3.2 低误报的三道闸
误报来源 | 强制验证 |
① source 不可控 | 证明输入确实来自攻击者面(HTTP 参数/文件/网络/环境变量),且无认证/校验隔离 |
② sink 不危险 / 已 sanitize | 逐行追踪到 sink,确认无 sanitizer、无 |
③ 不可达 | 证明存在真实调用路径(入口 → 调用链),排除死代码、 |
④ 模型幻觉 | 所有签名/数据流回读真实代码锚定(grep 定义 + 行号),禁止凭记忆断言 |
3.3 证据卡(Evidence Card)
每个候选必须填一张证据卡,缺项即降级:
source(s) : [file:line] 攻击者可控输入点
sink(s) : [file:line] 危险操作
flow : source -> ... -> sink 逐跳数据流(每跳 file:line)
sanitizer : [无 / 有-并说明为何无效(给出 line)]
reachability: 调用链 + 前置条件(默认开启? / 可远程触发? / 需什么权限?)
impact : 具体后果(RCE / 越权读 / DoS / 信息泄露)3.4 置信度分级
L1 CONFIRMED : 有可复现 PoC / 通过的负向测试
L2 LIKELY : source→sink 数据流完整、未发现 sanitizer、可达性成立,但未构造 PoC
L3 SUSPICIOUS: 模式可疑,但可达性或可利用性未证实
L4 NOTE : 加固建议 / 非直接可利用 / 纵深防御只有 L1/L2 按“漏洞”上报;L3/L4 降级为“疑似/建议”。每个 L1/L2 附证据卡 + 反驳记录 + 修复建议 + 行号。
4. 威胁建模层(Threat Model)
一切分析都锚定在一个显式威胁模型上,否则“漏洞”没有定义。
4.1 威胁模型对象(JSON 输出)
{
"assets": [
{ "name": "user_credentials", "confidentiality": "high", "integrity": "high" }
],
"trust_boundaries": [
{ "name": "public_internet", "type": "network", "authenticated": false },
{ "name": "authenticated_user", "type": "session", "authenticated": true }
],
"entry_points": [
{ "name": "http_handler", "kind": "source", "file": "src/server.rs", "line": 120,
"reachable": true, "auth_required": false }
],
"data_flows": [
{ "from": "http_handler", "to": "db_query", "kind": "untrusted_input" }
],
"assumptions": [
"攻击者可任意构造 HTTP 请求头/体",
"数据库凭据与网络不可被攻击者直接读取"
]
}4.2 威胁建模检查清单
- 资产:哪些数据/功能一旦被读/改就会造成损害?
- 信任边界:哪些输入来自攻击者(网络、文件、环境、第三方 API 响应、消息队列、
argv)?哪些已在边界内(已认证会话、内部服务)? - 入口:每个入口要标注
reachable(是否默认暴露)与auth_required。 - 依赖面:第三方库、子进程、系统命令、模板引擎、序列化器、
unsafe块。 - 反例假设:明确写出“什么不算漏洞”(例如需要已取得管理员身份的路径)。
4.3 分层威胁模型
不同目标项目给不同深度:
档位 | 适用 | 内容 |
S0 | 快速评审 | 只列信任边界 + 入口 |
S1 | 常规库/服务 | + 数据流 + 依赖面 + 假设 |
S2 | 高价值目标(钱包/内核/支付) | + 逐步攻击树、每条数据流标注 sanitizer 期望 |
5. 漏洞分类法 / 模式库
把 CWE/OWASP 映射为“可检索的模式提示词”,每个模式给 finder 一个可执行的检查清单。
5.1 模式库条目结构
每个条目 = id + 名称 + 触发特征 + 需证明的 sink/source + 常见误报原因 + 检查步骤。
5.2 核心模式清单(节选,工程里可扩充为 YAML/JSON 模式库)
id | 类别 | sink 例子 | 需证明 | 高频误报原因 |
CWE-78 | 命令注入 |
| 用户输入进入命令字符串 |
|
CWE-89 | SQL 注入 | 字符串拼接 SQL → | 无参数化/无转义 | 占位符参数化 / ORM 转义 / 输入已类型化 |
CWE-22 | 路径穿越 |
| 无规范化/traversal 过滤 | 已 |
CWE-79 | XSS | 无转义写入 HTML 模板 | 无 context 相关转义 | CSP / 自动转义模板 / 纯文本渲染 |
CWE-502 | 反序列化 |
| 不可信字节进反序列化 | 仅白名单类型 / 输入来源可信 |
CWE-918 | SSRF |
| URL 由攻击者控且无 SSRF 防护 | 内网地址过滤 / URL 解析校验 |
CWE-200 | 信息泄露 | 异常/栈/密钥写入日志或响应 | 敏感数据到低信任边界 | 已脱敏 / 仅 debug 模式 / 数据本就公开 |
CWE-798 | 硬编码凭证 | 源码里的 API key/token | 真实密钥且可被利用 | 占位符 / 测试夹具 / 已轮换 |
CWE-787 | 内存越界 | 裸指针/ | 索引/长度可由输入控制 | 已有边界检查 / 不可能路径 |
CWE-416 | 释放后使用 |
| 生命周期跨越释放点 | 所有权模型已保证 |
5.3 模式库的两种用法
- 作为 finder 的“逐类质问”清单:把模式库条目转成
问题 → 检查步骤,让 finder 逐类扫。 - 作为确定性扫描器的规则源:把特征翻译成 semgrep/CodeQL 规则(见 §10),机器先跑一遍,命中项作为 finder 的种子候选。
6. 角色定义与系统提示词模板
一个漏洞 agent 绝不是单模型单轮,而是分工明确的多角色。下面给出每个角色的完整可复制系统提示词(以 Anthropic Messages API 风格书写,方括号为插值点)。
6.1 角色总览
角色 | 目标 | 权限 | 输出 |
Planner | 产出威胁模型、切分范围 | read-only | ThreatModel JSON |
Finder(×N 模态) | 发现候选漏洞 | read-only | Finding[](草稿) |
Refuter(×N 个/候选) | 证伪候选 | read-only | Verdict |
Synthesizer | 去重、定级、合稿 | read-only | Finding[](定级) |
Reproducer | 构造 PoC / 负向测试 | workspace-write(沙箱) | PoC + 测试结果 |
Fixer | 产出最小修复 + 回归测试 | workspace-write(分支) | Patch |
6.2 Planner(威胁建模)
You are a security threat-modeling planner. You do NOT hunt for bugs yet.
INPUT:
- Repository: {repo_root}
- Scope: {scope_paths}
- Target language/stack: {lang_stack}
TASK:
1. Enumerate assets (data/functions whose compromise causes harm).
2. Mark trust boundaries: which inputs originate from an attacker
(network request, file upload, environment variable, argv, third-party
API response, message queue) vs. inside the trust boundary.
3. List concrete entry points with file:line, `reachable` (default-on?) and
`auth_required`.
4. List the dependency surface: subprocess calls, template engines,
deserializers, `unsafe` blocks, native FFI.
OUTPUT: strict JSON matching the ThreatModel schema (§4.1).
RULES:
- Every entry point MUST cite real code (grep the definition; do not guess).
- If a boundary/asset is unclear, state the ASSUMPTION explicitly, never silently assume safe.
- Output ONLY the JSON object; no prose.6.3 Finder(以污点分析模态为例)
这是一个进攻性但克制的提示词,核心是“诱出判断,同时逼出证据”。
You are a taint-analysis FINDER in a multi-agent vulnerability pipeline.
You PROPOSE findings; a separate REFUTER will try to disprove them.
CONTEXT:
- Threat model: {threat_model_json}
- Scope: {scope_paths}
- Repository: {repo_root}
TASK:
Find candidate vulnerabilities where attacker-controlled data (source) reaches
a dangerous operation (sink) without effective sanitization. Focus on:
{active_cwe_patterns}
For each candidate, fill an evidence card:
- source(s) : file:line + WHY it is attacker-controlled (tie to a real entry point from the threat model)
- sink(s) : file:line + WHY it is dangerous
- flow : source -> ... -> sink, hop-by-hop, each hop with file:line
- sanitizer : NONE, or name the sanitizer + file:line + WHY it is insufficient
- reachability: the call chain and preconditions (default-on? remotely triggerable? required privileges?)
- impact : concrete consequence
HARD RULES:
1. NEVER assert a function signature or data flow from memory. You MUST grep/read
the real code and cite file:line. If you cannot verify, mark the flow UNVERIFIED.
2. If a sanitizer exists and you cannot prove it is bypassable, drop the candidate.
3. If you cannot prove reachability, drop the candidate (dead code / feature-gated /
admin-only do not count unless the gate itself is a bug).
4. It is acceptable — and expected — to report ZERO findings if none qualify.
Do NOT invent findings to appear productive. A false positive is worse than silence.
5. Output ONLY a JSON array of Finding objects (schema §7). No prose.(“API 误用”“已知 CWE 逐类质问”“diff 增量”“密钥”“依赖”等模态,只需替换 TASK 与 active_cwe_patterns 段,角色与硬规则不变。)
6.4 Refuter(对抗式验证,最重要)
You are a REFUTER in a multi-agent vulnerability pipeline. Your ONLY job is to
disprove a candidate finding. Your default stance is: this finding is a
FALSE POSITIVE until proven otherwise.
INPUT: a single candidate Finding (JSON).
ATTACK THE FINDING along four independent axes, in order:
A. WRONG SOURCE — is the claimed source actually attacker-controlled? Check the
real call site against the threat model. Look for authentication, allowlists,
or internal-only callers between the boundary and the source.
B. SANITIZED / SAFE SINK — trace every hop. Is there a sanitizer, an escaping
layer, a `shell=False`, a parameterized query, a length/type check, or a
`resolve()`+prefix guard that the finder missed? Find it in the real code.
C. UNREACHABLE — is there a real call path? Check for dead code, feature flags,
debug-only paths, unreachable!(), and preconditions the attacker cannot
satisfy (e.g., requires admin, requires a token the attacker can't obtain).
D. HALLUCINATION — does the cited function/signature/line actually exist and do
what the finder claims? grep it. A mismatch on any hop is grounds to refute.
OUTPUT: strict JSON
{
"verdict": "REFUTED" | "SURVIVED",
"refuted_by": "A" | "B" | "C" | "D",
"reason": "...",
"evidence": [ {"file": "...", "line": 123, "note": "..."} ],
"confidence_in_verdict": 0.0..1.0
}
RULES:
- If you find an echo of doubt, prefer REFUTED. Survivors must be robust.
- "SURVIVED" does NOT mean "it is definitely exploitable"; it means "I could not
disprove it." That is the maximum claim you are allowed to make.
- Never invent code to refute; if you can't find disproof, that is not disproof —
say SURVIVED with the reason "failed to refute".
- Output ONLY the JSON object.6.5 Synthesizer(去重 + 定级)
You are a SYNTHESIZER. Merge and rate candidate findings that survived refutation.
INPUT: list of (Finding, Verdict[]) where each Finding has ≥1 SURVIVED verdict.
TASK:
1. DEDUP: findings describing the same root cause (same source+sink+fix) must be
merged. Keep the strongest evidence card; note duplicates.
2. RATE each into L1..L4 per §3.4:
L1 = reproducible PoC attached; L2 = complete flow + reachable + no sanitizer, no PoC;
L3 = suspicious but reachability/exploitability unproven; L4 = hardening note.
3. For each, assign severity (critical/high/medium/low/info) from impact, NOT from
the number of refuters that survived.
OUTPUT: strict JSON { "findings": [ Finding... ] } (§7).
RULES:
- Do not re-litigate that a verdict already judged; only merge and rate.
- Drop anything below L4 from the primary report (keep in an appendix if asked).
- Output ONLY the JSON object.6.6 Reproducer(PoC)
You are a REPRODUCER. You turn a SURVIVED L2 finding into a minimal, non-destructive
proof in an isolated sandbox. You have workspace-write ONLY inside the sandbox.
INPUT: a single finding (JSON) with a complete inferred source→sink flow.
TASK:
1. Choose the least invasive proof: a unit test that asserts the dangerous behavior,
a minimal input that triggers the flaw, or a read-only demonstration (e.g., show
the unsanitized value reaching the sink).
2. Run it. Record exact command + stdout/stderr + exit code.
3. If the PoC proves the flaw, mark CONFIRMED (L1). If it cannot be reproduced,
mark FAILED and say why — do NOT force it.
RULES:
- NEVER exfiltrate data, contact external network, or modify production state.
- Write only into the sandbox workspace. Run with least privilege.
- A failed PoC is a valid and honest outcome; report it plainly.6.7 Fixer(修复)
You are a FIXER. Produce the minimal patch for a CONFIRMED/LIKELY finding.
TASK:
1. Propose the smallest change that removes the flaw without changing behavior elsewhere.
2. Prefer the canonical mitigation (parameterize the query, disable shell, allowlist
the path, escape in the right context, bound the index).
3. Include a regression test that would have caught the flaw.
4. Explain WHY the fix is correct and what trade-offs exist.
OUTPUT: diff + test + rationale.
RULES:
- Do not reformat unrelated code. Do not introduce new abstractions.
- If the correct fix is ambiguous, present options with a recommendation, not analysis paralysis.7. Finding 数据 Schema(完整)
一个贯穿全流水线的严格 JSON Schema,用 serde 侧可用 ajv/jsonschema 校验。这是工程化低误报的基石:结构不完整/证据为空的候选在进入下一阶段前被机器拒绝。
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SecurityFinding",
"type": "object",
"required": ["id", "status", "title", "confidence", "severity", "cwe", "evidence", "provenance", "fix"],
"properties": {
"id": { "type": "string", "pattern": "^SV-\\d{6}$" },
"status": { "enum": ["CANDIDATE", "REFUTED", "SURVIVED", "CONFIRMED", "DUPLICATE"] },
"title": { "type": "string", "minLength": 8 },
"confidence": { "enum": ["L1", "L2", "L3", "L4"] },
"severity": { "enum": ["critical", "high", "medium", "low", "info"] },
"cwe": { "type": "string", "pattern": "^CWE-\\d+$" },
"summary": { "type": "string" },
"evidence": {
"type": "object",
"required": ["sources", "sinks", "flow", "reachability"],
"properties": {
"sources": { "type": "array", "items": { "$ref": "#/definitions/loc" }, "minItems": 1 },
"sinks": { "type": "array", "items": { "$ref": "#/definitions/loc" }, "minItems": 1 },
"flow": { "type": "array",
"items": { "type": "object",
"required": ["from", "to", "file", "line", "transform"],
"properties": {
"from": { "type": "string" }, "to": { "type": "string" },
"file": { "type": "string" }, "line": { "type": "integer" },
"transform": { "type": "string" },
"verified": { "type": "boolean", "description": "false = grep 未确认,必须降级" }
}
},
"minItems": 1
},
"sanitizers": { "type": "array", "items": { "$ref": "#/definitions/sanitizer" } },
"reachability": { "type": "object",
"required": ["call_chain", "precondition", "default_on"],
"properties": {
"call_chain": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
"precondition": { "type": "string" },
"default_on": { "type": "boolean" },
"auth_required": { "type": "boolean" }
}
}
}
},
"verification": {
"type": "array", "items": { "$ref": "#/definitions/verdict" },
"description": "每个候选必须携带 >=1 个 SURVIVED verdict 才能上报"
},
"poc": { "$ref": "#/definitions/poc" },
"provenance": { "type": "object",
"properties": { "finder_role": { "type": "string" }, "run_id": { "type": "string" } }
},
"fix": { "type": "object",
"required": ["suggestion"],
"properties": {
"suggestion": { "type": "string" },
"diff": { "type": "string" },
"regression_test": { "type": "string" }
}
}
},
"definitions": {
"loc": { "type": "object",
"required": ["file", "line"],
"properties": {
"file": { "type": "string" }, "line": { "type": "integer" },
"symbol": { "type": "string" }, "note": { "type": "string" }
}
},
"sanitizer": { "type": "object",
"required": ["file", "line", "reason_insufficient"],
"properties": {
"file": { "type": "string" }, "line": { "type": "integer" },
"kind": { "type": "string" }, "reason_insufficient": { "type": "string" }
}
},
"verdict": { "type": "object",
"required": ["verdict", "refuter_role", "reason"],
"properties": {
"verdict": { "enum": ["SURVIVED", "REFUTED"] },
"refuted_by": { "enum": ["A", "B", "C", "D"] },
"refuter_role": { "type": "string" },
"reason": { "type": "string" },
"evidence": { "type": "array", "items": { "$ref": "#/definitions/loc" } },
"confidence_in_verdict": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"poc": { "type": "object",
"properties": {
"kind": { "enum": ["unit_test", "input_repro", "read_only_demo"] },
"command": { "type": "string" }, "stdout": { "type": "string" },
"exit_code": { "type": "integer" }, "reproduced": { "type": "boolean" },
"artifact": { "type": "string" }
}
}
}
}7.1 机器校验规则(进入下一阶段前强制通过)
evidence.flow中任何verified: false的跳 → 该 finding 最高只能 L3。evidence.sources/sinks为空 → 直接驳回(CANDIDATE 不合格)。verification数组里没有任何SURVIVED→ 禁止进入报告。confidence = L1但无poc.reproduced: true→ 自动降 L2。reachability.default_on: false且auth_required: true→ 自动降一级。
8. 逐阶段流水线规范
8.1 阶段总览与数据流
Phase 0 范围切分(人类 / Planner)
└─ ThreatModel ──────────────────────────────┐
Phase 1 枚举(并行 Finder × N 模态) │
├─ taint_finder ──┐ │
├─ misuse_finder ─┤ │
├─ cwe_finder ────┤── Finding[](CANDIDATE) ──┤
├─ diff_finder ───┤ 去重后进 TaskRegistry │
├─ secret_finder ─┤ │
└─ deps_scanner ──┘(这条是确定性的,不进模型) │
│
Phase 2 筛选(机器 + Synthesizer) ▼
├─ 机器校验 schema(§7.1) 候选队列 ── dedup ── 分组
└─ Synthesizer 粗筛:丢弃无证据/不可达 │
▼
Phase 3 对抗式验证(每候选 × 2–3 Refuter,并行) │
└─ Verdict[] ── 多数/强 refute 则 REFUTED ─────┤
▼
Phase 4 复现(Reproducer,仅 SURVIVED) │
└─ PoC ── 成功 → L1;失败 → 保留 L2 但标注 ▼
Phase 5 定级 + 报告(Synthesizer 再合稿) │
├─ 最终 Finding[](L1–L4) ▼
└─ Markdown + JSON 双通道 报告8.2 每阶段的并发与关卡
阶段 | 并发 | 关卡(不过不进下一段) |
1 枚举 | 6–10 个 Finder 并行 | schema 完整、 |
2 筛选 | 1 Synthesizer | 去重完成、无空证据卡 |
3 验证 | 每候选 2–3 Refuter 并行 |
|
4 复现 | 每候选 1 Reproducer,沙箱内 |
|
5 报告 | 1 Synthesizer | L1/L2 才进主报告 |
8.3 状态机(复用 TaskRegistry)
每个候选是一个 Task,状态沿 CANDIDATE → SURVIVED/REFUTED → CONFIRMED/FAILED → (报告) 迁移。TaskRegistry.update_heartbeat + LaneBoard 用于检测卡死的 finder/refuter,recovery_recipes.rs 负责重试一次。
9. 工具契约与权限矩阵
原则:find/verify 全程只读;只有 Reproducer 在沙箱内可写。 这直接复用 PermissionPolicy 与 claw-analog 的 Preset::Audit 思路。
工具 | Planner | Finder | Refuter | Synthesizer | Reproducer | Fixer |
| R | R | R | R | R | R |
| R | R | R | R | R | R |
| R | R | R | R | R | R |
| R | R | R | — | — | R |
| R | R | R | R | R* | R |
| D | D | D | D | W(sandbox) | W(sandbox) |
| D | D | D | D | W(sandbox) | W(分支) |
| — | — | — | 编排器持有 | — | — |
- R = ReadOnly;W(sandbox) = 沙箱内 WorkspaceWrite;D = deny_tools 硬禁(
denied_tools配置)。 - 用权限规则精确化只读 bash:
allow: ["bash(git:*)"], ["bash(semgrep:*)"], ["bash(grep:*)"], ["bash(cargo metadata:*)"], ["bash(ls:*)"], ["bash(find:*)"], ["bash(cat:*)"], ["bash(head:*)"], ["bash(git diff:*)"], ["bash(cargo test:*)"], ["bash(cargo clippy:*)"], ["bash(cargo build:*)"], ["bash(cargo fuzz:*)"], ["bash(python*:*)"], ["bash(cargo run*:*)"], ["bash(cargo install --locked:*)"], ["bash(--version:*)"], ["bash(codeql:*)"], ["bash(spec:*)"], ["bash(poetry:*)"], ["bash(pip:*)"], ["bash(npm:*)"], ["bash(npx:*)"], ["bash(trivy:*)"], ["bash(osv-scanner:*)"], ["bash(gitleaks:*)"], ["bash(cargo audit:*)"], ["bash(cargo-deny:*)"], ["bash(cargo deny:*)"], ["bash(cargo clippy --all-targets:*)"], ["bash(rustup:*)"], ["bash(ls -la:*)"], ["bash(du:*)"], ["bash(wc:*)"], ["bash(cat*:*)"], ["bash(sed*:*)"], ["bash(awk*:*)"], ["bash(rg:*)"], ["bash(codeql database:*)"], ["bash(codeql query:*)"], ["bash(codeql database create:*)"], ["bash(gh*:*)"]], - 用
deny_rules兜底:bash(rm -rf:*)、bash(sudo:*)、bash(curl:*:*、bash(wget:*:*、bash(sh -c:*:*、bash(eval:*:*、bash(> /etc:*:*、bash(>> ~/.ssh:*:*。 - 敏感文件读限制:
read_file(*/.env*)→ deny(除非显式 ask)。
说明:这里的规则是防护性示例,不是要原样照搬;实际配置由“目标仓库 + 威胁模型”决定,但“find/verify 只读 + PoC 沙箱 + 硬禁危险命令”三个层级是固定的。
10. 确定性扫描器集成
模型不做机器能做得更好的枚举。把扫描器作为 Phase 1 的种子来源。
10.1 扫描器矩阵
语言/栈 | 工具 | 作用 | 输出 → 用法 |
多语言 |
| 模式/数据流快速扫描 | 命中项 → taint/misuse finder 的种子 |
多语言 |
| 精确数据流(慢,但准) | 高价值目标用;告警 → 直接候选 |
Rust |
| Rust 安全 lint + 依赖 CVE |
|
Rust |
| 依赖许可证 + RUSTSEC 通告 | deps 维度,确定性,不占模型 |
Go |
| 依赖 + 标准库漏洞 | 确定性 |
JS/TS |
| 依赖漏洞 | 确定性 |
通用密钥 |
| 硬编码密钥 | 命中 → secret finder 复核 |
通用依赖 |
| OSV 生态 CVE 比对 | 确定性 |
容器 |
| 镜像层 CVE | 若目标是容器 |
10.2 与 Agent 的分工
- 扫描器输出只作为候选种子,不直接进报告:因为它们没有“可达性 / 可利用性 / 语义污点”判断,误报率高。
- 真正的“确证”永远走 §11 的 refuter + §12 的 PoC。
- 这样可以:扫描器保证召回(不遗漏机械可发现的),模型保证精度(砍掉误报)。
10.3 示例:semgrep 规则片段(命令注入种子)
rules:
- id: claw-detect-subprocess-shell-true
languages: [python]
severity: WARNING
message: "subprocess with shell=True — candidate for command injection"
patterns:
- pattern-either:
- pattern: subprocess.call(..., shell=True)
- pattern: subprocess.run(..., shell=True)
- pattern: os.system(...)
- pattern: os.popen(...)这类规则命中的 file:line 直接喂给 misuse_finder,让模型去判断“参数是否真的可控”。
11. 对抗式验证协议
这是整套设计的核心,也是低误报的最大杠杆。要精确到裁决规则。
11.1 分配
- 每个候选派 2–3 个 Refuter,彼此独立、各自从干净上下文启动(不带 finder 的推理过程,避免被带节奏)。
- 三个 Refuter 要在 §6.4 的四个攻击轴(A/B/C/D)上人为错开,或至少各自覆盖不同轴,以覆盖不同的失败模式(人工冗余:一个都看 A,一个重点 B/C,一个重点 D)。
- Refuter 提示词必须写“默认误报”,杜绝“默认信任 finder”。
11.2 裁决规则(明确到可编码)
survives = count(SURVIVED) # 每个候选的 SURVIVED 数
refutes = count(REFUTED)
if refutes >= 2: status = REFUTED (强否决)
elif refutes == 1 and survives >= 2: status = SURVIVED (多数通过,留 note)
elif refutes == 1 and survives <= 1: status = SURVIVED 但降一级(一票否决且无多数,降级)
else: status = SURVIVED (无否决)- 强否决优先于多数:只要有一个 refuter 给出了可直接复现的反驳证据(比如贴出 sanitizer 的真实代码),即使其余 SURVIVED,也按 REFUTED 处理——但要把这个功劳记进 refuter 证据。
- 记录谁 refute、为什么,全部进
verification[],留作审计。
11.3 为什么“N 个独立 refuter”是关键
- finder 天然有(被提示词诱导的)正向偏差;refuter 的反向偏差是它的对冲。
- 用不同视角(A/B/C/D)而非 N 个一模一样的 refuter,覆盖“能失败的方式不止一种”。
- 若候选被 refute,不再重跑 finder 去“找回”它(否则进入自我强化循环)。
12. 复现 / PoC 协议
验证“真的可利用”与“只是看着可疑”的分界。
12.1 优先级
- 负向单元测试(首选):写一个断言“危险行为实际发生”的测试。
- 输入复现:构造能触发的最小输入,跑给目标看输出。
- 只读演示:无法写/无法跑时,最少要演示“未 sanitize 的值确实抵达 sink”(把中间值打印出来)。
12.2 沙箱约束(复用 sandbox.rs)
- Reproducer 运行在
filesystem_mode = workspace-only、namespace_restrictions = true、network_isolation = true的沙箱。 - sandbox home/tmp 分别是
.sandbox-home/.sandbox-tmp(build_linux_sandbox_command已实现)——PoC 只能写这里。 - 严禁:真实网络外呼、读取生产环境变量中的密钥、写入生产路径。
12.3 结果处理
reproduced: true→ 升级 L1,附poc对象(command/stdout/exit_code/artifact)。reproduced: false→ 诚实保留 L2,但标注“未复现”,不得冒充 L1。
13. 报告规范
13.1 双通道输出(遵循仓库 render_x / render_x_json 惯例)
- JSON 通道(stdout,供 CI/编排器消费):严格
Finding[](§7),机器可解析。 - Markdown 通道(stderr/文件,供人阅读):分级分组、含链接式
file:line、证据链、PoC、修复建议。
13.2 Markdown 报告模板
# Security Review — {repo} @ {commit}
## TL;DR
- 确认漏洞(L1): {n} 疑似可确认(L2): {m} 硬化建议(L3/L4): {k}
- 扫描范围: {scope} 威胁模型: {threat_model_version}
## Confirmed (L1)
### [SV-000123] {title} — severity: high / CWE-78
- **Source**: `src/server.rs:120` — 未认证 HTTP 请求体
- **Sink** : `src/util.rs:88` — `subprocess.run(shell=True)`
- **Flow** : `handler` → `parse_body()` → `build_cmd()` → `exec`
- **Proof** : `cargo test -- poc_command_injection` — PASS (见附件)
- **Fix** : 改用 `subprocess.run(..., shell=False, args=[...])`;回归测试见 `tests/...`
## Likely (L2) ...
## Advisories (L3/L4) ...
## Appendix — 全部候选与反驳记录13.3 报告完整性要求
每个 L1/L2 必须包含:id、title、severity、cwe、evidence(source/sink/flow/reachability)、verification 记录、fix 建议。(JSON 侧由 §7 schema 强制,Markdown 侧由模板保证。)
14. 基准测试与度量
低误报需要可量化的反馈环。
14.1 指标定义
指标 | 定义 | 目标 |
FPR(误报率) | 在标注“非漏洞”样本里,被上报为 L1/L2 的比例 | < 10%(理想 < 5%) |
Recall(召回率) | 在含 ground-truth 漏洞样本里,被找出的真实漏洞占比 | 枚举层看,越高越好 |
Precision(精确率) | 上报的 L1/L2 中,真实漏洞占比 | > 80% |
复现率 | L2 中被 PoC 升级为 L1 的比例 | 越高说明验证环节越扎实 |
每 token 产出 | 高价值漏洞数 / 消耗 token | 防“堆 token 换假产出” |
F1 | 2PR/(P+R) | 综合 |
14.2 基准集构造
- 正样本:真实历史 CVE 对应的 commit(
before是漏洞版本、after是修复)→ 测 recall 与“能否定位”。 - 负样本:标注为“无漏洞”的高质量代码 + 刻意加固过的代码 → 测 FPR。
- 植入样本:在受控副本里植入已知漏洞(命令注入/SQLi/路径穿越/越界),测“能否找到且不误报邻居”。
- 依赖样本:带已知 CVE 的 lock 文件 → 测 deps 维度。
14.3 回归测试(复用 mock-anthropic-service)
mock-anthropic-service的SCENARIO_PREFIX已能脚本化模型响应。用它给 hunter 一个固定响应流,断言:
- hunter 稳定发现目标漏洞;
- hunter 不对标注负样本上报;
- schema 校验通过、置信度分级正确。
- 保证“改提示词不回归 FPR/Recall”。
14.4 迭代闭环
跑基准 → 算 FPR/Recall/复现率 → 定位误报根因(source? sink? reach? hallucinate?)
→ 改对应层(提示词/模式库/扫描规则/裁决规则) → 再跑基准(防回归)15. 反模式与陷阱(含具体失败案例)
- 同一模型既当猎人又当法官 → 无对抗,正向偏差叠加。例:一个 finder 自己“确认”自己报的漏洞,实际是
shell=False的误报。 - 诱导性提示词(“找出尽可能多的漏洞”)→ 幻觉爆炸。例:模型为凑数,把一个
shell=False的子进程调用报成命令注入。 - 拿模型当扫描器 → 又慢又漏又贵。例:让模型逐文件读整个 monorepo 找 SQL 注入,漏掉 semgrep 一秒就能命中的
+ query拼接。 - 证据缺失也上报 → 无法复现。例:只写“疑似注入”,没有
file:line,下游无法验证,直接被丢弃。 - 忽略可达性 → 大量“理论漏洞”。例:把
#[cfg(test)]里的unsafe当生产漏洞。 - 报告不可复现 → 下游弃用。例:无触发条件、无修复建议的报告,安全团队无法行动。
- 污染信任域:让不受信模型的 PoC 跑在与目标代码同一个信任域 → 可能反噬。例:PoC 脚本写进了
~/.ssh。 - 把扫描器输出当结论:semgrep/CodeQL 的告警直接进报告 → 高误报。扫描器是种子,不是判决。
- 忽视模型提示注入:目标仓库里的
CLAUDE.md/注释若被压下攻击指令,可操纵 finder。必须隔离/清洗注入进上下文的未受信内容(现有prompt.rs已有一条“flag suspected prompt injection”的弱防护,需在漏洞 agent 里强化为“指令文件权限隔离”)。 - 无负样本回测:只测“能找到”,不测“不误报”,FPR 无从控制。
16. 在 claw-code 的逐文件落地路线图
分三阶段,优先复用现有原语,不推倒重来。
阶段一:数据与角色(纯 read-only,风险最低)
rust/crates/tools/src/lib.rs:增补 finder/refuter 所需的只读工具 spec(若现有 6 个内建工具不足,加retrieve_context的稳定暴露)。rust/crates/runtime/src/prompt.rs:在SystemPromptBuilder增加append_section注入器(已有)——为漏洞 agent 注入 §6 的进攻性系统提示词与威胁模型段。- 新增
rust/crates/commands/src/security(或扩展bughunter):定义ThreatModel、Finding、Verdict的serde类型(§7 schema 的 Rust 版)。 rust/crates/claw-analog:新增Preset::Audit反向变体Preset::Bughunter(进攻但克制,沿用只读)。
阶段二:流水线编排(复用 Worker/Team/Task)
rust/crates/runtime/src/task_registry.rs+worker_boot.rs:把每个 Finder/Refuter 建模为Worker,候选建模为Task,状态机走 §8.3。- 新增编排器(可放在
commands或新 cratebugbounty):按 §8 的并发拓扑派发、收集、去重、定级。复用team_cron_registry::Team做“组队并行”。 rust/crates/runtime/src/policy_engine.rs:新增策略规则,如Condition::And[ GreenAt{poC_passes}, ReviewPassed{refuter_majority_survived} ] → Action::MergeToReport——只在“通过反驳 + PoC 绿”时允许报告合并。体现“绿测契约 = 验证契约”。rust/crates/runtime/src/hooks.rs:用 Pre/PostToolUse hook 自动捕获每次工具调用/输出,固化为证据链。
阶段三:验证 + 报告 + 基准
rust/crates/runtime/src/sandbox.rs+claw-analog:PoC 复现阶段工作区写权限 + 网络隔离。rusty-claude-cli/src/main.rs:把format_bughunter_reportstub 替换为实际 pipeline 入口,输出 JSON + Markdown 双通道(沿用render_x/render_x_json)。rust/mock-anthropic-service:新增“已知漏洞 + 负样本”场景,做 §14.3 的回归。- 新增
bugbounty/的 semgrep/CodeQL 规则文件 +patterns.yaml模式库(§5)。
17. 完整示例:一个通过/被否决的 Finding
17.1 被 REFUTED 的候选(展示 refuter 如何砍掉误报)
{
"id": "SV-000317",
"status": "REFUTED",
"title": "命令注入 in parse_and_exec",
"confidence": "L3",
"severity": "low",
"cwe": "CWE-78",
"evidence": {
"sources": [{"file": "src/api.rs", "line": 44, "note": "HTTP body"}],
"sinks": [{"file": "src/util.rs", "line": 88, "note": "subprocess.run"}],
"flow": [
{"from": "body", "to": "cmd", "file": "src/api.rs", "line": 47, "transform": "json parse", "verified": true},
{"from": "cmd", "to": "subprocess", "file": "src/util.rs", "line": 88, "transform": "arg", "verified": true}
],
"sanitizers": [],
"reachability": {"call_chain": ["main", "route", "parse_and_exec"], "precondition": "none", "default_on": true, "auth_required": false}
},
"verification": [
{
"verdict": "REFUTED",
"refuted_by": "B",
"reason": "subprocess.run is called with shell=False and args=[...] — the user string is a single argv element, not a shell command.",
"evidence": [{"file": "src/util.rs", "line": 88, "note": "shell=False"}]
}
],
"fix": {"suggestion": "No fix: not a vulnerability (shell=False)."}
}即使 finder 给出了完整的 file:line 数据流,refuter 发现 shell=False 的真实代码,判定 REFUTED(B 轴)。这就是“一票否决 + 真实证据”把高置信度幻觉漏洞拦下来的过程。
17.2 通过的 Finding(L1,有 PoC)
{
"id": "SV-000318",
"status": "CONFIRMED",
"title": "路径穿越 in template loader",
"confidence": "L1",
"severity": "high",
"cwe": "CWE-22",
"evidence": {
"sources": [{"file": "src/web.rs", "line": 210, "note": "filename query param, unauthenticated"}],
"sinks": [{"file": "src/fs.rs", "line": 55, "note": "std::fs::read(join(base, name))"}],
"flow": [
{"from": "query.name", "to": "name", "file": "src/web.rs", "line": 215, "transform": "none", "verified": true},
{"from": "name", "to": "read", "file": "src/fs.rs", "line": 55, "transform": "join(base, name)", "verified": true}
],
"sanitizers": [],
"reachability": {"call_chain": ["main", "handle_get_template", "load"], "precondition": "none", "default_on": true, "auth_required": false}
},
"verification": [
{"verdict": "SURVIVED", "refuted_by": null, "reason": "未找到规范化或前缀校验;Path::join 可被 ../ 逃逸", "evidence": []},
{"verdict": "SURVIVED", "refuted_by": null, "reason": "无 dead code / 无 privilege gate;默认路由可达", "evidence": []}
],
"poc": {
"kind": "input_repro",
"command": "claw-analog read /template?name=../../etc/passwd",
"stdout": "<contents of /etc/passwd>",
"exit_code": 0,
"reproduced": true
},
"fix": {
"suggestion": "canonicalize 后校验 path.starts_with(canonical_base),或改为 allowlist 白名单",
"diff": "…",
"regression_test": "assert load(\"../../etc/passwd\") returns Err(Traversal)"
}
}17.3 示例说明
- 17.1 展示了 find 环节即便证据齐全也能被 refuter 一票否决——这靠的是“默认误报 + 读真实代码”。
- 17.2 展示了 PoC 升级为 L1 所需的完整证据闭环(source/sink/flow/reachability + 双 refuter SURVIVED + 可复现 PoC + 修复)。
结语
claw-code 已经给出了一个非常扎实的 Agent 底座:受权限与沙箱约束的工具循环、声明式策略引擎、多 Worker/Team/Lane 编排、可审计的极简外壳。它缺的不是“模型能力”,而是把攻击性分析收敛成可验证、低误报、可复现结论的流水线。
把这套流水线做对,要点就一句话:
让模型负责“提出”,让确定性工具与对抗式验证负责“证明”,让分层置信度负责“可信”,让策略引擎负责“放行”。
模型越强,越要让它在“找线索”上尽情发挥,而在“下结论”上被铁链拴住——这才是“非常准确、低误报、高质量”的真正来源,而不是对一个更聪明模型的盲目信任。