Repository-Level Graph Representation Learning for Enhanced Security Patch Detection

Paper: https://arxiv.org/pdf/2412.08068

Code: https://github.com/Xin-Cheng-Wen/RepoSPD

Overview

本文介绍了一种针对补丁的库级别的代码属性图(RepoCPG),同时使用渐进学习训练模型,使其同时学习到图和序列表示的知识,以实现更好的安全补丁检测。

Approach

RepoCPG构建

(1)生成文件级CPG:为pre-patch和post-patch补丁相应文件分别生成CPG图

(2)合并文件级CPG,生成MergeCPG:通过比较nodeInfo,获取common nodes、pre-patch nodes、post-patch nodes,并重新设置nodeID。

  • common nodes:pre-CPG和post-CPG中都存在的节点,合并为同一节点。
  • pre-patch nodes:仅在pre-CPG中存在的节点
  • post-patch nodes:仅在post-CPG中存在的节点
  • changes nodes:pre-patch nodes和post-patch nodes的并集

(3)合并库级别依赖,生成RepoCPG:a. 提取库级别函数调用图,b. 对MergeCPG中每个节点,检索是否存在函数调用依赖。如果存在,则将函数内依赖添加与节点相同的的标签 (pre-/post-/common),并添加到RepoCPG中。

(4)针对代码更改切片,生成slicing RepoCPG:以changes nodes作为criterion进行双向pdg (CDG+DDG)切片。

在实际代码实现中,作者将步骤 (2) (4) 以及 (3) (4) 分别进行合并。即首先合并文件及CPG,生成MergeCPG并切片;其次获取MergeCPG中节点的函数依赖关系,并对依赖函数进行切片,生成slicing RepoCPG。

example

patch

对于上图中的补丁,通过首先获取changes node,并以changes node为criterion进行双向pdg切片,可以获得如Fig. 3 所示的MergeCPG。其中,节点a, g, f, e 为补丁前后都存在的common nodes,节点 c, d, k, j 是仅在补丁前存在的pre-patch nodes,节点 b, h, i 是仅在补丁后存在的post-patch nodes。

此后,通过对MergeCPG节点的函数依赖关系进行分析,可以看到节点对于get_futex_key_refs以及hash_futex两个函数存在依赖关系。因此,获取这两个函数的CPG并进行切片,得到节点i, m, j, k,并为这些节点添加与依赖节点相同的标签 (pre-patch),完成RepoCPG的构建。

mergeCPG

补丁表示

总体来说,就是用了图和序列两种表示方式。其中,图就是刚刚生成的slicing RepoCPG,而序列就是pre-/post-patch中的代码更改。然后作者用了一些AI方法,将这些内容表示成模型能理解的样子。

(1)Graph branch:对slicing RepoCPG中的节点生成节点向量,使用Graph Attention Networks捕捉边信息,等等

(2)sequence branch:只包括pre-patch和post-patch中的修改代码和版本信息

渐进学习 Progress Learning

(1)训练序列分支,冻结图分支的权重。

(2)训练图分支,冻结序列分支的权重。

(3)推理时按照加权公式同时使用两个分支的输出,得到最终结果。

Results

作者将本文方法(RepoSPD)使用SPI-DB即PatchDB两个数据集,与

(1)安全补丁检测方法:a. 监督训练方法(GraphSPD, PatchRNN),b. 预训练模型(CodeBERT, CodeT5, UniXcoder),c. 大语言模型(llama3-70b)

(2)静态检测方法(Cppcheck, RATS, Semgrep, Flawfinder, VUDDY)进行比较

(3)并分析了RepoSPD在不同漏洞类型上的表现

(4)以及RepoSPD的消融实验

SPI-DB:FFMPeg以及Qemu,包含25k补丁,其中10k为安全相关

PatchDB:348个开源仓库,包括36k代码片段,其中约12k为安全相关补丁。


Reproduce

环境配置

  • 已配置 apt-get install openjdk-8-jre

(1)在运行./joern-parse( data_preproc/data_loader.py )的时候出现报错:Exception in thread "main" java.lang.NoClassDefFoundError: scala/collection/immutable/List

发现是由于joern/lib中的文件不全(部分文件为空)导致的。通过查看lib中的文件,发现作者是从joern-1.1.59基础上进行修改的。

(2)下载官方joern-1.1.59

curl -L -O https://github.com/ShiftLeftSecurity/joern/releases/download/v1.1.59/joern-cli.zip
unzip joern-cli.zip

(3)使用joern-cli/lib替换RepoSPD/joern/lib

cp -rn /path/to/joern-cli/lib/ /path/to/RepoSPD/joern/lib

Code (slicing RepoCPG生成)

(1) 预处理:提取补丁修改的内容所在文件,删除文件中的无关函数(非补丁修改的函数),并将补丁修改函数在补丁前/后文件中的行数对齐。

(2) diff函数cpg整合及切片:为处理后的补丁前/后文件分别生成cpg,通过合并相同nodeInfo的节点并重新赋值nodeID整合cpg,并进行切片。其中,criterion: 只在补丁前/后中存在的节点,direction: bidirectional, graph: pdg。

(3) 依赖获取:为处理后的补丁前/后文件分别生成函数调用图,并进行比较。提取只在补丁前/后存在的调用函数,分别存储为补丁前/后调用函数文件。

(4) diff依赖切片:为补丁前/后调用函数文件分别生成cpg,此后进行切片。其中,criterion: 只在补丁前/后调用函数中存在的节点,direction: bidirectional, graph: pdg。

(5) 将diff函数切片与diff依赖切片进行整合

data_preproc/data_loader.py

get_ab_file()

根据 diff --git 里的 /a & /b 获取补丁修改前和修改后的文件,存储到 data_preproc/ab_file/<commit_id>

gen_cpg()

(1) 使用Joern获取ab_file中每个函数的函数名、文件名、起始行号、结束行号,并以json格式存储到data_preproc/ab_file/<commit_id>/cpg_a(b).txt

@main def exec(inputFile: String, outFile: String) = {
   importCode(inputFile)
   cpg.method.map(x=>(x.fullName,x.filename,x.lineNumber,x.lineNumberEnd)).toJson |> outFile
}

(2) 调用locate_and_align.py,使用ab_file中补丁修改前/后文件取diff模块生成diff.txt。并根据diff模块对原本的补丁前/后文件进行修改,包括:

  • 不在diff模块的函数:置为空行
  • 在diff模块中的函数:确保补丁修改前/后文件相应代码行数对齐(e.g., 补丁在151增加一行,则在补丁前文件151行增加空行。)
merge_cpg()

(1) 遍历ab_file,使用joern生成cpg14,存储到outA(B)

(2) 调用 generateLog()

generateLog()

(1) 调用importCPG(),遍历outA(B),获取每个函数的nodes & edges信息。

a. 获取补丁前cpg(Acpg)的节点和边(ANodesbyFunc, AEdgesbyFunc),以及补丁后cpg(Bcpg)的节点和边(BNodesbyFunc, BEdgesbyFunc)

b. 对同时存在于Acpg和Bcpg的函数(ABFunc),只存在于Acpg中的函数(AFunc),只存在于Bcpg中的函数(BFunc)分别切片

(2) 调用slice() 切片:

a. 处理传入的AEdges, BEdges:

  1. 筛选AST, DDG, CDG属性边
  2. 将边分类为:只在Acpg中出现的边(PreEdges,-1),只在Bcpg中出现的边(PstEdges,1),以及在两者中都存在的边(CtxEdges,0)

b. 处理传入的ANodes, BNodes,将节点分类为:

  1. CtxNodes:ANodes与BNodes中都存在的节点,归属属性设为0,将两节点nodeID设为相同数值(合并)。
  2. PreNode:只在ANodes中的节点,归属属性设为-1。
  3. PstNodes:只在BNodes中的节点,归属属性设为1。

c. 从PreNodes + PstNodes 开始进行双向切片

d. 处理切片结果,包括:处理孤立节点 / 边,删除无效节点,删除无效边等

add_dependency/get_depend.py

(1) 使用cflow生成ab_file中修改后源码的call graph

(2) 比较补丁修改前/后的call graph,提取只在补丁前/后存在的依赖函数(diff_func)

(3) 在repo中查找并提取diff_func代码存储到 dataset/example_dep.jsonl

add_dependency/add_dep.py

(1) 提取dataset/example_dep.jsonl中存在依赖的函数,存储到 add_dependency/dots/<commit_id>。其中,dependency.c 为所有依赖函数;pre_dependency.c 为只在补丁前的依赖函数;post_dependency.c 为只在补丁后存在的依赖函数。

(2) 使用Joern获取dependency.c的cpg14,存储到dependency

(3) 调用get_dep_log() 解析dependency中的cpg14

a. 对pre_dependency.c & post_dependency.c 调用importCPG() 获取每个函数的nodes & edges。

b. 对所有依赖函数,调用slice()进行切片,切片结果为depEdges & depNodes。其中,由pre_dependency.c中的函数依赖切片得到的结果属性为A,由post_dependency.c中的函数依赖切片得到的结果属性为B。

c. 根据切片后 nodes & edges 的属性,划分ANodes, BNodes & AEdges, BEdges

(4) 调用connet_dep(), 将提取的edges & nodes与原函数的slice结果融合。