打造AI安全审计技能:SKILL.md设计与落地实践
最近我把团队内部用了两个月的security-audit-skill整理成了独立仓库。起因很直接团队里几个主力开发都开始用 Claude Code 和 Codex 写业务代码AI 产出速度确实快但安全上总是漏风。更尴尬的是你让模型直接做代码审计它给你的往往是“建议对输入进行校验”“注意 SQL 注入风险”这种正确的废话——没有具体位置、没有危害链路、没有可落地的修复方案。后来我把审计经验拆成结构化指令、检查清单和风险定级表做成一个专门用于安全审计的 skill技能包。这篇文章就把这个 skill 的完整设计思路、SKILL.md 的写法、安全规则的沉淀方式、以及落地实测效果一次性讲清楚既适合想给自己的 AI 助手加技能的人参考也适合安全工程师拿来当工作流的起点。1. 为什么“直接让 AI 审代码”和“用 skill 审代码”是两回事先说说我在做这个 skill 之前踩过的坑。最早我直接在对话里贴一段代码说“帮我做安全审计”模型回复得像模像样列了三五个“风险点”但仔细看全是套话。比如让它审一个登录接口它说“建议使用参数化查询防止 SQL 注入”可代码里压根没有数据库操作它说“密码应该用 bcrypt 加密”但代码里用的是 PBKDF2——只是想当然。真正的痛点是模型没有一套固定的工作方式你问一百次它有一百种回答角度下一次可能是 SQL 注入下一次可能是越权全凭上下文瞎猜。1.1 裸提示词缺少的东西我把问题拆开看发现裸提示词缺了四样东西角色与边界不清晰。模型不知道自己是“安全审计员”还是“代码解释器”经常跑偏去讲业务逻辑。检查清单不完整。它默认只会凭训练语料里的高频记忆答题OWASP Top 10 里那些低频但致命的漏洞类型比如反序列化、SSRF、路径穿越往往被忽略。执行流程不可控。没有一个标准的“从入口到数据流再到信任边界”的审计步骤输出顺序完全随机。输出格式不稳定。这次给表格下次给列表再下次给一段散文没法直接对接工单或修复任务。1.2 skill 本质上是什么后来我理解了skill 不是给模型加魔法而是把“一个老审计员的工作习惯”翻译成模型能严格遵循的程序。它通过SKILL.md文件告诉模型你是谁、什么情况下触发、按什么顺序做哪些事、输出必须长什么样。模型本身的能力没有变但约束变了行为就从“随机的生成”变成了“稳定的执行”。我当时查了一些热门 skill 仓库比如各种 codex skill、opencode skill 的做法发现它们的核心都是同一个模式结构化指令 领域知识 输出协议。security-audit-skill只不过是把这三个部分切到了安全审计这个具体场景里。对比维度裸提示词skill 驱动审计流程模型自行发挥固定五步法漏洞覆盖面凭记忆覆盖随机基于显式检查清单输出格式每次不一样固定报告模板知识引用泛泛而谈可引用本地规则文件可复现性低高团队复制能力无法复制一个目录拷贝即用所以如果你也想做任何领域的 skill先想清楚一个问题在这个领域里一个合格的人是怎么干活的把这个流程固化下来你的 skill 就成功了一半。2. security-audit-skill 的仓库结构与 SKILL.md 设计我建的项目结构是这样的它参考了当前 skill 生态里比较主流的组织方式也做了一些针对安全场景的调整security-audit-skill/ ├── SKILL.md ├── references/ │ ├── owasp_top10_mapping.md │ ├── dangerous_functions.md │ ├── secrets_patterns.md │ └── risk_rating.md ├── scripts/ │ ├── secret_scan.py │ └── dep_audit.py ├── prompts/ │ ├── audit_single_file.md │ └── audit_repo.md └── examples/ ├── report_sample.md └── vulnerable_fragment.js这个结构看着简单但每个目录都有明确的目的。references/存放模型执行审计时需要查阅的领域知识库scripts/放一些模型可以调用或参考的辅助脚本examples/放一份标准的报告样例用来校准输出格式。整体原则是让模型“查得到、跑得动、对齐得上”。2.1 SKILL.md 的头信息与触发条件SKILL.md是 skill 的核心入口。它的 frontmatter 部分非常关键因为 agent 是通过name和description来判断什么时候该调用这个 skill 的。description写得太笼统模型该触发时不触发写得太窄不该触发时瞎触发。--- name: security-audit description: 对代码做安全审计时使用。当用户要求检查代码漏洞、评估安全风险、 寻找 SQL 注入 / XSS / 越权 / 敏感信息泄露 / 不安全的反序列化、 或要求输出一份带风险等级和修复建议的安全审计报告时必须使用本 skill。 适用于单文件、多个文件或整个仓库粒度的静态安全分析。 ---这里我踩过一个坑一开始description只写了“perform security audit”结果在 Codex 里经常不触发因为模型看不出这个 skill 跟“帮我看看这段代码有没有问题”之间的联系。后来我明确列出了具体的漏洞名词和用户可能用的口语化表达触发率才上来。skill 的 description 要按用户会说出来的话去写而不是按你技术上的定义去写。2.2 审计指令的主流程SKILL.md正文部分我用的是“角色 触发条件 流程 规则优先级 输出协议”的结构。核心是五步审计流程每一步都写得很具体收集上下文确定审计对象的语言、框架、代码量、入口文件。如果信息不足先列出需要用户补充的问题不要直接开审。识别入口点与信任边界标记所有外部输入来源HTTP 参数、请求头、文件上传、环境变量、消息队列等这是之后追数据流的锚点。追踪数据流从入口点出发追踪用户可控数据经过了哪些处理、最终到达了哪个“危险函数”。这一步是决定出报告质量的核心。逐项核查漏洞清单按照references/里的 OWASP Top 10 映射表和危险函数表逐类检查不能跳项。输出审计报告按输出协议给出的固定模板生成报告每个问题必须包含文件位置、风险等级、漏洞描述、修复建议和验证方法。下面是SKILL.md里的一段核心指令示例你可以直接抄去改## 审计流程 你必须按以下顺序执行不能跳过任何步骤 1. 收集上下文 - 确定代码语言、框架、依赖清单 - 估算审计范围如果代码量大于 5000 行先给出审计计划 2. 识别入口点 - 查找所有用户输入来源request body、query params、headers、 cookies、file upload、env vars、外部 API 回调 - 列出每个入口点的可信度等级 3. 追踪数据流 - 从入口点追踪数据到危险函数 / API 调用 - 对每条路径标注输入来源 - 处理过程 - 汇点sink 4. 逐项核查漏洞清单 - 打开 references/owasp_top10_mapping.md - 按清单逐类排查不允许遗漏任何类别 - 如果某项不适用写明“不适用”并简单说明原因 5. 输出报告 - 严格按“输出协议”章节的模板输出 - 没有发现问题的文件也要在报告中列出标注“已审计未发现风险”这些指令听起来像是给流程管理的但它的作用非常实在它让模型的行为变得可预期。以前让模型审计一个仓库它经常“挑”看起来有问题的文件看一遍就下结论现在有了流程约束它知道要先找入口点再追数据流覆盖率明显提升。3. 把安全知识“翻译”成模型可执行的检查规则流程有了但如果知识库稀烂模型照样给不出专业的判断。我在这一步花了最多时间也是security-audit-skill比普通提示词模板强的最主要原因——我把安全知识从“模型脑内记忆”搬到了“本地可检索文件”而且格式是模型容易引用和执行的。3.1 从 OWASP Top 10 做裁剪映射OWASP Top 10 有十大类但并不是每一类都适合纯静态代码审计。比如“加密失败”这种大类很多判断需要业务上下文比如这是不是身份证号模型容易误报“软件和数据完整性失败”偏供应链层面单文件审计意义不大。我做了取舍保留和静态分析强相关的部分并一一映射到代码层面可观察的信号OWASP 类别代码层信号审计重点注入InjectionSQL 字符串拼接、eval、exec、shell、ORM 的 raw 查询用户输入是否拼接进执行语句身份验证失效硬编码口令、弱哈希、JWT 未校验、会话固定认证逻辑是否可绕过敏感数据暴露明文密码存储、日志打印密钥、前端硬编码 token数据是否在不应出现的地方XML 外部实体XML 解析器未禁用外部实体处理 XML 的配置访问控制失效接口缺少权限校验、IDOR、越权操作是否校验资源归属安全配置错误CORS 设置过宽、debug 模式未关闭、默认凭据框架部署相关配置跨站脚本 XSSinnerHTML 直接渲染用户输入、dangerouslySetInnerHTML输出点是否转义不安全的反序列化pickle.loads、ObjectInputStream、unserialize反序列化输入源是否可信已知漏洞组件依赖版本过旧、存在已知 CVE依赖版本比对日志与监控不足缺少关键操作日志、日志中写入敏感信息可审计性我把这个映射表放到了references/owasp_top10_mapping.md后续让模型在审计时逐项翻阅。相比让它背 OWASP给一份具体的“代码层信号 - 重点”表格判断准确率会高很多。3.2 危险函数表的整理方法第二份关键知识库是references/dangerous_functions.md。原理很简单静态审计的核心就是找“入口点 - 危险函数”的路径。把危险函数列得越全模型的查全率越高。以 Python/Node.js/Java 为例我分门别类整理了一批# 危险函数与汇点Sink对照 ## Python - 命令执行: os.system, subprocess.call, subprocess.Popen, eval, exec - SQL 操作: execute, executemany, raw()Django ORM - 反序列化: pickle.loads, yaml.load(不带 Loader), joblib.load - 文件操作: open, os.remove, shutil.rmtree - 模板渲染: render_template_string, Template(..., autoescapeFalse) ## Node.js - 命令执行: child_process.exec, spawn(不带 shell 参数), eval - SQL 操作: query, pool.query - 反序列化: unserialize, node-serialize - 路径操作: path.join(拼接用户输入), fs.readFile - 模板渲染: dangerouslySetInnerHTML, ejs.render, pug.render ## Java - 命令执行: Runtime.exec, ProcessBuilder - SQL 操作: Statement.execute, createStatement无 PreparedStatement - 反序列化: ObjectInputStream.readObject, XMLDecoder - 文件操作: new File(用户可控路径) - 表达式注入: SpelExpressionParser, ELProcessor.eval关键在于危险函数不是列得越多越好而是要分类关联到对应的漏洞类型。我最初只是列了一堆函数名模型确实能找到这些调用点但它不知道这些调用点代表什么漏洞、影响多大、怎么修复。后来我在每个函数后面标注了对应的漏洞类型和审计要领效果立刻不一样。比如yaml.load在 PyYAML 5.1 以下不仅是不安全的反序列化还可能导致 RCE修复方式是改用yaml.safe_load——“能够直接给修复方案”才是审计报告的价值。3.3 风险定级表让报告有轻重缓急安全审计报告如果没有风险等级业务方根本不知道该先修哪个。我在references/risk_rating.md里给了一个简化版的风险定级标准它参考了 CVSS 的思路但更偏静态代码审查场景# 风险定级标准 ## P0 严重 - 无需认证即可触发的远程代码执行 / SQL 注入 - 硬编码的密钥/口令出现在仓库并可能已对外暴露 - 认证逻辑存在可绕过的后门 - 修复建议: 立即修复禁止带着该问题发布 ## P1 高危 - 登录后才可触发的 SQL 注入 / 命令注入 - 越权访问IDOR可读取他人数据 - 敏感数据在日志或响应中明文暴露 - 已知 CVE 的严重漏洞CVSS 9.0 ## P2 中危 - 存储型 / 反射型 XSS - CORS 配置过宽允许任意来源 - 密码存储使用 MD5/SHA1 等弱哈希 - 缺少速率限制的登录接口 ## P3 低危 - 缺少安全响应头 - 文件上传未校验文件类型 - 注释中残留内部信息 / 临时调试代码 ## 定级原则 - 同时满足多个条件时按最高级别处理 - 如果漏洞危害依赖特定前提条件需在报告中明确说明触发前提这套定级表让模型的输出有了“业务语言”。不要小看这一点——安全团队拿到报告后可以直接按 P0/P1/P2/P3 分配修复优先级和截止时间而不是逐条去解读“这里有个问题”。4. 从“能审”到“审得稳”落地工作流与实测案例理论知识说得再多不如跑一个真实案例看看。我在做这个 skill 的过程中最兴奋的时刻不是 SKILL.md 写完成的那一刻而是第一次用它在一个真实项目里抓到俩让开发脸都绿了的问题时——那个项目之前还被某在线扫描工具测过。4.1 三种使用姿势使用方式上我主要用三种分别应对不同的场景。第一种是单文件审计适合快速 review 一个核心模块。直接把代码贴给 agent在对话里指定请使用 security-audit skill 审计以下代码它会自动加载 skill 并按流程输出。这种方式最快适合日常 merge request 前自查。第二种是仓库级审计适合对新接手项目做摸底。需要先把仓库里需要审计的文件列表整理出来再分批次喂给模型。这里有个非常关键的实操经验不要一次性把整个仓库都丢进去。上下文窗口有限模型在长上下文里的注意力会逐渐衰减后面代码中的问题往往会被忽略。我的做法是写一个简单的脚本先把文件列出来排除掉测试代码、静态资源、lock 文件然后按依赖关系排序分批审计。每一批审完后让模型输出“该批次摘要 发现的问题清单”最后再汇总一次。第三种是脚本辅助式审计。某些检查不适合让模型用“肉眼看代码”来做比如扫描硬编码密钥这种低语义但高精度的任务。我写了scripts/secret_scan.py先用正则把疑似密钥的行筛出来再让模型针对这些行做上下文研判效率高很多误报也少。这个思路叫“先机器粗筛再模型精查”。secret_scan.py的核心逻辑大致是这样import re import sys from pathlib import Path # 简化版的正则规则生产环境建议扩展更多模式 PATTERNS [ (r(?i)(api[_-]?key|secret|token|password|passwd|pwd)\s*[:]\s*[\][^\]{8,}[\], 疑似硬编码密钥), (r(?i)-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----, 疑似私钥), (r(?i)ACCT[:][\][A-Za-z0-9/]{16,}, 疑似云厂商访问凭证), ] def scan_file(path: Path): hits [] for lineno, line in enumerate(path.read_text(errorsignore).splitlines(), 1): for pattern, label in PATTERNS: if re.search(pattern, line): hits.append((lineno, label, line.strip()[:120])) return hits if __name__ __main__: root Path(sys.argv[1]) for p in root.rglob(*): if p.suffix in {.py, .js, .ts, .java, .go, .php, .env}: for hit in scan_file(p): print(f{p}:{hit[0]} [{hit[1]}] {hit[2]})4.2 一个实测样本FastAPI 登录接口的审计结果用一个简化后的 FastAPI 登录接口片段来演示这个 skill 的实际输出能力from fastapi import FastAPI, Request import sqlite3 import jwt app FastAPI(debugTrue) SECRET_KEY myhardcodedsecret123 app.post(/login) async def login(request: Request): data await request.json() username data[username] password data[password] conn sqlite3.connect(app.db) cur conn.cursor() query fSELECT * FROM users WHERE username {username} AND password {password} cur.execute(query) user cur.fetchone() if user: token jwt.encode({user: username}, SECRET_KEY, algorithmHS256) return {token: token} return {msg: login failed}把这个代码喂给挂了security-audit-skill的模型后它输出的报告片段是## 安全问题清单 ### [P0] SQL 注入 - app.py:16 - 位置: query fSELECT * FROM users WHERE username {username} ... - 风险: username / password 参数来自请求体直接拼接进 SQL 查询 攻击者可通过构造 OR 11 -- 绕过认证获取任意用户身份。 - 修复建议: 使用 sqlite3 的参数化查询 cur.execute(SELECT * FROM users WHERE username? AND password?, (username, password)) - 验证方法: 修复后尝试传入 OR 11 --应返回“无匹配记录”。 ### [P0] 硬编码 JWT 密钥 - app.py:8 - 位置: SECRET_KEY myhardcodedsecret123 - 风险: 密钥硬编码在源码中。攻击者拿到源码即可伪造任意用户的 JWT。 - 修复建议: 从环境变量读取密钥 import os; SECRET_KEY os.environ[JWT_SECRET_KEY] - 验证方法: 确认代码库中不再存在明文密钥且服务器环境变量已配置。 ### [P1] 明文密码认证 - app.py:15 - 位置: password data[password] 与数据库中的密码直接比对 - 风险: 数据库存储的密码若为明文一旦数据泄露即为脱库级事故。 - 修复建议: 使用 bcrypt / argon2 对密码做加盐哈希存储哈希值而不是明文。 - 验证方法: 查看数据库中密码字段应无法看到原始密码。 ### [P2] CORS / 调试配置风险 - app.py:5 - 位置: app FastAPI(debugTrue) - 风险: debugTrue 在生产环境会返回堆栈信息可能泄露路径、依赖版本等敏感信息。 - 修复建议: 生产环境将 debug 设为 False并通过环境变量控制。这个例子让我比较满意的是它给出的修复建议不是“不要用 SQL 拼接”这种废话而是具体的参数化查询代码验证方法也给了“你怎么确认这个问题修好了”在安全工作中往往比“问题是什么”更重要。注意SQL 注入这类问题在真实审计中模型查得比较准但 XSS、逻辑越权这类需要业务理解的漏洞仍然需要人工复核。把 skill 当作“自动化高级辅助”而不是“替代人工审计”才是正确预期。4.3 实测效果与误报处理我用这个 skill 跑了 10 个内部测试项目代码量从 500 行到 3 万行不等简单统计了效果指标数据有效检出问题总数46 个P0/P1 问题12 个占比 26%误报数经人工复核确认非漏洞17 个误报主要集中在加密类误判、业务逻辑误判、配置类过度解读误报率不算低但性价依然很高——因为 12 个 P0/P1 里有 4 个是之前扫描工具没发现的逻辑问题。我的经验是模型在“找可疑点”这件事上很强它的弱点是“判断一个可疑点是否真的可利用”。所以在 skill 的指令里我要求模型对每个风险点必须写明“触发前提”这样人工复核时能快速判断是否需要关注整体流程反而比全人工审计快得多。另外一个大仓库的实操建议审仓库时先跳过测试代码。测试代码里一般会有大量 mock 和临时数据经常产生密码、token 之类的误报同时也浪费上下文窗口。我第一版 skill 没写这条规则结果在测试目录里翻出一堆假密钥尴尬极了。后来在 SKILL.md 里加了“审计范围排除测试代码除非用户明确要求”误报率立刻降了一截。5. 迭代维护这个 skill 的几条经验写一个 skill 不是一锤子买卖用一段时间就会发现各种要改的地方。我现在大概每两到三周会更新一次security-audit-skill更新的来源主要有三个团队里新出现的漏洞类型、模型在实测中反复误报的点、以及安全社区里新公开的典型攻击样本。5.1 把误报当规则问题来修刚开始使用时模型经常把“日志中打印了用户 email”上报为“敏感信息泄露”。从技术上来说这确实是泄露但在实际业务里日志记录用户邮箱是非常普遍的行为很多合规标准还要求记录操作日志。上报之后根本没人改。后来我在secrets_patterns.md里明确了“哪些字段属于必须脱敏的高敏感数据密码、token、身份证号哪些属于业务常规字段”并在检查规则里加了“仅当同时包含密码/token 级别数据时才算 P1否则标注为 P3 建议项”。这一条规则就把这类误报压掉了大半。所以我的建议是遇到模型的“错误判断”先不要急着否定模型而是看你的规则是不是没有写清楚。skill 的优势就在于它是可以被迭代的——裸提示词里跟模型说“下次注意”大概率下次就忘了但在 skill 里写死的规则每次都会被执行。5.2 用 examples 校准输出质量examples/report_sample.md这个文件的作用可能被很多人忽略它其实是“少样本学习”的实现方式。模型看到一份标准报告是什么样的它输出的结构天然会更接近那个格式。我第一次没放 example 的时候模型给的风险等级是“High/Medium/Low”混着英文放了一份 P0-P3 的中文示例后它输出基本就稳定了。如果你要做的 skill 对应的是其他领域这个思路同样适用给一个你满意的输出样本比写一百句“请注意格式”都管用。5.3 skill 不是越复杂越好我在第一版 SKILL.md 里写了很多规则包括“在审计前先列出 20 个入口点”“每个漏洞必须给出 CWE 编号”等等结果用起来特别别扭。入口点列出 20 个有一半是重复的CWE 编号模型经常编错。后来我删掉了一半的“严格指令”只保留流程骨架和关键规则输出质量反而提升。原因不难理解模型的上下文窗口和“注意力预算”是有限的规则太多会挤压它真正做推理的空间。skill 里的每一条指令都值得用“删掉它会影响结果吗”来审视一遍。5.4 后续可以扩展的方向这个 skill 目前是我个人和团队内部在用后续有几个自然的扩展方向和 CI 集成让每次 MR 自动触发代码审计。不过基于模型的成本不建议全量扫描最好只审 diff 中变更到的文件。配合代码检索能力做大仓审计思路是先用检索把相关的代码片段拉到上下文里再让模型追踪更长的数据流在保持精度的同时尽量覆盖整个仓库。针对不同编程语言做更细的规则集现在的是通用规则对具体框架如 Spring、Django、Gin的审计还不够细但通用的安全审计已经覆盖了绝大多数共性问题。如果你只是想给日常开发加一层安全兜底不用急着把体系做这么大。先做一个最简可用版一个 SKILL.md、一个检查清单、一份输出模板跑起来之后再逐步迭代这比一开始就想着“全平台通用”靠谱得多。最后分享一点个人体会。把security-audit-skill从想法做成现成的过程中我最大的收获不是模型审计出了多少漏洞而是理解了怎么把一个专业领域的工作方法有效地“教”给模型。技巧性的东西每个领域都不一样但内核是一致的把隐性经验显性化把显性经验规则化把规则落进流程里。这件事做好之后skill 就不再是一个提示词模板了它真的变成了团队的技能资产。