关于增量分析
对每个拉取请求都进行完整 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、非默认分支 | No | No |
有关各种 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 中的每个结果,请检查其任一 locations 或 relatedLocations 是否与该文件的某个差异范围相交。 当且仅当 range.startLine <= location.endLine 且 location.startLine <= range.endLine 时,某个位置与某个范围相交。 特殊情形 range.startLine == range.endLine == 0 可匹配文件中的任意位置。 在比较之前,请确保将 SARIF 项目位置解析为差异范围中使用的同一绝对路径格式。
restrictAlertsTo谓词允许但不能保证查询省略范围外警报,因此需要 CI 端筛选才能获得稳定的结果。
有关 SARIF 筛选的参考实现,请参阅 filterAlertsByDiffRange() 源代码中的 。
用于差异分析的 CLI 标志摘要
| CLI 命令 | Flag | Purpose |
|---|---|---|
codeql database init | --codescanning-config=FILE | 代码扫描配置文件(用于查询筛选) |
codeql database run-queries | --additional-packs=DIR | 扩展包的位置 |
codeql database run-queries | --extension-packs=my-ci/ | 要加载的扩展包的名称 |
codeql database interpret-results | --sarif-run-property=incremental | (可选)使用差异感知元数据标记 SARIF |
叠加分析
叠加分析通过构建在预先存在的“基础”数据库之上,加快了 CodeQL 针对拉取请求的数据库创建和查询评估:
- 在默认分支上: 生成“覆盖基”数据库(包含缓存的中间结果的完整数据库)。 这可以是拉取请求所针对的任何长期存在的分支。
- 对于拉取请求: 下载缓存的 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=incremental | 覆盖 | 使用叠加元数据标记 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 |