本文分享自華為雲社群《尋找適合編寫靜態分析規則的語言》,作者:Uncle_Tom。
1. 程式靜態分析的作用
程式的靜態分析是一種在不執行程式的情況下,透過分析程式程式碼來發現潛在的錯誤、安全漏洞、效能問題以及不符合編碼規範的情況的技術。
程式的靜態分析在現代軟體安全中扮演著至關重要的角色。以下是靜態分析在軟體安全中的一些關鍵作用:
程式碼質量保證:
靜態分析有助於確保程式碼符合安全編碼標準和最佳實踐,從而提高程式碼的質量和安全性。
參考:程式碼的安全檢視
合規性檢查:
許多行業標準和法規要求對軟體進行安全合規性檢查。靜態分析工具可以幫助組織確保其軟體產品符合這些標準和法規要求。
參考:一圖看懂軟體缺陷檢查涉及的內容
漏洞檢測:
靜態分析工具可以在程式碼編寫階段檢測潛在的安全漏洞,如SQL隱碼攻擊、跨站指令碼攻擊(XSS)、緩衝區溢位等。
參考:2023年最具威脅的25種安全漏洞(CWE TOP 25)
減少開發成本:
透過在開發早期階段發現問題,靜態分析可以減少後期修復的成本和時間,因為後期修復通常成本更高。
參考:構建DevSecOps中的程式碼三層防護體系
2. 靜態分析工具的業務痛點
隨著現在工程專案的程式碼量越來越大,同時開發框架的快速迭代和出現。靜態分析工具所需要覆蓋的場景也隨之快速的增加,但靜態分析工具所提供的是通用的檢查能力,以及靜態分析工具有限的迭代速度,無法滿足客戶不斷出現的各種差異化需求。
目前靜態分析工具的主要痛點:
2.1. 無法開發自定義規則
大多數靜態分析工具由於設計之初多是為了解決特定的編碼問題,所以沒有考慮到後期的擴充套件和由使用者完成規則的開發。如果需要提供自定義開發能力,需要從架構上重新設計,或者因為檢查效率的問題,無法提供通用的檢查配置和自定義能力。這將導致無法快速提供客戶特定的需求的問題檢查。使用者只能透過需求反饋的方式,等待工具下個版本的釋出,需要的閉環週期很長。
2.2. 對誤報和漏報的規則無法快速修改
靜態分析工具由於是對程式碼的靜態分析,輸入存在不確定性,這些不確定性導致工具在分析策略在上近似(Over-approximation)、下近似(Under-approximation)以及檢查效率三者之間尋求某種平衡,這三個因素互相影響、互相制約。
- 上近似是指分析工具可能將一些實際上不會發生的程式行為錯誤地識別為可能發生的。換句話說,它可能導致分析結果過於寬泛,將一些安全的狀態或行為錯誤地標記為不安全的。這也就導致了誤報(false positives),即錯誤地將安全的程式碼標記為有問題。
- 下近似是指分析工具可能未能識別出實際上會發生的程式行為。這意味著分析結果可能過於保守,遺漏了一些潛在的問題。這就導致了漏報(false negatives),即未能發現實際存在的安全問題或錯誤。
- 效率是所有使用者一直追求的因素,快了還想快。但哪裡有又想馬兒跑得快,又想馬兒不吃草的好事情。
由於這些原因,靜態分析工具通常提供的是一種通用的檢查規則,往往不能覆蓋特定的場景,或覆蓋場景不適合特定使用者的使用條件,這也造成檢查工具無法避免誤報和漏報。比如說:從檔案讀對有的使用者是危險,但對有的使用者是安全的,工具無法識別使用者讀檔案的實際場景,只能將所有從檔案讀設定為危險的。如果使用者無法快速對工具規則進行修改,就會被檢查結果中的誤報或漏報造成困擾。
2.3. 開發自定義規則有一定的難度
分析引擎提供的自定義開發包,但也需要自定義規則的開發人員掌握靜態分析的相關技術,使用者上手的難度較大。且由於引擎對API的封裝能力,對外提供的檢查能力有限,在很大程度上限制了使用者自定義規則的實現能力。
基於這些痛點,需要尋找一種適合編寫靜態分析規則的語言,來降低自定義規則的難度,使使用者能夠直接開發滿足自己需求的規則,使用者可以自己在很大程度上來控制和解決誤報和漏報。
那麼什麼才是適合使用者的編寫靜態分析規則的語言呢?
3. 尋找適合編寫靜態分析規則的語言
為了尋找適合使用者的編寫靜態分析規則的語言,我們來看下我們常見的兩種程式設計正規化:宣告式語言(Declarative Language)和命令式語言(Imperative Language)。這兩者語言在如何描述程式行為和解決問題的方法上存在根本差異, 但同時各有優勢和適用場景, 許多現代程式語言支援這兩種正規化, 允許程式設計師根據具體問題選擇最合適的方法。
問題:
從一個人群中挑出成年人;
具體條件:
選出的人年齡大於等於 18 歲。
命令式語言 – Java 語言
public List<Person> selectAdults(List<Person> persons){ List<Person> result = new ArrayList<>(); for (Person person : persons) { if (person.getAge() >= 18) { result.add(person); } } return result; }
SELECT * FROM Persons WHERE Age >= 18;
從這個例子可以看出來,宣告式語言更適合使用者的使用,這也是為什麼 SQL 語言在短時間內能夠迅速的被推廣和使用。
宣告式語言的特點,也正是我們正在尋找的適合編寫靜態分析規則的語言。使用者只需要關注:“做什麼”(What to do),即描述期望的結果或目標狀態,而不指定如何達到這個結果的具體步驟或過程。
我們也可以把這個檢查語言稱為一種領域特定語言(Domain Specific Language,DSL),為特定領域或問題域定製的語言,專注於解決特定型別的問題。這個語言只專注於 – 編寫程式靜態分析的規則。
這裡沒有直接使用自然語言,主要是自然語言存在表述上的差異和描述的準確性的問題。當然隨著大模型的越來越成熟,直接透過自然語言完成規則的編寫,也離我們越來越近了。但不管怎樣,在識別到檢查條件後,還是需要有一個引擎將這些約束條件轉換成具體查詢的程式語言,完成問題程式碼的搜尋,這就像 SQL 語言負責描述條件,還需要一個 SQL 的查詢引擎,完成 SQL 語言的解析和實施查詢。
4. DSL 在程式靜態分析中的應用舉例
4.1. 編寫檢查規則
檢查問題:
- 生產環境中不應該有除錯程式碼。
問題檢查條件:
- 查詢所有函式宣告
- 並且(And):函式名以"debug"開頭
- 並且(And):函式只有一個引數
- 並且(And):引數型別為"java.util.List"
package com.dsl; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.List; /** * 檢查問題:生產環境中不應該有除錯程式碼。 * 問題檢查條件: * - 查詢所有函式宣告; * - 並且(And):函式名以"debug"開頭; * - 並且(And):函式只有一個引數; * - 並且(And):引數型別為"java.util.List"。 */ public class CheckDebug { private static final Logger LOG = LogManager.getLogger(CheckDebug.class); // 應檢查出的問題函式 public void debugFunction(List<String> msgs) { for (String msg : msgs) { LOG.error("print debug info: {}", msg); } } }
編寫檢查規則
DSL 寫的檢查規則
/** * 檢查問題:生產環境中不應該有除錯程式碼。 * 問題檢查條件: * - 查詢所有函式宣告; * - 並且(And):函式名以"debug"開頭; * - 並且(And):函式只有一個引數; * - 並且(And):引數型別為"java.util.List"。 */ functionDeclaration fd where and( fd.name startWith "debug", fd.parameters.size() == 1, fd.parameters[0].type.name == "java.util.List" );
4.1.1. 規則的解讀
程式是由空格分隔的字串組成的序列。在程式分析中,這一個個的字串被稱為"token",是原始碼中的最小語法單位,是構成程式語言語法的基本元素。
Token可以分為多種型別,常見的有關鍵字(如if、while)、識別符號(變數名、函式名)、字面量(如數字、字串)、運算子(如+、-、*、/)、分隔符(如逗號,、分號;)等。
程式在編譯過程中,詞法分析器(Lexer)讀取原始碼並將其分解成一系列的token。語法分析器(Parser)會使用這些 token 來構建一個抽象語法樹(Abstract Syntax Tree, AST),這個樹結構表示了程式碼的語法結構。這個時候每個 token 也可以稱為抽象語法樹的節點,樹上某個節點的分支就是這個節點的子節點。每個節點都會有節點型別、屬性、值。
下面來描述下規則中使用的 DSL 和需求之間的對應關係。
節點型別、屬性、值
在規則中,需要查詢的是函式宣告。這裡使用:functionDeclaration 為程式碼的函式宣告節點。在這個節點下有許多的屬性,可以透過“.”的方式獲取這些屬性。
例如函式節點有:函式名(name)、函式的引數(parameters)等子節點。同時每個屬性有自己的型別,以及值。例如:函式名(name)為字串型別,函式的引數(parameters)一個集合型別;
別名
在編寫規則時,定義別名可以顯著簡化規則編寫。在遇到複合條件查詢時,建議定義別名,方便後面的使用。
例如:函式宣告(functionDeclaration)的別名 “fd”,這樣後面可以使用 “fd” 方便了後面對這個函式宣告的使用;
集合
函式的引數(parameters)就是一個集合,裡面可能會存在 0-n 個引數。對於集合類的節點,可以透過指定集合的索引值得到集合下的子節點。
例如:引數的第一個引數,可以表示為:Parameters[0]。 同樣透過 “.” 得到這個引數的其他型別或屬性;
內建函式
規則中使用了內建字串函式。
例如:判斷字串以指定字串開始的函式,startWith(“debug”),表示判斷字串以 “debug” 開始的字串;
運算子和條件表示式
規則中的 “==” 是運算子,表示等於。透過運算子將程式碼的節點型別、屬性和具體的值聯絡在了一起,構成了條件表示式。由此構成了規則需要的條件判斷,適配我們期望的約束條件。
條件的組合
通常一個規則需要多個約束或限制條件構成。這裡透過並且(and)完成了三個子條件組成的複合邏輯條件表示式。
結論
- 透過這個案例我們可以看到,這個 DSL 語言非常接近我們的期望的需求表示式;
- 使用者可以透過 DSL 語言快速開發滿足需要檢查的問題;
- 使用者可以更多的關注如何描述需要檢查的問題,而不需要關注工具是如何的實現。
4.2. 替換已有工具規則,並增加檢查條件
4.2.1. 實現原有檢查規則
原有檢測問題:
- 繼承
java.util.TimerTask
類重寫 run 方法,run 方法的實現要有 try-catch 保護。
檢查的條件:
- 查詢類繼承自
java.util.TimerTask
; - 並且(and):重寫了 run 方法;
- 並且(and):run 方法中沒有 try-catch。
package com.dsl; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.TimerTask; /** * 檢查問題:繼承 java.util.TimerTask 類重寫 run 方法,run 方法的實現要有 try-catch 保護。 * 問題檢查條件: * - 查詢類繼承自 java.util.TimerTask; * - 並且(and):重寫了 run 方法; * - 並且(and):run 方法中沒有 try-catch。 */ public class CheckTimerTask extends TimerTask { private static final Logger LOG = LogManager.getLogger(CheckTimerTask.class); // 應檢查出的問題函式 @Override public void run() { LOG.info("do some thing"); } }
編寫檢查規則
DSL 寫的檢查規則
/** * 檢查問題:繼承 java.util.TimerTask 類重寫 run 方法,run 方法的實現要有 try-catch 保護。 * 問題檢查條件: * - 查詢類繼承自 java.util.TimerTask; * - 並且(and):重寫了 run 方法; * - 並且(and):run 方法中沒有 try-catch。 */ functionDeclaration fd where and( fd.enclosingClass.superTypes contain parType where parType.name == "java.util.TimerTask", fd.name == "run", fd notContain exceptionBlock );
4.2.2. 增加檢查條件
基於上面一個例子,使用者在使用時發現除了需要有異常捕捉之外,還需要記錄錯誤或警告資訊。由此需要對原來的規則進行修改,增加新的約束條件。
檢查問題:
- 繼承 java.util.TimerTask 類重寫 run 方法,run 方法的實現要有 try-catch 保護。
- 在異常處理塊中,需要有資訊處理的函式: error 或 warn。
問題檢查條件:
- 查詢類繼承自 java.util.TimerTask;
- 並且(and):重寫了 run 方法;
- 並且(and):run 方法中沒有 try-catch。
- 或者(or): run 方法中有異常處理塊;
- 並且(and): 異常處理塊中有函式呼叫;
- 並且(and):函式名為:error 或 warn。
package com.dsl; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.TimerTask; /** * 檢查問題: * - 繼承 java.util.TimerTask 類重寫 run 方法,run 方法的實現要有 try-catch 保護。 * - 在異常處理塊中,需要有資訊處理的函式: error 或 warn。 * 問題檢查條件: * - 查詢類繼承自 java.util.TimerTask; * - 並且(and):重寫了 run 方法; * - 並且(and):run 方法中沒有 try-catch。 * - 或者(or): run 方法中有異常處理塊; * - 並且(and): 異常處理塊中有函式呼叫; * - 並且(and):函式名為:error 或 warn。 */ public class CheckTimerTaskEnhance extends TimerTask { private static final Logger LOG = LogManager.getLogger(CheckTimerTaskEnhance.class); // 應檢查出的問題函式 @Override public void run() { try { LOG.info("do some thing"); } catch (Exception e) { LOG.info("do some thing"); } } }
編寫檢查規則
DSL 寫的檢查規則
/** * 檢查問題: * - 繼承 java.util.TimerTask 類重寫 run 方法,run 方法的實現要有 try-catch 保護。 * - 在異常處理塊中,需要有資訊處理的函式: error 或 warn。 * 問題檢查條件: * - 查詢類繼承自 java.util.TimerTask; * - 並且(and):重寫了 run 方法; * - 並且(and):run 方法中沒有 try-catch。 * - 或者(or): run 方法中有異常處理塊; * - 並且(and): 異常處理塊中有函式呼叫; * - 並且(and):函式名為:error 或 warn。 */ functionDeclaration fd where and( fd.enclosingClass.superTypes contain parType where parType.name == "java.util.TimerTask", fd.name == "run", or( fd notContain exceptionBlock, fd contain exceptionBlock eb where eb contain functionCall fc where fc.name notMatch "error|warn" ) );
- 透過這個案例,我們可以看到DSL 可以快速的替換現有工具已有的檢查;
- 並可以根據需求增加更多的檢查條件,以增加對特殊場景的覆蓋,從而減低規則的漏報率,同時也可以透過這個方式,降低工具的誤報率,提升規則的檢查的準確率。
5. 結論
透過上面兩個案例,我們可以看到這個編寫自定義規則的 DSL 語言,能夠:- 實現規則的編寫;
- 實現規則的改進,降低誤報率和漏報率;
- 降低了檢查規則的開發難度。
- 在 vscode 外掛中查詢:codenavi 新增外掛即可。
點選關注,第一時間瞭解華為雲新鮮技術~