静态 → LLM → 动态:一条 6 模块流水线,把 Python 漏洞告警噪声压到 4.3%
配套仓库:github.com/mabupt/open...
Python 3.11+
感谢各位查看并提出意见,劳烦各位star。
问题:现有 SAST 工具到底差在哪?
做应用安全的同学大概都有过这种经历:跑一遍 SAST(静态应用安全测试),出来 200 条告警, 真漏洞可能不到 20 条。剩下的全是误报------有的是跨文件有守卫函数没被静态规则识别到, 有的是参数化查询被框架包装后规则没跟上,有的干脆就是 docstring 里的示例代码被当成了真代码。
处理这些告警的时间,往往比修复真漏洞还长。
LLM 出来之后,很多人第一反应是"能不能用大模型做误报研判?"。想法很好,但实际落地时会遇到一堆工程问题: 模型会不会瞎判?上下文塞多少 token 才够?成本怎么控制?同一个告警换个 prompt 结果变了怎么办?
还有一个更根本的问题:静态分析给的是"可能有漏洞",但"真的能被利用吗?"只有动态执行才能回答。
于是我想:能不能把静态、LLM、动态这三件事串成一条流水线,让它们各司其职、层层过滤, 最终给你一个置信度分级的结论?
这就是 OpenSoft Detect 要做的事。
整体架构:6 个模块,一条流水线
文件过滤 ─▶ 静态发现 ─▶ 切片/路由/知识富化 ─▶ LLM 误报研判 ─▶ 容器内动态实证 ─▶ 报告
模块0 模块1 模块2 模块3 模块4 模块5
每个模块产出一个 JSON 文件传给下一个,整条链路的中间产物都落在 output/ 目录里,方便审计和归档。 任何环节缺失都能优雅降级,静态链路独立成立(后面专门讲)。
模块0 预处理:先搞清楚要扫什么
看起来简单,做起来有坑。测试目录、文档字符串里的示例代码、第三方 vendor 目录------ 这些东西如果不提前排掉,后面静态引擎会给你灌一堆无意义的告警。
模块0 做的事情:
- 硬排除 :
.git/、venv/、__pycache__/、migrations/、docs/ - 软排除 :
test_*.py、*_test.py、conftest.py(默认排除,--include-excluded可以拉回来) - 只保留
.py/.pyw
产出 file_manifest.json,里面有 scan_scope(实际要扫的文件列表)、excluded(被排除的文件及原因) 和 stats(文件数/行数统计)。后面所有模块都基于这份清单工作。
模块1 静态分析:三个引擎,谁也不依赖谁
静态分析是数据起点,我用了三个引擎并行跑:
| 引擎 | 擅长 | 实现 |
|---|---|---|
| Semgrep | 轻量、自定义规则灵活 | 8 条本地 taint 规则覆盖命令注入/SSRF/XXE 等 |
| CodeQL | 重量级、数据流分析强 | 离线 Python code-scanning 套件,DB 按目标哈希缓存复用 |
| pip-audit + OSV | 依赖漏洞 | requirements/pyproject.toml/dist-info METADATA 三路解析,OSV 精确 pin 回退,本地内容哈希缓存(二次审计 162s → 50s) |
三个引擎相互隔离:Semgrep 挂了?继续跑 CodeQL。CodeQL 建库超时?全量回退继续。 pip-audit 网络不通?用本地缓存。这种"谁也别耽误谁"的设计贯穿了整个项目。
去重后统一产出 findings.json。实测 pygoat(Django 漏洞靶场)得到 34 条代码告警 + 289 条依赖漏洞。
模块2 上下文富化:给 LLM 喂对的料
这步是把一条"裸告警"变成"带上下文的告警",LLM 研判的质量直接取决于这里喂的料对不对:
AST 路由/参数提取 :对 Flask/Django/FastAPI 项目,自动从 URL 配置里把路由和对应的处理函数关联起来, 再用 AST 扫一遍视图函数里的 request.GET.get() / request.POST.get() 调用,提取出参数名。 这样 LLM 就知道"这个告警对应的是 /cmd_lab 路由,用户输入参数叫 command"。
按工具粒度的代码切片:不是简单把整个函数丢进去,而是根据告警的规则类型(taint / injection / weak-hash) 做定向切片。单函数上限 100 行、跨文件数据流最多 8 步、总 token 控制在 3000 以内。 切片时会把被调的跨文件守卫函数也拉进来------这是缓解 CodeQL 自定义消毒函数误报的关键。
Qdrant 向量检索(RAG):预置了 CWE v4.20(969 条)+ ATT&CK Enterprise(2216 条)+ KEV(1665 条), 用 bge-small-en-v1.5(384 维)做嵌入。对每条告警跑语义检索,Top5 结果拼进去作为"交叉验证参考"------ 如果知识库也指向这个漏洞类型,LLM 判真阳性时信心更高。
富化后的 finding 长这样:
json
{
"rule_id": "python.lang.security.audit.command-injection",
"file_path": "introduction/apis.py",
"sink": "os.system",
"metadata": {
"code_context": {
"text": "def cmd_lab(request):\n cmd = request.GET.get('command')\n os.system(cmd)"
},
"route_summary": ["GET /cmd_lab -> introduction.apis.cmd_lab"],
"cwe_descriptions": {
"CWE-78": "OS Command Injection"
},
"vector_hits": [
{"source": "CWE", "id": "CWE-78", "name": "OS Command Injection"}
]
}
}
模块3 LLM 误报研判:确定性优先,LLM 兜底
这是我在工程上花时间最多的模块。核心原则:凡是能确定性判定的,都不调用 LLM。
预过滤层(完全不走 LLM)
- 命中测试文件 / 文档字符串示例代码 → 直接排除
- 切片里出现
secure_filename/shlex.quote/html.escape/is_relative_to/commonpath/literal_eval等消毒函数 → 预过滤判已防护 - 参数化查询特征 → 排除
- 依赖漏洞 → 事实性放行(不调模型)
- 规则明确的类别(如弱哈希策略)→ 确定性直判
LLM 调用层(只处理"含糊项")
temperature=0(消除随机性)- 强制 JSON mode
- 30s 超时 + 最多 3 轮重试
- Few-shot 双例:一个真实 TP(命令注入)+ 一个真实守卫误报,让模型对"跨文件守卫"有一致判据
- Prompt 版本号
prompt_v=1.4嵌入每次调用,方便追踪迭代
确定性守卫后过滤(不依赖 LLM 判断)
即使 LLM 判了 true_positive,只要切片里出现校验/消毒调用特征,就降级为待复核 。 这是专门治 CodeQL 对自定义消毒函数(如 chainlit 的 is_path_inside)的系统性误报。 不相信 LLM 能识别所有守卫,所以确定性规则再兜一遍。
知识库闭环
- 判为 false_positive 的样本写入
fp_features集合 - 研判前先检索 TP/FP 历史先验(相似度 > 0.85 才附加)
- Prompt 里标注"历史确认漏洞"或"历史判定误报",直接抑制重复误报
- 这比只存真阳性更有效------因为误报的模式重复率很高
成本控制
- 判定缓存 :按
provider/model/system/user哈希缓存 JSON 结果,TTL 30 天。 同 prompt 调两次 → 仅 1 次真实 API 调用 - 端点熔断:连续失败 ≥ 3 次即放弃剩余调用,避免"重试 × 条目数"放大耗时
- 模型路由:事实性/策略性直判项 0 次模型调用,只把含糊项送去模型
实测 pyload 项目:12 条策略直判 → 0 次模型调用。
模块4 动态验证:Import Hook 三层补丁 + 双轨污点检测
这是整条流水线的差异化核心,也是模块1-3 都无法替代的环节。
为什么需要动态验证?
静态分析说"这条数据流从 request 到 os.system,中间没消毒",LLM 说"我看过了,是真的"------但问题是: 这个代码路径真的可达吗?如果可达,真的能触发吗? 动态验证就是要回答这个问题。
Import Hook 三层补丁
我不想改写被测项目的源码(那不叫"检测"叫"插桩")。所以用了 Python 的 sys.meta_path 机制, 在被测代码 import 之前把危险函数替换成打点包装器。补丁分三层,互为兜底:
python
第一层:builtins 里的 eval / exec / open(直接替换)
第二层:os.system / subprocess.Popen / pickle.loads / sqlite3.Cursor.execute
(替换承载模块上的危险属性)
第三层:扫描 sys.modules,把 from os import system 产生的别名也替换掉
(兜底,覆盖 from-import 场景)
第四层(Finder 协议):拦截晚加载的目标模块,exec 后打点
覆盖的危险 Sink 超过 40 个,包括命令注入、反序列化、SQL 注入、SSRF、XXE、SSTI、弱哈希等各类别。 对于类(如 subprocess.Popen),只补 __init__ 而不整体替换------因为整体替换会破坏 isinstance 判断和子类化,实测踩过这个坑。
双轨判定
动态探针运行时,对每个 Sink 调用同时走两条轨道:
| 轨道 | 做什么 | 价值 |
|---|---|---|
| 轨道 A:Sink 调用记录 | 无差别记录所有命中 Sink 的调用 | 只要 Sink 被执行即证明路径可达;轨道 B 丢失时仍有旁证 |
| 轨道 B:Canary 污点追踪 | 构造带水印的 Canary 字符串注入用户输入点,在 Sink 参数里检查水印是否原样出现 | 直接证明污点从源到汇没被清洗 |
判定逻辑:轨道 B 命中 → DYNAMIC_CONFIRMED;轨道 A 有记录但 B 未命中 → retry_poc (可能污点传播中丢失了,需要更精准的 PoC);轨道 A 也没有 → 路径可能不可达。
还有一类"策略型漏洞"------比如弱哈希(hashlib.md5(password)),Canary 经过哈希后内容消失, 轨道 B 天然测不到。这种场景用类别化策略 Oracle:不查污点内容,查"用户可控字符串是否进入了弱哈希函数的位置参数"。
框架入口驱动
对 Flask/Django/FastAPI 项目,动态探针会自动:
- 从路由配置里解析全部 URL 和对应的视图函数(含
include()嵌套路由) - 用 AST 扫视图函数里的
request.GET/POST.get()调用,提取参数名 - 两种驱动模式------FastAPI/Flask 用 test_client,Django 支持 RequestFactory 直调 或真 HTTP(WSGI 起服 + 会话 + 免 CSRF + 按视图打点)
Docker 沙箱 + 副本隔离
动态探针必须跑在沙箱里,原因有二:
- 动态验证会真实触发 被测代码的漏洞------命令注入会真的执行
os.system(), SSRF 会真的发请求。这些操作在宿主上太危险。 - 更讽刺的是,有些漏洞本身就是"把用户输入写进文件"。我就遇到过自己的探针把被测靶场的源码 写坏了的事故(confirmed 从 15 掉到 2)。修复方式是把被测工程先复制一份,再以相同容器内路径挂载进去------ 探针的一切写入只落副本,宿主语料毫发无损。
容器配置:network_mode=none(断网防外泄)、512m 内存限制、60s 硬超时、统一回收。 还有止损闸:依赖声明超过 80 条直接跳过、构建超时 420s 自动 kill。
模块5 报告:四级置信度 + CWE-ATT&CK 关联 + 修复 Diff
最终产出两份报告:final_report.json(机器可读)和 final_report.html(人类可读)。
每条 finding 带:
- 四级置信度 :
DYNAMIC_CONFIRMED>TRUE_POSITIVE>UNVERIFIED>FALSE_POSITIVE - CWE → ATT&CK 关联:自动把 CWE ID 映射到 MITRE ATT&CK 技术/战术
- 修复 Diff(仅对动态确认项生成,带事实校验)
- 分引擎误报率:告诉你 CodeQL 在这个项目上误报率多少、Semgrep 多少
- 误报理由记录 :每条被判 false_positive 的 finding 都保留
fp_factors(LLM 给出的误报理由列表),方便人工复核
状态流转
markdown
NEW ─▶ UNDER_REVIEW ─▶ TRUE_POSITIVE ─▶ DYNAMIC_CONFIRMED ─▶ FIX_SUGGESTED
└─▶ FALSE_POSITIVE / UNVERIFIED
每一条 finding 都有明确的生命周期状态,不会出现"这条告警到底还管不管"的灰色地带。
逐级降级:任何环节缺失都能跑
这是我认为最能体现工程成熟度的部分。不是"我写了个工具,你必须装齐我所有依赖才能用", 而是任何环节缺失都能优雅降级,静态链路独立成立。
ini
CodeQL 不可用 → 只用 Semgrep + pip-audit
Semgrep 不可用 → 只用 CodeQL + pip-audit
pip-audit 不可用 → 跳过依赖审计
Qdrant 不可用 → 跳过向量检索(CWE 预载仍工作)
LLM Key 未配置 → 静态结论独立成立,自动跳过模块3
Docker 不可用 → 动态验证整体降级,skip_reason="no_docker"
缺 jinja2 → 只出 JSON 报告,不出 HTML
任何引擎失败 → 记录 engine_results 后继续,不阻断
甚至我用一个零第三方依赖的全新 venv 跑全流程,也能 exit 0。 Semgrep / CodeQL / pip-audit 走 PATH 正常工作,Qdrant 和 fastembed 缺失跳过, 无 LLM Key 走 static_only,无 docker 模块动态降级,无 jinja2 仅输出 JSON。
为什么要花精力做这个?因为用户的环境是不可预测的。有人只装了 Semgrep, 有人没有 Docker,有人 LLM 是自费的不想默认调------工具应该适配用户,而不是反过来。
实测数据:从 323 条到 14 条
跑 pygoat(人工设计的 Django 漏洞靶场,静态规则无特判):
| 环节 | 数量 | 说明 |
|---|---|---|
| 静态发现(去重) | 34 | Semgrep 8 + CodeQL |
| 依赖漏洞 | 289 | Django 4.2 / Pillow 9.4 / urllib3 / cryptography ... |
| LLM 研判 TP | 23 | 预过滤排除 11 条(docstring / 测试文件 / 已消毒) |
| 动态 confirmed | 14 | 11 条代码漏洞 + 3 条 DAST 独立发现 |
| retry_poc | 6 | 污点传播中丢失或非污点直达型(如懒查询、日志泄露) |
从 323 条 (34 代码 + 289 依赖)到 14 条 confirmed , 整条流水线把噪声压到了原来的 4.3%。
快速上手
bash
git clone https://github.com/mabupt/opensoft-detect.git
cd opensoft-detect
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
pip install semgrep pip-audit # 可选,缺失时自动降级
# 缺省:静态链路 + 报告(不跑动态探针)
python main.py --target <项目路径>
# 含动态验证(容器副本隔离,不会改动宿主源码)
python main.py --target <项目路径> --attempt-dynamic
# 一键基准:全流程 + 归档 + 摘要
python scripts/bench.py --target <项目路径> --name <归档名> --attempt-dynamic
配置 LLM(可选;未配置时模块3 自动降级,静态结论独立成立):
bash
export OPENSOFT_LLM_PROVIDER=openai
export OPENSOFT_LLM_API_BASE=https://api.example.com/v1
export OPENSOFT_LLM_MODEL=<model-name>
export OPENAI_API_KEY=<your-key>
已知局限(如实说)
- 静态覆盖面只扫
.py。XSS(模板里的|safe)、越权/认证逻辑、跨文件复杂数据流------ 本质检测不到,这不是规则能"写全"的,是检测面问题。 - 动态目前支持 Flask / Django / FastAPI;bottle、CherryPy 等小众框架不在范围内。
- Canary 污点追踪是"尽力而为",非完整跨函数污点传播。
retry_poc类多为"非污点直达型"(SQL 懒执行、日志泄露等),需要按类别的断言 Oracle。
这些我写在 CHANGELOG 和 docs 里了,不想粉饰。
接下来想做什么
| 优先级 | 方向 | 说明 |
|---|---|---|
| P0(已落地) | 依赖审计并入报告 + 运行归档 helper + 回归验证脚本 | 让结果可信、可复现、可迭代 |
| P1 | Django 视图直调补完(参数名从 urls/表单自动推断)+ 污点传播增强 | 动态是核心卖点,值得继续投 |
| P2 | HTML/XSS/模板检测独立 Analyzer + 真实语料 ground truth 评测 + 强模型 A/B | 静态广度与评测,按需做 |
做这个项目的最大收获不是"写了个漏洞扫描器",而是亲眼看到三层技术串起来之后, 噪声是怎么被一层层压下去的。静态给范围、LLM 筛误报、动态证真假------每一层都解决上一层的问题, 同时引入自己的局限,再由下一层兜住。
如果你也在做 SAST + LLM + 动态验证方向的工具,欢迎交流。仓库开了 Issue 和 Discussions。