🔎 Voge —— 多智能体漏洞挖掘流水线
基于污点追踪、完整性闸门与独立对抗式验证的盲测、系统化安全审计。
fork 自 tailcallhq/forgecode,从通用编码 agent 改造为漏洞挖掘引擎。
目录
Voge 是什么?#
Voge 是一条盲测漏洞挖掘流水线。把它指向一个源码树,它会像一位严谨的人类审计员那样审计代码的安全漏洞:枚举不可信输入的攻击面、把数据流追踪到每一个可达的危险 sink、逐一推理每一个净化器(sanitizer),然后用真实的 exploit 证明最强候选的成立——并且只有当独立的 agent 尝试反驳却失败后,才把一个发现标记为 confirmed。
它不是模式匹配器,也不是 CWE 关键词扫描器。发现靠的是语义化的污点追踪;最终产出是带可复现 PoC 支撑的、分级排序的发现报告。
它构建在 forgecode 的 agent 基础设施之上(provider 抽象、HTTP/工具调度、对话/记忆持久化、MCP、TUI 与 ZSH 插件),并用四个专门的安全 agent、一套污点图记忆系统以及一组机械化的完整性闸门替换了 forgecode 的通用编码 agent。
为什么需要单独的流水线?(它拦截了哪些失败模式)#
一个不受约束的 LLM「安全 agent」产出的报告,往往听起来很自信,却以特定的方式出错。Voge 的存在就是为了让这些失败模式在机制上无法被交付:
| 失败模式 | Voge 如何阻止它 |
|---|---|
| 凭空捏造的发现(引用根本不存在、或没读过的代码) | proof_of_read 反伪造:每条覆盖判定都必须引用一行从磁盘重新读取、且与所声明行号精确对应的源码行,并且该行对该文件必须是独特的(公共的 license 头/import 行会被拒绝)。 |
虚假的 confirmed(没有 exploit 却声称有) |
确认闸门:除非独立的 adversary agent 记录了一条绑定到该发现安全实质(CWE + 严重程度 + 污点路径)的 survived 验证,否则 confirmed 会被机械地拒绝。 |
| 确认了中间 sink(效果在下游被中和) | 记录的 sink 必须是权威执行闸门,而不是一个稍后被重新检查的中间值;adversary 必须把效果追踪到那里,并排除再执行(re-enforcement)。 |
| 「审计完成」但其实有文件从没打开过 | 覆盖完成闸门:除非 needs_review == 0、partial == 0、每个 census 路径都有 safe/unsafe 判定、且 census 计数内部自洽,否则拒绝 finalize。 |
| 缩小的 census(只审了 25 个文件中的 2 个就声称完成) | 每种语言一条「受认可、不可缩小」的 census 命令;闸门会用 agent 提供的路径列表交叉核对 census_total;只有 orchestrator 才能 finalize。 |
| 跨审计污染(同一仓库里的不同单元互相串台覆盖数据) | 按 engagement 作用域化覆盖:每次运行由自动生成的 engagement_id 隔离。 |
| 一个不稳定的工具让一次 60 分钟的审计中止 | gating 拒绝与「边路调查」工具(如 fetch)都被隔离出硬中止上限,各自有独立的后备上限。 |
工作原理 —— 四个 agent 的流水线#
┌────────────────────────────┐
│ voge (默认, task) │ ← orchestrator + exploit 验证者
└──┬──────────┬─────────┬────┘
委派 │ │ │ 委派
侦察 ▼ │ │ 验证
┌──────────────┐ │ │ ┌──────────────┐
│ sage │ │ │ │ adversary │
│ 只读 │ │ │ │ 独立 │
│ 污点追踪 │ │ │ │ 反驳+重跑 │
└──────────────┘ │ │ └──────────────┘
▼
┌──────────────┐
│ muse │ ← 分级、排序、写报告
└──────────────┘
| Agent | 角色 | 是否改文件? |
|---|---|---|
voge |
Orchestrator + exploit 验证。构建并运行真实 PoC;唯一可以把发现标记为 confirmed 的 agent(且必须在 adversary 给出 survived 之后)。 |
是(写/运行 PoC) |
sage |
系统化攻击面枚举 + 污点追踪。只读侦察。 | 否 |
adversary |
独立验证者:重新推导污点路径、寻找反驳性 guard、在未修改的目标上重跑 PoC。是 confirmed 的闸门。 |
仅 harness(绝不碰目标) |
muse |
分级、严重程度排序、撰写审计报告。 | 否(仅报告目录) |
发现沿着 speculative → confirmed | refuted 的生命周期流转,记录在一个共享的污点图记忆里(memory_add、memory_update_finding、memory_find_taint_paths、memory_coverage、memory_verification 等)。完整 agent 指南见 docs/agents.md ——两条不可违背的规则(PoC 完整性、系统化审计)、验证闸门、发现生命周期,以及配置。
sink 是按语言区分的:内存安全语言(Go/Rust/Java)使用命令注入 / 路径穿越 / SSRF / 反序列化 / 资源耗尽 sink 以及文件传输元数据污点类;C/C++/unsafe-Rust/CGo 还额外有算术派生分配与溢出类。当出现 import "C" 时,CGo 对应的 .h/.c/.m 文件会被纳入 census。
完整性模型(真正防止错误结果的东西)#
- 覆盖完成闸门(
memory_coverage_status(finalize=true))—— 一道代码闸门,而非文字检查。除非needs_review == 0、partial == 0、total_latest == census_total、每个 census 路径都有safe/unsafe判定(drop-check)、且报告上盖的 coverage revision 与实时图一致(防漂移),否则拒绝 finalize。计数是按 census 作用域的,所以同一工作区里其他审计的覆盖不会阻塞——也不会虚假地满足——本闸门。 - 确认闸门(绑定到安全实质) ——
confirmed要求一条survived验证,其记录的实质(CWE + 严重程度 + 污点 source/propagators/权威 sink/sanitizers)与发现精确匹配。「确认时拓宽」和「用窄祖先验证覆盖」这两种规避都会匹配失败。 - 反伪造(
proof_of_read) —— 从磁盘重新读取被引用的行,并拒绝公共的 license/import 片段,所以一个从没打开过的文件的「safe」判定不可能通过。 - engagement 作用域化 —— 覆盖与记忆按运行隔离;跨单元污染从结构上被消除。
- 隔离的失败预算 —— gating 拒绝(
gating_failure_limit)和边路调查工具(side_tool_names,如fetch)各自在独立、更高的后备上限下计数,所以无论是闸门正常工作还是网络资源抖动,都不会让一次本可完成的审计中止。
快速开始 —— 跑一次盲测审计#
从源码构建(需要 PATH 上有 protoc):
export PROTOC="$(which protoc)" # 或一个已知可用的 protoc 二进制
cargo build --release # 产物:target/release/voge
配置一次 provider(兼容 OpenAI、Anthropic、OpenRouter 等):
voge provider login
对一个包跑盲测审计(headless 单次模式是主要用法):
# 审计一个 Go 包
voge -C /path/to/target -p "对 lib/services/ 下的 Go 代码做一次盲测安全审计。\
使用完整流水线:把枚举+污点追踪委派给 sage,用真实 PoC 验证最强候选\
(你是唯一可以确认的 agent,且必须在 adversary 记录 survived 之后),\
并把分级报告委派给 muse。跑到覆盖闸门 finalize 或明确的 escalation。"
审计报告默认写到本地
plans/目录(已加入.gitignore,不会随仓库发布——里面含真实漏洞细节,请自行妥善保管)。
对于单次跑不完的大包,按子包拆分(每个分片有自己的 engagement):
scripts/voge_audit_split.sh /path/to/target lib/join 30
完整 agent 指南与配置参考见 docs/agents.md 与 docs/configuration.md。
Voge 与上游 forgecode 的对比#
Voge 是 tailcallhq/forgecode 的一个 fork(forge → voge 的重命名在历史里可见)。Forgecode 是一个通用 agentic 编码 CLI;Voge 保留了它的基础设施,把 agent 层改造用于安全审计。
| forgecode(上游) | voge(本 fork) | |
|---|---|---|
| 用途 | 通用编码 agent:解释、实现、重构、审查、提交 | 盲测安全漏洞审计 |
| Agent | 通用编码 agent | 四个专门 agent:sage(侦察)/ voge(exploit+orchestrator)/ adversary(验证)/ muse(报告),星型拓扑 |
| 记忆 | 跨会话记忆存储 | 污点图记忆 + 覆盖追踪 + 绑定实质的验证 |
| 完整性闸门 | 无 | 覆盖完成闸门、对抗式确认闸门、proof_of_read 反伪造、engagement 作用域、边路工具/gating 隔离 |
| 发现方法 | 由 prompt 驱动 | 从不可信输入边界出发的、按语言的系统化污点追踪 |
| 主要模式 | 交互式 TUI / ZSH : 插件 |
Headless -p 盲测审计(TUI + ZSH 插件继承自上游) |
| 产出 | 代码改动、提交信息 | 分级审计报告 + 可复现 PoC + 可查询的发现/污点图 |
| 共享 | provider 抽象、HTTP 客户端、工具调度、对话持久化、MCP、Nix flake | (原样继承) |
如果你想要一个编码助手,用上游 forgecode。如果你想用一条「无法悄悄交付凭空捏造或未确认发现」的流水线来审计代码库的真实漏洞,用 voge。
继承自上游的能力#
这些来自 forgecode,行为与上游一致:
- 三种运行模式 —— 交互式 TUI(
voge)、单次 CLI(voge -p)、ZSH:前缀插件(voge setup)。 - 多 provider —— OpenAI、Anthropic、OpenRouter、Google Vertex、Bedrock、Groq,以及兼容 OpenAI 的端点(
voge provider login)。 - 对话持久化 —— 保存 / 恢复 / 克隆 / 分叉对话。
- MCP ——
.mcp.jsonserver(项目级 + 全局)。 - 自定义 agent / 命令 / skill ——
.voge/agents/*.md、.voge/commands/、.voge/skills/。 - 语义搜索 ——
:sync给代码库建索引,支持按语义查找。
记忆系统 —— 跨审计隔离 + 跨会话学习#
Voge 的记忆不是「通用聊天历史」,是一个漏洞挖掘专用的污点图 + 覆盖追踪 + 验证历史,存于 ~/.voge/.voge.db(SQLite)。它让一个审计 run 的发现被持久化、可查询、跨会话累积,同时严格隔离不同审计/不同工程之间的覆盖与结论。下面是它怎么设计的、为什么这样设计。
记什么(按 kind 分类)#
| kind | 角色 | 生命周期 |
|---|---|---|
architecture |
二进制/代码结构(包边界、sink 分布、调用层) | 长期(很少变) |
pattern |
可复用的攻击模式(sink 类别、guard 模式、anti-pattern) | 长期(cite-boost 加固,被 reinforce) |
fact |
客观事实(目标语言、工具能力、配置) | 长期 |
bug / finding |
候选漏洞(finding 带 taint source/propagator/sink + substance fingerprint) | 活跃,被 supersede 推进 |
verification |
独立验证结果(survived/refuted),绑定到 finding 的 substance | 推动 finding → confirmed |
coverage |
已审计文件 + verdict (safe/unsafe/needs_review/partial) + proof_of_read | 闸门级(count equality) |
preference |
用户偏好 | 长期 |
finding 的 substance fingerprint = hash(CWE + severity + taint source/propagators/sink/sanitized_by file_paths)——只 hash 安全实质,不 hash 文案。所以同一漏洞用不同措辞 filing 会被识别为同一 substance,验证不会跨文案漂移。
怎么隔离:engagement-scoped 一切#
engagement_id 是 voge 记忆的隔离基线(eng-<epoch_secs>-<pid> 格式)。所有 write path 自动盖戳(finding/coverage/verification 的 row 上),所有 read path 自动过滤(coverage/确认闸门等只读本 engagement 的行)。两条铁律:
engagement.id显式 = 同 campaign 多审计共享(同一次活动的相关 audit 互相可见)。- 不设 = 每次 headless run 自动生成新 id(
run_headless在启动时设置eng-<epoch_secs>-<pid>),完全隔离。这堵死了「同 workspace 多个审计互相污染覆盖与 verified 结论」。
engagement 字段在 ~/.voge/.voge.toml(或项目 .voge.toml)配;不配就隔离。配置示例见下面「与安全相关的配置」一节。
怎么版本化:supersede + is_latest 链#
每个 memory 有 id(UUID)+ fingerprint(hash(title, content))+ version + supersedes_id + is_latest。当一个 memory 被「修订」:
- 旧 row 的
is_latest置 0,保留(不删)。 - 新 row 的
supersedes_id指向旧 id,is_latest=1。 - 查询只看
is_latest=1;回溯用supersedes_id链。
好处:审计可被「修正」(finding 加新证据、coverage 从 needs_review 转 safe),但审计轨迹完整——你随时能看「这条 finding 是怎么从 speculative → confirmed 的」「这块代码是何时被审计的、当时发现了什么」。被 refute 的 finding 不删,留作「曾考虑过 + 为何驳回」。
save_memory 的去重——同 fingerprint 的 latest 存在则 update(不是 insert),配合 UNIQUE(project, fingerprint) WHERE is_latest=1 部分唯一索引 + upsert_memory 捕获 UniqueViolation 当 idempotent no-op——三重去重防并发竞态 + 同内容双写。
什么会强化 / 衰减(reinforcement / decay)#
memory_reinforcement(默认开):每条 memory 被引用时 usage_count++、last_used_at 更新;非 finding 的 strength += 0.1(finding 的 strength 是 0..1 的置信度,不被引用强化——置信度来自证据,不是引用次数)。
memory_decay:strength *= exp(-lambda * age_days),下限 min_strength(默认 0.1)。很久没人用的 fact/pattern 慢慢淡出,避免系统被陈年过时信息淹没;但 finding 不衰减(仍是可发现/可确认的)。
跨会话学习的工作方式#
一次 run 结束后,DB 留下三类长期资产:
pattern记忆(如「libxml2 的 char-copy-loop 是 K 类 sink」):下次审计同语言/同类时,sage/voge 把它当强制规则对待。architecture记忆(如「SFTP 包 client-side vs server-side 的边界」):下次同项目再审计时直接复用。fact记忆(如「nsppe NS14.1.72.61 有 37840 函数、hexrays 可用」):下次扫同一二进制时直接 hit。
加上 supersede 链,长期演进的认知(v3 修了 v2 漏的 sink,v4 修了 v3 误报)都按 supersedes_id 串起来——你 cross-check 任意一条 finding 都能看到它的认知历史。
怎么用#
memory_search / memory_read / memory_find_taint_paths / memory_coverage / memory_coverage_status / memory_update_finding / memory_verification——这些是 voge agent 的内存工具。系统 prompt 自动注入 top-N 摘要(按 strength 排序)到每轮对话,所以每个新 run 都从上次积累开始。
~/.voge/.voge.db 可直接用 sqlite3 ~/.voge/.voge.db 查(Diesel 生成的 schema)——列出某 engagement 的 findings、查 substance fingerprint 重复、看 supersede 链。或者用 voge 自带工具:
voge memory search "q" # BM25 搜 title/content/concepts
voge memory read <id> # 单条
voge memory coverage # 覆盖状态
voge memory taint-paths # 查 taint 图
设计取舍(为什么不是别的样子)#
- 不是「每个文件一份独立 DB」:会让 cross-file 关联(taint source/propagator/sink 跨文件、跨包)做不到;用单库 + engagement_id 隔离更简单。
- 不是「按发现自动合并」:LLM-driven auto-consolidation(每 N 次 extraction 跑一次)会因 LLM 不稳定出错——所以 voge 的 substance_fingerprint + 闸门级 substance match 是机械的去重/确认,consolidation 是辅助的、LLM 决策失败也不污染基础。
- 不是「无限增长」:
memory_decay+forget_expired(按forget_after_days)+ superseded 链保留,保证 DB 不会无限膨胀。
具体配置(memory.auto_consolidate / consolidate_every_n_extractions / injection_summary / max_taint_depth / max_taint_paths)见 ~/.voge/.voge.toml。完整机制见 docs/configuration.md 与 docs/agents.md。
与安全相关的配置#
在 ~/voge/.voge.toml(或项目里的 .voge.toml):
# 对抗式确认闸门 —— 没有 survived 验证就拒绝 confirmed。
require_adversarial_verification = true
# 失败预算(相互隔离,所以正常工作的闸门或抖动的边路工具都不会让审计中止)。
max_tool_failure_per_turn = 3 # 核心工具
gating_failure_limit = 8 # 闸门拒绝(隔离)
side_tool_failure_limit = 12 # 边路调查工具(见下)
side_tool_names = ["fetch"]
# engagement 作用域 —— 显式 id 把一组相关审计归到一个 campaign;
# 不设 => 每次运行自动生成一个新 id(完全隔离)。
[engagement]
# id = "my-campaign"
authorized_targets = ["lib/"]
out_of_scope = ["vendor/"]
Headless 运行参数:VOGE_HEADLESS_STALL_SECS(流静默看门狗,不是总运行时长上限——总运行时长由你启动 voge 的外部包装,例如 timeout,来限定)。
局限与诚实的现状#
confirmed在被独立追踪到权威执行闸门之前,只是一个候选。 adversary 被指示这样做,但这是 prompt 层面的;请把 voge 的confirmed当作「高置信度候选」,对任何你会据此行动的发现,自行复核其执行路径。- 需要完整运行时部署的漏洞(真实的网络/crypto/auth-server 栈)往往无法在沙箱里 PoC 确认,会停留在
speculative。纯逻辑漏洞(常见情况)通过单元测试 PoC 可以正常确认。 - 非常大的单元可能超出单次运行的预算——用
scripts/voge_audit_split.sh按子包拆分。 - 这是一个研究/评估工具,不是一个被支持的产品。每个目标的评估往往都会暴露并修复另一个流水线缺口。
致谢与许可#
Voge fork 自 tailcallhq/forgecode。其 agent 基础设施(provider、HTTP、工具、对话/记忆持久化、TUI、ZSH 插件、MCP)属于 forgecode;漏洞挖掘 agent、污点图记忆与完整性闸门是本 fork 的工作。许可条款见 LICENSE。