Repository-Level Graph Representation Learning for Enhanced Security Patch Detection
Update: March 31, 2025
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
对于上图中的补丁,通过首先获取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的构建。
补丁表示
总体来说,就是用了图和序列两种表示方式。其中,图就是刚刚生成的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:
- 筛选AST, DDG, CDG属性边
- 将边分类为:只在Acpg中出现的边(PreEdges,-1),只在Bcpg中出现的边(PstEdges,1),以及在两者中都存在的边(CtxEdges,0)
b. 处理传入的ANodes, BNodes,将节点分类为:
- CtxNodes:ANodes与BNodes中都存在的节点,归属属性设为0,将两节点nodeID设为相同数值(合并)。
- PreNode:只在ANodes中的节点,归属属性设为-1。
- 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结果融合。