sriram98669704/code-review-agent
GitHub: sriram98669704/code-review-agent
一个基于 LLM 的 Python 代码审查 Agent,通过模拟人类审查员的索引、判断和误报推翻流程,对安全、合规和代码质量进行深度语义分析。
Stars: 0 | Forks: 0
# 代码审查 Agent
   
**LLM 能否像人类审查员一样审查代码——阅读每个函数,标记真实的风险,并解释原因?**
**[实时面板](https://ai-llm-code-review-agent.streamlit.app/)** — 粘贴一个 GitHub repo URL 和你自己的 OpenAI key,即可查看运行情况。· **[演示视频](https://drive.google.com/file/d/14TeN77E1aj-7FHNyH_5ylyWUTgnJmD6c/view?usp=drive_link)** — 完整运行过程的详细演示。
静态扫描器只匹配模式;它们不会去推理一个函数实际做了什么。这是一个 LLM *agent*,它会逐个阅读 Python 函数,并标记安全漏洞、合规性 (GRC) 问题、脆弱的 guardrails 以及重复逻辑——然后用通俗易懂的英文解释每一项发现。它是一个真正的 agent,而不是单一的 prompt:模型会**索引** repo,根据规则**判断**每个函数,在结论依赖于某个 helper 时**阅读其源码**,并且通过一个独立的 **triage**(筛选)过程**推翻**任何被 helper 证明无害的发现——因此你得到的是真实的风险,而不是虚惊一场。它补充了像 bandit/semgrep 这样的工具——而不是取代它们。
## 概览
```
venv/bin/streamlit run dashboard.py # live review in the browser
venv/bin/python agent.py # the same review from the CLI
```
- **适用于任何公开的 Python repo** — 在面板中粘贴 GitHub URL(或向 `run_agent()` 传入一个路径),只有其 `.py` 文件会被拉取、扫描并删除;内置的 [`code-review-sample`](https://github.com/sriram98669704/code-review-sample) 只是默认的演示输入,而不是该工具构建的核心
- **刻意的双层设计** — 确定性的*索引*过程遍历 repo 一次(一次仅在内存中处理一个文件),将每个函数映射到其文件并处理顶层代码,并**保留每个函数的审查结论** — embedding、规则检索、重复检测和判断都在稍后按函数在同一位置进行 — 这正是留给 agent 真正要做的工作
- **真正的 agent 循环** — LLM 索引 repo,判断每个函数,并在结论依赖于某个 helper 时提取其源码(决定 → 行动 → 观察)
- **Triage 推翻误报** — 一个独立的、持怀疑态度的审查过程会重新阅读被标记函数调用的 helper,并且*仅*在它能引用消除风险的确切代码行时才放弃该发现,因此真正的 bug 永远不会被悄无声息地忽略
- **四个维度** — Security(安全)、Compliance (GRC)(合规性)、Guardrails(护栏)和重复检测,每一项发现都用通俗易懂的英文进行解释,并附带建议的修复方案
- **检索而非暴力破解** — 两个 ChromaDB 向量存储库(余弦相似度):每个 chunk 按类别检索 top-k 规则(RAG,而不是“展示所有内容”),通过最近邻搜索查找重复项,而不是比较每个函数对
- **自带密钥 (BYOK)** — 在本地优先从环境变量读取,部署时提供基于会话的 BYOK 面板;密钥永远不会触及磁盘、日志或 `os.environ`
- **设计上的临时性** — 实时运行结果仅在屏幕上显示,绝不写入磁盘
## 工作原理
### Agent 循环
LLM 是大脑。我们交给他一个工作区和一个任务,然后让他自己驱动:他索引 repo,判断每个函数,在结论依赖于某个 helper 时阅读其源码,并写下结论 — 而 triage 过程会推翻其中的误报。这个**决定 → 行动 → 观察**的循环使它成为一个 agent,而不是一个简单的脚本。
**为什么手写循环,而不是使用框架。** 这个循环是直接针对模型的 tool-calling API 编写的,而不是封装在 agent 框架中(如 LangChain,或 SDK 的 `Agent`/`run()`)。这是有意为之的,并非缺失:框架的 `run()` 提供*相同*的架构 — 它不会让这变得“更像一个 agent” — 但直接驱动循环能提供**比托管的 `run()` 更精细的每一步控制**,这正是让两阶段设计(先进行偏执的初步审查,然后由独立的 triage 过程推翻它)以及污点引导的证据收集变得易于表达的关键,每一步都在时间线中明确且可见。这种控制就是权衡:框架会减少样板代码,但它不会改变系统的功能或使其更正确。真正的生产环境缺口与框架选择无关 — 请参阅[后续计划](#whats-next)。
```
mission ─▶ index_repo() — walk every file once, map each function to its file,
and settle top-level code → returns a WORKSPACE.
On purpose, it WITHHOLDS the per-function verdict.
↓
list_functions() — the checklist of what to review
↓
judge(name) for each function — embed it once, retrieve its rules,
check it for duplicates, return its rule verdict (in ISOLATION)
↓
does a verdict hinge on what a helper it calls actually does?
├─ yes ─▶ read_function(helper) ─▶ read its source
└─ no
↓
TRIAGE — a fresh, skeptical pass re-checks each finding against the
helpers its function calls, and DROPS the ones a helper proves safe
↓
write the risk report (consistent with triage)
```
**刻意的双层设计。** 第 1 层(`index_repo`)是只做*一次*的廉价结构化过程 — 遍历每个文件,将其分块,并记录每个函数所在的位置(名称 → 文件 + 源码)。顶层代码(如 imports,或像 `API_KEY = "..."` 这样的常量)没有 helper 可供调查,agent 也永远不会按名称审查它们,因此它们在这里直接处理完毕。第 1 层返回一个工作区(函数索引 + 这些顶层发现),但**保留对函数的审查结论** — 并且不对它们进行 embedding、规则检索或重复检测。这种保留是核心所在:它给 agent 留下了真正要做的工作,而不是一份等待重新措辞的完成报告。第 2 层是 agent 在该工作区上驱动的工具 — `list_functions()`,`judge(name)`(按函数执行的步骤,一次性完成一个函数的*所有*工作:对其代码进行一次 embedding,检索最接近的规则,将其与目前审查过的函数进行比较以查重,并返回其规则审查结论),以及 `read_function(name)` 用于按名称提取 helper 的源码(通过即时索引查找,跨文件解析;如果某个名称存在于多个文件中,agent 会获取所有匹配项)。
在索引期间,**内存中一次只保留一个文件**。重复检测的“堆”——每个已审查函数的 embedding 向量——在整个运行期间持续存在,因为发现重复项本质上意味着将一个函数与之前审查过的所有函数进行比较;`judge` 会将其审查的每个函数流式传输到其中。运行结束时,该堆会被释放。
### Triage — 推翻误报
`judge` 的审查结论是**在隔离状态下查看单个函数**的,因此它无法区分真正的风险和已被 helper 消除的风险 — 它对两者的标记方式相同。想象一下两个函数,它们都根据用户输入构建文件路径:审查器将**两者**都标记为路径遍历漏洞,并且这两个发现返回的结果*完全相同*。区分它们的唯一方法是阅读它们各自调用的 helper,并推断它是否真的进行了过滤 — 一个 helper 可能运行了 `os.path.basename`(去除了 `../` → 安全),而另一个只是替换了斜杠(保留了 `../` → 仍然容易受到攻击)。表面上发现相同;往下深读一层,结论却截然相反。
Triage 就是执行此操作的过程,并且它刻意与发现过程**分离**,原因有二,而单一的合一过程无法满足这两点:
- **全新的上下文。** 提出发现的模型不愿意删除自己的工作。一个唯一任务就是反驳该发现的独立上下文没有这种依恋 — 因此它确实会对误报扣下扳机。
- **证据纪律。** 我们不要求模型回忆 helper 做了什么 — 我们机械地阅读被标记函数的 AST,追踪它的调用,并将它们背后真正 helper 的**完整源码**交出来。两次静态过程使这种证据值得信赖。**解析**([`resolver.py`](resolver.py))通过读取调用者的 imports,将每个调用映射到**正确的文件** — `db.get_user()`、`from db import get_user`、带有别名的 `import db as d`,或者 `self.method()` 都会解析到它们指定的确切定义;如果某个调用确实无法确定(一个鸭子类型的 `x.save()`,或者一个跨文件冲突的裸名 — 在两个不同模块中定义的同一个 helper 名称),它将被**标记为未解析,而不是进行猜测**,因此 triage 永远不会基于错误的源码进行反驳。**可达性**([`callgraph.py`](callgraph.py))沿着链条追踪*超过第一跳* — `f` → `build` → `run` → sink — 但只沿着**受污染的数据**:从被标记函数的参数开始,它只扩展那些实际接收到风险值的调用(即 CodeQL/Semgrep 会遍历的切片),并受已访问集合、深度限制和 helper 上限的约束。因此,深埋三跳之外的修复仍然能被看到,而输入永远无法到达的分支则永远不会被拉取进来。要**放弃**一个发现,模型必须引用消除攻击的确切代码行;如果做不到,该发现将**保留**。在犹豫不决时,它会选择保留 — 错误的放弃会掩盖真正的 bug,这是我们最不希望看到的结果。
其结果是 agent 能够做到普通扫描器做不到的事情:安全的部分被**丢弃**(引用了消除风险的确切 helper 代码行),而真正容易受攻击的部分被**保留** — 这是通过阅读 helper 来区分的,而不是通过硬编码的规则。([`triage.py`](triage.py))
### 检索,而不是暴力破解 — 两个向量库
这两项检查都依赖于相同的技巧:将内容转化为向量嵌入,存储在一个**内存中的 ChromaDB 集合**(`hnsw:space=cosine`)中,并通过**余弦相似度**进行查询,而不是每次都与所有内容进行比较。为什么这很重要:在每个 chunk 上向 LLM 展示*整个*规则集,或者将每个 chunk 与之前的所有 chunk 进行比较,随着堆积量的增加,这两种做法都无法扩展 — 无论策略集或 repo 变得多大,检索都能保持每次调用廉价且快速。
- **Security / GRC / guardrail 检查 — 检索增强,而非暴力破解。** `policies/*.json` 中的每条规则都会在最开始被一次性嵌入。对于每个代码 chunk,我们对其嵌入,并仅提取*每个类别中最近的 top-k 规则*(在 [`security.py`](security.py) 中 `RULES_PER_CATEGORY = 5`) — 而不是整个规则手册。这使得 prompt 保持简小且切题,即使策略集增长到数百条规则也是如此。随后,grounding guard 会丢弃任何引用了检索集合之外规则 ID 的发现,因此模型永远无法捏造违规行为。
- **重复检查 — 最近邻,而不是成对比较。** 随着每个函数被审查,它会被嵌入并查询之前所有已审查函数向量组成的不断增长的“堆”,仅请求**最接近的单个匹配项**(在 [`duplicates.py`](duplicates.py) 中 `n_results=1`)。只有该最佳匹配项 — 如果其余弦相似度超过 `THRESHOLD = 0.80` — 才会被发送给 LLM 以确认它是真正的重复(而不仅仅是巧合的匹配)。确认的重复项会被标记为 **exact**(逐字节完全相同的主体)或 **similar**(即使在措辞改变后功能上仍然相同 — 例如重命名的变量或重新排序的行;通过 embedding 基于语义匹配,而不是固定的编辑列表),以便审查员知道它是字面意义上的副本还是近似的孪生兄弟。这避免了随着 repo 增长而产生的 O(n²) LLM 比较爆炸;ChromaDB 的索引通过最近邻搜索来完成这项工作。
在这两种情况下,LLM 永远只能看到通过向量搜索检索到的一小部分相关切片 — 绝不会看到整个规则集或函数的整个历史记录。
## 面板
```
venv/bin/streamlit run dashboard.py
```
一个单一的实时审查页面 — 没有 tab,也不存储历史记录:
1. **设置你的 key** — 在本地会从 `.env` 自动检测,或者在部署实例上粘贴到 BYOK 面板中。
2. **粘贴公开的 GitHub repo 链接** — 例如 `https://github.com/owner/repo`。添加 GitHub `/tree//` 后缀以将审查范围限定在一个文件夹内 — 例如 `https://github.com/fportantier/vulpy/tree/master/bad` 仅审查 `bad/`,而不审查同级的 `good/`。仅拉取其 `.py` 文件 — 通过 GitHub API,一次拉取一个文件,放入临时目录中([`fetcher.py`](fetcher.py)) — 然后扫描并删除;本地和部署的运行表现完全相同。请参阅[拉取 repo](#fetching-a-repo)。
3. **运行** — agent 运行完整的**决定 → 行动 → 观察**循环:它索引 repo,判断每个函数,在结论依赖于某个 helper 时将其源码拉取进来,运行 triage 过程推翻误报,并生成一份通俗易懂的风险报告。进度以决策时间线(index → judge → investigate → triage → write)实时流式传输,其中嵌入了索引树(每个文件和函数)。然后页面顶部会显示严重细分(数量 + 条形图),接着是保留下来的发现,一个**已推翻**部分列出了被 triage 丢弃的每个误报(以及它引用的消除风险的代码行),重复项,最后是作为通俗易懂总结的 agent 叙述性结论 — 每个发现都配有建议的修复方案(judge 在同一次调用中返回它,因此不需要额外的 API 请求)。
一次运行是**临时性**的:其结果存在于内存中,呈现在屏幕上,从不保存到磁盘 — 因此共享的公共实例永远不会将一个访问者的扫描结果泄露给下一个访问者。
### 拉取 repo
仅拉取 repo 的 `.py` 文件 — **绝不**拉取其 `.git` 历史记录或任何非 Python 文件。`fetched_repo()`([`fetcher.py`](fetcher.py))在一次 API 调用中列出文件树,保留 Python blob,并将每个 blob 下载到一个一次性临时目录中,扫描结束后该目录将被删除。配置 `GITHUB_TOKEN` 环境变量会将速率限制从 60 次请求/小时提高到 5000 次/小时;如果没有它,小型公开 repo 仍然可以工作;如果 API 路径无法运行(在没有 token 的情况下受到速率限制,或发生瞬时错误),它会回退到浅层的 `git clone`。面板会在每次审查时指明运行的是哪条路径 — *通过 GitHub API 拉取了 N 个 .py 文件*,或者是一条说明 API 不可用并改为 clone 的注释 — 因此拉取路径永远是可见的。
曾经考虑过真正的**不触及磁盘、仅在 RAM 中**的拉取方式,但被拒绝了:索引、重复堆和 resolver 本来就会在整个运行期间将每个函数保留在内存中,因此流式传输文件在下游不会节省任何东西 — 反而会迫使重写 chunker、indexer 和 resolver。临时目录拉取已经跳过了 `.git`/非 Python 的大块内容(真正的节省所在),而风险却小得多。
## 自带密钥 (BYOK)
实时审查会发起真实的、付费的 OpenAI 调用,因此需要一个 key。其安全模型严格且经过深思熟虑:
- **环境变量优先。** 如果本地 `.env` 提供了 `OPENAI_API_KEY`,面板会直接使用它,并且 BYOK 面板保持隐藏。该面板仅在环境中没有 key 时才会出现 — 例如在部署的应用上,它没有 `.env`。
- **粘贴的 key 仅存在于浏览器会话内存中** — 绝不会写入磁盘、日志或环境变量,也绝不在访问者之间共享。
- **绝不写入 `os.environ`**(这是进程全局的,并且在会话之间共享 — 将 key 写入那里可能会导致它在访问者之间泄露)。该 key 作为显式的函数参数直接传递给 OpenAI SDK 调用。
- **任何形状像 key 的内容都会被脱敏**(`«redacted-key»`),然后才能到达 UI 或日志 — 这是对提供商错误回显部分 key 的深度防御。
- 关闭标签页时 key 会消失,并且**清除 key** 按钮会根据需要擦除粘贴的 key。
Key 解析是纯净且无副作用的,位于 [`byok.py`](byok.py) 中。
**部署加固。** [`.streamlit/config.toml`](.streamlit/config.toml) 禁用了遥测(`gatherUsageStats`),保持 XSRF/CORS 保护开启(Streamlit 的安全默认设置),并关闭了 `showErrorDetails`,因此未捕获的崩溃会在公共页面上显示为简单的“出错了”,而不是带有内部路径的 traceback。永远不需要配置任何服务器端密钥 — 部署的应用没有 `.env`,因此它完全在 BYOK 上运行,不需要管理或泄露任何 `st.secrets`。
## 快速开始
### 前置条件
- Python 3.9+
- 一个 OpenAI API key
### 安装
```
git clone
cd code-review-agent
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
```
### 配置
```
cp .env.example .env
# 填写 OPENAI_API_KEY
```
`.env` 被 gitignored,永远不会被提交。对于部署的应用,不存在 `.env` — 面板会回退到 BYOK 面板,该面板的作用范围仅限于那一个访问者的浏览器会话。
### 运行面板
```
venv/bin/streamlit run dashboard.py
```
### 运行 CLI agent
```
venv/bin/python agent.py
```
运行完整的决定 → 行动 → 观察循环,并向终端输出一份纯文本风险报告。如果不带参数,它会将内置的 [`code-review-sample`](https://github.com/sriram98669704/code-review-sample) fixtures repo(本项目的一个同级项目)作为演示进行审查;要审查你自己的代码,请向 `run_agent()` 传入一个路径(请参阅[在代码中使用](#using-it-in-code)),或者在面板中粘贴 GitHub URL。
## 在代码中使用
运行完整的 agent — 它自己驱动工具,推翻误报,并生成叙述性报告:
```
from agent import run_agent
# 返回 {"report": verdict text,
# "review": {findings, dropped, duplicates, ...}, # cards: survivors + overruled
# "reads": agent 引入的 helpers,
# "exploration": agent 自行打开的 helpers,
# 以及超出保证的 triage walk 所触及的 helpers}。
out = run_agent("Review the repository at '/path/to/repo' and report its risks.",
api_key="sk-...")
review = out["review"]
print(f"{len(review['findings'])} findings, "
f"{len(review['dropped'])} overruled, "
f"{len(review['duplicates'])} duplicates")
for f in review["findings"]: # the risks that survived triage
print(f"[{f['severity']}] {f['path']}:{f['name']} — {f['rule_title']}")
for d in review["dropped"]: # false positives triage overruled
t = d["triage"]
print(f"DROPPED {d['name']} ({d['rule_id']}) via {t['helper']}: {t['line']!r}")
print(out["report"])
```
如果你只想要底层结构而不需要 agent 循环,这两个层也可以直接调用:`index_repo(directory)` 构建工作区(函数映射 + 顶层发现),然后 `judge(name)`(规则审查结论 + 任何重复项)/ `read_function(name)` 对其进行查询。请参阅 [`agent.py`](agent.py)。
## 项目结构
```
code-review-agent/
├── agent.py # Agent loop (decide → act → observe): index_repo + list_functions + judge + read_function tools
├── triage.py # the disconfirmation pass — kills false positives, citing the neutralising line
├── resolver.py # import-aware call resolution — maps db.get_user() to the RIGHT file's def (never guesses)
├── callgraph.py # taint-guided interprocedural expansion — follows the call chain to deep helpers
├── chunker.py # walk_files + scan_file — AST chunking (+ per-file import facts), one file at a time
├── security.py # retrieve_rules + judge_chunk — RAG-grounded security / GRC / guardrail checks
├── duplicates.py # check_duplicate + the embedding "pile" (Chroma) for dup detection
├── llm.py # OpenAI client wrapper — chat() + embeddings, key passed in
├── byok.py # Key resolution, format validation, redaction (no os.environ writes)
├── fetcher.py # fetched_repo() — .py-only GitHub API fetch into a temp dir (shallow-clone fallback)
├── dashboard.py # Streamlit single-page live review (ephemeral, GitHub-link only)
├── .streamlit/
│ └── config.toml # deployment hardening — telemetry off, XSRF/CORS on, no tracebacks on the public page
├── requirements.txt
└── .env.example
```
故意带有漏洞的 **fixtures 位于一个单独的 repo 中**,[`code-review-sample`](https://github.com/sriram98669704/code-review-sample),面板会在运行时直接从 GitHub 拉取它 — 因此它永远不是本项目内的嵌套 git repo。它是**一个示例输入,而不是该工具构建的核心** — 一个紧凑的测试套件,其文件涵盖了所有的审查维度:
```
code-review-sample/
├── accounts.py # PII in logs (compliance), mutable default arg (guardrail)
├── api.py # SQL injection; exact cross-file dup of db.get_user; two SAME-FILE path traversals — one a helper neutralizes (triage overrules it) + one it doesn't (kept)
├── db.py # hardcoded secret (top-level), SQL injection, in-file near-duplicate (get_user ↔ fetch_user), one clean control (user_exists)
├── downloads.py # CROSS-FILE path-traversal handlers — fetch_report / fetch_avatar, each flagged in isolation; the real verdict lives two hops away in paths.py
└── paths.py # the helpers downloads.py calls — strip_traversal (os.path.basename → safe, drop) vs collapse_slashes (only //→/, still vulnerable → keep)
```
## 审查内容
| 审查维度 | 寻找的示例 |
|---|---|
| **Security** | 硬编码的 secrets、SQL injection、command injection、path traversal、不安全的反序列化、对输入执行 `eval`、SSRF、缺少授权检查、禁用 TLS 验证、弱哈希 |
| **Compliance (GRC)** | 将 PII 写入日志或文件、在源码中提交 secrets、敏感数据处理 |
| **Guardrails** | 后门/硬编码凭据、裸 `except` / 吞没错误、可变的默认参数 |
| **Duplicates** | 冗余函数 — 包括**完全相同的副本**(逐字节相同)和**近似重复项**(即使在措辞改变后功能上仍然相同 — 例如重命名的变量或重新排序的行),无论是在文件内*还是*跨文件 |
[`code-review-sample`](https://github.com/sriram98669704/code-review-sample) repo 是一个故意做得小巧、带有预置漏洞的套件,它涵盖了每一个审查维度 — 包括跨文件重复 — 同时保持了低廉的扫描成本。它的核心是一对**匹配的** path-traversal 案例,按函数的 judge 对它们的标记*完全相同*,因为它们都是通过 helper 从用户输入构建文件路径的:
- `read_upload` 调用 `normalize_path`,它只是替换了斜杠并保留了 `../` → **真正容易受到攻击**,因此 triage **保留**了它。
- `read_export` 调用 `safe_name`,即 `os.path.basename`,它去除了 `../` → **安全**,因此 triage **推翻**了该发现并将其**丢弃**,并引用了该行。
这对匹配的案例随后**跨文件且深入两跳**重复出现(`downloads.py` → `paths.py`),这实际上是在测试跨过程图,而不是单个直接调用:
- `fetch_report` → `build_safe_path` → `strip_traversal`(`os.path.basename`,在另一个文件中) → **安全**,因此 triage **丢弃**了它。对 `build_safe_path` 进行一跳观察无法得出结论 — 它只是返回 `"reports/" + cleaned`;只有沿着链条进入 `strip_traversal` 才能揭示修复方案。
- `fetch_avatar` → `to_relative` → `collapse_slashes`(只处理 `//`→`/`,保留了 `../`) → 仍然**容易受到攻击**,因此 triage **保留**了它。
这两者具有相同的调用形状,但结论却截然相反,这只能通过进一步深入一个文件和一跳去阅读 helper 来决定 — 因此该套件端到端地测试了跨文件 resolver 和污点引导的追踪,而不仅仅是同文件调用。从调用点区分它们中的任何一个都是不可能的 — 这需要阅读 helper 并推断它是否进行了过滤,这正是 agent + triage 所做的工作。每个文件还包含至少一个*安全*的函数,因此该套件测试了 agent 不会产生狼来了的误报。这些 fixtures **没有带有任何暗示缺陷的注释**,因此一项发现意味着 agent 真正*检测到*了它 — 而不是它从注释中读出了答案。
## 局限性
这是一项针对**仅限 Python** 的 **LLM 辅助**审查。它像审查员一样对代码进行推理,这是它的强项,但这也意味着:
- 它**补充**了像 bandit/semgrep 这样基于模式的工具 — 它并不能取代它们。两者配合使用。
- **不同运行之间发现的次数可能会略有不同** — 模型并不是完全确定性的。
- 它审查你指定的代码;实时运行是**临时性的**,不会保留任何内容,因此没有运行历史记录可供随时进行比较。
- **agent 循环受制于宽松的安全上限** — 每次运行最多审查 `MAX_FUNCTIONS` (200) 个函数,并有 `MAX_ROUNDS` 次循环回合,这两者都远远高于任何正常的审查(示例大约是 30 个函数)。当达到上限时,报告会**说明跳过了什么**,而不是静默截断。
### 跨文件 resolver 不会追踪什么
为了读取另一个文件中的 helper,resolver 必须*证明*一个调用指向哪个函数,且仅使用它在静态分析中能看到的 imports 和定义。当它无法证明时,它会返回**空并保留该发现** — 它从不进行猜测,因为错误的猜测可能会读取错误文件中的 helper,从而错误地**丢弃一个真正的 bug**。因此,它故意放弃(并保留该发现)的情况包括:
- **在两个文件中都有定义的裸名**,当调用者既没有定义也没有显式导入它时 — 例如,一个恰好存在于两个不同模块中的 helper 名称。在干净的代码中很少见:这是一种回退机制,而不是常见路径,因为真正的裸调用通常要么定义在同一个文件中,要么通过名称导入(这两种情况*都*能解析)。
- **我们不追踪其类型的值上的方法** — `items.save()`,其中 `items` 是一个参数或局部变量。
- **遮蔽模块名称的局部变量** — `db = connect(); db.run()` 永远不会与 `db.py` 混淆。
- **Star imports** — `from x import *; mystery()` 掩盖了 `mystery` 的来源。
- **包相对导入** — `from . import helper`。
- **计算的调用目标** — `handlers[key]()`,`make_fn()()`(没有可解析的名称)。
第三方和标准库调用(`requests.get`,`os.path.basename`)不是 repo 函数,因此追踪在那里停止并将它们记录为叶子节点 **sink** — 它无法看到库代码内部。在所有这些情况中,发现都被**保留了,没有被丢弃**:稍微多了一点噪音是安全的选择;静默丢弃一个真正的漏洞是安全工具必须避免的一个结果。
### 追踪深度
污点引导的 call graph 追踪被刻意限制了:每个发现**深入 3 跳,横跨 12 个 helper**,并带有已访问集合的循环保护,因此它总是会终止(不会产生死循环,即使在相互递归的代码上也是如此)。这两个边界都是**可配置的** — [`gather_evidence()`](callgraph.py) 上的 `max_depth` / `max_helpers` — 而不是硬编码的魔法数字。埋藏深度超过 3 跳,或者超过第 12 个 helper 的修复将不会被看到 — 同样,该发现会被**保留**。污点追踪是**保守的**:它宁愿多包含一些 helper 也不愿冒着遗漏受污染路径的风险,因此一个集合中可能会携带一两个最终证明是无关的 helper。
追踪的污点源自函数的**参数** *以及* 一系列已知的输入**源** — `input()`、`os.environ`/`os.getenv`、`sys.argv`,以及 Flask 的 `request.form`/`request.args`/`request.get_json()` — 因此一个**不带参数**但从其中一个源读取输入的 handler(一个读取 `request.form` 并将其传递给 helper 的 Flask 视图)仍然会追踪其调用链并清除其发现。它唯一作为源种子的*全局变量*是 Flask 的 `request`,并且只有其**由调用者控制的成员** — 这是一个已知的框架输入边界,而不是任意值。它刻意**不**作为源处理的是**任意的模块全局变量**:全局变量可以容纳任何内容,因此污染每一个全局变量读取会导致过度丢弃 — 这是不安全的方向。这样的发现会被**保留**,只有 **agent**(其 `read_function` 不受污点限制)可以选择打开它的 helper### 受规则限制,分阶段进行
- **judge 只标记策略规则描述的内容** — 它不是通用的 bug 查找器。在 `policies/*.json` 中没有匹配规则的问题将不会被报告。每个 chunk 会显示**每个类别中最近的 5 条规则**,并且 grounding guard 会丢弃任何引用了未向其展示的规则的发现。
- **Triage 仅在模型能够引用所提供 helper 中确切消除风险的代码行时才丢弃发现**。resolver/walk 没有揭示的真正修复无法被引用 — 因此它会被保留。
- **重复检测报告每个函数相似度高于 `0.80` 的单个最近匹配项**,而不是聚类中的每一对;刚好低于该线的近似重复项将不会被确认。
## 后续计划
- **真实 repo 演示** — 展示针对已知漏洞项目的运行:vulpy、DVPWA 和 OWASP PyGoat。(Flask 的 `request` 现在是一个污点源,因此读取 `request.form`/`request.args` 并将其传递给 helper 的视图会追踪其跨函数调用链 — 这些 repo 正是用于测试这种案例的。)
- **离线测试** — 每一个纯净的、无 API 表面都在 `tests/` 下配备了一个免费套件(使用 `python tests/test_.py` 运行任意一个):具备 import 意识的 resolver、污点引导的 call graph 追踪、agent 探索摘要、BYOK 密钥解析 + 脱敏、AST chunker,以及确定性的安全检查。剩下的需要覆盖的部分需要一个 key — 依赖于 LLM 的过程(judge、triage、duplicate-confirm)的端到端集成测试。
- **按发现的预算** — 每次运行的上限(`MAX_FUNCTIONS`,`MAX_ROUNDS`)现在限制的是整个审查;一种更细粒度的、*按发现*划分的 token/tool-call 预算可以限制任何单个审查结论在病态调用链上可以花费的调查时间。
- **评估 — 衡量精确率/召回率。** 一个由已知漏洞和已知安全函数组成的带标签语料库,每次运行都进行评分,这样就可以*证明* prompt 或模型的改变是有益的,而不是靠肉眼观察。这是目前最大的现实差距:今天的正确性是从输出中读取的,而不是测量出来的。
- **可观测性 — 结构化的运行追踪。** 将每次运行的计时、token 成本和工具调用次数作为结构化日志发出,以便事后诊断缓慢或昂贵的运行 — 今天的实时运行是临时的,因此一旦它消失,就没有什么可供检查的了。
- **并发 / 规模。** 函数是按顺序审查的 — 一次进行一次 `judge` 调用,这对于约 30 个函数的演示来说很好,但与 repo 大小呈线性关系。在速率限制内对按函数的调用进行批处理或并行化,正是大型 repo 运行所需要的。
标签:Kubernetes, LLM Agent, Python, Streamlit, 人工智能, 代码审查, 无后门, 用户模式Hook绕过, 自动化payload嵌入, 访问控制, 逆向工具