1. 从"security-audit-skill"这个名字说起:它到底要解决什么问题
第一次看到security-audit-skill这个命名,我的直觉是:这不是一个普通的脚本工具,而是一个被封装成skill形态的安全审计能力单元。为什么这么说?因为命名里同时出现了security-audit(安全审计)和skill(技能)两个词,前者定义了它"做什么",后者定义了它"以什么形态存在"。
在 agent 生态里,skill 和 agent 是两个经常被混淆的概念。我见过太多人把两者混为一谈,结果在架构设计上走了大弯路。简单说,agent 是执行主体,skill 是能力封装。agent 负责决策、编排、调用工具、维护记忆;skill 则是一段被标准化封装、可被 agent 按需加载和调用的能力模块。你可以把 agent 理解成一个项目经理,skill 理解成项目组里各个专业工种的作业手册——项目经理决定"现在该干什么",作业手册决定"这件事具体怎么干"。
security-audit-skill要解决的,正是把"安全审计"这件事从零散的脚本、临时的命令、口口相传的经验,沉淀成一个可复用、可编排、可被 agent 自动调用的标准化技能。它要回答的核心问题是:当 agent 需要对一个代码仓库、一个依赖清单、一份配置文件做安全审计时,它应该按什么流程走、检查哪些维度、如何记录发现、如何评估覆盖度。
这个 skill 的适用人群其实比想象中广。如果你在做 agent 开发,需要给 agent 挂载一个安全审计能力,那它是直接可用的模块;如果你是安全工程师,想把自己的审计经验固化成可复用的流程,那它是很好的参考范式;如果你只是好奇 skill 这种形态到底怎么落地,那它也是一个足够具体的案例。
关键词里出现的findings和coverage-ledger两个词,其实已经暗示了这个 skill 的核心数据结构:findings 是审计发现,coverage-ledger 是覆盖度账本。这两个概念是理解整个 skill 设计的钥匙,后面我会专门展开讲。
2. 为什么安全审计必须做成 skill,而不是写成一个脚本
2.1 脚本式审计的三个致命短板
我早期做安全审计,习惯写一个audit.sh或者audit.py,把所有检查逻辑塞进去,跑一遍输出一份报告。这种方式在单人、单项目、一次性场景下没问题,但一旦进入 agent 驱动的自动化流程,问题就暴露了。
第一个短板是不可编排。脚本是一个黑盒,agent 只能"调用它然后等结果",无法在审计过程中插入决策。比如 agent 发现某个依赖有高危漏洞,它可能想先查一下这个依赖是否真的被业务代码引用,再决定是否升级。脚本做不到这种"边审计边决策"。
第二个短板是不可组合。安全审计其实包含很多子能力:依赖漏洞扫描、密钥泄露检测、权限配置检查、注入风险识别、加密算法合规性检查。如果全塞在一个脚本里,任何一个子能力想单独复用都做不到。而 skill 的封装粒度天然支持组合——一个 skill 可以调用另一个 skill。
第三个短板是不可追溯。脚本跑完只给一个结果,中间检查了哪些项、跳过了哪些项、为什么跳过,全都没有记录。这在合规审计场景下是致命的。coverage-ledger这个概念就是为了解决这个问题而生的。
2.2 skill 形态带来的三个结构性优势
把安全审计做成 skill,本质上是把"能力"从"实现"里解耦出来。我总结下来有三个结构性优势。
第一,接口标准化。skill 有统一的输入输出契约。agent 不需要知道这个 skill 内部是用正则匹配还是用 AST 分析,它只需要知道"我给它一个仓库路径和审计策略,它返回一组 findings 和一份 coverage-ledger"。这种契约化让 agent 的编排逻辑和 skill 的实现细节彻底分离。
第二,可被 agent 自主发现和调用。在成熟的 agent 框架里,skill 是可以被注册、被检索、被按需加载的。当 agent 接到"帮我审计一下这个项目的安全性"这个任务时,它能自动匹配到security-audit-skill,而不需要人工指定。这是 skill 生态相比脚本库最大的价值。
第三,可独立演进。安全审计的规则是不断更新的,新的漏洞类型、新的攻击手法、新的合规要求层出不穷。skill 形态允许你只更新这一个 skill,而不影响 agent 的其他能力。脚本式方案往往牵一发动全身。
2.3 一个具体的对比
我拿一个真实场景做对比。假设 agent 需要审计一个 Node.js 项目,检查项包括依赖漏洞、硬编码密钥、危险的正则表达式。
脚本式方案:写一个audit.js,里面依次跑npm audit、grep 密钥模式、扫描正则。跑完输出 JSON。agent 拿到 JSON 后只能整体接受或整体拒绝。
skill 式方案:security-audit-skill内部把三个检查拆成三个子能力,每个子能力独立产出 findings,最后汇总成 coverage-ledger。agent 可以在拿到依赖漏洞 findings 后,先调用另一个 skill 去查这些依赖的调用链,再决定是否标记为"需修复"。整个过程可追溯、可干预、可组合。
这个差异在简单场景下不明显,但在复杂项目里,skill 式方案的灵活度是脚本方案完全比不了的。
3. findings 的数据结构设计:审计发现怎么记才不丢信息
3.1 findings 不是简单的"问题列表"
很多人设计 findings 的时候,习惯做成一个数组,每个元素就是一条问题描述。这种设计在 demo 阶段够用,但一旦进入真实审计,信息丢失非常严重。我踩过的坑是:审计报告里写"发现硬编码密钥",但没记录这个密钥在哪个文件、哪一行、什么类型的密钥、置信度多高、是否已确认可利用。结果修复的人拿到报告一脸懵,还得重新定位。
一个合格的 finding 至少应该包含以下字段:
| 字段 | 说明 | 是否必填 |
|---|---|---|
| id | 唯一标识,便于追踪和去重 | 必填 |
| category | 分类,如 dependency、secret、injection、config | 必填 |
| severity | 严重级别,critical/high/medium/low/info | 必填 |
| location | 位置,文件路径 + 行号 + 列号 | 必填 |
| evidence | 证据,实际匹配到的内容片段 | 必填 |
| confidence | 置信度,0-1 之间,表示误报可能性 | 建议填 |
| remediation | 修复建议 | 建议填 |
| references | 参考链接,如 CVE 编号、规范文档 | 可选 |
| status | 状态,open/fixed/ignored/false-positive | 必填 |
3.2 severity 和 confidence 是两个独立维度
这是我在实际审计中体会最深的一点。严重级别和置信度必须分开。一个 critical 级别的发现,如果置信度只有 0.3,那它可能是个误报;一个 low 级别的发现,如果置信度 0.95,那它确实存在但影响有限。
我见过太多审计工具把这两个维度混在一起,导致 agent 拿到结果后无法做合理的优先级排序。正确的做法是:agent 根据 severity 和 confidence 的组合来决定处理策略。比如 critical + high confidence 立即阻断,critical + low confidence 先人工确认,low + high confidence 记录待办。
3.3 findings 的去重与聚合
真实审计里,同一个问题可能在多个位置出现。比如一个硬编码密钥在三个文件里被复制粘贴了。如果不去重,findings 列表会膨胀得很快,agent 处理起来效率极低。
我的做法是引入一个fingerprint字段,用 category + 规范化后的 evidence 做哈希。相同 fingerprint 的 findings 聚合为一条,location 变成一个数组。这样既保留了所有位置信息,又避免了重复处理。
import hashlib def compute_fingerprint(category, evidence): normalized = evidence.strip().lower() raw = f"{category}:{normalized}" return hashlib.sha256(raw.encode()).hexdigest()[:16]这个 fingerprint 还有一个好处:跨次审计时可以对比。如果上次审计的某个 finding 这次还在,说明没修复;如果消失了,说明已修复。这让审计从"一次性快照"变成"持续追踪"。
4. coverage-ledger:为什么"没发现问题的检查项"同样重要
4.1 审计报告最大的谎言:只报告发现的问题
传统审计报告只告诉你"发现了什么",不告诉你"检查了什么"。这导致一个严重问题:你无法判断这份报告的可信度。一个报告说"未发现高危漏洞",可能是因为真的没有,也可能是因为根本没检查高危漏洞相关的项。
coverage-ledger就是为了解决这个信任问题。它记录的是审计覆盖度:这次审计检查了哪些维度、每个维度检查了多少项、哪些项被跳过、为什么跳过。
4.2 coverage-ledger 的结构
我设计的 coverage-ledger 是一个二维结构:维度 × 检查项状态。
| 维度 | 总检查项 | 已检查 | 跳过 | 跳过原因 |
|---|---|---|---|---|
| 依赖漏洞 | 120 | 118 | 2 | 私有仓库无法访问 |
| 密钥泄露 | 45 | 45 | 0 | - |
| 注入风险 | 30 | 28 | 2 | 动态拼接无法静态分析 |
| 权限配置 | 15 | 15 | 0 | - |
| 加密合规 | 20 | 12 | 8 | 项目未使用加密相关代码 |
这张表的价值在于:它让审计的边界变得透明。agent 拿到这份 ledger 后,可以判断"这次审计是否足够全面",从而决定是否需要补充其他 skill 来覆盖跳过的部分。
4.3 跳过原因必须结构化
我一开始把跳过原因写成自由文本,结果发现 agent 无法理解。后来改成枚举值:
not-applicable:项目不涉及该检查项access-denied:无权限访问unsupported:当前 skill 不支持该检查timeout:检查超时manual-required:需要人工介入
结构化之后,agent 可以针对不同原因采取不同策略。比如unsupported的项,agent 可以去调用其他 skill 来补;access-denied的项,agent 可以提示用户提供权限。
4.4 coverage-ledger 和 findings 的联动
这两个数据结构不是孤立的。每条 finding 都应该能追溯到 coverage-ledger 里的某个检查项。反过来,coverage-ledger 里标记为"已检查但无发现"的项,也应该能被验证。
我的做法是给每个检查项一个check_id,finding 里记录它来自哪个check_id。这样审计报告就能回答:"依赖漏洞维度检查了 118 项,其中 12 项产生了 findings,106 项确认无问题。"这种颗粒度的报告,可信度远高于"发现 12 个漏洞"。
5. 把 security-audit-skill 接入 agent 的完整流程
5.1 skill 的输入契约设计
skill 要被 agent 调用,输入契约必须清晰。security-audit-skill的输入我设计成三部分:
{ "target": { "type": "repository", "path": "/path/to/repo", "language": ["javascript", "python"] }, "policy": { "severity_threshold": "medium", "categories": ["dependency", "secret", "injection"], "max_findings": 500 }, "context": { "previous_ledger": null, "exclude_paths": ["node_modules", "dist", "*.test.js"] } }target定义审计对象,policy定义审计策略,context提供上下文。这种分层设计让 skill 既能被简单调用(只传 target),也能被精细控制(传完整 policy)。
5.2 skill 的输出契约
输出同样分三部分:findings、coverage-ledger、metadata。
{ "findings": [...], "coverage_ledger": {...}, "metadata": { "skill_version": "1.2.0", "started_at": "2025-01-15T10:00:00Z", "duration_ms": 45230, "target_hash": "abc123..." } }metadata里的target_hash很关键。它是对审计对象内容的哈希,用于判断两次审计之间目标是否变化。如果 hash 没变,agent 可以直接复用上次结果,避免重复审计。
5.3 agent 侧的编排逻辑
agent 调用这个 skill 的典型流程是这样的:
- 任务解析:agent 收到"审计项目安全性"的任务,识别出需要 security-audit 能力。
- skill 匹配:从已注册的 skill 列表里找到
security-audit-skill。 - 参数构造:根据任务上下文构造输入参数,比如从项目配置里读取语言类型。
- 调用执行:调用 skill,等待返回。
- 结果处理:拿到 findings 和 coverage-ledger 后,根据 severity 和 confidence 决定后续动作。
- 补充审计:如果 coverage-ledger 显示有
unsupported的项,agent 尝试调用其他 skill 补充。 - 报告生成:汇总所有结果,生成最终报告。
这个流程里,第 5 步和第 6 步是 agent 价值的体现。纯脚本方案做不到这种"根据审计结果动态调整后续动作"。
5.4 一个容易忽略的细节:超时与中断
安全审计可能很慢,尤其是大仓库。skill 必须支持超时和中断。我的做法是给 skill 设置一个max_duration_ms,超时后返回已完成部分的 findings 和 ledger,ledger 里标记未完成的项为timeout。
这样 agent 拿到的是"部分结果 + 明确的未完成标记",而不是一个失败。agent 可以决定是重试、缩小范围、还是接受部分结果。这比直接抛异常友好得多。
6. 实测中踩过的坑与排查链路
6.1 坑一:findings 爆炸导致 agent 上下文溢出
第一次把 skill 接入 agent 时,我审计了一个中型项目,findings 返回了 3000 多条。agent 的上下文直接被撑爆,后续处理全部失败。
排查链路:先看 findings 分布,发现 80% 是info级别的低价值发现,比如"使用了 console.log"。这些发现对安全审计毫无意义,但被 skill 无差别地记录了。
修复方案:在 skill 里引入severity_threshold,低于阈值的发现不进入 findings,只在 coverage-ledger 里记录"已检查"。同时引入max_findings上限,超过后按 severity 排序截断,并在 metadata 里标记truncated: true。
这个坑的教训是:skill 的输出必须为下游消费者(agent)设计,而不是为完整性设计。完整性由 coverage-ledger 保证,findings 只放真正需要处理的内容。
6.2 坑二:误报率高导致 agent 误判
密钥检测用正则匹配,结果把测试文件里的假密钥、文档里的示例密钥全匹配出来了。agent 拿到这些 findings 后,误以为项目有严重密钥泄露问题,触发了阻断流程。
排查链路:抽样检查误报的 findings,发现它们集中在test/、docs/、examples/目录。这些目录本来就不应该参与生产环境的安全审计。
修复方案:在 skill 里引入exclude_paths,默认排除测试、文档、示例目录。同时给每条 finding 加confidence字段,正则匹配的 confidence 设为 0.6,AST 分析的设为 0.9。agent 根据 confidence 决定是否需要人工确认。
这个坑的教训是:安全审计的误报比漏报更危险,因为误报会导致 agent 做出错误的阻断决策,影响正常流程。
6.3 坑三:coverage-ledger 与实际不符
有一次审计报告显示"依赖漏洞检查了 120 项,全部通过",但实际上项目有 200 个依赖。排查发现,skill 只检查了package.json里的直接依赖,没有检查package-lock.json里的传递依赖。
排查链路:对比 coverage-ledger 的"总检查项"和实际依赖数量,发现对不上。进一步检查 skill 的依赖解析逻辑,发现它只读了dependencies字段,没读devDependencies和 lock 文件。
修复方案:重写依赖解析逻辑,递归解析 lock 文件,把所有传递依赖都纳入检查范围。同时在 coverage-ledger 里区分"直接依赖"和"传递依赖"两个子维度,让覆盖度更透明。
这个坑的教训是:coverage-ledger 的"总检查项"必须来自权威数据源,不能靠 skill 自己估算。否则 ledger 本身就是不可信的。
6.4 坑四:skill 版本升级导致 agent 行为突变
skill 从 1.1 升级到 1.2 时,我调整了 severity 的判定规则,把某些原本是medium的发现改成了high。结果 agent 的阻断逻辑被频繁触发,整个流程卡死。
排查链路:对比两个版本的 findings 输出,发现 severity 分布变化很大。agent 的阻断逻辑是基于 severity 阈值的,阈值没变但 severity 变了,导致行为突变。
修复方案:引入 skill 版本兼容性策略。severity 判定规则的变更必须走大版本号(1.x → 2.x),并且 agent 侧要记录它依赖的 skill 版本。同时给 skill 加一个severity_schema_version字段,让 agent 能感知 severity 语义的变化。
这个坑的教训是:skill 是 agent 的依赖,skill 的语义变更必须被 agent 感知。不能悄悄改 severity 规则,否则 agent 的行为会不可预测。
7. 让 security-audit-skill 真正好用的几个设计取舍
7.1 宁可少报,不可误报
这是我在安全审计领域最坚定的一个取舍。误报会让 agent 做出错误决策,而漏报只是让某些问题没被发现——后者可以通过增加检查维度来弥补,前者会直接破坏 agent 的信任。
具体做法:所有基于模式匹配的检查,confidence 默认不超过 0.7;只有经过 AST 分析或数据流分析确认的,才给高 confidence。agent 侧根据 confidence 分级处理,高 confidence 自动处理,低 confidence 转人工。
7.2 coverage-ledger 优先于 findings
如果只能保留一个数据结构,我会保留 coverage-ledger。因为 findings 是"这次发现了什么",coverage-ledger 是"这次审计的可信度"。前者是点,后者是面。
一个只有 findings 没有 ledger 的审计报告,你无法判断它是否可信。一个有 ledger 的审计报告,即使 findings 为空,你也能知道"它确实认真检查了"。
7.3 skill 要能被"部分执行"
真实场景里,agent 可能只需要审计某个特定维度,比如只查密钥泄露。skill 必须支持按维度执行,而不是只能全量跑。
我的做法是在 policy 里支持categories过滤,只跑指定的维度。同时 coverage-ledger 里明确标记哪些维度被跳过、为什么跳过。这样 agent 可以组合多个 skill 调用来覆盖所有维度,而不是被一个 skill 绑死。
7.4 输出要能被机器和人都读懂
findings 和 coverage-ledger 都是结构化数据,机器读没问题。但最终报告是要给人看的。我的做法是 skill 同时输出两种格式:JSON 给 agent,Markdown 给人。
Markdown 报告里,findings 按 severity 分组,coverage-ledger 用表格呈现。这样安全工程师拿到报告后,不需要额外工具就能快速理解审计结果。
8. 从 security-audit-skill 延伸出去的几个方向
8.1 多 skill 协同审计
单个 skill 的能力边界是有限的。真实的安全审计需要多个 skill 协同:security-audit-skill负责静态检查,dependency-scan-skill负责依赖漏洞,runtime-audit-skill负责运行时行为。agent 的编排能力在这里体现得淋漓尽致。
我现在的做法是让 agent 维护一个"审计计划",根据项目特征动态选择需要调用的 skill 组合。比如一个纯前端项目,可能不需要运行时审计;一个后端服务,运行时审计就是必须的。
8.2 审计结果的持续追踪
单次审计是快照,持续审计是趋势。我通过target_hash和 finding 的fingerprint实现了跨次审计的对比。agent 可以回答"这个漏洞是这次新出现的,还是上次就有的"、"上次标记为已修复的,这次是否真的修复了"。
这种持续追踪能力,让安全审计从"事后检查"变成"持续保障"。
8.3 skill 的自我评估
skill 自己也需要被评估。我引入了一组指标来衡量 skill 的质量:误报率、漏报率、执行时间、覆盖度。这些指标定期统计,用于指导 skill 的迭代。
误报率的计算方式是:人工抽查 findings,统计其中被标记为false-positive的比例。漏报率则通过对比 skill 结果和人工审计结果来估算。这两个指标是 skill 迭代的核心依据。
8.4 面向不同技术栈的 skill 变体
security-audit-skill目前主要面向 JavaScript 和 Python。但不同技术栈的审计重点差异很大。Java 项目要重点查反序列化,Go 项目要重点查并发安全,Rust 项目要重点查 unsafe 块。
我的规划是把通用逻辑抽出来做成基础 skill,各技术栈的特定检查做成插件式扩展。这样既能复用核心能力,又能针对不同技术栈做深度优化。
9. 我在实际使用中总结的几条经验
第一条经验:skill 的输入输出契约要尽早冻结。我早期频繁改契约,导致 agent 侧的适配代码反复重写。后来学乖了,先把契约定死,内部实现随便改,契约不动。
第二条经验:coverage-ledger 的粒度要适中。太粗了看不出问题,太细了维护成本高。我的经验是按"检查维度 + 检查项类型"两级就够了,不需要细到每个具体检查。
第三条经验:findings 的 severity 判定要有明确的规则文档。不能靠感觉。我把每条规则都写进文档,包括什么情况算 critical、什么情况算 high。这样 skill 迭代时不会因为个人判断变化导致 severity 漂移。
第四条经验:skill 要能被单独测试。不要依赖 agent 才能跑。我给 skill 写了一套独立的测试用例,输入固定的 target,断言输出的 findings 和 ledger。这样 skill 迭代时能快速回归。
第五条经验:agent 和 skill 的边界要清晰。agent 负责决策,skill 负责执行。我见过有人把决策逻辑塞进 skill,结果 skill 变得又大又难维护。记住:skill 只回答"是什么",不回答"怎么办"。
最后分享一个我最近在用的技巧:给 skill 加一个dry-run模式。在这个模式下,skill 只输出 coverage-ledger,不输出 findings。这样 agent 可以先跑一遍 dry-run,看看覆盖度是否满足要求,再决定是否跑完整审计。这个技巧在大仓库审计时特别有用,能省下大量时间。