最近在學習CodeQL,對於CodeQL就不介紹了,目前網上一搜一大把。本系列是學習CodeQL的個人學習筆記,根據個人知識庫筆記修改整理而來的,分享出來共同學習。個人覺得QL的語法比較反人類,至少與目前主流的這些OOP語言相比,還是有一定難度的。與現在網上的大多數所謂CodeQL教程不同,本系列基於官方文件和情景例項,包含大量的個人理解、思考和延伸,直入主題,只切要害,幾乎沒有廢話,並且堅持用從每一個例項中學習總結歸納,再到例項中驗證。希望能給各位一點不一樣的見解和思路。當然,也正是如此必定會包含一定的錯誤,希望各位大佬能在評論區留言指正。
先來看一下一些基本的概念和結構
// 基本結構
from /* variable declarations */
where /* logical formulas */
select /* expressions */
from int a, int b
where x = 3, y = 4
select x, y
// 找出1-10內的勾股數
from int x, int y, int z
where x in [1..10], y in [1..10], z in [1..10] and
x * x + y * y = z * z
select x, y, z
// 或下面這種類寫法,封裝和方法複用
class SmallInt extends int {
SmallInt(){
this in [1..10]
}
int square(){
result = this * this
}
}
from SmallInt x, SmallInt y, SmallInt z
where x.sqrt() + y.square() = z.sqrt()
select x, y, z
邏輯連線詞、量詞、聚合詞
from Person p
where p.getAge() = max(int i | exists(Person t | t.getAge() = i ) | i) // 通用聚合語法,比較冗長
select p
// 或用以下有序聚合方式
select max(Person p | | p order by p.getAge())
exists(<變數宣告> | <條件表示式>)
<aggregates> ( <變數宣告> | <邏輯表示式(限制符合條件的資料範圍)> | <表示式(返回經過篩選的)> )
e.g.
exists( Person p | p.getName() = "test" )
,判斷是否存在一個人的名字為test
max(int i | exists(Person p | p.getAge() = i) | i)
,第二部分的意思是拿到所有人的年齡放入i,第三部分是作用範圍為i,目前i為int陣列,存放所有人的年齡,即最終計算的是max(i)
select max(Person p | | p order by p.getAge())
,考慮每個人,取出年齡最大的人。過程是按照年齡來取最大值,換句話說,order by p.getAge()
是告訴 max() 函式要基於 getAge() 來找最大值,並不涉及對所有物件的排序操作。
// 其他有序聚合練習
select min(Person p | p.getLocation() = "east" | p order by p.getHeight()) // 村東最矮的人
select count(Person p | p.getLocation() = "south" | p) // 村南的人數
select avg(Person p | | p.getHeight()) // 村民平均身高
select sum(Person p | p.getHairColor() = "brown" | p.getAge()) // 所有棕色頭髮的村民年齡總和
// 綜合練習,https://codeql.github.com/docs/writing-codeql-queries/find-the-thief/#the-real-investigation
import tutorial
from Person p
where
p.getHeight() > 150 and // 身高超過150
not p.getHairColor() = "blond" and // 頭髮顏色不是金髮
exists(string c | p.getHairColor() = c) and // 不是禿頭。這裡表示這個人存在某種髮色,但不用具象化
not p.getAge() < 30 and // 年齡滿30歲。也可以是p.getAge() >= 30
p.getLocation() = "east" and // 住在東邊
( p.getHairColor() = "black" or p.getHairColor() = "brown" ) and // 頭髮是黑色或棕色
not (p.getHeight() > 180 and p.getHeight() < 190) and // 沒有(超過180且矮於190)
exists(Person t | t.getAge() > p.getAge()) and // 不是最年長的人。這裡用存在語法是,存在一個人比他的年齡大
exists(Person t | t.getHeight() > p.getHeight()) and // 不是最高的人
p.getHeight() < avg(Person t | | t.getHeight()) and // 比平均身高要矮。所有人,沒有限制範圍
p = max(Person t | t.getLocation() = "east" | t order by t.getAge()) // 東部年紀最大的人。這一行是官方給的參考,但是官方文件中說 "Note that if there are several people with the same maximum age, the query lists all of them.",如果存在最大年齡相同的兩個人會同時列出,可能會造成不可控的後果。
// p.getAge() = max(Person t | t.getLocation() = "east" | t.getAge()) // 按照個人理解和chatgpt的解答,應該使用這種方式
select p
謂詞和類別
CodeQL中的謂詞(Predicates)的說法大概可以理解為其他高階程式語言中的函式,同樣擁有可傳參、返回值、可複用等特性
先來看一個簡單的例子
import tutorial
predicate isSouthern(Person p) {
p.getLocation() = "south"
}
from Person p
where isSouthern(p)
select p
這裡的predicate為一個邏輯條件判斷,返回true or false,有些類似於boolean,當然ql中有單獨的boolean型別,還是有一定區別的,只是理解上可以聯絡起來理解,這裡先不展開
謂詞的定義方式和函式類似,其中的predicate可以替換為返回結果型別,例如int getAge() { result = xxx }
,謂詞名稱只能以小寫字母開頭
此外,還可以定義一個新類,直接包含isSouthern的人
class Southerner extends Person {
Southerner() { isSouthern(this) }
}
from Southerner s
select s
這裡類似於面嚮物件語言(OOL)中的類定義,同樣擁有繼承、封裝、方法等;這裡的
Southerner()
類似於建構函式,但是不同於類中的建構函式,這裡是一個邏輯屬性,並不會建立物件。ool類中的方法在ql中被稱為類成員謂詞表示式
isSouthern(this)
定義了這個類的邏輯屬性,稱作特徵謂詞
,他用一個變數this
(這裡的this理解同ool)表示:如果屬性isSouthern(this)
成立,則一個Person
--this
是一個Southerner
。簡單理解就是ql中每個繼承子類的特徵謂詞表示的是什麼樣的父類是我這種子類
、我這種子類在父類的基礎上還具有什麼特徵/特性
引用官方文件:QL 中的類表示一種邏輯屬性:當某個值滿足該屬性時,它就是該類的成員。這意味著一個值可以屬於許多類 — 屬於某個特定類並不妨礙它屬於其他類。
來看下面這個例子
class Child extends Person {
Child(){
this.getAge() < 10
}
override predicate isAllowedIn(string region) {
region = this.getLocation()
}
}
// Person父類中的isAllowedIn實現如下:
predicate isAllowedIn(string region) { region = ["north", "south", "east", "west"] }
// 父類isAllowedIn(region)方法永遠返回的是true,子類返回的是當前所在區域才為true(getLocation()方法)
看一個完整的例子
import tutorial
predicate isSoutherner(Person p) {
p.getLocation() = "south"
}
class Southerner extends Person {
Southerner(){isSoutherner(this)}
}
class Child extends Person {
Child(){this.getAge() < 10}
override predicate isAllowedIn(string region) {
region = this.getLocation()
}
}
from Southerner s
where s.isAllowedIn("north")
select s, s.getAge()
這裡有個概念非常重要,要與ool的類完全區別開來,在ool的類中,繼承的子類中重構的方法是不會影響其他繼承子類的,每個子類不需要考慮是否交錯。但是在QL中,引用官方文件的一句話QL 中的類表示一種邏輯屬性:當某個值滿足該屬性時,它就是該類的成員。這意味著一個值可以屬於許多類 — 屬於某個特定類並不妨礙它屬於其他類
,在ql的每個子類中,只要滿足其特徵謂詞,就是這個子類的成員。
針對如上程式碼中這個具體的例子,如果Person中有人同時滿足Southerner和Child的特徵關係,則同時屬於這兩個類,自然也會繼承其中的成員謂詞。
個人理解,其實QL中的子類就是把父類全部拿出來,然後根據特徵謂詞來匹配父類中的某些元素,然後去複寫/重構其中的這些元素的成員謂詞,事實上是對父類中的元素進行了修改。下面用三個例項來對比理解
// 從所有Person中取出當前在South的,然後取出其中能去north的。因為把child限定了只能呆在當地,因此取出的這部分Southerner中的Child全都沒法去north,因此就把這部分(原本在South的)Child過濾了
from Southerner s
where s.isAllowedIn("north")
select s
// 取出所有Child,因此他們都只能呆在原地,因此找誰能去north就是找誰原本呆在north
from Child c
where c.isAllowedIn("north")
select c
// 取出所有Person,要找誰能去north的,即找所有成年人(預設所有人都可以前往所有地區)和找本來就呆在north的Child
from Person p
where p.isAllowedIn("north")
select p
延伸一下,如果多個子類別同時重構override了同一個成員謂詞,那麼遵循如下規則(先假定有三個類A、B、C)(後面有總結):
- 假定A是父類,即其中的某個成員謂詞
test()
沒有override,B和C同時繼承A,並且都override了A的test()
成員謂詞。
- 如果from的謂詞型別是A,則其中的
test()
方法會被B和C全部改寫。碰到B與C重疊的部分,不衝突,保持並存- 如果from的謂詞型別是B或C,則以B/C為基礎,在滿足B/C的條件下加上與另一個重疊的部分,不衝突,保持並存
- 如果A是父類,B繼承A,C繼承B,則C會把B中的相同成員謂詞override掉,而不是共存
- 對於多重繼承,C同時繼承A和B,如果A和B的成員謂詞有重合,則C必須override這個謂詞
例如:
class OneTwoThree extends int { OneTwoThree() { // 特徵謂詞 this = 1 or this = 2 or this = 3 } string getAString() { // 成員謂詞 result = "One, two or three: " + this.toString() } } 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() /* result: o getAString() result 1 One or two: 1 2 One or two: 2 3 One, two or three: 3 // 理解:onetwothree類定義了1 2 3,onetwo重構了onetwothree中1和2的成員謂詞。因此onetwothree o中有3個,其中的1和2使用onetwo的成員謂詞,3使用onetwothree的成員謂詞 */
情況1: 在這個基礎上加上另一個類別(重要),A->B, A->C
class TwoThree extends OneTwoThree{ TwoThree() { this = 2 or this = 3 } override string getAString() { result = "Two or three: " + this.toString() } } /* command: from OneTwoThree o select o, o.getAString() result: o getAString() result 1 One or two: 1 2 One or two: 2 2 Two or three: 2 3 Two or three: 3 // 理解:twothree和onetwo重合了two,但是不像其他ool,ql並不會衝突,而是並存。 --- command: from OneTwo o select o, o.getAString() result: 1 One or two: 1 2 One or two: 2 2 Two or three: 2 // 理解:twothree和onetwo都重構了其中的2,由於ql不會衝突,所以並存。由於o的型別是onetwo,因此"地基"是1和2,然後再加上twothree重構的2 --- command: from TwoThree o select o, o.getAString() result: 2 One or two: 2 2 Two or three: 2 3 Two or three: 3 // 理解: twothree和onetwo都重構了2,由於ql不會衝突,會並存。由於o的型別是twothree,所以“地基”是2和3,然後再加上onetwo重構的2 */
情況2: A->B->C(繼承鏈)
class Two extends TwoThree { Two() { this = 2 } override string getAString() { result = "Two: " + this.toString() } } from TwoThree o select o, o.getAString() /* result: o getAString() result 1 One or two: 2 2 Two: 2 3 Two or three: 3 // 理解:在上面的例子的基礎上,Two重構了twothree中的成員謂詞,因此與twothree不是共存關係 */ from OneTwo o select o, o.getAString() /* result: o getAString() result 1 One or two: 1 2 One or two: 2 3 Two: 2 // 理解:在之前例子的基礎上,OneTwo和TwoThree共存,但是Two把TwoThree中的一部分給override了(即Two和TwoThree並不是共存關係) */
階段總結:根據上面這麼多例子的學習,總結歸納起來其實很簡單,核心要義就是搞清楚“繼承鏈關係”。如果兩個類別是繼承的同一個父類,那麼他們兩個的結果共存;如果兩個類是從屬關係(父與子),那麼子類覆蓋父類對應的部分。
例如上面的例子中,OneTwo和TwoThree是並存關係,同時繼承OneTwoThree,所以他們的結果共存,不衝突;TwoThree和Two是從屬關係,所以根據最子類優先原則,覆蓋對應TwoThree中的內容(Two也間接繼承OneTwoThree,所以對所有父類包括OneTwoThree造成影響)。
情況3: 多重繼承
class Two extends OneTwo, TwoThree { Two() { this = 2 } override string getAString() { result = "Two: " + this.toString() } } // 解釋1:Two同時繼承TwoThree和OneTwo,如果不寫條件謂詞,則預設為同時滿足兩個父類條件,如果寫,則範圍也要小於等於這個交集範圍。 // 解釋2:如果多重繼承的父類中同一名稱的成員謂詞有多重定義,則必須覆蓋這些定義避免歧義。在這裡的Two的getAString()是不能省略的 from OneTwoThree o select o, o.getAString() /* result: o getAString() result 1 One or two: 1 2 Two: 2 3 Two or three: 3 // 理解:由於two與onetwo和twothree是父子關係,因此直接把共有的two全部覆蓋,不是並存關係 */
在這個基礎上再去建立一個謂詞,用於判斷是否是禿頭isBald
predicate isBald(Person p) {
not exists(string c | p.getHairColor() = c) // 不加not表示某人有頭髮
}
// 獲得最終結果,允許進入北方的南方禿頭
from Southerner s
where s.isAllowedIn("north") and isBald(s)
select s, s.getAge()