从零构建AI编程助手的安全审计Skill:原理、实践与避坑指南
打开任何一个AI编程工具的会话界面你有没有过这种感觉代码生成速度飞快但安全审计反而成了最容易被跳过的环节。最近在Claude Code、Codex、opencode这类工具里给Agent挂一份专属的skill是很多团队在折腾的事情。我基于这个思路从零做了一个security-audit-skill把代码安全审计的方法论固化成一个可复用的技能包让Agent在合代码、做重构、提交MR之前能顺手做一轮安全审查而不是等问题上线了再去找人补窟窿。这篇文把我从设计到落地过程中踩过的坑、反复试出来的方案一起整理出来适合正在用AI编程助手、又需要给代码安全把关的工程师参考。1. 为什么我最终把安全审计做成了Skill而不是一段提示词很多人的第一反应是安全审计不就是给模型一段提示词让它找一下代码里的安全漏洞吗我最初也这么干后来发现提示词方案在真实项目里根本扛不住。原因可以从Skill的本质聊起。1.1 Skill是什么一份给Agent的岗位说明书Skill本质上是一份结构化的技能说明书通常由一个包含SKILL.md主文件和若干辅助资源的目录组成。SKILL.md里写清楚这个技能适用于什么场景、要按什么步骤执行、需要输出什么格式辅助资源里则是检查清单、规则表、脚本等更详细的材料。和把指令硬塞进系统提示词相比Skill的核心机制是按需加载。Agent在实际执行任务时会根据用户请求决定要不要读取这份说明书说明书里提到的references目录和scripts目录也是按需展开的。这个设计的好处是你可以在项目里放几十个Skill而Agent的上下文不会被长期占用只有真正需要用到某个技能时才去翻对应手册。我自己实测下来的感觉是把安全审计写成Skill本质上是把一份容易被大段提示词淹没的方法论变成了一份独立、可复用、可版本管理的岗位说明书。换一个项目、换一台机器、换一个Agent环境直接把这个目录拷过去就能用。1.2 Skill、Agent、插件边界到底在哪里热词里一大堆人在搜skill和agent的区别skill插件之类的问题。我用自己的说法给你捋一下Agent是执行任务的员工负责拆解目标、调用工具、决策下一步动作。Skill是给Agent看的工作手册定义某个专业领域怎么做。**插件/工具tool**是员工手里的工具负责执行具体动作比如执行Shell命令、读文件、调API。一个常见的误区是把Skill理解成插件。插件告诉Agent你能做什么比如能执行命令、能抓网页Skill告诉Agent这件事该怎么做比如安全审计要先收集范围、再识别入口、最后出报告。两者可以配合使用——Skill的流程里可以指定Agent去调用某些插件工具但Skill本身不直接暴露给Agent作为可调用的函数接口。安全审计这个场景特别适合Skill化因为审计方法论相对稳定有CWE分类、有OWASP Top 10、有STRIDE模型打底规则是可以沉淀下来的。同时审计又是一个需要多轮搜证的过程不是一句帮我找漏洞就能完成的它需要Agent反复看文件、追调用链、对比依赖版本最后输出结构化报告。这部分流程编排正好是Skill的强项。1.3 做一个安全审计Skill比直接扫描器好在哪你可能还会问市面上有那么多SAST工具静态应用安全测试工具为什么还要让AI Agent做这事我最直观的感受是传统扫描器擅长找已经命名的漏洞模式但对于业务逻辑漏洞、越权访问、敏感数据过度暴露这类需要理解业务上下文的问题传统扫描器几乎无能为力。而Agent配合Skill可以在理解整个仓库结构的前提下做推理把代码路径、数据流、业务语义串起来这就是扫描器和初级审计员之间的区别。当然Skill方案也不是替代扫描器后面我会讲到它更适合做扫描器的前置分诊和补充判断。2. 开工之前先定好这个Skill的审计边界动手写文件之前最重要的一件事是明确这个Skill到底该干什么、不该干什么。我见过很多翻车的Skill基本都是因为边界没定清楚导致Agent在执行时要么像无头苍蝇要么越权做了不该做的事。2.1 目标定位让Skill当初级评审专家不是自动修复机器人我设计security-audit-skill时的定位是发现风险、给出证据、建议修复但绝不直接改代码。为什么不让它自动改因为安全审计的KPI是发现问题多、误报少、证据扎实一旦让模型在审计过程中顺手改代码它很容易为了完成任务而做出不安全的修复比如把验证逻辑删掉、把加密算法换成更弱的、或者改了接口签名导致业务崩溃。审计和修复是两个认知负荷完全不同的任务混在一起两件事都做不好。所以在SKILL.md的description里我就明确写了适用于对代码仓库、变更集、配置进行安全审计 输出结构化漏洞报告与修复建议 不负责自动修改代码不负责部署后渗透测试。这段描述不只是写给用户看的更是写给Agent看的。Agent会靠这段描述来判断现在要不要加载这个Skill、加载后能做什么、边界在哪。2.2 目录结构一次设计到位后面扩展省心我在设计目录时参考了一个原则主文件做流程编排细节内容全部下沉到references和scripts里。这样好处很明显——SKILL.md本身不会太长Agent读取时不会占据大量上下文侦察到具体场景时再按需拉取对应细节。我最终用的目录结构是这样的security-audit-skill/ ├── SKILL.md ├── scripts/ │ ├── collect_evidence.py # 收集高危文件的静态信息 │ ├── lockfile_scan.py # 扫描依赖锁文件 │ └── gen_report.py # 生成审计报告模板 ├── references/ │ ├── cwe_checklist.md # CWE Top 25检查清单 │ ├── dependency_risks.md # 依赖审计细则 │ ├── config_hardening.md # 配置加固清单 │ └── untrusted_input.md # 污染源定义与追踪方法 └── assets/ └── report_template.md # 审计报告模板不同Agent工具对Skill目录的放置路径略有差异比如Claude Code习惯放在.claude/skills/下Codex可以放在项目级目录或用户级目录opencode也有自己的约定。对于安全审计这种团队级能力我建议放在仓库根目录的.claude/skills/、.codex/或其他Agent约定的目录下这样所有用这个仓库的人都能自动继承这个技能。2.3 Frontmatter决定Skill什么时候被触发SKILL.md开头的Frontmatter是整个Skill的门面直接影响Agent会不会正确调用它。我踩过最典型的坑就是description写得过于笼统导致Agent根本不触发或者乱触发。推荐写法是这样--- name: security-audit description: 当用户要求进行安全审计、漏洞扫描、安全性评审、检查代码安全问题、分析变更集风险、审查依赖安全时使用。适用于代码仓库、独立文件、配置、依赖锁文件的静态安全审查。不适用于渗透测试、运行时动态扫描。 version: 1.0.0 allowed-tools: read_only_shell, grep, ripgrep, git_diff ---几个关键点description里要写明触发词和应用范围。Agent会用这个description和用户请求做匹配写清楚什么时候该用比写它能做什么更重要。version字段建议保留Skill文件迭代时Agent可以根据版本号判断是否读取过旧版缓存。allowed-tools字段不是所有Agent都支持但它是一个很好的安全声明。安全审计这个Skill只需要只读工具明确限制可以极大降低误操作风险。目录名最好用小写加连字符风格避免和Frontmatter里的name大小写不一致导致部分Agent加载失败这个坑我后面细说。3. 手写SKILL.md从侦察到出报告的全流程编排SKILL.md是Skill的灵魂。它决定Agent按什么思路干活。我第一版写得很随意结果Agent经常跳过侦察直接报漏洞或者把日志级别的风险提示当成高危漏洞。后来我把整条流程重写成了固定五步每一步做什么、输出什么全部显式写清楚。3.1 审计流程的五步编排先侦察、再深挖、后下结论在SKILL.md的主体部分我要求Agent必须按照以下顺序执行不允许跳步范围收集确定审计目标。如果是整仓分析仓库语言、目录结构、构建方式如果是变更集先用git diff确定变更文件列表。这一步的输出是一个清单列出要审计的文件和优先级。入口识别定位所有与外部交互的代码入口包括HTTP接口、消息队列消费端、命令行参数处理、文件导入功能、反序列化点。这是审计的关键起点也是我见过Agent最常跳过的一步。逐项检查按references里的检查清单对入口代码做数据流追踪。重点追踪不可信输入从入口到敏感函数SQL、Shell、文件路径、反序列化、加密的完整路径。交叉验证对有嫌疑的点做二次验证。判断是否存在前置校验、是否使用了安全API、依赖版本是否已被修复、是否存在可利用场景。这一个步骤是降低误报的命门。输出报告严格按照模板输出包含机器可读的JSON汇总和人工可读的Markdown详情。我在SKILL.md里用必须禁止这样的强指令来约束Agent比如在完成步骤2之前禁止输出任何漏洞结论。这样写虽然听起来挺粗暴但对提升审计质量非常有效。3.2 检查清单从六个维度去找问题我的references/cwe_checklist.md里维护了一张清单Agent会按这个维度逐项排查。这张表我直接贴出来供你参考检查维度具体风险点对应CWE参考注入类SQL注入、命令注入、路径穿越、模板注入CWE-89 / 78 / 22 / 1336认证与授权硬编码凭证、弱口令逻辑、越权访问、JWT缺陷CWE-798 / 287 / 285敏感信息泄露日志打印密钥、前端硬编码密钥、明文传输CWE-532 / 312 / 200加密缺陷使用了弱哈希、不安全随机数、自定义加密算法CWE-327 / 338依赖风险高危版本依赖、锁定文件缺失、包来源不可信CWE-1104配置安全Debug模式开放、CORS配置过宽、权限配置过大CWE-284 / 942每个维度下还有更细的指引比如在注入类里我会要求Agent特别关注字符串拼接SQL、eval()、os.system()、file_get_contents()这类敏感函数同时要求它必须以数据流为线索不能看到敏感函数就直接报漏洞。3.3 证据链要求漏洞报告里必须写清从哪里来到哪里去这是我和这个Skill反复磨合后最满意的设定之一。我要求Agent在每一条漏洞发现里都必须给出完整证据链缺一不可漏洞位置文件路径和行号格式为src/xxx.py:42。污染源不可信输入的来源比如HTTP参数、用户上传文件、HTTP请求头。传播路径从污染源到触发点经过的关键步骤和中间变量。触发场景一段尽量短的、可以人工复现的恶意输入描述。修复建议至少给两种可选方案并说明各自的适用场景和副作用。这个设定看起来只是输出格式要求实际效果是逼着Agent把猜测变成推演。因为如果没有证据链要求模型非常容易基于模糊匹配就给出一堆存在SQL注入风险的泛泛结论有了传播路径要求之后它必须真的去追踪变量之间的数据流准确率会明显上了一个台阶。3.4 输出报告模板人工可读为主机器可读兜底我在assets/report_template.md里定义了标准的报告结构核心骨架如下## 审计范围 - 审计目标 - 仓库/文件规模 - 审计语言与框架 ## 漏洞汇总表 | 编号 | 严重级 | 漏洞名称 | 位置 | CWE | ## 漏洞明细 ### [P0] 漏洞名称 - 位置 - 类型CWE - 污染源 - 传播路径 - 触发场景 - 修复建议 ## 审计结论 - 总体风险评级 - 优先处理项同时在报告末尾我会要求Agent输出一段JSON格式的机器可读摘要方便后续接CI脚本做自动判断。这个JSON可以直接放进Markdown的代码块里不会影响人工阅读{ critical: 0, high: 1, medium: 3, low: 5, total: 9, suggested_action: block }4. 让Agent真正执行审计的三个关键设置写完了SKILL.md还有一个很现实的问题摆在那里Agent的上下文窗口是有限的一个大型仓库可能有几千个文件怎么让它审得动实测下来必须从机制上做一些设计。4.1 上下文窗口有限采用分批审计、最后合并策略我在SKILL.md里明确写了一条规则单次审计会话默认不超过30个目标文件。如果审计范围超过30个文件就按以下批次执行第一次迭代先审计入口类文件和敏感API调用类文件。第二次迭代审计数据模型、配置、数据库访问层。第三次迭代审计工具类、测试代码、脚本。每次迭代结束后生成临时审计小结存入工作记忆。所有批次完成后合并生成完整报告。为什么是30个文件这个数值是我实测得出的折中方案。一次给模型灌太多文件它后期会明显出现漏审尤其是中间段的文件几乎被遗忘一次太少迭代次数太多容易在批次衔接时丢失上下文。30个文件大约对应几千行代码能让模型在仔细阅读和总览全局之间找到平衡。另外我强烈建议在SKILL.md里明确审计优先级规则——优先处理处理外部输入的文件、初始化权限的文件、进行加解密的文件把这些文件放在第一批次这样即使后面批次来不及完成最关键的风险也已经被覆盖了。4.2 让审计结论既可读又可算JSON摘要不能省前文提到报告末尾要求输出JSON摘要这个设计不是可有可无的装饰。实际使用中这个JSON摘要承担了两个重要职责接入CI流水线做自动门禁。CI脚本读取JSON里的critical和high计数如果超过阈值就直接让流水线失败阻止高危变更合入。做多轮审计的进度对账。把一次重大重构拆成多次审计每次留一份JSON最后汇总对比看风险是收敛还是发散。我在scripts/gen_report.py里还加了一个小功能解析Markdown报告提取所有[P0]、[P1]标记的行自动生成一个漏洞清单。这样即使Agent输出的格式偶尔有偏差脚本也能兜底提取关键信息。4.3 只读工具限定的两层含义安全审计Skill里Agent能调用的工具必须严格限定为只读操作Read、Grep、Ripgrep、Git Diff、正则搜索。在SKILL.md的流程说明里我会专门加一段审计过程中禁止执行可能修改系统或仓库状态的命令包括但不限于写入文件、运行依赖安装、执行数据库迁移、启动服务、调用外部API发送数据。如需执行脚本辅助分析脚本本身必须只读且运行前先向用户确认。这个限制不只是出于安全考虑也是为了让Agent更专注。如果它总是想着要不要运行一个脚本把项目跑起来注意力就会从静态审计上散掉很容易把审计搞成一次半吊子动态测试——既没有静态的完整性也没有动态的准确性。5. 在Claude Code、Codex上实测踩过的坑把Skill写好和让Skill在真实环境里稳定跑起来是两种完全不同的体验。我在几个主流Agent工具Claude Code、Codex、opencode这类环境里反复测试了几轮踩了不少坑挑几个最典型的分享一下。5.1 模型把潜在风险当成已确认漏洞一刀切这是我在第一轮测试时遇到的最大问题。Agent看到代码里用了eval()就直接报高危命令注入但实际场景是那个eval()的输入是开发者自己写死在配置文件里的根本不接收外部输入。这类误报会让整个审计报告失去可信度团队看着一堆假漏洞就会对审计结论集体免疫狼来了喊多了真漏洞反而被忽略。我的对策是在references/untrusted_input.md里给污染源做了非常明确的定义并要求Agent对输入可控性做三级判定已确认可控输入来自网络请求参数、消息队列消息、用户上传文件等外部通道。此类可确认为有污染源。疑似可控输入来自配置文件、环境变量、数据库记录但暂不确定是否可被外部影响。此类标记为疑似。不可控输入是代码内联的字面量、编译期常量。此类必须排除或降级为代码异味。有了这个三级判定Agent在报漏洞前必须先把污染源状态写清楚误报率明显下降。我曾经在一个中型项目上对比过加上污染源判定后高误报量从第一轮的十几条降到了三条以内。5.2 严重级别分级不搞CVSS全套用简洁四级制一开始我用CVSS打分思路来要求Agent结果模型输出极不稳定同一类漏洞在不同文件里经常给出不同分数。后来我抛弃了精确打分改为简化四级制并把这套分级写死在SKILL.md里级别定义典型场景处理策略P0无需认证即可远程利用影响范围大未授权SQL注入、任意文件读取RCE立即阻止合入P1需要认证或本地触发的高危问题越权访问、反序列化、硬编码密钥当轮迭代修复P2配置与依赖层面的风险过宽CORS、高危版本依赖、错误日志泄露排期修复P3规范类或潜在风险弱随机数、明文存储、缺少输入长度校验择机优化这个四级分级最大的好处是让Agent在级别判断上不需要做复杂计算只需要把漏洞场景和表格里的定义做模式匹配。分级越简洁模型输出的一致性越高这一点在多次实测中验证过。5.3 Skill加载失败和触发混乱的排查思路我在换了一个项目目录后发现Skill怎么都不触发排查了半天才发现是目录名和Frontmatter里的name大小写不一致导致的。目录叫SecurityAuditSkillname却写的是security-audit部分Agent加载时对不上号直接忽略。排查Skill加载问题我建议按这个顺序来定位先确认目录路径是否正确。不同Agent对不同目录有偏好有些只认项目根目录有些也认用户全局目录路径不对一切白搭。检查Frontmatter格式。name、description字段是否完整YAML缩进是否正确description里是否有特殊字符。确认目录名与name的一致性。统一用小写连字符风格这是兼容性最高的做法。检查SKILL.md体积。有些Agent会限制单个技能文件的体积过大可能导致加载超时。我一般把主文件控制在200行以内额外的细节全部下沉到references。用最短指令验证。比如只输入对当前项目做安全审计输出摘要观察Agent是否触发排除了用户指令本身就很模糊的情况。6. 从代码审计到供应链审计继续扩展的方向Skill的目录结构天然支持渐进式扩展。security-audit-skill第一版只覆盖代码级审计后来我给它加了不少能力扩展起来非常顺滑。6.1 从单仓库审计扩展到依赖供应链现代应用超过八成的代码其实来自第三方依赖只审业务代码远远不够。我给Skill额外加了一个lockfile_scan能力通过scripts/lockfile_scan.py实现解析常见的package-lock.json、pnpm-lock.yaml、Cargo.lock、go.sum、poetry.lock文件标记可疑的版本来源比如非官方registry镜像域名检查锁文件是否存在缺少锁文件本身就是一个供应链风险信号尝试识别已知的高危版本区间这个需要定期更新一份本地规则文件不能依赖模型记忆。实测下来这个扩展对新项目初始化场景特别有价值。很多脚手架生成的初始依赖里都藏着历史漏洞版本模型在没有实时漏洞库的情况下很难凭记忆判断但配上规则文件后准确率就完全可用了。6.2 把审计结果接入CI流水线让Skill成为团队门禁一个Skill的价值如果只停留在开发者本地跑一跑那它只是个人提效工具。把它变成团队资产最直接的方式是接入CI流水线。我在团队里设计了一个非常轻量的方案在CI脚本里调用Agent工具让它用security-audit这个Skill扫描本次变更涉及的文件让Agent输出JSON格式摘要用一条很短的后置判断命令处理摘要如果P0或P1数量大于0流水线直接失败并附上报告链接。# 参考命令需结合具体Agent工具调整 agent_cli audit --diff HEAD~1 --skill security-audit --output audit_result.json python3 -c import json with open(audit_result.json) as f: data json.load(f) if data.get(p0_count, 0) 0 or data.get(p1_count, 0) 0: print(安全审计未通过) exit(1) print(安全审计通过) 这种做法虽然不能替代人类安全工程师的最终裁定但能挡住相当一部分低级错误在合入主干之前流入主线已经能为团队省下大量返工时间。关于这个Skill我个人实际使用最大的体感是它不是用来炫技的而是能把一次专业的代码安全评审经验沉淀成任何一个Agent都能照着执行的标准作业流程。如果团队里每周还在重复做同样模式的代码评审那把这份经验固化成一个Skill可能是这个月性价比最高的一件事。一个小建议与其一开始就想做一个全知全能的安全审计Skill不如把团队过去半年里踩过的最常见的安全问题整理进references从五个最痛的场景起步效果一定比照搬我这一套来得更快。