《我写了 31 个静态扫描插件,踩了 7 个「假绿」》

0 阅读9分钟

我写了 31 个静态扫描插件,踩了 7 个「假绿」

写了两周,31 个插件。这篇不复盘功能,只复盘我踩的 7 个坑 —— 它们有个共同点:每一个看起来都是绿的。 每个 bug 都能回查,代码在这:github.com/feely0208/dsh-guard-core


一、先说一个让人心虚的界面

一个检查工具跑完后的输出

你写了个检查工具。跑完,屏幕上写着:

扫描范围   ./src
           142 个文件
检查规则   6/6 条
命中       0 条

这时候,你怎么知道「0」是哪个意思?

含义 A:查了,都没问题。
含义 B:一条规则都没真正跑起来。

这两句话在终端上长得一模一样。  一个绿字,一个也绿字。

同样是「0 命中」,含义完全相反

这就是「假绿」。  它不只是一个 bug —— 它是检测类工具的结构性风险:工具的失败模式和成功模式,输出完全一致。

做过检测、审计、扫描、CI 卡口的人都心虚过这一下。区别只在于,有人停下来查了,有人没有。


二、为什么「靠人用心发现」是不可靠的

我一开始的想法很朴素:我写的规则,我自己知道它有没有生效。

结果两周内踩了 7 个,全是设计错误,不是笔误:

错误 1:package-lock.json 被默认忽略列表排除

我写了「检查依赖漏洞」的规则。但我的默认忽略列表里包含了 package-lock.json(本意是「跳过锁定文件这种噪音」)。

于是:依赖检查一次都没跑过。  报告永远是「0 命中」。

错误 2:file-exists 依赖扫描器的输出

我要检查「有没有 lockfile」。实现方式是:在扫描到的文件列表里找 package-lock.json。

但 package-lock.json 已经被错误 1 排除了 —— 所以我这个检查永远报告「没有 lockfile」。

一个不存在的文件,和一个被过滤掉的文件,在检查逻辑里没有区别。

错误 3:path.extname('.env') 返回空串

我要检查 .env 文件。规则写的是「扫这些扩展名:arr, .env」。

跑下来永远不命中。查了半天:

path.extname('.env')   // → ''   加了点也不对

path.extname 对 dotfile 返回空串 —— 因为 .env 整个被当成文件名,没有扩展名。

修法:dotfile 用整个文件名当扩展名。

(这个坑有两层:如果 .env 只是被跳过,你会看到「跳过文件」列表;但它连跳过都没记,等于静默消失。)

错误 4:Dockerfile 没有扩展名,被当二进制跳过

我写了容器安全检查(比如「容器里是不是跑 root」)。样本里明明有 Dockerfile。

它没生效。  原因:我的扫描器按扩展名判断「是不是文本文件」,Dockerfile 没有扩展名 → 被当成二进制跳过。

修法:对无扩展名文件,读前 512 字节看有没有 NUL 字节 —— 没有就当文本处理。

这个修法后来救了不止一个规则:Makefile、LICENSE、id_rsa 全靠它才进来。

错误 5:逐行匹配让跨行正则永远不命中

这条最隐蔽。

我的规则引擎为了性能,默认逐行匹配。而我写了一条规则,正则跨越了两行:

{
  "pattern": "如果没有这个配置[\s\S]*就不要执行"
}

[\s\S]* 技术上能跨行 —— 但逐行匹配时它永远看不到下一行。  规则一次都没报过。

更麻烦的是:这个错误静态看不出来。「这条正则能不能跨行」是没有意义的问法 —— 因为规则本身并不需要跨行,是我写的时候脑子里跨了行。

修法:需要跨行的规则显式标 wholeFile: true。

错误 6:notApplicable 算出来了,但没透出去

我支持了「条件规则」:某些规则只在特定项目上跑(比如国密合规只在有密码学代码的项目上跑)。

引擎内部算得很对:本项目不适用 → 记为 notApplicable。

但它没出现在报告里。

于是用户看到的是「检查规则 6/6 条 · 命中 0 条」—— 实际上只有 3 条跑了。

「不适用」和「没问题」在报告里长得一样 —— 又是一次假绿。

修法:报告里必须写「检查规则 3/6 条(3 条因不适用本项目未运行)」。

错误 7:exts: ["js"] —— 少了那个点

这个是我写这篇文章的今天踩的。

我新建一个插件,写了 6 条规则,其中 4 条是扫代码的,exts 写成了:

"exts": ["js", "mjs", "cjs", "ts"]

而引擎里取扩展名的方式是:

const rawExt = path.extname(name).toLowerCase();   // → '.js'  带点

不带点,永远匹配不上。  4 条规则全是死的。

但报告显示「检查规则 6/6 条」—— 看着是绿的。

这一条后来被我加了静态检查:exts 不带点 = 必然是死规则,静态就能判定。 加完对着全部 32 个插件跑了一遍,存量 0 条。


三、7 个错误,收敛成 5 道门

假绿防治·五道门

踩完这些,我意识到一件事:「靠人用心发现」这件事本身就不成立。

上面 7 个错误,每一个我都「用过心」—— 但每一个都看起来是绿的。人不可能对每一个绿字都保持怀疑,那是灾难。

所以我把它们变成每次发布前必过的门:

门 1 · 死规则检测

每一条规则,都必须在「精心构造的正例样本」上报出至少一次。

报不出来 = 死规则 = 它永远为 0,看起来像「项目没问题」。

为什么这条比正则体检可靠:我试过用静态分析判断「这条正则会不会永远不命中」—— 判不准(见错误 5)。别猜正则,直接看它在真实样本上到底报没报。

门 2 · 规则体检

缺判据出处 / 正则编译不过 / exts 不带点 / 标题语义和实现相反(写「找不到」却用「匹配到就报」)。

这一层能静态判的全静态判 —— 能自动抓的,不该指望人记得。

门 3 · 双向验证

正例必须报 + 反例必须 0 命中。

前面所有门都在防「漏报」,这道门防误报 —— 而误报是工具被弃用的头号原因。一个天天报假警的工具,第三次之后没人看了。

门 4 · 跳过可见性

跳过率异常高的插件要单独报出来。

错误 1 和错误 3 都是「文件被静默跳过」。这条门就是盯这个:跳过了多少、跳过了什么,必须出现在报告里。

门 5 · 市场就绪

这一层跟假绿无关,是发布纪律:仓库公开吗、包名发上 npm 了吗、README 里的安装命令对吗。

(顺带说:这一条本身也踩过假绿 —— 我的检查脚本把「拉取超时」报成了「未发布」,于是网络一抽就误报 24 个插件没达标。判据必须分清「查不到」和「不存在」。 )


四、一个反直觉的推论:假的红和假绿一样坏

今天写这一节的时候,我又撞上一个。

我的门禁脚本 check-dead-rules.py,用绝对路径跑:

✅ 6 条规则全部能在正例上报出 · 死规则 0 条

用相对路径跑:

❌ 6/6 条是死规则

同一份代码、同一批样本,路径不同,结论相反。

根因是一行:

cli = os.path.join(pdir, 'bin', 'cli.mjs')       # 相对路径
subprocess.run(['node', cli, ...], cwd=pdir)     # cwd 切过去
# → node 实际找的是  pdir/pdir/bin/cli.mjs   ❌ 不存在
# → 无输出 → JSON 解析失败 → 【所有规则都被判成死规则】

一片假红。

为什么假红和假绿一样坏 —— 因为人会习惯性忽略红。

一个天天报错、但每次都是环境问题的门禁,一周之后没人看了。真正的规则回归,就藏在那片红里面。

所以那一整类门禁脚本,我后来都加了同一句:

export PATH="/usr/local/bin:$PATH"

别把「我能不能正确运行」交给调用者决定。


五、如果你也在写检测工具,这五道门可以直接拿走

门 1  死规则        每条规则都要在正例上报出(报不出 = 没查)
门 2  规则体检      静态能判的全静态判(exts 带点、正则编译、判据出处)
门 3  双向验证      正例必报 + 反例 0 命中(误报是头号杀手)
门 4  跳过可见性    跳过多少、跳过什么,必须在报告里
门 5  发布纪律      仓库/包/文档,三边一致

四条心法:

  1. 「0 命中」必须能拆开解释 —— 查了几条、跳过几个、几条不适用,全都写出来
  2. 判据不要猜正则 —— 直接拿真实样本跑一遍,看它报没报
  3. 静默跳过是最危险的 —— 跳过的文件、跳过的规则,都要露出来
  4. 判据必须分清「查不到」和「不存在」 —— 网络超时 ≠ 没有;不适用 ≠ 没问题

代码在这里(MIT):github.com/feely0208/dsh-guard-core


六、最后说一句这是干什么的

上面这些门,是在写 深鲸网安守护系列 的过程中长出来的。

31 个插件,一个插件查一件事 —— 等保对照、日志留存、数据出境、备案公示、个人信息权利、 未成年人保护、影响评估、数据分类分级、算法备案、国密合规、AI 内容标识、 AI 输出信任、MCP 安全、Agent 权限、策略核对、可疑指令、 敏感数据、密钥轮换、数据留存、配置审计、密钥扫描、开源许可、依赖投毒、CI 安全、 工具审查、物料清单、最小权限、告警外发、应急证据包……

全部本地跑,代码不外传;每条规则都标依据,可查、可核;开源免费。

官网直接下载,不经过任何第三方:deepwhale.org.cn/guard.html


附:本文提到的事实,都可以自己验

说法怎么验
path.extname('.env') 返回空串Node 里跑一下
逐行匹配让跨行正则失效写一条 [\s\S]* 的规则,看它报不报
exts 不带点就永不匹配同上
五道门能跑上面那个 GitHub 仓库,bash guard-audit.sh

我写这些不是为了证明我很小心 —— 恰恰相反。  是因为我发现自己「很小心」也没用,所以把它变成了机器的事。


这类「工具自己会不会骗人」的问题,我还在慢慢记 —— 归在《检测工具工程实践》这个系列里。

(本文部分内容由 AI 辅助生成)