简介:一份面向毕业设计与安全方向初学者的APT攻击检测项目,以Python实现基于溯源图的检测思路,核心代码涉及RGAT与GRU等模型,适合计算机、软件工程、人工智能等专业用于毕设、课设或项目演示。包内共26个文件,源码以11个py文件为主,覆盖模型训练与主程序;7个xml为工程配置,4个md提供部署与说明文档,另含数据压缩包和项目辅助文件,整体仅49KB。除源码外,部署文档和全部数据资料可帮助读者理解从数据预处理、模型构建到结果输出的完整流程,README与设计文档给出实验环境与运行说明,方便在现有代码基础上修改实现其他功能或改进检测算法。目前已有217人学习,项目获导师认可且答辩评审分达95分,适合作为安全方向毕业设计的参考模板。
1. 从“盲检”到“看行为”:基于 Python 溯源图的 APT 攻击检测
先说结论:这份基于 Python 的溯源图 APT 攻击检测源码,核心不是某个模型多先进,而是把“检测”从特征匹配换成了“行为链路推断”。流量层面的恶意流量特征可以加密、可以伪装,但攻击者要在内网横向移动、提权、窃取数据,必然留下系统调用和文件操作的痕迹——溯源图(Provenance Graph)就是把这些痕迹串成一张有向图。这个项目在 DARPA TC(Transparent Computing)数据集上做检测,答辩能拿下 95 分,靠的不是模型堆叠,而是对数据、模型和评价流程的完整闭环把控。
它适合两类人:一类是正在做毕业设计或课程设计、需要一套能复现的完整流水线(数据处理→模型训练→评估)的学生;另一类是想快速介入 APT 检测方向、手头缺数据集处理代码和工作流参考的安全方向从业者。项目源码里既包括 StreamSpot 方法的复现,也包括 RGAT(关系图注意力网络)配合 GRU 的检测分支,后者是近年论文里比较有代表性的做法。
从“这是什么”到“怎么跑起来”,我按数据、模型、部署和坑位四个部分逐层拆。
2. 溯源图检测的基本盘:为什么 APT 检测要看图结构
2.1 攻击检测从“单点”到“链路”的转变
传统入侵检测盯着单条日志提特征:某个进程连接了某个陌生 IP、某个用户文件被改了一次,命中规则就报警。APT 攻击的特点是慢、分散、多阶段,单点行为看起来完全正常:一个员工下载了一个文档、一个进程发起了一次 DNS 查询,单独拆开没有任何入侵的味道,但把这些点连成线,能看到“读文件→提权→外连命令控制端”的完整链路。溯源图正好提供这种关联能力,节点是进程、文件、套接字,边是系统调用和操作关系,攻击路径在图里就是一条或多条子图模式。
这个项目里的溯源图构建,数据来自 DARPA 提供的审计日志。每个审计事件解析出来后包含五个核心字段:主体(谁发起的操作)、客体(作用在谁身上)、操作类型、时间戳、结果状态。把这些字段组装成图就是两个映射——节点映射和边映射。节点去重用 (type, id) 二元组,边重用 (src, dst, relation) 三元组,权重统计一段时间内相同边的出现频次。图的规模控制和阈值使用很关键,日志量一大,全量节点塞进内存会直接崩掉。
2.2 项目代码结构与数据流向
解压源码后,项目根目录下main.py是 StreamSpot 的入口,darpa_cadets_RGAT.py和darpa_cadets_RGAT_GRU.py对应两套 RGAT 方案的分支入口。数据目录darpa-tc-cadets存放从 DARPA 数据集转换出来的原始字段表,model.py、model_RGAT.py、model_RGAT_GRU.py三个文件分别装不同模型的网络结构定义。整个调用路径如下:
# 入口层面,数据先走统一解析,再分别送到不同模型 python main.py --data ./darpa-tc-cadets --window 30 --topk 1000 python darpa_cadets_RGAT.py --data ./darpa-tc-cadets --epochs 60 --lr 0.001 python darpa_cadets_RGAT_GRU.py --data ./darpa-tc-cadets --epochs 60 --lr 0.0005第一行指令跑的是 StreamSpot,用滑动窗口切分流水数据,对每个窗口生成图摘要,然后做相似度比对;第二行进入 RGAT 分支,把图编码成向量做分类;第三行在 RGAT 上叠加 GRU,把时间步串起来。实际跑起来,GRU 叠加分支最耗时,但效果往往比单 RGAT 稳一点。
2.3 为什么选 RGAT + GRU 而不是纯 GCN
同样是对图建模,GCN 是拉普拉斯平滑式的邻居聚合,但 APT 检测里的边是有方向、有类型的:进程“读”文件和进程“写”文件在语义上完全不同,GCN 用一个权重矩阵处理所有边,等于把方向信息抹掉。RGAT 给每种关系类型分配独立的注意力权重,能在聚合邻居时区分“这个操作是从属于哪个阶段的攻击”。GRU 加在 RGAT 输出层之上,本质是处理图的时序演化——攻击者的每一步动作之间有先后逻辑,单图分类只会看某一个时间窗口内的结构,GRU 是跨窗口跟踪路径演进,专治分阶段攻击。
这套组合能复现到现在这个程度,项目里最有价值的部分是model_RGAT_GRU.py那段“RGAT 编码 + GRU 时间聚合”的衔接逻辑,stgraph 和 GRU 的边界怎么切、中间张量要不要做层归一化,都是见效快的细节。
2.4 图数据预处理阶段为什么要做“窗口滑切”
溯源图如果按天构建,一张图好几万个节点,正负样本比例会极度失衡——一天里大部分操作都是正常的。这个项目借鉴 StreamSpot 的滑动窗口做法,把连续审计日志切成固定长度的重叠窗口,每个窗口单独建一张小图。窗口设短了,攻击路径会被拦腰截断,链路不完整;设长了一窗口里全是正常节点,攻击者的几条关键边淹没在噪声里。源代码里 Window 参数默认给的是 30 秒,这个值在 CADETS 数据上试出来效果是好的,换到别的数据集时先用 10、30、60 三档做网格搜索再确定。
窗口产出的小图接下来要采样定长根路径。根路径指的是图上从一个根节点出发、按边方向走到深处的一条路径,用它代表一个窗口图的行为模式。这一步主要目的是把变长的图固定成等长序列,方便后面塞进 GRU。路径长度控制在 5-8 跳,跳数太浅图模式区分不出攻击变种,跳数太深会混进大量无关节点,检测的精确率和召回率都会被拖下来。
2.5 三套检测策略的适用边界
同一份源码里同时提供 StreamSpot、RGAT、RGAT+GRU 三套方法,这是它的优势,也是初学者的混用重灾区。StreamSpot 是图摘要加相似度比对的经典方法,轻量、可解释,但攻击路径复杂时摘要会把关键结构抹平;RGAT 分类精度高,适合在固定数据集上做批量离线分析;RGAT+GRU 是“离线精度再拔高”的选项,代价是训练时间成倍增加。
| 检测策略 | 输入依赖 | 输出形式 | 训练开销 | 适合场景 |
|---|---|---|---|---|
| StreamSpot | 原始审计日志 | 异常得分序列 | 数十秒 | 快速基线、流式初筛 |
| RGAT | 溯源图节点+边类型 | 分类置信度 | 中等 | 离线分析、跨数据段比对 |
| RGAT+GRU | 溯源图+时间窗口序列 | 分类置信度序列 | 高 | 慢速分阶段攻击检测 |
从代码维护角度说,刚拿到项目时优先跑通 StreamSpot 分支,因为它的依赖最少、出问题最好查;RGAT 分支适合作为第二个里程碑,最后再上 RGAT+GRU。一次就全链路跑三套模型,环境配置和时间成本不划算,效果上也分不出主次。
3. 溯源图数据准备:从 DARPA TC 原始日志到可训练的图样本
3.1 市售数据的格式困境与这个项目给的解法
DARPA TC 数据集免费、标注相对完整,但它的原始审计日志是 JSON 行嵌套结构,字段名和当前主流图模型需要的输入对不上。直接拿原始 JSON 喂给 PyTorch 是不行的,EVENT 里包含进程、线程、文件、网络流多重嵌套,需要一层层展平。这个项目做了最关键的一步转换:把嵌套 JSON 解析成六列平铺表,再构建图。
项目给的streamspot.py和darpa_cadets_RGAT.py已经包含了这段解析逻辑。转换产物大致包含以下字段:
| 原数字段 | 解析后字段 | 用途说明 |
|---|---|---|
| subject.pid | src_id | 主体进程 PID,作为节点 id |
| object.path | dst_id | 对象标识(文件路径或目标地址) |
| operation.type | rel_type | 操作类型(read/write/exec) |
| timestamp | ts | 时间戳,用于窗口切分 |
| subject.uid | uid | 用户属性,便于按用户隔离分析 |
| event.result | status | 操作成功或失败,失败状态要保留 |
转换过程里遇到的第一个坑是进程退出后 PID 复用。两个不同时间段运行的进程可能共用同一个 PID,直接按 PID 建节点会把两条不相关的链路合并到同一个点上。源码里处理方式是只在窗口内部去重,避免跨窗口复用带来的污染。这一点在读代码时能直接看到,src_id的生成带有时间窗口前缀,而不是裸 PID。
3.2 构建图的完整流程与参数设定
这个环节的核心是从平铺表构建有向异构图。异构图的意思是节点分类型(PROCESS、FILE、SOCKET),边分类型(READ、WRITE、EXECUTE、CONNECT)。PyTorch 生态里处理这种图通常用 DGL 的 HeteroGraph,项目源码里的建图过程简化成了手工实现,效果和 DGL 等价,重算效率稍差但代码更易改。
# 从解析后的 Dataframe 构建有向图的核心逻辑(简化版) def build_graph_from_log(df, window_id): graph = nx.DiGraph() for _, row in df.iterrows(): src = f"{row['src_type']}:{row['src_id']}" dst = f"{row['dst_type']}:{row['dst_id']}" # src 和 dst 带类型前缀,避免进程与文件同 id 冲突 if not graph.has_node(src): graph.add_node(src, node_type=row['src_type']) if not graph.has_node(dst): graph.add_node(dst, node_type=row['dst_type']) # 相同边统计次数作为权重,聚合重复操作 edge_key = (src, dst, row['rel_type']) if graph.has_edge(*edge_key): graph[src][dst][row['rel_type']]['weight'] += 1 else: graph.add_edge(src, dst, rel_type=row['rel_type'], weight=1) return graph权重聚合这一步容易被忽略,但它直接影响 RGAT 的消息传递质量。攻击者的持久化行为会反复执行同一条命令链,体现在图中就是同一条边反复出现;权重把这种频次信息编码进去了,RGAT 在消息传递时就能感知到“这条路径在时间窗口里被走了多次”。不聚合直接保留重复边,图规模会膨胀好几倍,训练速度会明显下滑。
3.3 正负样本怎么标:攻击路径的标注策略
检测模型是监督学习,必须给每个窗口图打出 benign 或 attack 标签。这个项目的标签来源是 DARPA TC 官方提供的攻击时间区间,代码里做的是时间窗口与攻击时间段的交集判定。
# 窗口标签判定逻辑 def assign_label(window_start, window_end, attack_windows): for att_start, att_end in attack_windows: # 窗口与攻击区间有重叠即标攻击,用于召回优先的检测任务 if window_start <= att_end and window_end >= att_start: return 1 # attack return 0 # benign需要注意这个标注方式对毒化训练的影响。攻击前期的探测动作和攻击后期的数据窃取,中间隔着几个小时甚至几天的正常行为,如果攻击窗口设得太大,会把大量正常操作标成攻击样本,噪声非常大。源码里给了一个折中方案:把真实攻击时间再截取一个“检测关注段”,只看攻击真正活跃的那部分。具体操作在那个attack_windows的配置数组里手动调,不同数据集形态不一样,先跑通再按实际报警时间反推调整。
3.4 平衡正负样本数量:少数类过采样
CADETS 场景下攻击只占百分之几,直接训练分类器会让模型全部预测为 benign 也能拿到 95% 准确率。项目里处理方式综合了两条路:一是窗口滑动时以攻击窗口为中心多切几个偏移,产生更多攻击图;二是对攻击图做子图级别的增强——扰动边的权重、在邻接矩阵上加随机掩码,让模型见过更多攻击路径变体。前者是数据层面的事,后者是代码里增强管线做的事。具体两端的权重怎么配,根据测试集 F1 调即可,优先调过采样倍数而不是增强幅度,增强幅度过大容易让模型对具体边权重过敏。
4. 模型训练的主干拆分:RGAT 与 GRU 分别解决什么问题
4.1 RGAT 的输入与消息传递机制
RGAT 吃进去的样本是“异构图 + 边类型数组”。DGL 实现里调用dgl.heterograph()时通常要显式传入多个边类型字典,但这个项目手工实现,结构上等于维护两个索引表:节点特征表(node_feat)和边特征表(edge_feat)。消息传递阶段对每条边按rel_type取对应的注意力权重矩阵,再做 softmax 归一化和邻居聚合。
干讲图注意力机制比较飘,直接看关键实现更直观。以model_RGAT.py里的核心层为例,每一层 RGAT 做的事情是基于目标节点和源节点的特征计算注意力分数,再带着关系加权做邻居聚合。模型的隐藏层维度配的是 64 或 128,这个数值在 DARPA CADETS 这种中等规模图上够用了。头数配 4 是效率跟效果之间的平衡点,头数再多的话参数量翻倍,但没带来显著精度提升。
# RGAT 单层消息传递的简化实现思路 attn_score = self.attention(rel_type, src_feat, dst_feat) # 不同 rel_type 使用独立的 weight 矩阵,保持异构性 attn_score = torch.softmax(attn_score, dim=1) # softmax 在目标节点的所有入边上归一 messages = attn_score.unsqueeze(-1) * src_feat agg_feat = torch.sum(messages, dim=1) # 累加邻居消息得到节点输出特征参数rel_type的取值集合需要随着数据集变化而调整。CADETS 数据里有 read、write、exec、connect 等常见操作,但换到其他数据集,可能还有内存映射、fork 之类的新关系。新关系没有对应的权重矩阵,模型会退化成随机初始化,造成意外掉点。初次换数据集时花十分钟清点一下关系类型,比反复调模型参数更有效。
4.2 GRU 模块的输入序列组织与序列长度
GRU 分支要把 RGAT 编码后的节点向量按时间排成序列。同一窗口索引下的多个节点向量需要池化成单个图级向量——通常用最大池化或平均池化。项目源码里用的是torch.max池化,因为攻击子结构(比如某条关键路径)往往只占窗口图的一小部分,均值池化会被大量正常节点稀释,最大值池化能保留显著子结构的信号。
# 图级池化再进 GRU pooled = torch.max(node_embeds, dim=1).values # node_embeds 形状 (num_nodes, hidden_dim),池化后 (hidden_dim,) gru_input = pooled.unsqueeze(0) # 加时间步维度GRU 的序列长度设为 10-20 个窗口。序列太短模型看不到攻击的阶段性演进,太长则输入维度爆炸,也把模型注意力分散到无关的时间步上。源码里默认配置是 10,意思是每次预测要看前 10 个连续窗口的图向量。这个数字依赖数据的采样密度,如果日志量本身不大,序列长度要下调到 6 左右,否则 GRU 大部分时间在吃空的或重复的窗口。
4.3 两套分支训练流程对比
理解两条主线(RGAT 与 RGAT+GRU)的训练差异,部署的时候会少走弯路。单跑 RGAT 分支时,每个窗口图独立参与训练,批次内随机采样图和标签;叠加 GRU 后,最小训练单元从单图变成了连续序列集,一个批次内要同时加载多个序列,每个序列又包含多个窗口图,这会改变内存占用模式。
# RGAT+GRU 分支的批次循环关键片段 for epoch in range(epochs): for batch in sampler: graph_seq = [batch[i] for i in range(seq_len)] # graph_seq 是连续窗口的图列表 embeddings = rgat_model(graph_seq) # 每个窗口输出节点嵌入 seq_vec = pool_and_stack(embeddings) # 池化成 (seq_len, hidden_dim) 序列张量 pred = gru_model(seq_vec) loss = criterion(pred, batch_attack_label)从这个代码结构可以看出,GRU 模式训练迭代慢的核心原因是不能像单图训练那样随便 shuffle,序列内部的窗口顺序必须保持连贯。因此 GRU 分支的batch_size一般要调小到 8-16 之间,否则显卡内存会顶不住。
4.4 损失函数与类别权重设置
训练配置里值得关注的是损失函数的选择。因为是二分类,代码默认用的是交叉熵,但正负样本不平衡迫使代码实现里给损失函数加了权重参数。class_weight跟逆类别频率绑定——少数类权重提高,相当于告诉模型“少犯漏报”。使用中常见问题是新手把这个权重值过度调大导致精确率暴跌,攻击类被高概率误报成正常行为被漏检的假象消失之后,换来一堆错报。项目源码给的经验起点是小数类权重为 2.0,然后按 F1 分数微调。
学习率层面也有说头。RGAT 内部结构对学习率敏感,lr=0.001时 RGAT 能正常收敛;GRU 模块叠加后要降到0.0005左右,原因是 GRU 的时序梯度回传路径长,学习率稍高就震荡。项目源码里两个入口文件分别设了不同的浮点默认值,看起来是刻意为之,实操中新手往往想统一用一个学习率,实际效果不如分开调。
5. 部署落地的常见问题避坑:从 DARPA 数据到训练报错的处理记录
5.1 文件路径映射错位导致数据加载失败
现象:换了数据目录后,训练启动时报FileNotFoundError,报错指向darpa-tc-cadets下的某个子目录不存在。
排查过程:源码里读取数据用的是相对路径./darpa-tc-cadets/,但两套模型入口文件所在目录不一致——darpa_cadets_RGAT.py在根目录,而数据实际挂载路径在data/streamspot_RGAT.py的同级目录时,入口文件所在目录不同,相对路径就会跑偏。
解决:在入口脚本开头打印os.getcwd(),确保当前工作目录与数据实际位置对应;更省事的做法是统一改成Path(__file__).resolve().parent / "datasets",这样不管你从哪个目录启动脚本都能定位到数据目录。
5.2 图节点 ID 冲突导致特征错位
现象:训练时 loss 正常下降,但测试时的 F1 分数极低,或者准确率稳定在 52% 附近,比随机猜略好。
原因:构建节点 ID 时没有带节点类型前缀,进程123和文件123互相混淆,一个节点混入了两类完全无关的特征信号。GNN 的聚合是邻居感知的,节点类型错配会让两类节点的特征互相“污染”。
解决:构建图时用src_type:src_id做节点唯一标识,而不是裸 ID。这个改动全局影响建图函数一步,代码里有两三处add_node的调用点要一起检查,漏掉一个仍会有部分混叠。
5.3 审计日志时间戳字段解析异常
现象:解析到的ts字段是字符串而不是数值,导致窗口切分和排序逻辑报类型错误或窗口错乱。
原因:DARPA 原始日志里不同数据子集的时间戳格式不完全统一,有的带时区偏移,有的是相对启动时间。项目源码里那段int(ts)强转在某个子集上会炸。
解决:统一在数据加载层做一层标准的to_datetime转换,再转成 Unix 毫秒;强转前先判断是否是负值——DARPA 的部分子集用负数表示“系统启动前”的时间,需要整体加偏移量归一化。
5.4 CUDA 内存不足,窗口图批量加载时反复 OOM
现象:RGAT+GRU 分支训练到第二个 epoch 中途报out of memory,单跑 RGAT 分支却不报。
原因:GRU 分支一个批次要保留十个窗口图的中间激活值,梯度回传时内存占用量成倍增加。单图训练每个 batch 只保留一层的中间量,内存占用完全不同。
解决:batch_size从 32 降到 8,seq_len从 10 放到 20 时,图规模大导致内存不够时,优先调小num_workers到 0 来避免数据加载线程复制张量。如果还爆,就把图采样中的根路径长度从 8 缩到 5,这是最有效的省内存手段。
6. 进阶玩法:基于这个小项目扩展成自己的检测系统
如果只是跑通代码,这个项目的价值只释放了一半。扩展方向是现成的:把 StreamSpot 的滑动窗口结论和 RGAT 的分类结果做决策级融合,形成一套两阶段检测管线。第一阶段 StreamSpot 快速打分,把得分超过阈值的窗口标记为候选;第二阶段只有候选窗口进入 RGAT+GRU 细化判断。这套架构的好处是误报率降得很明显——第一步粗筛已经过滤掉了大量复合特征的干扰。
验证融合是否有效,按照源码里的评估脚本计算候选集的召回率和精度的差值。候选集召回率高于 90%、最终输出精度也高于 90%,融合就是成功的。这也是论文里经常出现的“多方法集成”卖点,答辩时比单模型更有故事线。
模型层面值得折腾的还有两个调参点。第一个是 RGAT 的注意力头数,默认 4 可以试 8——头数翻倍后每个头捕获的边语义更独立,代价是计算时间变长,所以在 DARPA 这种中等图上考量效率收益不划算。第二个是 GRU 的序列长度,10 个窗口优于 20 个时,说明你的数据集攻击行为主要集中在短时间序列内,这时可以考虑把窗口长度本身也改小,让短时细节不被长窗口平均掉。调参顺序永远是先窗口大小再序列长度,最后才是头数。窗口大小是整个数据准备的基础参数,动了它,后面所有调优都要重来。
做这类溯源图检测还有一个容易被忽略的硬约束:数据标注的可靠性。DARPA 官方标注的攻击时间范围通常是依据最终报告人工框定的,边界本身有模糊性。碰到边界上分类结果反复横跳的窗口,建议直接丢掉而不是硬标——这类样本本来就没法给出明确 ground truth,留在训练集里只会让模型学习到噪声。这是我做过好几轮实验后得出的关键认知;从那以后我每次拿到新数据都会先跑一遍边界窗口统计,把所有“攻击时间点前后 2 秒内”的样本单独拎出来查看,再决定是否纳入训练。有效的检测系统讲究的是干净的数据流,明确标注比堆积数据更可靠。
等这套流水线在你自己的机器上能跑通,建议再用开源数据集替换掉 DARPA 的原始日志做一个小规模复现实验,选一段攻击痕迹明显的子集,从构建图到出检测结果的整个流程会在两小时内跑完。它能帮你验证这个框架不绑定于任何单一数据集,也是拿去应对答辩或项目验收时最有说服力的东西。希望这篇拆解对你有用,愿你少踩几个我踩过的坑。
本文还有配套的精品资源,点击获取