ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent安全审计实战:security-audit-skill技能包设计与findings.json解析

Agent安全审计实战:security-audit-skill技能包设计与findings.json解析

1. 从零拆解 security-audit-skill:一个让 Agent 真正会做安全审计的技能包

第一次看到security-audit-skill这个标题,我脑子里蹦出来的不是“又一个安全扫描脚本”,而是一个更具体的问题:怎么让一个通用 Agent 在拿到一份代码仓库之后,像一名有经验的安全审计工程师那样,按部就班地跑完整个审计流程,最后还能交出一份结构化的、可追溯的结论?这个标题里的skill不是随便加的,它指向的是当下 Agent 生态里一个非常关键的抽象——技能包。而security-audit则限定了这个技能包的领域。两者组合起来,本质上是在回答一个问题:安全审计这件事,能不能被封装成一个 Agent 可以稳定调用、可复现、可评估的能力单元。

我接触过不少团队做 Agent 安全审计,最常见的失败模式不是模型不够聪明,而是流程没有骨架。Agent 东扫一下、西看一眼,最后给你一段“看起来很像那么回事”的自然语言总结,但你没法验证它到底覆盖了哪些文件、哪些规则、哪些风险点被漏掉了。security-audit-skill这类项目的价值,就在于它把“审计”从一次性的对话行为,变成了一个有输入、有中间产物、有最终交付物的工程化流程。从热搜词里出现的findings.json和coverage-ledger.json就能看出来,这个技能包的核心设计思路是:用结构化文件承载审计状态,而不是让结论只存在于模型的上下文里。

这篇文章适合几类人看。如果你正在做 Agent 开发,尤其是想让 Agent 承担代码审查、安全扫描这类高责任任务,那这个技能包的设计思路可以直接借鉴。如果你是一名安全工程师,想理解 Agent 时代的审计流程和传统 SAST 工具的区别,这里会有不少实操层面的对比。如果你只是刚入门 Agent 开发,想找一个有完整闭环的项目来学习技能包怎么写,那security-audit-skill也是一个很好的样本,因为它同时涉及工具调用、状态管理、结果聚合和覆盖率追踪这几个核心问题。

我下面会按照“设计思路 → 核心文件解析 → 实操流程 → 问题排查”的顺序来讲,中间会穿插我自己在类似项目里踩过的坑,以及一些参数和结构上的取舍逻辑。你不需要有很深的 Agent 框架经验,但最好对代码审计的基本概念有一点了解,这样读起来会更顺。

2. 为什么安全审计需要被封装成 Skill 而不是一个 Prompt

2.1 Prompt 式审计的三个致命缺陷

很多人做 Agent 安全审计的第一反应是写一个超长的 Prompt,把 OWASP Top 10、CWE 清单、审计步骤全塞进去,然后让模型对着代码仓库跑。我早期也这么干过,实测下来有三个问题几乎无法回避。

第一个问题是上下文漂移。当仓库文件数量超过几十个之后,模型在长上下文里会逐渐丢失早期的审计指令。你可能在 Prompt 开头写了“必须检查所有涉及用户输入的 SQL 拼接”,但跑到第二十个文件的时候,模型已经开始只做表面扫描了。这不是模型能力问题,而是注意力机制在长上下文下的固有衰减。

第二个问题是结论不可追溯。Prompt 式审计的输出通常是一段自然语言报告,你很难判断某一条结论对应的是哪个文件、哪一行、哪条规则。当开发团队反问“你说这里有注入风险,具体在哪”的时候,你只能让 Agent 重新跑一遍,而重新跑的结果可能又不一样。

第三个问题是覆盖率无法量化。一个仓库有 200 个文件,Agent 到底看了多少个?哪些目录被跳过了?哪些规则被应用了?Prompt 模式下这些问题基本没有答案。而安全审计恰恰是一个对覆盖率极度敏感的工作,漏掉一个文件可能就意味着漏掉一个高危漏洞。

security-audit-skill的设计正是针对这三点。它把审计流程拆成可枚举的步骤,每一步的中间结果都落到文件里,最后用coverage-ledger.json来记录覆盖情况。这就把“模型记不记得”变成了“文件里有没有”,可靠性完全不是一个量级。

2.2 Skill 与 Agent 的分工边界

热搜词里有个高频问题:skill 和 agent 的区别是什么。放到这个项目里,边界其实很清楚。Agent 负责决策和调度,比如“现在该扫描哪个目录”“这个发现要不要标记为高危”“下一步是继续扫描还是生成报告”。Skill 负责提供确定性的能力,比如“给定一个文件路径,返回其中的危险函数调用列表”“给定一组发现,生成符合 schema 的 findings.json”。

这个分工的好处是,Agent 的行为可以变化,但 Skill 的输出格式是稳定的。你可以换一个 Agent 框架,甚至换一个模型,只要 Skill 的接口不变,整个审计流程的骨架就不会散。这也是为什么security-audit-skill这类项目通常会定义一个明确的输入输出契约,而不是把逻辑全写在 Prompt 里。

我在实际项目里会把 Skill 分成两类:一类是采集型 Skill,负责从代码里提取事实,比如文件清单、依赖列表、危险模式匹配;另一类是聚合型 Skill,负责把事实整理成结论,比如去重、定级、生成报告。采集型 Skill 要求确定性极高,最好用脚本实现;聚合型 Skill 可以允许一定的模型判断,但输出必须符合固定 schema。

2.3 findings.json 与 coverage-ledger.json 的角色定位

这两个文件是整个技能包的骨架。findings.json承载的是“发现了什么”,每一条 finding 通常包含文件路径、行号、规则 ID、严重级别、描述和证据片段。coverage-ledger.json承载的是“检查了什么”,记录每个文件或每个目录的审计状态,比如已扫描、已跳过、扫描失败。

这两个文件分开设计是有讲究的。如果只有 findings,你无法判断一个没有 finding 的文件是真的安全还是根本没被检查。如果只有 coverage,你又拿不到具体的风险结论。两者结合,才能回答“在已覆盖的范围内,发现了哪些问题”这个完整问题。

我在自己的项目里会额外加一个字段叫scan_reason,记录某个文件为什么被跳过,比如“二进制文件”“测试夹具”“生成代码”。这个字段在后期复盘时非常有用,因为很多漏报其实不是规则问题,而是覆盖策略问题。

3. 核心文件结构与字段设计详解

3.1 findings.json 的 schema 设计要点

一个能用的findings.json不是随便塞几个字段就行。我见过太多项目把 finding 写成{"file": "a.py", "issue": "SQL injection"},这种结构在后续聚合和展示时几乎没法用。下面是我在实际项目中验证过的一套字段设计,你可以直接参考。

字段名类型是否必填说明
finding_idstring是全局唯一 ID,建议用规则ID+文件哈希+行号生成
rule_idstring是对应规则库中的规则标识,便于统计和去重
severityenum是critical / high / medium / low / info
categorystring是如 injection、auth、crypto、config
file_pathstring是相对于仓库根目录的路径
line_startint是起始行号
line_endint否结束行号,单行可省略
evidencestring是触发问题的代码片段,建议截取上下文
messagestring是人类可读的描述
confidenceenum是high / medium / low,表示判断置信度
remediationstring否修复建议

finding_id的生成方式很关键。如果每次扫描生成的 ID 都不一样,你就没法做跨次对比。我通常用rule_id + file_path + line_start的哈希作为 ID,这样同一个问题在代码没变的情况下 ID 稳定,方便追踪“这个问题是这次新引入的还是历史遗留的”。

confidence字段是很多人会忽略的。安全审计里有一类问题是“疑似但不确定”,比如一个变量经过了多层传递才进入危险函数,静态分析很难 100% 确认。这时候标confidence: medium比强行标 high 更诚实,也方便后续人工复核时优先看高置信度的发现。

3.2 coverage-ledger.json 的记录粒度

coverage-ledger.json的粒度选择是一个权衡。记到文件级别,信息最全但文件会很大;记到目录级别,文件小但定位精度不够。我的建议是文件级别记录,目录级别汇总。

具体结构可以是这样:顶层有一个summary对象,记录总文件数、已扫描数、跳过数、失败数;下面有一个entries数组,每个 entry 对应一个文件,包含file_path、status、reason、finding_count、scan_duration_ms。status的取值建议固定为scanned、skipped、failed、partial四种。

partial这个状态很有用。有些大文件可能因为超时只扫描了一部分,标成scanned会误导,标成failed又太绝对。partial配合一个scanned_lines字段,能准确反映实际情况。

注意:coverage-ledger 一定要在审计开始前初始化,而不是审计结束后补写。我踩过的坑是,Agent 中途崩溃后没有 ledger,完全不知道跑到哪了,只能从头再来。边扫边写 ledger,崩溃后可以从断点恢复。

3.3 两个文件之间的关联方式

findings.json和coverage-ledger.json通过file_path关联。一个文件在 ledger 里是scanned状态,但 findings 里没有对应记录,说明这个文件被检查过且没发现问题。一个文件在 ledger 里是skipped,那 findings 里就不应该有它的记录,如果有,说明流程有 bug。

我在项目里会加一个校验步骤:审计结束后,遍历 findings 里的每个file_path,检查它在 ledger 里是否存在且状态为scanned或partial。如果发现 findings 里的文件在 ledger 里是skipped,直接报流程错误。这个校验能抓出不少 Agent 调度层面的问题。

4. 实操流程:从仓库到审计报告的完整链路

4.1 环境准备与技能包加载

假设你用的是常见的 Agent 框架,技能包通常以一个目录的形式存在,里面有skill.yaml(或类似的清单文件)、scripts/目录放采集脚本、schemas/目录放 JSON schema、rules/目录放规则定义。加载技能包的过程就是把清单文件注册到 Agent 的工具列表里。

我建议在加载阶段做一次 schema 校验,确保findings.json和coverage-ledger.json的 schema 文件存在且合法。这一步看起来多余,但实际能避免很多“跑到一半发现输出格式不对”的问题。schema 校验用 Python 的jsonschema库几行代码就能搞定。

import json from jsonschema import validate with open("schemas/findings.schema.json") as f: findings_schema = json.load(f) with open("schemas/coverage.schema.json") as f: coverage_schema = json.load(f) def validate_findings(data): validate(instance=data, schema=findings_schema)

环境准备阶段还要确认一件事:Agent 有没有文件系统读取权限。安全审计需要遍历仓库,如果 Agent 只能通过受限的工具访问文件,那采集型 Skill 的设计就要相应调整,比如改成批量读取而不是逐个读取。

4.2 仓库预扫描与文件分类

正式审计之前,先做一次预扫描,把仓库里的文件分类。这一步的目的是确定哪些文件需要审计、哪些可以跳过。常见的分类维度包括:文件类型(源码、配置、文档、二进制)、是否生成代码、是否测试夹具、文件大小。

我通常会把文件分成四类:必审(业务源码、配置文件)、选审(测试代码、脚本)、跳过(二进制、依赖目录、生成代码)、待定(超大文件、无法识别的类型)。这个分类结果直接写入coverage-ledger.json的初始状态,status先标成skipped或pending。

分类规则可以用 glob 模式加文件大小阈值来实现。比如**/node_modules/**直接跳过,*.min.js直接跳过,超过 1MB 的源码文件标成partial候选。这些规则不复杂,但能显著减少无效扫描。

提示:跳过规则一定要记录reason。我见过一个项目把vendor/目录整个跳过了,结果那个目录里其实有被修改过的第三方库,藏了一个后门。跳过可以,但要知道自己跳过了什么。

4.3 逐文件审计与发现记录

这是整个流程的核心环节。对每个pending或scanned状态的文件,Agent 调用采集型 Skill 提取事实,然后根据规则库判断是否存在问题。每发现一个问题,就往findings.json里追加一条记录,同时更新 ledger 里对应文件的finding_count。

这里有个实操细节:不要等所有文件扫完再写 findings。每扫完一个文件就落盘一次,虽然 IO 次数多了,但崩溃恢复的成本低得多。我一般用追加模式写 findings,每条记录一行 JSON,这样即使中途崩溃,已写入的记录也不会丢。

规则匹配这块,我建议把规则定义和规则执行分开。规则定义放在rules/目录下的 YAML 文件里,每条规则包含rule_id、pattern、severity、category、message_template。规则执行用一个通用引擎,读取 YAML 然后对文件内容做匹配。这样加规则不用改代码,改 YAML 就行。

- rule_id: SQL_INJECTION_001 pattern: "execute\\(.*\\+.*\\)" severity: high category: injection message_template: "检测到字符串拼接形式的 SQL 执行,可能存在注入风险" languages: [python]

4.4 覆盖率核算与报告生成

所有文件处理完之后,进入聚合阶段。先核算覆盖率:scanned + partial的文件数除以必审 + 选审的文件数,得到一个百分比。这个数字要写进最终报告,因为它直接决定了审计结论的可信度。覆盖率 60% 的报告和覆盖率 95% 的报告,可信度完全不一样。

然后对 findings 做去重和排序。去重主要针对同一位置被多条规则命中的情况,保留 severity 最高的那条。排序按 severity 降序,同级别按文件路径排序。最后生成一份人类可读的报告,通常包含:审计概览(覆盖率、发现数量、严重级别分布)、高危发现详情、中低危发现列表、跳过文件说明。

报告生成这一步我建议也做成 Skill,而不是让 Agent 自由发挥。因为报告的格式一旦不稳定,后续做趋势对比就没法自动化。用模板引擎(比如 Jinja2)渲染报告,输入就是 findings 和 coverage 两个 JSON,输出是 Markdown 或 HTML。

5. 常见问题与排查技巧实录

5.1 审计结果不稳定,两次跑出来不一样

这是最常见的问题,根因通常有三个。一是 Agent 的调度有随机性,比如文件处理顺序不固定,导致某些依赖上下文的判断结果不同。解决办法是固定文件处理顺序,按路径排序后再逐个处理。二是规则匹配用了模糊匹配,比如正则写得太宽泛,不同上下文下命中结果不同。解决办法是把正则收紧,必要时加上语言和文件类型限制。三是模型判断环节的 temperature 设置过高,解决办法是把聚合型 Skill 的 temperature 调到 0 或接近 0。

我在项目里会做一个“稳定性测试”:同一个仓库连续跑三次,对比三次的 findings 集合。如果差异超过 5%,就说明流程里有不稳定因素,需要定位。这个测试在 CI 里跑,能提前发现很多问题。

5.2 覆盖率上不去,大量文件被跳过

先看 ledger 里的reason分布。如果大部分跳过原因是“文件过大”,那就要调整大小阈值,或者对大文件做分片扫描。如果原因是“无法识别的类型”,那就要补充文件类型映射表。如果原因是“二进制文件”,那要确认这些二进制文件是否真的不需要审计,有些配置文件虽然扩展名奇怪但其实是文本。

还有一种情况是 Agent 主动跳过了某些文件,因为它“觉得”这些文件不重要。这是 Prompt 设计问题,需要在技能包的指令里明确:跳过任何文件都必须记录 reason,且 reason 必须在预定义的枚举值里。不允许 Agent 自由发挥跳过理由。

5.3 findings 里出现重复记录

重复通常来自两个地方。一是同一段代码被多条规则命中,比如一个函数既匹配了“危险函数调用”又匹配了“未校验输入”。这种情况在聚合阶段按file_path + line_start去重,保留 severity 最高的。二是同一个文件被扫描了两次,这通常是 ledger 状态更新不及时导致的。解决办法是在扫描前检查 ledger 状态,已经是scanned的文件直接跳过。

我还会加一个dedup_key字段,用rule_id + file_path + line_start生成,聚合时按这个 key 去重。这样即使规则库有重叠,也不会产生重复 finding。

5.4 Agent 执行中途报错终止

热搜词里有个agent execution terminated due to error,这在安全审计场景里很常见,因为审计过程长、工具调用多,任何一步出错都可能导致整个流程挂掉。我的做法是把审计流程做成可恢复的:每个文件处理完就更新 ledger,Agent 重启后先读 ledger,找到第一个pending或partial的文件继续处理。

另外要给每个 Skill 调用加超时和重试。单个文件扫描超过 30 秒就标partial跳过,不要让一个文件卡死整个流程。重试次数建议 2 次,超过就记录失败原因继续下一个。

问题现象可能原因排查方向解决手段
两次结果不一致调度随机、正则过宽、temperature 高固定顺序、收紧正则、降 temperature加稳定性测试
覆盖率低跳过规则过严、类型识别缺失看 ledger 的 reason 分布调整阈值、补类型映射
findings 重复多规则命中、重复扫描检查 dedup_key聚合去重、状态前置检查
中途终止单文件超时、工具报错看 ledger 最后状态断点恢复、超时重试

5.5 独家避坑:规则库的版本管理

这一点很少有人提,但非常重要。规则库是会演进的,你今天加了 10 条规则,明天可能改 5 条。如果不做版本管理,你就没法解释“为什么上周没报的问题这周报了”。我的做法是给规则库打版本号,每次 findings 里记录ruleset_version。这样对比两次审计结果时,能区分是代码变了还是规则变了。

规则库的版本号建议用语义化版本,主版本号在规则有破坏性变更时递增,次版本号在新增规则时递增,修订号在修改规则描述时递增。这个习惯一旦养成,后期做审计趋势分析会轻松很多。

6. 把 security-audit-skill 用起来的几个实战建议

如果你打算在自己的项目里落地这套东西,我有几个具体建议。第一,先从一个小仓库开始,不要一上来就扫几万行代码的 monorepo。小仓库能让你快速跑通全流程,验证 findings 和 coverage 两个文件的结构是否合理。第二,规则库不要贪多,先写 10 到 20 条高置信度的规则,确保误报率低。误报率高的规则库会让整个审计结果失去信任。第三,把 coverage-ledger 的初始化做成独立步骤,不要混在审计流程里。这样即使审计流程改了,ledger 的结构也不会受影响。

关于 Agent 的选择,我的经验是采集型 Skill 用脚本实现,聚合型 Skill 用模型实现,两者通过文件系统解耦。这样你可以随时替换模型,而不影响采集逻辑。热搜词里提到的各种 Agent 框架,本质上都是调度器,真正决定审计质量的是 Skill 的设计和规则库的质量。

最后分享一个我在实际使用中的小技巧:在 findings 里加一个first_seen和last_seen字段,记录这个问题第一次和最近一次被发现的时间。这样你可以快速识别出“长期未修复”的问题,在报告里单独列出来。这个字段用审计时间戳填充,跨次审计时保留历史值。对于安全团队来说,一个存在了三个月还没修的高危漏洞,比一个新发现的高危漏洞更值得关注。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进