Files
netzwerk_diagramm_scanner/svg_generator.py

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()