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 是一个二维结构维度 × 检查项状态。维度总检查项已检查跳过跳过原因依赖漏洞1201182私有仓库无法访问密钥泄露45450-注入风险30282动态拼接无法静态分析权限配置15150-加密合规20128项目未使用加密相关代码这张表的价值在于它让审计的边界变得透明。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_idfinding 里记录它来自哪个check_id。这样审计报告就能回答依赖漏洞维度检查了 118 项其中 12 项产生了 findings106 项确认无问题。这种颗粒度的报告可信度远高于发现 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 和 ledgerledger 里标记未完成的项为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.6AST 分析的设为 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 给 agentMarkdown 给人。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看看覆盖度是否满足要求再决定是否跑完整审计。这个技巧在大仓库审计时特别有用能省下大量时间。