简介面向安全分析场景的Python攻击图自动化生成项目以attack-graph-generator为内核通过整合拓扑关系、漏洞数据与攻击路径建模帮助安全分析师快速识别潜在入侵链条减少手工绘制攻击图的重复劳动。压缩包共101个文件体积约37.75MB文件类型涵盖Python源码与字节码、Shell脚本、JSON/YAML配置、DOT拓扑描述、PDF报告及ZIP归档等分别用于系统逻辑实现、自动化部署、参数配置、图形渲染与结果分发结构清晰且便于按需提取。项目附带版本控制配置与说明文档支持二次定制。目前已有320人学习下载。从中可学习攻击图数据建模、DOT图生成、多格式输出等核心思路也可借助JSON/YAML调整拓扑与漏洞参数快速套用到自身评估环境适合安全运维、渗透测试人员以及高校安全专业学生作为实战参考。1. 自动化攻击图生成器为什么手工作业者总是最晚出报告攻防演练或红队评估中最耗时的一环不是漏洞扫描而是把扫描结果拼接成一张“哪里能打到哪”的攻击路径图。手工画图通常是这样打开漏洞报告对照拓扑文件用 Visio 或 draw.io 一条条连边连到一半发现某台主机跨段可达又回头改配。基于 Python 的 attack-graph-generator 自动化攻击图生成器就是用代码把“资产解析→漏洞关联→路径生成→图可视化”这一串动作串成流水线输入扫描数据和连通性清单输出一张可交互、可追溯的攻击图。它适合红队人员、安全研究员、护网蓝军也适合刚用 python 入门不久、想找一个能落地的安全工具的开发者。本文会把这类工具的模型、源码结构和避坑经验一次讲透让你能照着自己搭一套。2. 攻击图模型与 Python 选型先把“图”拆成节点和边再谈自动化2.1 攻击图的抽象过程攻击图不是网络拓扑图很多初学者第一次接触 attack-graph-generator会把它理解成“自动画一张网络拓扑图”这是个误判。网络拓扑图描述的是设备之间物理或逻辑上的连接关系而攻击图描述的是攻击者从初始状态出发通过一系列原子攻击动作逐渐获得权限、扩大控制范围的状态转化过程。两者目标完全不同后者本质上是一个有向图上的状态转移系统。在 attack-graph-generator 的实现里图被抽象成 G(V,E)其中 V 是节点集合每个节点代表一个攻击者可以达到的状态最常见的是“主机权限”组合比如“(web01, user)”“(db01, root)”E 是有向边集合每条边代表一个攻击动作比如“web01 存在 SQL 注入漏洞允许通过网络访问 mysql 端口从而获得 db01 的普通权限”。边携带的条件通常包含前置条件和后置条件这是一个典型的“条件-状态”分析器。为什么用“状态”而不是“主机”做节点因为攻击路径分析需要体现出权限提升过程。如果只以主机为节点那么“拿到 root”和“拿到 user”就是同一个节点路径搜索时无法发现提权链条。攻击图生成器的一个核心价值正是把这类容易被 Excel 表格淹没的提权路径显式暴露出来。这也是为什么把路径搜索和网络拓扑图软件混为一谈的方案最后都很难落地。2.2 攻击图生成的三大输入资产、漏洞与连通性自动化攻击图生成器不能凭空画图它的输入一般分成三类资产清单、漏洞信息、连通性关系。资产清单描述有哪些主机、运行什么服务、开放哪些端口漏洞信息描述每台主机上存在的 CVE 或弱配置连通性关系描述主机之间是否存在可达路径包括 IP 层可达和应用层可达。三者缺一边的生成条件就不完整。下表是一份常见源码实现里定义的数据输入规范实际项目中你会用扫描工具或 CMDB 导出后填充输入名称关键字段来源示例缺失后果资产清单host_id / ip / os / service / portnmap 扫描结果、CMDB没有节点漏洞清单host_id / cve_id / cvss / affected_versionNessus、OpenVAS、NVD没有边连通性清单src_ip / dst_ip / protocol / port防火墙策略、路由快照边乱连或全部可达在设计源码时我习惯把这三种输入统一解析成 DataFrame 或字典表再进入构图环节而不是直接在扫描输出文件上构图。这样做的原因是数据清洗逻辑和构图逻辑可以分开测试也方便接入不同的数据源。漏洞清单里尤其要保留 cvss 分数后续过滤高风险路径会用到。2.3 为什么选 Python 而不是 Java 或 Go攻击图生成器这个方向选 Python 作为实现语言主要因为三个原因生态、胶水能力、调试成本。生态方面网络扫描可以用 python-nmap 或 scapy图处理可以用 networkx可视化可以用 graphviz全部是成熟库不需要自己造轮子胶水能力方面Python 调用外部工具非常方便能在一个脚本里把整个流水线串起来调试成本上交互式环境里可以直接查看图的节点数和边数比对结果比编译语言直观。当然 Python 不是没有缺点。攻击图在大规模网络中节点数会上万纯 Python 遍历性能容易成为瓶颈但这类工具通常跑在离线分析场景几秒钟的图生成时间完全可以接受。如果真到了百万级节点常见的做法是改用并行计算或接入图数据库这个放到后续的进阶章节讨论。在环境准备上开发机装好 Python 3.9 即可一般开发使用 venv 隔离依赖。许多新手是从 python 教程或 vscode python 环境配置开始接触 Python 的这里直接给出我常用的最小依赖安装方式python3 -m venv .venv source .venv/bin/activate pip install networkx pandas python-nmap graphviz pyyaml这段命令做了三件事第一行创建虚拟环境避免依赖污染系统 Python第二行激活虚拟环境第三行安装五个常用库。networkx 负责图结构pandas 负责数据清洗python-nmap 负责扫描graphviz 负责最终渲染pyyaml 负责读取配置文件。如果你只需要解析已有扫描结果python-nmap 可以不装数据从 ES 或 CSV 导入即可。参数说明venv 虚拟环境建议放在项目根目录命名为.venv而不是venv避免与代码里的模块名冲突pip 安装时如果网络较慢可添加-i指定镜像源但镜像地址因环境而异这里不指定。这一章讲清楚了攻击图的抽象模型和输入输出格式。下一步真正写代码实现一个可运行的 attack-graph-generator 源码骨架。3. 用 Python 实现 attack-graph-generator 源码目录设计、核心代码与参数调优3.1 源码目录结构与最小运行流程先看一份典型的 attack-graph-generator 源码目录设计这是我从多个安全工具里总结出来的常见结构便于后续扩展attack-graph-generator/ ├── config/ │ └── generator.yaml ├── data/ │ ├── assets.csv │ ├── vulnerabilities.csv │ └── connectivity.csv ├── core/ │ ├── __init__.py │ ├── graph_builder.py │ ├── path_search.py │ └── vuln_matcher.py ├── utils/ │ ├── __init__.py │ ├── loader.py │ └── visualizer.py ├── tests/ │ └── test_small_graph.py ├── main.py └── requirements.txt目录说明config 存放配置文件data 存放扫描原始数据core 存放图构建和路径搜索的核心逻辑utils 存放数据加载和可视化工具tests 放回归测试main.py 是入口。这样拆分的好处是单个文件职责单一后续做单元测试时可以直接对 core 里的类进行断言不必整个流程跑通。运行流程上main.py 先读取 config 中的配置项再调用 loader 加载三个 CSV然后交给 graph_builder 构图最后输出到 output 目录或直接渲染成图。一个最小可运行的 main.py 可以长这样# main.py from utils.loader import load_all_data from core.graph_builder import build_attack_graph if __name__ __main__: assets, vulns, conns load_all_data(data/) graph build_attack_graph(assets, vulns, conns, cvss_threshold7.0) print(f节点数: {graph.number_of_nodes()}, 边数: {graph.number_of_edges()})这段逻辑不算复杂load_all_data 把三个 CSV 读成 DataFramebuild_attack_graph 按漏洞分数阈值构图最后打印图规模。配置项 cvss_threshold 是第一个关键参数用于过滤低危漏洞避免边数爆炸后面会专门讲。3.2 数据加载把三类原始输入解析成统一结构loader.py 里最需要注意的就是字段兼容。不同扫描工具生成的字段名五花八门OpenVAS 输出 hostnmap 输出 address如果不做统一映射后面构图代码会乱成一团。这里给出一个能容纳常见字段差异的加载实现# utils/loader.py import pandas as pd from pathlib import Path def load_assets(csv_path: str) - pd.DataFrame: df pd.read_csv(csv_path) df[host_id] df.get(host_id) or df.get(ip) or df.get(address) required [host_id, os, services] for col in required: if col not in df.columns: df[col] return df[[host_id, os, services]] def load_vulnerabilities(csv_path: str) - pd.DataFrame: df pd.read_csv(csv_path) if cve_id not in df.columns: raise ValueError(vulnerabilities.csv 缺少 cve_id 列) df[host_id] df.get(host_id) or df.get(ip) df[cvss] pd.to_numeric(df.get(cvss), errorscoerce).fillna(0.0) return df[[host_id, cve_id, cvss, affected_version]] def load_connectivity(csv_path: str) - pd.DataFrame: df pd.read_csv(csv_path) return df[[src_ip, dst_ip, protocol, port]]逻辑说明load_assets 用df.get(host_id) or df.get(ip)做列名兼容避免因为源文件字段名不同导致程序中断load_vulnerabilities 强制 cve_id 必须存在这是后面的边生成依据cvss 转成数值并填充 0这样即使有的行缺失评分也不会导致后续比较崩溃load_connectivity 只保留四个关键列其余无关列不载入内存。这里的一个实用细节是pandas 的df.get如果列不存在返回 None配合or链可以做到多字段名回退。如果你在真实项目里遇到来源特别乱的 Excel我一般会在这一步先把列名打印出来看一眼再决定映射关系不要盲目套默认字段。3.3 图构建从漏洞和连通性推导攻击边构建攻击图是 attack-graph-generator 的核心环节。边生成规则可以抽象成一句话若 src 主机到 dst 主机存在连通性且 dst 主机上存在一个可被 src 利用的漏洞则生成一条从 (src, 权限状态) 到 (dst, 权限状态) 的边。为了简化这里的权限状态只使用 user 和 root 两档实际项目可以扩展成更多档位。实现上我用 networkx 的 DiGraph节点使用二元组 (ip, privilege)边属性里挂上漏洞信息。代码如下# core/graph_builder.py import networkx as nx def build_attack_graph(assets, vulns, conns, cvss_threshold7.0): g nx.DiGraph() for row in assets.itertuples(): g.add_node((row.host_id, user)) g.add_node((row.host_id, root)) vuln_index {} for row in vulns.itertuples(): if row.cvss cvss_threshold: continue vuln_index.setdefault(row.host_id, []).append(row) for row in conns.itertuples(): src, dst row.src_ip, row.dst_ip if src dst: continue if dst not in vuln_index: continue for v in vuln_index[dst]: g.add_edge( (src, user), (dst, user), cve_idv.cve_id, cvssv.cvss, actionf利用{v.cve_id}从{src}访问{dst} ) g.add_edge( (dst, user), (dst, root), cve_idv.cve_id, cvssv.cvss, actionf{dst}本地提权至root ) return g这段代码做了几件事先把每台主机的 user 和 root 两个状态节点加入图然后根据 cvss_threshold 过滤漏洞构建 dst 主机到漏洞列表的索引接着遍历连通性表对每条可达关系尝试生成攻击边最后给每个存在漏洞的主机再加一条 user 到 root 的本地提权边表示攻击者在拿到低权限后可能通过提权获得更高权限。参数说明cvss_threshold 建议从 7.0 开始调在漏洞密集的环境中调到 8.5 可以显著降低图的连通度src 到 dst 如果存在多条协议连接每个协议都会尝试建边容易产生重复边networkx 的 DiGraph 默认会对相同边自动去重但边属性会保留最后一次写入值如果你需要保留多元信息可以在 add_edge 时加上 key 参数改用 MultiDiGraph本地提权边是所有主机都会生成的实战中有些主机并不存在提权漏洞这一步过度生成比较常见建议给资产表增加 local_privesc 字段控制只有值为 True 才生成提权边或者检查漏洞库中是否存在本地提权类 CVE。3.4 路径搜索找到从入口到关键资产的攻击路径图构建完成后下一步通常是做路径搜索。攻击图分析最关心的问题是入口主机集合到关键资产主机之间是否存在可达路径以及最短可达路径是什么。这里给出一个基于 BFS 的实现代码比递归 DFS 更安全不会在大图上栈溢出。# core/path_search.py from collections import deque def find_shortest_paths(g, entry_nodes, target_node, max_depth6): results [] for entry in entry_nodes: visited set() queue deque([(entry, [entry])]) while queue: node, path queue.popleft() if node target_node: results.append(path) continue if len(path) max_depth: continue visited.add(node) for neighbor in g.successors(node): if neighbor in visited: continue queue.append((neighbor, path [neighbor])) results.sort(keylen) return results逻辑说明BFS 从每个入口节点开始遍历记录经过的路径达到目标节点时保存结果max_depth限制路径深度防止在连通性过强的图中搜索时间失控。默认 6 层是经验值一般攻击链跨度不会超过 6 跳如果你的网络层次更深可以加大但要注意搜索耗时和结果数量同步增长。参数说明entry_nodes一般是外部攻击者可以触及的暴露主机集合比如放在 DMZ 区、开放了 SSH 或 HTTP 服务的主机target_node通常是数据库主机或域控主机的 root 状态节点。这个函数返回的是完整路径列表结果数量可能很多建议调用时按最短长度截取前 20 条即可避免分析时信息过载。4. 把扫描器接进流水线从手动 CSV 到一键生成的自动化攻击图4.1 用 python-nmap 把资产扫描和端口探测自动化手工整理 CSV 最大的问题是不及时网络环境一变昨天的图就失效了。一种常见的做法是在源码头直接调用扫描器用 python-nmap 完成资产发现和端口探测再把结果自动转成 assets 和 connectivity 表。这个思路与用爬虫抓取外部数据是同一个模式——写程序把外部数据源变成结构化输入。# utils/scanner.py import nmap def scan_network(network192.168.1.0/24): nm nmap.PortScanner() nm.scan(hostsnetwork, arguments-sV -T4 --top-ports 100) assets [] for host in nm.all_hosts(): for proto in nm[host].all_protocols(): ports nm[host][proto].keys() for port in ports: service nm[host][proto][port].get(name, ) assets.append({ host_id: host, os: nm[host].get(osmatch, [{}])[0].get(name, ), services: f{port}/{service} }) return assets逻辑说明scanner.py 调用系统里的 nmap 可执行文件-sV做版本探测-T4提升扫描速度--top-ports 100只扫最常见的一百个端口适合快速出具资产清单返回结果会转成和 3.2 节相同字段名的 dict 列表这样下游 load_assets 和 graph_builder 无需改动。参数说明network参数改成你实际授权的网段top-ports建议 100 到 1000 之间数量越大越能发现非标准端口上的服务但扫描时间线性上升-T4在较慢的网络上可能造成丢包误报内网环境可以放宽到-T3。如果目标网络规模很大建议拆成多个网段并行扫描避免单进程耗时太长。4.2 漏洞自动关联本地漏洞库与 CVSS 阈值过滤资产有了漏洞从哪来生产环境常见做法是接入 Nessus 或 OpenVAS 导出结果也可以用本地维护的 CVE 情报库做匹配。匹配逻辑并不复杂根据资产上的服务名称和版本号去本地漏洞库查找对应软件版本的 CVE 记录再计算是否高于过滤阈值。下面是一个简化实现# core/vuln_matcher.py def match_vulns(assets, vuln_db, cvss_threshold7.0): matched [] for asset in assets: for vuln in vuln_db: if vuln[software] in asset[services] and version_matches(vuln[affected_version], asset[services]): if vuln[cvss] cvss_threshold: matched.append({ host_id: asset[host_id], cve_id: vuln[cve_id], cvss: vuln[cvss], affected_version: vuln[affected_version] }) return matched def version_matches(affected, service): return affected in service逻辑说明match_vulns 遍历资产和服务查找存在漏洞记录且版本匹配的条目版本匹配函数被单独提取便于之后做复杂版本区间比较时不影响主逻辑。实际项目中受影响版本往往是一个区间比如“小于 8.5.0”可以用 packaging 库的 Version 类做规范比较这里为了保持代码简洁只做了子串匹配。参数说明漏洞库建议使用 dict 列表字段固定为 software、affected_version、cve_id、cvss 四项cvss_threshold 在自动化流程里不要设太低否则内网主机一多边数会瞬间膨胀我实际使用中在 7.0 到 8.5 之间调整居多匹配逻辑务必加日志输出哪些资产匹配到了哪些 CVE 要能追溯不然出错时整个流程是个黑匣子没法排错。4.3 用 YAML 配置文件控制整个流水线自动化流水线最怕的是每次跑都要改代码。把参数全部外置到 YAML 配置文件里是一个很常见的做法。generator.yaml 的设计如下# config/generator.yaml scan: network: 192.168.1.0/24 top_ports: 100 timing: -T4 graph: cvss_threshold: 7.0 max_depth: 6 local_privesc: False output_dir: output/ search: entry_nodes: [192.168.1.10, 192.168.1.11] target_node: [192.168.1.200, root]配置文件里的参数会分别传给 scanner、graph_builder、path_search 三者。main.py 启动时读取配置然后按配置执行扫描、匹配漏洞、构图、搜索路径和可视化输出整个链路不再需要手工介入。配置项中 local_privesc 是控制是否生成用户态到 root 的提权边如果关闭则只有漏洞库里的明确提权漏洞才会生成提权边。这一章的自动化改造完成后攻击图生成器才真正称得上“自动化”输入只有配置文件和网络地址输出是一批攻击路径和可视化图片。对于企业内部的安全评估这套流水线可以让结果每天定时跑一遍结合 cron 或 CI 就能持续观察攻击面变化。5. 避坑指南攻击图生成器常见的 5 个关键问题与解决记录自己写过的攻击图生成器也好帮别人排查类似的源码也好我积累了一些高频翻车点。这些坑不会导致程序崩溃但会让生成的图“看起来挺对用起来全错”非常难缠。5.1 攻击图爆炸边数从几百条直接涨到几百万条现象第一次在真实内网数据上运行程序没有报错但构建图耗时极长渲染出的图片动辄上百 MB打开就卡死。原因构图逻辑里每个漏洞对每个连通关系都建边边数是漏洞数和连通数的乘积。假设 200 台主机之间的连通关系有 10000 条高危漏洞有 300 个最坏情况下会产生 300 万条边。攻击图生成器在连通性过于密集时边数爆炸几乎是必然。解决在三个环节做限制。第一提高 cvss_threshold只保留高价值漏洞第二对连通性数据做聚合同一网段内多个 IP 的相同服务连通关系合并成一条记录第三在构图前明确排除不需要分析的端口和协议例如内网主机之间的 NTP、ICMP 连通性通常不构成攻击面。提示构图完成后先打印边数和节点数对比。如果边数超过节点数的 10 倍大概率要收敛条件而不是继续加大数据量。5.2 CVE 版本匹配不上明明装了受影响版本却匹配不到现象资产管理显示某台主机运行 Apache 2.4.49漏洞库里有对应的 CVE-2021-41773 记录结果生成的图里完全没有出现该漏洞。原因版本字段格式不一致。资产管理里服务名是“Apache httpd”漏洞库里的软件名可能是“httpd”版本号有的带前缀、有的不带匹配时用了严格相等或子串判断一旦“httpd”不在服务字符串中就直接跳过。解决在漏洞匹配前做字段标准化。具体的做法是先把软件名全部转小写、去掉版本号和扩展名统一成规范名版本比较改用 packaging.version 解析多个版本区间交集才算命中。改完之后我在测试集上的匹配率从不到 50% 上升到 90% 以上剩下的基本是信息缺失导致的历史数据质量问题。5.3 生成的攻击路径存在环路路径搜索卡死现象搜索入口到目标主机的路径时程序迟迟不返回CPU 占用 100%等它跑完结果里出现大量来回往返的循环路径。原因有向图存在环攻击者可以利用 A 主机访问 B 主机B 主机又可以通过另一个漏洞访问 A 主机BFS 实现如果没有做全局已访问标记会把这两条边反复遍历无限次。解决给 path_search 里的 visited 增加全局意义。注意 BFS 里 visited 在入队时标记而不是出队时标记可以避免很多重复计算另一个补充手段是给结果去重将路径序列化后放入 set只在无重复时才保留若环路特别多限制 max_depth 才是最直接的兜底手段。5.4 图中节点太多渲染结果根本看不出攻击关系现象生成的 SVG 或 PNG 里有大量节点和边图片缩放到看不清标签想从里面找一条关键路径视觉上完全做不到。原因攻击图把每台主机的每个权限状态都渲染成一个节点再加上每条边上的 CVE 标签整个图的信息密度远超人类可读范围。解决分两层看。分析层使用文本或列表展示路径不做可视化展示层只保留关键路径上的节点和边其余折叠起来。在具体实现上我会先做路径搜索再把所有命中路径的节点集合提取出来只对这个子图进行渲染。这样图片即使有百余个节点也在可读范围内。5.5 扫描结果不及时跑出来的图和当前网络环境对不上现象昨天生成的攻击图显示某主机有高危漏洞今天修复后重新扫描图还是老样子分析结论变得不可信。原因自动化流水线里的扫描结果缓存没有失效机制。攻击图生成器读取的是上一轮的扫描文件而不是本轮新扫描的结果数据源已经过期。解决每个运行周期使用带时间戳的目录存放扫描结果并比较当前时间和数据文件生成时间的间隔超过配置的最大数据年龄就直接触发重新扫描在核心分析结果中输出数据来源的扫描时间让使用者能判断图的新鲜度。这个习惯帮我避免了不止一次基于过期数据的错误判断算是积累下来的血泪经验。6. 进阶验证如何确认生成的攻击图真的可信、可用6.1 用可重现的最小验证集做回归攻击图生成器是安全决策的辅助工具如果它输出的路径是错的误导性比没有工具还大。我常用的验证思路是准备一个最小验证集三台主机 A、B、CA 能访问 BB 能访问 CA 不能直接访问 C但 B 上有一个已知漏洞理论上生成图应该出现 A 到 C 的可达路径且该路径必然经过 B。每次修改源码后运行一遍这个验证集如果路径结果和预期不一致说明构图逻辑出了问题。# tests/test_small_graph.py def test_expected_path_exists(): graph build_attack_graph(SMALL_ASSETS, SMALL_VULNS, SMALL_CONNS, cvss_threshold5.0) paths find_shortest_paths(graph, [A], (C, root), max_depth5) assert any(B in [x[0] for x in path] for path in paths)这段 assert 的判断逻辑是A 到 C 的路径列表只要有一条经过主机 B 就算通过。如果没有通过逐层检查连通性表、漏洞匹配和边生成规则如果通过了再检查路径数量是否异常增多排除边生成过宽的情况。6.2 攻击路径去重与排序回答“最值得关注的热点路径”路径一多分析就无从下手。建议在输出前对路径做三步处理去重、剪枝、排序。去重是把经过的主机序列完全相同的路径合并剪枝是去掉那些包含更多节点且权限状态完全相同的高冗余路径因为它们提供不了新的攻击价值排序按路径长度升序和经过漏洞的平均 CVSS 降序权重综合打分。经过这样处理的攻击图输出不再是几百条原始路径而是几十条有代表性的路径每条路径旁标注了涉及的漏洞编号和评分。分析人员只需要看这些路径即可不需要逐个打开节点验证。攻击图生成器这个方向入门门槛不高但做好很难。写这个工具时最大的体会是每次生成完图先检查边数、再人工抽验一两条边界路径最后看数据来源时间这三步做完心里才有底。也因为这个习惯多次避开了“图上很好看、实际打不通”的假路径误判。希望这套基于 Python 的 attack-graph-generator 实现思路和避坑记录能帮到你让自动化攻击图真正成为可依赖的安全分析工具而不是一个黑匣子。祝好用。本文还有配套的精品资源点击获取