alexpermiakov/sast-triage
GitHub: alexpermiakov/sast-triage
利用 LLM agent 对 SAST 静态扫描结果进行自动化安全分拣,区分真实漏洞与误报,通过基于代码证据的缓存机制降低重复判定成本。
Stars: 0 | Forks: 0
# 🔍 Triage Agent
[](https://github.com/alexpermiakov/sast-triage/actions/workflows/ci.yml)
[](LICENSE)
**你开启了 SAST 扫描器,发现了 400 个漏洞,然后又把它关了。** 它们大多是误报;没有人有时间去检查。
`sast-triage` 所做的工作就像安全分析师一样:阅读每个漏洞背后的代码,追踪污点,判定它是否真实——并附带引用证据。只有在出现 _新的可利用_ 漏洞时,PR 才会验证失败。判定结果缓存在 git 中,以它们引用的证据作为键,并由人类通过 PR 进行审核。在第一次运行之后,分拣成本约为 $0。
### 工作原理
```
graph LR
A["findings.sarif"] --> B["Parse & Cache"]
B --> C{"New finding?"}
C -->|Hit cache| D["Use cached verdict"]
C -->|Miss cache| E["LLM agent\n(bounded loop)"]
E --> F["benign | exploitable | uncertain"]
D --> G["Report + Cache PR + Issues"]
F --> G
```
**安全边界:**
- 仅使用只读工具(`read_file`、`grep_repo`)——不进行写入,不执行命令
- 为每个漏洞设置 token 和迭代预算;每次运行设置漏洞数量上限
- 三值判定(无危险的默认值)
- 代码变更时缓存失效
**如果遇到 Prompt 注入——比如一条声称“这是安全的”注释——该怎么办?** 仓库内容作为证据进入 prompt,绝不会作为指令。判定为 `benign`(无害)需要引用该工具会重新验证的 `file:line`(文件:行号)证据——纯文本声称达不到要求。模型被欺骗的最坏情况就是给出错误的判定,而危险的方向(错误的 `benign`)需要最多的证据证明,需要在 PR 中由人工批准,并且在任何被引用的代码行发生改变时自动过期。
## 快速开始:CI
CI 是主要的用例,默认情况下它运行一个 **本地** 模型——不需要 API 密钥,不需要机密信息,源代码也不会离开运行环境。将以下内容放入 `.github/workflows/triage.yml`:
```
name: Triage
on: [pull_request]
permissions:
contents: read
jobs:
triage:
runs-on: ubuntu-latest
services:
ollama:
image: ollama/ollama:latest
ports: ["11434:11434"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with: { go-version: stable }
- run: GOBIN=/usr/local/bin go install github.com/alexpermiakov/sast-triage/cmd/sast-triage@latest
- run: curl -fsS http://localhost:11434/api/pull -d '{"name":"qwen2.5-coder:7b"}'
- name: Scan with opengrep → findings.sarif
run: |
curl -fsSLo /usr/local/bin/opengrep \
https://github.com/opengrep/opengrep/releases/download/v1.25.0/opengrep_manylinux_x86
chmod +x /usr/local/bin/opengrep
git clone --depth 1 https://github.com/opengrep/opengrep-rules /tmp/rules
opengrep scan -f /tmp/rules/go --sarif --dataflow-traces --output findings.sarif
- name: Triage — fail only on NEW exploitable findings
run: |
sast-triage -sarif findings.sarif -repo . \
-base-url http://localhost:11434/v1 -model qwen2.5-coder:7b \
-fail-on-new-exploitable
```
倾向于使用 Claude(或任何托管模型)?添加 `-provider anthropic` 和 `ANTHROPIC_API_KEY` 机密信息,或者添加 `-base-url https://api.openai.com/v1` 和 `OPENAI_API_KEY`——工具是一样的,只是 endpoint 发生了变化。
将 `/tmp/rules/go` 替换为你所用语言的 [规则目录](https://github.com/opengrep/opengrep-rules)。想先看看输出结果?本仓库的 [打开的警报](https://github.com/alexpermiakov/sast-triage/security/code-scanning) 和 [issues](https://github.com/alexpermiakov/sast-triage/issues) 均来自 [`demo/`](demo/)——这是我们故意保留未修复的漏洞代码,以便始终展示 pipeline 的真实输出。对于生产环境,请使用 [本仓库自身运行的工作流](.github/workflows/triage.yml):一切都被固定(action 按 SHA 固定,opengrep 按 sha256 固定,规则按 commit 固定),外加一个 push-to-main 作业,它会为可利用漏洞提交 issue,将分拣结果上传到 Security 标签页,并维护缓存审核 PR——合并该 PR 才是使后续运行命中缓存的关键。
## 直接运行
用于 CI 之外的一次性分拣。该工具从不凭空发明 endpoint——你始终通过 `-base-url` 指定模型所在位置,因此任何内容都不会发送到你未指定的任何地方(将其指向本地的 Ollama,就不会有任何数据离开你的机器):
```
go install github.com/alexpermiakov/sast-triage/cmd/sast-triage@latest
# 1. 扫描 — 任何发出带有稳定指纹的 SARIF 2.1.0 的工具都可以;
# opengrep(二进制文件:github.com/opengrep/opengrep/releases)是经过测试的工具
git clone --depth 1 https://github.com/opengrep/opengrep-rules /tmp/opengrep-rules
opengrep scan -f /tmp/opengrep-rules/go --sarif --dataflow-traces --output findings.sarif
# 2. 通过 Ollama 使用本地模型进行 Triage。-provider openai 是默认设置;
# -base-url 和 -model 始终是明确的 —— 该工具绝不会虚构 endpoint。
ollama serve & # http://localhost:11434
ollama pull qwen2.5-coder:7b
sast-triage -sarif findings.sarif -repo . \
-base-url http://localhost:11434/v1 -model qwen2.5-coder:7b
cat triage-report.md
```
输出:`triage-report.md`(阅读此文件),`triage-cache.json`(提交此文件——它是 agent 的记忆)。需要 Go 1.22+。
### Providers
`-provider openai`(默认)使用 OpenAI chat-completions API,因此它适用于 **任何** 兼容 OpenAI 的 endpoint——Ollama、vLLM、LM Studio 或 OpenAI 本身。`-base-url` 和 `-model` 都是必填项且没有默认值:该工具只与你指定的主机通信。本地 Ollama:`-base-url http://localhost:11434/v1`。OpenAI 官方:`-base-url https://api.openai.com/v1` 加上 `OPENAI_API_KEY`。
对于 Claude,使用 `-provider anthropic`(`-model` 默认为 `claude-sonnet-5`)并导出 `ANTHROPIC_API_KEY`:
```
sast-triage -provider anthropic -sarif findings.sarif -repo .
```
` |
**Effort 预设**(缩放每个漏洞的预算):
| Effort | read_file 行数 | grep 匹配数 | token 预算 | 迭代次数 |
| -------- | --------------- | ------------ | ------------ | ---------- |
| `small` | 100 | 25 | 30k | 6 |
| `medium` | 200 | 50 | 60k | 10 |
| `large` | 400 | 100 | 120k | 15 |
## 成本示例
| 场景 | Tokens | 成本 |
| --------------------------------- | --------- | ----------- |
| 首次运行(50 个漏洞,`medium`) | ~60k–300k | $0.30–$1.50 |
| 第二次运行(命中缓存) | ~0 | ~$0 |
| 增量更新(1 个新漏洞 + 49 个缓存) | ~6k | $0.03 |
典型漏洞:2k–6k tokens。首次引导开销很大;之后的每一次都很廉价。
## 功能
- ✅ **以证据为键的缓存** —— 当引用的代码发生变更时,判定结果自动过期
- ✅ **有边界的循环** —— 没有失控的 LLM 调用;每个漏洞都有迭代和 token 预算
- ✅ **只读工具** —— 不生成代码,不进行写入,没有意外
- ✅ **PR 审核工作流** —— 缓存更新汇集到一个审核 PR 中,由人工审核
- ✅ **CI 集成** —— 仅在出现 _新的_ 可利用漏洞时才使 PR 失败
- ✅ **Security 标签页集成** —— 分拣后的 SARIF 上传到 GitHub Code Scanning;无害的漏洞会被标记为忽略,并以判定理由作为解释
- ✅ **多扫描器支持** —— 带有稳定指纹的 SARIF 2.1.0
## 常见问题
## 开发
**许可证:** MIT | **使用 LLM agents 进行 SAST 漏洞分拣,受限且有缓存**
Flags
| Flag | Default | Purpose | | -------------------------- | ----------------- | ------------------------------------------------ | | `-provider` | `openai` | `openai`(任何兼容 OpenAI 的 endpoint)或 `anthropic` | | `-base-url` | — | 兼容 OpenAI 的 endpoint;`openai` 必填(无默认值),例如 `http://localhost:11434/v1` | | `-model` | — | 模型名称;`openai` 必填,`anthropic` 默认为 `claude-sonnet-5` | | `-effort` | `medium` | 深度:`small`、`medium`、`large` | | `-max-findings-budget` | `50` | 每次运行分拣的最大漏洞数 | | `-fail-on-new-exploitable` | off | 如果发现任何新的可利用漏洞则退出码为 3 | | `-parallel` | `4` | 并发的漏洞数 | | `-create-issues` | off | 为可利用的漏洞提交 GitHub issues | | `-link-base` | — | 例如,`https://github.com/owner/repo/blob/为什么要将缓存提交到 git?
- 漏洞级别的细粒度(相比于忽略文件和内联抑制注释) - 非破坏性(判定结果,而不是删除) - 携带原因、证据、时间戳 - PR diffs 即为审计追踪它只适用于 opengrep 吗?
不。它可以处理来自任何扫描器的 SARIF 2.1.0。opengrep 和 semgrep 是经过测试的工具——它们的 `matchBasedId` 指纹和数据流追踪被直接使用。其他任何支持 SARIF 的工具(CodeQL、Snyk Code、gosec、Bandit、Brakeman、SonarQube 等)也可以工作:当扫描器未输出稳定的指纹时,系统会根据规则 + 位置生成一个合成指纹,并且扫描器的特殊行为将被归入 `internal/sarif` 适配器中——这是一个解析问题,而不是 prompt 问题。我可以使用哪些模型?
开箱即用支持任何兼容 OpenAI 的 endpoint(`-provider openai`,默认):Ollama、vLLM、LM Studio 或 OpenAI 本身——通过 `-base-url` 和 `-model` 进行选择。Claude 可通过 `-provider anthropic` 使用。两者都是基于单方法 `Client` 接口([`internal/agent/client.go`](internal/agent/client.go))的轻量级适配器;新的 provider 只需实现一个包含 `Complete` 方法的文件即可。判定逻辑采用失败即关闭原则,因此较弱的本地模型会产生更多 `uncertain`(不确定)的判定,而绝不会静默产生 `benign`(无害)的判定——并且缓存会记录是由哪个模型对每个判定做出的决定。为什么 agent 不编写修复程序?
出于范围考虑。分拣是一项带有可验证输出契约的判断任务。拥有写入权限会将错误的判定变成错误的提交。仅作判断。测试与架构
``` go vet ./... go test -race ./... ``` - 纯包(`sarif`、`cache`、`report`)针对 fixtures 进行了表驱动测试 - `internal/agent` 使用伪造的 client 重放预设的工具使用记录 - 架构决策:[docs/DESIGN.md](docs/DESIGN.md)标签:AI智能体, AI风险缓解, DevSecOps, EVTX分析, Petitpotam, SAST, 上游代理, 日志审计, 漏洞审计, 盲注攻击, 错误基检测, 静态代码分析