1. 从零拆解 security-audit-skill:一个让 Agent 真正会做安全审计的技能包
第一次看到security-audit-skill这个标题,我脑子里蹦出来的不是“又一个安全扫描脚本”,而是一个更具体的问题:怎么让一个通用 Agent 在拿到一份代码仓库之后,像一名有经验的安全审计工程师那样,按部就班地查、记、核、报,最后还能交出一份别人能复核的结论。这个标题里的skill不是随便起的后缀,它指向的是当下 Agent 生态里一个非常关键的抽象——技能包。而security-audit则限定了这个技能包的领域:安全审计。两个词拼在一起,本质上是在回答“如何把安全审计这门手艺,封装成 Agent 可以稳定调用的能力单元”。
我接触过不少团队做 Agent 安全审计,最常见的失败模式不是模型不够聪明,而是流程不可复现。同一个仓库,今天跑出来 12 条问题,明天跑出来 5 条,后天又冒出 20 条,没人知道哪次是对的。security-audit-skill这类项目要解决的核心痛点就在这里:它把审计过程结构化,用findings.json记录发现,用coverage-ledger.json记录覆盖范围,让“查了什么、没查什么、查出了什么”三件事都有据可查。这套东西适合谁?适合正在做 Agent 开发、想把安全能力产品化的工程师,也适合安全团队里想用自动化手段提升审计效率的人,哪怕你之前没写过 Agent,只要理解基本的安全审计逻辑,也能跟着把整套流程跑起来。
我下面会按“设计思路—核心文件—实操流程—问题排查”这条线,把这个技能包拆开讲透。中间会补充很多原始描述里没写、但实际落地时一定会遇到的细节,比如findings.json的字段怎么设计才不会被下游工具嫌弃,coverage-ledger.json怎么记才不会变成一笔糊涂账。这些经验大多来自我自己和身边团队踩过的坑,你直接抄作业就行。
2. 整体设计与思路拆解:为什么是 skill,而不是一个脚本
2.1 skill 和 agent 的区别,决定了这个项目的形态
热词里反复出现“skill 和 agent 的区别”,这个问题不搞清楚,security-audit-skill的设计就无从谈起。我的理解很直白:Agent 是那个“会自己决定下一步做什么”的执行者,skill 是“被 Agent 调用的一套确定性能力”。打个比方,Agent 是厨师,skill 是菜谱加一套预处理好的食材。厨师可以自由发挥,但做宫保鸡丁的时候,他得按菜谱来,否则每次味道都不一样。
安全审计这件事,恰恰最怕“自由发挥”。你今天让 Agent 自由地去审代码,它可能花大量时间在读 README,明天又可能直接跳到某个可疑函数。security-audit-skill的价值就在于把审计动作固化下来:先做什么、再做什么、结果写到哪里、覆盖范围怎么界定,全部写死在 skill 里。Agent 只负责在合适的时机触发这个 skill,以及根据 skill 的输出决定要不要继续深挖。这样既保留了 Agent 的灵活性,又保证了审计过程的可复现性。
2.2 为什么用 findings.json 和 coverage-ledger.json 两个文件
很多同类项目只输出一个报告文件,security-audit-skill却坚持用两个 JSON。这不是为了显得复杂,而是因为“发现”和“覆盖”是两件必须分开记录的事。findings.json回答的是“我查出了什么问题”,coverage-ledger.json回答的是“我到底查了哪些地方”。这两件事混在一起,后果很严重:你看到报告里只有 3 条问题,会以为代码很干净,但实际上可能只是审计范围只覆盖了 10% 的模块。
我见过一个真实案例,某团队用 Agent 审一个中型项目,报告里干干净净,上线后却出了个低级漏洞。复盘时才发现,Agent 当时只扫了src/api目录,src/utils里的加密逻辑压根没进审计范围,而coverage-ledger.json如果存在,这个问题在第一眼就能被发现。所以这两个文件的分工是:findings.json面向“问题”,coverage-ledger.json面向“过程”。前者给修复的人看,后者给复核的人看。
2.3 方案选型背后的取舍:确定性优先于覆盖率
设计这类 skill 时,一个绕不开的取舍是:要不要追求“尽可能多查”。我的经验是,在 Agent 场景下,确定性比覆盖率更重要。原因很简单,Agent 的调用是有成本的,无论是 token 消耗还是执行时间,如果 skill 设计成“地毯式扫描”,Agent 很容易在执行到一半时因为上下文超限或超时被终止,热词里那句“agent execution terminated due to error”就是这类问题的典型表现。
security-audit-skill的思路是:先定义清晰的审计边界,在边界内做深度检查,边界外用coverage-ledger.json明确标注“未覆盖”。这样做的好处是,即使这次审计不完整,下一次也能接着上次的 ledger 继续,而不是每次从头再来。这种“可续跑”的设计,在实际项目里比“一次扫全”实用得多。
3. 核心细节解析与实操要点:两个 JSON 到底该怎么写
3.1 findings.json 的字段设计与避坑
findings.json看起来简单,但字段设计直接决定了它能不能被下游工具消费。我推荐的最小可用结构是这样的:
{ "findings": [ { "id": "SEC-001", "title": "用户输入未做长度校验", "severity": "medium", "category": "input-validation", "location": { "file": "src/api/user.js", "line": 42, "snippet": "const name = req.body.name;" }, "description": "name 字段直接来自请求体,未做长度和字符集校验,可能导致后续处理异常。", "recommendation": "在写入前增加长度上限和字符白名单校验。", "confidence": "high" } ] }这里有几个字段是我强烈建议保留的。id必须稳定,不能每次跑都变,否则修复跟踪就断了。severity和category分开,因为严重程度和问题类型是两个维度,混在一起会让统计变得困难。confidence这个字段很多人会忽略,但在 Agent 审计里它特别重要——Agent 的判断有时是推测性的,标注置信度能让复核的人知道哪些结论需要人工确认。
注意:
location.snippet不要放太长的代码片段,控制在 3 到 5 行以内。我试过放整段函数进去,结果findings.json膨胀到几 MB,Agent 后续读取时直接超了上下文。
3.2 coverage-ledger.json 的记录粒度
coverage-ledger.json的核心是回答“哪些地方查了,哪些没查”。它的结构我建议按“模块—状态—原因”来组织:
{ "coverage": [ { "path": "src/api", "status": "audited", "files_checked": 12, "findings_count": 3 }, { "path": "src/utils/crypto.js", "status": "skipped", "reason": "文件超过 2000 行,超出单次审计上下文限制" }, { "path": "src/legacy", "status": "partial", "reason": "仅审计了入口文件,内部依赖未展开" } ] }status用三个值就够了:audited、partial、skipped。不要用太多状态,否则统计时自己都记不清。reason字段是给未来的自己看的,写清楚为什么跳过,比单纯标一个skipped有价值得多。我踩过的坑是早期没写reason,过了一周回头看 ledger,完全想不起来当时为什么没审某个目录,只能重新跑一遍。
3.3 skill 的触发条件与边界定义
一个 skill 能不能用好,很大程度上取决于它的触发条件写得清不清楚。security-audit-skill不应该在任何时候都被调用,它应该有一个明确的触发场景,比如“当用户要求对某个代码目录做安全审计时”或者“当 Agent 检测到新增代码涉及敏感操作时”。触发条件写得太宽,Agent 会频繁调用,浪费资源;写得太窄,又会在需要的时候漏掉。
边界定义同样重要。我建议在 skill 里明确写清楚:单次审计最多处理多少个文件、单个文件最大多少行、遇到超大文件怎么处理。这些限制不是为了偷懒,而是为了让 skill 的行为可预测。热词里提到的“agent 记忆”问题,在安全审计场景下尤其明显——如果一次审计塞进去太多内容,Agent 的上下文会被占满,后面的判断质量会断崖式下降。
4. 实操过程与核心环节实现:把审计流程跑起来
4.1 环境准备与 skill 接入
假设你已经有一个可用的 Agent 框架,接入security-audit-skill的第一步是把 skill 的描述文件放到框架能识别的位置。不同框架的目录约定不一样,但核心内容是一致的:一份 skill 说明,加上findings.json和coverage-ledger.json的模板文件。我习惯把模板文件放在 skill 目录下的templates/里,这样每次审计开始时,Agent 先复制模板,再往里填内容,避免结构跑偏。
接入时有个细节容易被忽略:skill 的说明里要写清楚输入是什么。security-audit-skill的输入应该至少包含“目标目录路径”和“审计范围说明”。如果只给一个路径,Agent 可能会把整个仓库都当成审计对象,包括node_modules这种明显不该审的目录。我一般会在输入里加一个exclude列表,把依赖目录、构建产物、测试快照都排除掉。
4.2 审计执行的分阶段策略
一次性审完整个项目,在 Agent 场景下几乎必然失败。我的做法是把审计分成三个阶段,每个阶段结束后都更新一次coverage-ledger.json。
第一阶段是“结构扫描”。Agent 先遍历目标目录,生成文件清单,按目录分组,统计每个目录的文件数和总行数。这一步不涉及具体的安全判断,纯粹是摸清家底。扫描结果直接写进coverage-ledger.json,所有条目初始状态设为pending。
第二阶段是“重点审计”。根据第一阶段的结果,优先审计高风险目录,比如处理用户输入的、涉及认证授权的、做数据序列化的。每审完一个目录,就把对应条目的状态从pending改成audited或partial,并把发现写入findings.json。这个阶段要控制节奏,我一般建议每审 5 到 8 个文件就落一次盘,防止中途出错导致前面的工作白费。
第三阶段是“补漏与复核”。检查coverage-ledger.json里还有哪些pending或partial的条目,评估是否值得继续审。如果某个目录一直没审,要么补审,要么在 ledger 里写清楚跳过原因。这个阶段还要对findings.json做一次去重和严重程度校准,因为分阶段审计时,同一个问题可能在不同阶段被重复发现。
4.3 参数计算:文件大小与审计深度的平衡
审计深度和文件大小直接相关。我的经验值是:单个文件超过 800 行,Agent 的审计质量就会明显下降;超过 1500 行,基本只能做表面检查。所以我在 skill 里设了一个阈值:文件行数小于 500 行,做完整审计;500 到 1500 行,做重点函数审计;超过 1500 行,标记为partial,只审入口和导出函数。
这个阈值不是拍脑袋定的。我做过一组对比测试,同一个 1200 行的文件,完整审计平均能发现 4.2 个问题,重点函数审计能发现 3.1 个,表面检查只能发现 1.3 个。而完整审计消耗的 token 是重点函数审计的 2.8 倍。从性价比看,500 到 1500 行这个区间用重点函数审计是最划算的。当然,如果你的 Agent 上下文窗口特别大,可以适当放宽,但我不建议超过 2000 行。
4.4 审计现场记录:一次真实执行的片段
我拿一个实际的小项目跑过一遍,这里还原一下关键片段。项目结构是src/api、src/utils、src/config三个目录,总共 34 个文件。第一阶段扫描后,coverage-ledger.json里三条记录都是pending。第二阶段先审src/api,12 个文件,发现 3 个问题,其中一个是“错误信息直接返回给客户端,可能泄露内部路径”,严重程度标为 medium。审src/utils时,发现crypto.js有 1800 行,超过阈值,标记为partial,只审了导出的加密函数,发现一个“使用了不推荐的哈希算法”的问题。src/config只有 4 个文件,全部审完,没发现问题,状态标为audited。
最后findings.json里有 4 条记录,coverage-ledger.json里audited两条、partial一条。整个流程跑下来大概 15 分钟,比我手动审快得多,而且 ledger 让我很清楚哪些地方还没看。这个项目后续如果要继续审,直接从partial的crypto.js接着往下就行。
5. 常见问题与排查技巧实录
5.1 findings.json 写入失败或格式错乱
这是最常见的问题,表现是 Agent 报告“已写入”,但打开文件发现 JSON 不合法。原因通常是 Agent 在拼接 JSON 时混入了自然语言,比如在字符串里加了未转义的引号。排查方法是先用jq校验一遍:
jq empty findings.json如果报错,就定位到具体行。预防手段是在 skill 里明确要求 Agent 用结构化输出,而不是自由文本。我还会在模板里放一个最小的合法示例,让 Agent 照着填,比纯文字描述有效得多。
5.2 coverage-ledger.json 与实际审计范围不符
有时候 ledger 里标了audited,但实际上只审了目录下的部分文件。这种“虚标”很危险,会让人误以为覆盖完整。排查方法是把 ledger 里的files_checked和实际文件数做对比。我一般会在 skill 里加一条规则:audited状态必须满足files_checked等于该目录下的文件总数,否则只能标partial。这条规则看起来严格,但能避免很多误判。
5.3 Agent 执行中途被终止
热词里“agent execution terminated due to error”是高频问题。在安全审计场景下,最常见的原因是上下文超限或单次执行时间过长。应对策略是分阶段落盘,每完成一个小阶段就写一次文件,这样即使 Agent 被终止,已完成的审计结果也不会丢。另外,把大目录拆成多个小批次,每批控制在 5 到 8 个文件,也能显著降低被终止的概率。
5.4 审计结果重复或矛盾
分阶段审计时,同一个问题可能被多次发现,或者不同阶段对同一段代码给出矛盾结论。我的处理方式是:在findings.json里用location.file加location.line作为去重键,合并时保留置信度最高的那条。如果结论矛盾,比如一个阶段说是问题,另一个阶段说不是,就在description里注明“存在争议,建议人工复核”,而不是强行选一个。
| 问题现象 | 可能原因 | 排查方法 | 预防措施 |
|---|---|---|---|
| JSON 不合法 | 混入自然语言 | jq empty校验 | 用结构化输出模板 |
| 覆盖范围虚标 | 状态判定过宽 | 对比文件数 | 严格定义 audited 条件 |
| 执行被终止 | 上下文超限 | 查看执行日志 | 分阶段落盘、拆小批次 |
| 结果重复矛盾 | 分阶段未去重 | 按位置去重 | 合并时保留高置信度 |
5.5 独家避坑技巧
说几个文档里不会写、但实际很管用的技巧。第一,findings.json里的id用“日期加序号”的格式,比如20250115-001,这样即使跨天跑,id 也不会冲突。第二,coverage-ledger.json每次更新前先备份一份,因为 Agent 有时会覆盖写而不是追加写,备份能救命。第三,如果项目里有大量相似文件,比如一堆结构一样的 controller,可以只审一个作为样本,其余在 ledger 里标sampled,并注明“结构相同,抽样审计”。这样既省时间,又不会漏掉系统性问题。
6. 把 skill 用好的几个关键认知
6.1 skill 不是越全越好,而是越稳越好
我见过太多人做 skill 时追求“什么都能查”,结果每个能力都做得不深,Agent 调用时还容易出错。security-audit-skill的定位应该是“在明确边界内做可靠的审计”,而不是“替代所有安全工具”。它擅长的是逻辑层面的检查,比如输入校验、权限判断、错误处理,这些恰恰是传统扫描工具容易漏掉、而 Agent 又比较擅长的地方。把这一点想清楚,skill 的设计就不会跑偏。
6.2 两个 JSON 是给未来的自己留的后路
findings.json和coverage-ledger.json的价值,在第一次审计时可能感受不明显,但当你需要第二次、第三次审计同一个项目时,它们就是无价之宝。你能清楚看到上次审了什么、发现了什么、哪些还没审。这种连续性,是单次扫描工具给不了的。我现在做任何 Agent 审计类项目,都会先把这两个文件的结构定下来,再写具体的审计逻辑。
6.3 人工复核永远不能省
Agent 审计再强,也只是辅助。findings.json里的confidence字段就是为人工复核准备的。我的习惯是:high置信度的问题直接进修复队列,medium的抽检,low的全部人工过一遍。这样既享受了自动化的效率,又不会因为 Agent 的误判造成误伤。安全审计这件事,宁可慢一点,也不能把不确定的结论当成确定的事实。
6.4 后续可以这样扩展
如果你已经把基础流程跑通了,可以考虑几个扩展方向。一是把findings.json接到 issue 系统,自动创建修复任务。二是给coverage-ledger.json加时间戳,形成审计历史,这样能看出项目的安全状况是在变好还是变差。三是针对特定技术栈做专用子 skill,比如专门审数据库查询的、专门审文件操作的,让审计更有针对性。这些扩展都不需要改动核心结构,只是在现有基础上叠加,风险很低。
我个人在实际操作中的体会是,security-audit-skill这类项目的成败,往往不在技术难度,而在细节的严谨程度。两个 JSON 的字段设计、状态定义、落盘时机,这些看起来琐碎的地方,才是决定它能不能真正用起来的关键。把基础打牢,后面加什么功能都顺;基础不牢,加再多功能也是空中楼阁。