1. 从一次代码提交说起:为什么我们需要一个安全审计技能
上个月团队里发生了一件事,一位同事在提交代码时不小心把一个测试用的密钥硬编码进了配置文件,虽然最终在代码评审阶段被拦下来了,但这件事让我意识到一个问题:靠人眼去盯每一行代码的安全隐患,迟早会出事。尤其是现在越来越多的项目开始引入 coding agent 来辅助开发,代码产出速度翻了几倍,但安全审计的节奏根本没跟上。
这就是我动手做security-audit-skill这个项目的直接原因。简单来说,它是一个面向 coding agent 的安全审计技能模块,目标是在代码生成和提交的环节中,自动识别常见的安全风险,比如硬编码凭证、注入风险、不安全的依赖引用、权限配置错误等。它不是一个独立的扫描工具,而是作为 coding agent 的一项“技能”嵌入到开发流程里,让 agent 在写代码的同时就具备安全审查的意识。
适合谁来参考这篇内容?如果你正在做 coding agent 相关的开发,或者你在团队里负责代码安全规范落地,又或者你单纯想了解怎么把安全审计能力模块化、技能化地集成到自动化流程中,那这篇分享应该能给你一些可以直接抄作业的思路。我会从设计动机、核心机制、实操步骤、踩坑记录几个维度展开,尽量把每个决策背后的逻辑讲清楚。
2. security-audit-skill 到底解决什么问题
2.1 传统安全扫描工具在 agent 场景下的尴尬
传统做法一般是这样的:代码写完之后,跑一遍静态扫描工具,比如 Semgrep、Bandit、CodeQL 之类的,然后根据报告去修。这套流程在纯人工开发的时代没什么大问题,但放到 coding agent 的场景里就有点水土不服了。
第一个问题是时机。agent 生成代码的速度非常快,可能几秒钟就产出一个完整的函数甚至一个模块。如果等 agent 全部写完再去扫描,那中间已经产生了大量需要返工的内容,效率损失很大。第二个问题是上下文。传统扫描工具拿到的是最终代码文本,它不知道 agent 在生成这段代码时的意图是什么,也不知道这段代码在整个项目里的角色,所以误报率往往很高。第三个问题是交互。扫描工具输出一份报告,人去读、去判断、去修改,这个循环在 agent 自动化流程里是断掉的。
security-audit-skill的思路就是把这几个问题一起解决:把审计时机提前到代码生成过程中,把上下文信息纳入判断依据,把审计结果直接反馈给 agent 让它自己修正。
2.2 技能化封装的核心价值
把安全审计做成一个“技能”,而不是一个独立工具,这个选择本身是有讲究的。在 coding agent 的架构里,技能通常是一组可复用的能力单元,agent 可以根据任务需要动态调用。把安全审计技能化之后,它就不再是一个外挂的检查步骤,而是 agent 自身能力的一部分。
具体来说,技能化带来了三个好处。一是可组合性,安全审计技能可以和其他技能(比如代码生成、单元测试生成、依赖管理)串联起来,形成一条完整的流水线。二是可配置性,不同项目对安全的要求不一样,有的项目对注入攻击特别敏感,有的项目更关注依赖链安全,技能化之后可以按需启用不同的审计规则集。三是可追溯性,每次审计的结果都可以作为 agent 决策链路的一部分被记录下来,方便后续复盘。
2.3 目标用户与实际使用场景
这个技能主要面向三类使用者。第一类是 coding agent 的开发者和维护者,他们需要把安全审计能力集成到 agent 的决策流程中。第二类是项目团队的技术负责人,他们希望在不增加太多人工负担的前提下,提升代码的安全基线。第三类是安全工程师,他们可以把团队的安全规范转化成审计规则,通过这个技能自动落地。
实际使用场景我举几个例子。场景一:agent 在生成一个数据库查询函数时,技能自动检查是否存在字符串拼接导致的注入风险,如果发现就提示 agent 改用参数化查询。场景二:agent 在生成配置文件时,技能检测到疑似密钥的字符串,直接阻断并建议使用环境变量。场景三:agent 在引入新的第三方依赖时,技能查询该依赖的已知漏洞库,如果存在高危漏洞就建议替换或升级。
3. 核心机制拆解:审计规则是怎么组织和执行的
3.1 规则引擎的分层设计
security-audit-skill的规则引擎我设计成了三层结构,这样做的目的是让规则的管理和扩展更清晰。
第一层是模式匹配层。这一层负责最基础的文本级检测,比如用正则表达式去匹配常见的密钥格式、危险的函数调用、不安全的配置项。这一层的优点是速度快、实现简单,缺点是容易误报,所以它通常只作为初步筛选。
第二层是语义分析层。这一层会结合代码的语法树和上下文信息来判断风险。比如同样是字符串拼接,如果拼接的内容全部来自常量,那就不构成注入风险;如果拼接的内容包含用户输入,那就需要告警。这一层需要依赖代码解析器,实现复杂度高一些,但准确率也高很多。
第三层是策略决策层。这一层负责把前两层的检测结果综合起来,结合项目配置的安全策略,给出最终的审计结论。比如项目配置里规定“所有数据库操作必须使用参数化查询”,那策略层就会把相关的检测结果提升为阻断级别。
三层之间是逐级过滤的关系,模式匹配层产生候选告警,语义分析层做精确判断,策略决策层做最终裁决。这样的设计既保证了性能,又保证了准确性。
3.2 审计规则的描述格式
规则怎么描述,直接决定了技能的可维护性。我试过几种方案,最后选了一种基于 YAML 的结构化描述方式。每条规则包含几个核心字段:规则 ID、规则名称、风险等级、适用语言、检测模式、修复建议。
rule_id: SA-001 name: 硬编码密钥检测 severity: high languages: - python - javascript - java pattern: type: regex expression: "(?i)(api[_-]?key|secret|password|token)\\s*[=:]\\s*['\"][^'\"]{8,}['\"]" message: "检测到疑似硬编码凭证,建议使用环境变量或密钥管理服务" fix_suggestion: "将该值替换为 os.environ.get('KEY_NAME') 或对应的配置读取方式"这种格式的好处是规则和代码分离,安全工程师不需要懂技能的内部实现,只要按照格式写规则就能扩展审计能力。同时 YAML 的可读性很好,方便在代码评审时审查规则本身是否合理。
3.3 与 coding agent 的交互协议
技能和 agent 之间的交互协议是我花时间最多的地方。核心问题是:技能什么时候被调用?调用之后返回什么?agent 怎么根据返回结果决定下一步?
我的设计是这样的:技能暴露一个统一的接口,接收三个参数——代码片段、代码上下文(包括文件路径、所属模块、相关依赖信息)、项目安全策略配置。返回结果是一个结构化的审计报告,包含风险列表、每条风险的详细说明、修复建议、以及一个综合的风险评分。
agent 在生成代码之后、提交代码之前调用这个接口。如果返回的风险评分低于阈值,agent 直接继续;如果高于阈值,agent 会根据风险等级决定是自动修复、重新生成、还是标记出来让人工介入。这个决策逻辑本身也可以配置,不同团队可以根据自己的风险偏好调整。
4. 实操落地:从零搭建一个可用的审计技能
4.1 环境准备与依赖选型
搭建这个技能需要几个基础组件。代码解析方面,Python 我用的是ast模块加tree-sitter,JavaScript 用的是acorn,Java 用的是javalang。选tree-sitter的原因是它支持多语言、解析速度快、社区维护活跃,虽然学习曲线稍微陡一点,但长期来看值得投入。
规则匹配引擎我用的是regex加自定义的语义分析器。没有直接用现成的 SAST 工具,是因为那些工具通常太重,而且和 agent 的交互不够灵活。自己搭一套轻量的引擎,虽然前期工作量大一些,但后续扩展和调试都更可控。
依赖漏洞查询这块,我接的是 OSV 数据库的 API。OSV 的好处是数据格式统一、覆盖范围广、更新及时,而且查询接口简单。实际使用的时候记得加缓存,不然每次审计都去查一遍 API,延迟会很难看。
pip install tree-sitter tree-sitter-languages regex requests pyyaml4.2 规则集的初始化与裁剪
技能刚搭起来的时候,我建议不要一上来就启用所有规则。规则太多会导致误报率飙升,agent 频繁被阻断,开发体验很差。我的做法是先启用一批高置信度的规则,比如硬编码密钥、SQL 拼接、命令注入这几类,这些规则的误报率低、风险高,先跑起来看看效果。
等这批规则稳定之后,再逐步加入语义分析类的规则,比如权限检查、加密算法强度检查、日志脱敏检查。每加一批规则,都要观察一段时间的误报情况,根据反馈调整规则的阈值和匹配模式。
规则裁剪的另一个维度是按项目类型。Web 后端项目重点关注入和认证授权,前端项目重点关 XSS 和敏感信息泄露,数据处理项目重点关数据脱敏和访问控制。技能支持按项目类型加载不同的规则集,这样既保证了覆盖面,又避免了无关规则的干扰。
4.3 审计流程的编排
审计流程的编排我分成了四个阶段。第一阶段是预检,在 agent 开始生成代码之前,先加载项目配置和规则集,确认审计环境就绪。第二阶段是增量审计,agent 每生成一个代码块,技能就对这块代码做一次快速检查,这一步主要靠模式匹配层,追求速度。第三阶段是全量审计,agent 完成一个完整模块之后,技能对整个模块做一次深度检查,这一步会启用语义分析层。第四阶段是提交前审计,在代码最终提交之前,技能做一次综合检查,结合策略决策层给出最终结论。
这四个阶段的耗时和精度是递增的,前两个阶段追求快速反馈,后两个阶段追求准确性。实际跑下来,增量审计的延迟可以控制在毫秒级,全量审计在秒级,提交前审计根据项目规模可能在几秒到几十秒之间。
4.4 审计结果的处理与反馈
审计结果出来之后怎么处理,这个环节直接决定了技能的实际价值。我的做法是把结果分成三个级别:提示、警告、阻断。
提示级别的结果只是记录,不影响 agent 的流程,比如“建议给这个函数添加输入校验”。警告级别的结果会反馈给 agent,agent 可以选择自动修复或者继续但标记风险,比如“检测到可能的注入风险,建议改用参数化查询”。阻断级别的结果会直接中止 agent 的当前操作,必须修复之后才能继续,比如“检测到硬编码的生产环境密钥”。
反馈的形式也很重要。我试过只返回一个风险列表,agent 拿到之后往往不知道怎么处理。后来改成返回结构化的修复建议,包括问题描述、风险等级、修复方案、参考示例,agent 的处理准确率明显提升。
5. 踩坑记录:那些让我重新设计的地方
5.1 误报率失控的那一周
技能刚上线的时候,我信心满满地启用了几十条规则,结果第一周就被误报淹没了。最夸张的一次,agent 生成了一段包含“password”字段名的数据库模型代码,被硬编码密钥规则误判了。还有一次,一段正常的字符串格式化被 SQL 注入规则误报了。
问题出在哪里?我复盘之后发现,模式匹配层的规则太粗糙了,只看了字符串的字面内容,没有看上下文。比如“password”出现在字段名里和出现在赋值语句里,风险等级是完全不同的。后来我在模式匹配层和语义分析层之间加了一个上下文过滤层,专门处理这类误报。
具体做法是:对于每一条模式匹配的告警,先检查它所在的语法节点类型。如果是在变量声明、函数参数、类型注解这些位置,就降低风险等级或者直接忽略。只有出现在赋值、函数调用、字符串拼接这些位置时,才保留告警。这个改动之后,误报率下降了大概七成。
5.2 agent 把修复建议当成了新需求
还有一个坑让我印象很深。有一次技能检测到一个注入风险,返回的修复建议是“使用参数化查询替换字符串拼接”。结果 agent 理解成了“需要新增一个参数化查询的功能”,直接在代码里加了一个新的函数,原来的风险代码还在那里。
这个问题的根源是修复建议的表述方式。我原来写的建议太像需求描述了,agent 容易误解。后来我改成更明确的指令式表述,比如“将第 12 行的query = \"SELECT * FROM users WHERE id = \" + user_id替换为query = \"SELECT * FROM users WHERE id = %s\"; cursor.execute(query, (user_id,))”。给出具体的替换代码,agent 就不会跑偏了。
5.3 性能瓶颈出现在意想不到的地方
性能问题也是踩过坑的。我原以为瓶颈会在语义分析或者依赖查询上,结果实测下来,最耗时的居然是 YAML 规则的加载和解析。因为每次审计都要重新加载规则文件,而规则文件随着项目推进越来越大,加载时间从最初的几十毫秒涨到了几百毫秒。
解决办法很简单,加了一层规则缓存。规则文件只在启动时加载一次,后续审计直接复用内存中的规则对象。如果规则文件有更新,通过文件监听机制触发重新加载。这个改动之后,审计的整体延迟下降了一半以上。
另一个性能坑是依赖查询。OSV 的 API 虽然好用,但网络延迟不稳定,有时候一次查询要等好几秒。后来我加了两级缓存:本地内存缓存加持久化缓存。同一个依赖包的查询结果缓存 24 小时,这样大部分查询都能命中缓存,只有新引入的依赖才需要走网络。
5.4 规则冲突与优先级处理
随着规则越来越多,规则之间的冲突也开始出现。比如一条规则说“检测到疑似密钥就阻断”,另一条规则说“测试文件中的密钥可以忽略”。当这两条规则同时命中一个测试文件中的密钥时,到底听谁的?
我最后引入了一个优先级机制。每条规则有一个优先级数值,数值越高优先级越高。当多条规则对同一段代码给出不同结论时,取优先级最高的那条规则的结论。同时,规则之间可以定义互斥关系,比如“测试文件豁免规则”可以显式地覆盖“硬编码密钥阻断规则”。
这个机制的关键是优先级的设计要合理。我的经验是:阻断级别的规则优先级最高,豁免级别的规则优先级次之,提示级别的规则优先级最低。这样既保证了安全底线,又给了合理的灵活性。
6. 规则编写实战:几个高频场景的审计逻辑
6.1 硬编码凭证的检测与修复
硬编码凭证是最常见也最容易出问题的场景。检测逻辑我分了两步:第一步用正则匹配常见的凭证格式,比如以sk-开头的 API Key、以AKIA开头的访问密钥、长度超过 20 位的随机字符串等。第二步用熵值计算来判断字符串的随机性,随机性高的字符串更可能是密钥。
修复建议的生成也有讲究。如果检测到的凭证在配置文件中,建议改用环境变量读取;如果在代码中,建议改用密钥管理服务的客户端调用;如果在测试文件中,建议改用占位符或者 mock 值。不同类型的文件给出不同的修复方案,agent 处理起来更准确。
import re import math from collections import Counter def calculate_entropy(s): if not s: return 0 counter = Counter(s) length = len(s) entropy = 0 for count in counter.values(): p = count / length entropy -= p * math.log2(p) return entropy def detect_hardcoded_secret(code_line): patterns = [ r'(?i)(api[_-]?key|secret|password|token)\s*[=:]\s*[\'"]([^\'"]{8,})[\'"]', r'(?i)(AKIA[0-9A-Z]{16})', r'(?i)(sk-[a-zA-Z0-9]{20,})', ] for pattern in patterns: match = re.search(pattern, code_line) if match: secret_value = match.group(match.lastindex) if calculate_entropy(secret_value) > 3.5: return True, secret_value return False, None6.2 注入风险的上下文判断
注入风险的判断不能只看有没有字符串拼接,还要看拼接的内容来源。我的做法是构建一个简单的污点分析模型:把用户输入、外部接口返回值、环境变量标记为“污点源”,把数据库查询、命令执行、文件操作标记为“污点汇聚点”。如果一条污点传播路径从源到达了汇聚点,就判定为注入风险。
这个模型不需要做到完全精确,只要覆盖常见的传播路径就够了。比如用户输入经过简单的字符串拼接、格式化、编码之后到达数据库查询,这条路径就要被标记。如果中间经过了参数化处理或者白名单校验,那就可以认为污点被清除了。
实际实现的时候,我用的是 AST 遍历加符号表的方式。先遍历一遍收集污点源和汇聚点,再遍历一遍追踪污点传播路径。对于跨函数的传播,做一个简单的函数摘要,记录每个函数的输入输出污点状态。
6.3 依赖漏洞的查询与决策
依赖漏洞的查询流程是这样的:先从项目的依赖文件(比如requirements.txt、package.json、pom.xml)中解析出所有依赖包及其版本号,然后逐个查询 OSV 数据库,获取每个依赖的已知漏洞信息。查询结果会包含漏洞 ID、严重等级、影响版本范围、修复版本等信息。
决策逻辑根据漏洞的严重等级来定。高危漏洞直接阻断,要求 agent 升级到修复版本或者替换依赖。中危漏洞给出警告,建议升级但不强制阻断。低危漏洞只做记录,不干扰 agent 流程。如果某个依赖没有可用的修复版本,技能会建议寻找替代方案,并给出几个功能相似的替代依赖供参考。
这里有个细节需要注意:依赖的版本号格式在不同生态里不一样,Python 的==、>=,JavaScript 的^、~,Java 的[1.0,2.0),解析的时候要分别处理。我在这上面花了不少时间做兼容。
6.4 权限配置的合规检查
权限配置的检查主要针对云服务配置文件、容器编排文件、CI/CD 配置文件这几类。检查内容包括:是否使用了过宽的权限范围、是否明文存储了访问凭证、是否关闭了必要的安全选项、是否暴露了敏感端口等。
这类检查的难点在于配置文件的格式多样,YAML、JSON、HCL、INI 都有。我的做法是为每种格式写一个轻量的解析器,把配置内容转成统一的键值对结构,然后在统一的结构上做规则匹配。这样规则只需要写一遍,就能适配多种配置格式。
检查结果的处理上,权限配置问题通常给警告级别,因为有些配置在特定环境下是合理的。但如果检测到明文凭证或者完全开放的权限,那就升级为阻断级别。
7. 技能集成后的效果与持续优化
7.1 实际跑下来的数据
技能在团队内部跑了一个多月,积累了一些数据可以分享。审计覆盖率方面,agent 生成的代码中有 92% 经过了至少一次审计,剩下 8% 主要是纯注释和空行。风险检出方面,平均每千行代码检出 3.2 个风险点,其中高危占 12%,中危占 35%,低危占 53%。误报率方面,经过上下文过滤层优化之后,误报率控制在 8% 左右,团队反馈可以接受。
修复效率方面,agent 自动修复的比例大约是 65%,剩下 35% 需要人工介入。自动修复的成功率在 80% 以上,失败的主要原因是修复建议不够具体或者上下文信息不足。这些数据帮助我定位了后续优化的方向。
7.2 规则迭代的节奏把控
规则迭代不能太频繁,也不能太慢。我的经验是每两周做一次规则评审,收集这两周内的误报案例和漏报案例,针对性地调整规则。调整的时候遵循一个原则:先修误报,再补漏报。因为误报对开发体验的影响更大,误报多了大家就不信任这个技能了。
每次规则调整之后,要跑一遍回归测试,确保新规则没有引入新的误报,也没有破坏已有规则的效果。我维护了一个测试用例集,包含各种典型场景的代码片段和期望的审计结果,每次调整规则都跑一遍这个测试集。
7.3 和 agent 团队的协作经验
技能的效果很大程度上取决于和 agent 团队的协作。我的经验是,技能开发者不能闭门造车,要深入了解 agent 的工作流程和决策逻辑。比如 agent 在什么阶段会调用技能、期望的响应时间是多少、对返回结果的格式有什么要求,这些信息直接影响技能的设计。
另外,技能的接口设计要留有余地。我一开始把接口设计得很死,后来 agent 团队提出需要支持流式审计,就是代码一边生成一边审计,我不得不重构了整个接口。如果一开始就考虑到这种扩展性,可以省下不少返工时间。
7.4 后续可以扩展的方向
这个技能目前主要覆盖的是代码层面的安全审计,后续可以往几个方向扩展。一是往基础设施层面延伸,比如审计容器镜像的构建配置、Kubernetes 的部署清单、云服务的 IAM 策略。二是往数据层面延伸,比如审计数据脱敏规则、数据访问日志、数据跨境传输配置。三是往供应链层面延伸,比如审计依赖包的来源可信度、构建产物的完整性校验、发布流程的权限控制。
每个方向的扩展都需要补充相应的规则集和解析器,但核心的审计引擎和交互协议可以复用。这也是当初把技能设计成模块化架构的好处,扩展的时候不需要推倒重来。
8. 一些实操中的小技巧和注意事项
规则编写的时候,正则表达式尽量写得宽松一点,宁可多匹配一些候选,也不要漏掉真正的风险。因为后续还有语义分析层做精确判断,模式匹配层的漏报比误报更致命。
审计结果的返回格式要稳定,不要频繁变动。agent 团队需要根据返回格式来写处理逻辑,格式一变他们就要跟着改。如果确实需要调整格式,提前沟通,给足迁移时间。
技能的日志要记全,但不要记敏感信息。审计过程中会接触到代码内容,日志里如果直接记录代码原文,可能会造成新的泄露风险。我的做法是日志里只记录风险类型、位置、规则 ID,不记录具体的代码内容。
性能优化要抓大放小。审计流程里最耗时的环节通常是依赖查询和语义分析,优先优化这两个地方。模式匹配层的优化空间不大,投入产出比不高。
和团队沟通的时候,多用数据说话。不要说“误报率有点高”,要说“过去两周误报率是 12%,主要集中在这三类场景”。具体的数据和案例更容易推动改进。
最后分享一个我踩过的坑:技能刚上线的时候,我没有设置审计开关,所有项目都强制启用全部规则。结果有个老项目因为历史遗留问题,一审计就报几百个风险,agent 直接卡死了。后来加了项目级别的审计配置,允许按项目启用不同的规则集,问题才解决。所以一定要留好开关,不要一刀切。