CodeQL學習筆記(1)-QL語法(邏輯連線詞、量詞、聚合詞、謂詞和類)

xzajyjs發表於2024-10-25

最近在學習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)(後面有總結):

  1. 假定A是父類,即其中的某個成員謂詞test()沒有override,B和C同時繼承A,並且都override了A的test()成員謂詞。
    1. 如果from的謂詞型別是A,則其中的test()方法會被B和C全部改寫。碰到B與C重疊的部分,不衝突,保持並存
    2. 如果from的謂詞型別是B或C,則以B/C為基礎,在滿足B/C的條件下加上與另一個重疊的部分,不衝突,保持並存
  2. 如果A是父類,B繼承A,C繼承B,則C會把B中的相同成員謂詞override掉,而不是共存
  3. 對於多重繼承,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

image-20241025105910532

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(繼承鏈)

image-20241025105937484

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: 多重繼承

image-20241025105948674

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()

相關文章