序言
倚杖柴门外,临门听暮蝉。
学习CodeQL查询语法。
基本查询结构
1 | /** |
用户可以在查询过程中自定义谓词和类来自主分析。
一些专有名词:
statement 语句
predicate 谓词
clause 子句
import语句
定于需要导入到查询中的库和模块,以及源码对应的语言模块:
cpp,csharp,go,java,javascript,python
CodeQL为开发者提供了一些库,包含常用的谓词、类型和适用于各种分析的模块,比如控制流、数据流、污点追踪。
三个clause字句
from
1 | from <type> <variable name>,... |
声明在查询中需要使用的变量
where
对from中声明对变量加以限制的逻辑条件。
select
寻找那些from声明的、满足where条件的所有查询结果。
举个例子,到所有勾股值:
1 | import java |
告警模板,包含两列:
1 | select element, string |
- element:查询结果,查询标识的代码元素,定义危险点的位置。
- string:为element显附加的报警消息,描述警报的原因。
自定义类:
1 | class SmallInt extends int { |
QL语言参考
About
QL特点:
- Declarative 命令式 :描述了结果必须满足的属性,而不是提供计算结果的过程。
- Object-oriented 面向对象
QL中的class实际上并不是像Java语言一样需要占用一段内存空间,QL中的类是限制已有数据的逻辑属性
谓词 Predicate
关键字predicate,QL中的“函数”,本质上帮助QL程序限制逻辑特性。
1 | predicate isCountry(string country) { |
规范:
谓词可以在任意地方定义,可以有返回值也可以没有,返回值默认变量为
result
。谓词名称为小写字母开头,驼峰命名就是一个不错的选择。
参数列表逗号分割,每个参数类型+形参名称。
谓词体,逻辑公式。
抽象谓词和外部谓词是可以没有谓词体的。
声明;
即可
没有返回值的谓词
1 | predicate isSmall(int i){ |
如果i是int,并且在[1~9]之间,那么这个谓词成立。
有返回值的谓词
1 | int isSmall(int i){ |
如果i是[1~9]的整数,返回2~10。
对于这个下面这个例子,穷举3个case来返回不同的谓词结果。
1 | predicate hasCapital(string country, string capital) { |
递归谓词
QL 中的谓词可以是递归的。这意味着它直接或间接地取决于自身。
1 | string getANeighbor(string country) { |
这种case下,返回“Belgium”的全部邻居:“France” & “Germany“
谓词的种类
三种
- 非成员谓词Non-member predicate : class外定义的谓词,不属于任何class
- 成员谓词:class内定义的常规谓词
- 特征谓词:简单粗暴,构造函数级别的谓词,直接规定
1 | int getSuccessor(int i) { // 非成员谓词 non-member predicate |
行为绑定
绑定的意思是谓词的参数/结果一定是finite的,一定是有限的。
两个反例:
1 | int multiplyBy4(int i) { |
绑定集合 bindingset[]
如何使用非绑定谓词呢?加个bindingset限制就好了。(很像注解)
1 | bindingset[i] |
1 | bindingset[x] bindingset[y] |
区别:
bindingset[x] bindingset[y]
表示x, y至少有一个是有界的bindingset[x,y]
表示x,y 必须都是有界的
数据库谓词
如果一个数据库内部包含Person表,我们可以用:
1 | persons(x, firstName, _, age) //第三个_表示Don‘t care expressioon 表示任意值都可以,不做限制 |
唯一注意的是不可以自定义数据库谓词,一切要按照数据库提供的谓词去使用
查询 Query
查询是 QL 程序的输出。
select子句
正常模板:
1 | from /* ... variable declarations ... */ |
from/select是可选的
select子句中除了表达式之外,还可以加:
as
as后面加label,相当于给查询结果某一列加了label,后续可以继续使用该labelorder by
排序 ;order by x asc
升序;order by y desc
降序;
举例子:
1 | from int x, int y |
按照y的降序排列:
1 | from int x, int y |
查询谓词
查询谓词是带有query
注释的非成员谓词。
举例:
1 | query int getProduct(int x, int y){ |
直接执行就行,不用写select,或者硬要写的话,用select getProduct(_,_)
优点:
我们可以在其他的谓词内部使用这种查询谓词:
1 | class MultipleOfThree extends int { |
类型 Type
QL是一门静态类型语言,所以每一个变量都是拥有自己的声明类型的。
一个type是一组value;比如说,int是整数集合,代表所有的整数;
需要注意的是,一种value可以属于多个集合,也就是属于多种type。
Type有五种:数据库类型、class类型、字符类型、类域(class domain)类型
原始类型 primitive types
原始类型属于QL内置,应用于全局namespace的一种类型。
- boolean:true or false
- float:64位浮点数
- int:32位整数
- string:字符串
- date:日期
QL为这些原始类型提供了很多build-in方法。
可以通过在适当类型的表达式上使用 dispatch 来获得恰当的方法。
类Classes
用户可以制定自己的type,可直接定义一个类。
类存在的目的:
- 将一些相关的value聚合到一起,使他们成为成员
- 为这些成员创建成员谓词
- 定义子类覆盖成员谓词,继承所有父类谓词
QL 中的类不会“创建”一个新对象,它只是表示一个逻辑属性。如果某值满足特定的逻辑属性,则该值在这个特定类中。
1 | class OneTwoThree extends int { |
注意:
- QL中自定义的类一定要至少extends一个父类,被extends的父类被称为base types
- QL中的类允许多继承
- 不得extends final类
类内成分
- 特征谓词 (也就是“构造函数”
- field声明
- 成员谓词
- 继承自父类的所有非私有成员谓词和字段,可以在本类中override
特征谓词Characteristic predicate
使用this
关键字来对类中可能的逻辑属性进行限制
成员谓词Member predicate
可以通过“对象”调用,也可以使用(类名).func()
强制进行cast调用
字段Field
成员属性
1 | class SmallInt extends int { |
SmallInt类表示的是[1..10]之间的整数,DivisibleInt继承了SmallInt,结果就是每个数字的因数分解
具体类Concrete classes
上面提到全部是具体类,拥有实际成分。
抽象类Abstract classes
使用abstract
修饰的类
例子:
假设你对所有可以解释为 SQL 查询的表达式感兴趣,可以自己加一层抽象类。
1 | abstract class SqlExpr extends Expr { |
接下来就可以为每一种数据库类型定义各种之类class PostgresSqlExpr extends SqlExpr
Override 成员谓词
在谓词前面,添加override注释就行
1 | class OneTwo extends OneTwoThree { |
1,2会按照OneTwo来走,3会按照老方法来走,因为逻辑限制不一样。
多继承
一个类可以继承多种type。在这种情况下,它继承自所有这些类型。
比如说:
1 | class Two extends OneTwo, TwoThree {} |
Two
必须满足逻辑属性OneTwo
, 和TwoThree
。这里的类Two
包含一个值,2。
它还(间接)继承自OneTwoThree
和int
。
非继承子类型Non-extending subtypes
一个类可以声明与其他类型的instanceof
关系
class A instanceof B
并不是代表B是A的父类
官方说法是:
class A extends B:B称为base type
class C instance of D:D称为supertypes
子类并不会继承supertype的任何成员谓词
下面这个例子中,Bar类可以使用super的关键字调用Foo类中的foo_method()方法
1 | class Foo extends int { |
在这个例子中,Foo的特征谓词也适用于Bar。然而,foo_method在Bar中没有暴露,所以查询select any(Bar b).foo_method()的结果是编译时出错。从这个例子中可以看出,仍然可以用super关键字访问instanceof supertypes的方法。
intanceof的类其实并不属于子类的一部分,不参与子类,不可以override。
下面这个例子中,方法Bar::foo不是重写Foo::foo。相反,它只是重写Interface::foo。这意味着select any(Foo f).foo()只返回foo。
如果Bar被定义为extends Foo,那么select any(Foo b)将产生bar。
1 | class Interface extends int { |
模块 Module
模块提供了一种通过将相关类型、谓词和其他模块组合在一起来组织 QL 代码的方法。
可以将模块导入到其他文件中,这样可以避免重复,并有助于将您的代码组织成更易于管理的部分。
定义一个模块
here is an example of the simplest way,显示模块
1 | module Example { |
只可以对显示模块加注解,不能注解文件模块
模块种类
文件模块
每个查询文件.ql
和库文件.qll
都属于是文件模块,模块名称就是文件名(文件名中的空格被替换为_)。
文件的内容构成了模块的主题。
库模块
每个.qll
文件都是一个库模块,库模块是一种文件模块。
OneTwoThreeLib.qll
1 | class OneTwoThree extends int { |
查询模块
每个.ql
文件都是一个查询模块。查询模块是一种文件模块。
查询模块不可以被import。
查询模块一定要至少有一次查询,select或者query predicate都行
OneTwoQuery.ql
1 | import OneTwoThreeLib |
显示模块
可以在模块中定义一个模块。
我们可以在上面的OneTwoThreeLib.qll文件中定义一个模块:
1 | module M { |
模块体
模块中可以包含以下结构:
- import 语句
- predicates
- types 包括用户定义的class
- alias 别名
- 显示模块
- select语句 仅在查询模块中可用
导入模块
当导入一个模块时,会将其命名空间中的所有名称(私有名称除外)带入当前模块的命名空间中。
1 | import <module_expression1> as <name> |
可以起别名 用as关键字
1 | import javascript as js |
别名 Alias
QL 实体的替代名称。
变量 Variable
QL 中变量的使用方式与代数或逻辑中的变量类似。变量代表一组值,这些值通常受公式限制。
声明一个变量
type+name
Free variable & bound variable
1 | "hello".indexOf("l") // 2 and 3 |
表达式 Expression
表达式计算一组值并具有类型。
表达式的种类:
- module expression
- type expression
- predicate expression
关键字
- Boolean : true false
- Integer:整数(正,负)
- Float:浮点数(正,负)
- String:字符串
The following string formats are recognized as dates:
- ISO dates, such as
"2016-04-03 17:00:24"
. The seconds part is optional (assumed to be"00"
if it’s missing), and the entire time part can also be missing (in which case it’s assumed to be"00:00:00"
). - Short-hand ISO dates, such as
"20160403"
. - UK-style dates, such as
"03/04/2016"
. - Verbose dates, such as
"03 April 2016"
.
范围Range
[3..7] 表示 3,4,5,6,7
自定义set:
前十个素数:[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Super
和Java中的super一样,用来调用super types中的同名方法
1 | class A extends int { |
调用谓词 with result
调用有返回值的谓词本身就是表达式 a.getAChild()
调用没有返回值的谓词是公式formula
聚合
聚合是一种映射,它根据公式指定的一组输入值计算结果值。
1 | <aggregate>(<variable declarations> | <formula> | <expression>) |
fromula加以限制,expression计算结果值,aggregate算出来想要的目标
关键词:
排序关键词:min,max,rank,concat,strictconcat
排序是可以自动选择按照数值排序,还是按照字符排序(unicode value)。
特殊需求排序:order by <element> [desc/asc]
缺省是asc升序排列
比如:order by o.getName()
charAt() 返回值是去重的
一些例子:
count:确定
<expression>
不同结果的个数1
count(File f | f.getTotalNumberOfLines() > 500 | f) // 寻找文件行数大于500行的文件数
min,max:
<expression>
的最小、最大值<expression>
必须是数字类型或者string类型聚合返回所有js文件中文件行数最多的文件名
1
max(File f | f.getExtension() = "js" | f.getBaseName() order by f.getTotalNumberOfLines(), f.getNumberOfLinesOfCode())
avg:
<expression>
的平均值<expression>
结果必须的是数值。1
avg(int i | i =[0..3]|i) // 0,1,2,3的平均值
sum:
<expression>
的总和<expression>
结果必须的是数值。1
sum(int i, int j| i in [0..2] and j in [3..5]| i*j)
concat:将
<expression>
的各种结果都聚合到一起。<expression>
结果必须是string。1
2select concat(int i| i in [0..3] | i.toString() order by i desc) // 3210
select concat(int i| i in [0..3] | i.toString(),"|" order by i desc) // 3|2|1|0rank
给
<expression>
的结果排序。1
2rank[4](int i | i = [1..10] | i) // 4
select rank[4](int i | i = [1 .. 10] | i order by i desc) // 7[index]从1开始,rank[0] has no result
rank[1] == min(…)
strictconcat,strictcount,strictsum
没有满足formula的,就返回empty set,而不是默认的0或者空字符串
unique
返回
<expression>
的结果集中满足formula的唯一值。1
2
3from int x
where x in [-5 .. 5] and x != 0
select unique(int y | y = x or y = x.abs() | y)[1..5]满足的x和x.abs()的结果是唯一的
单调聚合
1 | string getPerson() { result = "Alice" or |
一种苹果,两种橙子
区别:
- 非单调谓词:Alice买了苹果和橙子,sum会把这些一起求和100+100+1=201
- 单调谓词:Alice会分别计算100+1=101,100+100=200
Charies想买苹果+香蕉,但是香蕉根本不卖
非单调谓词:直接返回苹果+0
单词谓词:因为香蕉的值没有,所以直接不算
Diane什么都不买,所以都显示0。但如果用strictsum,两种谓词都不显示
单调谓词普遍用于递归,因为每一轮结果都是有意义(fix-point)。
递归单调聚合
单调聚合函数经常用来递归,这种递归通常应用在表达式中。
举例:计算当前节点到叶子结点的距离:
1 | int depth(Node n) { |
Any
1 | any(<variable declarations> | <formula> | <expression>) |
Cast 类型强转
两种方式:
- postfix 后缀,
x.(Foo)
表示将x限制为Foo类型 - prefix 前缀,
(Foo)x
也表示将x限制为Foo类型
一般用法:x.(Foo) 等同于((Foo)x)
例子:查找父类是List的类
1 | import java |
无关表达式 Don’t-care expression
参数单个下划线表示,表示any value,不关心实际是什么
例子:
1 | from string s |
公式 Formula
Formulas define logical relations between the free variables used in expressions.
Formula限制了表达式中那些free类型的变量。
当一个formula为true,习惯说是hold住了
hold:
“Ann” < “Anne”
[1 .. 2] = [2 .. 5]
Type check
1 | <expression> instanceof <type> |
range check
1 | <expression> in <type> |
两种形式表达都可以
量化公式
显式量词
exists
1 | exists(<variable declarations> | <formula1> and <formula2>) |
forall
1 | forall(<variable declarations> | <formula 1> | <formula 2>) |
hold formula1的全部变量也能够hold formula2 那么整个formula就hold
forex
1 | forex(<variable declarations> | <formula 1> | <formula 2>) |
等同于
1 | forall(<vars> | <formula 1> | <formula 2>) and |
隐式量词
Don’t-care expression
逻辑连接词
比如:
- not
- if…then…else
- and
- or
- implies
重点说一下implies
A implies B 表示的是 (not A) or B
举例子:找到[1..10]之内 是奇数 或者 是4的倍数
1 | class SmallInt extends int{ |
注解 Annotation
注释是一个字符串,可以直接放在 QL 实体或名称的声明之前。
私有类:
1 | private module M { |
一些注解可以作用于实体,另一些作用于实体的特定名称
作用于实体:
abstract
,cached
,external
,transient
,final
,override
,pragma
,language
, andbindingset
作用于名称:
deprecated
,library
,private
, andquery
abstract
Available for: classes , member predicates 可用于:类, 成员谓词
抽象谓词 abstract predicate
have no body,一定要被子类去overirde
1 | abstract class Configuration extends string { |
自定义数据流分析,之后可以对不同的case,实现不同的子类override多种的isSource
1 | class ConfigA extends Configuration { |
cached
Avilable for:class , algebraic datatypes , 特征谓词,成员谓词,非成员谓词, 模块
不怎么用 没细看。保存select到缓存中。
deprecated
Avilable for:class , algebraic datatypes , 成员谓词,非成员谓词, 类成员,模块,别名
表示过时,不建议使用的版本。
external
Avilable for:非成员谓词
外部模板谓词,常用于数据库谓词
trainsient
Avilable for:非成员谓词
查询不会持久化存储在磁盘上
final
Avilable for:类,成员谓词,类成员
表示被修饰的不可以被重写或者继承,final类不可以被任何类继承。
1 | class Element ... { |
override
Avilable for:成员谓词,类成员
private
Avilable for:类,代数类型,成员谓词,非成员谓词,import,类成员,模块,别名
query
Avilable for:非成员谓词,别名
将一个谓词转换为查询
编译器参数
优化执行性能。
Inling 内联
对于简单的谓词,QL优化器有时会用谓词体本身取代对谓词的调用,这就是所谓的内联。
例如,假设你有一个定义谓词one(int i) { i = 1 }和一个对该谓词的调用…one(y) …. QL优化器可以将该谓词内联为…y = 1 ….
你可以使用以下编译器pragma注释来控制QL优化器内联谓词的方式。
pragma[inline]
告诉QL优化器将这个谓词内联到它被调用的地方。当一个谓词体完全计算起来非常昂贵时,这可能很有用,因为它可以确保谓词在被调用的地方与其他上下文信息一起被评估。
pragma[noinline]
用于防止一个谓词被内联到它被调用的地方。在实践中,当你已经在一个 “辅助 “谓词中把某些变量组合在一起时,这个注解是有用的,以确保关系被整体评估。这可以帮助提高性能。QL优化器的内联可能会撤销辅助谓词的工作,所以用pragma[noinline]来注释它是个好主意。
pragma[nomagic]
用于防止QL优化器在谓词上执行 “魔法集 “优化。
这种优化包括从谓词调用的上下文中获取信息并将其推入谓词的主体。这通常是有益的,所以你不应该使用pragma[nomagic]注解,除非GitHub推荐这样做。
请注意,nomagic也就意味着noinline。
pragma[noopt]
用于阻止QL优化器对谓词进行优化,除非它对编译和评估的工作绝对必要。
这很少有必要,你不应该使用pragma[noopt]注解,除非GitHub推荐这样做,例如,为了帮助解决性能问题。
pragma[only_bind_out]
可以让你指定QL编译器应该绑定表达式的方向。这对于在QL优化器以低效方式排列QL程序的部分的罕见情况下提高性能是很有用的。
例如,x = pragma[only_bind_out](y)
在语义上等同于x = y,但有不同的绑定行为。x = y从y绑定x,反之亦然,而x = pragma[only_bind_out](y)
只从y绑定x。
欲了解更多信息,请参见 “Binding“。
pragma[only_bind_into]
指定QL编译器应该绑定表达式的方向。这对于在QL优化器以低效率的方式排列QL程序的部分的罕见情况下提高性能是很有用的。
例如,x = pragma[only_bind_into](y)
在语义上等同于x = y,但有不同的绑定行为。x = y从y绑定x,反之亦然,而x = pragma[only_bind_into
](y)只从x绑定y。
欲了解更多信息,请参见 “Binding“。
bindingset[…]
https://codeql.github.com/docs/ql-language-reference/predicates/#binding-behavior
明确说明谓词或类的绑定集。绑定集是谓词或类主体的参数的一个子集,如果这些参数被限制在一个有限的值集上,那么谓词或类本身就是有限的(也就是说,它被评估为一个有限的图元集)。
bindingset 注解接受一个用逗号分隔的变量列表。
当您注释一个谓词时,每个变量必须是谓词的参数,可能包括this(对于特征谓词和成员谓词)和result(对于返回结果的谓词)。
当你注解一个类时,每个变量必须是this或该类中的一个字段。从CodeQL CLI的2.3.0版和LGTM Enterprise的1.26版开始支持类的绑定集。
递归
传递闭包 Transitive closures
原始谓词必须要有两个参数,this 和 result, 参数类型必须兼容
传递闭包 +
一次或者多次
1 | Person getAnAncestor() { |
自反传递闭包 *
0次或者多次
1 | Person getAnAncestor2() { |
名称解析 Name resolution
QL将名称解析为程序元素。
名称
1 | import javascript |
标准化引用
模块表达使用.
来 导入相对路径下的其他module
举例Example.ql
:
1 | import examples.security.MyLibrary |
QL如何寻找:
- 首先将所有的
.
替换为当前操作系统的文件分割符,所以QL首先在Example.ql
文件目录下寻找examples/security/MyLibrary.qll
文件,找到了就import。 - 如果没找到,QL会去包含
qlpack.yml
文件的目录下寻找examples/security/MyLibrary.qll
文件。 - 如果还没找到,那么就去每个库目录下进行寻找,库目录就是
qlpack.yml
定义的libraryPathDependencies
属性的值。
选择
可以使用选择来引用特定模块内的模块、类、或谓词。
1 | <module_expression>::<name> |
举例:
libs/CountriesLib.qll
1 | class Countries extends string { |
CustomModule.ql:
1 | import libs.CountriesLib // qll文件名 |
第二种:只导入M模块中的内容
1 | import libs.CountriesLib::M |
命名空间
实体:module type predicate
global namespace
包含所有内置实体的区域都叫做global namespace
QL自动引入,直接使用关键字就行,不用import任何东西
local namespace
对于之前提到的M来说,主要分为三种:
- declared:M模块中声明的任何内容
- imported:谁import了M模块,那么自动也会import M模块中的import
- exported:
举例:OneTwoThreeLib.qll
1 | import MyFavoriteNumbers |
MyFavoriteNumbers内部import的任何东西都被当前OneTwoThreeLib.qll import了
OneTwoThreeLib declare了两个成分:class OneTwoThree 和 private module P
OneTwoThreeLib export了class OneTwoThree 以及任何在MyFavoriteNumbers export内容
由于module P 是private修饰的 所以不可以被export
QL程序的演变 evolution
过程
当 QL 程序对数据库查询时,它会被编译成逻辑编程语言Datalog的变体格式。这种格式针对性能进行了优化,然后进行了评估以产生结果。
QL的查询结果都是元组tuple
QL语言规范
太多了,挑一些重点
Types
QL中的type包括:
基础类型(5种)、数据库类型、类类型、字符类型、类域(class domain)类型
Types in QL are either primitive types, database types, class types, character types or class domain types.
Within the specification the class type for C is written C.class, the character type is written C.C and the domain type is written C.extends. However the class type is still named C.
Value
5种基础类型 int float string boolean date + 1种 entity
Key Words
1 | and |
Annotation
1 | annotations ::= annotation* |