1. 从"security-audit-skill"这个名字说起:它到底在解决什么问题
第一次看到security-audit-skill这个命名,我的直觉是:这不是一个普通的脚本集合,而是一个面向 Agent 场景的安全审计技能包。关键词里同时出现了security-audit、skill、agent、findings、coverage-ledger,这几个词拼在一起,指向的场景非常明确——给 AI Agent 装上一套可复用的安全审计能力,并且用账本(ledger)的方式追踪审计覆盖率和发现项(findings)。
为什么我这么判断?因为单独一个security-audit可以是任何静态扫描工具,但加上skill和agent,就意味着它要遵循 Agent Skill 的规范来组织:有明确的触发条件、有输入输出契约、有可被 Agent 调用的执行入口。而coverage-ledger这个词是整条线索里最有信息量的——它说明这套技能不只是"扫一遍出报告",而是要记录哪些文件、哪些模块、哪些规则被审计过,哪些还没被覆盖,形成一个可追溯的账本。
这件事的价值在哪?我做过不少代码审计相关的项目,最大的痛点从来不是"扫不出来问题",而是扫完之后你不知道自己漏了什么。传统扫描器给你一份 findings 列表,但你无法回答"这个仓库里还有多少代码没被规则覆盖到"。Agent 场景下这个问题更严重:Agent 会自己决定看哪些文件、跳过哪些文件,如果没有一个 ledger 去约束和记录,审计结果就是不可信的黑盒。
所以security-audit-skill要解决的核心问题是三件事:
- 把安全审计能力封装成 Agent 可调用的 skill,让 Agent 在需要时能自动触发审计流程,而不是靠人手动跑脚本;
- 用 findings 结构化管理审计发现,每条发现都有明确的严重级别、位置、证据和修复建议;
- 用 coverage-ledger 追踪审计覆盖率,让"审了什么、没审什么"变成可量化、可审计的账本。
适合读这篇内容的人:正在做 Agent 开发、想把安全能力沉淀成 skill 的工程师;做代码审计平台、需要设计 findings 和覆盖率模型的技术负责人;以及任何对"Agent Skill 怎么写才靠谱"感兴趣的人。下面我会把这套东西从设计动机到落地细节完整拆一遍。
2. 为什么安全审计必须做成 Skill,而不是一个脚本
2.1 脚本思维和 Skill 思维的本质差异
大多数人做安全审计的第一反应是写个脚本:遍历目录、匹配规则、输出报告。这个思路在单人、单仓库、一次性审计的场景下没问题。但一旦放到 Agent 环境里,脚本思维会立刻暴露三个致命缺陷。
第一,脚本没有触发语义。Agent 不知道什么时候该调用它。你得在 prompt 里硬编码"遇到安全相关任务就运行 xxx.py",这种耦合极其脆弱。而 Skill 的核心特征之一就是自带触发描述——它用自然语言声明"我擅长做什么、什么情况下该用我",Agent 的编排层可以据此自动路由。
第二,脚本没有输入输出契约。脚本的输入是命令行参数,输出是 stdout 里的一堆文本。Agent 拿到这堆文本后还得再解析一遍,解析逻辑散落在各处。Skill 则要求定义清晰的 schema:输入是什么结构、输出是什么结构、错误怎么表达。这样 Agent 才能可靠地把审计结果传给下游(比如生成修复 PR、写入工单系统)。
第三,脚本无法被组合。一个安全审计往往需要多个能力协作:依赖漏洞检查、密钥泄露扫描、注入点识别、权限配置审查。如果每个都是独立脚本,Agent 要自己编排调用顺序和结果合并。而 Skill 体系下,这些可以设计成一组可组合的 skill,由 Agent 按需串联。
我个人的经验是:当你发现自己在 prompt 里反复描述"什么时候该做什么",那就是该把它抽成 Skill 的信号。安全审计恰好是这种高频、有明确触发条件、需要结构化输出的典型场景。
2.2 Skill 的边界设计:审计什么,不审计什么
设计security-audit-skill时,最容易犯的错误是贪大求全,想让它什么都能审。我的建议是明确划定边界,因为 Skill 的可靠性来自"能力可预期"。
一个务实的边界划分是这样的:
| 审计维度 | 是否纳入本 Skill | 理由 |
|---|---|---|
| 硬编码密钥/凭证 | 纳入 | 规则明确,误报可控,Agent 易判断 |
| 危险函数调用(如命令拼接) | 纳入 | 模式清晰,可结合上下文判断 |
| 依赖库已知漏洞 | 纳入(需外部数据源) | 需要 CVE 数据,但接口可标准化 |
| 权限与访问控制配置 | 纳入 | 配置文件结构化,易解析 |
| 业务逻辑漏洞 | 不纳入 | 需要深度业务理解,误报率极高 |
| 运行时动态行为 | 不纳入 | 超出静态审计范畴,交给专门的动态工具 |
这个边界背后的逻辑是:Skill 应该只做那些"规则可表达、证据可呈现、结论可复现"的审计。业务逻辑漏洞这类需要人类直觉的东西,硬塞进 Skill 只会让 Agent 输出一堆似是而非的结论,反而降低可信度。
提示:边界不是一成不变的。当某类审计的规则积累到足够成熟、误报率降到可接受范围时,再把它从"人工"升级为"Skill 覆盖"。这是一个渐进过程,不要一步到位。
2.3 触发条件怎么写才不会误触发
Skill 的触发描述是它和 Agent 之间的"接口文档"。写得太宽,Agent 会在无关场景乱调用;写得太窄,该用的时候用不上。
我的写法是三段式:能力声明 + 触发场景 + 反例排除。举个例子:
能力:对代码仓库执行静态安全审计,识别硬编码凭证、危险调用、依赖漏洞、权限配置问题。 触发场景:用户要求安全审查、代码审计、漏洞排查;提交前检查;CI 流程中的安全门禁。 不适用:运行时渗透测试、业务逻辑漏洞分析、性能问题排查。那个"不适用"段落特别关键。我踩过的坑就是:早期只写了能力声明,结果 Agent 在用户问"这段代码性能怎么样"时也触发了安全审计,输出一堆无关的 findings,体验很差。加上反例排除后,误触发率明显下降。
3. findings 的数据结构:审计结果怎么组织才可用
3.1 一条 finding 最少要包含哪些字段
findings 是审计的产出物,它的结构直接决定了后续能不能自动化处理。我见过太多审计工具输出的 findings 就是一段文本,导致下游想做个统计都费劲。一条合格的 finding 至少要有这些字段:
- id:唯一标识,便于追踪和去重
- rule_id:命中的规则编号,便于归类统计
- severity:严重级别(critical/high/medium/low/info)
- location:文件路径 + 行号 + 列号,精确定位
- evidence:触发问题的代码片段或配置内容
- message:人类可读的问题描述
- remediation:修复建议
- confidence:置信度,区分"确定是问题"和"疑似问题"
其中confidence 字段是我强烈建议加的。静态审计必然有误报,如果所有 finding 都标成"确定",用户很快就不信任这个工具了。把置信度显式暴露出来,让用户自己决定哪些要优先处理,反而更专业。
3.2 severity 分级不能拍脑袋
severity 分级最忌讳"凭感觉"。我建议用影响面 × 可利用性两个维度来定级:
| 低可利用性 | 中可利用性 | 高可利用性 | |
|---|---|---|---|
| 影响面大 | medium | high | critical |
| 影响面中 | low | medium | high |
| 影响面小 | info | low | medium |
比如硬编码的生产环境数据库密码:影响面大(能直接访问核心数据)、可利用性高(拿到代码就能用),定 critical 没争议。而一个测试文件里的假密钥:影响面小、可利用性低,定 info 或 low 就够了。
这个矩阵的好处是可解释。当用户质疑"为什么这个是 high 不是 critical"时,你能拿出两个维度的判断依据,而不是"我觉得"。
3.3 findings 的去重与聚合
同一个问题在多个文件里重复出现,是审计输出的常见噪音。比如某个危险函数在 20 个文件里都被调用,如果输出 20 条 finding,报告就没法看了。
我的处理策略是两级聚合:
- 同规则同文件:合并为一条,evidence 里列出所有行号
- 同规则跨文件:保留为多条,但在报告层面按 rule_id 聚合展示,给出"该规则命中 N 处"的汇总
这样既保留了精确定位能力,又让报告有整体视角。实现上,finding 的 id 可以用rule_id + file_path的哈希生成,天然支持去重。
注意:去重不能简单按 message 文本合并。不同位置的同类问题,修复方式可能不同(比如一个是删代码,一个是改配置),合并会丢失修复上下文。按规则+文件聚合是更安全的粒度。
4. coverage-ledger:让"审了什么"变成可追溯的账本
4.1 为什么覆盖率比 findings 更重要
这是我在实际项目里最深的体会:用户对审计工具的信任,一半来自 findings 的质量,另一半来自覆盖率的透明。
想象两个场景。场景 A:工具报告"发现 3 个高危问题"。场景 B:工具报告"扫描了 1200 个文件中的 1180 个,跳过 20 个(原因:二进制文件、超大文件、排除目录),发现 3 个高危问题,覆盖的规则集共 45 条"。
场景 B 明显更让人放心,因为它回答了"你没扫的那 20 个文件里会不会有问题"。coverage-ledger 就是干这个的——它是一份审计过程的账本,记录每个文件、每条规则的执行状态。
4.2 ledger 的条目结构设计
coverage-ledger 的每一条记录,我建议包含:
- target:被审计的对象(文件路径 / 模块 / 配置项)
- status:状态(audited / skipped / failed / partial)
- rules_applied:对该目标实际应用的规则列表
- skip_reason:如果跳过,原因是什么
- timestamp:审计时间
- finding_count:该目标上产生的 finding 数量
这个结构让 ledger 同时具备两个用途:审计时作为进度追踪,审计后作为可信度证明。用户拿到报告时,可以一眼看到"哪些没审、为什么没审"。
4.3 覆盖率指标怎么算才有意义
覆盖率不能只算"文件数比例",那样会误导。我建议至少算三个维度:
- 文件覆盖率= 已审计文件数 / 应审计文件总数
- 规则覆盖率= 实际应用规则数 / 规则集总规则数
- 代码行覆盖率= 已审计代码行数 / 总代码行数
三个指标各有盲区。文件覆盖率高但规则覆盖率低,说明"看了但没细看";规则覆盖率高但代码行覆盖率低,说明"规则全但只扫了小部分代码"。三个一起看,才能判断审计的完整度。
我在一个中型项目上实测过:文件覆盖率 98%,看起来很好,但代码行覆盖率只有 72%——因为几个超大文件被跳过了,而它们恰好是核心逻辑所在。如果只看文件覆盖率,就会误判审计质量。
4.4 ledger 的持久化与增量审计
ledger 如果只在内存里,审计一结束就没了,价值大打折扣。我建议持久化成结构化文件(JSON 或 SQLite 都行),这样能支持增量审计:
- 上次审计后没变的文件,可以标记为"沿用上次结果",跳过重复扫描
- 变化的文件重新审计,ledger 更新对应条目
- 最终报告合并历史 ledger,给出"本次新增/修复/遗留"的对比
增量审计对大型仓库是刚需。全量扫描一次可能要几十分钟,而日常提交往往只改几个文件。有了 ledger,Agent 可以只审变更部分,效率提升一个数量级。
提示:增量审计要小心"规则集变化"的情况。如果规则更新了,即使文件没变,也应该重新审计,否则会漏掉新规则能发现的问题。ledger 里记录 rules_applied 就是为了处理这个——规则集变了,对应条目的 status 要重置为 pending。
5. 把 Skill 跑起来:从定义到 Agent 调用的完整链路
5.1 Skill 描述文件的关键字段
一个可被 Agent 识别的 Skill,通常需要一个描述文件(不同框架叫法不同,有的叫 manifest,有的叫 skill definition)。核心字段包括:
name: security-audit-skill version: 1.0.0 description: 对代码仓库执行静态安全审计,输出结构化 findings 和覆盖率账本 triggers: - 安全审查 - 代码审计 - 漏洞排查 inputs: - name: target_path type: string required: true - name: rule_set type: string default: default - name: incremental type: boolean default: true outputs: - name: findings type: array - name: coverage_ledger type: object这里每个字段都有讲究。triggers决定 Agent 何时路由到这个 skill;inputs的 default 值让 Agent 在信息不全时也能调用;outputs的 schema 让下游能可靠解析。
5.2 Agent 调用时的参数传递陷阱
Agent 调用 skill 时,参数往往来自自然语言解析,这里有几个坑:
路径歧义。用户说"审一下这个项目",Agent 需要推断 target_path。如果当前工作目录有多个子项目,就可能传错。我的做法是在 skill 里加一层校验:路径不存在或不是目录时,返回明确的错误提示,让 Agent 追问用户,而不是硬跑。
规则集选择。用户可能说"用严格模式审",但 skill 只认strict这个枚举值。这需要在 skill 描述里列出所有合法值,让 Agent 做映射。我一般会在 description 里写清楚"rule_set 可选值:default / strict / owasp-top10"。
增量标志。用户说"重新完整审一遍",Agent 要能把它映射成incremental: false。这个映射关系也要在描述里体现。
5.3 执行流程的分阶段设计
一个健壮的 security-audit-skill,执行流程应该分阶段,每阶段有明确的产出和失败处理:
- 准备阶段:解析 target_path,加载规则集,读取历史 ledger(如果增量)
- 枚举阶段:遍历目标,生成待审计清单,写入 ledger 的 pending 条目
- 审计阶段:逐目标应用规则,产生 findings,更新 ledger 状态
- 聚合阶段:去重、分级、计算覆盖率指标
- 输出阶段:生成 findings 列表和 ledger 报告
分阶段的好处是可中断、可恢复。审计大仓库时如果中途失败,ledger 里已经记录了进度,下次可以从断点继续,而不是从头再来。我在实际使用中,这个设计救过好几次——一次审计跑到 80% 因为某个文件编码问题崩了,靠 ledger 恢复后只重跑了失败的那部分。
5.4 错误处理:审计失败也要留痕
审计过程中失败是常态:文件读不了、规则解析出错、外部数据源超时。关键原则是失败也要写进 ledger,status 标为 failed,附上错误原因。
为什么这点重要?因为如果失败被静默吞掉,用户会以为"这个文件审过了没问题",而实际上根本没审。这是最危险的情况——假阴性比假阳性危害大得多。把失败显式记录,用户至少知道"这里没审成,需要人工介入"。
6. 实测中踩过的坑和几条硬核经验
6.1 规则误报的抑制策略
静态审计的误报是绕不过去的。我试过几种抑制策略,效果最好的是上下文白名单:对某些规则,允许在特定上下文里豁免。比如"硬编码密钥"规则,如果代码在test/目录下、或者变量名包含example、dummy、sample,就降级为 info 而不是 high。
另一个有效手段是注释指令。允许开发者在代码里写# security-audit: ignore rule_id=xxx reason=...,显式声明豁免。这比全局关规则更精细,而且豁免理由被记录在案,审计时能看到。
但要注意:豁免不能无限制。我建议对豁免数量做监控,如果某个仓库的豁免指令异常多,可能说明规则本身有问题,需要回头调整规则而不是继续加豁免。
6.2 大文件处理的取舍
超大文件(比如几万行的生成代码)是审计的性能杀手。我的处理是设一个阈值(比如 5000 行),超过就跳过并在 ledger 里标记skipped: file_too_large。
但这里有个陷阱:核心业务逻辑有时就在大文件里。所以跳过不能一刀切,我加了个例外——如果文件在配置的"关键路径"列表里,即使超大也要审,只是改用流式处理,避免一次性加载爆内存。
6.3 覆盖率账本的"诚实"原则
最后分享一个我认为最重要的经验:ledger 必须诚实,不能为了好看而美化覆盖率。
我见过有的实现,把"跳过的文件"从分母里剔除,算出来的覆盖率永远是 99%。这是自欺欺人。正确的做法是:应审计总数包含所有文件,跳过的单独列出并说明原因。覆盖率低不可怕,可怕的是用户以为覆盖率高。
一个诚实的 ledger 长这样:
总文件数: 1200 已审计: 1150 (95.8%) 跳过: 45 (二进制 30, 超大文件 10, 排除目录 5) 失败: 5 (编码错误 3, 权限不足 2)用户看到这个,心里有数:95.8% 的覆盖率,但有 5 个文件失败了需要关注。这种透明度,才是审计工具建立信任的基础。
6.4 和 Agent 编排层的协作要点
security-audit-skill 不是孤立运行的,它要和 Agent 的编排层配合。几个协作要点:
- 幂等性:同一个 target 重复调用,结果应该一致(增量模式下第二次应该很快返回)。这让 Agent 可以放心重试。
- 超时控制:skill 要支持超时参数,避免 Agent 被一个卡住的审计拖死。超时后 ledger 要记录 partial 状态。
- 结果可序列化:findings 和 ledger 都要能序列化成 JSON,方便 Agent 在上下文里传递和后续处理。
我在实际集成时发现,幂等性是最容易被忽略但最重要的。Agent 有时会因为不确定而重复调用同一个 skill,如果 skill 不幂等,就会产生重复 findings 或者 ledger 冲突。加上幂等保证后,整个链路稳定多了。
这套security-audit-skill的设计思路,核心就一句话:把安全审计从"跑个脚本看结果"升级成"有契约、有账本、可追溯的 Agent 能力"。findings 保证发现的质量,coverage-ledger 保证过程的透明,两者结合,才让 Agent 的安全审计真正可信、可用、可复现。