354 lines
12 KiB
Python
Executable File
354 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
SVG Network Diagram Generator
|
|
Converts network scan results into an SVG network diagram
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
from typing import Dict, List, Tuple
|
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
from xml.dom import minidom
|
|
|
|
|
|
class NetworkDiagramGenerator:
|
|
"""Generate SVG diagrams from network scan data"""
|
|
|
|
# Device type styling
|
|
DEVICE_STYLES = {
|
|
'router': {'color': '#FF6B6B', 'icon': '🔀', 'shape': 'rect'},
|
|
'firewall': {'color': '#FF0000', 'icon': '🛡️', 'shape': 'rect'},
|
|
'switch': {'color': '#4ECDC4', 'icon': '🔌', 'shape': 'rect'},
|
|
'server': {'color': '#45B7D1', 'icon': '🖥️', 'shape': 'rect'},
|
|
'linux_server': {'color': '#45B7D1', 'icon': '🐧', 'shape': 'rect'},
|
|
'windows_client': {'color': '#95E1D3', 'icon': '💻', 'shape': 'rect'},
|
|
'client': {'color': '#A8E6CF', 'icon': '💻', 'shape': 'rect'},
|
|
'default': {'color': '#CCCCCC', 'icon': '❓', 'shape': 'rect'}
|
|
}
|
|
|
|
# Network segment colors
|
|
SEGMENT_COLORS = [
|
|
'#E3F2FD', '#F3E5F5', '#E8F5E9', '#FFF3E0', '#FCE4EC',
|
|
'#E0F2F1', '#F1F8E9', '#FFF9C4', '#FFE0B2', '#F8BBD0'
|
|
]
|
|
|
|
def __init__(self, scan_data: Dict):
|
|
self.scan_data = scan_data
|
|
self.width = 1600
|
|
self.height = 1200
|
|
self.margin = 50
|
|
self.device_width = 120
|
|
self.device_height = 80
|
|
self.segment_spacing = 200
|
|
|
|
def generate_svg(self, output_file: str):
|
|
"""Generate SVG diagram"""
|
|
svg = Element('svg', {
|
|
'width': str(self.width),
|
|
'height': str(self.height),
|
|
'xmlns': 'http://www.w3.org/2000/svg',
|
|
'version': '1.1'
|
|
})
|
|
|
|
# Add styles
|
|
self._add_styles(svg)
|
|
|
|
# Add title
|
|
self._add_title(svg)
|
|
|
|
# Calculate layout
|
|
segments = self.scan_data.get('segments', [])
|
|
layout = self._calculate_layout(segments)
|
|
|
|
# Draw network segments
|
|
for i, (segment, positions) in enumerate(zip(segments, layout)):
|
|
self._draw_segment(svg, segment, positions, i)
|
|
|
|
# Draw connections
|
|
self._draw_connections(svg, segments, layout)
|
|
|
|
# Add legend
|
|
self._add_legend(svg)
|
|
|
|
# Save to file
|
|
self._save_svg(svg, output_file)
|
|
|
|
def _add_styles(self, svg: Element):
|
|
"""Add CSS styles"""
|
|
style = SubElement(svg, 'style')
|
|
style.text = """
|
|
.device-box { stroke: #333; stroke-width: 2; }
|
|
.device-label { font-family: Arial, sans-serif; font-size: 12px; fill: #000; }
|
|
.device-icon { font-size: 24px; }
|
|
.segment-box { stroke: #666; stroke-width: 2; fill-opacity: 0.1; }
|
|
.segment-label { font-family: Arial, sans-serif; font-size: 14px; font-weight: bold; fill: #333; }
|
|
.connection { stroke: #999; stroke-width: 2; stroke-dasharray: 5,5; }
|
|
.title { font-family: Arial, sans-serif; font-size: 24px; font-weight: bold; fill: #333; }
|
|
.info-text { font-family: Arial, sans-serif; font-size: 10px; fill: #666; }
|
|
.legend-text { font-family: Arial, sans-serif; font-size: 11px; fill: #333; }
|
|
"""
|
|
|
|
def _add_title(self, svg: Element):
|
|
"""Add diagram title"""
|
|
title = SubElement(svg, 'text', {
|
|
'x': str(self.width // 2),
|
|
'y': '30',
|
|
'text-anchor': 'middle',
|
|
'class': 'title'
|
|
})
|
|
title.text = 'Network Topology Diagram'
|
|
|
|
def _calculate_layout(self, segments: List[Dict]) -> List[List[Tuple]]:
|
|
"""Calculate positions for all devices"""
|
|
layout = []
|
|
|
|
# Arrange segments horizontally
|
|
segment_width = (self.width - 2 * self.margin) / len(segments) if segments else self.width
|
|
|
|
for seg_idx, segment in enumerate(segments):
|
|
devices = segment.get('devices', [])
|
|
positions = []
|
|
|
|
# Calculate segment area
|
|
seg_x = self.margin + seg_idx * segment_width
|
|
seg_y = self.margin + 60
|
|
seg_w = segment_width - 20
|
|
seg_h = self.height - seg_y - self.margin
|
|
|
|
# Arrange devices in a grid within segment
|
|
cols = math.ceil(math.sqrt(len(devices)))
|
|
rows = math.ceil(len(devices) / cols) if cols > 0 else 1
|
|
|
|
device_spacing_x = seg_w / (cols + 1) if cols > 0 else seg_w / 2
|
|
device_spacing_y = seg_h / (rows + 1) if rows > 0 else seg_h / 2
|
|
|
|
for dev_idx, device in enumerate(devices):
|
|
col = dev_idx % cols
|
|
row = dev_idx // cols
|
|
|
|
x = seg_x + (col + 1) * device_spacing_x
|
|
y = seg_y + (row + 1) * device_spacing_y
|
|
|
|
positions.append((x, y, device))
|
|
|
|
layout.append(positions)
|
|
|
|
return layout
|
|
|
|
def _draw_segment(self, svg: Element, segment: Dict, positions: List[Tuple], seg_idx: int):
|
|
"""Draw a network segment and its devices"""
|
|
if not positions:
|
|
return
|
|
|
|
# Calculate segment bounds
|
|
xs = [pos[0] for pos in positions]
|
|
ys = [pos[1] for pos in positions]
|
|
|
|
min_x = min(xs) - self.device_width
|
|
max_x = max(xs) + self.device_width
|
|
min_y = min(ys) - self.device_height
|
|
max_y = max(ys) + self.device_height
|
|
|
|
# Draw segment background
|
|
color = self.SEGMENT_COLORS[seg_idx % len(self.SEGMENT_COLORS)]
|
|
SubElement(svg, 'rect', {
|
|
'x': str(min_x - 20),
|
|
'y': str(min_y - 40),
|
|
'width': str(max_x - min_x + 40),
|
|
'height': str(max_y - min_y + 60),
|
|
'fill': color,
|
|
'class': 'segment-box',
|
|
'rx': '10'
|
|
})
|
|
|
|
# Draw segment label
|
|
label = SubElement(svg, 'text', {
|
|
'x': str(min_x),
|
|
'y': str(min_y - 20),
|
|
'class': 'segment-label'
|
|
})
|
|
vpn_indicator = ' (VPN)' if segment.get('is_vpn') else ''
|
|
label.text = f"{segment.get('name', 'Unknown')} - {segment.get('cidr', '')}{vpn_indicator}"
|
|
|
|
# Draw devices
|
|
for x, y, device in positions:
|
|
self._draw_device(svg, device, x, y)
|
|
|
|
def _draw_device(self, svg: Element, device: Dict, x: float, y: float):
|
|
"""Draw a single device"""
|
|
device_type = device.get('device_type', 'default')
|
|
style = self.DEVICE_STYLES.get(device_type, self.DEVICE_STYLES['default'])
|
|
|
|
# Draw device box
|
|
box_x = x - self.device_width / 2
|
|
box_y = y - self.device_height / 2
|
|
|
|
SubElement(svg, 'rect', {
|
|
'x': str(box_x),
|
|
'y': str(box_y),
|
|
'width': str(self.device_width),
|
|
'height': str(self.device_height),
|
|
'fill': style['color'],
|
|
'class': 'device-box',
|
|
'rx': '5'
|
|
})
|
|
|
|
# Add icon
|
|
icon_text = SubElement(svg, 'text', {
|
|
'x': str(x),
|
|
'y': str(y - 15),
|
|
'text-anchor': 'middle',
|
|
'class': 'device-icon'
|
|
})
|
|
icon_text.text = style['icon']
|
|
|
|
# Add IP address
|
|
ip_text = SubElement(svg, 'text', {
|
|
'x': str(x),
|
|
'y': str(y + 5),
|
|
'text-anchor': 'middle',
|
|
'class': 'device-label',
|
|
'font-weight': 'bold'
|
|
})
|
|
ip_text.text = device.get('ip', 'Unknown')
|
|
|
|
# Add hostname
|
|
hostname = device.get('hostname', '')
|
|
if hostname:
|
|
hostname_text = SubElement(svg, 'text', {
|
|
'x': str(x),
|
|
'y': str(y + 20),
|
|
'text-anchor': 'middle',
|
|
'class': 'info-text'
|
|
})
|
|
# Truncate long hostnames
|
|
if len(hostname) > 20:
|
|
hostname = hostname[:17] + '...'
|
|
hostname_text.text = hostname
|
|
|
|
# Add additional info
|
|
info_y = y + 32
|
|
if device.get('ssh_accessible'):
|
|
ssh_text = SubElement(svg, 'text', {
|
|
'x': str(x),
|
|
'y': str(info_y),
|
|
'text-anchor': 'middle',
|
|
'class': 'info-text',
|
|
'fill': 'green'
|
|
})
|
|
ssh_text.text = '🔓 SSH'
|
|
|
|
def _draw_connections(self, svg: Element, segments: List[Dict], layout: List[List[Tuple]]):
|
|
"""Draw connections between devices based on routing info"""
|
|
# This is simplified - you'd analyze routing tables to determine actual connections
|
|
|
|
# For now, connect routers between segments
|
|
routers = []
|
|
for positions in layout:
|
|
for x, y, device in positions:
|
|
if device.get('device_type') in ['router', 'firewall']:
|
|
routers.append((x, y, device))
|
|
|
|
# Connect consecutive routers
|
|
for i in range(len(routers) - 1):
|
|
x1, y1, _ = routers[i]
|
|
x2, y2, _ = routers[i + 1]
|
|
|
|
SubElement(svg, 'line', {
|
|
'x1': str(x1),
|
|
'y1': str(y1),
|
|
'x2': str(x2),
|
|
'y2': str(y2),
|
|
'class': 'connection'
|
|
})
|
|
|
|
def _add_legend(self, svg: Element):
|
|
"""Add legend explaining device types"""
|
|
legend_x = self.width - 200
|
|
legend_y = self.height - 200
|
|
|
|
# Legend box
|
|
SubElement(svg, 'rect', {
|
|
'x': str(legend_x - 10),
|
|
'y': str(legend_y - 10),
|
|
'width': '190',
|
|
'height': str(len(self.DEVICE_STYLES) * 25 + 30),
|
|
'fill': 'white',
|
|
'stroke': '#333',
|
|
'stroke-width': '1',
|
|
'rx': '5'
|
|
})
|
|
|
|
# Legend title
|
|
title = SubElement(svg, 'text', {
|
|
'x': str(legend_x),
|
|
'y': str(legend_y + 5),
|
|
'class': 'legend-text',
|
|
'font-weight': 'bold'
|
|
})
|
|
title.text = 'Device Types'
|
|
|
|
# Legend items
|
|
y_offset = 25
|
|
for device_type, style in self.DEVICE_STYLES.items():
|
|
if device_type == 'default':
|
|
continue
|
|
|
|
# Color box
|
|
SubElement(svg, 'rect', {
|
|
'x': str(legend_x),
|
|
'y': str(legend_y + y_offset - 8),
|
|
'width': '15',
|
|
'height': '15',
|
|
'fill': style['color'],
|
|
'stroke': '#333'
|
|
})
|
|
|
|
# Label
|
|
label = SubElement(svg, 'text', {
|
|
'x': str(legend_x + 20),
|
|
'y': str(legend_y + y_offset + 4),
|
|
'class': 'legend-text'
|
|
})
|
|
label.text = f"{style['icon']} {device_type.replace('_', ' ').title()}"
|
|
|
|
y_offset += 25
|
|
|
|
def _save_svg(self, svg: Element, output_file: str):
|
|
"""Save SVG to file with pretty formatting"""
|
|
rough_string = tostring(svg, encoding='unicode')
|
|
reparsed = minidom.parseString(rough_string)
|
|
pretty_svg = reparsed.toprettyxml(indent=' ')
|
|
|
|
# Remove extra blank lines
|
|
pretty_svg = '\n'.join([line for line in pretty_svg.split('\n') if line.strip()])
|
|
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
f.write(pretty_svg)
|
|
|
|
|
|
def main():
|
|
"""Command line interface"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description='Generate SVG network diagram from scan data')
|
|
parser.add_argument('input', help='Input JSON file from network scan')
|
|
parser.add_argument('-o', '--output', default='network_diagram.svg',
|
|
help='Output SVG file (default: network_diagram.svg)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Load scan data
|
|
with open(args.input, 'r') as f:
|
|
scan_data = json.load(f)
|
|
|
|
# Generate diagram
|
|
generator = NetworkDiagramGenerator(scan_data)
|
|
generator.generate_svg(args.output)
|
|
|
|
print(f"✓ Network diagram generated: {args.output}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|