我原本以为,把本地安全扫描器接进 GitHub Actions,只要写十几行 YAML。
结果真正花时间的,是下面四个问题:
扫描结果有没有权限上传?
扫描失败了,报告还会不会上传?
输出文件和上传路径是不是同一个?
第一次接入就阻断,会不会把历史问题全部变成 PR 事故?
Action 地址:
第一版工作流,看起来没有错
最初大概是这样:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: yuan1521913/code-audit-action@v0
with:
path: .
format: sarif
output: code-audit.sarif
fail-on: high
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: code-audit.sarif
逻辑上没问题:
读取代码
扫描
生成 SARIF
上传 SARIF
发现 high 就失败
但这套配置有三个隐藏假设。
隐藏假设 1:读取代码的权限,等于上传安全结果的权限
actions/checkout 只需要读取代码。
GitHub Code Scanning 的 SARIF 上传是另一种权限。
需要加:
permissions:
contents: read
security-events: write
少了 security-events: write,扫描本身可能全绿,文件也已经生成,但上传阶段没有权限。问题不在扫描器,而在工作流权限边界。
另外,Fork PR 的令牌权限通常更低。不要为了解决上传问题,直接把不可信 PR 代码放进高权限工作流里执行。先让同仓库 PR 和主分支 push 跑通,再单独设计 Fork 场景。
这和代码里的权限问题很像:
能读取资源
不等于
能写入另一个系统
隐藏假设 2:前一步失败,后一步一定还会执行
如果设置:
fail-on: high
扫描发现 high 时,Action 返回退出码 1。
这时 job 已经失败。后续上传步骤默认不会执行。
结果就成了:
扫描失败了
但开发者看不到 SARIF
只知道 CI 红,不知道红在哪里
所以上传步骤要加:
if: always()
完整写法:
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: code-audit.sarif
这一步不是“忽略失败”,而是先保证失败证据仍然可见。
隐藏假设 3:危险位置等于应该阻断
第一次接入就写:
fail-on: high
听起来很安全。
但如果项目里有历史问题,结果通常是所有 PR 一起变红。
真正需要拆开的是:
扫描发现问题
人工确认问题
团队决定什么时候阻断
这三件事不是一回事。
更合理的第一轮是:
fail-on: none
先让 SARIF 出现在 Code Scanning 或 artifact 里,连续跑几次,确认:
- 规则输出是否稳定。
- 误报主要来自哪些规则。
- 历史问题是否需要 baseline。
- 哪些 high 能立即修复。
然后再把新增 high 设为阻断条件。
Action 到底解决了什么
它解决的是触发和分发:
PR 或 push
-> 自动扫描
-> 输出 SARIF
-> GitHub 展示文件、行号和规则
它没有解决的是:
这条 finding 是不是真实可达
输入是否来自不可信来源
中间是否有参数化、转义、校验或 allowlist
修复后是否需要契约测试和重新扫描
自动扫描给的是位置。
人工确认给的是结论。
我会怎么分阶段接
第一阶段,只收集:
fail-on: none
第二阶段,修复新增 high,再收紧:
fail-on: high
第三阶段,再把历史问题纳入 baseline,只审查新增结果。
这条路径比“第一天上 Action,第二天所有 PR 全红”慢一点,但团队真的会继续用。
Action:
社区版源码和规则:
自动扫描不是正式安全审计,0 findings 也不等于代码安全。