LLM 替代 SAST?Claude Code 安全审查工具的设计取舍与工程启示

1. 前言

Anthropic 开源了一个用于代码提交时的安全检查工具:Claude Code Security Review。这个工具没有使用传统的静态分析方法,而是完全使用大模型来检查代码的安全性。检查的内容涵盖了我们常见的安全问题,如 SQL 注入、XSS、CSRF 等。

从这个工具的设计里,我们可以看到:工具在 CI/CD 模式和本地检查时,设计上的差异;以及大模型检查安全问题时的处理逻辑;最后如何完成检查结果误报的消除。

2. 安全检查的两种使用模式

这个项目里,Claude 使用工具的方式有两种完全不同的架构,对应两个使用场景。

2.1. 方式一:GitHub Action --- Claude Code CLI 子进程调用

2.1.1. 架构

text 复制代码
Python 脚本 (github_action_audit.py)
    │
    ├── 1. 通过 GitHub API 拉取 PR 数据 + Diff
    ├── 2. 拼装完整 prompt(prompts.py)
    ├── 3. subprocess.run(['claude', '--output-format', 'json', ...], input=prompt)
    │       └── Claude Code CLI 作为黑盒子进程运行
    │           ├── 模型内部可使用文件搜索工具探索仓库
    │           └── 输出 JSON 格式的 findings
    └── 4. Python 脚本解析 JSON → 硬规则过滤 → 可选 Claude API 二次过滤 → 输出

2.1.2. 工具使用特点

Prompt 里对工具的描述非常模糊:

text 复制代码
Phase 1 - Repository Context Research (Use file search tools):
- Identify existing security frameworks and libraries in use

只说了 "Use file search tools",没有指定具体用哪个工具、怎么调用。因为 Claude Code CLI 作为一个封装好的运行时,它自带了一组内置工具(文件读取、搜索等),模型在这个运行时里自动知道可以用什么。

实际的工具权限控制在 CLI 命令行参数里:

python 复制代码
cmd = [
    'claude',
    '--output-format', 'json',
    '--model', DEFAULT_CLAUDE_MODEL,
    '--disallowed-tools', 'Bash(ps:*)'   # 禁止执行 ps 命令
]

从命令行参数看,显式声明的唯一排除项是 Bash(ps:*)(防止模型查看系统进程)。CLI 其余默认权限需以官方文档与仓库当前实现为准,不宜简单理解为"全部放开"。

Prompt 里要求模型用工具做的事:

阶段 工具用途 具体行为
Phase 1 探索仓库上下文 搜索安全框架、编码模式、校验函数
Phase 3 逐文件检查 读取改动文件、追踪数据流
Diff 太大时 替代 diff 阅读 模型自己去读改动文件(prompt 里说 "use the file exploration tools")

模型和工具的关系是"外包"式的: Python 脚本负责所有的编排逻辑(拉数据、拼 prompt、过滤结果),Claude Code CLI 只负责"拿到 prompt 后做安全分析",工具使用完全在 CLI 内部完成,外部脚本看不到模型用了什么工具、怎么用的。需要说明的是,"黑盒"指的是工具调用轨迹不进入最终 JSON 输出,并非完全无法审计------CLI 的 --output-format json 仍会输出最终的 findings。


2.2. 方式二:/security-review 斜杠命令 --- Claude Code 原生工具编排

2.2.1. 架构

text 复制代码
用户在 Claude Code 终端输入 /security-review
    │
    ├── security-review.md 作为 prompt 注入
    │   ├── 通过 !`git diff` 等 shell 命令自动获取上下文
    │   ├── allowed-tools 声明可用工具集
    │   └── 模型直接使用 Read/Grep/Glob/Bash 等工具
    │
    ├── Step 1: 创建子任务(sub-task)识别漏洞
    │       └── 子任务独立使用工具探索仓库 + 分析代码
    │
    ├── Step 2: 对每个 finding 创建并行过滤子任务
    │       └── 过滤子任务只能用读取工具,不能执行命令
    │
    └── Step 3: 汇总,丢弃 confidence < 8 的 finding

2.2.2. 工具声明

security-review.md 开头有一段 YAML frontmatter,显式声明了可用工具:

yaml 复制代码
---
allowed-tools: Bash(git diff:*), Bash(git status:*), Bash(git log:*), 
               Bash(git show:*), Bash(git remote show:*), 
               Read, Glob, Grep, LS, Task
---
工具 权限范围 用途
Bash(git diff:*) 只允许 git diff 相关命令 获取代码变更
Bash(git status:*) 只允许 git status 了解当前分支状态
Bash(git log:*) 只允许 git log 查看 commit 历史
Bash(git show:*) 只允许 git show 查看特定 commit 内容
Bash(git remote show:*) 只允许 git remote show 查看远程仓库信息
Read 读取文件 阅读源码
Glob 文件模式匹配 搜索特定类型的文件
Grep 内容搜索 在代码库中搜索特定模式
LS 列出目录 了解项目结构
Task 创建子任务 实现发现-验证的双轮架构

(以上 frontmatter 与权限表以仓库当前版本为准。)

注意 Bash 的限制: 只允许 git 相关的 5 种命令,其他所有 bash 操作都被禁止。这比 GitHub Action 版本的 --disallowed-tools 'Bash(ps:*)'(仅显式排除 ps)严格得多。

2.2.3. 自动注入的上下文

prompt 里用 ! 语法在 prompt 渲染时自动执行 shell 命令并注入结果:

markdown 复制代码
GIT STATUS:
!`git status`

FILES MODIFIED:
!`git diff --name-only origin/HEAD...`

COMMITS:
!`git log --no-decorate origin/HEAD...`

DIFF CONTENT:
!`git diff --merge-base origin/HEAD`

这意味着模型启动时通常已经拿到了变更上下文,不需要自己再跑 git 命令。工具主要用于深入探索------比如读某个文件的完整内容、搜索某个函数在项目中的调用位置等。若 diff 过大或命令执行失败,仍可能需要模型自行用 Read/Grep 补充。


2.3. 两种方式的核心差异对比

维度 GitHub Action(CLI 子进程) /security-review(斜杠命令)
工具声明方式 命令行参数排除(相对 CLI 默认工具集的黑名单) YAML frontmatter 声明(白名单)
工具权限粒度 粗:仅显式排除 Bash(ps:*) 细:只允许 git 的 5 种命令 + 只读工具
上下文获取 Python 脚本通过 GitHub API 拉取,塞进 prompt ! 语法自动执行 git 命令注入
工具使用者 单个 Claude 实例 主任务 + 多个子任务(Task 工具)
编排控制 Python 脚本控制流程 Prompt 里的自然语言指令控制流程
过滤阶段的工具 Python 代码做正则 + 调小模型 API 过滤子任务只能用 Read 类工具,禁止 Bash
输出格式 JSON(给脚本解析) Markdown(给人阅读)
运行环境 CI/CD 服务器(无人值守) 开发者本地终端(有人交互)

2.4. 两种架构的设计哲学

2.4.1. GitHub Action:脚本编排 + 模型分析

text 复制代码
┌─────────────────────────────────────┐
│         Python 脚本(编排层)         │
│  ┌──────┐  ┌──────┐  ┌──────────┐  │
│  │拉数据│→ │拼prompt│→ │调Claude CLI│  │
│  └──────┘  └──────┘  └──────────┘  │
│       ↓                             │
│  ┌──────────┐  ┌────────────────┐   │
│  │正则硬过滤 │→ │Claude API 软过滤│   │
│  └──────────┘  └────────────────┘   │
│       ↓                             │
│  ┌──────────┐                       │
│  │输出报告   │                       │
│  └──────────┘                       │
└─────────────────────────────────────┘
         ↕ stdin/stdout
┌─────────────────────────────────────┐
│     Claude Code CLI(黑盒分析层)     │
│  - 模型 + 内置工具                    │
│  - 只负责"分析 diff,输出 JSON"       │
│  - 外部看不到工具使用过程              │
└─────────────────────────────────────┘

哲学:把模型当成一个"分析函数"来调用。 Python 脚本控制一切流程,模型只负责中间那一步"看代码找漏洞"。过滤、格式化、评论发布全部在脚本层完成。适合无人值守的 CI/CD 场景。

2.4.2. /security-review:Prompt 编排 + 模型自主

text 复制代码
┌─────────────────────────────────────┐
│       security-review.md(全在 prompt 里)│
│                                       │
│  ┌────────────┐                       │
│  │ !`git diff` │ ← 自动注入上下文      │
│  └──────┬─────┘                       │
│         ↓                             │
│  ┌──────────────┐                     │
│  │ 分析 Prompt   │ ← 三阶段方法论      │
│  └──────┬───────┘                     │
│         ↓                             │
│  ┌──────────────────────────────────┐ │
│  │ Step 1: Task 子任务(发现漏洞)    │ │
│  │   └─ 模型自主使用 Read/Grep/Glob │ │
│  └──────┬───────────────────────────┘ │
│         ↓                             │
│  ┌──────────────────────────────────┐ │
│  │ Step 2: N 个并行 Task 子任务      │ │
│  │   (每个 finding 一个过滤子任务)  │ │
│  │   └─ 只能用 Read,不能用 Bash    │ │
│  └──────┬───────────────────────────┘ │
│         ↓                             │
│  ┌──────────────────────────────────┐ │
│  │ Step 3: 丢弃 confidence < 8     │ │
│  └──────────────────────────────────┘ │
└─────────────────────────────────────┘

哲学:把模型当成一个"自主工程师"来指挥。 整个流程通过 prompt 中的自然语言指令控制,模型自己决定用什么工具、读什么文件、怎么分析。发现-验证的双轮架构也完全靠 Task 工具实现。适合交互式的本地开发场景。


2.5. 总结

GitHub Action 是"脚本驱动,模型打工";/security-review 是"Prompt 驱动,模型自主"。 前者用 Python 代码做编排、模型做分析;后者把编排也交给了模型,通过 prompt 中的结构化指令让模型自己规划工具使用和任务分解。两种方式各有适用场景,CI/CD 需要确定性和可控性所以用脚本编排,本地审查需要灵活性和深度所以让模型自主。


3. 工具的扫描流程

claude-code-security-review 的完整安全检查流程:
#mermaid-svg-shfET47Rjodfpgmu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-shfET47Rjodfpgmu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-shfET47Rjodfpgmu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-shfET47Rjodfpgmu .error-icon{fill:#552222;}#mermaid-svg-shfET47Rjodfpgmu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-shfET47Rjodfpgmu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-shfET47Rjodfpgmu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-shfET47Rjodfpgmu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-shfET47Rjodfpgmu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-shfET47Rjodfpgmu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-shfET47Rjodfpgmu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-shfET47Rjodfpgmu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-shfET47Rjodfpgmu .marker.cross{stroke:#333333;}#mermaid-svg-shfET47Rjodfpgmu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-shfET47Rjodfpgmu p{margin:0;}#mermaid-svg-shfET47Rjodfpgmu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-shfET47Rjodfpgmu .cluster-label text{fill:#333;}#mermaid-svg-shfET47Rjodfpgmu .cluster-label span{color:#333;}#mermaid-svg-shfET47Rjodfpgmu .cluster-label span p{background-color:transparent;}#mermaid-svg-shfET47Rjodfpgmu .label text,#mermaid-svg-shfET47Rjodfpgmu span{fill:#333;color:#333;}#mermaid-svg-shfET47Rjodfpgmu .node rect,#mermaid-svg-shfET47Rjodfpgmu .node circle,#mermaid-svg-shfET47Rjodfpgmu .node ellipse,#mermaid-svg-shfET47Rjodfpgmu .node polygon,#mermaid-svg-shfET47Rjodfpgmu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-shfET47Rjodfpgmu .rough-node .label text,#mermaid-svg-shfET47Rjodfpgmu .node .label text,#mermaid-svg-shfET47Rjodfpgmu .image-shape .label,#mermaid-svg-shfET47Rjodfpgmu .icon-shape .label{text-anchor:middle;}#mermaid-svg-shfET47Rjodfpgmu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-shfET47Rjodfpgmu .rough-node .label,#mermaid-svg-shfET47Rjodfpgmu .node .label,#mermaid-svg-shfET47Rjodfpgmu .image-shape .label,#mermaid-svg-shfET47Rjodfpgmu .icon-shape .label{text-align:center;}#mermaid-svg-shfET47Rjodfpgmu .node.clickable{cursor:pointer;}#mermaid-svg-shfET47Rjodfpgmu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-shfET47Rjodfpgmu .arrowheadPath{fill:#333333;}#mermaid-svg-shfET47Rjodfpgmu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-shfET47Rjodfpgmu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-shfET47Rjodfpgmu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-shfET47Rjodfpgmu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-shfET47Rjodfpgmu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-shfET47Rjodfpgmu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-shfET47Rjodfpgmu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-shfET47Rjodfpgmu .cluster text{fill:#333;}#mermaid-svg-shfET47Rjodfpgmu .cluster span{color:#333;}#mermaid-svg-shfET47Rjodfpgmu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-shfET47Rjodfpgmu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-shfET47Rjodfpgmu rect.text{fill:none;stroke-width:0;}#mermaid-svg-shfET47Rjodfpgmu .icon-shape,#mermaid-svg-shfET47Rjodfpgmu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-shfET47Rjodfpgmu .icon-shape p,#mermaid-svg-shfET47Rjodfpgmu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-shfET47Rjodfpgmu .icon-shape .label rect,#mermaid-svg-shfET47Rjodfpgmu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-shfET47Rjodfpgmu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-shfET47Rjodfpgmu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-shfET47Rjodfpgmu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ≤ 阈值(代码常量)
> 阈值 (TOO_LONG)
是
否
Claude 大模型内部分析
Phase 1: 仓库上下文研究

探索安全框架、编码模式
Phase 2: 对比分析

新代码 vs 已有安全模式
Phase 3: 漏洞评估

数据流追踪、注入点识别
PR 创建/更新
GitHub Action 触发
获取 PR 数据与 Diff
过滤生成文件 & 排除目录
构造安全审计 Prompt
Prompt 大小检查
调用 Claude Code CLI

claude --output-format json
去掉 Diff 内容,重试
Claude 大模型执行三阶段分析
输出 JSON 格式安全发现

(文件/行号/严重级别/漏洞类型/

描述/利用场景/修复建议/置信度)
第一轮: 硬规则过滤

丢弃 DoS/资源耗尽/速率限制等
启用 Claude API

二次过滤?
第二轮: 调用 Claude API

逐条判断是否误报
仅硬规则过滤
第三轮: 排除目录过滤

命中排除目录的发现丢弃
生成最终报告
以 PR 评论形式

标注到具体代码行
上传 JSON 结果

作为 Artifact


4. 工具的提示词

提示词是一个工具的灵魂。Prompt 的内容在 claudecode/prompts.py 这个文件里,具体是 get_security_audit_prompt() 这个函数。

4.1. 整个 prompt 核心结构

模块 内容
角色设定 "You are a senior security engineer conducting a focused security review..."
上下文注入 仓库名、作者、改动文件列表、增删行数(从 GitHub API 拿到的 pr_data)
Diff 内容 完整的 PR diff(如果太大则省略,让 Claude 自己去探索文件)
分析目标 只关注高置信度(>80%)的安全漏洞,不做通用 code review
排除清单 明确不报 DoS、资源耗尽、速率限制、磁盘上的密钥等
检查类别 输入验证、认证授权、密码学、注入/RCE、数据泄露等详细分类
三阶段方法论 Phase 1 仓库上下文研究 → Phase 2 对比分析 → Phase 3 漏洞评估
输出格式 严格的 JSON schema,包含 file、line、severity、category、description、exploit_scenario、recommendation、confidence
严重性定义 HIGH(直接可利用)、MEDIUM(需特定条件)、LOW(纵深防御)
置信度打分 0.9-1.0 / 0.8-0.9 / 0.7-0.8 / <0.7 不报告
自定义扩展 支持通过 custom_scan_instructions 参数追加组织专属的安全扫描规则

4.2. 角色设定 + 上下文注入

原文:

text 复制代码
You are a senior security engineer conducting a focused security review
of GitHub PR #{pr_data['number']}: "{pr_data['title']}"

CONTEXT:
- Repository: {repo_name}
- Author: {author}
- Files changed: {changed_files}
- Lines added: {additions}
- Lines deleted: {deletions}

Files modified:
{files_changed}

解读:

开头让模型扮演「高级安全工程师」,这不是随便写的------角色设定直接影响模型输出的专业程度和判断标准。「senior」意味着它应该有经验去区分真正的漏洞和理论上的风险。

上下文部分把 PR 的元数据全部注入:仓库名、作者、改动文件数、增删行数、具体文件列表。这些信息让模型的审查有了具体的锚点,而不是泛泛而谈。知道改了 3 个文件和改了 200 个文件,审查策略显然不同。


4.3. Diff 内容注入

原文:

python 复制代码
# 如果 diff 存在且未超长
PR DIFF CONTENT:

{pr_diff}

text 复制代码
Review the complete diff above. This contains all code changes in the PR.

# 如果 diff 太大被省略
NOTE: PR diff was omitted due to size constraints.
Please use the file exploration tools to examine the specific files
that were changed in this PR.

解读:

这里有两条路径。正常情况下,完整的 unified diff 会被直接塞进 prompt,模型可以直接看到每一行新增和删除的代码。

但如果 PR 太大(超过第 3 节所述的 prompt 大小阈值),diff 会被省略,转而让模型用 Claude Code 自带的文件搜索工具自己去读文件。这是一个降级策略------大 PR 不会让 prompt 爆掉,而是让模型切换到「主动探索」模式。


4.4. 审查目标定义

原文:

text 复制代码
OBJECTIVE:
Perform a security-focused code review to identify HIGH-CONFIDENCE
security vulnerabilities that could have real exploitation potential.
This is not a general code review - focus ONLY on security implications
newly added by this PR. Do not comment on existing security concerns.

解读:

这段极其关键,划定了三个边界:

  1. 只找高置信度的漏洞 --- "HIGH-CONFIDENCE" 和 "real exploitation potential" 反复强调不要报理论风险。这是整个 prompt 最核心的设计哲学:宁可漏报,不可误报。
  2. 只做安全审查,不做通用 Code Review --- 代码风格、性能、可维护性这些统统不管。边界越窄,模型的注意力越集中。
  3. 只看本次 PR 新引入的问题 --- 不翻旧账。如果项目里本来就有一段不安全的代码,但这次 PR 没改它,就不报。这避免了审查结果被大量历史遗留问题淹没。

注意:这里的"宁可漏报,不可误报"是检测阶段(模型输出 finding)的取向。


4.5. 关键指令(CRITICAL INSTRUCTIONS)

原文:

text 复制代码
CRITICAL INSTRUCTIONS:
1. MINIMIZE FALSE POSITIVES: Only flag issues where you're >80%
   confident of actual exploitability
2. AVOID NOISE: Skip theoretical issues, style concerns, or
   low-impact findings
3. FOCUS ON IMPACT: Prioritize vulnerabilities that could lead to
   unauthorized access, data breaches, or system compromise
4. EXCLUSIONS: Do NOT report the following issue types:
   - Denial of Service (DOS) vulnerabilities
   - Secrets or sensitive data stored on disk
   - Rate limiting or resource exhaustion issues

解读:

四条铁律,每条都有明确的工程考量:

指令 为什么这么设计
>80% 置信度才报 给模型一个量化的门槛。低于 80% 的猜测性结论直接过滤,大幅降低误报
跳过理论问题 很多"漏洞"在理论上存在但实际无法利用(比如需要物理接触的攻击),报出来只是噪声
聚焦高影响 未授权访问、数据泄露、系统被控制------这些才是真正的安全事故。UI 上的小问题不算
明确排除三类 DoS 和资源耗尽类问题在 PR 级别很难判断,而且通常有专门的基础设施防护;磁盘上的密钥有专门的扫描工具(如 git-secrets、gitleaks)处理,不需要这里重复

第 4 条排除清单特别值得注意------它反映了一个工程判断:不同的安全问题应该由不同的工具在不同的层次处理。这个工具只负责「代码逻辑漏洞」这一层。


4.6. 安全检查类别(SECURITY CATEGORIES)

这是 prompt 里最长的部分,定义了模型要检查的五大类漏洞:

4.6.1. 输入验证漏洞

text 复制代码
- SQL injection via unsanitized user input
- Command injection in system calls or subprocesses
- XXE injection in XML parsing
- Template injection in templating engines
- NoSQL injection in database queries
- Path traversal in file operations

解读: 覆盖了 OWASP Top 10 里最经典的注入类漏洞。注意它没有笼统地说「检查注入」,而是列出了具体的注入类型和触发场景(system calls、XML parsing、templating engines),这让模型知道该在什么样的代码模式中寻找什么问题。

4.6.2. 认证与授权问题

text 复制代码
- Authentication bypass logic
- Privilege escalation paths
- Session management flaws
- JWT token vulnerabilities
- Authorization logic bypasses

解读: 这块关注的是「谁能做什么」的逻辑缺陷。特别提了 JWT,因为 JWT 的常见实现错误(算法混淆、不验签、密钥硬编码)是近年高频漏洞。

4.6.3. 密码学与密钥管理

text 复制代码
- Hardcoded API keys, passwords, or tokens
- Weak cryptographic algorithms or implementations
- Improper key storage or management
- Cryptographic randomness issues
- Certificate validation bypasses

解读: 硬编码密钥和弱随机数是 PR 中最容易引入也最容易检测的问题。证书验证绕过(比如把 SSL 验证关掉)在开发阶段很常见但极其危险。

4.6.4. 注入与代码执行

text 复制代码
- Remote code execution via deserialization
- Pickle injection in Python
- YAML deserialization vulnerabilities
- Eval injection in dynamic code execution
- XSS vulnerabilities (reflected, stored, DOM-based)

解读: 反序列化和 eval 注入是 RCE 的主要途径。这里特意提到了 Python 的 pickle 和 YAML,说明 prompt 设计者知道这些是特定语言中高频被忽略的攻击面。XSS 列出了三种子类型,覆盖了服务端和客户端两个方向。

4.6.5. 数据泄露

text 复制代码
- Sensitive data logging or storage
- PII handling violations
- API endpoint data leakage
- Debug information exposure

解读: 这类问题不是传统的「漏洞」,但在实际安全事故中占比极高。生产环境日志里打印了用户密码、API 返回了多余的内部字段、debug 模式没有关闭------这些都是 PR 审查中最容易发现也最容易被忽略的问题。


4.7. 三阶段分析方法论(ANALYSIS METHODOLOGY)

4.7.1. Phase 1 --- 仓库上下文研究

  • 原文
text 复制代码
Phase 1 - Repository Context Research (Use file search tools):
- Identify existing security frameworks and libraries in use
- Look for established secure coding patterns in the codebase
- Examine existing sanitization and validation patterns
- Understand the project's security model and threat model
4.7.1.1. 步骤解读

核心目的:先搞懂这个项目"本来是怎么做安全的",再去看新代码有没有破坏它。

这一步要求 Claude 在分析 diff 之前,先用文件搜索工具去探索整个仓库,搞清楚四件事:

  1. 用了哪些安全框架和库
    比如项目用的是 Django 的 ORM 还是原生 SQL?用的是 bcrypt 还是自己写的哈希?知道了基线,才能判断新代码有没有绕开已有的保护。
  2. 已有的安全编码模式
    比如这个项目是不是所有数据库操作都走参数化查询?是不是所有用户输入都经过某个统一的 sanitize 函数?这些"约定俗成"的模式就是安全基线。
  3. 现有的过滤和校验模式
    具体看项目里已有的输入校验怎么写的、XSS 过滤用的什么方案、权限校验在哪一层做。
  4. 项目的安全模型和威胁模型
    这个项目的信任边界在哪?谁是可信用户、谁不是?认证和授权是怎么分层设计的?
  • 点评:
    传统 SAST 工具不看这些------它们拿着固定规则扫,不管项目本身的安全设计是什么。而 LLM 先理解了"这个项目应该怎么做安全",这样确定了现有代码的安全基线,避免过度的扫描和报错。这样后面报告的错误,容易被开发者接受,避免大量的误报。

4.7.2. Phase 2 --- 对比分析

  • 原文
text 复制代码
Phase 2 - Comparative Analysis:
- Compare new code changes against existing security patterns
- Identify deviations from established secure practices
- Look for inconsistent security implementations
- Flag code that introduces new attack surfaces
4.7.2.1. 步骤解读

核心目的:拿新代码和 Phase 1 建立的基线做对比,找"不一致"和"偏离"。

这一步是承上启下的桥梁,做四件事:

  1. 新代码 vs 已有安全模式
    例如整个项目都用参数化查询,唯独这次 PR 里有人拼了一条 SQL 字符串,这就是偏离。
  2. 偏离已建立的安全实践
    例如项目约定所有 API 端点都要过认证中间件,新加的接口却跳过了。
  3. 不一致的安全实现
    例如同一个项目里,A 模块用 JWT 验证、B 模块用 session 验证,新代码又搞了第三种方式。不一致本身就是风险。
  4. 引入新的攻击面
    例如原来项目没有文件上传功能,这次 PR 加了一个,那就是新增了攻击面,需要重点关注。
  • 点评:
    很多漏洞不是"写了一段明显有问题的代码",而是"这段代码放在这个项目的上下文里才有问题"。同样的写法在另一个项目里可能完全安全。Phase 2 做的就是这个上下文关联。

4.7.3. Phase 3 --- 漏洞评估

text 复制代码
Phase 3 - Vulnerability Assessment:
- Examine each modified file for security implications
- Trace data flow from user inputs to sensitive operations
- Look for privilege boundaries being crossed unsafely
- Identify injection points and unsafe deserialization
4.7.3.1. 步骤解读

核心目的:对具体的改动逐文件做深度漏洞分析。

前两步是"侦察"和"对比",这一步才是真正动手找漏洞,做四件事:

  1. 逐文件检查安全影响
    每个改动的文件都过一遍,评估这个改动从安全角度看意味着什么。
  2. 追踪数据流:从用户输入到敏感操作
    这是最核心的动作。比如用户提交的表单数据,最终流向了数据库查询、文件写入、系统命令执行中的哪一步?中间有没有经过校验和过滤?这条链路上任何一个环节缺失就是漏洞。
  3. 权限边界被不安全地跨越
    比如一个普通用户的请求,是否在某一步拿到了管理员才能执行的操作?水平越权(看别人的数据)和垂直越权(做更高权限的事)都在这一步检查。
  4. 注入点和反序列化
    SQL 注入、命令注入、XSS、不安全的 pickle/yaml 反序列化等具体技术漏洞,在这一步定位。

4.7.4. 三阶段的关系

#mermaid-svg-BFE345tcKaJ4ZaMC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BFE345tcKaJ4ZaMC .error-icon{fill:#552222;}#mermaid-svg-BFE345tcKaJ4ZaMC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BFE345tcKaJ4ZaMC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BFE345tcKaJ4ZaMC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BFE345tcKaJ4ZaMC .marker.cross{stroke:#333333;}#mermaid-svg-BFE345tcKaJ4ZaMC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BFE345tcKaJ4ZaMC p{margin:0;}#mermaid-svg-BFE345tcKaJ4ZaMC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC .cluster-label text{fill:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC .cluster-label span{color:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC .cluster-label span p{background-color:transparent;}#mermaid-svg-BFE345tcKaJ4ZaMC .label text,#mermaid-svg-BFE345tcKaJ4ZaMC span{fill:#333;color:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC .node rect,#mermaid-svg-BFE345tcKaJ4ZaMC .node circle,#mermaid-svg-BFE345tcKaJ4ZaMC .node ellipse,#mermaid-svg-BFE345tcKaJ4ZaMC .node polygon,#mermaid-svg-BFE345tcKaJ4ZaMC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BFE345tcKaJ4ZaMC .rough-node .label text,#mermaid-svg-BFE345tcKaJ4ZaMC .node .label text,#mermaid-svg-BFE345tcKaJ4ZaMC .image-shape .label,#mermaid-svg-BFE345tcKaJ4ZaMC .icon-shape .label{text-anchor:middle;}#mermaid-svg-BFE345tcKaJ4ZaMC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BFE345tcKaJ4ZaMC .rough-node .label,#mermaid-svg-BFE345tcKaJ4ZaMC .node .label,#mermaid-svg-BFE345tcKaJ4ZaMC .image-shape .label,#mermaid-svg-BFE345tcKaJ4ZaMC .icon-shape .label{text-align:center;}#mermaid-svg-BFE345tcKaJ4ZaMC .node.clickable{cursor:pointer;}#mermaid-svg-BFE345tcKaJ4ZaMC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BFE345tcKaJ4ZaMC .arrowheadPath{fill:#333333;}#mermaid-svg-BFE345tcKaJ4ZaMC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BFE345tcKaJ4ZaMC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BFE345tcKaJ4ZaMC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BFE345tcKaJ4ZaMC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BFE345tcKaJ4ZaMC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BFE345tcKaJ4ZaMC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BFE345tcKaJ4ZaMC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BFE345tcKaJ4ZaMC .cluster text{fill:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC .cluster span{color:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BFE345tcKaJ4ZaMC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BFE345tcKaJ4ZaMC rect.text{fill:none;stroke-width:0;}#mermaid-svg-BFE345tcKaJ4ZaMC .icon-shape,#mermaid-svg-BFE345tcKaJ4ZaMC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BFE345tcKaJ4ZaMC .icon-shape p,#mermaid-svg-BFE345tcKaJ4ZaMC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BFE345tcKaJ4ZaMC .icon-shape .label rect,#mermaid-svg-BFE345tcKaJ4ZaMC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BFE345tcKaJ4ZaMC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BFE345tcKaJ4ZaMC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BFE345tcKaJ4ZaMC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Phase 1

侦察

这个项目怎么做安全的?
Phase 2

对比

新代码偏离了基线吗?
Phase 3

评估

偏离导致了什么具体漏洞?

这是一个从宏观到微观、从背景到细节的递进过程:

阶段 视角 类比
Phase 1 整个仓库 新来的安全工程师先花一天读项目文档和代码
Phase 2 PR 改动 vs 仓库基线 Code Review 时对比团队编码规范
Phase 3 具体代码行 渗透测试式的数据流追踪

这个设计的巧妙之处在于,Phase 1 和 Phase 2 大幅降低了 Phase 3 的误报率------传统 SAST 之所以误报多,就是因为缺少前两步的上下文理解,直接拿规则去匹配代码片段。而 LLM 先理解了项目的安全设计,再判断新代码是否真正构成威胁,判断质量自然更高。

  • 概括:
text 复制代码
Phase 1 - Repository Context Research(用搜索工具探索仓库)
  → 建立安全基线:这个项目怎么做安全的?

Phase 2 - Comparative Analysis(对比分析)
  → 新代码偏离了基线吗?偏离是否引入了风险?

Phase 3 - Vulnerability Assessment(漏洞评估)
  → 逐文件追踪数据流,定位具体的注入点和权限跨越

设计精妙之处: 这三步模拟了一个资深安全工程师的真实工作流------不是拿到 diff 就直接扫,而是先理解项目背景,再对比找异常,最后深入验证。这比「直接把 diff 扔给模型问有没有漏洞」效果好得多。


4.8. 输出格式定义(REQUIRED OUTPUT FORMAT)

原文:

json 复制代码
{
  "findings": [
    {
      "file": "path/to/file.py",
      "line": 42,
      "severity": "HIGH",
      "category": "sql_injection",
      "description": "...",
      "exploit_scenario": "Attacker could extract database contents...",
      "recommendation": "Replace string formatting with parameterized queries...",
      "confidence": 0.95
    }
  ],
  "analysis_summary": {
    "files_reviewed": 8,
    "high_severity": 1,
    "medium_severity": 0,
    "low_severity": 0,
    "review_completed": true
  }
}

解读:

每条发现包含 8 个字段,每个都有明确用途:

字段 作用
file + line 精确定位,可以直接生成 GitHub PR 的行级评论
severity 分级处理,HIGH 优先修复
category 机器可读的漏洞分类,便于统计和聚合
description 给人看的漏洞描述
exploit_scenario 攻击场景------这是降低误报的关键字段。如果模型写不出一个具体的攻击场景,说明这个"漏洞"可能是理论性的
recommendation 给出修复建议,让开发者可以直接行动
confidence 量化置信度,配合后面 >80% 的门槛过滤

analysis_summary 提供了全局视图,review_completed 字段可以检测模型是否中途被截断或超时。


4.9. 严重性分级(SEVERITY GUIDELINES)

原文:

text 复制代码
- HIGH: Directly exploitable vulnerabilities leading to RCE,
  data breach, or authentication bypass
- MEDIUM: Vulnerabilities requiring specific conditions but
  with significant impact
- LOW: Defense-in-depth issues or lower-impact vulnerabilities

解读:

三级划分非常清晰:

  • HIGH = 不需要任何前提条件就能被利用,后果严重(远程代码执行、数据泄露、绕过认证)
  • MEDIUM = 需要特定条件才能利用(比如攻击者需要在内网、或者需要特定的用户输入组合),但一旦利用后果也很严重
  • LOW = 纵深防御问题(defense-in-depth),单独不太可能造成安全事故

注意 prompt 最后有一句:"Focus on HIGH and MEDIUM findings only", LOW 级别的基本就不要报了。


4.10. 置信度打分标准(CONFIDENCE SCORING)

原文:

text 复制代码
- 0.9-1.0: Certain exploit path identified, tested if possible
- 0.8-0.9: Clear vulnerability pattern with known exploitation methods
- 0.7-0.8: Suspicious pattern requiring specific conditions to exploit
- Below 0.7: Don't report (too speculative)

解读:

这是整个 prompt 里最精巧的设计之一。它把置信度和可利用性绑定在一起:

分数段 含义 是否报告
0.9-1.0 明确的利用路径,甚至测试过了 必须报告
0.8-0.9 清晰的漏洞模式,有已知的利用方法 报告
0.7-0.8 可疑模式,需要特定条件 边缘地带
<0.7 太猜测性了 不报告

结合前面 >80% 的门槛,实际上 0.7-0.8 这一档也会被过滤掉,只有 0.8 以上的才会出现在最终结果中。

注意量表区分: 本节 confidence 是 0--1 制 ,用于模型输出 finding 时的自评;第 5.4 节二次过滤中的 confidence_score 是 1--10 制,用于 API 复审时的判断。两者是两套独立量表,不可直接比较。


4.11. 最终排除清单(FINAL REMINDER)

原文:

text 复制代码
IMPORTANT EXCLUSIONS - DO NOT REPORT:
- Denial of Service (DOS) vulnerabilities or resource exhaustion
- Secrets/credentials stored on disk
- Rate limiting concerns or service overload scenarios
- Memory consumption or CPU exhaustion issues
- Lack of input validation on non-security-critical fields

解读:

这份排除清单在 prompt 的头尾出现了两次(头部在 CRITICAL INSTRUCTIONS 里,尾部在这里),这是有意为之的------首尾呼应确保模型在生成过程中始终记得这些约束。

最后一条特别值得注意:「非安全关键字段缺少输入验证不要报」。这解决了一个非常常见的误报------很多 SAST 工具会对每个没有做输入校验的字段都报警,但实际上大多数字段(比如一个内部的计数器)不做校验也不会有安全问题。


4.12. 收尾指令

原文:

text 复制代码
Begin your analysis now. Use the repository exploration tools to
understand the codebase context, then analyze the PR changes for
security implications.

Your final reply must contain the JSON and nothing else.
You should not reply again after outputting the JSON.

解读:

两条硬约束:

  1. 执行顺序 --- 先探索仓库上下文(Phase 1),再分析 PR 改动。不允许跳过 Phase 1 直接下结论。
  2. 输出纯净 --- 最终回复只能是 JSON,不能有任何额外的文字解释、前言、总结。这是因为下游的 github_action_audit.py 需要直接解析 JSON,任何多余文字都会导致解析失败。"You should not reply again after outputting the JSON" 防止模型在 JSON 后面追加说明性文字。

4.13. 自定义扩展(custom_categories_section)

python 复制代码
if custom_scan_instructions:
    custom_categories_section = f"\n{custom_scan_instructions}\n"

解读:

prompt 在安全检查类别的末尾留了一个插槽,用户可以通过 custom-security-scan-instructions 参数注入组织专属的安全扫描规则。比如某个公司特别关注 GDPR 合规,就可以在这里追加一段关于 PII 处理的检查规则。这让 prompt 模板变得可扩展,而不需要改源码。

总结:

这段 prompt 的核心设计哲学可以用一句话概括:用严格的结构化约束,把一个通用大模型变成一个低误报率的安全审计专家。

它通过角色设定、边界划定、排除清单、置信度门槛、首尾呼应等多重机制,把模型的注意力牢牢锁定在「高置信度、高影响、本次 PR 新引入的安全漏洞」上。


5. 误报排除

误报一直是安全检查工具一个令人头疼的问题。大量的误报,往往使开发人员放弃使用安全检查工具。

对于告警,我们需要明确这个告警的价值。这里先解释工具中提到的两个概念:信号质量(SIGNAL QUALITY) 和 先例(PRECEDENTS)。

5.1. 告警的评估和预判

5.1.1. 信号质量(SIGNAL QUALITY)

SIGNAL QUALITY(信号质量)是从信号处理领域借来的概念,用在安全审查场景下,指的是:**怎么区分一个 finding 是"真正的安全威胁信号"还是"干扰噪声"。**通过这个概念,我们可以判断一个告警是否真正有价值,是否需要报告这个告警。

5.1.1.1. 信号 vs 噪声

在信号处理中,一个传感器收到的数据里混杂着两种东西:

  • 信号(Signal):真正有价值的信息(比如雷达探测到的敌机)
  • 噪声(Noise):无意义的干扰(比如云层反射的杂波)

工程师用**信噪比(Signal-to-Noise Ratio)**来衡量数据质量------信号占比越高,数据越有用。

安全审查面临完全相同的问题:

  • 信号:真正可利用的安全漏洞(SQL 注入、认证绕过、密钥泄露)
  • 噪声:理论上存在但实际无法利用的"假漏洞"(缺少限流、变量命名不安全、日志格式不规范)

传统 SAST 工具的信噪比很差。报 100 个问题,80 个是噪声。SIGNAL QUALITY CRITERIA 就是一套滤波器,用来提高信噪比。


5.1.1.2. 四问准则
  • 问题 1:有没有具体的、可利用的漏洞和清晰的攻击路径?

Is there a concrete, exploitable vulnerability with a clear attack path?

这一问区分的是**"存在风险"和"存在漏洞"**。

"存在风险"(噪声) "存在漏洞"(信号)
"这个函数没有做输入校验" "攻击者通过 search 参数传入 ' OR 1=1 --,可以绕过认证查询所有用户数据"
"这个 API 没有鉴权" "未登录用户直接调用 POST /admin/users 可以创建管理员账号"
"密码可能不够强" "密码用 MD5 哈希且无 salt,彩虹表可在 30 秒内破解"

左边是笼统的"不安全",右边是可以写出具体攻击步骤的漏洞。只有右边才算信号。

  • 问题 2:这是真实安全风险,还是理论上的最佳实践?

Does this represent a real security risk vs theoretical best practice?

这一问区分的是**"应该做"和"必须做"**。

很多 finding 的本质是"你没遵守最佳实践"------这当然是对的,但不等于是一个安全漏洞:

理论最佳实践(噪声) 真实安全风险(信号)
"HTTP 响应缺少 X-Content-Type-Options 头" "用户输入未经转义直接渲染到 HTML,导致存储型 XSS"
"数据库连接字符串不应该明文写在配置文件里" "数据库连接字符串被硬编码在开源的 GitHub 仓库中"
"建议使用 HTTPS" "登录接口通过 HTTP 传输,密码明文暴露在网络中"

左边是"做了更好",右边是"不做就会出事"。安全审查只报后者。

  • 问题 3:有没有具体的代码位置和复现步骤?

Are there specific code locations and reproduction steps?

这一问区分的是**"泛泛建议"和"可定位的问题"**。

泛泛建议(噪声) 可定位问题(信号)
"建议对所有用户输入进行校验" "user_service.py:42,username 参数直接拼接进 SQL 查询,未使用参数化查询"
"应该加强日志安全" "auth.py:78,登录失败时将用户提交的密码明文写入了 error log"
"建议实施最小权限原则" "worker.py:15,普通 worker 进程使用 root 权限的数据库连接字符串,可读写所有表"

左边看完开发者不知道该改哪里,右边看完可以直接定位到代码行去修复。安全审查的价值在于可操作。

  • 问题 4:安全团队拿到这个报告后能做什么?

Would this finding be actionable for a security team?

这一问是最终的实用性检验------即使前三个问题都过了,如果安全团队看了不知道该怎么处理,这个 finding 仍然没有价值。Q3 解决"能不能定位",Q4 解决"定位之后能不能行动",是 Q3 的进一步延伸。

不可操作(噪声) 可操作(信号)
"整体安全架构需要改进" "把 eval() 替换为 ast.literal_eval() 即可消除代码执行风险"
"需要加强输入验证" "使用参数化查询替换字符串拼接,修复 SQL 注入"
"第三方库可能存在漏洞" "jwt-decode v2.x 存在已知算法混淆漏洞,升级到 v4.x 可修复"

左边是正确但无用的废话,右边给出了明确的修复动作。


5.1.1.3. 层层递进的四问漏斗

四问是层层递进的漏斗:
#mermaid-svg-vxbco4msY6thqDFR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vxbco4msY6thqDFR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vxbco4msY6thqDFR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vxbco4msY6thqDFR .error-icon{fill:#552222;}#mermaid-svg-vxbco4msY6thqDFR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vxbco4msY6thqDFR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vxbco4msY6thqDFR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vxbco4msY6thqDFR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vxbco4msY6thqDFR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vxbco4msY6thqDFR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vxbco4msY6thqDFR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vxbco4msY6thqDFR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vxbco4msY6thqDFR .marker.cross{stroke:#333333;}#mermaid-svg-vxbco4msY6thqDFR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vxbco4msY6thqDFR p{margin:0;}#mermaid-svg-vxbco4msY6thqDFR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vxbco4msY6thqDFR .cluster-label text{fill:#333;}#mermaid-svg-vxbco4msY6thqDFR .cluster-label span{color:#333;}#mermaid-svg-vxbco4msY6thqDFR .cluster-label span p{background-color:transparent;}#mermaid-svg-vxbco4msY6thqDFR .label text,#mermaid-svg-vxbco4msY6thqDFR span{fill:#333;color:#333;}#mermaid-svg-vxbco4msY6thqDFR .node rect,#mermaid-svg-vxbco4msY6thqDFR .node circle,#mermaid-svg-vxbco4msY6thqDFR .node ellipse,#mermaid-svg-vxbco4msY6thqDFR .node polygon,#mermaid-svg-vxbco4msY6thqDFR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vxbco4msY6thqDFR .rough-node .label text,#mermaid-svg-vxbco4msY6thqDFR .node .label text,#mermaid-svg-vxbco4msY6thqDFR .image-shape .label,#mermaid-svg-vxbco4msY6thqDFR .icon-shape .label{text-anchor:middle;}#mermaid-svg-vxbco4msY6thqDFR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vxbco4msY6thqDFR .rough-node .label,#mermaid-svg-vxbco4msY6thqDFR .node .label,#mermaid-svg-vxbco4msY6thqDFR .image-shape .label,#mermaid-svg-vxbco4msY6thqDFR .icon-shape .label{text-align:center;}#mermaid-svg-vxbco4msY6thqDFR .node.clickable{cursor:pointer;}#mermaid-svg-vxbco4msY6thqDFR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vxbco4msY6thqDFR .arrowheadPath{fill:#333333;}#mermaid-svg-vxbco4msY6thqDFR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vxbco4msY6thqDFR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vxbco4msY6thqDFR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vxbco4msY6thqDFR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vxbco4msY6thqDFR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vxbco4msY6thqDFR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vxbco4msY6thqDFR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vxbco4msY6thqDFR .cluster text{fill:#333;}#mermaid-svg-vxbco4msY6thqDFR .cluster span{color:#333;}#mermaid-svg-vxbco4msY6thqDFR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vxbco4msY6thqDFR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vxbco4msY6thqDFR rect.text{fill:none;stroke-width:0;}#mermaid-svg-vxbco4msY6thqDFR .icon-shape,#mermaid-svg-vxbco4msY6thqDFR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vxbco4msY6thqDFR .icon-shape p,#mermaid-svg-vxbco4msY6thqDFR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vxbco4msY6thqDFR .icon-shape .label rect,#mermaid-svg-vxbco4msY6thqDFR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vxbco4msY6thqDFR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vxbco4msY6thqDFR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vxbco4msY6thqDFR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
否
是
否
是
一个候选 Finding
Q1: 有具体攻击路径?
❌ 噪声
Q2: 真实风险而非最佳实践?
❌ 噪声
Q3: 能定位到代码行?
❌ 噪声
Q4: 安全团队能据此行动?
❌ 噪声
✅ 信号:保留

每一问淘汰一批噪声,四问全过的才是真信号。这和硬排除、先例一起构成了信号质量评估的三级漏斗:

层级 工具 判断方式 过滤什么
硬排除 规则匹配 看到关键词就丢 已知类别的噪声(DoS、限流、资源泄漏...)
先例 案例参照 翻翻以前怎么判的 特定场景下的灰色地带(React XSS、UUID、环境变量...)
信号质量 四问框架 从零开始全面评估 硬排除和先例都没覆盖到的新型/罕见 finding

硬排除是"快速筛查",先例是"经验判断",信号质量是"深度分析"。三者成本递增,依次兜底。注意:这三级漏斗描述的是"判断逻辑",与 5.2 描述的"代码执行流水线"是两套不同的组织方式,后者见 5.2。


5.1.1.4. 总结

SIGNAL QUALITY 的本质是:用"工程师能不能据此修复"来检验每一个 finding 的价值。 说不出攻击路径的是噪声,修不了的是噪声,找不到代码位置的是噪声,看完不知道干啥的也是噪声。只有能回答"哪里出了问题、怎么攻击、怎么修"三个问题的 finding,才值得出现在安全报告里。


收到,我将为你解析"先例"的概念。在这个项目的语境下,"先例"(PRECEDENTS)是从法律领域借来的概念,指的是"过去做过的判断案例,作为未来类似情况的参考标准"。


5.1.2. 先例(PRECEDENTS)

5.1.2.1. 法律中的先例

在普通法系(如英美法)中,先例(precedent)的核心原则是 "遵循先例"(stare decisis):

法官在之前的案件中做出了某个判决 → 以后遇到类似案件时,应该做出一致的判决。

比如:某法院曾判决"在公共场合录音不侵犯隐私权"→ 以后所有类似"公共场合录音"的案件都参照这个判决来判。

先例的价值在于:保证判断的一致性。同样的事实,不能今天判有罪、明天判无罪。


5.1.2.2. 安全审查 prompt 中的先例

security-review.md 借用了这个概念。它的 PRECEDENTS 部分本质上是一组已经判定的"案例",告诉模型:

"以后遇到类似的情况,按照这些判例来做判断。"

简单说,先例就是给模型一本"判例集":遇到类似情况不用从零分析,翻翻之前怎么判的,照着来就行。既提高了判断效率,又保证了一致性。

举几个具体的先例来理解:

  • 先例 #1:日志中的密钥

"Logging high value secrets in plaintext is a vulnerability. Logging URLs is assumed to be safe."

这相当于一个"判例":

  • 案情:代码把某些数据写进了日志

  • 判决:如果写的是 API key / 密码 → 算漏洞;如果写的是 URL → 不算漏洞

  • 以后的类似情况:看到"日志里记录了用户搜索关键词"→ 参照此先例,不算漏洞

  • 先例 #6:React 的 XSS

"React is generally secure against XSS... unless using dangerouslySetInnerHTML"

  • 案情:React 组件渲染了用户输入
  • 判决 :默认安全,除非用了 dangerouslySetInnerHTML
  • 以后的类似情况 :看到 <div>{userInput}</div> → 不算 XSS;看到 <div dangerouslySetInnerHTML={``{__html: userInput}} /> → 算 XSS

5.1.2.3. 先例 vs 硬排除(HARD EXCLUSIONS)的区别

这是最容易混淆的地方。两者都是"过滤规则",但性质完全不同:

维度 硬排除(HARD EXCLUSIONS) 先例(PRECEDENTS)
性质 绝对规则 判断参考
执行方式 命中即丢,不讨论 需要模型结合上下文判断
粒度 粗:按问题类型一刀切 细:同一类型下区分不同情况
有没有例外 没有 有------先例本身会说明例外条件
类比 法律中的"成文法"(法条写死了) 法律中的"判例法"(参考过往判决)

举个对比来说明:

硬排除 #12 说:

"Log spoofing concerns. Outputting un-sanitized user input to logs is not a vulnerability."

这是绝对的------只要是"日志伪造"类 finding,直接丢。

先例 #1 说:

"Logging high value secrets in plaintext is a vulnerability."

这是有条件的------同样是"日志"相关的 finding,如果记录的是高价值密钥,反而应该保留。

如果只有硬排除没有先例,模型看到"日志中暴露了 AWS Secret Key"也会按硬排除 #12 直接丢掉------这显然不对。先例的存在就是为了修正硬排除的过度杀伤。


5.1.2.4. 先例 vs 信号质量准则(SIGNAL QUALITY)的区别
维度 先例 信号质量准则
作用 告诉你"这类情况该怎么判" 告诉你"怎么评估一个 finding 的质量"
具体性 针对具体场景(React XSS、UUID、环境变量...) 通用的四问框架
类比 具体的判例 法官的审判方法论

信号质量准则是:

  1. 有没有具体的攻击路径?
  2. 是真实风险还是理论最佳实践?
  3. 有没有代码位置和复现步骤?
  4. 安全团队能不能据此行动?

这四问适用于任何 finding,不针对特定场景。先例则是在特定场景下已经回答了这四问的"标准答案"。


5.1.3. 信号质量评估的三级漏斗

信号质量评估的三级漏斗:硬排除 → 先例 → 四问,是"判断单个 finding 值不值得报"的方法论;
#mermaid-svg-Md9BvEigU5T0hD5V{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Md9BvEigU5T0hD5V .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Md9BvEigU5T0hD5V .error-icon{fill:#552222;}#mermaid-svg-Md9BvEigU5T0hD5V .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Md9BvEigU5T0hD5V .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Md9BvEigU5T0hD5V .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Md9BvEigU5T0hD5V .marker.cross{stroke:#333333;}#mermaid-svg-Md9BvEigU5T0hD5V svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Md9BvEigU5T0hD5V p{margin:0;}#mermaid-svg-Md9BvEigU5T0hD5V .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Md9BvEigU5T0hD5V .cluster-label text{fill:#333;}#mermaid-svg-Md9BvEigU5T0hD5V .cluster-label span{color:#333;}#mermaid-svg-Md9BvEigU5T0hD5V .cluster-label span p{background-color:transparent;}#mermaid-svg-Md9BvEigU5T0hD5V .label text,#mermaid-svg-Md9BvEigU5T0hD5V span{fill:#333;color:#333;}#mermaid-svg-Md9BvEigU5T0hD5V .node rect,#mermaid-svg-Md9BvEigU5T0hD5V .node circle,#mermaid-svg-Md9BvEigU5T0hD5V .node ellipse,#mermaid-svg-Md9BvEigU5T0hD5V .node polygon,#mermaid-svg-Md9BvEigU5T0hD5V .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Md9BvEigU5T0hD5V .rough-node .label text,#mermaid-svg-Md9BvEigU5T0hD5V .node .label text,#mermaid-svg-Md9BvEigU5T0hD5V .image-shape .label,#mermaid-svg-Md9BvEigU5T0hD5V .icon-shape .label{text-anchor:middle;}#mermaid-svg-Md9BvEigU5T0hD5V .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Md9BvEigU5T0hD5V .rough-node .label,#mermaid-svg-Md9BvEigU5T0hD5V .node .label,#mermaid-svg-Md9BvEigU5T0hD5V .image-shape .label,#mermaid-svg-Md9BvEigU5T0hD5V .icon-shape .label{text-align:center;}#mermaid-svg-Md9BvEigU5T0hD5V .node.clickable{cursor:pointer;}#mermaid-svg-Md9BvEigU5T0hD5V .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Md9BvEigU5T0hD5V .arrowheadPath{fill:#333333;}#mermaid-svg-Md9BvEigU5T0hD5V .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Md9BvEigU5T0hD5V .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Md9BvEigU5T0hD5V .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Md9BvEigU5T0hD5V .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Md9BvEigU5T0hD5V .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Md9BvEigU5T0hD5V .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Md9BvEigU5T0hD5V .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Md9BvEigU5T0hD5V .cluster text{fill:#333;}#mermaid-svg-Md9BvEigU5T0hD5V .cluster span{color:#333;}#mermaid-svg-Md9BvEigU5T0hD5V div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Md9BvEigU5T0hD5V .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Md9BvEigU5T0hD5V rect.text{fill:none;stroke-width:0;}#mermaid-svg-Md9BvEigU5T0hD5V .icon-shape,#mermaid-svg-Md9BvEigU5T0hD5V .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Md9BvEigU5T0hD5V .icon-shape p,#mermaid-svg-Md9BvEigU5T0hD5V .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Md9BvEigU5T0hD5V .icon-shape .label rect,#mermaid-svg-Md9BvEigU5T0hD5V .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Md9BvEigU5T0hD5V .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Md9BvEigU5T0hD5V .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Md9BvEigU5T0hD5V :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
有
没有
是
否
候选 Finding
硬排除命中?
❌ 直接丢弃

(成文法:法条写死了)
有相关先例?
按先例判断

(判例法:参照过往判决)
用信号质量四问评估

(方法论:从零开始判断)
confidence ≥ 8?
✅ 保留
❌ 丢弃

三层的判断成本递增:

  • 硬排除最便宜(正则匹配一下),
  • 先例中等(需要模型理解场景是否匹配),
  • 信号质量准则最贵(需要模型做完整的四问分析)。

把便宜的放前面、贵的放后面,和整个项目的三层误报过滤哲学一脉相承。


5.2. 后处理管道的三层防线

Prompt 里的排除清单只是第一道防线(告诉模型"不要报这些"),但模型不一定听话。所以代码层面还有一个独立的后处理管道,由两个文件实现:

  • findings_filter.py --- 过滤主逻辑
  • claude_api_client.py --- Claude API 二次审查客户端

整体流程如下:

text 复制代码
Claude Code 大模型审计 → 输出原始 findings
        ↓
  第一层:硬规则过滤(正则匹配)
        ↓
  第二层:Claude API 逐条审查(可选)
        ↓
  第三层:排除目录过滤
        ↓
  最终报告

由此建立了后处理管道的三层防线:硬规则过滤 → Claude API 复审 → 排除目录,是代码层面实际执行的过滤流水线;

5.2.1. 完整的误报过滤图

#mermaid-svg-5rKK3XaVXMQ2Q2Va{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .error-icon{fill:#552222;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .marker.cross{stroke:#333333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va p{margin:0;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .cluster-label text{fill:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .cluster-label span{color:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .cluster-label span p{background-color:transparent;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .label text,#mermaid-svg-5rKK3XaVXMQ2Q2Va span{fill:#333;color:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .node rect,#mermaid-svg-5rKK3XaVXMQ2Q2Va .node circle,#mermaid-svg-5rKK3XaVXMQ2Q2Va .node ellipse,#mermaid-svg-5rKK3XaVXMQ2Q2Va .node polygon,#mermaid-svg-5rKK3XaVXMQ2Q2Va .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .rough-node .label text,#mermaid-svg-5rKK3XaVXMQ2Q2Va .node .label text,#mermaid-svg-5rKK3XaVXMQ2Q2Va .image-shape .label,#mermaid-svg-5rKK3XaVXMQ2Q2Va .icon-shape .label{text-anchor:middle;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .rough-node .label,#mermaid-svg-5rKK3XaVXMQ2Q2Va .node .label,#mermaid-svg-5rKK3XaVXMQ2Q2Va .image-shape .label,#mermaid-svg-5rKK3XaVXMQ2Q2Va .icon-shape .label{text-align:center;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .node.clickable{cursor:pointer;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .arrowheadPath{fill:#333333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5rKK3XaVXMQ2Q2Va .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5rKK3XaVXMQ2Q2Va .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5rKK3XaVXMQ2Q2Va .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .cluster text{fill:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .cluster span{color:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5rKK3XaVXMQ2Q2Va rect.text{fill:none;stroke-width:0;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .icon-shape,#mermaid-svg-5rKK3XaVXMQ2Q2Va .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .icon-shape p,#mermaid-svg-5rKK3XaVXMQ2Q2Va .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .icon-shape .label rect,#mermaid-svg-5rKK3XaVXMQ2Q2Va .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5rKK3XaVXMQ2Q2Va .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5rKK3XaVXMQ2Q2Va .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5rKK3XaVXMQ2Q2Va :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
命中排除目录
未命中
命中
未命中
keep=false
keep=true
API 调用失败
🤖 Claude API 二次审查(小模型)
读取 finding 对应源文件
构造 prompt:

16 条硬排除 + 17 条先例

  • 4 条信号质量准则
    逐条调用 API
    输出 confidence_score

  • keep_finding 判断
    🔒 HardExclusionRules(8 类正则)
    DoS / 资源耗尽
    速率限制
    资源泄漏
    开放重定向
    正则注入
    内存安全

(非 C/C++ 文件才丢弃)
SSRF

(仅 HTML 文件丢弃)
Markdown 文件
Claude Code 输出原始 Findings
第一层:硬规则过滤

正则匹配 title + description
❌ 丢弃
ENABLE_CLAUDE_FILTERING?
第二层:Claude API 逐条审查
保留所有硬规则幸存者
❌ 丢弃(记录 justification)
✅ 保留(带 confidence_score 元数据)
✅ 默认保留

(confidence_score=10.0,记录失败原因)
第三层:排除目录过滤
❌ 丢弃
✅ 最终报告


5.2.2. 第一层:硬规则过滤(HardExclusionRules)

这一层完全不用大模型 ,而是用预编译的正则表达式去匹配 findings 的 description 和 title 字段。命中就丢,没有商量余地。

代码里定义了 8 类正则模式:

规则类别 匹配示例 丢弃理由
DoS 攻击 denial of service、resource exhaustion、infinite loop 低信号,通常有专门的基础设施防护
速率限制 missing rate limit、unlimited requests 不属于代码安全漏洞
资源泄漏 memory leak、unclosed connection、database leak 属于资源管理问题,不是安全漏洞
开放重定向 open redirect、unvalidated redirect 影响不够高
正则注入 regex injection、regex denial of service 不适用于大多数场景
内存安全 buffer overflow、use-after-free、segmentation fault 仅在非 C/C++ 文件中丢弃 。如果文件是 .c/.cc/.cpp/.h,保留
SSRF ssrf、server-side request forgery 仅在 HTML 文件中丢弃(客户端代码不存在 SSRF)
Markdown 文件 任何 .md 文件中的 finding 文档文件不存在可执行的安全漏洞

设计精妙之处在于上下文感知:

内存安全规则不是无脑丢弃。它检查文件扩展名。如果你写的是 C/C++ 代码,buffer overflow 就是真实的安全问题,会被保留;但如果你写的是 Python/Java/Go 这类有内存管理的语言,报一个 buffer overflow 就是误报,会被丢掉。SSRF 同理,HTML 文件不存在服务端请求伪造的问题。

核心判断逻辑(get_exclusion_reason)的伪代码:

python 复制代码
def get_exclusion_reason(finding):
    # 1. Markdown 文件?→ 直接丢
    if file.endswith('.md'):
        return "文档文件"

    # 2. 拼接 title + description,全部小写
    combined = (title + " " + description).lower()

    # 3. 依次匹配各正则规则
    if any(re.search(p, combined) for p in DOS_PATTERNS):
        return "DoS"
    if any(re.search(p, combined) for p in RATE_PATTERNS):
        return "速率限制"
    if any(re.search(p, combined) for p in RESOURCE_PATTERNS):
        return "资源泄漏"
    if any(re.search(p, combined) for p in REDIRECT_PATTERNS):
        return "开放重定向"
    if any(re.search(p, combined) for p in REGEX_PATTERNS):
        return "正则注入"

    # 4. 内存安全:仅非 C/C++ 文件丢弃
    if file_ext not in {'.c', '.cc', '.cpp', '.h'}:
        if any(re.search(p, combined) for p in MEMORY_PATTERNS):
            return "内存安全(非 C/C++ 文件)"

    # 5. SSRF:仅 HTML 文件丢弃
    if file_ext == '.html':
        if any(re.search(p, combined) for p in SSRF_PATTERNS):
            return "SSRF(HTML 文件)"

    return None  # 没命中任何规则,保留

5.2.3. 第二层:Claude API 逐条审查(可选)

这一层是可选的,通过环境变量 ENABLE_CLAUDE_FILTERING=true 开启。它的思路是:再调一次 Claude API(用的是更便宜的小模型,具体模型 ID 以代码中 DEFAULT_CLAUDE_MODEL 或相关配置常量为准),让模型逐条判断每个 finding 是不是误报。

量表提醒: 本节的 confidence_score 是 1--10 分制 ,与 4.10 中模型输出 finding 时的 confidence(0--1 制)是两套独立量表,不可直接比较。

5.2.3.1. 工作流程
python 复制代码
for finding in 通过硬规则的findings:
    success, result, error = claude_client.analyze_single_finding(finding)
    
    if result['keep_finding'] == False:
        丢弃(记录 confidence_score + justification)
    else:
        保留(附带 confidence_score 元数据)

每条 finding 独立过一遍 Claude API,不批量处理。

5.2.3.2. 审查 Prompt 的设计

claude_api_client.py 里的 _generate_single_finding_prompt 构造了一份非常详细的二次审查 prompt。核心结构:

  1. 上下文注入:
text 复制代码
PR Context:
- Repository: owner/repo
- PR #42: "Add user authentication"
- Description: ...
  1. 文件内容注入:

这是特别值得注意的一点, 它会把 finding 对应的那个文件的源码读进来塞进 prompt(实际实现中可能对超大文件做截断,以代码为准):

python 复制代码
def _read_file(self, file_path):
    # 读取 finding 对应的源文件内容
    with open(path, 'r') as f:
        content = f.read()
    return content

这意味着二次审查时,模型不仅看到了 finding 的描述,还看到了实际的代码上下文,判断质量远高于只看描述文字。

  1. 过滤规则(16 条硬排除 + 17 条先例 + 4 条信号质量准则):

这是整个误报过滤系统最精华的部分。默认过滤规则极其详尽:

硬排除清单(16 条):

# 排除规则 排除原因
1 DoS / 资源耗尽 有专门工具管
2 磁盘上的密钥 有 git-secrets 等专门工具
3 速率限制 不属于代码安全
4 内存/CPU 消耗 性能问题,不是安全漏洞
5 非安全关键字段缺少输入校验 误报重灾区
6 GitHub Action 工作流的输入净化 实际中几乎不可利用
7 缺少加固措施 "没做到最佳实践" ≠ "有漏洞"
8 理论性的竞态条件 只有真正有破坏性的竞态才报
9 过时第三方库 有专门的依赖扫描工具(Snyk 等)
10 Rust 代码的内存安全 Rust 的所有权机制天然免疫
11 纯测试文件 测试代码不是攻击面
12 日志伪造 输出未净化的用户输入到日志不是漏洞
13 仅控制路径的 SSRF 只有能控制 host 或 protocol 才算
14 AI prompt 中包含用户输入 一般来说不构成漏洞
15 依赖包不在公开仓库 依赖内部库不是安全漏洞
16 导致崩溃但不是漏洞的问题 变量未定义/null 不算安全问题

先例判例(17 条):

"先例"(PRECEDENTS)是从法律领域借来的概念,指的是"过去做过的判断案例,作为未来类似情况的参考标准"。这些是更细粒度的判断指南,非常有实战价值:

# 先例 核心判断
1 日志中记录高价值密钥 vs 普通数据 记录密钥是漏洞,记录 URL 不是;记录请求头是危险的(可能含凭证)
2 UUID 可猜测性 假设 UUID 不可猜测,需要猜 UUID 的漏洞无效
3 审计日志 审计日志缺失或被修改不算安全漏洞
4 环境变量和 CLI 参数 视为可信值,攻击者无法在安全环境中修改
5 资源管理 内存泄漏、文件描述符泄漏不算
6 低影响 Web 漏洞 tabnabbing、XS-Leaks、原型链污染、开放重定向不算
7 过时第三方库 另外管
8 React 的 XSS React 默认安全,除非用了 dangerouslySetInnerHTML
9 GitHub Action 工作流漏洞 实际中大多不可利用
10 客户端代码的权限检查 前端不实现权限检查是正常的,后端负责
11 MEDIUM 级别 只有明显且具体的问题才保留
12 Jupyter Notebook 漏洞 大多不可利用
13 日志中记录非 PII 数据 不算漏洞
14 Shell 脚本的命令注入 通常不接受不可信输入
15 客户端 JS 的 SSRF 客户端代码不存在 SSRF
16 HTTP 请求中的 ../ 路径遍历只在读文件时才有问题
17 日志查询注入 只有会向外部用户暴露敏感数据时才报

信号质量四问:

text 复制代码
1. 是否有具体的、可利用的漏洞和清晰的攻击路径?
2. 这是真实安全风险还是理论上的最佳实践?
3. 有没有具体的代码位置和复现步骤?
4. 这个发现对安全团队来说是否可操作?
  1. 置信度打分(1--10 制):
text 复制代码
1-3 分:低置信度,大概率是误报
4-6 分:中置信度,需要进一步调查
7-10 分:高置信度,大概率是真实漏洞
  1. 输出格式:
json 复制代码
{
  "original_severity": "HIGH",
  "confidence_score": 8,
  "keep_finding": true,
  "exclusion_reason": null,
  "justification": "Clear SQL injection vulnerability with specific exploit path"
}
5.2.3.3. 容错设计

如果 Claude API 调用失败(超时、网络错误等),这条 finding 会被默认保留,并附上默认高置信度 10.0 和失败原因。

这与检测阶段"宁可漏报,不可误报"(4.4)的取向相反,但并不矛盾:检测器的职责是"不误报",过滤器的职责是"不误杀"。过滤器本身出问题时,不应导致真正的漏洞被丢掉------这是"宁可误报,不可漏报"的保守策略。


5.2.4. 第三层:排除目录过滤

在 github_action_audit.py 的 apply_findings_filter 里还有一步:

python 复制代码
for finding in kept_findings:
    if _is_finding_in_excluded_directory(finding, github_client):
        directory_excluded_findings.append(finding)
    else:
        final_kept_findings.append(finding)

用户可以配置 exclude-directories 参数(比如 vendor,node_modules,test),命中这些目录的 finding 直接丢弃。这是最后一道兜底,因为有些目录用户明确知道不需要审查。

5.2.5. 三层防线的分工

层 实现方式 用什么判断 特点
Prompt 约束 告诉大模型"不要报这些" 模型的自我约束 成本最低、但可靠性最弱,不保证 100% 执行
硬规则过滤 正则表达式匹配 关键词/模式 确定性、零成本、速度快
Claude API 二次审查 小模型逐条判断 + 读源文件 语义理解 + 代码上下文 最贵但最准,可选开启
排除目录 路径匹配 文件位置 用户自定义的最后兜底

这套设计的核心思想是:让便宜快速的过滤在前面挡掉大量噪声,让昂贵精准的过滤只在必要时介入。 硬规则是零成本的快速筛查,Claude API 审查是按需调用的高精度判断,两者配合实现了低误报率。


6. 总结

6.1. 核心结论

claude-code-security-review 是一个以大语言模型为检测核心、以工程化规则为辅助过滤 的代码安全审查工具。它用 Claude 的语义理解和推理能力替代了传统 SAST 的模式匹配,通过先理解项目安全基线、再对比新代码偏离、最后定位具体漏洞的三阶段方法论,试图解决传统工具误报率高的根本问题。

6.2. 设计要点回顾

  • 两种模式:场景决定架构

    GitHub Action 是"脚本驱动、模型打工",适合 CI/CD 的确定性和可控性;/security-review 是"Prompt 驱动、模型自主",适合本地交互的灵活性和深度。

  • 检测层:大模型主导

    无论是 GitHub Action 的脚本编排模式,还是 /security-review 的 Prompt 编排模式,漏洞发现环节完全由 Claude 承担。Prompt 设计通过角色设定、边界划定、置信度门槛(>80%)、首尾呼应的排除清单等多重约束,把一个通用模型"调教"成低误报的安全审计专家。

  • 过滤层:工程规则辅助

    三层防线分工明确,Prompt 约束靠模型自律(最轻但不保证)、硬规则正则匹配(确定性、零成本)、Claude API 二次审查(最贵但最准、可选)、排除目录(用户兜底)。核心思想是"便宜快速的挡在前面,昂贵精准的按需介入"。

6.3. AI 在安全检查中的价值与边界

这个工具最值得借鉴的,是它对 AI 能力边界的清醒认知:

  • AI 擅长什么:理解上下文、建立基线、追踪跨文件的数据流、判断"这段代码放在这个项目里是否有问题"------这些正是传统规则引擎的盲区。
  • AI 不擅长什么:确定性、可复现性、低成本。因此工具用硬规则过滤噪声、用置信度打分量化不确定性、用 API 失败默认保留的保守策略兜底。
  • 不该让 AI 做什么:DoS、密钥扫描、依赖漏洞这些有专门工具处理的领域,Prompt 里明确排除,避免重复劳动和噪声。

6.4. 对实践的启示

  1. AI 安全工具不是"扔掉规则",而是把规则从"检测主力"降级为"过滤辅助",让规则做它擅长的事(快速排除已知噪声),让模型做它擅长的事(语义级深度分析)。
  2. Prompt 工程是决定 AI 安全工具误报率的关键变量。这份 Prompt 里对排除清单的反复强调、对置信度的量化定义、对攻击场景的强制要求,本质上是用自然语言编写的一套"安全审计规范"。
  3. 误报控制是落地关键。再强的检测能力,如果误报泛滥也会被开发者抛弃。这个工具用四层机制(Prompt 约束、硬规则、二次审查、目录排除)层层收敛,体现了对工程现实的尊重。
  4. 混合架构优于单一方案。纯规则误报高,纯大模型成本高且不确定,二者结合才能兼顾覆盖率、准确率和成本。

6.5. 补充说明

  • 这个工具只适合代码提交时的增量安全检查,不适合对存量代码做全量扫描------全量扫描缺乏 Phase 1 建立安全基线所需的"diff 锚点"。
  • 对变动代码的上下文获取,取决于模型自身的推理能力,以及其调用检索工具、定位关键依赖代码的能力,这存在很大的不确定性,容易产生漏报。

7. 参考

相关推荐
叁两1 小时前
别再手动刷行业信息了,你可能需要一个"行业研究员"
人工智能
2601_965449081 小时前
AI视频制作:做系列历史地图视频想固定音色,AI 音色克隆的流程实测
人工智能·音视频
2601_962071001 小时前
从OpenRouter到Crazyrouter:Claude Code三处改动
人工智能·ai
阿里云大数据AI技术2 小时前
云栖实录|从代码上下文到千亿级文档:Qoder × 阿里云 Elasticsearch 落地生产级Agent向量检索
人工智能·elasticsearch
汤姆宝库2 小时前
具身智能深拆:VLA、世界模型与数据飞轮
开发语言·人工智能·php
鲜于言悠9052 小时前
Jev爆火:不生成文字,直接输出概率的决策模型
人工智能
美团技术团队2 小时前
如何在“不能出错”的系统里使用 AI Coding?分布式存储实践与边界
人工智能
枫叶丹42 小时前
AI 提出科学假设之后:从数据筛选到实验验证
人工智能·chatgpt·开源·agent·codex
kaixin_啊啊2 小时前
【零基础学AI】第 19 章——用分类模型识别类别
人工智能·分类·数据挖掘