去年年底我们组接手了一个老项目的安全整改,代码量不大,但是历史包袱极重。我当时想偷个懒:让 AI 助手帮忙做一次安全审计,把常见漏洞先筛一遍。结果用下来发现,普通对话模式根本扛不住这活儿——它要么漏掉关键检查项,要么给出"建议使用参数化查询"这种正确但毫无用处的废话。后来我把需求收敛成了一个可复用的能力包,也就是这篇文章要聊的security-audit-skill。简单说,它是一个面向 AI 编程助手的专用 Skill,把安全审计的检查范围、规则分级、工具调用和报告格式全部固化下来,让 AI 能在一次执行里完成从扫描到输出结构化审计报告的全流程。这篇文章我会从设计思路、工程实现、实测调优到踩坑记录都摊开讲,适合正在做 DevSecOps 建设、或者想让 AI 辅助代码安全审查的团队参考。
1. 安全审计为什么需要可复用的 Skill,而不是临时开个对话
1.1 通用对话做审计的三个致命短板
我一开始的做法很简单,把待审计的代码目录拖给 AI,说"帮我看看这个项目有什么安全漏洞"。表面上看模型确实能识别出一些问题,但往深了用就会发现三个短板非常致命。
第一是经验不连续。每次审计都是全新对话,模型不记得上一轮项目里哪些模块已经看过了、哪些规则已经排除过了。同一个项目分三次聊,三次结果都不一样,甚至同一段代码在不同时间审查,给出的风险等级都能差出两级。这对需要留痕、需要对比整改前后状态的安全审计来说,几乎是不可接受的。
第二是检查项漏得厉害。通用对话模式下,模型对"审计"的理解完全靠训练语料里的平均印象。你让它查 SQL 注入,它可能盯着字符串拼接看半天,却完全忽略了这个项目里真正危险的点是child_process.exec命令拼接和对象属性层面的原型污染。没有一套固定的检查清单,漏检就是必然的。
第三是输出格式完全没法对接流程。模型以自然语言回答时,我需要人工把漏洞描述、文件路径、修复建议一条条摘出来,再填进工单系统。审计一次生成几千字分析,真正能用的字段却要二次加工。这个成本比我自己人肉看代码还高。
1.2 Skill 和 Prompt、Agent 的区别,这次彻底说清
在开始动手之前,我先理清了一个概念层面的问题:我要做的东西到底是 Skill、Prompt 还是 Agent?这个区分不是学术抠字眼,它直接决定了后续的实现方式。
我用一个类比来讲。Prompt 是一张答题卡,你把问题写清楚,AI 照着答;Agent 是一个会主动打电话、查资料、跑腿的办事员,它有目标、有工具、有自主决策能力;而 Skill 是一本标准作业手册,里面写清楚了"遇到什么情况、按什么流程、用什么工具、产出什么格式"。
所以 Skill 和 Agent 不是竞争关系,而是配套关系:Skill 是 Agent 大脑里的"程序化肌肉记忆",一套可复用的标准作业流程;Agent 是那个决定"什么时候调用这套流程"的调度者。具体到我的场景,我需要的不是让 AI 自由发挥做审计,而是让它严格按照一套经过验证的审计流程执行,所以我锁定的是 Skill 而不是纯 Agent。
1.3 被一次路径遍历漏洞漏报逼出来的项目
真正让我决定自建 Skill 的是一次线上事故。那个老项目里有一个文件下载接口,用户传入文件名参数,后端直接拼接路径读取文件。我让 AI"检查一下这个接口的安全性",它回了一长串分析,结论是"没有发现明显问题"。
但我的审计经验告诉我这里一定有问题。我把代码贴到另一个模型里,换了个更直接的问法:"这个接口的参数有没有可能被用来读取任意文件?"结果秒出结论:路径遍历,高危,../../etc/passwd就能打穿。
同一个模型,同一个项目,就因为提问方式不同,结论从"没问题"变成"高危漏洞"。这件事让我意识到:审计质量不能依赖模型的临场发挥,必须把"怎么问、按什么顺序问、检查哪些点"固化下来。
2. 动手之前先做减法:审计范围与规则分级设计
2.1 范围四个象限:代码、依赖、配置、密钥
做 Skill 的第一个大坑,是试图覆盖所有安全审计场景。我最早列需求清单时野心很大:代码审计、依赖漏洞、云安全基线、容器镜像扫描、Kubernetes 配置检查……全都要。后来被现实教育了:范围越大,Skill 越臃肿,模型越容易在无关细节上浪费 token,真正关键的检查反而被稀释。
最终我把security-audit-skill收敛为四个象限,只做应用安全审计里最刚需的部分:
- 代码审计:注入类(SQL、命令、模板)、路径穿越、反序列化、硬编码密钥、危险函数调用。
- 依赖审计:检查第三方组件是否有已知 CVE,版本是否过旧。
- 配置审计:重点看认证鉴权配置(比如 Spring Security 的放行规则)、CORS 策略、敏感信息暴露面。
- 密钥泄露审计:API Key、数据库密码、私钥等是否被提交到仓库。
为什么收这么窄?很简单,这四个象限是我的团队在日常迭代中真正会反复踩到的坑,也是 AI 辅助审计性价比最高的区域。云安全基线、容器加固这些领域,专业工具已经做得非常成熟,不需要模型半吊子地瞎猜。
2.2 P0/P1/P2 规则分级
范围确定后,我开始为每个象限设计检查规则。这里我采用了一个安全团队常用的分级方式:按漏洞的利用难度和潜在危害分为 P0、P1、P2 三级。
| 级别 | 定义 | 典型场景 | Skill 里的处置策略 |
|---|---|---|---|
| P0 | 可直接远程利用,危害大 | 未授权接口、SQL 注入、硬编码数据库密码 | 立即阻断,详细报告 |
| P1 | 需要一定前置条件才能利用 | 存储型 XSS、越权访问、CORS 配置不当 | 优先修复,给出补丁建议 |
| P2 | 风险有限或需要交互 | 信息泄露、版本过旧、安全响应头缺失 | 记录在案,迭代修复 |
这个分级不只是标签,它直接影响 Skill 的执行逻辑。比如 P0 级发现会触发工具自动复扫同一文件确认,P1 级发现会要求模型补充攻击路径描述,P2 级则只做记录不展开分析。这样设计是为了控制整个审计过程的成本——安全审计的 token 开销非常容易失控,如果不分级,模型会对一个无伤大雅的注释里出现的疑似密码穷追不舍。
2.3 结构化报告格式设计
安全审计结果最终要落到报告,而报告最大的痛点是没法直接进工单系统。早期用自然语言报告时,每个字段都要人工二次提取,效率极低。所以我在 Skill 里强制规定了 JSON 结构化输出格式。
我的设计原则是:每个漏洞条目必须包含severity、category、file、line、description、remediation六个核心字段。其中remediation必须给出具体的修改建议,不能只写"建议使用参数化查询"这种正确的废话,而要写清楚"第 47 行userInput直接拼入 SQL 语句,建议改为使用PreparedStatement,参考示例:..."。
我实测过,强制 JSON 结构化输出之后,整个审计流程的落地效率至少翻了一倍。模型天然有"给 JSON 就按 JSON 想"的倾向,输出质量反而比自由发挥更稳定。
3. 从零搭建 security-audit-skill 的工程细节
3.1 SKILL.md 的定义文件怎么写
Skill 的载体是一份目录,核心入口是SKILL.md。这份文件相当于 Skill 的说明书,里面定义了能力边界、触发条件和执行流程。我见过很多人把SKILL.md写成一段简单的"你是安全审计专家",那是完全不够用的。
我的SKILL.md包含四个模块:能力声明、输入要求、执行流程、输出约束。其中执行流程是最关键的部分,我把审计拆成了五个固定步骤:
- 读取项目结构,识别语言栈和框架。
- 根据语言栈加载对应的规则集。
- 并行执行代码扫描、密钥检测、依赖检查三项任务。
- 汇总结果,按 P0/P1/P2 分级排序。
- 生成 JSON 报告,标注需要人工复核的条目。
这里我还要特别强调一个细节:SKILL.md里的指令必须用祈使句,直接命令模型"必须做什么",而不是"建议考虑什么"。模型的指令遵循度跟指令的肯定程度强相关,写得软弱,执行效果就差。
3.2 工具链的挂载与调用逻辑
Skill 不能只靠模型脑补,否则和普通 Prompt 没有本质区别。真正的 Skill 需要挂载工具,让模型在需要时可以调用外部程序获取事实性结果。
我在security-audit-skill里挂了四类工具:
gitleaks:扫描仓库密钥泄露,速度快,适合 CI 环境。semgrep:静态代码分析,支持自定义规则,误报率相对可控。npm audit/pip-audit:依赖漏洞检查,跟随语言栈切换。dependency-check:OWASP 出品的依赖检查工具,覆盖面比 npm audit 更广,但速度慢。
工具链的调用逻辑也是有讲究的。我要求在审计开始时先并行跑gitleaks和语言对应的依赖检查——这两个工具输出明确、耗时短。semgrep因为规则集大小和扫描范围的不同,耗时波动大,放到第二步再跑。这样设计的好处是:即使semgrep因为某些原因卡住,前面的结果也能先落盘,不至于整个审计流程被一个工具拖死。
3.3 跑通第一个完整审计流程
工具挂好之后,我跑了一个最小验证:用一个故意包含三个漏洞的测试项目试跑整个 Skill。
测试项目里埋了一个 SQL 注入点、一个硬编码密钥、一个过期的依赖。Skill 执行后的结果让我比较满意:gitleaks在 3 秒内报出了硬编码密钥的位置,精确到行号;npm audit定位到了那个过期依赖并给出了升级版本和对应 CVE 编号;semgrep的 SQL 注入检测规则在第一条就命中,还给出了触发规则 ID,方便我追溯规则来源。
这个流程跑通之后,我心里有底了。原来靠人工对话需要半小时才能出报告的安全审计,现在整个流程压缩到了 1-2 分钟,而且结果格式规范,可以直接写进安全工单。
4. 实测三组项目后,我做了哪些调优
4.1 三组项目实测对比
Skill 成型后,我拿三组真实项目做了验证,对比普通对话模式和security-audit-skill模式的效果,差异非常大。
| 项目特征 | 普通对话模式 | security-audit-skill 模式 |
|---|---|---|
| Node.js 全栈项目,约 2 万行 | 耗时 15 分钟,检出漏洞 3 个,输出 2000 字自然语言 | 耗时 90 秒,检出漏洞 11 个,输出结构化 JSON |
| Spring Boot 项目,约 1.2 万行 | 耗时 10 分钟,检出漏洞 2 个,且 1 个为误报 | 耗时 110 秒,检出漏洞 8 个,误报 2 个 |
| Python 数据处理项目,约 5000 行 | 耗时 5 分钟,检出漏洞 1 个 | 耗时 40 秒,检出漏洞 4 个,全部为 P1 级 |
这个对比里最扎眼的不是检出数量,而是覆盖面的差距。普通对话模式下,模型只会盯着代码里最"显眼"的问题,按下葫芦浮起瓢;Skill 模式因为有固定的检查清单和工具兜底,覆盖到了依赖和密钥泄露这些人类很容易漏掉的角落。
4.2 误报压降的三种手段
第一次实测的检出数上去了,但误报率也让我头疼。分析了一下,误报主要来自两类:一是semgrep规则对业务代码的上下文不敏感,经常把普通方法调用误判为危险函数;二是模型在生成报告时,对工具结果"照单全收",没有做二次研判。
我用了三种手段压误报:
第一是白名单机制。在 Skill 里维护一个false_positive_whitelist.json,把项目里已验证过安全风险的函数名、类名、接口路径加进去,扫描时跳过。这个文件每个项目单独维护,直接丢在仓库根目录。
第二是二次研判指令。在SKILL.md里强制要求模型在生成报告前,对每个工具命中结果做一次"是否真正可被外部输入触达"的判断。语义上就是让它思考一下:这个危险函数是不是真的接收外部可控参数,还是只是内部调用。对于无法确定的命中,标记为needs_review而不是直接归为漏洞。
第三是降低semgrep的自定义规则数量。我发现规则越多,误报率越高。最终把规则精简到 20 条左右,牺牲部分覆盖面换来了报告可信度的大幅提升。
4.3 token 开销控制
安全审计的 token 开销是一个必须正视的问题。尤其是semgrep的输出可能非常长,全部塞进上下文里,模型还没开始分析,几千 token 就没了。
我的控制方案是分轮执行。第一轮只让模型读取工具输出的摘要和命中文件路径列表;第二轮才让模型针对命中的文件逐一读取具体代码段。这个"先摘要后详情"的策略,把审计单次执行的 token 消耗压低了大约 40%。
另外,我在 Skill 里对输出长度加了硬约束:报告里的每条漏洞描述不超过 80 个字,修复建议不超过 120 字。长答案不等于好答案,在安全审计这个场景里,精准定位比洋洋洒洒的分析更有价值。
5. 踩坑实录:调这个 Skill 时踩过的雷
5.1 规则全量喂给模型,输出接近不可用
最早我犯过一个很蠢的错误:把semgrep的官方规则库里上千条规则全部塞给模型,指望它"参考这些规则进行审计"。结果惨不忍睹——模型被海量规则淹没,每条代码都往规则上靠,一个简单的console.log都能被解读出"潜在信息泄露风险"。
复盘之后我彻底明白了:Skill 里只能放经过筛选的、适用于当前项目类型的高质量规则,不能贪多。规则的意义不在于让模型知道所有可能性,而在于让模型知道当前项目里哪些问题最可能发生。
5.2 工具超长输出导致的截断与幻觉
有一次在扫描一个依赖特别多的 Node.js 项目时,npm audit输出了超过 200 条记录。这么大的结果丢给模型,直接导致了两个问题:一是上下文窗口溢出,后面的结果被截断;二是模型面对大量记录开始"总结式幻觉",主动且自信地编造了一些根本不存在的漏洞描述。
解决方式也很直接:对工具输出做预处理,超过 50 条的记录只保留severity=high以上的条目和它们对应的 CVE 编号,其余记录写入临时文件,等模型需要时再按需读取。永远不要高估模型处理超长列表的耐心和准确性。
5.3 多语言项目的规则分流问题
第三个坑发生在同时包含 Java 和 JavaScript 代码的仓库里。SKILL.md 里的规则是通用的,但 Java 的危险函数(比如Runtime.exec)和 JavaScript 的危险函数(比如eval)根本不是一个体系。模型在审计时经常搞混,把 Java 代码里不存在的问题张冠李戴到报表里。
最终我用语言分流解决了这个问题。Skill 在执行第一步"识别语言栈"时,会分别统计各类文件的占比,超过 20% 的语言种类才分配对应的检测规则集。单一语言的规则集之间互相隔离,不再混用。这也是为什么我说安全审计 Skill 必须做减法:规则越多,分流越复杂,越容易出岔子。
6. 进阶玩法:Skill 与 Agent 协同,以及 CI 落地
6.1 Skill 和 Agent 的实际分工
到了这一步,security-audit-skill已经能独立完成从扫描到报告的全过程了。但我在实际使用中又发现了新的问题:它太"听话"了。让它审哪个目录就审哪个目录,不会主动判断这个目录是不是已经被历史审计覆盖过,也不会在发现 P0 漏洞时提示我应该立即停止手头工作去处理。
这个问题的解法是引入 Agent 做调度层。Agent 负责判断"要不要启动一次安全审计、应该扫描哪些范围、结果出来后是否需要立即告警",而 Skill 负责具体的审计执行。简单说,Agent 是那个决定"做什么"的角色,Skill 是那个负责"怎么做"的把式。
在实际工程里,我用一个松耦合的方式让它们协同:Agent 通过调用 Skill 的入口函数触发审计,拿到 JSON 报告后进行意图判断。如果报告里有 P0 级漏洞,Agent 会额外生成一份面向管理层的摘要,并附上修复优先级建议。
6.2 接入 GitLab CI 的参考配置
Skill 不能只活在本地交互里,它真正的价值是在 CI 流水线里守门。我参照社区里一些开源项目的做法,把 Skill 的核心逻辑封装成了一个 CLI 程序security-audit-cli,这样既能被模型调用,也能脱离 AI 在流水线里独立运行。
在 GitLab CI 里的接入方式很简单,加一个专门的审计阶段:
security-audit: stage: test script: - security-audit-cli --scan . --output report.json --fail-on P0 artifacts: paths: - report.json when: always关键参数是--fail-on P0,意思是只要报告里出现 P0 级漏洞,流水线直接失败。这个配置能让安全审计从"被动的事后检查"变成"主动的卡点控制",问题代码根本走不到合并请求这一步。
6.3 把审计结果沉淀成回归测试样本
最后分享一个我在实践中觉得特别有价值、但很少人做的扩展:把每次审计出的真实漏洞样本沉淀成一个回归测试集。
具体做法是:每当security-audit-skill发现一个此前未被规则覆盖的新漏洞,我会手动把触发它的最小代码片段和修复后的版本保存到test-fixtures目录里。下次 Skill 更新规则时,先用这批历史样本跑一遍,验证新规则不会漏检旧问题,也不会误伤已经修复的代码。
这个做法的价值在于,它把 AI 审计的经验积累从一个封闭的"能力包"变成了一个开放的"成长型资产"。每审一个项目,Skill 对这类项目的敏感度就会提升一档。经过几个项目的沉淀之后,你会惊喜地发现,这个 Skill 已经比很多通用的安全扫描工具更了解你团队的代码风格和典型问题模式了。
从我自己的使用体感来看,做security-audit-skill这件事,最大的收益不是把审计时间缩短了多少,而是把审计质量从"看模型心情"变成了"看规则覆盖"。它让我在处理安全事务时有了一个稳定的抓手,也让团队里经验不够丰富的新人,在执行审计任务时能做到有章可循,产出不输给有经验的老手。这里面的方法不复杂,核心就是:界定范围、固化流程、挂好工具、控制好上下文。照着这个思路,你也可以在自己的项目里做一个专属的安全审计 Skill。