2.CodeQL语法#
前言#
CodeQL的很多语法和现在的主流高级语言有很多相似之处,但也有许多的不同,学习的时候需要注意。
举一个简单的例子,在CodeQL中不存在==
,只有=
,当一个变量定义了而没有初始化的时候,=
的意思是赋值,但当其已经被赋值了之后,=
的意思就变成了比较。
基础数据类型(Primitive types)#
CodeQL 是一种静态类型的语言,因此每个变量都必须有一个声明的类型。类型是一组值。例如,int 类型是一组整数。注意,一个值可以属于这些集合中的多个,这意味着它可以有多个类型。
整型(int),浮点型(float),日期型(date),字符型(stirng),布尔型(boolean),简单介绍下日期型和布尔型。
日期型(date)#
日期型变量用于保存公历表示的时间值和日期值,如年、月、日、时、分、秒以及毫秒等,注意,它们的取值都是整数。其中,表示年的整数的取值范围是从-16777216到16777215,表示月的整数的取值范围为从0到11,表示日的整数的取值范围是从1到31,表示时的整数的取值范围是从0到23,表示分的整数的取值范围是从0到59,表示秒的整数的取值范围是从0到59,表示毫秒的整数的取值范围是从0到999。
编写一个简单的实例用于计算从今年9月1日到今天(11月2日)一共过了多久:
from date start, date end
where start = "01/09/2021".toDate() and end = "02/11/2021".toDate()
select start.daysTo(end)
这里我们用到了字符串的一个内置函数
toDate()
,更多的相关函数可以查阅CodeQL文档- string - go
- date - go
布尔型(boolean)#
布尔型变量用来存放布尔值,即false(假)或者 true(真)。
编写一个简单的例子来实现两个布尔之间的和关系:
from boolean a, boolean b
where a = true and b = false
select a.booleanAnd(b)
更多相关函数可以查阅CodeQL文档
- boolean - go
谓词(Predicates)#
谓词有点类似于其他语言中的函数,但又与函数不同,谓词用于描述构成 QL 程序的逻辑关系。确切的说,谓词描述的是给定参数与元组集合的关系。
定义谓词有以下几个注意点(坑点):
1. 需要注意的是谓词的名字开头必须是小写字母。
2. 绑定行为与绑定集,这个在后面会介绍。
无结果谓词#
没有结果的谓词以predicate作为开头,剩下的语法结构类似于定义函数。这种谓词只能在where语句中使用
一个简单的例子如下:
predicate isCity(string city) {
city = "Beijing"
or
city = "ShangHai"
}
from string city
where city = "Beijing" and isCity(city)
select city
结果谓词#
有结果的谓词的定义类似于c/c++语言的函数定义,以返回类型替代predicate作为开头。这种谓词可以在where与select语句中使用
一个简单的例子如下:
int addOne(int i) {
result = i + 1 and
i in [1 .. 10]
}
from int v
where v = 1
select addOne(v)
递归谓词#
这里说的递归并非我们常规理解的函数递归,我们可以理解为一个reverse(可反向查找的)谓词,或者换一个思维,把非递归的结果谓词理解为一个有向图,那么递归的结果谓词可以理解为一个无向图。
一个简单的例子如下:
string getANeighbor(string country) {
country = "France" and result = "Belgium"
or
country = "France" and result = "Germany"
or
country = "Germany" and result = "Austria"
or
country = "Germany" and result = "Belgium"
or
country = getANeighbor(result)
}
from string people
where people = getANeighbor("Germany")
select people
可以看到查询到三个结果,
France
,Austria
,Belgium
,查询到France
的原因就是它可以反向查找。
特征谓词,非成员谓词,成员谓词#
谓词分为三种,即非成员谓词、成员谓词和特征谓词。
非成员谓词是在类之外定义的,也就是说,它们不是任何类的成员,而成员谓词则是在类里面定义的。特征谓词则是类中的特殊谓词,类似于其他语言中类的构造函数。
下面是每种谓词的示例:
int getSuccessor(int i) { // 1. Non-member predicate
result = i + 1 and
i in [1 .. 9]
}
class FavoriteNumbers extends int {
FavoriteNumbers() { // 2. Characteristic predicate
this = 1 or
this = 4 or
this = 9
}
string getName() { // 3. Member predicate for the class `FavoriteNumbers`
this = 1 and result = "one"
or
this = 4 and result = "four"
or
this = 9 and result = "nine"
}
}
绑定行为与绑定集#
谓词所描述的集合通常不允许是无限的,换句话说,谓词只能包含有限数量的元组(It must be possible to evaluate a predicate in a finite amount of time, so the set it describes is not usually allowed to be infinite. In other words, a predicate can only contain a finite number of tuples.)
举个简单的正例和反例:
// 正例,i被限定在1到10内,或者你也可以给i赋一个确定的值如i=1
int addOne(int i) {
result = i + 1 and
i in [1 .. 10]
}
// 反例,i是无限数量值的,此时CodeQL编译器会报错: 'i' is not bound to a value
int addOne(int i) {
result = i + 1 and
i > 0
}
单个绑定集#
为了使上述的反例谓词能够通过编译,我们可以使用绑定集(bindingset),但是当我们去调用这个谓词时,传递的参数还是只能在有限的参数集中。
上面的反例可以修改为如下:
bindingset[i]
int addOne(int i) {
result = i + 1 and
i > 0
}
// 此时我们可以去调用这个谓词,但是需要注意传递过来的参数还是只能在有限的参数集中
from int i
where i = 1
select addOne(i)
多个绑定集#
我们同样可以添加多个绑定集,下面是一个例子:
bindingset[x] bindingset[y]
predicate plusOne(int x, int y) {
x + 1 = y
}
这个绑定集的意思是如果x或y绑定(bound)了,那么x和y都绑定,即至少有一个参数受到约束。
如果我们想要两者都受约束,可以将例子修改一下:
bindingset[x, y]
predicate plusOne(int x, int y) {
x + 1 = y
}
那么这个谓词就变为了一个类似于校验的函数,即
x+1 == y
查询(Query)#
查询是CodeQL的输出。查询有两种类型,分别是
- select子句
- 查询谓词,这意味着我们可以在当前模块中定义或者从其他模块中导入
select子句#
select子句的格式如下:
[from] /* ... variable declarations ... */
[where] /* ... logical formula ... */
select /* ... expressions ... */
其中from和where语句是可选的。我们可以在from中定义变量,在where中给变量赋值和对查询结果的过滤,最后在select中显示结果。
在select语句中我们还可以使用一些关键字:
-
as
关键字,后面跟随一个名字。作用相当于sql中的as
,为结果列提供了一个"标签",并允许在后续的select表达式中使用它们。-
order by
关键字,后面跟随一个一个结果列名。作用相当于sql中的order by
,用于排序结果,并且在结果列名后可选asc
(升序)或desc
(降序)关键字。
一个简单的例子如下:
from int x, int y
where x = 3 and y in [0 .. 2]
select x, y, x * y as product, "product: " + product
查询谓词#
查询谓词是一个非成员谓词,并在最开头使用query
作为注解。它返回谓词计算结果的所有元组,下面是一个简单的示例:
query int getProduct(int x, int y) {
x = 3 and
y in [0 .. 2] and
result = x * y
}
它返回的结果如下:
编写查询谓词而不是select子句的好处是我们可以在代码的其他部分中调用谓词。例如,我们可以在类中的特征谓词内部调用:
query int getProduct(int x, int y) {
x = 3 and
y in [0 .. 2] and
result = x * y
}
class MultipleOfThree extends int {
MultipleOfThree() { this = getProduct(_, _) }
}
from MultipleOfThree m
select m
这样我们查询结果就有2个,一个是内置的
#select
,一个是getProduct
,#select
的结果如下:类(Classes)#
我们可以在CodeQL中定义自己的类型,一个方法是定义一个类。
类提供了一种简单的方法来重用和构造代码。例如,我们可以:
- 在类中定义成员谓词
- 定义子类以重写成员谓词
类的定义#
定义类的格式如下:
class ClassName [extends Parent] {
// ...
}
注意这里有个坑点是类名首字母必须是大写。
一个简单的例子如下:
class OneTwoThree extends int {
OneTwoThree() { // characteristic predicate
this = 1 or this = 2 or this = 3
}
string getAString() { // member predicate
result = "One, two or three: " + this.toString()
}
predicate isEven() { // member predicate
this = 2
}
}
在CodeQL中,类允许多重继承,但是以下操作是非法的:
- 不能继承本身
- 不能继承final类
- 不能继承不兼容的类型,请参阅类型兼容性
类的主体#
类的主体可以包含以下内容
- 一个特征谓词
- 任意数量的成员谓词
- 任意数量的字段(field)
在类中,我们可以使用this来指代类本身。当我们定义类时,该类还会从其父类继承所有非私有成员谓词和字段,我们可以覆盖(override)这些谓词和字段。
特征谓词#
类似于其他语言中类的构造函数,只能定义一个,我们可以在特征谓词中使用this来限制类中可能的值。在上述例子中,OneTwoThree
被限制为1-3中的整数。
成员谓词#
这些谓词仅适用于类中。我们可以这样去调用上述类的成员谓词:
(OneTwoThree).getAString()
// 结果是 One, two or three: 1
字段(Field)#
字段是在类的主题中声明的变量,一个类的主题中可以有任意数量的字段声明。我们可以在类中的谓词适用这些变量,用法和this类似,字段必须受限于特征谓词。
一个简单的例子如下,它输出10以内每个数字的除数:
class SmallInt extends int {
SmallInt() { this = [1 .. 10] }
}
class DivisibleInt extends SmallInt {
SmallInt divisor; // declaration of the field `divisor`
DivisibleInt() { this % divisor = 0 }
SmallInt getADivisor() { result = divisor }
}
from DivisibleInt i
select i, i.getADivisor()
具体类#
上面的例子都是具体类,具体类是通过限制较大类型中的值来定义的。
抽象类#
抽象类使用关键字abstract
放在关键字class
前来定义。抽象的概念相信在许多其他语言中我们都有接触到(例如java)。抽象类我们又可以叫做元类,它定义其子类的谓词和字段。
一个简单的例子如下:
import go
abstract class SqlExpr extends Expr {
int ID;
}
class PostgresSqlExpr extends SqlExpr {
PostgresSqlExpr(){ ID=1 }
}
class MySqlExpr extends SqlExpr {
MySqlExpr(){ ID=2 }
}
重写成员谓词#
我们使用关键字override
来重写一个成员谓词,子类重写的成员谓词会影响父类的成员谓词。
例如,我们可以扩展上面的OneTwoThree
类:
class OneTwo extends OneTwoThree {
OneTwo() {
this = 1 or this = 2
}
override string getAString() {
result = "One or two: " + this.toString()
}
}
那么假如我们编写以下查询:
from OneTwoThree o
select o, o.getAString()
得到的结果如下:
在CodeQL中,与其他语言不同,相同父类的子类不会互相冲突。例如,我们可以定义一个TwoThree,它同样继承于
OneTwoThree
,它的特征谓词的一部分与OneTwo
重叠。class TwoThree extends OneTwoThree {
TwoThree() {
this = 2 or this = 3
}
override string getAString() {
result = "Two or three: " + this.toString()
}
}
现在整数2既是
TwoThree
也是OneTwo
的特征,这两个类都覆盖了原始父类的成员谓词getAString
,那么我们重新执行上述查询,在遇到2时会得到2个结果:多重继承#
在CodeQL中一个类可以继承多个类型,例如:
class Two extends OneTwo, TwoThree {}
这个类
Two
同时继承了OneTwo
和TwoThree
,它还间接继承自OneTwoThree
和int
,那么通过特征谓词我们知道其值只能是2。如果一个子类继承了同一个谓词的多个定义,那么该类还需要手动重写该谓词以避免歧义,在这种情况下我们可以考虑使用
super
表达式,例如:class Two extends OneTwo, TwoThree {
override string getAString() {
result = TwoThree.super.getAString()
}
}
### 非继承子类(Non-extending subtypes)
除了使用关键字extends
之外,我们还可以使用instanceof
来"继承"父类,将一个类声明为instanceof Foo
大致等价于在特征谓词中声明了this instanceof Foo
,主要的区别是其可以通过super来调用父类的方法。
需要注意的是,在使用关键字instanceof
"继承"父类时,使用override
关键字重写成员谓词不会对父类造成影响。
## 模块(Modules)
模块提供了一种通过将相关类型、谓词和其他模块组合在一起来组织 QL 代码的方法。
我们可以将模块导入到其他文件中,这样可以避免重复,并有助于代码管理。
### 模块主体
模块中允许包含以下的结构:
- 导入模块语句
- 谓词
- 类型
- 别名
- 显式模块
- select子句(仅可在查询模块中使用)
### 定义模块
直接使用关键字module
来显式定义一个模块,一个简单的例子(example.qll)如下:
``ql
module Example {
class OneTwoThree extends int {
OneTwoThree() {
this = 1 or this = 2 or this = 3
}
}
}
from ### 导入模块
我们可以使用关键字`import`导入模块,这会将命名空间中的所有名称(除私有名称外)都导入当前模块的命名空间里,导入模块语句的形式如下:
```ql
import <module_expression1> as <name>
import <module_expression2>
模块种类#
文件模块#
每个查询文件(.ql)或库文件(.qll)都隐式地定义了一个模块,其模块名与文件同名,但文件中的任何空格都被转换为下划线
库模块#
库模块由.qll文件定义,它可以包含除了select字句之外的模块主体。
查询模块#
库模块存在于库文件(.qll)中,它可以包含除了select字句之外的模块主体。
查询模块由.ql文件定义。它可以包含以下模块主体中列出的任何元素。
查询模块与其他模块稍有不同:
- 无法导入其他查询模块
- 一个查询模块必须包含一个查询,它可以是select语句或者一个查询谓词
显式模块#
我们还可以在一个模块中显式地定义另外一个模块。例如,我们可以在上述的例子(example.qll)中再定义一个模块,例子如下:
...
module M {
class OneTwo extends OneTwoThree {
OneTwo() {
this = 1 or this = 2
}
}
}