CodeQL:从构建抽取到查询门禁的数据库分析链
CodeQL 不是对源码目录跑一遍规则。它先由语言 Extractor 观察源码或构建过程,把程序结构、控制流与数据流抽取成 CodeQL database,再由 query suite 或 query pack 查询。数据库漏了业务模块,后面再强的查询也只能对缺失模型给出绿色。
本文使用 CodeQL CLI 2.26.3 作为实验坐标。CLI bundle、Extractor、查询包和目标构建环境必须一起校准;GitHub 托管工作流的默认组合也会变化,不能把某次 Action 结果当成本地 CLI 的永久事实。
验证 CLI bundle 的来源
从官方 Release 下载适合平台的 bundle,核对发布摘要或组织制品库记录,再确认 CLI 与语言:
codeql version
codeql resolve languages
codeql resolve qlpacks生产 Runner 固定 bundle 版本或镜像 digest。仅更新 CLI 而继续复用旧查询包、旧 database 或旧缓存,会产生难以解释的混合状态。受限网络应提前同步 bundle 与 pack,代理和 CA 问题不能通过关闭 TLS 校验解决。
每种语言都要选择正确的创建模式
编译型语言通常需要在 database create 期间执行真实构建,让 Extractor 看到编译单元:
rm -rf codeql-db
codeql database create codeql-db \
--language=java-kotlin \
--command='./gradlew clean compileJava' \
--source-root=.解释型语言可以由 CLI 扫描源码,但 Monorepo、生成代码、依赖恢复和自定义构建仍会影响覆盖。自动构建适合标准项目,手工 --command 更可控;none 模式只适用于官方明确支持无需构建的语言。选择模式应基于目标语言与仓库构建事实,而不是为了缩短 CI 时间。
复杂流水线可以显式开始追踪、执行构建并完成 database:
codeql database init codeql-db --language=java-kotlin --source-root=.
codeql database trace-command codeql-db -- ./gradlew clean compileJava
codeql database finalize codeql-dbfinalize 成功不等于覆盖完整。它只表示数据库进入可查询状态,仍要检查日志、源文件数量、编译单元、语言和预期模块。
先验证数据库,再运行查询
创建完成后查看 database 信息与诊断:
codeql database info codeql-db
codeql database print-baseline codeql-db组织应维护一个小型夹具仓库,包含应被抽取的业务模块、明确不参与构建的目录和一个可由自定义查询识别的无害缺陷。验收不只看 database 大小,还要核对模块、源码路径、Extractor 警告和编译错误。缓存命中后同样复核 revision,防止拿上一提交的 database 分析当前代码。
查询与门禁是独立阶段
对 database 运行固定 suite,并输出 SARIF:
mkdir -p artifacts/security
codeql database analyze codeql-db \
codeql/java-queries:codeql-suites/java-security-and-quality.qls \
--format=sarif-latest \
--output=artifacts/security/codeql.sarif \
--threads=0 \
--rerun
test -s artifacts/security/codeql.sarif查询执行失败、结果命中和报告上传失败是三类状态。database analyze 的成功说明查询完成并写出结果,不自动等于团队 Quality Gate 通过。若平台按严重度、精度或新旧代码裁决,规则必须显式版本化,并保留原扫描状态。
Query Pack 是依赖包。自定义查询需要 pack 清单、版本约束、测试和发布渠道;引用浮动版本会让同一 database 在不同时间得到不同结果。升级 CLI 时也要验证 pack 兼容,不能把查询编译错误排除后继续宣称全量分析成功。
构建覆盖决定结论上限
Monorepo 需要逐个核对语言、模块和构建入口。构建脚本若只编译示例模块,database 仍可能正常完成。生成源码在构建前后出现的时点、条件编译、平台特定代码、子模块与 LFS 都可能形成空白。
CI 应记录目标 revision、语言、创建模式、构建命令、Extracted source 数、预期模块、数据库大小和警告摘要。数量不是通用阈值,而是与仓库基线比较的不变量;模块突然消失或 extracted files 断崖下降,应让门禁失败。
跨语言分析通常每种语言创建独立 database。不要把多个语言参数写进一条模糊命令后只确认其中一个成功。编译型语言还要区分构建失败与 Extractor 失败:前者先修复项目构建,后者保留 tracer 日志、环境变量和进程树证据。
SARIF 上传需要最小权限
SARIF 可能包含源码位置、消息、数据流路径和片段,本身是敏感制品。扫描任务只需要读取源码、依赖和构建产物;上传任务使用单独的最小权限 Token。Fork PR 不获得可写入安全平台或读取私有结果的凭据。
上传成功不能替代 database 覆盖验证。平台去重依赖规则 ID、位置与指纹,查询升级或路径归一化变化可能让旧结果重新出现。抑制记录需要 owner、理由、到期和查询版本,不能仅在平台点“dismiss”而没有仓库侧解释。
数据库是敏感且昂贵的中间资产
CodeQL database 包含程序结构、路径、字符串和构建上下文,不能当普通缓存公开共享。缓存键至少绑定 CLI、语言、构建输入和 revision,写权限按信任域隔离,保留期服从源码安全策略。
成本来自真实构建、Extractor 追踪、数据库磁盘、查询 CPU/内存和 SARIF 分诊。PR 可以对高风险语言运行固定 suite,默认分支与周期任务承担更深查询;分层必须以覆盖和查询合同为准,不是简单减少线程或跳过构建。
查询测试与误报处置
自定义查询应使用 CodeQL test 夹具,包含应命中、应通过和模型边界。规则结果要能回到 source、sink、path 和 sanitizer 的查询逻辑。误报处置优先修正模型或查询,再使用窄抑制;宽泛排除整个目录会让 database 有数据却不再产生结果。
升级前用同一源码快照分别创建新旧 database,并运行固定 pack,比较抽取模块、诊断、结果、路径、精度和耗时。复用旧 database 测新 CLI 只能验证查询兼容,不能验证 Extractor 变化。
常见故障如何分型
database 创建很快且结果为空,先确认构建命令是否真的编译业务模块。自动构建失败,改为可审计的手工命令并保存日志。查询包找不到,检查 codeql resolve qlpacks、下载源和 lock。SARIF 生成但平台无结果,区分上传权限、类别配置、revision 与平台过滤。
本机和 CI 结果不同,比较 bundle、pack、JDK/编译器、依赖缓存、构建参数与 source root。进程被 OOM 杀死时保留最大内存、线程数、database 大小和阶段;盲目重试可能重复昂贵构建,却没有定位 Extractor 或查询瓶颈。
回滚与退出
回滚必须同时恢复 CLI bundle、查询 pack、创建模式、构建命令和门禁口径,并从同一 revision 重建 database。保留旧 database 只适合对照,不应让它成为当前扫描输入。
迁移到其他分析器前,用原夹具验证构建覆盖与查询语义,再撤下 CodeQL。删除 database 和缓存前确认审计所需报告已经归档;清理目标必须是明确的分析目录,不能递归指向工作区根目录或共享缓存根。
