有人觉得“安全审计”这种东西,丢给大模型写个Prompt就能干,实际试过才知道,输出基本是“正确但没用”的废话。前阵子我把一个十几万行的Node.js项目交给Codex跑安全审计,它给我交了一份充满“注意输入验证”“小心XSS”这种大路货建议的报告,看完等于没看。后来我把一套以前在企业里手工做代码审计的流程、清单和判定规则,封装成了标准的Skill技能包,再拿同一个项目去跑,出来的结果含漏洞类别、CWE编号、复现证据、修复示例,质感和内审报告一个级别。
这篇文章就围绕security-audit-skill的完整设计和落地过程展开,讲清楚一个安全审计Skill该怎么拆结构、写指令、接工具、埋质量控制点,以及我在实测里踩过的坑。
1. 为什么安全审计这件事,天生适合做成Agent Skill
2025年这个节点上,凡是聊AI Agent的人几乎都会碰到Skill这个词。它和普通的Prompt有本质区别。Skill不是一段让模型“自由发挥”的提示词,而是一套可复用的能力封装。一个完整的Skill由主文件、知识库、模板、脚本等组成,模型在遇到对应任务时按“工作手册”的节奏读取并执行,每一步做什么、输出什么结构,是预先定义好的。
很多人分不清Skill和Agent的区别。简单说,Agent是那个会自己决定“先做什么后做什么”的执行者,Skill则是执行者随时可以从包里掏出来的“专项工作手册+工具箱”。一个Agent可以同时挂十几个Skill,遇到写代码任务加载编码Skill,遇到审计任务加载审计Skill。没有Skill的Agent就像经验不足的新人,什么都能聊,但一落到专业产出就露馅。
安全审计之所以特别适合Skill化,是因为它同时满足三个条件:
- 流程高度标准化。代码审计这件事不管项目多大、用什么语言,核心路径永远是“摸清结构→识别风险面→逐类检查→验证→出报告”。这套流程在安全行业已经沉淀了几十年,完全可以固化成步骤。
- 知识密度极高。安全审计涉及注入、越权、敏感信息泄露、反序列化、供应链攻击等几十个漏洞类别,每个类别又有不同技术栈下的变体。单靠模型参数里的通用知识,不可能在几千行上下文里记得全面,更别提准确映射CWE编号。
- 结果必须可复现可追溯。审计报告是要拿来推动研发改代码、给管理层做风险决策的。如果Agent给不出证据链,报告没人敢信。这个是Prompt方案几乎没法保证的,但Skill可以在模板层面强制约束。
实测对比下来,裸Prompt处理同一个审计任务,普遍有三个毛病:第一是审计范围会漂移,模型聊着聊着就跑偏到无关的代码风格、性能优化上;第二是结论空泛,没有具体文件和行号支撑,全是“可能存在风险”这种无法推动整改的废话;第三是输出格式每次都不一样,有的人喜欢列表有人喜欢段落,没法直接对接缺陷管理平台。
把审计方法论固化成Skill之后,这些问题的根因直接被结构性解决:范围由checklist锁定,证据由模板强约束,输出由报告骨架固定。这也是为什么我会建议做安全领域Agent应用的人,优先从Skill而不是纯Agent工作流入手。
2. 设计一个安全审计Skill,先想清楚四个边界问题
拿到任务就开写目录和文件,是做Skill最容易踩的坑。Skill不是越全越好,而是边界越清晰越好。一个什么都想覆盖的Skill,最后一定是什么都覆盖不深。我在动手前先花了一晚上回答四个问题,这四个答案基本决定了整个Skill的骨架。
边界一:审计对象到底是什么?是代码库、容器镜像、云资源配置、还是K8s集群?不同对象的检查方式完全不同。代码库看源代码逻辑,容器看基础镜像和层信息,云配置看IAM权限和存储桶策略。定位不清的话,模型会在错误的对象上套错误的检查姿势。
边界二:审计深度到哪里?只做静态分析,还是要跑依赖漏洞比对、密钥扫描、甚至动态验证?深度决定Skill需要挂多少外部工具。浅度审计只需要模型自己读代码推断,深度审计就得编排gitleaks、semgrep、trivy这类扫描器,还得处理扫描结果的分析判断。
边界三:以什么标准作为判断基线?OWASP Top 10是最常用的Web应用基线,CWE是工业级漏洞字典,PCI-DSS这类合规基线则适合金融场景。基线对了,报告才能被安全团队认可。我最后选择以OWASP Top 10 + CWE Top 25作为默认基线,这两个标准覆盖面广、社区认可度高,而且都有公开的详细定义,方便写进Skill的知识文件。
边界四:报告给谁看?给研发自测用,报告重点是代码定位和修复建议;给甲方交付用,报告要增加风险评级和整改优先级;给CISO汇报用,报告得附上业务影响分析和整改投入估算。这决定了报告模板的粒度。
这四个问题想清楚后,我确定自己要做的方向是:面向通用代码库(偏Web应用和后端服务)的安全审计Skill,默认执行静态分析级别的深度,输出为“研发能改、Leader能排期”的分级报告。这个定位也符合大多数团队的实际诉求——不是每次都做渗透测试,而是想在代码阶段就把隐患拦下来。
确定边界还有个额外好处:它直接决定了目录结构。比如我最终没有把容器检查塞进来,因为那是另一套知识体系,硬塞进来只会让SKILL.md臃肿到模型都懒得从头读到尾。Skill的边界就是能力的边界,这句话值得反复强调。
3. 从零搭建security-audit-skill的完整文件结构
定位确定后,目录结构基本水到渠成。我最终采用的布局是:
security-audit-skill/ ├── SKILL.md ├── knowledge/ │ ├── audit_checklist.md │ ├── cwe_mapping.md │ └── tech_stack_patterns.md ├── templates/ │ ├── audit_report_template.md │ └── finding_card.md └── scripts/ ├── secret_scan.sh └── dep_audit.py这个结构里,SKILL.md是总入口,模型首先读它。它包含YAML格式的元信息(Skill名称、触发条件、适用场景)和正文(工作流程、步骤要求、质量门禁)。它不是用来承载全部知识的,而是告诉模型“你有哪些知识资源、什么时候该读哪个文件、每一步该做什么”。有点像一个战场指挥官手里的作战地图,不是百科全书。
knowledge/下面放的是不同维度的知识文件。audit_checklist.md是最核心的审计清单,把几十个漏洞类别转成模型可直接判断的规则条目;cwe_mapping.md用于把检查项映射到CWE编号和风险等级,出报告时直接引用;tech_stack_patterns.md放不同技术栈的常见漏洞模式,比如Node.js的child_process.exec注入模式、Python的eval使用风险、Java的ObjectInputStream.readObject反序列化风险等。模型在审计具体项目时,按需加载与当前技术栈匹配的那个文件。
templates/放输出模板。audit_report_template.md定义审计报告的完整结构,finding_card.md定义单条漏洞发现的标准卡片格式。为什么要单独拆模板文件?因为我发现如果把报告格式直接写进主文件,模型在长篇审计过程中很容易走形,而且模板文件可以被其他流程复用。
scripts/放辅助工具脚本。secret_scan.sh负责用正则快速筛敏感信息,dep_audit.py负责比对依赖库版本与已知漏洞库。工具脚本的价值在于把一些模型不擅长的高成本操作(比如全库正则扫描)用程序高效完成,再把结果交给模型做语义分析。工具输出和模型分析结合,比任何一个单点都稳。
这里要特别强调:整个Skill的知识文件加在一起,控制在能一次载入模型上下文窗口的合理范围。Skill不是论文,不是内容越多越好。文件越多,模型在整个审计过程中需要反复读取文件,反而容易分散注意力。我的原则是主文件控制在300行以内,每个知识文件控制在200行以内,原则是“密度高,检索友好”,每一条都要能直接触发决策,而不是百科词条式的内容堆砌。
4. 把安全审计方法论翻译成Agent指令:六步流程设计
文件结构搭好后,最关键的是SKILL.md里的流程指令怎么写。这一步决定Agent是在“按方法论工作”还是“假装在按方法论工作”。
我按人工审计的习惯,把流程拆成六个步骤,并且给每个步骤设定了明确的输入、动作和产出:
第一步,项目侦察与技术栈识别。指令要求模型先列出仓库根目录、主要配置文件(package.json、pom.xml、requirements.txt等)、目录结构和入口文件,判断这是前端、后端、脚本还是移动项目,识别主语言和框架版本。这个步骤的产出是“技术栈识别卡”,后续所有检查项都要基于这个卡片匹配。
第二步,依赖与供应链审计。指令要求模型提取依赖清单,执行版本比对脚本,列出高危依赖和已知CVE。这一步不要求模型记住所有漏洞库,而是让它调用脚本拿结果。脚本没有扫描到不代表安全,报告里要注明“本步骤依赖本机漏洞库数据”。
第三步,按清单逐类静态审查。这是最重的步骤。指令要求模型基于audit_checklist.md,按“注入类→身份认证与越权→敏感信息→加密与传输→文件操作→日志输出→第三方组件使用”的顺序,逐类对源代码做检查。每发现一个问题,必须记录文件路径、行号、问题代码片段,并映射CWE编号。这里有个关键指令:对于拿不准的疑似点,先记录为“待验证”,不要凭感觉下结论。这个设计对降低误报率帮助巨大。
第四步,密钥与敏感信息扫描。指令要求先执行secret_scan.sh,再对脚本命中结果做语义验证。正则扫出来的东西很多是测试密钥、示例代码或误报,模型必须结合上下文判断是否为真实密钥。这一步可以拦截大量误报。
第五步,验证与优先级排序。指令要求模型把所有发现按CVSS风格重新评级,列出“高危优先处理”列表,并在每一项后面给修复建议。单条漏洞发现的完整度用“证据是否完整”作为门禁。
第六步,生成结构化报告。指令要求严格套用报告模板输出,包括执行摘要、审计范围、工程概况、风险统计、高危漏洞明细、中低危漏洞汇总、整改建议清单、附录(未覆盖项说明)。每一类风险都要给出严重程度和整改建议。
流程中间的指令措辞也很讲究。比如“逐类检查”这个词,实际执行时我会附加一句“检查完一个类别再进入下一个类别,不要并行跳跃”。原因是模型在Agent循环中经常自作聪明地跳跃,导致检查覆盖不全。我在实测中观察到,加上这个约束后,漏捡率明显下降。
另外,输出模板要在这一步就介入。在第五步的时候,每个独立漏洞就要按finding_card格式填写,而不是最后统一生成,避免最后因为上下文过长信息丢失。
5. 让它会调用工具:扫描器与Skill的协作方式
安全审计里有不少动作光靠模型“看代码”效率太低,比如扫全仓库的密钥、比对几百个依赖的版本、搜特定危险函数的调用链。这些应该交给工具做,模型做判断和整合。
我在Skill中集成工具遵循一个原则:能用脚本的事情不让模型硬看,模型只负责脚本解决不了的分析和判断。secret_scan.sh就是一个示例,运行逻辑是先扫出候选文件,再把完整内容交给模型做语义确认,这样就不用让模型把整个仓库都读一遍。
#!/bin/bash # 基于常见密钥格式做低误报预筛,命中后交给Agent做二次语义验证 scan_dir="${1:-.}" grep -rInE --include="*.{js,ts,py,go,java,php,rb,yaml,yml,json,env,tf}" \ -E "(AKIA[0-9A-Z]{16}|-----BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY-----|sk_live_[a-zA-Z0-9]{16,}|password[[:space:]]*[:=][[:space:]]*[^[:space:]\"']{6,})" \ "$scan_dir" -l 2>/dev/null | head -50这个脚本只输出候选文件列表,不让Agent直接基于正则结果下结论,因为正则命中不一定是真泄漏。比如测试代码里的password: "123456"经常被误报,模型看了上下文就能判断这只是一个测试桩还是真实硬编码。工具负责效率,模型负责语义,两者不互相替代。
依赖审计脚本同理,它的产出是“依赖名+版本+已知漏洞数”的结构化列表。真正判断某个漏洞在目标项目里是否可利用、影响面有多大,还是需要有经验的模型来分析。在集成工具时,SKILL.md里要明确写一条fallback规则:如果检测到对应工具不可用(比如grep版本不支持某些参数、环境里没有python3),就自动切换到“纯静态审查模式”,同时在报告里标注“密钥扫描步骤未执行,覆盖范围可能不完整”。这样即使工具缺失,整个Skill也不会跑飞到中断,报告的可信度也不会被破坏。
工具的集成方式不需要多复杂。Skill本质上是通过指令编排工具链,而不是硬编码一个固定命令。我在SKILL.md中写的是“先检查工具是否存在,存在则执行,不存在则降级”,后面实测阶段发现这个设计救了很多次场。
6. 实测踩坑记录:为什么Skill有时不生效、报告仍然敷衍
写完了不等于好用。我把Skill接到Codex和OpenCode上跑了三个真实项目,前前后后挖出来五个比较典型的坑,每个都值得单独说。
坑一:description写得太泛,Skill压根不会被触发。早期我在frontmatter里写的描述是“帮助用户进行安全审计”。结果模型遇到审计请求时经常不加载这个Skill,而是直接进入闲聊式回答。原因在于触发判断需要非常具体的语义匹配。修改后我把description改成“当用户要求对代码库、项目或应用进行安全漏洞扫描、风险审计、渗透测试辅助、代码安全检查、依赖漏洞分析时,使用本Skill。典型触发词:security audit、安全审计、漏洞扫描、代码审计、检查安全隐患、找安全漏洞。” 改完触发成功率显著提升。触发描述里尽量列举动词和任务目标,而不是形容词和名词。
坑二:主文件太长,步骤写得太细,模型反而不好好执行。我最初把每个漏洞类别的详细检查规则全写进了SKILL.md,结果文件超过700行。实测模型会挑着读,后面的步骤基本被忽略。后来我把详细规则全部迁移到knowledge/下的独立文件,SKILL.md只留流程和触发条件。模型读主文件成本低,才会真正把整个流程跑完。知识文件不需要在主文件里展开,只需要写清楚“什么时候该去读哪个文件”。
坑三:没有定义“完工标准”,模型提前交卷。早期报告经常有这种情况:审计到一半,模型开始写总结,说“以上是对本项目的安全建议”。根源是它不知道什么时候才算真正完成任务。后来我在SKILL.md里新增了“本轮审计的完工标准”章节,明确要求必须覆盖全部检查类别、所有高危漏洞必须有完整证据链、没有进行动态验证的项必须在报告中声明。模型有了明确的“done”定义,偷懒行为大幅减少。没有质量门禁的Skill绝对不会产出高质量结果,这句话希望大家牢记。
坑四:多宿主兼容问题。Skill的加载方式在Codex、OpenCode、Roo Code之间是有差异的,主要在于frontmatter字段的解析和辅助文件加载方式。同一个Skill在Codex上表现正常,在OpenCode上可能读不到knowledge目录下的文件。排查下来发现是不同宿主对“辅助资源路径”的解析规则不一样。最实用的解法是:在SKILL.md正文里显式写清楚每个辅助文件的作用和读取时机,同时在knowledge文件里加上与主文件互相引用的文本。让“必须被读取”的文件在正文中被明确点名,不同宿主的加载差异就不会导致核心资产丢失。
坑五:模型在审计时乱用工具参数。我有一次观察到模型执行secret_scan.sh时,把路径参数拼错了,导致脚本扫了整个根目录,输出结果变成了全盘文件列表。这个坑的本质是模型对命令语法理解不到位。修复方案是:在SKILL.md中给出工具的标准调用示例文本,要求模型“严格按下述命令格式调用,不要自行创意修改”。同时脚本内部也做防护,比如限制扫描目录必须存在且不是根目录,扫描文件数超过阈值就退出。这些硬性防护比单纯写指令可靠。
每个坑修完之后,我都重新跑了一遍回归测试,确认修复没有破坏其他环节。这个习惯建议所有人都养成,Skill的迭代必须建立在回归验证的基础上,不能修一个坑引出另一个坑。
7. 让安全审计Skill真正好用,最后还要做这三件事
基础版本能跑通之后,我做了三件事让Skill从“能跑”变成“好用”。
第一件事,建立误报漏报回填机制。每次审计后,把模型判断错误的地方记录下来:误报的案例看是因为规则写得模糊、还是上下文缺失;漏报的案例看是因为checklist没覆盖、还是模型没有正确触发检查项。这些案例持续回填到audit_checklist.md里,相当于给Skill做“经验反哺”。Skill是一套静态文件,但它的设计迭代应该是动态的。每次真实项目的审计结论都会成为下一个版本的知识补充。
第二件事,给Skill加“审计级别”入口参数。用一句话控制扫描深度:默认级别做结构化清单检查,适合日常自测;深度级别增加调用链审查和数据流分析,适合上线前验收;快速级别只做密钥扫描和依赖比对,适合每天CI里快速跑一遍。这些级别不是让用户改代码,而是在Skill内定义分支指令,让模型根据用户输入自动选择执行路径。参数化让同一个Skill可以适配开发、测试、运维三种角色。
第三件事,写一个最小验证用例集。我准备了三个测试用项目:一个故意埋了多个漏洞的Java Demo、一个包含硬编码密钥的Python脚本仓库、一个依赖版本过老的前端项目。每次修改Skill后,依次跑这三个用例,通过才算改完。这个习惯很重要,没有验证用例的Skill维护就是盲人摸象。
现在这个Skill已经成为我日常交付前自检的标准动作,每次给客户交付代码前都会自动跑一轮。个人体会是,做Skill和做产品本质上没有区别——真正让一个Skill有价值的不是写出来的那一刻,而是后续在真实项目上反复被锤打、不断修正的那些版本。