在渗透测试或网络资产梳理时,Nmap是使用频率极高的扫描工具。它输出的XML格式报告包含了主机状态、开放端口、服务指纹、操作系统猜测等丰富信息,但XML本身的可读性并不好,尤其在目标数量较多时,靠人工逐行分析非常耗时。借助Node.js,我们可以把这份结构化的扫描结果转换成直观的图像,比如网络拓扑图、端口热点图或者服务分布图。这样既能快速了解整个网段的存活情况,也方便在报告中呈现攻击面。

解析Nmap的XML输出
想让扫描结果变成图像,第一步是把Nmap生成的XML报告解析成程序可读的数据结构。使用nmap -oX scan.xml 192.168.1.0/24命令可以得到一份完整的XML文件,里面包含<nmaprun>根节点,以及每个主机的<host>节点。每个<host>节点下通常有<address>、<status>、<ports>等子节点,分别记录IP地址、存活状态和端口信息。
在Node.js中解析XML,推荐使用xml2js库。它能把XML转换成JavaScript对象,并且支持Promise风格的异步解析。安装命令是npm install xml2js。下面的代码演示了如何读取扫描结果并提取主机列表、开放端口和服务名称。注意Nmap的XML中有些字段可能重复出现,例如一个主机可能有多个地址,所以解析时需要判断是数组还是单个对象。
const fs = require('fs');
const xml2js = require('xml2js');
async function parseNmapXml(filePath) {
const xml = fs.readFileSync(filePath, 'utf8');
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(xml);
const hosts = result.nmaprun.host || [];
const devices = [];
for (const host of hosts) {
const address = host.address;
const ip = Array.isArray(address)
? address.find(a => a.$ && a.$.addrtype === 'ipv4')?.$.addr
: address?.$.addr;
const status = host.status?.$.state;
const ports = [];
if (host.ports && host.ports.port) {
const portList = Array.isArray(host.ports.port) ? host.ports.port : [host.ports.port];
for (const p of portList) {
ports.push({
port: p.$.portid,
protocol: p.$.protocol,
state: p.state?.$.state,
service: p.service?.$.name
});
}
}
devices.push({ ip, status, ports });
}
return devices;
}
这段代码会返回一个设备数组,每个设备包含IP、存活状态和端口列表。后续绘图时,我们可以根据状态字段决定节点颜色,根据端口列表生成端口分布图。如果扫描时启用了-sV服务识别,还能拿到服务名称和版本,这些信息可以作为节点的标签或图例。
设计可视化数据模型
解析完成后,原始数据还不能直接用于绘图。为了画出网络拓扑或者端口矩阵,需要先定义一套适合图像生成的中间数据结构。通常把每台主机看作一个节点,开放端口看作该节点上的属性;如果要画主机之间的连接关系,则需要额外的边数据。对于Nmap扫描结果,最常见的可视化形式有两种:一种是按IP段排列的主机存活状态图,另一种是按端口维度汇总的服务分布图。
以主机存活状态图为例,我们可以建立一个二维网格模型:横轴表示IP地址的最后一段,纵轴表示主机组或子网,每个格子根据主机是否存活填充不同颜色。这样一张图就能直观反映整个网段的在线情况。数据结构可以设计成下面这样:
const visualModel = {
gridWidth: 16,
nodes: [
{ ip: '192.168.1.1', status: 'up', ports: [22, 80, 443] },
{ ip: '192.168.1.2', status: 'down', ports: [] }
]
};
这种模型的好处是容易映射到Canvas的像素坐标,计算量小,即使主机数量达到几百台也能快速渲染。如果还想展示端口信息,可以给每个主机节点附加一个端口数组,并在图例中标注不同颜色对应的服务类型。
使用Canvas生成图像
Node.js本身没有DOM,但可以通过node-canvas这个库来模拟Canvas API。先安装npm install canvas,然后创建画布、设置宽高、绘制矩形和文字,最后保存为PNG文件。下面的示例展示了如何根据上面设计的数据模型绘制一张简单的存活状态图。
const { createCanvas } = require('canvas');
const fs = require('fs');
function drawHostStatus(devices, outputPath) {
const cellWidth = 30;
const cellHeight = 20;
const margin = 40;
const cols = 16; // 每个子网最多16个主机
const rows = Math.ceil(devices.length / cols);
const canvas = createCanvas(margin * 2 + cols * cellWidth, margin * 2 + rows * cellHeight);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
devices.forEach((device, index) => {
const col = index % cols;
const row = Math.floor(index / cols);
const x = margin + col * cellWidth;
const y = margin + row * cellHeight;
ctx.fillStyle = device.status === 'up' ? '#4caf50' : '#f44336';
ctx.fillRect(x, y, cellWidth - 2, cellHeight - 2);
ctx.fillStyle = '#000';
ctx.font = '10px sans-serif';
ctx.fillText(device.ip.split('.')[3], x + 2, y + 14);
});
const buffer = canvas.toBuffer('image/png');
fs.writeFileSync(outputPath, buffer);
}
代码中先根据主机数量计算画布宽度,然后遍历设备数组,按IP最后一段定位横坐标,用绿色表示存活、红色表示离线或过滤。绘制完成后调用fs.writeFileSync把PNG写入磁盘。这种方式生成的是位图,适合直接插入报告;如果希望图像可以无损缩放,也可以选择生成SVG,思路类似但使用的是字符串拼接。
除了存活状态图,端口分布图也可以采用类似的思路:以端口号为横轴,主机为纵轴,每个开放端口绘制一个色块。这样能快速发现哪些端口在多个主机上同时开放,对发现内网常见服务很有帮助。
处理大量主机与性能优化
当扫描目标是一个B段甚至更大网段时,解析和绘图都可能遇到性能问题。XML文件可能达到几十MB,一次性读入内存并转换成对象会占用大量资源。此时可以使用流式解析器,例如sax或xml-stream,边读边处理主机节点,避免内存峰值过高。
绘图方面,如果主机数量超过一千,直接绘制每个节点会让图像变得拥挤。可以考虑聚合策略:按子网或IP段分组,用热力图表示密度;或者只绘制存活主机和开放了关键端口的主机。另外,Canvas的绘制操作是同步的,大量绘制会阻塞事件循环,建议将绘图函数拆分成异步任务,或者在数据准备好后一次性批量绘制。
实际使用中,还可以把解析和绘图分成两个独立模块,解析结果缓存为JSON文件,绘图时直接读取JSON,这样重复生成图像就不需要重新解析XML。如果对接Web界面,可以用Express提供一个简单的API,上传Nmap报告后返回生成的图片地址,实现自动化。

