静态 → LLM → 动态:一条 6 模块流水线,把 Python 漏洞告警噪声压到 4.3%

静态 → 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.pyconftest.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 项目,动态探针会自动:

  1. 从路由配置里解析全部 URL 和对应的视图函数(含 include() 嵌套路由)
  2. 用 AST 扫视图函数里的 request.GET/POST.get() 调用,提取参数名
  3. 两种驱动模式------FastAPI/Flask 用 test_client,Django 支持 RequestFactory 直调 或真 HTTP(WSGI 起服 + 会话 + 免 CSRF + 按视图打点)

Docker 沙箱 + 副本隔离

动态探针必须跑在沙箱里,原因有二:

  1. 动态验证会真实触发 被测代码的漏洞------命令注入会真的执行 os.system(), SSRF 会真的发请求。这些操作在宿主上太危险。
  2. 更讽刺的是,有些漏洞本身就是"把用户输入写进文件"。我就遇到过自己的探针把被测靶场的源码 写坏了的事故(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>

已知局限(如实说)

  1. 静态覆盖面只扫 .py。XSS(模板里的 |safe)、越权/认证逻辑、跨文件复杂数据流------ 本质检测不到,这不是规则能"写全"的,是检测面问题。
  2. 动态目前支持 Flask / Django / FastAPI;bottle、CherryPy 等小众框架不在范围内。
  3. Canary 污点追踪是"尽力而为",非完整跨函数污点传播。
  4. retry_poc 类多为"非污点直达型"(SQL 懒执行、日志泄露等),需要按类别的断言 Oracle。

这些我写在 CHANGELOG 和 docs 里了,不想粉饰。

接下来想做什么

优先级 方向 说明
P0(已落地) 依赖审计并入报告 + 运行归档 helper + 回归验证脚本 让结果可信、可复现、可迭代
P1 Django 视图直调补完(参数名从 urls/表单自动推断)+ 污点传播增强 动态是核心卖点,值得继续投
P2 HTML/XSS/模板检测独立 Analyzer + 真实语料 ground truth 评测 + 强模型 A/B 静态广度与评测,按需做

做这个项目的最大收获不是"写了个漏洞扫描器",而是亲眼看到三层技术串起来之后, 噪声是怎么被一层层压下去的。静态给范围、LLM 筛误报、动态证真假------每一层都解决上一层的问题, 同时引入自己的局限,再由下一层兜住。

如果你也在做 SAST + LLM + 动态验证方向的工具,欢迎交流。仓库开了 Issue 和 Discussions。

相关推荐
蜗牛互联网1 小时前
语音AI开始边听边说,改变的不只是响应速度
java·人工智能·后端·语音识别
leoZ2311 小时前
2026-09-10-静态扫描连错三次-用运行环境当裁判
前端·javascript·vue.js·人工智能·opencv·机器学习·数据挖掘
一切皆是因缘际会1 小时前
轻量化端侧部署
人工智能
FL16238631291 小时前
苹果果梗枝条识别分割数据集labelme格式712张3类别
人工智能
齐齐大魔王1 小时前
机器学习(七)
人工智能·机器学习
跨境卫士—小依1 小时前
2026跨境电商数据分析入门:用指标判断选品与投放是否有效
大数据·人工智能·数据分析·跨境电商·营销策略
m4Rk_1 小时前
【论文阅读】Agent 记忆机制(68):Memp——把历史轨迹沉淀为可检索、可纠错的程序性记忆
论文阅读·人工智能·学习·开源·github
知识分享小能手1 小时前
深度学习学习教程,从入门到精通,自编码器 — 完整知识点与代码案例(14)
人工智能·深度学习·学习
来让爷抱一个1 小时前
2026 纹理一致性实战:贴图不许花脸,百智云精灵图把材质契约写进素材包
人工智能·机器学习·材质·贴图