为 CodeQL 自定义查询规则编写测试文件

0x00 前言

最近花了点时间研究 CodeQL,写了几个查询规则,效果还凑活。在翻 CodeQL 的官方库的时候里头有一些 test 文件啥的,这对我理解官方的查询规则非常有帮助。然后总 jio 着自己写的这几个规则差了点意思,就学了下 CodeQL 的测试文件怎么写,一边看文档一边测试,于是便有了本文。

CodeQL 提供了一个测试框架,用于对查询规则进行自动化回归测试,确保我们自定义的查询规则符合预期。

在执行查询测试时,CodeQL 会对用户期望的结果,和执行测试时实际产生的结果进行比较。如果预期的结果与实际产生的结果不同,该查询测试将会失败。为了 Fix 该条测试,我们应该迭代查询规则以及预期的查询结果,直到预期结果与实际结果完全一致。

本文主要介绍如何创建测试文件,以及使用 test run子命令执行测试。

全文主要包含如下内容:

  1. 为自定义查询设置测试 QL 包
  2. 为查询规则设置测试文件
  3. 运行 codeql test run
  4. 示例
  5. 后记
  6. References

0x01 为自定义查询设置测试 QL 包

CodeQL 测试文件必须存储于指定的测试 QL 包中,即我们将包含 qlpack.yml 文件的目录称为“测试 QL 包(test QL pack)”,qlpack.yml 文件格式如下:

1
2
3
4
name: <name-of-test-pack>
version: 0.0.0
libraryPathDependencies: <codeql-libraries-and-queries-to-test>
extractor: <language-of-code-to-test>

在 CodeQL 的官方库中,Java Queries 的 test QL pack 为 codeql/java/ql/test,其中 qlpack.yml 内容为:

1
2
3
4
5
6
7
name: codeql/java-tests
version: 0.0.2
dependencies:
codeql/java-all: "*"
codeql/java-queries: "*"
extractor: java
tests: .

libraryPathDependencies 的值指定了测试哪些查询规则。extractor 定义哪一个语言的 CLI 将被用于基于 QL pack 中的代码文件创建测试数据库,详情可参考链接 [3]。

在 CodeQL 的官方仓库中,每一个语言均有一个 src 目录,ql//ql/src,包含库和查询规则(我看了一下,实际上库是放在与 src 同级的 lib 目录下),同级目录下还有一个 test 目录,即为用于测试这些库和查询规则的测试文件存放位置。

test 目录被定义为 test QL Pack,其中包含若干个子目录,每个子目录的作用如下:

  1. query-tests 目录下,包含一系列子目录,每一个子目录下包含测试代码,和一个 QL reference 文件,用于指定对应的查询规则。
  2. library-tests 目录下,包含一系列列子目录,每一个子目录下包含测试代码,以及一个查询规则,该查询规则引用了对应的库,作为单元测试使用。
  3. experimental 目录下,包含一系列子目录。Github Security Lab 搞了一个 bounty 项目[4],接收外部安全研究员提交过来的有价值的查询规则,相关查询规则的测试文件均会放在该目录下。

0x02 为查询规则设置测试文件

对于每一个我们想要测试的查询规则来说,我们都应该在测试 QL 包(test QL pack)下创建一个子目录。然后在运行测试命令之前增加下列文件:

  1. 一个 query reference 文件(.qlref 文件),定义需要测试的查询规则的位置。该位置定义为包含查询规则的 QL pack 根目录下的相对位置。如:experimental/Security/CWE/CWE-759/HashWithoutSalt.ql 通常情况下,这个 QL pack 的目录会在 test pack 中通过 libraryPathDependencies 进行指定,参考链接[5]。

如果我们的查询规则位于 test 目录下,则无需定义 query reference 文件,但是从通用的最佳实践的角度来讲,仍然建议将查询规则与 test 文件分离在不同的目录下。唯一的例外是对 QL 库进行单元测试,其更倾向于存储与 test pack 中,和生成告警和 path 的查询规则进行分离。

  1. 一个查询规则针对的测试代码,这应该包含一个或多个文件,包含查询规则可以识别的代码示例。

我们可以定义一个预期的结果,用于与当我们针对测试代码执行指定的查询规则时产生的结果进行比较,该文件为 .expected 后缀。我们可以使用测试命令生成对应的 .expected 文件。(需要注意的是,当我们采用 CodeQL CLI 2.0.2–2.0.6 时,需要创建一个空的 .expected 文件,否则测试命令无法找到 test 查询。)

注:

  1. .ql、.qlref 、.expected 文件必须采用统一的文件命名。
  2. 如果想要在测试命令后直接指定 .ql 文件,必须有与之相对应的 .expected 文件。举例来说,如果查询规则名为 MyJavaQuery.ql,预期的执行结果文件必须为 MyJavaQuery.expected。
  3. 如果需要在命令中指定 .qlref 文件,也必须有与之相对应的 .expected 文件,但此处查询规则文件可以有与之不相同的名字。
  4. 示例代码文件名字不是必须与其他的测试文件统一,在 .qlref (或 .ql)文件相邻的示例代码以及子目录中的文件均会被用于创建测试数据库。因此,不要将测试文件保存在上级目录中。

0x03 运行 codeql test run

通过如下命令可以执行 CodeQL 的查询测试:

1
codeql test run <test|dir>

<test|dir> 参数可以是如下内容的一个或多个:

  1. .ql 文件地址
  2. .qlref 文件地址
  3. 用于递归检索 .ql 和 .qlref 文件位置的目录

也可以指定如下参数:

–threads,可选参数,用于指定运行查询规则时的线程数,默认值为 1,可以指定更多的线程数加快查询执行速度。指定 0 将会匹配 逻辑处理器(logical processors)的数量。

详细命令选项可以参考链接[6]。

0x04 示例

下列的示例代码展示了,如何为一个查询规则设置测试文件,该查询规则的内容是查询 Java 代码中 if 语句中,空的 then 代码块。包括如何增加自定义的查询规则和自定义的测试文件到一个 CodeQL 仓库 checkout 之外的 QL pack 中。

一、准备查询规则和相应的测试文件

  1. 写一个查询规则, 举例来说,如下查询规则, 可以发现 Java 代码中空的 then 代码块:
1
2
3
4
5
import java

from IfStmt ifstmt
where ifstmt.getThen() instanceof EmptyStmt
select ifstmt, "This if statement has an empty then."
  1. 在我们自建的查询目录中,创建一个名为 EmptyThen.ql 的文件写入上述文件内容。如:C:\Users\Administrator\Downloads\CodeQL_HOME\custom-queries\java\queries\EmptyThen.ql
  2. 在 custom-queries/java/queries 目录下创建 qlpack.yml 文件,定义 QL Pack,文件内容如下:
1
2
3
name: my-custom-queries
version: 0.0.0
libraryPathDependencies: codeql-java

关于 QL packs 的更多信息,可以参考链接[7]。

  1. 在 test 目录(custom-queries/java/tests)下,创建 qlpack.yml 文件,定义为 test QL pack,文件内容如下,注意 libraryPathDependencies 的值要与我们自定义的查询 QL pack 相匹配:
1
2
3
4
5
name: my-query-tests
version: 0.0.0
libraryPathDependencies: my-custom-queries
extractor: java
tests: .

qlpack.yml 文件声明了,my-query-tests 依赖 my-custom-queries,同时该文件也声明了,CLI 会使用 Java extractor 创建数据库。支持 CLI 2.1.0 及以上版本,tests: . 行,声明在 pack 中的所有 .ql 文件当我们执行 codeql test run 命令指定 –strict-test-discovery 参数时都会被当做 test 进行运行。

  1. 在 Java test pack 中创建一个测试目录,用于包含与 EmptyThen.ql 相关联的测试目录,如:custom-queries/java/tests/EmptyThen

  2. 在这个新的目录中,创建 EmptyThen.qlref 定义EmptyThen.ql 的位置。查询规则的地址,必须指定为包含查询的 QL pack 的相对根路径。在被例中,查询规则所在 QL Pack 的顶级目录为 my-custom-queries,其作为依赖被声明在了 my-query-tests 中。因此,EmptyThen.qlref 中的内容为 EmptyThen.ql 即可。

  3. 创建用于测试的代码片段,如下代码片段在第三行包含了一个空的 if 代码块,保存在 custom-queries/java/tests/EmptyThen/Test.java 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
public void problem(String arg) {
if (arg.isEmpty())
;
{
System.out.println("Empty argument");
}
}

public void good(String arg) {
if (arg.isEmpty()) {
System.out.println("Empty argument");
}
}
}

二、执行测试

移动到 custom-queries 目录,执行 codeql test run java/tests/EmptyThen 命令进行测试。

执行测试时,CodeQL 会进行如下几项操作:

  1. 在 EmptyThen 目录下查找测试文件
  2. 基于 EmptyThen 目录下的 .java 文件生成 CodeQL 数据库
  3. 编译 EmptyThen.qlref 中引用的查询规则

如果第 3 步骤失败了,这可能是由于 CodeQL 无法找到自定义的 QL Pack 导致的,重新运行命令,并且指定自定义 QL Pack 的位置,如:codeql test run –search-path=java java/tests/EmptyThen,如何将搜索地址(search path)作为配置文件的一部分,可以参考链接[8]。

  1. 通过运行查询规则、执行测试,生成 EmptyThen.actual 结果文件
  2. 检查 EmptyThen.expected 文件和 .actual 文件内容进行比较
  3. 报告测试结果,在本例中,存在一个失败的 case 0 tests passed; 1 tests failed:,测试失败是因为我们没有增加 EmptyThen.expected 文件

1

三、查看查询规则的测试输出

CodeQL 会在 EmptyThen 目录中,生成如下测试结果:

  1. EmptyThen.actual - 查询规则生成的真实测试结果
  2. EmptyThen.testproj - 可以加载进 VS Code 用于 debug test 失败原因的测试数据库。当测试完全成功,测试数据库会被自动删除,可以通过 –keep-databases 参数保留该测试数据库。

在本例中,测试失败符合预期,并且容易被解决。EmptyThen.actual 中的文件内容如下:

1
| Test.java:3:5:3:22 | if (...) | This if statement has an empty then. |

文件中包含了一张表,一列是查询结果的代码位置,接下来每一列是查询规则,select clause 的输出。由于结果符合预期,我们可以将该文件名更新为 EmptyThen.expected 作为符合预期的文件。

此时,重新运行测试命令,将会执行成功,所有的测试用例将会通过。

2

如果查询结果发生了改变,举例来说,如果你修改了查询规则的 select 语句,测试将会失败。对于失败的测试结果,CLI 的输出包括 EmptyThen.expected 和 EmptyThen.actual 的 diff 内容内容,这些信息可以用来 debug 简单的 test 失败场景。

对于难以去 debug 的复杂 test 场景,我们可以导入 EmptyThen.testproj 至 CodeQL for VS Code 中,执行 EmptyThen.ql,分析针对 Test.java 的查询结果,详情可以参考链接 [9]。

0x05 后记

最后晒一下,我的几个自定义查询规则的测试执行结果(官方示例就是直来直去的 Java 代码,直接编译即可,我的是 Spring Boot 下头的几个场景,所以会复杂一丢丢):

1
codeql test run java/ql/test/experimental/query-tests/security/XXX --search-path=java --show-extractor-output

3

0x06 References

  1. codeql/java/ql/test/qlpack.yml - https://github.com/github/codeql/blob/main/java/ql/test/qlpack.yml
  2. Testing custom queries - https://codeql.github.com/docs/codeql-cli/testing-custom-queries/
  3. About QL packs - https://codeql.github.com/docs/codeql-cli/about-ql-packs/
  4. Github Security Lab Bounty Project - https://hackerone.com/github-security-lab?type=team&view_policy=true
  5. Query reference files - https://codeql.github.com/docs/codeql-cli/query-reference-files/
  6. test run - https://codeql.github.com/docs/codeql-cli/manual/test-run/
  7. About QL packs - https://codeql.github.com/docs/codeql-cli/about-ql-packs/
  8. Specifying command options in a CodeQL configuration file - https://codeql.github.com/docs/codeql-cli/specifying-command-options-in-a-codeql-configuration-file/#specifying-command-options-in-a-codeql-configuration-file
  9. Analyzing your projects - https://codeql.github.com/docs/codeql-for-visual-studio-code/analyzing-your-projects/#analyzing-your-projects