☰
用security-audit-skill构建AI代码安全审计技能包
2026/10/27 8:39:03 网站建设 项目流程

1. 先说痛点:为什么常规 AI 代码审查经常“审不出东西”

1.1 我拿通用审查工具跑了三周,效果一言难尽

几个月前,我帮一个创业团队搭自动化代码审查流程。团队产品迭代很快,后端 Python、前端 TypeScript、再加两个微服务,PR 频繁得让人头皮发麻。他们之前依赖人工 Code Review,但两个人负责全部审查,明显扛不住。我最初的想法很简单:接一个通用 AI 审查工具,让模型跑一下 diff,输出问题列表,人工复核,成本低见效快。

想法很美,现实打脸。通用模型做代码审查,它确实能发现一些“代码风格问题”——变量命名不统一、函数太长建议拆分、缺少类型注解等。但真正和安全相关的场景,它的表现相当不稳定。比如有一段直接把用户输入拼进数据库查询的代码,它给出的评论居然是“建议使用参数化查询以提升可读性”,完全没有意识到这是一条高危 SQL 注入链路。还有一次,代码里有一个基于用户角色做权限判断的接口,模型压根没有检查绕过风险,反而夸“逻辑清晰”。

问题出在哪?因为通用模型的“审查”本质上是模式匹配,它见过很多优秀代码,也能识别常见 bug,但它没有一个完整的“安全审计作业体系”。它不知道你的信任边界在哪,不清楚哪些数据源是“用户可控”,更不会主动去构建污染数据流。它像是一个初来乍到、没有清单的新员工,光靠“感觉”说哪里不对劲,而不是拿着安全基线逐项核对。

1.2 缺失“安全基线”:通用大模型不知道你的代码哪里会出事

安全审计和代码审查看起来是一回事,实际是完全不同的认知层次。代码审查关心的是“代码能不能跑、好不好改、有没有 bug”,安全审计关心的是“谁在什么时候、通过什么路径、能对这个系统做什么不该做的事”。后者的核心是攻击面分析和信任边界划分,这是通用模型天然欠缺的——它没有一份属于你项目的威胁模型。

举个例子:一个普通的文件上传接口。通用审查会关注文件类型校验是不是太宽松、有没有限制大小。但安全审计会追问:文件是直接落在 Web 根目录吗?文件名是不是用户可控的?最终文件路径是否会导致路径穿越?上传后的文件能不能被直接 URL 访问从而变成任意文件上传漏洞?同一段代码,四个问题,只有带着审计规则清单去检查才会逐一验证。

这就引出了我这篇博文要讲的security-audit-skill。简单来说,它是一个把安全审计方法论结构化、可复用、可编程化的“技能包”,专门用来引导大模型完成一次有章法的代码安全审计。它不是那种“把 prompt 写长一点”的通用技巧,而是一套真正落地的作业系统——包含审计上下文、检查规则库、审计流程编排和报告输出协议。适合谁用?团队里负责安全的工程师、想做自动化 Code Review 的 DevOps、以及所有被“手工审不完、AI 审不准”折磨过的人。

1.3 security-audit-skill 的定位:把审计方法论做成技能资产

我先说清楚这个东西的边界:它不是一种编程语言,也不是一个独立的安全扫描器,更不是某个商业平台的 SaaS 服务。它是一个可以被加载到 AI 代码审查工作流里的“技能定义包”,本质是一组结构化的指令、规则和模板的集合,通过特定格式让大模型在每次审计时严格按专业方法论执行。

为什么做这个?因为我在试过几轮通用 AI 审查之后,发现一个最核心的问题:prompt 里的安全审计知识是一次性的。这次写得再详细,换个项目、换个人、换个会话,全得重来。就算复制过去,上下文一变效果也跟着变。技能的沉淀、维护、迭代全都缺失。而security-audit-skill把审计知识从“聊天的临时记忆”变成了“项目的长期资产”,这才是它价值最大的地方。

2. 拆开技能包:四层结构把审计方法论固化进了代码

在做这个技能包之前,我参考了安全圈里比较成熟的审计方法论,也看过不少开源项目的审计 checklist(比如墨菲安全、OWASP 相关的检查项),最后把整个过程抽象成四层结构。这里我不讲空概念,直接展示我实际定义的每个层级,以及为什么必须这样分层。

2.1 第一层:审计上下文固化

第一层解决的问题是:让模型知道“它在审什么”。同样一段代码,在 Django 项目里和在 Flask 项目里,安全的关注点完全不同;在一个权限体系不完善的管理后台里,和在对外公开的 REST API 里,信任边界也完全不一样。没有上下文,AI 就只能瞎猜。

我在技能包中定义了一个context模块,专门用来灌入项目元信息,例如:

tech_stack: language: python framework: django auth_model: session-based db_orm: django-orm trust_boundaries: public: [url_prefix: "/api/public/*", auth: none] authenticated: [url_prefix: "/api/me/*", auth: login] admin: [url_prefix: "/api/admin/*", auth: role=admin] threat_priorities: - sql-injection - auth-bypass - path-traversal - ssrf

这一层做两件事。第一,约束模型的检查范围,不要把时间浪费在跟安全无关的地方;第二,明确信任边界,让后续所有数据流分析都有了坐标系。比如说一个接口如果在admin边界,那么第一步就要验证角色校验逻辑是否真的生效,而不是把时间花在“登录态存不存在”上。

我的体会是,上下文里最容易被忽视的是“威胁优先级”。安全审计不是均匀分布精力,而是要把精力集中在高风险区。我一般按 TOP 4 到 TOP 6 列出来,每个项目不同。有些老项目 SQL 注入风险极低(因为全项目用了 ORM),但 SSRF 风险极高(因为业务需要频繁拉取外部链接),那威胁优先级排序就完全不一样。

2.2 第二层:检查规则库

第二层是技能包的核心骨架,也是“审计作业系统”的检查清单。这一层我拆成了三类文件:

  • injection_rules.md:SQL、NoSQL、命令注入、模板注入
  • access_control_rules.md:越权、缺少授权校验、RBAC 不完整、JWT 校验错误
  • dangerous_function_rules.md:反序列化、eval、exec、子进程调用、文件操作

规则文件的组织方式不是简单的“常见漏洞描述”,而是按“触发语义 + 证据要求 + 输出格式”三段式去组织。我直接贴一段injection_rules.md的内部片段,大家可以感受一下:

## 检查项:SQL 注入(数据流追踪) - trigger_sources: - request.get_param("") - request.get_json().get("") - form_data - sink_functions: - execute_raw_sql - cursor.execute - model.objects.raw - check_steps: 1. 从 trigger_sources 出发,追踪变量直到 sink_functions 2. 检查路径中是否有参数化处理节点 3. 检查路径中是否有白名单校验节点 4. 若没有以上处理节点,判定为高危风险 - evidence_required: - 完整数据流路径文本 - 漏洞位置(文件+行号) - 可利用性分析(是否需要前置条件) - output_severity_rule: - 可达且无校验: critical - 可达但需要低权限角色: high - 不可达: low

这里重点说说“证据要求”为什么重要。因为大模型在生成审计结果时,最大的毛病是“自己信自己”——它会凭印象直接说“这里存在 SQL 注入”,但没有给出从用户输入到数据库执行之间完整的数据流路径。有了evidence_required,模型就不得不把链路写清楚。我甚至发现,强制要求证据链之后,很多误报会自动消失。因为模型试图构建完整路径时,经常发现自己写不下去,这时候它就意识到“其实输入根本到不了这里”。

2.3 第三层:审计流程编排

有了上下文,有了规则,AI 还是可能走偏,因为规则再全,它也不知道先做哪一步、后做哪一步。这就是流程编排层存在的意义。我把审计流程编排成五个固定阶段,模型只能按顺序执行,不许跳跃:

  1. 信息收集:先梳理项目中的入口点(URL 路由、API endpoint、消息队列消费者)和信任边界映射。
  2. 攻击面枚举:列出所有能被外部触达的代码路径,标记哪些在 public 边界、哪些在 authenticated 边界。
  3. 规则扫描:拿着rules/目录里的清单对着攻击面逐项过,每命中一条规则都要记录“值得怀疑”的证据。
  4. 路径验证:对有嫌疑的点做二次数据流确认,判断真伪和可利用性。
  5. 报告生成:严格按输出协议生成报告,不允许把“应该看看这里”这类模糊表述写进最终结论。

这个编排顺序是我反复调出来的结果。我早期的版本上来就让模型直接看diff然后输出问题,结果它的问题清单非常碎片化,缺乏优先级,顺序越看越乱。后来改成先枚举攻击面,再引导模型按规则扫描,效果明显改善——因为我们给了它一个“寻找入侵入口”的框架,而不是让它像无头苍蝇一样乱撞。

流程编排还有一个隐含作用:它抑制了模型过度发挥的倾向。大模型在没有流程约束时容易把项目里所有不符合“代码洁癖”的地方都列出来,比如“建议使用 dataclass 替代字典”。但我给最终的审计报告做了硬约束:只允许输出和规则库匹配、并且通过了路径验证的问题。其他内容统统不要写。

2.4 第四层:输出契约

最后一层是输出协议,就是定义“一次审计的最终交付物长什么样”。我和团队实际用过几轮后,发现一个很微妙的问题:审查结果太长了,根本没人看。AI 一口气输出几十条建议,其中一大半是低价值的,真正的高危漏洞反而淹没在里面。

为了让 AI 输出“可以直接丢进 MR review 评论区的干货”,我为输出协议定了极严格的格式要求:

report_schema: - summary: critical_count: int high_count: int medium_count: int - findings: - id: "F001" title: string severity: critical | high | medium | low confidence: high | medium | low location: "file_path:line_number" vulnerability_type: cwe_id data_flow_evidence: [step1, step2, step3] exploitability: string recommended_fix: string - non_issues_examined: - item 1 with reason why it is not a vulnerability

特别注意最后一项non_issues_examined,这是我踩坑后才加进去的。没有它的时候,审计报告里只有“发现了什么问题”,没有“检查了什么、排除了什么”。结果就是:安全审计变成了挑刺工具,团队看到报告只会焦虑,因为永远不知道哪些值得改、哪些可以忽略。加了这一项之后,报告反而像一份真正的审计记录——有肯定、有排除、有风险判断,而不是单纯的“踩踩踩”。

3. 从零到一搭建并接入:项目结构、配置与 CI 集成全流程

结构设计得再好,落不了地等于零。这一节直接给实操路径,从目录结构说起,到怎么挂进 CI,每步我都有真实配置可以抄。

3.1 项目目录设计与 SKILL.md 的写法

我实际使用的项目目录是这样的:

security-audit-skill/ ├── SKILL.md ├── contexts/ │ ├── python_django.yaml │ ├── python_flask.yaml │ └── nodejs_express.yaml ├── rules/ │ ├── injection_rules.md │ ├── access_control_rules.md │ ├── dangerous_function_rules.md │ ├── secrets_management_rules.md │ └── misconfiguration_rules.md ├── workflow/ │ ├── audit_stages.md │ └── priority_matrix.md ├── templates/ │ ├── report_template.md │ └── mr_review_template.md └── scripts/ ├── load_skill.py └── generate_report.py

SKILL.md是整个技能包的入口文件,它描述了“什么情况下启用这个技能、技能包含哪些组件、从哪里开始读”。这段的写法要注意,不是给 AI 看的简单描述,而是要写成一个“启动指令”,让 AI 在加载技能后知道下一步该打开哪个文件。我的SKILL.md核心部分长这样:

# security-audit-skill ## Activation 当用户要求对代码进行安全审计、安全审查、安全检查时,启用本技能。 ## Entry Points 1. 读取 contexts/ 下匹配当前技术栈的上下文文件,完成信任边界初始化。 2. 读取 workflow/audit_stages.md,获取审计阶段定义。 3. 解析待审计代码时,逐条匹配 rules/ 目录下的检查规则。 4. 所有发现必须经过 workflow/priority_matrix.md 的严重性评级。 5. 输出必须符合 templates/report_template.md 的要求。 ## Constraints - 禁止输出与安全无关的代码风格建议。 - 所有高危及以上发现必须附带数据流证据链。 - 无法确认的隐患,归入 non_issues_examined 而不是 findings。

为什么要单独弄一个SKILL.md而不是把全部指令拼在一段 prompt 里?因为大模型的上下文窗口虽然越来越大,但有效的注意力长度有限,文件路径跳转式的加载方式可以让模型“按需读入”,先读入口,再逐层打开子文件。这比一次把所有内容都灌进去更可控,也更容易维护。

3.2 规则文件怎么写:以注入检查和认证绕过为例

规则文件的密度不能太低也不能太高。太低则模型没有抓手,太高则上下文爆炸。我在实践中把每个规则文件控制在 30~50 行左右,聚焦在一个漏洞类别。以access_control_rules.md为例,里面的认证绕过检查规则是这样的:

## 检查项:水平越权(IDOR) - trigger_condition: 接口路径中包含资源 ID,并且接口在 authenticated 边界内 - check_steps: 1. 检查是否存在“属于当前用户”的对象归属校验 2. 从入口参数提取 user_id 或 resource_id 3. 追踪该 ID 在数据查询中的使用方式 4. 若查询时只按 resource_id 过滤、未按当前登录用户过滤 -> 判定越权 - common_mistakes_to_find: - 只校验了登录态,未校验资源归属 - 查询接口直接使用 URL 上的 id 参数而不校验 session - 前端做了判断但后端未做校验 - evidence_required: - 接口签名 + 当前登录态获取方式 - 数据查询语句 - 对象归属校验是否存在的完整判定

我特别想强调“common_mistakes_to_find”这一节带来的效果提升。通用 AI 审查之所以很多越权漏洞发现不了,是因为它倾向于认为“代码写出来了就有效”——比如前端判断了权限,它就以为后端也判断了。但实际审计必须警惕一种经典问题:前端限制根本不代表后端安全。在规则里显式列出这些反模式,模型才能在看到相似代码时立即警觉。我后来测试过,加入这一节之后,IDOR 类问题的检出率上升非常明显。

3.3 接入代码评审工作流:命令行执行与 CI 机器人

技能包不只是给“人和 AI 对话”用的,我把它做成了可编程执行的模式,方便在合并到主干之前自动跑一遍。执行方式是,在目标项目根目录下挂载技能包,然后让 AI 命令行工具按下面的方式运行:

ai-audit --skill ./security-audit-skill \ --context python_django.yaml \ --target ./myproject/ \ --diff-only \ --output mr-report.md

这里--diff-only很重要。整仓全量扫描太耗 token,而且噪音太大,低频团队通常不需要;真正的价值是把这个技能包用在每次 PR 的增量代码上,审查新增代码有没有引入安全问题。

在 CI 集成上,我用了一个简单的 GitHub Actions workflow,核心配置是这样的:

name: security-audit on: pull_request: types: [opened, synchronize] jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Run security audit skill run: | ai-audit --skill ./security-audit-skill \ --context python_django.yaml \ --target ./src/ \ --diff-only \ --output /tmp/audit-report.md - name: Post report to PR run: | ai-bot job post --field /tmp/audit-report.md

大家要注意的关键操作是fetch-depth: 0——如果只拉取浅克隆,AI 工具拿不到 base 分支的完整历史,diff-only计算会失败。这个坑我踩过,当时 CI 日志里报了一堆 diff 对比失败,排查半天才发现是这个设置问题。

3.4 报告长什么样:一次真实输出

为了让读者有直观感,我贴一段技能包生成的实际报告核心片段(格式化后):

## Audit Summary - Critical findings: 1 - High findings: 1 - Medium findings: 2 ## Findings ### F001: SQL Injection via Raw Query in user_search - Severity: Critical - Confidence: High - Location: apps/user/views.py:41 - CWE: CWE-89 - Data Flow Evidence: 1. request.GET.get("q") -> view parameter `q` 2. `q` passed directly to `User.objects.raw("SELECT ... WHERE name = '%s'" % q)` 3. Raw query executed without parametrization - Exploitability: Public endpoint, no authentication required - Recommended Fix: ```python User.objects.filter(name__contains=q)
这份报告最直观的价值,是开发人员拿到后可以立即定位、立即理解。它不啰嗦,每个发现都有证据链和修复建议,直接解决了“AI 说有问题但我不知道哪里有问题”的尴尬。 ## 4. 实测验证:在伪代码仓库里跑一轮,看看它会不会翻车 结构完整不等于有效。为了验证技能包到底行不行,我造了一个包含常见安全隐患的小型 demo 仓库,有意埋了几个经典漏洞,也埋了几个“像漏洞但其实是误报”的陷阱,用来测试技能包的辨识能力。 ### 4.1 埋了哪些雷:设计一次“半真半假”的审计实验 我构造的 demo 仓库模拟的是一个用户管理系统,包含 4 个文件、约 200 行代码。埋的雷如下: 1. 用户搜索接口存在拼接式 SQL 查询(真实高危)。 2. 文件导出接口存在路径拼接,文件名直接来自用户输入(真实高危)。 3. 管理员接口用装饰器做权限控制,但所有请求体的 `is_admin` 字段直接透传到数据库更新语句(真实但隐蔽)。 4. 一个看似危险的 `os.system("echo " + param)` 调用,但实际上前面已经用白名单验证了 param 只能是固定的几个值(预埋误报)。 为什么故意埋一个误报?因为我觉得好的审计技能包不仅要能“抓住坏人”,还要能“不冤枉好人”。如果业务代码里所有疑似危险操作都被当漏洞报,报告噪音会把人淹死,最后团队反而不信任这个工具。 ### 4.2 审计结果:技能包给出的判定逻辑 跑完一轮,结果在这几个点上比较有意思: 第一个雷,没有任何悬念,被直接判为 Critical。因为规则库明确覆盖了 `execute/raw/拼接` 这条路径,证据链直接追到了 `request.GET` 参数。 第二个雷,路径穿越问题,也被识别为 Critical。规则库里的 `dangerous_function_rules.md` 有一条专门针对 `os.path.join` + 用户输入 + 未做 `normalize` 检查的组合。 第三个雷比较值得说。代码里确实用了装饰器 `@admin_required`,表面上看权限没问题。但审计过程从“更新用户信息”这个动作反推,发现更新语句直接使用了请求里的 `is_admin` 字段去覆盖数据库里的对应字段,这就意味着任何登录用户都能通过构造请求体把自己提权成管理员。技能包没有因为装饰器的存在就放过它,而是沿着数据流发现“装饰器只挡了入口的 URL,但没有挡住数据字段的滥用”,最终判为 High。这一步是我最满意的,因为它说明技能包不只是做表面扫描,而是真的在做链路追踪。 第四个陷阱,`os.system` 的调用确实危险,但审计的输出协议要求做“可利用性分析”。技能包发现参数在被使用之前过了一道白名单校验,因此把这个点归入了 `non_issues_examined`,而不是 `findings`。如果你只看报告标题,会觉得技能包“漏”了它,可细看报告正文,会发现它其实注意到了且给出了排除理由。这比那些见 `os.system` 就报警的“伪扫描器”高级太多。 ### 4.3 边界问题:误报、漏报,以及技能包能力的真实下限 没有工具是万能的,我也要说清楚这个技能包的边界,免得读者抱有不切实际的期望。 先讲误报。规则库再细致,还是会遇到“技术上危险、业务上安全”的场景——比如前面那个 `os.system` 白名单场景。如果技能包只看到函数调用没有看到前置校验,就会误报。这个问题我通过两个手段缓解:一是规则里强制要求检查“前置节点”,二是输出协议里允许 `low` 严重级别存在,避免动不动就 Critical 引来恐慌。 再讲漏报。技能包对分布式架构问题的覆盖有限。如果一个漏洞需要跨服务追踪——比如在 API 网关层接收了用户输入,传到内部服务 A,再通过消息队列传给服务 B,最终在服务 C 里触发了危险函数——纯代码静态审计是追不全的,因为链路分散在多个仓库和多个运行时组件里。这时候需要配套运行时 IAST 工具做补充,技能包就不要硬撑了。 最后是模型本身的算力。技能包再精良,底模推理能力不行也白搭。我用多套模型实测过,整体效果差别很大:强模型的报告几乎不需要二次修改,弱模型连规则都读不进去,输出格式潦草、证据链断裂。所以这个技能包更适合配合当前头部模型使用,大家要注意这个前提条件。 ## 5. 落地过程中的坑与我的取舍 这一节讲实操中踩到的具体坑。有三次踩坑给我的影响最大,也直接改变了这个技能包的设计方向。 ### 5.1 规则文件太详细导致上下文被吃光 早期版本里,我把规则库写得极其详细,每个漏洞类别都配了二十几个检查点,加内部参考示例,整个规则库加起来接近 3000 行。结果模型加载完技能包后,实际能用于分析代码的上下文空间所剩无几,输出质量反而下降。 后来我做了“分层加载”改造:`SKILL.md` 只保留核心指令,子文件按需打开。比如在扫描到文件操作时才打开 `dangerous_function_rules.md` 中的文件路径章节,而不是一开始全量读入。这个改动让上下文占用降低了大概 70%,审计质量不降反升。我得出的经验是:技能包不是“把所有东西塞给 AI 看”,而是“让 AI 知道到哪里去查什么东西”。 ### 5.2 模型容易“自己说服自己” 这个坑我非常想重点讲,因为它是 AI 审计独有的现象。模型在初步判断某个函数存在注入风险后,会倾向于收集“支持漏洞存在”的证据,而忽略“漏洞不存在”的信号。它对规则的理解变成了自我论证,而不是客观排查。 打个比方,这就像一个审计员先咬定“这家公司做假账”,然后只盯着能支撑这个结论的账本看,对合规的流程视而不见。我初期用这个技能包跑测试时,就见过模型把经过白名单校验的输入仍然判为 SQL 注入,理由是“存在到达危险函数的路径”——但它没有验证路径上面有拦截器。 解决方案是在流程编排里强制加入“反证步骤”:每个规则命中后,必须回答“是否存在阻断该漏洞路径的前置校验”。没有这一步,就不允许下最终结论。加入后,误报率明显下降。如果你复现时发现报告里还有这种“自我说服”型误报,十有八九就是反证步骤没有生效。 ### 5.3 技能包本身也要维护,不是一次性资产 最后这点更多是工程管理层面的经验。技能包不维护,几个月后就会落后——新框架的出现、新漏洞模式的公开,都需要定期更新。我给技能包设置了半年的迭代节奏,每轮迭代要做三件事:把最近半年公开的高危漏洞通告转成规则条目;回归测试已有规则,删掉误报率过高的检查点;更新上下文模板,匹配新的框架版本特性。 这个维护成本和收益是被低估的。很多团队试过 AI 审计,用完一两轮就嫌麻烦放弃了,大概率是因为把技能包当成“写一次就永久生效的静态文件”。实际上安全审计本来就是对抗性工作,攻击手法一直在变,技能包自然要跟着变。养成“迭代技能包”的习惯,比追求一个完美的初始版本重要得多。 ## 6. 一点收尾:我把后续扩展确定在这三个方向上 在带了这个技能包跑过多个项目之后,我个人最大的体感是,代码安全审计这件事完全可以被 AI 重新做一遍。但核心不是模型有多聪明,而是你有没有把审计方法论结构化地喂给它。`security-audit-skill` 目前已经证明了“规则库 + 流程编排 + 输出约束”这条路是走得通的。后面我还想做三件事:第一,把规则库从纯文本扩展成可配置的 JSON Schema,方便不同团队按自己的合规需求裁剪;第二,把跨服务数据流的追踪做成半自动化的插件,让静态审计和动态调用链信息能对得上;第三,把每次审计的报告沉淀成历史库,这样长期下来可以形成针对特定业务线的风险趋势分析,而不只是一次性的点状结论。代码审计这件事永远不会终结,但至少现在,我终于不用再靠肉眼在几千行 diff 里找 try-except 了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询