我真正下决心做security-audit-skill,是在一次线上事故复盘之后。当时我让Codex直接审查一个订单服务的代码,结果它花了二十分钟,给出一份看起来很全面、但实际上漏掉了最关键的越权接口的检查报告。问题不在于模型能力,而在于我给的指令太含糊。后来我把安全审计的流程、规则、工具调用方式全部做成了Agent Skill,再让同一个模型去跑,表现稳定了一个层级。这篇内容就是记录我如何设计、编写、调试一个security-audit-skill,并让它落地到日常开发和CI流程里。如果你正在给Codex、Claude这类AI代理编写专属skill,或者想用AI做代码安全审计,这篇文章应该能帮你少踩不少坑。
1. 项目概览:为什么AI代理需要“安全审计技能”
1.1 直接对话式审计的痛点
很多人的第一反应是:既然大模型会写代码、能读文件,那直接让它“审计一下这个项目”不就行了?我一开始也这么干,但实际用下来会碰到几个非常具体的问题。
第一是检查范围不收敛。模型可能只读了一个目录,或者被某个文件里的细节带跑偏,压根没有系统性地覆盖整个仓库。安全审计要求的是“资产面全覆盖”,漏一个配置文件可能就是事故,但大模型默认不会像专业工具那样按清单执行。
第二是判断标准不可控。同一个代码片段,你可能希望它按你的团队规范判断,而不是按模型的通用知识判断。比如你们内部规定数据库连接不上传、密钥必须走密钥管理服务,这种组织级的规则,靠聊天式prompt很难稳定生效。
第三是没有可复现的执行流程。聊天式审计每次的输出结构、执行时长、覆盖深度都不一样,你很难把它接入自动化流水线,也没法做前后对比。
把审计经验固化成security-audit-skill,本质上就是把“人怎么审代码”的作业指导书写成模型能稳定执行的指令包,再挂上真正的扫描工具。大模型负责调度、解释和补上下文,工具负责确定性检测,各干各擅长的事。
1.2 Skill到底是什么,它和普通Prompt有什么不同
这里先说清楚一个概念。在Codex、Claude这类助手生态里,Skill并不神秘,通常就是一个约定格式的目录,核心是SKILL.md,外加若干辅助脚本和示例文件。模型会在任务开始前读取这个文件,从中获得完整的执行规程。
它和普通Prompt的区别在于:
- 普通Prompt是一次性的,临时写在对话里,换一次会话就失效。
- Skill是可复用的,放在项目的
.agents/skills/或者用户级配置目录下,Agent能自动发现并按需加载。 - 普通Prompt只有“文字指导”,Skill可以捆绑脚本和规则文件,让模型调工具而不是空想。
- Skill支持维护和版本管理,你可以像维护代码一样去改进它。
给Agent一个security-audit-skill,就好比给刚入职的安全工程师发一本《安全审计SOP手册》,里面写着先做什么、后做什么、遇到什么情况调用哪个工具、报告必须怎么写。模型拿着这份手册去工作,产出质量自然比“临场发挥”高得多。
1.3 适用对象与典型场景
这个东西适合谁用?我列几个在我工作中真实出现的场景:
- 个人开发者或小团队,希望在PR合并前让AI助手自动检查敏感信息泄露和明显的注入点,而不想搭一整套重量级安全平台。
- 安全工程师,想把自己沉淀的检测规则快速变成团队可复用的流程,减少重复解释工作。
- AI应用工程师,正在研究Agent Skill机制,需要一个完整的参考实现作为学习样例。
- 想给私有仓库做例行巡检的团队,希望每晚自动跑一次基础安全扫描,生成报告供第二天排查。
这个项目的定位不是替代商业级安全产品,而是把“AI代码助手的审计能力”标准化。它解决的核心问题是:让同一个模型、同一个流程,在不同项目里输出稳定可用的审计结果。
2. Skill设计思路:先让Agent“知道怎么审”再动手
2.1 拆解安全审计的底层能力
写Skill之前,我没有急着写代码,而是先把“安全审计”拆成了几个可以独立执行的能力项。这个步骤非常关键,它可以避免把SKILL.md写成一团浆糊。参考我平时人工做审计的方法,拆成了四层:
| 能力层 | 要发现什么 | 典型工具 | 输出重点是啥 |
|---|---|---|---|
| 敏感信息层 | 硬编码密钥、Token、数据库口令、私钥 | gitleaks、trufflehog、正则规则 | 文件路径、行号、密钥类型 |
| 依赖风险层 | 直接和传递依赖的已知漏洞 | npm audit、pip-audit、osv-scanner | 漏洞编号、严重级别、修复版本 |
| 代码缺陷层 | SQL注入、命令注入、不安全反序列化、路径穿越 | semgrep、CodeQL、自定义规则 | 缺陷类型、触发位置、调用链 |
| 配置与权限层 | 过度开放权限、危险CRON、错误security group配置 | semgrep、checkov、云厂商策略工具 | 配置项、责任方、修复建议 |
拆完之后,我就能明确告诉模型:Agent负责调用这些工具、解析输出、结合代码上下文判断是否为真实风险;工具负责精确匹配,避免模型靠记忆瞎编漏洞。这个分工也解决了很多人担心的“模型幻觉”问题——凡是涉及精确扫描的部分,都让工具先出结果,模型只做标记和解释。
2.2 把人工经验转成可执行步骤
拆完能力层之后,我把人工审计的顺序也固化下来。为什么顺序很重要?因为有些步骤之间存在依赖关系。
比如,我先跑敏感信息扫描,再看依赖漏洞,最后看代码缺陷。一旦发现硬编码密钥,我就知道这个项目可能存在“配置管理不规范”的问题,在最后输出报告时会更关注整体风险;反过来如果先看代码缺陷,很容易陷进某一段业务逻辑,忽略全局风险。
我的执行顺序是:
- 先收集项目基础信息,包括语言、框架、包管理器、目录结构。
- 执行敏感信息扫描,快速定位高风险泄漏。
- 执行依赖漏洞扫描,区分生产依赖和开发依赖。
- 执行代码缺陷扫描,按语言和框架选择规则集。
- 检查敏感配置文件、权限和部署配置。
- 汇总所有发现,输出按严重级别排序的报告。
把这套顺序写进SKILL.md之后,模型在每次执行时都会按照这个节奏推进,而不是想一出是一出。
2.3 为什么选择“指令包+外部工具”而不是纯模型判断
方案设计过程中,我也考虑过“完全靠大模型知识做审计”的方案,就是不让模型调用任何扫描器,纯粹通过阅读代码找出问题。这个方案看起来部署简单,但实测不靠谱。
模型对常见漏洞模式的识别能力不错,但它无法精确计算依赖版本号对应的漏洞编号,也容易漏掉藏在长文件第300行的硬编码密钥。纯靠模型判断,还有一个问题:它给出的“位置”“行号”偶尔是错的,这种报告拿到开发那里,信任度会迅速归零。
所以我最终选定了“指令包+外部工具”的混合架构:
- 确定性高的事情交给gitleaks、semgrep这类工具完成。
- 模型负责理解工具输出、筛选误报、补充调用链分析、生成修复建议。
- 规则匹配结果是证据,模型解释是辅助。
这样做的成本是需要在环境里预装工具,并且写一些胶水脚本,但换来的可靠性远远大于成本。
3. 核心实现:从SKILL.md到可跑通的最小版本
3.1 SKILL.md:Agent的作业指导书
整个Skill的核心文件是SKILL.md。我把它放在项目里的.agents/skills/security-audit/SKILL.md。Codex、Claude等工具在扫描项目时,会自动识别这类结构。
下面是一个精简但能跑通的版本:
--- name: security-audit description: 对代码仓库执行多维度安全审计,包括敏感信息、依赖漏洞、代码缺陷和配置风险检查。适用于代码审查、CI流程和上线前巡检。 --- # security-audit 你是一名安全审计工程师。请按照以下步骤对仓库执行审计,不要跳过任何一步。 ## 输入 - 目标目录:默认是当前工作目录 - 需要审计的范围:可以通过用户指定,例如“只审计src目录” ## 执行步骤 1. 收集项目信息 - 识别语言和框架,读取 package.json、requirements.txt、go.mod 等依赖清单。 - 如果存在多个子项目,列出子项目清单并逐个覆盖。 2. 检查敏感信息 - 在项目根目录执行 `gitleaks detect --source . --no-banner --redact`。 - 同时运行配套脚本中的正则扫描,检测私有IP、AK/SK形态的字段。 - 输出时记录文件路径、行号和匹配到的密钥类型,严禁输出完整密钥值。 3. 检查依赖漏洞 - 根据依赖清单类型调用对应扫描器: - Node.js:`npm audit --json` - Python:`pip-audit` - Go:`govulncheck ./...` - 只保留 severity 为 high 或 critical 的结果,并列出修复版本。 4. 检查代码缺陷 - 运行 `semgrep scan --config auto --json -o semgrep_result.json .` - 重点分析注入、认证缺失、路径穿越、反序列化等规则。 - 对每一条结果,给出是否可能被利用的简要判断。 5. 检查配置与权限 - 查找 Dockerfile、docker-compose.yml、Kubernetes YAML 中是否包含明文环境变量、危险权限、latest镜像标签。 - 检查 `.env` 是否被提交到仓库。 6. 输出审计报告 - 按严重级别排序,输出 markdown 表格或 JSON。 - 每条记录必须包含:问题描述、文件路径、触发位置、证据、修复建议。 - 如果没有发现问题,明确说明“未发现明显风险”,不要编造发现。这份SKILL.md大概有40多行,它给模型划定了清晰的边界。我特意强调了两点:一是“严禁输出完整密钥值”,避免审计报告本身成为泄露源;二是“没有发现就别编造”,这是安全领域的大忌。
3.2 配套脚本:把工具调用封装成统一入口
SKILL.md可以指导模型,但模型在执行时如果自己拼命令,很容易出错。我写了一个Python脚本作为统一入口,把敏感信息扫描、依赖扫描和代码缺陷扫描都封装起来,模型只需要记住一个命令:python scripts/audit.py --source . --output report.json。
脚本核心逻辑如下:
import subprocess import json import sys import os def run_gitleaks(source_dir): result = subprocess.run( ["gitleaks", "detect", "--source", source_dir, "--no-banner", "--redact", "--report-format", "json", "--report-path", "gitleaks.json"], capture_output=True, text=True ) if os.path.exists("gitleaks.json"): with open("gitleaks.json", "r", encoding="utf-8") as fp: return json.load(fp) return [] def run_semgrep(source_dir): result = subprocess.run( ["semgrep", "scan", "--config", "auto", "--json", "-o", "semgrep_result.json", source_dir], capture_output=True, text=True ) if os.path.exists("semgrep_result.json"): with open("semgrep_result.json", "r", encoding="utf-8") as fp: return json.load(fp) return {"results": []} def summarize(raw): items = [] for leak in raw.get("Leaks", []): items.append({ "category": "secret", "rule": leak.get("RuleID"), "file": leak.get("File"), "line": leak.get("StartLine"), "message": "检测到疑似敏感信息,已脱敏", "severity": "high", }) return items if __name__ == "__main__": source = sys.argv[1] if len(sys.argv) > 1 else "." output = sys.argv[2] if len(sys.argv) > 2 else "report.json" leaks = run_gitleaks(source) semgrep_data = run_semgrep(source) report = { "secrets": summarize(leaks), "code_issues": semgrep_data.get("results", []), } with open(output, "w", encoding="utf-8") as fp: json.dump(report, fp, ensure_ascii=False, indent=2) print(json.dumps(report, ensure_ascii=False, indent=2))注意脚本里的两个设计点:
- 所有外部命令都指定了输出文件,避免直接解析stdout时因为编码或格式问题翻车。
- gitleaks使用了
--redact参数,报告里只保留脱敏后的信息,真实密钥不会出现在日志里。
在实际项目中,我会把这个脚本再扩展一层,支持--skip-dep和--severity-filter之类参数。这样Agent可以根据用户需求减少扫描范围,避免每次审计都要等全部工具跑完。
3.3 自定义Semgrep规则:让模型和工具用同一种语言说话
工具自带的规则库覆盖面很广,但企业项目往往有自己特有的坑。比如团队经常误用某个自研加密函数,或者经常在日志里打印敏感字段,这些用通用规则检测不到。所以我为security-audit-skill设计了一个自定义Semgrep规则目录,模型在做代码缺陷检查时会优先加载这些规则。
一个简单的检测“print敏感字段”的规则示例:
rules: - id: log-sensitive-data patterns: - pattern-either: - pattern: print($SECRET) - pattern: logger.info($SECRET) metavariable-regex: metavariable: $SECRET regex: "(?i)(password|token|secret|api[_-]?key)" message: 疑似在日志或输出中打印敏感信息:$SECRET languages: - python - javascript - go severity: WARNING这个规则的价值在于,它把团队的编码规范也变成了可执行检查项。写好之后,放在Skill的rules/目录下,模型执行Semgrep时用--config rules/指定即可。
我建议每个团队都做这一步。通用规则本质上来自“行业的共性教训”,但你们项目的特殊性,只有你们自己最清楚。把特殊性沉淀成规则,才是安全审计技能最有价值的部分。
3.4 与Codex、Claude等Agent的接入方式
Skill写好后,接入方式根据代理工具不同略有差异。以我常用的两种为例:
- Codex:把整个目录放到项目的
.agents/skills/security-audit/下面,或者放到用户级目录,例如~/.codex/skills/security-audit/。这样在任何项目里都能自动识别到。 - Claude Code等工具:通常也是读取项目下的
.claude/skills/或通过插件机制导入。我会同时把Skill目录复制到项目级,确保团队clone后能直接使用。
接入之后,要验证Agent是否真的会加载这个Skill。我最快的验证方法是:在项目里输入“帮我审计当前目录”,然后看Agent是否主动执行了SKILL.md里的步骤,而不是凭空回答。
如果Agent没有加载,优先检查目录名、SKILL.md文件名是否准确,以及是否放进了Agent扫描的顶层目录。很多工具对Skill目录有固定的前缀约定,不能随意改名。
4. 实操过程:在真实项目里跑一次完整审计
4.1 前置环境准备
我这里以Linux/macOS环境为例,所有工具都需要提前安装。依赖扫描工具按项目语言来选,我把常见的都列出来:
| 工具 | 安装方式 | 用途 |
|---|---|---|
| gitleaks | brew install gitleaks或go install github.com/gitleaks/gitleaks/v8@latest | 敏感信息扫描 |
| semgrep | pip install semgrep | 静态代码缺陷扫描 |
| npm | Node环境自带 | npm依赖漏洞检查 |
| pip-audit | pip install pip-audit | Python依赖漏洞检查 |
| govulncheck | go install golang.org/x/vuln/cmd/govulncheck@latest | Go依赖漏洞检查 |
安装完成后,先用一条命令验证工具能正常调用:
gitleaks version semgrep --version我踩过的坑是:gitleaks在某些系统上默认不进入PATH,导致Agent执行时找不到命令。解决办法是在SKILL.md里写上工具的绝对路径,或者把安装目录加入PATH环境变量。Agent的Shell环境和你手动终端的环境未必一致,这一点特别容易被忽略。
4.2 执行一次最小审计并解读结果
我在一个模拟的Node.js项目上跑了一次审计,项目结构大致如下:
demo-app/ ├── package.json ├── src/ │ ├── server.js │ ├── db.js │ └── utils/ │ └── encrypt.js ├── .env └── README.md启动Skill后,Agent首先读取package.json,识别出这是一个Express应用,然后依次执行敏感信息、依赖、代码缺陷扫描。我这里简化一下最终的输出报告:
{ "summary": { "total": 3, "high": 1, "medium": 2 }, "findings": [ { "type": "secret", "severity": "high", "file": ".env", "message": "检测到疑似数据库口令,文件已被提交到仓库", "suggestion": "将密钥迁移到密钥管理服务,删除仓库中的.env,并轮换该口令" }, { "type": "dependency", "severity": "medium", "file": "package.json", "message": "axios 1.2.0 存在已知的的高危漏洞,建议升级到1.6.8以上", "suggestion": "运行 npm install axios@^1.6.8" }, { "type": "code_issue", "severity": "medium", "file": "src/db.js", "message": "拼接SQL存在注入风险,使用参数化查询", "suggestion": "改用 prepared statement" } ] }Agent拿到这些工具输出后,会做一次“人力审查”。比如对于.env被提交这个问题,它会去git log里确认这个文件的历史,判断是否真的曾被推到远端;对于SQL注入,它会沿着调用链去查输入是不是用户可控。这一步正是纯工具做不到的,也是security-audit-skill相比普通扫描器的真正优势。
4.3 审计结果接入CI/CD
项目里的安全审计不能只在本地偶尔跑一次,必须进CI。我把Skill封装成一个GitHub Actions工作流,每次PR都会自动执行。
一个可用的配置示例:
name: security-audit on: push: branches: [ main ] pull_request: jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install security tools run: | pip install semgrep go install github.com/gitleaks/gitleaks/v8@latest export PATH="$PATH:$(go env GOPATH)/bin" - name: Run security audit skill run: | python .agents/skills/security-audit/scripts/audit.py \ --source . \ --output audit-report.json - name: Upload report artifact uses: actions/upload-artifact@v4 with: name: security-audit-report path: audit-report.json如果只是生成报告,团队不一定真的会看。我通常再加一个步骤:如果审计结果中存在high级别问题,就让CI失败并阻止合并。这一步的力度取决于团队文化,有些团队觉得太严格推进不下去,那就先把critical设为阻塞级别也是可以的。
4.4 从报告到修复:Agent如何给出可操作建议
纯粹的报告只是第一步,真正有价值的是修复。Skill里我明确要求Agent在输出每个问题时,必须附带一个“最小修复示例”。例如:
- 如果是硬编码密钥,给出改用环境变量的前后对比。
- 如果是SQL注入,给出参数化查询的改写方式。
- 如果是依赖漏洞,给出具体的升级命令。
这个要求让模型从“报问题的人”变成“能解决问题的人”,实际使用中开发者的接受度一下子高了很多。之前有人抱怨安全工具只会报一堆漏洞,现在Agent会顺手把补丁示例贴出来,开发只需要review后合入即可。
5. 常见问题与排查技巧实录
5.1 Skill明明放好了,Agent却没有调用
这个问题我排查了无数次,常见的三个原因:
第一,目录结构不对。多数Agent要求Skill目录名和SKILL.md文件名严格匹配,比如security-audit/SKILL.md,如果你写成了security_audit.md或者目录嵌套多了一层,可能就无法识别。
第二,Agent没有重新加载。有些工具启动时会缓存Skill索引,新增Skill后需要重启会话或重新加载项目。
第三,权限不足。Agent可能没有读取Skill目录的权限,尤其是放在/Users/xxx/.codex/skills/这类目录时,需要检查属主和权限位。
我的排查模板是:先手动用cat确认文件能被读取,再用Agent的调试模式查看它加载了哪些Skill,最后看日志里是否有“skill not found”之类的提示。
5.2 扫描结果太多,误报率高得没法看
误报是所有静态分析工具的通病。你的Skill如果同时加载了gitleaks和semgrep,第一次跑出来的报告可能有一百多条“疑似问题”,但真正值得处理的不到10条。
我的处理思路不是删除规则,而是分级处理:
- 把
severity=WARNING的信息在报告中折叠,只展示摘要。 - 在Skill的步骤里明确告诉模型:先过滤掉测试文件、mock目录、自动生成代码。
- 对高频误报规则,直接在Semgrep配置里增加
paths排除。 - 对确认误报的规则,在审计脚本里维护一个白名单,例如某些路径允许包含特定格式测试字符串。
不要轻易禁用规则,因为禁用规则会同时禁用掉它本来能发现的问题。更好的办法是让Agent在报告里自动分级:哪些可以自动修复,哪些需要人工确认。
5.3 Agent扫描时“偷懒”,给了个空报告
这是我最担心的现象:Agent绕过了所有工具,直接说“未发现明显风险”。这种情况在模型上下文过长或工具执行超时时容易发生,模型为了尽快完成任务,给出了一个看似合理但实际没做事的回答。
为了堵住这个口子,我在SKILL.md里做了两条硬约束:
- 必须先生成工具调用记录,再输出报告;报告里的每条结论必须引用具体工具输出路径。
- 如果某一类检查没有执行,必须在报告里写明“未执行”和原因,而不是用“未发现风险”来替代。
实际操作中,如果Agent试图给空报告,我会检查是不是哪个工具运行超时了。给脚本加上合理的超时处理,让Agent知道运行失败后应该重试或报告错误,而不是默默跳过。
5.4 Skill在大型仓库里运行太慢
大型Monorepo里跑一次完整审计可能要十几分钟,非常影响体验。我的优化方案是按层拆分执行:
- 先只跑敏感信息扫描,这个最耗时小于30秒。
- 再按改动文件范围跑增量代码扫描,使用
semgrep scan --baseline-commit。 - 依赖漏洞检查可以放到定时任务里每晚执行,而不是每次PR都跑全量。
另外一个技巧是把Skill拆成两个子Skill:security-audit-quick和security-audit-full。快速版用于日常开发,全量版用于上线前或夜间巡检。模型会根据用户指令自动选择合适的版本,体验会好很多。
5.5 Skill写得很完整,但Agent就是不按步骤走
大模型对长指令的执行并不总是严格遵循,尤其当SKILL.md超过300行时,模型的注意力可能被分散。我的经验是:
- 核心步骤控制在6步以内,每步描述不超过5行。
- 把详细规则放到独立文件中,SKILL.md里只写“按
rules/sensitive-patterns.txt执行”。 - 在步骤后面增加“完成检查”的描述,例如“如果上一步失败,不要继续执行后续步骤”。
从实测来看,一个精简的SKILL.md比冗长的文档执行效果好得多。模型只需要记住主干,细节放在外部规则文件里,需要时再读。
6. 从Skill到团队资产:一点实际经验
回头看,security-audit-skill真正解决的不是“让AI更聪明”,而是“让AI按标准干活”。它把过去只能靠人传人、靠开会宣讲的安全规范,变成了一个可安装、可更新、可度量效果的工程产物。我现在的做法是每月更新一次规则库,把新发现的问题模式补充进去,相当于让这个Skill随着团队代码一起演进。
如果你只是一个人用,那直接在本地放一个目录就行。但如果你想让整个团队都用起来,我建议做到两点:把Skill目录纳入Git管理,保证所有人拿到的是同一版本;为规则和报告格式建立一份简单的更新日志,免得团队看到一份格式和上周完全不一样的报告。
最后分享一个个人感受:Skill设计得好不好,有一个判断标准——你把它交给另一个团队的人,他不需要你额外解释,照着SKILL.md就能跑出可用的结果。如果还需要你从头讲一遍流程,那说明指令包还不够完整。多做几轮“把Skill交给别人测试”的循环,你会发现自己做出来的东西越来越像一个合格的安全审计同事,而不是一个“偶尔给点建议的聊天机器人”。