🛠️ 第12章 场景四:PR、Review 与 CI/CD 自动化
📄 一句话概括

三类扫描不可替代:SAST 看代码怎么写的,依赖扫描看引用了谁,密钥扫描看有没有把秘密写进去。 六个直接依赖带出十三个传递依赖,攻击面是声明数量的三倍多。 密钥扫描最该放在提交前——进了 git 历史,改代码也没用,得换密钥。

📝 本章和前面几章的关系

上一篇审的是自己写的代码。这一篇把范围扩到「你引用的代码」。 为什么代码审查不够? 因为出问题的地方可能根本不在你的 diff 里——它在一个你没看过、也没写过的依赖里。 Demo 造一个小仓库,注入三类问题:SQL 拼接、硬编码令牌、三个有漏洞的依赖版本。 三个扫描器各跑一遍,各命中各的。 再算一次供应链的攻击面。

一、三类扫描的分工

扫描器 看什么 典型命中
SAST 源码怎么写的 SQL 拼接、命令注入、路径穿越
依赖扫描 引用了谁、哪个版本 已知漏洞的库版本
密钥扫描 有没有把秘密写进仓库 令牌、私钥、连接串

三类不可替代,因为它们的输入不一样。 SAST 读源码,依赖扫描读清单,密钥扫描读全部文本(包括配置文件)。 Demo 里三类各命中一处、一处、三处。 注意依赖扫描命中三处,因为那三个库的版本都刚好落在有洞的那一版上。 密钥扫描那一类的处理方式跟其他两类完全不同。 别的两类发现问题,改代码就行;密钥进了 git 历史,改代码没用—— 历史里还留着,任何能 clone 的人都能拿到。 所以它唯一的修法是换密钥,而换密钥意味着所有用到它的地方都要改。 这就是为什么密钥扫描要放在提交前。 放在提交后,你面对的不是「改一行」,是「走一遍密钥轮换流程」。

二、供应链风险

「我不用第三方库」这句话在今天基本不成立。 就算你一个都不装,你用的框架装了一堆。

层级 数量 你有多熟
直接依赖 6 写在清单里,大致知道
传递依赖 13 多半没看过
合计 19 每一个都是一次信任

Demo 里六个直接依赖带出十三个传递依赖,合计十九个,是声明数量的三倍多。 这个倍数不算高——真实项目里十倍很常见。 传递依赖的风险不在「它有害」,在「你不知道它存在」。 出问题的时候,你的清单里找不到它,所以第一反应是「不是我们的锅」。 然后排查两天,发现是你依赖的库依赖的那个库。 所以「锁版本」这件事的意义不只是「可重复构建」。 它还让「我到底用了什么」这个问题有一个确定的答案。 没有锁文件的时候,同一个清单在不同机器上装出来的传递依赖可能不一样。

三、演进方向

现在 在往哪走
提交后跑扫描 提交前跑,问题不进历史
扫源码文本 扫数据流,追污点从入口到汇点
人看告警 先按可利用性排序
审自己的代码 连依赖和构建脚本一起审

第二行的变化最大。 文本扫描只能告诉你「这里有个拼接」,数据流扫描能告诉你「用户输入能走到这里」。 后者才是「能不能被利用」的判据。 第三行更实际:告警多到没人看,所以「先看哪一条」比「报了多少条」重要。 一个百分之九十是噪音的扫描器,实际效果不如一个只报三条但条条要命的。 第四行是最容易被忽略的一行。 构建脚本和 CI 配置也是代码,而且它们通常有更高的权限—— 一个被改过的 Makefile 比一个被改过的 .py 危险得多。

四、动手 Demo:三类扫描:源码、依赖、密钥,各管一段

一个小仓库:一个 SQL 拼接、一个硬编码令牌、六个依赖里三个有洞。 三个扫描器各跑一遍,看各命中什么。 扫描器都是二十行左右的规则——真实工具复杂得多,但「三类各管一段」这件事是一样的。

# SAST:找 SQL 字符串拼接
if re.search(r"\+\s*\w+\s*\+|", ln) and "SELECT" in ln:
    print("SQL 字符串拼接", i)

# 密钥扫描:长串 + 变量名带 TOKEN/KEY/SECRET
if re.search(r"[\"'][A-Za-z0-9_\-]{20,}[\"']", ln) and \
        re.search(r"TOKEN|KEY|SECRET", ln, re.I):
    print("疑似硬编码令牌", i)

# 依赖扫描:比对已知有洞的版本表
if name in VULN and ver == VULN[name][0]:
    print(name, ver, "->", VULN[name][1])

实跑输出:

--------------------------------------------------------------------
三类扫描:源码、依赖、密钥,各管一段
--------------------------------------------------------------------

  三个扫描器分工明确,缺一类就有一类问题没人管。

  ① 被扫的仓库
  --------------------------------------------------------------------
  app/client.py(4 行)
  app/query.py(5 行)
  requirements.txt(6 行)

  ② 三类扫描各跑一遍
  --------------------------------------------------------------------
| 扫描器 | 文件 | 行 | 命中 |
|---|---|---|---|
| SAST | app/query.py | 6 | SQL 字符串拼接 |
| 密钥扫描 | app/client.py | 2 | 疑似硬编码令牌 |
| 依赖扫描 | requirements.txt | — | requests 2.19.0 有洞,升到 2.20.0 |
| 依赖扫描 | requirements.txt | — | pyyaml 3.13 有洞,升到 5.1 |
| 依赖扫描 | requirements.txt | — | urllib3 1.23 有洞,升到 1.24.2 |

  三类各命中 1 / 1 / 3 处。
  ! 三类不可替代:SAST 看「代码怎么写的」,依赖扫描看「引用了谁」,
  密钥扫描看「有没有把秘密写进仓库」。
  其中密钥扫描最该放在提交前——因为它一旦进了 git 历史,改掉代码也没用,得换密钥。

  ③ 供应链风险怎么估
  --------------------------------------------------------------------
| 层级 | 数量 | 说明 |
|---|---:|---|
| 直接依赖 | 6 | 你写在 requirements 里的 |
| 传递依赖 | 13 | 依赖的依赖,你多半没看过 |
| 合计 | 19 | 每一个都是一次信任 |

  六个直接依赖带出 13 个传递依赖,攻击面是声明数量的 3.2 倍。
  ! 这个倍数不是危言耸听,是「你信任了没看过的代码」的量化。
  它不能让你不装依赖,但能让你在装第十个的时候多想一秒。

  ④ 演进方向
  --------------------------------------------------------------------
| 现在 | 在往哪走 |
|---|---|
| 提交后跑扫描 | 提交前跑,问题不进历史 |
| 扫源码文本 | 扫数据流,追污点从入口到汇点 |
| 人看告警 | 先按可利用性排序,能利用的排前面 |
| 审自己的代码 | 连依赖和构建脚本一起审 |

  第三行值得单说:安全告警的数量会多到没人看,所以「先看哪一条」比「报了多少条」更重要。

三张表一起看,能看出「分工」这两个字的意思。 SAST 命中一行代码,依赖扫描命中一个版本号,密钥扫描命中一个字符串——三种东西,三种修法。 依赖扫描那三行要看清:requests 2.19.0、pyyaml 3.13、urllib3 1.23。 三个都是当年的正常版本,只是后来被发现有洞—— 这就是为什么依赖扫描要持续跑,不是装的时候跑一次。 供应链那段:六带出十三,合计十九。 数字不大,但要看「你有多熟」那一列——十三那一行写的是「多半没看过」。 这不是危言耸听,这是事实描述。 最后那句「先看哪一条比报了多少条更重要」值得记。 安全工具的价值不在检出数量,在「能不能让人真的去修」。

30 秒实验

  1. 在你项目里搜一次 sk-、ghp_、AKIA 开头的字符串。 搜到就说明密钥扫描该上了。
  2. 数一下你的锁文件里有多少个包。 跟你的清单条数比一比,那个倍数就是你的攻击面。
  3. 查一下你构建脚本最近的改动记录。 它是代码,而且权限比普通代码高。

常见误区

  1. 「我们自己写的代码没问题,不用扫。」 问题可能不在你的代码里,在你依赖的库里。 而那个库你多半没看过。
  2. 「密钥泄露了改掉代码就行。」 它已经在 git 历史里了。 唯一的修法是换密钥,而换密钥要走一遍轮换流程。
  3. 「扫描器报得多说明覆盖全。」 报得多说明噪音大。 一个没人看的扫描器,等于没有。
自测三问(点开对答案) 1. 三类扫描的输入有什么不同? —— **SAST 读源码,依赖扫描读清单,密钥扫描读全部文本。** **输入不一样,所以不可替代。** 2. 为什么密钥扫描要放在提交前? —— **因为进了 git 历史,改代码没用。** **修法从「改一行」变成「走一遍密钥轮换」。** 3. 传递依赖的风险核心是什么? —— **不是「它有害」,是「你不知道它存在」。** **出事时你在自己清单里找不到它。**

最后一篇收束整章:四级风险分级,以及信任是怎么一级一级攒出来的。

相关笔记