安全阅读约 7 分钟

误报首先是文化问题,其次才是模型问题

把误报率降到零最简单的办法是关掉扫描器。多数团队不会关掉扫描器,他们只是开始不读结果。我们在客户现场反复看到的,不是模型精度问题,而是运营方式问题。

作者 FloatFactory 安全工程团队

FlawDetector 引擎 · 研究

3.2% 也足以压垮一个团队

在基准测试里,3.2% 的误报率是个好数字。但这个比例在真实队列中变成多少条,取决于代码库规模。扫描结果的条数几乎与代码行数线性相关,而分诊时间又与条数成正比。

3.2% 误报率带来的每周分诊负荷
代码库每周结果条数每周误报条数分诊时间(每条 12 分钟)
5 万行约 120 条4 条48 分钟
40 万行约 900 条29 条5.8 小时
200 万行约 4,300 条138 条27.6 小时

每条 12 分钟是我们观测到的中位数,包含阅读代码、尝试复现、记录判定。

每周 27.6 小时相当于 0.7 个安全工程师,而这项工作的产出是“什么都没发生”这个结论。没有哪个组织能长期为此配备专人。于是结局无一例外朝同一个方向发展:队列积压,积压的队列不再被阅读,而在没人读的队列里,真正的缺陷也一并被埋掉。

误报的代价是信任,不是时间

计算误报成本时,多数人只算分诊工时。真正昂贵的是之后发生的事。连续遇到三次误报的开发者不会打开第四条告警。这种习得不会停留在个人层面,而会变成团队的默认设定。当“安全机器人又在叫了”成为固定笑话时,那条流水线的检出率无论数字多高,实质上都趋近于零。

告警疲劳不是人变懒了。而是当信噪比越过某个临界点后,忽略就成了理性策略。

被称作“误报”的四种不同情形

分析客户的分诊记录会发现,开发者以“误报”关闭的结果中,引擎真的判断错误的不到一半。其余都是引擎判断正确、但组织决定不处理的情形。用同一个按钮关掉这两类,正是问题的起点。

被关闭结果的实际性质分类
实际上是表现正确处方
真正的误报代码中并不存在该缺陷提交引擎反馈并调整规则。只有在这里讨论精度才有意义
不可达缺陷成立,但外部输入到不了记录不可达的论证依据,并设置带到期日的抑制
已接受的风险知情且决定承担带负责人、补偿控制与复审日期的例外审批
优先级判断成立,但现在不是处理它的时候移入待办而非抑制,下次扫描应当再次出现

团队自己制造的误报

很多时候放大误报体感的不是工具,而是引入方式。三种模式反复出现。

  • 没有策略的全量扫描 — 第一天就对整个代码库打开所有规则,历史债务会和新缺陷一起倾泻而出。站在开发者的位置看,这整个队列都是噪声。
  • 没有门禁的报告 — 什么都不拦的报告没人会读;反过来一上来就拦住所有级别,团队就会关掉门禁。只拦新代码严重缺陷的窄门禁留存率最高。
  • 没有主人的队列 — 缺陷不自动分派,队列就变成公地。应当与代码归属文件打通,让缺陷生成的瞬间就确定责任团队。

五条有效的处方

  1. 01

    定下分诊 SLA

    严重缺陷 24 小时内、高危 3 个工作日内给出“首次判定”。是判定,不是修复。未经判定的缺陷堆积,比未修复的缺陷堆积更危险。

  2. 02

    强制抑制带到期日

    不允许永久抑制。上限设为 180 天,到期后在下一次扫描中自动重新打开。语境会变,半年前的“不可达”今天可能已经可达。

  3. 03

    就“可达”的标准达成共识

    什么算“外部输入能到达”,各团队理解不同。内部管理 API 算吗?批处理任务的参数呢?不先把标准写下来,分诊每次都会变成争论。

  4. 04

    给每条缺陷指定负责人

    与代码归属信息打通,在生成时就指定责任团队。可以转派,但不允许无人认领。

  5. 05

    每季度做一次误报复盘

    只把被归类为“真正误报”的案例聚起来做 30 分钟复盘,分成规则调整可解决的、需要补充框架识别的、以及应当改变我方代码惯例的三类。

把抑制规则写成代码

抑制若只以仪表盘上的一次点击存在,半年后没人知道是谁点的、为什么点。更稳妥的做法是把抑制作为仓库内的文件管理、走评审,并在 CI 中强制到期。

.flawdetector/suppressions.ymlyaml
- finding: FD-2026-05-0417
  rule: CWE-22/path-traversal
  path: internal/tools/migrate.go
  class: unreachable          # false-positive | unreachable | accepted-risk
  reason: >
    仅 CLI 路径。输入路径由部署流水线生成,
    不暴露在 HTTP 表面。参见威胁模型文档。
  evidence: docs/threat-model.md#migrate-cli
  owner: platform-team
  expires: 2026-11-30         # 到期后在下一次扫描中自动重开

- finding: FD-2026-05-0902
  rule: CWE-327/weak-hash
  path: legacy/auth/session.rb
  class: accepted-risk
  reason: "遗留会话兼容,计划随 2026-09 迁移移除。"
  compensating-control: "会话 TTL 15 分钟 + IP 绑定"
  evidence: JIRA-SEC-2841
  owner: identity-team
  expires: 2026-09-30
到期日、负责人与依据均为必填字段

然后在 CI 中检查这个文件本身。已过期的抑制、依据链接失效的抑制、责任团队已不存在的抑制,都会被流水线拦下。

bashbash
# 到期与孤儿抑制检查(放进每周定时任务)
flawdetector suppressions lint \
  --max-age 180d \
  --require-evidence \
  --require-owner \
  --fail-on expired,orphaned

# 查看当前被隐藏了什么
flawdetector scan --include-suppressed --format table

应该测量什么

误报率是评价工具的指标,不是评价团队的指标。要看安全运营是否健康,得看下面这些。

安全队列运营指标
指标定义建议目标
分诊前置时间 (P50)缺陷生成 → 首次判定24 小时以内
抑制到期合规率已过期却仍存在的抑制比例0%
重开率解除抑制后再次被确认为漏洞的比例低于 5%
严重缺陷封堵前置时间检出 → sealed 判定72 小时以内

这四项健康,误报率是 3% 还是 5%,流水线都转得动。反之这四项垮掉,换成误报率 1% 的工具结果也一样:队列照样积压,告警照样被忽略。

小结

提高精度是引擎的职责,我们会持续做。但在落地现场决定成败的变量,大多在运营侧:抑制有没有到期日?缺陷有没有主人?首次判定要等几天?先理顺这三件事的团队,用什么工具都能管住队列;没理顺的团队,用什么工具都会被队列淹没。

引入扫描器之前要定下的不是阈值,而是由谁、在什么期限内、依据什么来关闭一条缺陷

常见问题

换用误报率更低的工具,分诊问题就解决了吗?
负担会减轻,但问题不会消失。分诊队列中最耗时的不是“真正的误报”,而是可达性判断与风险接受决策。这两项与工具精度无关,需要组织自己定标准。
强制抑制带到期日,管理成本不会上升吗?
短期会上升。但没有到期的抑制会随时间演变成“没人说得清为何被关闭”的缺陷清单,在审计或事件响应时以更高代价回来。设定 180 天上限并自动重开后,实际续期负担约为每季度一次。
门禁应该从哪个级别开始开?
建议从只拦新代码中的严重缺陷开始。把历史债务一并拦住,往往导致团队直接关掉门禁。把债务拆成独立待办、按自己的节奏消化,留存率更高。
判定为不可达的缺陷还要留在报告里吗?
要留。不可达依赖于代码结构,一次重构就可能推翻。把论证依据与到期日一并记录,结构变化时才有重新评估的依据。

继续阅读

全部文章

在你自己的仓库上看循环跑一遍

接入仓库即可获得完整扫描、可直接合并的补丁,以及每个严重缺陷的判定日志。无需绑定信用卡。