最近花了点时间研究 CodeQL,写了几个查询规则,效果还凑活。在翻 CodeQL 的官方库的时候里头有一些 test 文件啥的,这对我理解官方的查询规则非常有帮助。然后总 jio 着自己写的这几个规则差了点意思,就学了下 CodeQL 的测试文件怎么写,一边看文档一边测试,于是便有了本文。
CodeQL 提供了一个测试框架,用于对查询规则进行自动化回归测试,确保我们自定义的查询规则符合预期。
在执行查询测试时,CodeQL 会对用户期望的结果,和执行测试时实际产生的结果进行比较。如果预期的结果与实际产生的结果不同,该查询测试将会失败。为了 Fix 该条测试,我们应该迭代查询规则以及预期的查询结果,直到预期结果与实际结果完全一致。
本文主要介绍如何创建测试文件,以及使用 test run
子命令执行测试。
全文主要包含如下内容:
为自定义查询设置测试 QL 包
为查询规则设置测试文件
运行 codeql test run
示例
后记
References
CodeQL 测试文件必须存储于指定的测试 QL 包中,即我们将包含 qlpack.yml
文件的目录称为“测试 QL 包(test QL pack)”,qlpack.yml
文件格式如下:
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 内容为:
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/<language>/ql/src
,包含库和查询规则(我看了一下,实际上库是放在与 src 同级的 lib 目录下),同级目录下还有一个 test 目录,即为用于测试这些库和查询规则的测试文件存放位置。
test 目录被定义为 test QL Pack,其中包含若干个子目录,每个子目录的作用如下:
query-tests
目录下,包含一系列子目录,每一个子目录下包含测试代码,和一个 QL reference 文件,用于指定对应的查询规则。
library-tests
目录下,包含一系列列子目录,每一个子目录下包含测试代码,以及一个查询规则,该查询规则引用了对应的库,作为单元测试使用。
experimental
目录下,包含一系列子目录。Github Security Lab 搞了一个 bounty 项目[4],接收外部安全研究员提交过来的有价值的查询规则,相关查询规则的测试文件均会放在该目录下。
对于每一个我们想要测试的查询规则来说,我们都应该在测试 QL 包(test QL pack)下创建一个子目录。然后在运行测试命令之前增加下列文件:
experimental/Security/CWE/CWE-759/HashWithoutSalt.ql
通常情况下,这个 QL pack 的目录会在 test pack 中通过 libraryPathDependencies
进行指定,参考链接[5]。如果我们的查询规则位于 test 目录下,则无需定义 query reference 文件,但是从通用的最佳实践的角度来讲,仍然建议将查询规则与 test 文件分离在不同的目录下。唯一的例外是对 QL 库进行单元测试,其更倾向于存储于 test pack 中,和生成告警和 path 的查询规则进行分离。
我们可以定义一个预期的结果,用于与当我们针对测试代码执行指定的查询规则时产生的结果进行比较,该文件为 .expected
后缀。我们可以使用测试命令生成对应的 .expected
文件。(需要注意的是,当我们采用 CodeQL CLI 2.0.2–2.0.6 时,需要创建一个空的 .expected
文件,否则测试命令无法找到 test 查询。)
注:
.ql
、.qlref
、.expected
文件必须采用统一的文件命名。
如果想要在测试命令后直接指定 .ql
文件,必须有与之相对应的 .expected
文件。举例来说,如果查询规则名为 MyJavaQuery.ql
,预期的执行结果文件必须为 MyJavaQuery.expected
。
如果需要在命令中指定 .qlref
文件,也必须有与之相对应的 .expected
文件,但此处查询规则文件可以有与之不相同的名字。
示例代码文件名字不是必须与其他的测试文件统一,在 .qlref (或 .ql)文件相邻的示例代码以及子目录中的文件均会被用于创建测试数据库。因此,不要将测试文件保存在上级目录中。
codeql test run
通过如下命令可以执行 CodeQL 的查询测试:
codeql test run <test|dir>
<test|dir>
参数可以是如下内容的一个或多个:
.ql
文件地址
.qlref
文件地址
用于递归检索 .ql
和 .qlref
文件位置的目录
也可以指定如下参数:
--threads
,可选参数,用于指定运行查询规则时的线程数,默认值为 1,可以指定更多的线程数加快查询执行速度。指定 0 将会匹配 逻辑处理器(logical processors)的数量。
详细命令选项可以参考链接[6]。
下列的示例代码展示了,如何为一个查询规则设置测试文件,该查询规则的内容是查询 Java 代码中 if 语句中,空的 then 代码块。包括如何增加自定义的查询规则和自定义的测试文件到一个 CodeQL 仓库 checkout 之外的 QL pack 中。
import java
from IfStmt ifstmt
where ifstmt.getThen() instanceof EmptyStmt
select ifstmt, "This if statement has an empty then."
在我们自建的查询目录中,创建一个名为 EmptyThen.ql
的文件写入上述文件内容。如:C:\Users\Administrator\Downloads\CodeQL_HOME\custom-queries\java\queries\EmptyThen.ql
在 custom-queries/java/queries
目录下创建 qlpack.yml
文件,定义 QL Pack,文件内容如下:
name: my-custom-queries
version: 0.0.0
libraryPathDependencies: codeql-java
关于 QL packs 的更多信息,可以参考链接[7]。
qlpack.yml
文件,定义为 test QL pack,文件内容如下,注意 libraryPathDependencies
的值要与我们自定义的查询 QL pack 相匹配: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
进行运行。
在 Java test pack 中创建一个测试目录,用于包含与 EmptyThen.ql
相关联的测试目录,如:custom-queries/java/tests/EmptyThen
在这个新的目录中,创建 EmptyThen.qlref 定义EmptyThen.ql 的位置。查询规则的地址,必须指定为包含查询的 QL pack 的相对根路径。在本例中,查询规则所在 QL Pack 的顶级目录为 my-custom-queries,其作为依赖被声明在了 my-query-tests 中。因此,EmptyThen.qlref 中的内容为 EmptyThen.ql 即可。
创建用于测试的代码片段,如下代码片段在第三行包含了一个空的 if 代码块,保存在 custom-queries/java/tests/EmptyThen/Test.java 中。
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 会进行如下几项操作:
在 EmptyThen 目录下查找测试文件
基于 EmptyThen 目录下的 .java 文件生成 CodeQL 数据库
编译 EmptyThen.qlref 中引用的查询规则
如果第 3 步骤失败了,这可能是由于 CodeQL 无法找到自定义的 QL Pack 导致的,重新运行命令,并且指定自定义 QL Pack 的位置,如:codeql test run --search-path=java java/tests/EmptyThen
,如何将搜索地址(search path)作为配置文件的一部分,可以参考链接[8]。
通过运行查询规则、执行测试,生成 EmptyThen.actual 结果文件
检查 EmptyThen.expected 文件和 .actual 文件内容进行比较
报告测试结果,在本例中,存在一个失败的 case 0 tests passed; 1 tests failed:
,测试失败是因为我们没有增加 EmptyThen.expected 文件
CodeQL 会在 EmptyThen 目录中,生成如下测试结果:
EmptyThen.actual - 查询规则生成的真实测试结果
EmptyThen.testproj - 可以加载进 VS Code 用于 debug test 失败原因的测试数据库。当测试完全成功,测试数据库会被自动删除,可以通过 --keep-databases
参数保留该测试数据库。
在本例中,测试失败符合预期,并且容易被解决。EmptyThen.actual 中的文件内容如下:
| Test.java:3:5:3:22 | if (...) | This if statement has an empty then. |
文件中包含了一张表,一列是查询结果的代码位置,接下来每一列是查询规则,select
clause 的输出。由于结果符合预期,我们可以将该文件名更新为 EmptyThen.expected 作为符合预期的文件。
此时,重新运行测试命令,将会执行成功,所有的测试用例将会通过。
如果查询结果发生了改变,举例来说,如果你修改了查询规则的 select 语句,测试将会失败。对于失败的测试结果,CLI 的输出包括 EmptyThen.expected 和 EmptyThen.actual 的 diff 内容内容,这些信息可以用来 debug 简单的 test 失败场景。
对于难以去 debug 的复杂 test 场景,我们可以导入 EmptyThen.testproj 至 CodeQL for VS Code 中,执行 EmptyThen.ql,分析针对 Test.java 的查询结果,详情可以参考链接 [9]。
最后晒一下,我的几个自定义查询规则的测试执行结果(官方示例就是直来直去的 Java 代码,直接编译即可,我的是 Spring Boot 下头的几个场景,所以会复杂一丢丢):
codeql test run java/ql/test/experimental/query-tests/security/XXX --search-path=java --show-extractor-output
codeql/java/ql/test/qlpack.yml - https://github.com/github/codeql/blob/main/java/ql/test/qlpack.yml
Testing custom queries - https://codeql.github.com/docs/codeql-cli/testing-custom-queries/
About QL packs - https://codeql.github.com/docs/codeql-cli/about-ql-packs/
Github Security Lab Bounty Project - https://hackerone.com/github-security-lab?type=team&view\_policy=true
Query reference files - https://codeql.github.com/docs/codeql-cli/query-reference-files/
test run - https://codeql.github.com/docs/codeql-cli/manual/test-run/
About QL packs - https://codeql.github.com/docs/codeql-cli/about-ql-packs/
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
Analyzing your projects - https://codeql.github.com/docs/codeql-for-visual-studio-code/analyzing-your-projects/#analyzing-your-projects