#!/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()