1. 从零拆解 security-audit-skill:一个给编码助手装上安全雷达的实战项目
第一次看到security-audit-skill这个项目名,我的直觉是:这大概率是给某个 coding agent 框架写的一个技能插件,专门用来做代码安全审计。后来翻了一圈资料,确认了判断——它本质上是一个可挂载到编码助手上的安全审计能力模块,让 AI 在写代码、改代码、审代码的过程中,顺手把安全漏洞也扫一遍。
这件事的价值在哪儿?你想想,现在用 coding agent 写代码的人越来越多,但绝大多数 agent 的默认行为是“能跑就行”。它不会主动告诉你:这段 SQL 拼接有注入风险、这个接口没做鉴权、这个依赖版本有已知 CVE。security-audit-skill要解决的就是这个断层——把安全审计从“事后专门找人做”变成“编码过程中自动触发”。
这篇文章适合三类人看:一是正在给自研 coding agent 扩展能力的开发者;二是想了解 AI 辅助安全审计怎么落地的一线工程师;三是团队里负责代码质量、想把安全左移做到位的技术负责人。我会从设计思路、核心机制、实操接入、问题排查几个维度,把这个项目拆透,尽量让你看完就能动手复现。
2. 为什么要在 coding agent 里塞一个安全审计技能
2.1 传统安全审计的痛点在哪
做过代码审计的人都知道,传统模式基本是这么走的:开发写完代码 → 提交 PR → 安全团队或资深工程师人工 review → 发现问题打回 → 开发修改 → 再 review。这个流程有两个致命问题。
第一是滞后。漏洞在代码写完几天甚至几周后才被发现,这时候开发脑子里早就不记得当时的上下文了,修起来成本极高。第二是覆盖不全。人工 review 受限于精力和经验,一个几百行的 PR 能认真看完就不错了,更别说跨文件追踪数据流。我见过太多团队的安全 review 最后变成“看看有没有硬编码密码”这种表面功夫。
还有一个隐性成本:安全团队和开发团队之间的沟通摩擦。安全说“这里有注入”,开发说“这是内部接口不存在注入”,来回扯皮。根因是审计发生在编码之后,双方不在同一个上下文里。
2.2 coding agent 天然适合承载安全审计
coding agent 和传统静态扫描工具最大的区别是:它有上下文。它知道你正在写什么功能、这个函数的调用方是谁、数据从哪来要到哪去。传统 SAST 工具只能做模式匹配,看到execute(就报,误报率高得吓人。而 agent 可以结合语义判断:这个参数是不是用户可控的?有没有经过校验?
security-audit-skill的设计思路就是利用这一点。它不是独立跑一个扫描器,而是作为 agent 的一个“技能”存在。当 agent 在写代码或读代码时,这个技能被触发,agent 用自己已经掌握的上下文去做安全判断。这比冷启动的扫描器精准得多。
另外,agent 可以在生成代码的同时做审计。比如你让它写一个用户查询接口,它在生成 SQL 语句的那一刻就能意识到“这里用了字符串拼接,应该改成参数化查询”,然后直接生成安全的版本。这是真正的安全左移——左移到代码还没写出来的时候。
2.3 这个技能的核心能力边界
需要说清楚的是,security-audit-skill不是万能的。它擅长的是:
- 注入类漏洞:SQL 注入、命令注入、模板注入、XSS
- 鉴权与访问控制:接口是否缺少权限校验、越权风险
- 敏感信息处理:硬编码密钥、日志泄露敏感数据、不安全的加密用法
- 依赖与配置:已知漏洞依赖、不安全的默认配置
- 数据流追踪:从用户输入到危险函数的完整路径
它不擅长或做不好的:
- 复杂的业务逻辑漏洞(需要理解业务规则)
- 竞态条件、时序攻击这类需要运行时分析的问题
- 加密协议层面的深度缺陷
理解这个边界很重要,否则你会对它产生不切实际的期望,然后在某个它没查出来的漏洞上翻车。
3. security-audit-skill 的核心机制拆解
3.1 技能是怎么挂载到 agent 上的
从项目结构来看,security-audit-skill遵循的是主流 coding agent 框架的 skill 规范。通常一个 skill 包含几个核心部分:一个描述文件(声明技能名称、触发条件、能力范围)、一个或多个提示词模板(告诉 agent 在不同场景下怎么执行审计)、以及可选的辅助脚本或规则库。
描述文件的作用是让 agent 知道“我什么时候该用这个技能”。比如当用户说“帮我 review 这段代码”或者 agent 自己检测到正在生成涉及数据库操作的代码时,就触发安全审计流程。这个触发机制设计得好不好,直接决定了技能是“有用”还是“烦人”。触发太频繁,agent 每写一行都唠叨安全问题,体验极差;触发太少,又起不到作用。
我看到的实现里,触发条件通常分两类:显式触发(用户主动要求安全审计)和隐式触发(agent 根据代码特征自动判断)。隐式触发的规则一般包括:检测到危险函数调用、检测到用户输入处理、检测到鉴权相关代码、检测到加密操作等。
3.2 审计提示词的设计逻辑
这是整个技能最核心的部分。安全审计的质量,八成取决于提示词写得好不好。一个粗糙的提示词是“检查这段代码有没有安全问题”,agent 大概率给你返回一堆泛泛而谈的东西。好的提示词需要做到几件事。
第一,给出明确的审计维度清单。不是让 agent 自由发挥,而是告诉它按注入、鉴权、敏感信息、依赖、配置这几个维度逐一检查。每个维度下再给出具体的检查点。比如注入维度下要检查:是否使用参数化查询、是否对用户输入做了转义、是否使用了安全的模板引擎。
第二,要求 agent 追踪数据流。光看一个函数是看不出问题的。提示词里要明确要求 agent 从入口点(如 HTTP 请求参数)开始,追踪数据经过的每一个函数,直到它被使用的地方。这条路径上任何一环缺少校验,都是潜在漏洞。
第三,强制给出证据和修复建议。不能只说“这里有风险”,要指出具体哪一行、为什么有风险、攻击者可能怎么利用、应该怎么改。这个要求会倒逼 agent 做更深入的分析,而不是浅尝辄止。
第四,区分严重等级。不是所有问题都一样紧急。提示词里要定义清楚什么算高危(可直接利用的注入、缺失的鉴权)、什么算中危(信息泄露、弱加密)、什么算低危(代码规范类)。这样输出结果才有可操作性。
3.3 规则库与知识注入
纯靠提示词让 agent 判断安全问题,覆盖面有限。security-audit-skill通常会配套一个规则库,里面沉淀了常见漏洞模式、危险函数清单、安全编码规范等。这个规则库的作用是给 agent 提供“检查清单”,避免遗漏。
规则库的形态可以是多种多样的。简单的是 YAML 或 JSON 格式的规则列表,每条规则包含:规则 ID、描述、匹配模式、严重等级、修复建议。复杂一点的会包含数据流分析规则,定义 source(用户输入点)和 sink(危险操作点)以及 sanitizer(净化函数)。
我个人的经验是,规则库不要贪多。一开始就堆几百条规则,维护成本极高,而且很多规则在特定技术栈下根本不适用。更好的做法是:先覆盖团队技术栈里最常用的框架和最常见的漏洞类型,跑通之后再逐步扩展。
3.4 与 agent 主循环的集成方式
技能和 agent 主循环的集成方式,决定了审计是“顺手做”还是“专门做”。有两种模式。
内联模式:agent 在生成或修改代码的同一个回合里,顺便执行安全审计。好处是上下文完全一致,审计结果直接反馈到代码生成中,可以立即修正。坏处是会增加单次响应的长度和耗时。
分离模式:agent 完成代码生成后,单独触发一次审计流程。好处是审计可以更深入、更系统,不受生成过程的干扰。坏处是上下文可能已经部分丢失,而且需要额外的交互轮次。
从实际效果看,内联模式更适合“预防”,分离模式更适合“检查”。成熟的实现通常会结合两者:生成时做轻量级内联检查(快速拦截明显问题),生成后做一次完整的分离审计(深度排查)。
4. 实操接入:把安全审计技能跑起来
4.1 环境准备与依赖确认
在接入之前,先确认你的 coding agent 框架支持 skill 扩展机制。目前主流的几个框架(如 Claude Code、Cursor 的 agent 模式、开源的 Continue 等)对 skill 的支持方式不太一样。你需要先搞清楚你的框架是加载本地 skill 目录,还是通过配置文件注册,还是通过 API 动态注入。
以本地 skill 目录加载为例,通常的目录结构是这样的:
skills/ security-audit-skill/ skill.yaml # 技能描述文件 prompts/ audit-full.md # 完整审计提示词 audit-inline.md # 内联快速检查提示词 rules/ injection.yaml # 注入类规则 auth.yaml # 鉴权类规则 secrets.yaml # 敏感信息规则 scripts/ scan_deps.py # 依赖扫描辅助脚本skill.yaml是入口,里面声明技能名称、版本、触发条件、引用的提示词和规则文件。不同框架的 schema 有差异,但核心字段大同小异。
4.2 技能描述文件的编写要点
描述文件写得好不好,直接影响技能能不能被正确触发。以下是一个参考结构:
name: security-audit-skill version: 1.0.0 description: 对代码进行安全审计,覆盖注入、鉴权、敏感信息、依赖等维度 triggers: explicit: - "安全审计" - "security audit" - "检查安全问题" implicit: - pattern: "execute\\(|query\\(|raw\\(" context: "database" - pattern: "eval\\(|exec\\(|system\\(" context: "command" - pattern: "password|secret|token|api_key" context: "hardcoded" prompts: full: prompts/audit-full.md inline: prompts/audit-inline.md rules: - rules/injection.yaml - rules/auth.yaml - rules/secrets.yaml这里的关键是implicit触发条件的设计。模式匹配要精准,不能太宽泛。比如password这个词在变量名里出现太常见了,如果只要出现就触发审计,会非常吵。更好的做法是结合上下文判断,比如检测到password = "xxx"这种字面量赋值才触发。
4.3 审计提示词的完整模板
下面是我根据项目思路整理的一份完整审计提示词模板,你可以直接拿去改:
你是一名资深应用安全工程师,正在对以下代码进行安全审计。 ## 审计范围 - 文件:{{file_path}} - 变更内容:{{diff}} - 技术栈:{{tech_stack}} ## 审计维度 请按以下维度逐一检查,每个维度都要给出结论(有问题/无问题/不适用): ### 1. 注入类 - SQL 注入:是否使用参数化查询?是否有字符串拼接 SQL? - 命令注入:是否调用了系统命令?参数是否用户可控? - 模板注入:模板渲染是否使用了用户输入作为模板内容? - XSS:输出到前端的数据是否做了转义? ### 2. 鉴权与访问控制 - 每个接口是否有权限校验? - 是否存在越权访问风险(水平/垂直)? - 敏感操作是否有二次确认? ### 3. 敏感信息 - 是否有硬编码的密钥、密码、token? - 日志中是否输出了敏感数据? - 加密算法是否安全(禁用 MD5、SHA1 做密码哈希)? ### 4. 依赖与配置 - 新增依赖是否有已知漏洞? - 配置项是否安全(如 debug 模式、CORS 配置)? ## 输出格式 对每个发现的问题,按以下格式输出: - **严重等级**:高危/中危/低危 - **位置**:文件:行号 - **问题描述**:具体是什么问题 - **攻击场景**:攻击者可能如何利用 - **修复建议**:具体的修改方案,最好给出代码示例 如果没有发现问题,明确说明"未发现安全问题"。这份提示词的核心是结构化。它不给 agent 自由发挥的空间,而是逼着它按维度逐一检查。实测下来,结构化提示词的漏报率比开放式提示词低很多。
4.4 规则库的落地写法
规则库用 YAML 写比较直观,以下是一个注入类规则的示例:
rules: - id: SQL_INJECTION_001 name: 字符串拼接 SQL severity: high description: 检测到使用字符串拼接构造 SQL 语句 patterns: - "execute\\(.*\\+.*\\)" - "query\\(.*\\+.*\\)" - "raw\\(.*\\+.*\\)" languages: - python - javascript - java fix: 使用参数化查询,将用户输入作为参数传入,而非拼接进 SQL 字符串 references: - "CWE-89" - id: CMD_INJECTION_001 name: 命令注入 severity: high description: 检测到将用户输入传入系统命令执行函数 patterns: - "os\\.system\\(.*\\)" - "subprocess\\.(call|run|Popen)\\(.*shell=True.*\\)" - "exec\\(.*\\)" languages: - python fix: 避免使用 shell=True,使用参数列表形式传参,或对输入做严格白名单校验 references: - "CWE-78"规则库的维护是个长期活。我的建议是:每次审计发现新类型的问题,就把它沉淀成一条规则。这样规则库会随着项目使用越来越完善。
4.5 依赖扫描脚本的接入
依赖漏洞是很容易被忽略的一块。security-audit-skill通常会带一个辅助脚本,用来扫描项目依赖的已知漏洞。Python 项目可以用pip-audit,Node 项目可以用npm audit,通用方案可以用osv-scanner。
脚本的核心逻辑是:读取依赖清单文件(requirements.txt、package.json、go.mod 等),调用漏洞数据库 API,返回有问题的依赖列表。这个脚本的输出会被注入到审计提示词里,作为依赖维度的检查依据。
import subprocess import json def scan_dependencies(project_path): """扫描项目依赖的已知漏洞""" result = subprocess.run( ["osv-scanner", "--format", "json", "--recursive", project_path], capture_output=True, text=True ) if result.returncode != 0: return {"error": result.stderr} return json.loads(result.stdout)这个脚本不需要多复杂,关键是能跑通、能拿到结构化结果。拿到结果后,在提示词里告诉 agent:“以下是依赖扫描结果,请结合这些信息评估依赖风险。”
5. 审计效果验证与常见问题排查
5.1 怎么验证审计技能真的有效
技能接入之后,不能光看它“有没有输出”,要验证它“输出得对不对”。我通常用三种方式验证。
第一种:用已知漏洞的代码做回归测试。准备一批包含典型漏洞的代码样本(SQL 注入、XSS、硬编码密钥等),跑一遍审计,看能不能全部检出。这个测试集要持续维护,每次改提示词或规则库都跑一遍,防止改出回归。
第二种:用干净代码测误报率。拿一批确认没有安全问题的代码跑审计,看会不会报出一堆假阳性。误报率太高的话,开发很快就会忽略审计结果,技能就废了。
第三种:对比人工审计结果。找几段真实项目代码,先让资深工程师人工审计一遍,记录发现的问题,再让技能审计一遍,对比两者的差异。技能漏掉的问题,分析原因并补充规则;技能多报的问题,分析是提示词太激进还是规则太宽泛。
5.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 技能完全不触发 | 描述文件格式错误或路径不对 | 检查 skill.yaml 语法和框架日志 | 修正 YAML 缩进,确认 skill 目录被正确加载 |
| 触发太频繁,干扰编码 | 隐式触发条件太宽泛 | 检查 implicit triggers 的模式匹配 | 收窄匹配模式,增加上下文条件 |
| 审计结果泛泛而谈 | 提示词不够结构化 | 检查提示词是否有明确维度清单 | 改用结构化提示词模板,强制逐维度输出 |
| 漏报明显漏洞 | 规则库覆盖不足或提示词遗漏维度 | 用已知漏洞样本测试,定位漏报类型 | 补充对应规则,或在提示词中增加检查点 |
| 误报率高 | 规则模式太宽泛,缺少上下文判断 | 分析误报样本的共同特征 | 增加白名单机制,或要求 agent 追踪数据流后再下结论 |
| 依赖扫描失败 | 扫描工具未安装或版本不兼容 | 手动执行扫描命令看报错 | 安装对应工具,确认版本,检查网络访问 |
| 审计耗时过长 | 全量审计范围太大 | 检查是否每次都对整个项目审计 | 改为只审计变更部分(diff),全量审计定期执行 |
5.3 几个我踩过的坑
坑一:提示词里没要求给行号。早期版本的提示词只说“指出问题位置”,agent 经常只给个函数名,开发得自己去找。后来强制要求输出文件:行号,可操作性大幅提升。
坑二:规则库和提示词重复。一开始我在规则库里写了很多检查项,提示词里又写了一遍,结果 agent 收到重复指令,输出变得混乱。后来明确分工:规则库负责“模式匹配”,提示词负责“语义分析”,两者互补但不重复。
坑三:忽略了技术栈适配。通用规则在特定框架下可能完全不适用。比如在用了 ORM 的项目里,execute方法可能已经被 ORM 封装成安全的了,但规则还是会报。解决办法是在规则里加languages和frameworks字段,做技术栈过滤。
坑四:审计结果没有优先级。一开始所有问题都平铺直叙地列出来,开发不知道先修哪个。后来强制要求标注严重等级,并且高危问题必须给出攻击场景,开发一看就明白为什么要修。
5.4 性能与体验的平衡
安全审计技能最大的体验问题是“慢”和“吵”。慢是因为深度审计需要 agent 做大量推理,吵是因为触发太频繁。
针对慢的问题,我的做法是分级审计:内联检查只做快速模式匹配(毫秒级),完整审计在代码生成后单独触发(秒级到十秒级)。用户感知到的是“写代码时偶尔有提示,写完代码后有一份完整报告”,而不是“每写一行都卡一下”。
针对吵的问题,核心是只在真正有风险时出声。这需要不断调优触发条件和规则精度。我的经验是,误报率控制在 10% 以下,开发才愿意看审计结果;超过 30%,基本就被无视了。
6. 进阶玩法:让审计技能越用越聪明
6.1 从审计结果反哺规则库
每次审计发现的问题,都是一条潜在的规则。我养成了一个习惯:每周花半小时回顾本周的审计结果,把新出现的漏洞模式沉淀成规则。这样规则库会随着项目使用自然生长,而不是一开始就追求大而全。
具体做法是:审计报告里每个“高危”问题,都问自己一句“这个模式能不能写成规则”。能写就写,不能写就记录在案,作为提示词优化的输入。
6.2 结合项目历史做上下文增强
同一个漏洞模式,在不同项目里的风险等级可能完全不同。比如一个内部管理后台的 SQL 注入,和一个面向公网的用户接口的 SQL 注入,严重性差着量级。security-audit-skill可以通过读取项目配置(如是否有公网入口、是否处理敏感数据)来动态调整风险评级。
这个增强需要 agent 能访问项目的元信息。实现方式可以是在提示词里注入项目上下文,或者让 agent 主动读取项目里的配置文件。
6.3 团队协作场景下的审计技能
在团队里用这个技能,有几个额外考虑。一是审计标准要统一,不能每个人用自己的提示词版本。二是审计结果要能沉淀,不能看完就丢。三是误报反馈要有渠道,开发发现误报后能快速反馈,规则维护者能快速修正。
我的做法是把审计技能和代码审查流程绑定:PR 创建时自动触发审计,审计结果作为评论附在 PR 上。开发修复后重新触发,直到没有高危问题才能合并。这样安全审计就成了流程的一部分,而不是一个可有可无的附加项。
6.4 持续演进的思路
安全审计技能不是一次接入就完事的。攻击手法在变,技术栈在变,项目在变,技能也得跟着变。我建议每季度做一次技能回顾:看看这季度漏了哪些漏洞、误报了多少、规则库更新了多少条。用数据驱动技能的迭代,而不是凭感觉。
另外,多关注安全社区披露的新漏洞模式,及时把新的检查点加进提示词和规则库。比如近几年供应链攻击频发,依赖审计的权重就应该相应提高。
这个项目后续还可以往几个方向扩展:一是支持更多语言和框架的规则;二是引入数据流分析的自动化能力,减少对 agent 推理的依赖;三是和 CI/CD 流水线深度集成,做到提交即审计、合并前拦截。不过这些都是后话,先把基础能力跑通、跑稳,再谈扩展。