ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

security-audit-skill:让编码智能体具备安全审计能力

security-audit-skill:让编码智能体具备安全审计能力

1. 从"security-audit-skill"这个名字说起:它到底在解决什么问题

第一次看到security-audit-skill这个命名,我的直觉是:这大概率是一个面向编码智能体(coding-agent)的技能包,而不是一个独立的扫描器。为什么这么判断?因为skill这个词在当下的智能体生态里,通常指的是一段可被智能体调用的能力封装——它可能是一组提示词、一套工具函数、一段可执行脚本,或者三者的组合。而security-audit则明确了它的职责边界:安全审计。

把这两个词拼在一起,含义就很清楚了:让编码智能体具备执行安全审计的能力。这件事的价值在于,传统的安全审计工具(SAST、DAST、依赖扫描)大多是"人驱动"的——你得手动跑、手动看报告、手动判断误报。而一旦把审计能力封装成智能体可调用的 skill,整个流程就变成了"智能体在写代码的过程中顺手把安全问题查了",这是工作流的根本性变化。

我在实际项目里见过太多这样的情况:开发同学用智能体生成了一大段代码,功能跑通了,测试也过了,但里面藏着一个硬编码的密钥、一个没做参数化的 SQL 拼接、或者一个用了已知有漏洞版本的依赖。这些问题不是开发同学不负责,而是"生成"和"审计"是两个割裂的动作,中间隔着一道人为的墙。security-audit-skill要做的,就是把这道墙拆掉。

这篇文章适合三类人看:一是正在给自家编码智能体扩展能力的工程师,二是想理解智能体安全审计落地细节的技术负责人,三是对"AI 辅助安全"这个方向感兴趣、想动手试一下的开发者。我会从设计思路、核心能力拆解、实操落地、误报处理、以及我踩过的坑这几个角度,把这个 skill 讲透。需要说明的是,原始项目正文和关键词都是空的,所以下面的内容是我基于这个命名和常见工程实践做的合理推演与补全,凡是推演的部分我都会明确标注出来。

2. 一个安全审计 skill 应该具备哪些核心能力

2.1 静态代码审计:从"模式匹配"到"语义理解"

安全审计 skill 最基础的能力是静态代码审计。传统 SAST 工具靠的是规则引擎和模式匹配,比如正则去匹配eval(、exec(、字符串拼接的 SQL 语句。这种方式的问题是误报率高得离谱——一个正常的日志打印里出现了exec字样,也可能被标红。

智能体驱动的审计 skill 不一样,它可以做语义级的判断。举个我实际遇到的例子:一段 Python 代码里有个函数叫run_command,参数来自一个配置字典。传统工具看到subprocess加变量就直接报警,但智能体可以往上追溯这个配置字典的来源——如果它来自一个受信任的本地配置文件,那风险等级就低;如果它来自 HTTP 请求参数,那就是高危的命令注入。

这种"追溯数据流"的能力,是 skill 设计的核心。我建议在设计时把审计拆成三层:

  • 第一层:危险函数识别。维护一份危险 API 清单(如eval、exec、pickle.loads、os.system、subprocess的 shell 模式等),先做粗筛。
  • 第二层:数据流追踪。对粗筛出来的点,向上追溯参数的来源,判断是否用户可控。
  • 第三层:上下文判断。结合代码所在的位置(是否在鉴权中间件之后、是否有输入校验)给出最终风险等级。

这三层不是必须严格串行的,但缺了任何一层,要么漏报要么误报。我在一个内部项目里试过只做第一层,结果一个 5000 行的服务扫出 200 多个"高危",开发同学直接不看了。加上第二、三层之后,真正需要人工确认的降到了 8 个,其中 3 个是真问题。这个转化率才是可用的。

2.2 依赖与供应链审计:别只盯着自己写的代码

很多人做安全审计只审自己写的代码,这是个大盲区。现代项目的代码量里,自己写的可能只占 10%,剩下 90% 是依赖。一个用了有已知漏洞版本的第三方库,风险可能比你自己写的一个小 bug 大得多。

security-audit-skill在依赖审计这块,我建议至少覆盖这几件事:

审计维度具体动作常见工具/数据源
已知漏洞比对依赖版本与漏洞库OSV、NVD、GitHub Advisory
许可证合规检查依赖的开源协议本地维护的协议白名单
维护状态判断依赖是否已停止维护仓库最近提交时间、issue 活跃度
传递依赖检查间接引入的依赖锁文件解析

这里有个实操细节:锁文件(lock file)才是真相。很多项目在package.json或requirements.txt里写的是宽松版本范围,实际安装的版本由锁文件决定。审计时必须解析锁文件,否则你审的版本和实际跑的版本可能对不上。我见过一个项目,requirements.txt里写的是flask>=2.0,锁文件里锁的是 2.0.1,而 2.0.1 恰好有个已知问题,但审计工具读的是requirements.txt,直接漏了。

2.3 配置与密钥审计:最容易被忽视的高危区

代码写得再规范,一个硬编码的密钥就能让所有努力归零。配置审计这块,skill 需要能识别几类典型问题:

  • 硬编码凭证:API Key、数据库密码、JWT 密钥直接写在代码或配置文件里。
  • 不安全的默认配置:比如调试模式在生产环境开启、CORS 设置为*、对象存储桶权限为公开。
  • 敏感信息泄露:日志里打印了完整请求体、错误堆栈直接返回给前端。

识别硬编码密钥这件事,难点在于"区分真密钥和假密钥"。代码里经常有password = "your_password_here"这种占位符,也有测试用的假密钥。我的经验是结合熵值判断——真实密钥的随机性通常很高,熵值明显高于普通字符串。同时结合变量名(secret、token、key、password)和字符串长度做综合判断,能大幅降低误报。

提示:审计配置时,别忘了检查.env文件是否被误提交到版本库。这个坑我踩过不止一次,一个.env里躺着生产数据库密码,提交记录里躺了三个月才被发现。

2.4 审计结果的呈现:让开发同学愿意看

审计做得再好,如果报告没人看,等于没做。我在设计 skill 的输出时,坚持一个原则:按"可操作性"排序,而不是按"严重程度"排序。

什么意思?一个"理论上高危但需要特定条件才能触发"的问题,和一个"中危但明天就可能被利用"的问题,后者应该排在前面。具体来说,输出里每条问题至少包含:

  • 问题位置(文件、行号)
  • 问题类型和一句话说明
  • 触发条件(什么情况下会被利用)
  • 修复建议(最好给出可直接替换的代码片段)
  • 置信度(高/中/低,让开发同学知道要不要细看)

置信度这个字段特别重要。它相当于给开发同学一个"信任锚"——标了高置信度的,基本可以直接改;标了低置信度的,扫一眼确认下就行。没有这个字段,开发同学要么全信(浪费时间改误报),要么全不信(漏掉真问题)。

3. 把审计能力封装成 coding-agent 可调用的 skill

3.1 skill 的接口设计:输入输出要"窄"而"准"

给智能体设计 skill,接口设计是第一位的。我见过太多 skill 失败在接口太宽——输入是一个"项目路径",输出是一坨"审计报告",智能体根本不知道怎么用。

好的接口应该是窄而准的。以security-audit-skill为例,我倾向于把它拆成几个独立的子能力,每个子能力有明确的输入输出:

{ "skill": "security-audit", "action": "scan_file", "input": { "file_path": "src/handlers/user.py", "language": "python", "focus": ["injection", "hardcoded_secret"] }, "output": { "findings": [ { "line": 42, "type": "sql_injection", "confidence": "high", "snippet": "cursor.execute(f\"SELECT * FROM users WHERE id={user_id}\")", "suggestion": "使用参数化查询:cursor.execute(\"SELECT * FROM users WHERE id=%s\", (user_id,))" } ] } }

这样设计的好处是,智能体可以在不同场景下调用不同的 action。写代码时调scan_file做增量审计,提交前调scan_diff只审改动部分,发布前调scan_project做全量审计。focus参数让智能体可以按需聚焦,避免每次全量扫描拖慢响应。

3.2 增量审计:只审改动,才跑得动

全量审计一个中型项目,动辄几分钟甚至更久。如果每次智能体写完代码都跑全量,体验会非常差。所以增量审计是必须的。

增量审计的核心是拿到"改动集"。在 git 环境下,可以通过git diff拿到改动的文件和行范围,然后只对这些范围做审计。但这里有个坑:改动引入的问题,根因可能在没改动的代码里。比如你改了一个函数的返回值类型,导致调用方原本安全的代码变得不安全。

我的处理方式是"改动集 + 一跳依赖":审计改动文件本身,同时审计直接调用这些改动函数的文件。这样既控制了范围,又不会漏掉因改动而"变坏"的旧代码。实测下来,一个 200 文件的项目,单次改动通常涉及 3-5 个文件,加上一跳依赖也就 10 个左右,审计时间从几分钟降到十几秒,完全能接受。

3.3 与智能体工作流的集成时机

skill 什么时候被调用,比 skill 本身做了什么更重要。我总结下来有三个黄金时机:

  • 生成后立即审计:智能体写完一段代码,立刻对这段代码做审计。好处是上下文新鲜,智能体自己就能根据审计结果修正。坏处是可能打断生成流程。
  • 提交前审计:在代码提交到版本库之前审计。这是最后一道防线,能拦住大部分问题。
  • 定时全量审计:每天或每周跑一次全量,兜底那些增量审计漏掉的。

我个人的偏好是"生成后立即审计 + 提交前审计"双保险。生成后审计负责快速修正,提交前审计负责兜底。定时全量审计作为补充,但不要指望它——问题发现得越晚,修复成本越高。

注意:生成后立即审计时,不要让审计结果直接触发自动修改。我试过让智能体根据审计结果自动改代码,结果它把一个"看起来像 SQL 注入"的正常字符串拼接改成了参数化查询,反而引入了 bug。审计结果应该作为"建议"呈现,由智能体或人来决定是否采纳。

4. 实操:从零搭一个可用的审计 skill

4.1 环境准备与依赖选择

假设我们用 Python 来实现这个 skill(这是最常见的选择,生态也最全)。核心依赖我建议这几类:

  • 代码解析:tree-sitter或各语言的原生解析库。tree-sitter的好处是支持多语言,一套接口搞定 Python、JavaScript、Go 等。
  • 依赖漏洞查询:osv-scanner的 Python 封装,或者直接调 OSV 的 API。
  • 密钥检测:detect-secrets或gitleaks,这两个都是成熟工具,没必要自己造轮子。
  • 结果输出:pydantic做数据模型,保证输出结构稳定。

这里有个选型心得:能复用成熟工具的就别自己写。我一开始想自己实现密钥检测的正则,写了 20 多条规则还是漏,后来换成detect-secrets,它的规则库有上百条,还支持自定义插件。省下来的时间用来做数据流分析,价值高得多。

4.2 核心审计逻辑的实现骨架

下面是一个简化的实现骨架,展示审计主流程怎么串起来:

from dataclasses import dataclass from typing import List import tree_sitter_python as tspython from tree_sitter import Language, Parser @dataclass class Finding: file_path: str line: int type: str confidence: str snippet: str suggestion: str class SecurityAuditor: def __init__(self): self.parser = Parser(Language(tspython.language())) self.dangerous_calls = { "eval", "exec", "os.system", "pickle.loads" } def scan_file(self, file_path: str, focus: List[str]) -> List[Finding]: with open(file_path, "r", encoding="utf-8") as f: source = f.read() tree = self.parser.parse(bytes(source, "utf-8")) findings = [] if "injection" in focus: findings.extend(self._check_injection(tree, source, file_path)) if "hardcoded_secret" in focus: findings.extend(self._check_secrets(source, file_path)) return findings def _check_injection(self, tree, source, file_path): findings = [] # 遍历 AST,找到危险函数调用 # 对每个调用,向上追溯参数来源 # 根据来源判断置信度 return findings def _check_secrets(self, source, file_path): # 调用 detect-secrets 的扫描逻辑 return []

这个骨架的关键在于_check_injection里的"向上追溯参数来源"。具体做法是:从危险函数调用的参数节点出发,沿着 AST 往上找变量定义,判断这个变量是否来自函数参数、HTTP 请求、环境变量等外部输入。如果来自外部输入且没有经过校验,置信度标"高";如果来自本地配置,标"低"。

4.3 数据流追踪的具体做法

数据流追踪是审计 skill 里技术含量最高的部分。我用的是"污点分析"的思路:把外部输入标记为"污点源",把危险函数标记为"污点汇聚点",然后看污点能不能从源流到汇聚点。

实现上,我建议先做"函数内追踪",再做"跨函数追踪"。函数内追踪简单,就是在一个函数体里找变量赋值链。跨函数追踪复杂,需要构建调用图,但收益也大——很多注入问题的污点源和汇聚点不在同一个函数里。

跨函数追踪的一个实用简化:只追踪一层调用。也就是如果函数 A 调用了函数 B,B 的参数来自 A 的外部输入,且 B 内部有危险操作,就报警。不做更深层的追踪,因为再深误报率会飙升,而且实现复杂度指数级上升。这个取舍我在多个项目里验证过,性价比最高。

4.4 审计报告的生成与排序

报告生成这块,我坚持前面说的"按可操作性排序"。具体排序算法:

score = severity_weight * confidence_weight * exploitability_weight
  • severity_weight:问题类型的固有严重程度(注入 > 密钥泄露 > 配置问题)
  • confidence_weight:置信度(高=1.0,中=0.6,低=0.3)
  • exploitability_weight:可利用性(无需认证=1.0,需认证=0.5,需特定条件=0.2)

按这个 score 降序排列,开发同学从上往下看就行。实测下来,前 5 条里通常有 2-3 条是真问题,这个命中率足够让开发同学愿意继续看下去。

5. 误报处理:审计 skill 能不能用,全看这一关

5.1 误报为什么这么致命

我前面提过一个数据:只做模式匹配时,5000 行代码扫出 200 多个"高危"。这不是个例,是常态。误报的致命之处不在于"多",而在于它摧毁了信任。开发同学被误报坑过几次之后,就会形成"这个工具报的都不准"的认知,然后所有报告都不看了。这时候哪怕工具后来改进了,真问题也照样被忽略。

所以误报处理不是"锦上添花",是"生死线"。我的标准是:高置信度的误报率要控制在 5% 以内,中置信度控制在 20% 以内。超过这个线,工具就不可用了。

5.2 降低误报的四个手段

手段一:白名单机制。对确认过的误报,记录到白名单里,下次不再报。白名单要支持按"文件+行号+问题类型"精确匹配,也要支持按"模式"匹配(比如某个特定的函数调用永远不报)。

手段二:上下文感知。前面说的数据流追踪就是干这个的。一个eval调用,如果参数是硬编码的常量,就不报;如果参数来自用户输入,才报。

手段三:置信度分级。不确定的一律标"低置信度",而不是不报。低置信度的报告折叠起来,开发同学想看再看。这样既不漏,又不干扰。

手段四:人工反馈闭环。让开发同学能对每条报告标记"真问题"或"误报",这些反馈用来优化规则。这个闭环建立起来之后,误报率会随着使用时间下降。

5.3 一个真实的误报排查过程

我遇到过一个典型的误报:审计 skill 报了一个"命令注入",位置是一个subprocess.run调用,参数是一个变量。看起来确实可疑,但开发同学说这个变量是硬编码的。

排查过程是这样的:先看变量定义,发现它来自一个函数返回值;再看那个函数,发现它返回的是一个常量字符串;但常量字符串是通过一个字典查出来的,字典的 key 来自配置。追到配置这一层,发现配置是打包在代码里的,不可外部修改。所以最终结论是:这个变量确实不可控,是误报。

这个案例说明,数据流追踪的深度直接决定误报率。只追一层,会误报;追到源头,才能准确判断。但追得太深又影响性能。我的折中是:默认追三层,如果三层还没追到源头,标"中置信度"而不是"高置信度"。

提示:排查误报时,一定要把完整的追踪链路记录下来。这不仅是给开发同学看的证据,也是优化规则的依据。我习惯在报告里附上"数据流路径",让开发同学一眼看到污点是怎么流的。

6. 我在落地过程中踩过的坑

6.1 坑一:把审计 skill 做成了"另一个扫描器"

最开始我的思路是做一个"更聪明的扫描器",结果做着做着发现,如果只是扫描器,那和现成的 SAST 工具没本质区别。真正的差异在于"和智能体的协同"——审计结果要能被智能体理解、采纳、修正。

这个认知转变之后,我把重点从"扫描能力"转到了"结果的可操作性"上。每条报告都带上修复建议,建议要具体到可以直接替换的代码。这个改动之后,开发同学的采纳率明显上升。

6.2 坑二:忽略了审计本身的性能开销

审计 skill 如果太慢,智能体就不愿意调用了。我一开始的实现,单文件审计要 2-3 秒,一个改动涉及 5 个文件就是十几秒,智能体的响应变得很卡。

优化方向有几个:一是缓存 AST 解析结果,同一个文件没改动就不重新解析;二是把审计做成异步的,不阻塞智能体的主流程;三是增量审计,只审改动部分。这三个做完,单次审计降到了 1 秒以内。

6.3 坑三:审计规则"一刀切"

不同项目对安全的容忍度不一样。一个内部工具和一个面向公网的服务,安全要求天差地别。如果审计规则一刀切,内部工具会被大量"高危"淹没,公网服务又可能漏掉关键问题。

解决办法是让规则可配置。项目根目录放一个.security-audit.yaml,里面可以指定:

severity_threshold: medium ignore_rules: - hardcoded_secret # 内部工具,密钥硬编码可接受 focus_rules: - sql_injection - command_injection

这样每个项目可以按自己的情况调整,审计结果才有针对性。

6.4 坑四:忘了审计 skill 自身的安全

这是个有点讽刺的坑:审计别人代码的 skill,自己可能就有安全问题。比如 skill 读取文件时如果路径没做校验,可能被利用来读取敏感文件;skill 执行外部命令时如果参数没转义,可能被注入。

所以 skill 自身也要过一遍审计。我现在的做法是,skill 的所有文件操作都限制在项目根目录内,所有外部命令调用都用参数列表形式而不是 shell 字符串。这两条是底线。

7. 这套东西后续还能怎么扩展

security-audit-skill目前聚焦在"发现问题",但它的价值可以往两端延伸。

往前延伸,是"预防"。把审计规则前置到代码生成阶段,让智能体在生成代码时就避开已知的危险模式。比如生成数据库操作时,默认就用参数化查询,而不是先生成再审计再改。这需要把审计规则转化成生成约束,是另一个层面的工作。

往后延伸,是"修复"。审计发现问题之后,能不能自动生成修复补丁?这个方向我试过一部分,对于模式化的问题(比如硬编码密钥替换成环境变量读取),自动修复的成功率还不错;对于需要理解业务逻辑的问题(比如权限校验缺失),自动修复风险太大,还是得人工来。

还有一个方向是"审计知识的沉淀"。每次人工确认的误报、每个修复的案例,都可以沉淀成规则或提示词,让 skill 越来越准。这个沉淀机制建起来之后,skill 就从一个静态工具变成了一个会进化的系统。

我个人在实际操作中的体会是,安全审计这件事,工具能解决 80% 的"发现"问题,但"判断"和"修复"还是得靠人。skill 的定位应该是"把人的注意力引导到真正重要的地方",而不是"替代人做判断"。想清楚这个定位,设计上的很多取舍就自然清晰了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进