CodeQL 和 Semgrep 放一条流水线里,别让它们互相替代¶
昨天把团队自己的坑写成了 Semgrep 规则,顺手想把 CodeQL 也塞进同一个 job,理由是「深一点的分析总没坏处」。跑完发现这个想法本身就有问题:两个工具吃的根本不是同一种输入,硬放一起只会让 PR 变慢,结果还互相覆盖。
今天把边界理清楚,顺便把结果汇总那一段的坑记下来。
一、输入不同,能回答的问题就不同¶
| Semgrep | CodeQL | |
|---|---|---|
| 分析对象 | 源码 / 文件,直接模式匹配 | 先生成 CodeQL 数据库,再对数据库跑查询 |
| 要不要构建 | 不需要 | 编译型语言要在 init 与 analyze 之间完成构建,让 CLI 监控编译过程 |
| 规则怎么写 | YAML,pattern / pattern-either / metavariable-pattern |
QL 查询,写 source、sink、barrier 的数据流约束 |
| 擅长 | 团队自定义的约定、框架误用、快速增量 | 跨文件、跨函数的污点追踪,同一个漏洞的各种变体 |
| 盲区 | 跨文件长链路的数据流 | 没建模的内部框架、不在语言矩阵里的代码 |
| 反馈速度 | 快,适合 PR | 多一步建库,注定慢一截 |
一句话:Semgrep 回答「我们规定不能这么写,你写了」,CodeQL 回答「用户输入能不能一路流到这个 sink」。前者是约定的自动化,后者是语义分析,谁也替不了谁。
耗时这块我不给通用数字,仓库规模、模块数、runner 规格差太多,随便写个分钟数反而误导。能确定的只有相对关系:CodeQL 多了一次完整建库,全量跑必须放到 nightly,不能进 PR 的关键路径。
二、动手前先查两件事:语言和许可¶
CodeQL 的语言支持是白名单,官方明确写了不支持列表之外的语言(点名 PHP、Scala),用了也不会生成告警,结果是不完整的,比不跑更危险。当前的语言标识符是这些:
注意 java-kotlin 和 javascript-typescript 是合并的标识符:写 javascript 等价于写 javascript-typescript,不会把 TypeScript 排除掉。actions 是分析 .github/workflows/*.yml 这类工作流文件的,跟业务代码不是一回事。
第二件事更容易被跳过:许可。CodeQL 官网写得很清楚,它免费用于学术研究、OSI 批准开源许可的代码库,以及在这几种情形下为自动化分析 / CI / CD 生成数据库——GitHub.com 上托管维护的开源代码库,或测试你自己以 OSI 开源许可发布的查询。除此以外,把 CodeQL 用于正常的软件工程流程里的自动化分析、CI/CD,需要联系销售。
也就是说,商业闭源项目想把它塞进内部流水线,别默认「开源工具随便用」,先确认许可。我们这边做规则库研究用的是自己的非商业代码,属于可以用的范围;换成公司生产仓库,这一步得先问过法务或者走 GitHub 的授权。
三、分工怎么定¶
我这边的切法:PR 只留 Semgrep 增量,CodeQL 放 nightly 全量。
# PR:沿用昨天那条命令,只扫 diff,几十秒到几分钟量级
- name: Semgrep (diff)
run: |
semgrep scan \
--config p/python --config .semgrep/ \
--baseline-commit "$(git merge-base origin/main HEAD)" \
--severity ERROR --error \
--sarif --output semgrep.sarif
- name: Upload Semgrep SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: semgrep.sarif
category: semgrep # 关键,见下一节
# nightly:CodeQL 全量,编译型语言必须把构建放在中间
- uses: github/codeql-action/init@v4
with:
languages: java-kotlin
queries: security-extended
- uses: github/codeql-action/autobuild@v4
# autobuild 失败就换成显式构建命令,别让它静默跳过
- uses: github/codeql-action/analyze@v4
with:
category: "/language:java-kotlin-nightly"
init → build → analyze 三步不能省中间那步。编译型语言靠 CLI 监控编译过程来建库,没构建就没数据库,最后会得到一个「分析成功、零告警」的假绿。解释型语言(Python、JS/TS、Ruby)可以直接从源码建库,不需要这一步。
Go 的行为跟版本和构建模式有关,这轮没验证,先不写死。
顺手记一下 action 版本¶
配这段的时候翻了 10 月那篇《DevSecOps 与安全测试左移》,里面写的是 github/codeql-action/*@v3。查了 GitHub 的 changelog:v4 在 2025-10-07 发布,跑 Node.js 24,v3 会在 2026 年 12 月随 GHES 3.19 一起弃用,之后不再有更新。用 advanced setup(也就是自己写 workflow 的这种)得手动把 init / autobuild / analyze / upload-sarif 四个 step 的引用改成 @v4,default setup 会自动迁。
GHES 3.18 及更旧的版本跑不了 Node 24 的 action,得先升服务端;3.19 要先让管理员开 GitHub Connect 才拉得到 v4。我们在 github.com 上,直接改引用就行,所以上面几个示例都是 @v4。
四、两个工具的结果汇到一处,有两个硬约束¶
4.1 同一 commit 传多个 SARIF,必须给不同的 category¶
GitHub 用 runAutomationDetails.id(SARIF 里字段写作 automationDetails.id)区分不同分析,格式是 category/run-id。如果两个上传用了同一个 category,后传的会覆盖前面的那组结果——表现是「Semgrep 的告警昨天还在,今天全没了」,其实是被 CodeQL 那次上传盖掉了。
规则很直白:同一工具、同一 commit、不同语言或不同代码部分,靠 category 区分。反过来,如果 id 里没有 /,整串会被当成 run id,category 为空,也起不到区分作用。用 upload-sarif action 就直接填 category 输入,比手写 SARIF 里的 id 稳。
partialFingerprints.primaryLocationLineHash 负责跨 commit 去重,GitHub 实际只用这个字段。用 upload-sarif action 上传第三方 SARIF 时,缺了它 action 会尝试从源文件补;直接调 API 上传而不带指纹,告警能显示,但会出现一堆重复条目。这也是我优先用 action 而不是自己 POST 的原因。
4.2 SARIF 有硬上限,超了是整份拒绝¶
官方文档这张表值得贴在流水线旁边:
| 对象 | 硬上限(超过即拒绝) | 实际保留 / 展示 |
|---|---|---|
| gzip 压缩后的文件 | 10 MB | 超过直接拒绝 |
| 每次 run 的 results | 25,000 | 只含前 5,000,按严重性优先 |
| 每个文件的 runs | 20 | — |
| 每次 run 的 rules | 25,000 | — |
| 每次 run 的 tool extensions | 100 | — |
| 每个 result 的 locations | 1,000 | 只含 100 |
| 每条 rule 的 tags | 20 | 只含 10 |
| 每个 result 的 thread flow locations | 10,000 | 只含前 1,000 |
第一次全量扫老仓库最容易撞的是 results 和文件体积。真撞上了别去拆分上传绕过——先把查询范围收窄、把干扰最多的查询禁掉。CodeQL 侧可以用 --max-paths=1 限制每条结果附带的数据流路径数,SARIF 会小很多。
还有个细节:ruleId 和文件路径要跨 run 保持稳定,否则指纹算不出来,同一问题每次都新建一条告警、关掉旧的,Security 页里看起来像问题反复出现又消失。
五、验收¶
1. PR 里故意写一条 Semgrep 违规,job 红,Security 页出现 semgrep 分类的告警
2. 同一 commit 再跑一次 CodeQL,Semgrep 的告警应仍在(没被覆盖)
3. 改一行无关代码再上传,告警数量不翻倍(指纹生效)
4. 故意让 autobuild 失败,确认流水线报错而不是「零告警通过」
5. nightly 产出的 SARIF 看一眼体积,离 10 MB 还有多少余量
第 2 条和第 4 条是这轮真正想验的。工具都跑通、结果却互相覆盖或静默空跑,比工具没接上更难发现。
坑位表¶
| 现象 | 根因 | 处理 |
|---|---|---|
| 一个工具的告警隔天全没了 | 两个 SARIF 用了同一 category | 每次上传给独立 category |
| CodeQL 零告警但代码明显有问题 | 编译型语言没在 init/analyze 之间构建 | 修 autobuild 或换显式构建命令 |
| PHP / Scala 扫不出东西 | 不在支持的语言矩阵里 | 换工具,别把「无告警」当通过 |
| SARIF 上传被拒 | 超过 25,000 results 或 gzip 后 10 MB | 收窄查询范围、--max-paths=1 |
| 告警反复新建又关闭 | ruleId 或路径不稳定、缺指纹 | 用 upload-sarif action、稳定路径 |
| PR 排队变长 | 把 CodeQL 全量放进了 PR | 挪到 nightly,PR 只留增量 |
还没验证的¶
queries: security-extended 比默认查询集多出来的告警,误报率到底高多少,这轮只跑了两个仓库,样本不够,先不下结论。另外多语言仓库一次 init 多个 language 时的数据库并行策略也还没试,等下周把前端那个仓加进来再说。