如何构建模型无关的漏洞发现 Harness
背景:不要把安全扫描押注在单一模型上
面向企业级代码库做漏洞发现时,单个模型、单条提示词或一次性 Agent 会话很难承担完整任务。原因并不复杂:
- Agent 往往一次只能维持一个主要假设;
- 真实代码库很快会填满上下文窗口;
- 上下文压缩可能丢失前面发现的线索;
- 单仓库分析无法覆盖跨仓库依赖关系;
- 如果模型供应商调整能力、缓存、温度或推理预算,依赖单一模型的系统会变得脆弱。
更稳妥的做法,是把模型视为可替换组件,把长期有效的部分放在编排层:任务状态、阶段流转、去重、验证、恢复、跨仓库追踪和人工可读报告。
两个常见疑问
为什么不用 subagent?
Subagent 很有用,也适合作为起点。但安全分析通常需要数百个彼此独立的调查任务,它们要能跨运行保留状态,不共享同一个上下文窗口,并且后续可以重新定界、交叉引用和去重。
这类需求包括:
- 持久化;
- 去重;
- 可恢复执行;
- 跨仓库依赖追踪;
- 多阶段验证;
- 可审计的任务队列。
这些更像是编排系统问题,而不是单靠提示词就能解决的问题。
这是不是只适用于前沿模型?
核心并不是某个具体模型,而是 Harness。漏洞发现可以使用当前最适合任务的模型,但系统设计应从第一天开始保持模型无关。
一个重要经验是:不同模型看同一目标时,发现的漏洞集合并不完全相同。因此,可以用一个模型负责初步发现,用另一个模型负责验证,让不同逻辑路径彼此交叉检查。
从一个安全审计 Skill 开始
实践起点是一个约 450 行的 security-audit skill,用于在单个代码仓库上运行。通过反复调整提示词,它能发现真实漏洞。后续的 Harness 只是把其中各阶段工程化、持久化和可扩展化。
这个 skill 最初设计为一次会话内完成 7 个阶段:
- 三个并行研究 Agent 做侦察,并写出
architecture.md; - 每类攻击由一个 Hunter Agent 执行,目标不是普通代码审查,而是尝试“打破”代码;
- 对每个发现进行对抗式验证,尝试推翻它;
- 对幸存发现生成面向人的漏洞报告;
- 按 schema 输出
findings.json,并进行机械校验; - 用一个全新 Agent 独立复核每个发现;
- 将通过复核的发现提交到接收接口。
这个 skill 与后来的 Harness 阶段几乎一一对应:
| Skill 阶段 | Harness 阶段 |
|---|---|
Recon Agent 写 architecture.md |
Recon |
| Hunter 按攻击类别运行 | Hunt |
| Validator 尝试推翻发现 | Validate |
| 幸存发现形成报告 | Report |
findings.json 做 schema 校验 |
机械校验行号、函数和路径 |
| 全新 Agent 再次复核 | 独立验证 |
单次运行的局限
单次运行可以发现一部分问题,但覆盖并不稳定。实践中观察到,单次运行只能发现多次运行累计结果中的一部分,且更容易命中相对简单、不那么隐蔽的问题。
当流程变成“运行十次,然后人工 diff”时,就说明需要更正式的 Harness 了。
最早遇到的三个主要瓶颈是:
1. 上下文耗尽
运行一段时间后,上下文窗口会被填满。模型可能在压缩上下文时丢掉早先追踪的漏洞线索。解决方式是把状态完全外置,把 LLM 当作无状态计算引擎使用。
2. 缺少持久化
如果中途崩溃、触发速率限制或连接不稳定,任务可能需要从头开始。对长时间安全分析来说,这种损耗很昂贵。
3. 难以进行跨仓库推理
单仓库会话看不到应用之间的调用关系和依赖边界,而不少漏洞恰恰出现在组件接口之间。
最小可行 Harness 应该长什么样?
一个真正可用、但仍然足够简洁的 Harness,可以先只包含:
- Recon:理解目标架构;
- Hunt:按攻击类别寻找问题;
- Validate:验证和尝试推翻发现;
- 数据库:保存阶段状态、发现、任务和运行记录;
- 独立 Validator:只能验证,不能提交自己的新发现。
不建议一开始就追求完整系统:
- 如果只有一个重要仓库,暂时不需要跨仓库追踪;
- 如果噪声还不多,暂时不需要专门的 Dedup Agent;
- 先在开发环境中把 skill 和提示词调好;
- 只有当缺少某个架构阶段真正拖慢流程时,再把它补上。
将 Skill 固化为流水线
把一次性 skill 变成覆盖整个代码库舰队的扫描系统,关键在于把每个阶段拆成独立 Agent,并在后面接数据库、前面接编排器。
实践中,这套系统覆盖了 128 个不同代码仓库,并能自动发现和追踪相关依赖。代码库语言包括 Rust、Go、C、Lua、TypeScript、Python,以及多种配置管理系统和静态配置。
该 Harness 没有为每种语言做专门调优,而是让模型处理语法差异,编排层关注更高层的安全流程:任务分解、依赖追踪、验证和去重。
两阶段漏洞研究流程
整体流程可以拆成两个主要系统:
- Vulnerability Discovery Harness(VDH):漏洞发现引擎,主动扫描代码库并产出潜在安全问题;
- Vulnerability Validation System(VVS):漏洞验证系统,接收一个或多个 Harness 的输出,再执行去重、判断和修复流转。
一个关键设计是:VDH 和 VVS 使用不同模型。
这样做有两个好处:
- 安全上:模型 B 会审查模型 A 的输出,相当于用不同训练分布和推理权重进行对抗式复核;
- 运营上:模型供应商可能改变行为细节,系统不应依赖某个模型长期保持完全一致。
阶段一:漏洞发现 Harness(VDH)
VDH 不只是让模型“看代码”,而是由多个阶段组成。
| 阶段 | 主要职责 | 关键机制 |
|---|---|---|
| Recon | 绘制目标架构,识别潜在威胁向量 | 3 个并行 Recon 子 Agent 写 architecture.md |
| Hunt | 按攻击类别执行探索、编译片段、探测二进制 | 可派生兄弟任务,并向 Wishlist 工具读写需求 |
| Validate | 对发现做机械校验和对抗式验证 | 先由普通代码检查 schema、路径等,再由隔离 Agent 尝试推翻发现 |
| Gapfill | 补齐覆盖不足的区域 | 对薄弱的“区域 × 攻击类别”组合生成新 Hunt 任务 |
| Dedup | 合并重叠发现 | 结合确定性代码和 Agent,按根因聚类并实时折叠 |
| Trace | 追踪跨仓库依赖 | 识别漏洞是否通过依赖链影响其他仓库 |
其中最重要的不是某个 Agent,而是阶段之间的“胶水”:
- 每个任务都能恢复;
- 每个发现都能追踪状态;
- 每个阶段都能被替换;
- 每个验证步骤都尽量隔离;
- 机械检查和语义判断分离。
降低误报:先机械检查,再对抗式推翻
漏洞发现系统如果没有强验证环节,很容易被误报淹没。
有效做法是把验证拆成两层:
- 机械校验:检查 schema、路径、行号、函数名、文件是否存在等;
- 语义验证:由隔离 Agent 读取相关代码,主动尝试证明该漏洞不存在。
只有经得起反驳的发现,才进入后续流程。
这种“先质疑,再提交”的流程能显著减少模型幻觉带来的噪声,也能避免 Hunter Agent 同时扮演发现者和裁判。
外置状态:把 LLM 当作无状态计算单元
长任务中,最脆弱的部分是上下文窗口。Harness 的核心策略是把状态放到系统外部,而不是让模型记住一切。
外置状态至少包括:
- 仓库和目标范围;
- 架构摘要;
- 攻击类别;
- 已覆盖区域;
- 候选发现;
- 验证结果;
- 去重关系;
- 跨仓库依赖;
- 任务失败与重试记录。
这样即使某次模型调用失败,也可以从数据库中的任务状态继续运行,而不是丢掉整轮分析。
什么时候需要跨仓库追踪?
跨仓库追踪的价值在于识别组件边界上的漏洞。例如一个库本身看似安全,但调用方以危险方式使用它;或者一个仓库暴露的接口,在另一个仓库中形成实际攻击路径。
但这不是最早应该做的功能。只有当组织中存在多个彼此依赖、且安全影响显著的仓库时,跨仓库追踪才值得优先投入。
可复用的设计原则
这套方法背后的原则可以概括为:
- 模型无关:不要绑定单一模型或供应商;
- 阶段化:把侦察、发现、验证、报告、去重拆开;
- 状态外置:避免把关键记忆放在上下文窗口里;
- 对抗式验证:每个发现都要经过反驳;
- 机械检查与智能判断分离:能用代码检查的,不交给模型;
- 先小后大:从单仓库 skill 开始,逐步演进成 Harness;
- 不同模型交叉复核:发现和验证尽量不要依赖同一套模型判断。
结论
构建漏洞发现 Harness 的重点,不是找到一条完美提示词,也不是押注某个最强模型,而是把安全研究流程工程化。
从一个可运行的 security-audit skill 开始,先验证提示词和攻击场景是否有效;当上下文、持久化、重复运行和跨仓库分析开始成为瓶颈,再逐步加入数据库、编排器、独立验证、去重和依赖追踪。
最终可持续的部分不是某个模型,而是这套可恢复、可替换、可审计的安全分析流水线。
