Skip to main content

通过 CodeQL CLI 使用增量分析

通过仅分析变更内容,更快获得拉取请求的 CodeQL 结果。 在自己的 CI/CD 系统中运行 CodeQL CLI 时,增量分析最多可减少 10 倍的扫描时间。

谁可以使用此功能?

CodeQL 可用于以下存储库类型:

关于增量分析

对每个拉取请求都进行完整 CodeQL 扫描可能会很慢,尤其是在大型代码库中。 如果在自己的 CI/CD 系统中运行 CodeQL CLI ,增量分析提供了两种方法来加快速度:

  • 基于差异信息的分析 仅报告你新增或修改的代码行中的警报,因此查询运行得更快,结果也更相关。
  • 覆盖分析 重复使用默认分支中的缓存数据库,而不是从头开始生成一个数据库,从而大幅缩短数据库创建和查询评估时间。

可以独立使用这些功能,也可以一起使用。 对于大多数在成熟代码库中分析拉取请求的团队,我们建议同时使用这两种方法:使用叠加分析来快速创建数据库并进行查询评估,以及使用基于差异信息的分析来获得更聚焦且相关的结果。

如果您使用 code scanning 默认设置或在 codeql-action 上使用 GitHub,系统会自动处理增量分析。 本文适用于直接在自己的 CI/CD 基础结构中运行 CodeQL CLI 的团队。

先决条件

在设置增量分析之前,请确保满足以下要求:

  • CodeQL CLI 捆绑版本: 2.21.0 或更高版本,用于差异感知分析;2.23.8 或更高版本用于覆盖分析(使用每种语言的最小值,请参阅 最低 CLI 捆绑版本
  • 源根 必须位于 Git 存储库中
  • Git 版本 2.38.0 或更高版本(叠加分析所必需,具体来说是 --format 使用的 git ls-files 选项)
  • 所有相关文件必须由 Git 跟踪(不在 .gitignore 中)
  • Git 索引 必须准确反映正在分析的源树
  • 构建模式: 覆盖分析仅支持 build-mode: none(不支持跟踪构建)。 Go 适用于覆盖分析,尽管没有显式支持此模式。

选择方法

Scenario基于差异的Overlay
默认分支推送否(不是 PR)覆盖基模式
PR 分析(第一次,无缓存)Yes否(运行完整分析)
PR 分析(带缓存基)Yes叠加模式
非 PR、非默认分支NoNo

有关各种 CI 系统中的完整工作示例,请参阅 示例 CodeQL 管道配置 存储库。

基于差异的分析

基于差异的分析是针对拉取请求分析的一种优化。 它不会报告代码库中发现的所有告警,而是仅报告拉取请求差异中新增或修改的代码行里的告警。

步骤 1:识别差异范围

你需要从拉取请求的差异中获取新增或修改的行范围。 输入可以来自任何源(git diffCI 平台的 API 或其他机制)。

对于每个已更改的文件,请生成具有以下结构的范围列表:

  • path:绝对文件路径(始终使用正斜杠)
  • startLine:从 1 开始编号,且包含起始行
  • endLine:从 1 开始的,包含结束行

例如,给定以下统一 diff(由 git diff 生成):

--- a/src/utils.ts
+++ b/src/utils.ts
@@ -2,7 +2,6 @@ import { helper } from './helper';
 
 function existing() {
   const x = 1;
-  const unused = 2;
   return x;
 }
 
@@ -14,6 +13,8 @@ function validate(input: string) {
 function process(input: string) {
   // validate
   if (!input) return;
+  const sanitized = input.trim();
+  console.log(sanitized);
   return input;
 }
 
@@ -23,5 +24,5 @@ function format(value: number) {
 
 function render(data: object) {
   const output = JSON.stringify(data);
-  return output;
+  return `<div>${output}</div>`;
 }

src/utils.ts 的最终差异范围为:

  • ["/path/to/repo/src/utils.ts", 16, 17](第二个差异块中插入的两行)
  • ["/path/to/repo/src/utils.ts", 27, 27](第三个代码块中修改后的那一行)

第一个块仅包含删除内容,因此不会产生任何范围。 请注意,范围使用“to”(新文件)行号,而不是“from”(旧文件)行号。

特殊情况:

  • 二进制文件或非常大的差异内容(没有可用的补丁内容):使用哨兵范围 {path, startLine: 0, endLine: 0} 表示“整个文件”。
  • 重命名的文件没有内容更改:返回空数组(无范围)。
  • 截断的差异:如果对于大型拉取请求,差异来源不完整(例如,某个 API 会限制已更改文件的数量),则应跳过基于差异的分析,并对此次运行执行完整分析。

有关差异分析的参考实现,请参阅getDiffRanges()源代码。codeql-action

步骤 2:创建数据扩展包

创建包含两个文件的临时目录。 此扩展包接入了在 restrictAlertsTo 标准库中定义的 CodeQL 可扩展谓词。

** qlpack.yml:**

name: my-ci/pr-diff-range
version: 0.0.0
library: true
extensionTargets:
  codeql/util: '*'  # Target the codeql/util pack where restrictAlertsTo is defined
dataExtensions:
  - pr-diff-range.yml

** pr-diff-range.yml:**

extensions:
  - addsTo:
      pack: codeql/util
      extensible: restrictAlertsTo
      checkPresence: false  # Don't error if the predicate doesn't exist in older CLI versions
    data:
      # Each row: [filePath, startLine, endLine]
      - ["/path/to/repo/src/utils.ts", 16, 17]
      - ["/path/to/repo/src/utils.ts", 27, 27]

每个数据行都是 [filePath, lineStart, lineEnd]。 行号从 1 开始计数。 特殊情况 lineStart = 0, lineEnd = 0 表示整个文件匹配。

重要

如果差异中没有新增或修改的行(例如,只有删除),则仍必须提供一个非空的数据扩展,其中包含一个哨兵条目 ["", 0, 0]。 空 data 部分将使 restrictAlertsTo 谓词处于非活动状态,这意味着将生成所有警报,这与所需行为相反。

步骤 3:将扩展包传递给 CodeQL CLI

运行查询时,将以下标志添加到 codeql database run-queries

codeql database run-queries \
  --additional-packs=PATH_TO_EXTENSION_PACK \
  --extension-packs=my-ci/pr-diff-range \
  PATH_TO_DATABASE \
  QUERIES
  • --additional-packs 指示 CodeQL 在磁盘上查找包的位置。 有关详细信息,请参阅“数据库运行查询”。
  • --extension-packs 指示 CodeQL 加载命名的扩展包。

步骤 4:排除诊断查询

使用基于差异的分析时,应排除带有 exclude-from-incremental 标记的查询。 这些诊断查询不生成警报(例如指标或代码覆盖率),因此它们不会在增量上下文中提供任何值,但仍会消耗资源。

可以将此项添加到代码扫描配置文件:

query-filters:
  - exclude:
      tags: exclude-from-incremental

或者,创建排除这些查询的查询套件文件(.qls):

- description: Pull request queries for Java
- import: codeql-suites/java-code-scanning.qls
  from: codeql/java-queries
- exclude:
    tags contain: exclude-from-incremental

有关详细信息,请参阅“代码扫描的工作流配置选项”。

步骤 5:筛选 SARIF 输出

在 CodeQL 生成 SARIF 文件后,必须在 CI 端对输出进行筛选,以移除位置落在差异范围之外的结果。

对于 SARIF 中的每个结果,请检查其任一 locationsrelatedLocations 是否与该文件的某个差异范围相交。 当且仅当 range.startLine <= location.endLinelocation.startLine <= range.endLine 时,某个位置与某个范围相交。 特殊情形 range.startLine == range.endLine == 0 可匹配文件中的任意位置。 在比较之前,请确保将 SARIF 项目位置解析为差异范围中使用的同一绝对路径格式。

restrictAlertsTo谓词允许但不能保证查询省略范围外警报,因此需要 CI 端筛选才能获得稳定的结果。

有关 SARIF 筛选的参考实现,请参阅 filterAlertsByDiffRange() 源代码中的

用于差异分析的 CLI 标志摘要

CLI 命令FlagPurpose
codeql database init--codescanning-config=FILE代码扫描配置文件(用于查询筛选)
codeql database run-queries--additional-packs=DIR扩展包的位置
codeql database run-queries--extension-packs=my-ci/pr-diff-range要加载的扩展包的名称
codeql database interpret-results--sarif-run-property=incrementalMode=diff-informed(可选)使用差异感知元数据标记 SARIF

叠加分析

叠加分析通过构建在预先存在的“基础”数据库之上,加快了 CodeQL 针对拉取请求的数据库创建和查询评估:

  1. 在默认分支上: 生成“覆盖基”数据库(包含缓存的中间结果的完整数据库)。 这可以是拉取请求所针对的任何长期存在的分支。
  2. 对于拉取请求: 下载缓存的 overlay-base 数据库,然后创建一个轻量级的“overlay”数据库,该数据库只处理发生变更的文件。

叠加基础模式(默认分支)

每次合并后,在默认目标分支或长期存在的目标分支上运行 overlay-base 模式,以创建并缓存基础数据库。

1. 使用 --overlay-base 初始化数据库

codeql database init \
  --overlay-base \
  --db-cluster \
  PATH_TO_DATABASE \
  --source-root=PATH_TO_SOURCE \
  --language=LANGUAGE

--overlay-base 标志指示 CodeQL 生成一个数据库,该数据库可用作未来覆盖分析的基础。

2. 像平常一样构建并提取

像你平时为项目所做的那样,运行所有构建步骤并进行提取。

3. 记录文件 OID

提取完成后,记录源根下所有跟踪文件的 Git 对象 ID(OID)。 从源根目录运行此命令(PATH_TO_SOURCE)。 此快照稍后用于确定哪些文件已更改。

cd PATH_TO_SOURCE && git ls-files --recurse-submodules --format='%(objectname)_%(path)'

将此输出分析为 JSON 映射 { "relative/path": "git-oid" } ,并将其与数据库一起存储。 输出包含 Git 子模块中的文件,因此覆盖层分析需要准确跟踪基础层与覆盖层之间所有文件的更改。

4.运行查询并保留缓存

在覆盖基数据库上运行查询时, 请不要 传递 --expect-discarded-cache。 缓存的中间结果正是拉取请求构建速度快的原因。 放弃它们将强制对每个 PR 进行完全重新评估。

5. 清理和缓存数据库

分析后,使用 overlay 清理级别清理数据库:

codeql database cleanup PATH_TO_DATABASE --cache-cleanup=overlay

overlay清理级别保留比默认clear级别更多的缓存数据。 覆盖模式重复使用此缓存的数据,以便对拉取请求进行高效的查询评估,因此放弃会消除性能优势。

然后将数据库(包括 OID 文件)存储在缓存系统中,以便以后通过拉取请求生成进行检索。

叠加模式(拉取请求)

在拉取请求生成上运行覆盖模式,以在缓存基础之上创建轻型数据库。 如果缓存中没有可用的兼容覆盖基数据库(例如,在首次运行或版本升级后 CodeQL CLI ),请跳过 --overlay-changes 并运行正常的完整分析。 缓存键应至少包括 CodeQL CLI 版本和语言设置,以避免基础数据库不兼容。

1.下载缓存的覆盖基数据库

从缓存中检索最新的覆盖基数据库。 数据库应包含在 overlay-base 模式下记录的 OIDs 文件。

2. 计算已更改的文件

将基本数据库中记录的 OID 与当前的 Git 状态进行比较。 从覆盖基模式中使用的同一源根目录(PATH_TO_SOURCE)运行以下命令:

cd PATH_TO_SOURCE && git ls-files --recurse-submodules --format='%(objectname)_%(path)'

比较这两个映射,找出已添加、已删除或已修改(OID 不同)的文件。 将结果写入 JSON 文件:

{
  "changes": ["src/modified-file.ts", "src/new-file.ts", "src/deleted-file.ts"]
}

文件路径必须相对于源根目录。

3. 使用 --overlay-changes 初始化数据库

针对还原的覆盖基数据库目录运行 codeql database init 。 该 PATH_TO_DATABASE 命令必须指向还原的缓存覆盖基数据库,而不是新的空目录,该命令扩展了拉取请求分析的现有基础。

codeql database init \
  --overlay-changes=PATH_TO_OVERLAY_CHANGES_JSON \
  --db-cluster \
  PATH_TO_DATABASE \
  --source-root=PATH_TO_SOURCE \
  --language=LANGUAGE

重要

在覆盖模式下,不传递 --overwrite--force-overwrite。 你正在基于现有的缓存基础数据库生成,而不是替换它。

4.按正常方式生成、提取和运行查询

照常进行构建、提取和查询执行。 您可以将 --sarif-run-property 标志添加到现有的 codeql database interpret-results 命令中,以使用覆盖元数据对 SARIF 输出进行标记:

codeql database interpret-results \
  --format=sarif-latest \
  --output=results.sarif \
  --sarif-run-property=incrementalMode=overlay \
  PATH_TO_DATABASE \
  QUERIES_OR_SUITES

如果叠加层和基于差异的分析都已启用,请使用 incrementalMode=overlay,diff-informed

来自增量分析的警报会像来自完整扫描的警报一样,显示在拉取请求的代码扫描结果中。 任何叠加基础数据库无论新旧都能正常工作,但更新的基础数据库会带来更快且更准确的结果。

与基于差异的分析一样,在使用叠加模式时,排除带有 exclude-from-incremental 标记的查询。 有关详细信息,请参阅 步骤 4:排除诊断查询

覆盖分析的 CLI 参数概述

CLI 命令Flag模式Purpose
codeql database init--codescanning-config=FILE覆盖代码扫描配置文件(用于查询筛选)
codeql database init--overlay-base叠加基础为将来叠加使用构建基础数据库
codeql database init--overlay-changes=FILE覆盖仅使用已更改的文件构建叠加数据库
codeql database init
(否 --overwrite覆盖不要覆盖缓存的基础数据库
codeql database run-queries
(否 --expect-discarded-cache叠加基础保留缓存的中间结果
codeql database cleanup--cache-cleanup=overlay叠加基础使用叠加层专用清理级别
codeql database interpret-results--sarif-run-property=incrementalMode=overlay覆盖使用叠加元数据标记 SARIF

最低 CLI 捆绑版本

覆盖分析的基本最低版本为 2.23.8。 某些语言需要更高的最低版本:

| 语言 | CodeQL CLI 捆绑包最低版本要求 | |---|---| | C/C++ | 2.25.0 | | C# | 2.24.1 | | Go | 2.24.2 | | Java | 2.23.8 | | JavaScript | 2.23.9 | | Python | 2.23.9 | | Ruby | 2.23.9 |