FlawDetector-LLM V1.0:1,000 次重复试验,以及方差告诉我们的事
安全模型基准只报一个平均值,实际上就是宣传。团队真正在意的是:同一份代码周一和周五给出的答案一样吗。V1.0 试验给我们的不是一个漂亮的平均值,而是一张方差藏在哪里的地图。
作者 FloatFactory 安全工程团队
FlawDetector 引擎 · 研究
只报平均值,基准就变成了宣传
安全扫描器的基准通常浓缩成两个数字:检出率和误报率。两者多半来自一次运行,或若干次运行的均值。问题在于,团队在实际工作中承受的痛苦来自方差,而不是均值。
如果周一还是严重级别的缺陷到周五就消失了,那么这个工具的检出率是 94% 还是 97% 已经不重要——团队不会再相信输出。代码没变而判定变了,这种经历有一次就够了。所以 V1.0 试验设计里我们首先确定的,不是“怎么把平均值做高”,而是“怎么测量同一输入是否给出同一答案”。
试验设计:到底重复了什么 1,000 次
重复的单位是“单个样本的一次判定”,而不是“整套基准跑一遍”。我们固定语料、固定采样参数,反复投入同一输入,测量判定分歧的比例。这里的“判定”是一个三元组——是否存在缺陷、CWE 分类、严重级别——三者中任意一项不同就计为不一致。
| 语料 | 样本数 | 有漏洞 : 正常 | 作用 |
|---|---|---|---|
| OWASP 基准派生集 | 2,740 | 1 : 1.4 | 检出率与误报率基线 |
| 公开 CVE 复现仓库 | 318 | 1 : 0 | 验证真实代码库中的可达性 |
| 内部合成变异集 | 1,960 | 1 : 3.1 | 检查是否只是记住了特征 |
| 正常代码对照组 | 5,400 | 0 : 1 | 专用于误报率测量 |
合成变异集是把公开语料中的有漏洞样本改写变量名、结构和框架后重建的,用于滤除对训练数据的记忆。
重复试验的对象是分层抽样得到的 1,200 个样本,每个样本重复 1,000 次以上,判定总数超过 120 万次。之所以需要这个规模,原因很简单:当不一致率降到 1% 以下时,100 次重复无法在任何有用的置信区间内把它和噪声区分开。
FlawDetector-LLM V1.0 总体指标
94.7%
检出率
以 OWASP 基准套件为准
3.2%
误报率
经去重与可利用性排序之后
99.1%
判定一致性
同一输入重复 1,000 次以上
41 秒
整仓扫描中位数
p95 为 1 分 58 秒
3.2% 不是原始输出值。引擎一次输出的误报率为 5.8%,经过去重和基于可达性的排序后才降到 3.2%。两个数字都公开,是因为只给后处理值会让对比变得不可能。
方差藏在哪里
99.1% 这个总体值在不同缺陷族之间分布相当不均。下表有意思的地方在于,检出率和一致性是同向变动的:越难检出的缺陷族,判定也越容易摇摆。
| 缺陷族 | 检出率 | 判定一致性 | 特征 |
|---|---|---|---|
| 注入 (CWE-89 / 78 / 94) | 97.2% | 99.6% | 在单个函数内即可完成判断 |
| 弱加密 (CWE-327 / 330) | 96.1% | 99.4% | 以常量和 API 签名为主 |
| 认证与授权绕过 (CWE-287 / 862) | 93.8% | 98.2% | 中间件与处理函数分处不同文件 |
| 路径穿越与 SSRF (CWE-22 / 918) | 92.4% | 97.1% | 需要跟踪归一化函数 |
| 跨文件数据流 | 88.9% | 94.3% | 改善空间最大的区间 |
规律非常清楚:判断所需的依据是否落在同一个分块内几乎解释了一切。注入类缺陷的查询串拼接发生在一个函数内,模型每次看到的依据都相同;授权类缺陷则把路由、中间件、处理函数散落在不同文件,分块边界落在哪里,模型看到的依据就跟着变。
为什么会摇摆
我们抽取约 1 万条不一致案例做归因,三类原因解释了其中绝大部分。
- 分块边界问题(54%) — 校验逻辑和使用点被切进不同分块。模型只能凭看到的那一半下判断,而每次运行看到哪一半会有细微差异。
- 上下文截断(29%) — 大文件中开头的 import 和配置被截掉,模型无从得知哪个库已经提供了默认防护。ORM 自动做参数绑定却被误判为原始 SQL 的误报就出在这里。
- 顺序敏感性(13%) — 同一组分块只要呈现顺序变化,判定就会翻转,最常见的表现是严重级别上下浮动一档。
我们改了什么
三项改动依次应用,每一步之后都重新跑一遍重复试验协议。
- 01
锚点分块 — 让边界贴合代码结构
不再按固定 token 长度切分,而是以函数、类、路由定义为锚点,并强制把被引用符号的定义放进同一分块。分块大小变成可变的,但判断依据不再被切成两半。一致性 96.4% → 98.0%。
- 02
符号图预处理 — 先画地图再阅读
在调用大模型之前,用静态解析器生成调用图与数据流摘要,作为头部附加到每个分块上。模型始终知道“这个函数有三处调用,其中两处是外部入口”。一致性 98.0% → 98.7%。
- 03
三次多数表决 — 吸收残余抖动
仅对高危及以上的候选项打乱分块呈现顺序判定三次,再取多数。因为范围限定在候选项,总体延迟只增加 9%。一致性 98.7% → 99.1%。
误报率作为副产品也一并下降。预处理让框架自带防护变得可见之后,针对使用 ORM、模板引擎、校验中间件的代码的误报明显减少:一次输出口径从 5.8% 降到 4.1%,后处理口径为 3.2%。
剩下的瓶颈与 V1.1
一致性是用延迟换来的。加入预处理后,扫描时间的构成发生了变化。
| 阶段 | 占比 | V1.1 目标 |
|---|---|---|
| 符号图预处理 | 27% | 通过增量缓存在重扫时削减 90% |
| 大模型推理 | 61% | 通过分块批处理与提前终止削减 40% |
| 排序与报告生成 | 12% | 保持不变 |
V1.1 的 2.4 倍吞吐目标,是上面两项改善的合计值。
文件不变,预处理的结果就不会变——这是一个可缓存的计算,而 V1.0 每次都重做。增量缓存是 V1.1 中最确定的改进项,重复扫描越频繁收益越大,因此 CI 环境获益最多。
读安全模型基准的五个问题
包括我们自己的数字在内,任何基准只要问清下面五点,大部分夸大都会被过滤掉。
- 有没有重复试验结果?只有一次运行的数字,其可复现性无从判断。
- 误报率是后处理前还是后处理后的值?只公开排序去重后的数字是行业惯例,但要做对比就需要原始值。
- 正常代码对照组有多大?只有有漏洞的样本,无法测量误报率。
- 是否包含合成变异集?只用公开语料,无法区分记忆训练数据和真实检测能力。
- 有没有按缺陷族拆解的数值?一个总平均值会掩盖哪一族最薄弱。
小结
V1.0 试验最实用的产出是诊断而不是性能。方差集中的位置就是下个版本要修的位置,而要找到这个位置,就得看拆解后的数值而非平均值。我们花掉 120 万次判定,不是为了拿到一个好看的数字,而是为了知道该往哪里下手。
所有项目都以自有试验基准文档管理,包含语料构成与重复协议的详细报告可按需提供。
常见问题
- 判定一致性 99.1% 具体指什么?
- 指代码完全未变的同一输入重复投入 1,000 次以上时,是否存在缺陷、CWE 分类、严重级别三项全部相同的比例。三者中任意一项不同即计为不一致。
- 94.7% 的检出率基于哪套语料?
- 基于 2,740 个样本的 OWASP 基准派生集。此外还单独运行公开 CVE 复现仓库与内部合成变异集,用于区分训练数据记忆与真实检测能力。
- 跨文件数据流的一致性为什么较低?
- 因为判断所需依据分散在多个文件中,分块边界不同,模型看到的信息就不同。锚点分块与符号图预处理把该族提升到 94.3%,这一区间在 V1.1 中仍是优先改进目标。
- 有和其他扫描器的直接对比数据吗?
- 在公开语料上直接对比并不可靠,因为各工具的预处理与排序策略不同。我们改为公开语料构成与重复协议,以便在同等条件下复现结果。