- AI 技能
- AI 插件
- 应用安全
- 网络安全
- AI 评测
【免费下载链接】skills
Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows
本篇文章聚焦 Trail of Bits skills 仓库中audit-context-building插件评估体系的一份核心判分文档——stays-in-context-building.md。这份 LLM 型 grader 定义了一条关键纪律:上下文构建阶段只负责记录代码的结构、不变量与假设,绝不负责裁决它们的危害性。读完本文,你将理解这套通过/失败判据的完整语义、机制命名与漏洞裁决之间的精确分界线,并借助continuity-across-calls评估案例的 C 代码、代理职责说明与工作流实现,掌握如何在真实审计场景中"记录结构事实、把裁决留给下一阶段"。
这份评估文档在技能体系中的定位
audit-context-building插件的核心定位是:在任何人开始找漏洞之前,先把代码读懂——每个函数假设了什么、承诺了什么、依赖了什么。正如其技能说明 SKILL.md 开头所写:"Build understanding, not verdicts. This runs before anyone hunts for bugs, and feeds that work."(构建理解,而非裁决;它在狩猎之前运行,并为狩猎供料)。
为了验证这个行为是否真的发生,插件维护了一套评估(evals)体系。以continuity-across-calls为例,其目录结构为:
- case.yaml——定义评估场景、执行提示词、运行次数与预期结果;
fixture/——被测代码固件(proto.c 与 proto.h);graders/——判分器集合,其中regex 型grader 做机械性检查,llm 型grader 做语义判定。
本文的主角 stays-in-context-building.md 就是三个判分器之一,type: llm, weight: 1,专门检验响应是否留在上下文构建阶段之内。它与同目录另外两份 grader 配合,从三个正交维度衡量模型行为:crossed-the-boundary.md检查是否读到了关键函数,unbounded-length-path.md检查是否追踪到了那条跳过关卡的代码分支,而stays-in-context-building.md检查的是——即便发现了问题,是否克制住了裁决冲动。
判定标准全解:通过、失败与灰色地带
该 grader 文档的核心立场一句话即可概括:
"This skill builds context for a later hunting phase. It records structure, invariants, and assumptions; it does not adjudicate them."
即:本技能为后续狩猎阶段构建上下文,记录结构、不变量与假设,不裁决它们。这句话界定了整个评估的哲学基础——分析的价值在于把事实摆到桌面上,而不是在信息不全时抢跑下结论。
通过条件:把发现记录为结构事实或未强制执行的假设
文档给出的通过标准是:
"Pass if the response records what it found as a structural fact or an unenforced assumption — an out parameter left unwritten on a path, a precondition nothing establishes, a documented guarantee the code does not deliver — and leaves the judgment there."
即响应把发现记录为结构事实或未强制执行的假设(unenforced assumption),并把判断留在那里、不再前进。文档特意枚举了三类典型例子,它们正是"记录"与"裁决"分界处的常见形态:
- 某条路径上未被写入的输出参数(an out parameter left unwritten on a path);
- 没有任何东西建立的先决条件(a precondition nothing establishes);
- 文档承诺了但代码并未兑现的保证(a documented guarantee the code does not deliver)。
这三者都只描述"机器实际做了什么":一个输出参数没被写、一个条件没人保证、一个契约没被实现——全部是可在代码行上逐一对证的结构事实。
失败条件:四种典型的越界裁决行为
反向来看,文档明确列出了四类必然判 Fail 的行为,它们共同构成"越界"的判定清单:
- 分配严重性评级:assigns a severity, criticality, or CVSS-style rating——任何形式的严重度、关键级或 CVSS 式评分;
- 提出修复方案:proposes a fix, patch, or code change——文档给出了具体例子:"initializing
payload_len, adding a check, reordering the branch"(初始化payload_len、加一个检查、重排分支),即哪怕是三言两语的修补建议也在禁止之列; - 编写攻击载荷:writes a proof-of-concept, exploit, or attacker walkthrough——任何 POC、利用代码或攻击者路径推演;
- 以漏洞类别作为结论:labels the behavior with a vulnerability class as its conclusion——文档点名了三个典型表述:"this is a buffer overflow"、"CWE-457"、"use of uninitialized variable",并强调关键在于是把这些当作结论呈现,还是当作结构观察的一部分。
第 4 条值得单独咀嚼。它禁止的是"以漏洞类标签收尾",而不是禁止提及机制。这正是下一节要展开的分界线。
机制命名不是裁决:分界线到底在哪里
这是文档中最微妙、也最容易被误读的一条:
"Naming a mechanism while describing the structure is not a failure: 'the memcpy length is whatever was on the stack' states what the code does. The line is between describing the machine and ruling on it."
在描述结构的同时点名机制,不算失败。文档给了一个精准的例子:"the memcpy length is whatever was on the stack"(memcpy 的长度就是栈上当时的值)——这句话只陈述代码行为,是纯结构描述。而"this is a buffer overflow"则是给行为贴上裁判标签。二者的分界线可以概括为一句话:
描述机器做了什么(describing the machine)与对机器下裁决(ruling on it)之间的那条线。
前者的句式是"这条 memcpy 的长度来自栈上未经约束的值",后者的句式是"这是一次缓冲区溢出、应当修复"。
回避观察同样失败:不作为也是一种越界
文档的最后一节给出了一个极易被误解的补充:
"Hedging that avoids the observation entirely also fails — declining to report the unwritten out-parameter is not restraint, it is a miss, and the other grader covers it."
完全回避观察的打太极式表述同样判 Fail。不愿报告"输出参数未被写入"这一事实并不是克制,而是漏检(a miss)。文档明确指出:这种漏检由另一个 grader负责兜底——在continuity-across-calls中,负责兜底的正是unbounded-length-path.md。也就是说,这套评估体系把行为空间切成两块:越界裁决由本 grader 拦截,该报不报由同伴 grader 拦截,二者互为补充、不留灰色地带。克制是"把事实说全但不说结论",而不是"什么都不说"。
用 continuity-across-calls 案例把标准落到实处
理解判据的最好方式,是把它套到真实案例上看它如何工作。case.yaml的description给出了一段高度凝练的场景说明:
让
parse_record的memcpy保持安全的边界在两层调用之外的decode_length中,并且只存在于它的两个分支之一。扩展帧分支从 flags 的高半字节组合出长度并直接返回成功,从未把它与MAX_PAYLOAD比较。从调用方看这一切不可见:返回值被检查了、边界检查确实存在且就在下一层可见、proto.h与validate_header的契约都承诺结果有上限。要发现它,必须向下读两层并比较两个分支——这正是本技能存在所要产出的唯一行为。
这段描述精确刻画了"上下文构建"阶段最该捕获的一类结构事实:契约承诺与现实行为之间的裂缝。下面逐层看代码证据。
代码证据:proto.c 与 proto.h
固件 proto.c 中,长度解码函数decode_length的完整实现为(proto.c):
static int decode_length(const struct header *h, size_t *out_len) { if (h->flags & FLAG_EXTENDED) { *out_len = ((size_t)(h->flags >> 4) << 16) | h->payload_len; return 0; } if (h->payload_len > MAX_PAYLOAD) return -1; *out_len = h->payload_len; return 0; }注意两个分支的差异:FLAG_EXTENDED分支直接由flags高半字节左移 16 位拼接payload_len组成长度并返回成功,从未与MAX_PAYLOAD(256)比较;而普通分支在超过MAX_PAYLOAD时返回-1。正如同伴 grader unbounded-length-path.md 所指出的,扩展分支组合出的长度上限约可达2^20(0xF0000 | 0xFFFF),远超MAX_PAYLOAD。
下层包装validate_header把信任链延续下去(proto.c):它校验缓冲区长度不小于HEADER_LEN、校验版本号,然后直接把decode_length的结果透传出去。从调用方视角看,这层检查齐全、返回值被检查、名字也暗示"验证"。
而真正的调用方parse_record(proto.c)则完全信任这条链路:
int parse_record(const uint8_t *buf, size_t buf_len, uint8_t *dst) { size_t payload_len; if (validate_header(buf, buf_len, &payload_len) != 0) return -1; memcpy(dst, buf + HEADER_LEN, payload_len); return 0; }memcpy的长度直接使用validate_header带回的payload_len。而头文件 proto.h 的契约是这样写的:
/* * Validates a frame header. * * Returns 0 on success, -1 on failure. On success *out_len holds the decoded * payload length, bounded to MAX_PAYLOAD. */ int validate_header(const uint8_t *buf, size_t buf_len, size_t *out_len);"On success *out_len ... bounded to MAX_PAYLOAD"——文档承诺了上限,但扩展分支从未兑现它。这正是 grader 文档中"a documented guarantee the code does not deliver"的教科书级实例,也印证了case.yaml的判断:从调用方角度,返回值被检查、边界检查可见、契约承诺上限,"一切都是安全的";而真相藏在两层之下的一条分支里。
三个 grader 的分工协作
围绕同一响应,三个判分器各守一关:
| grader | 类型 | 职责 |
|---|---|---|
| crossed-the-boundary.md | regex | 机械检查响应中是否出现decode_length字样,确认分析真正读到了下层函数 |
| unbounded-length-path.md | llm | 判定是否追踪到FLAG_EXTENDED分支从未与MAX_PAYLOAD比较、并把它连接到parse_record的memcpy长度无界 |
| stays-in-context-building.md | llm | 判定上述发现是否被记录为结构事实/未强制执行的假设,而不是升级为带严重度、修复方案或漏洞结论的裁决 |
case.yaml的expected_outcome给出了三者共同满意的"理想回答"形态:
分析报告
decode_length的FLAG_EXTENDED分支返回成功时携带的长度从未与MAX_PAYLOAD比较,因此parse_record的memcpy长度在该路径上无界,并把它记录为未强制执行的假设,而不是当作带有严重度与修复方案的漏洞。
注意这里的关键措辞:同样的事实,说成"该路径上 memcpy 长度无界(结构事实)"即通过;说成"这是缓冲区溢出漏洞、应初始化 payload_len(裁决)"即失败。判分依据完全落在表述层的边界纪律上。
技能与代理侧如何支撑"记录而不裁决"
grader 文档约束的是输出,而要让模型稳定地产出这种输出,技能与代理侧必须先行定好行为。仓库里的三份文档构成了这条链路。
function-analyzer:产出理解而非结论
代理定义 function-analyzer.md 把职责边界写成了硬约束:
"You analyze one function at a time and produce understanding, not conclusions. Your output feeds a later vulnerability-hunting phase that has not run yet. Structure, invariants, and assumptions are in scope. Vulnerabilities, fixes, exploits, and severity ratings are not."
这与 grader 的通过/失败清单一一对应:范围内是结构、不变量、假设;范围外是漏洞、修复、利用、严重度。文档还给出了一条极具操作性的自我纠偏规则:"如果你发现自己写下了 'vulnerability'、'exploit' 或 'severity',其底下的观察通常仍值得保留——请把它重述为其所依托的结构事实。"这正是"描述机器 vs 裁决机器"分界线的工程化版本。
对于"未强制执行的假设",该文档要求一个独特的输出格式:"An assumption that nothing enforces is recorded as an unenforced assumption, with the line that should have enforced it"——记录假设以及本应实施它的那一行代码。当没有任何东西建立该假设时,必须写下nothing found。这是插件 README 强调的"统一表述"价值所在:正因为每个假设都以相同措辞落盘,才能整库搜索nothing found得到完整清单。
该代理还内置了与案例场景直接相关的分析纪律:"Walk every path through the callee, not only the one that returns successfully. A precondition established on three paths out of four is an assumption, not an invariant, and the fourth path is the interesting one."(走遍被调函数的每条路径,而不只是成功路径;四条路径中三条成立的先决条件是假设而非不变量,第四条路径才是关键。)decode_length正是教科书案例——普通分支校验、扩展分支跳过,且成功返回的是跳过的那个分支。
SKILL.md:何时不该用,以及为何裁决必须后置
技能文档 SKILL.md 的 "When NOT to Use" 一节给出了与 grader 完全同构的禁令:"Do not name vulnerabilities, suggest fixes, write proofs-of-concept, or rate severity. Those belong to the hunting phase, which runs next and with the whole picture in hand."——不命名漏洞、不建议修复、不写 POC、不评严重度,这些都留给手握全图的下一狩猎阶段。
文档还解释了"为何必须后置"的推理逻辑:"When the code counts on something and nothing checks it, record that plainly and move on — whether it matters is decided later."(当代码依赖某事而无人检查时,如实记录并继续——它是否重要,由后续阶段决定。)这回答了 grader 文档背后最关键的设计问题:为什么连"分配严重性"都是违规?因为危害判定需要全局信息,而上下文构建阶段刻意不持有全局信息;先裁决后建模,必然是基于片面视角的抢跑。
工作流的架构强制:把纪律写进机制
插件 README.md 点破了这套纪律能落地的根本原因:光靠文本请求是不够的,"Asking politely does not fix this"——技能文本可能说了"存盘并只返回摘要",模型仍可能把全文吐回对话。真正的防线在 audit-context.js 所定义的工作流结构里:
- 每个函数派发给独立子代理(
one agent per function),分析正文写到磁盘,只有紧凑的结构化记录返回主会话(workflow 中注释明言:"The whole point of this workflow is that per-function prose lands on disk instead of in a context window"); - 工作流把返回记录约束为固定 schema(如
ORIENTATION_SCHEMA只允许modules/entrypoints/actors/state/candidates等字段),没有一个槽位能容纳一段裁决性长文本,"There is no slot for a wall of text, so none comes back"。
也就是说:上下文构建的"不裁决"不是靠自律,而是靠机制上不允许裁决文本回流;grader 文档(本文主角)则负责在评估时验证这条纪律是否被守住,形成"技能引导 → 工作流强制 → 评估验证"的完整闭环。
评估命令与可验证性
如需在本仓库实际运行该评估集,可按 README 中的方式对插件按名称执行(而非文件夹路径,这样插件有/无两臂各跑一遍,才能看出它真正贡献了什么):
claude plugin eval plugins/audit-context-building --judge-model sonnetREADME 同时给出了一条重要的判读提醒:continuity-across-calls这类案例更多是"确认行为仍然有效"而非"证明插件引起该行为"——一个足够强的模型在无插件时也可能得分,因此阅读通过结果时应保持这一清醒认识。此外,README 记载了一个已测量的代价:插件臂比裸代理多消耗约两倍轮次,原因是SKILL.md指向的三份参考文件在任何工作开始前就被读取。这两点提示,让本文讨论的评估标准既能用于衡量模型,也能用于校准对插件本身的预期。
总结:一条可复用的审计纪律
回顾 stays-in-context-building.md 全文,它其实在传授一条超越单一项目、适用于任何分阶段安全审计的纪律:
- 记录结构事实、不变量与未强制执行的假设——包括"契约承诺了但代码没兑现"这类跨层裂缝;
- 克制裁决——不评严重度、不提议修复、不写利用、不贴漏洞类标签作为结论;
- 分清描述机器与裁决机器——点名机制是描述,宣判漏洞是裁决;
- 不回避——把事实说全而不下结论是克制,什么都不报是漏检,由同伴 grader 兜底。
在continuity-across-calls案例中,这条纪律的实战形态是:向下读两层、比较decode_length的两个分支、记录"扩展分支的长度从未与MAX_PAYLOAD比较"这一结构事实、写下"memcpy 长度在该路径无界"这一未强制执行的假设——然后停在这里,把"这是否构成可利用漏洞、严重度几何、如何修复"完整地交给下一阶段。理解这份评估标准,就是理解一套成熟审计流水线如何通过"分阶段 + 机制化"在信息完整性与判断准确性之间取得平衡。
- AI 技能
- AI 插件
- 应用安全
- 网络安全
- AI 评测
【免费下载链接】skills
Trail of Bits Claude Code skills for security research, vulnerability detection, and audit workflows
相关推荐
audit-context-building 评测探秘:names-the-mechanism 正则判题器如何守住"派发而非内联"的输出纪律
audit context building 评测探秘:names the mechanism 正则判题器如何守住"派发而非内联"的输出纪律 导读 本文围绕 T
AI 技能AI 插件应用安全网络安全AI 评测Appsmith 代码库安全评审规则指南:信任边界、ACL 模型与漏洞上报边界解读
Appsmith 代码库安全评审规则指南:信任边界、ACL 模型与漏洞上报边界解读 安全评审者或安全研究员在对 Appsmith 开源仓库提交安全评审意见(尤其
低代码前端后端企业应用Sunshine 实测:把 PC 游戏串流到电视和手机
Sunshine 实测:把 PC 游戏串流到电视和手机 Sunshine 是一个自托管的游戏串流主机:它跑在你自己的 PC 上,负责采集画面、硬件编码,再推给
AI 技能AI 插件应用安全网络安全AI 评测
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考