Files
netzwerk_diagramm_scanner/network_scanner.py

533 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Network Scanner - Comprehensive network topology discovery tool
Scans local and VPN networks, gathers device info, routing tables, and generates
structured data for network diagram visualization.
"""
import subprocess
import ipaddress
import json
import re
import socket
import argparse
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@dataclass
class Device:
"""Represents a network device"""
ip: str
hostname: Optional[str] = None
mac: Optional[str] = None
manufacturer: Optional[str] = None
os_type: Optional[str] = None
os_version: Optional[str] = None
device_type: Optional[str] = None # router, switch, server, client, etc.
open_ports: List[int] = None
ssh_accessible: bool = False
services: List[str] = None
routes: List[Dict] = None
interfaces: List[Dict] = None
def __post_init__(self):
if self.open_ports is None:
self.open_ports = []
if self.services is None:
self.services = []
if self.routes is None:
self.routes = []
if self.interfaces is None:
self.interfaces = []
@dataclass
class NetworkSegment:
"""Represents a network segment"""
name: str
cidr: str
gateway: Optional[str] = None
vlan: Optional[int] = None
is_vpn: bool = False
devices: List[Device] = None
def __post_init__(self):
if self.devices is None:
self.devices = []
class NetworkScanner:
"""Main network scanner class"""
def __init__(self, config: Dict):
self.config = config
self.segments: List[NetworkSegment] = []
self.ssh_user = config.get('ssh_user', 'root')
self.ssh_key = config.get('ssh_key_path')
self.timeout = config.get('timeout', 2)
def discover_networks(self) -> List[str]:
"""Discover all network segments from local routing table"""
logger.info("Discovering network segments...")
networks = []
try:
# Get local routing table
result = subprocess.run(
['ip', 'route', 'show'],
capture_output=True,
text=True,
timeout=5
)
for line in result.stdout.splitlines():
# Parse routes like: 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10
match = re.search(r'(\d+\.\d+\.\d+\.\d+/\d+)', line)
if match:
network = match.group(1)
try:
ipaddress.ip_network(network, strict=False)
if network not in networks:
networks.append(network)
logger.info(f"Found network: {network}")
except ValueError:
continue
# Add configured networks
for net in self.config.get('additional_networks', []):
if net not in networks:
networks.append(net)
logger.info(f"Added configured network: {net}")
except Exception as e:
logger.error(f"Error discovering networks: {e}")
return networks
def ping_sweep(self, network: str) -> List[str]:
"""Perform ping sweep to find live hosts"""
logger.info(f"Performing ping sweep on {network}")
live_hosts = []
try:
net = ipaddress.ip_network(network, strict=False)
hosts = list(net.hosts())
# Limit to reasonable size
if len(hosts) > 254:
logger.warning(f"Large network {network}, limiting scan")
hosts = hosts[:254]
with ThreadPoolExecutor(max_workers=50) as executor:
future_to_ip = {
executor.submit(self._ping_host, str(ip)): str(ip)
for ip in hosts
}
for future in as_completed(future_to_ip):
ip = future_to_ip[future]
try:
if future.result():
live_hosts.append(ip)
logger.info(f"Host alive: {ip}")
except Exception as e:
logger.debug(f"Error checking {ip}: {e}")
except Exception as e:
logger.error(f"Error in ping sweep: {e}")
return live_hosts
def _ping_host(self, ip: str) -> bool:
"""Ping a single host"""
try:
result = subprocess.run(
['ping', '-c', '1', '-W', str(self.timeout), ip],
capture_output=True,
timeout=self.timeout + 1
)
return result.returncode == 0
except:
return False
def get_device_info(self, ip: str) -> Device:
"""Gather comprehensive information about a device"""
logger.info(f"Gathering info for {ip}")
device = Device(ip=ip)
# Get hostname
device.hostname = self._get_hostname(ip)
# Get MAC address
device.mac = self._get_mac_address(ip)
# Port scan for common services
device.open_ports = self._scan_common_ports(ip)
# Try SSH access
if 22 in device.open_ports:
device.ssh_accessible = self._test_ssh(ip)
if device.ssh_accessible:
# Gather detailed info via SSH
self._gather_ssh_info(device)
# Identify device type based on ports and services
device.device_type = self._identify_device_type(device)
return device
def _get_hostname(self, ip: str) -> Optional[str]:
"""Get hostname via reverse DNS"""
try:
hostname, _, _ = socket.gethostbyaddr(ip)
return hostname
except:
return None
def _get_mac_address(self, ip: str) -> Optional[str]:
"""Get MAC address from ARP table"""
try:
# First ensure the host is in ARP table
subprocess.run(['ping', '-c', '1', '-W', '1', ip],
capture_output=True, timeout=2)
result = subprocess.run(
['ip', 'neigh', 'show', ip],
capture_output=True,
text=True,
timeout=2
)
match = re.search(r'([0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2})',
result.stdout, re.IGNORECASE)
if match:
return match.group(1).upper()
except:
pass
return None
def _scan_common_ports(self, ip: str) -> List[int]:
"""Scan common ports"""
common_ports = [22, 80, 443, 8080, 8443, 3389, 445, 139, 21, 23, 25, 53, 3306, 5432]
open_ports = []
for port in common_ports:
if self._check_port(ip, port):
open_ports.append(port)
logger.debug(f"{ip}:{port} - OPEN")
return open_ports
def _check_port(self, ip: str, port: int) -> bool:
"""Check if a port is open"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex((ip, port))
sock.close()
return result == 0
except:
return False
def _test_ssh(self, ip: str) -> bool:
"""Test SSH connectivity"""
try:
cmd = ['ssh', '-o', 'ConnectTimeout=3',
'-o', 'StrictHostKeyChecking=no',
'-o', 'BatchMode=yes']
if self.ssh_key:
cmd.extend(['-i', self.ssh_key])
cmd.extend([f'{self.ssh_user}@{ip}', 'echo', 'test'])
result = subprocess.run(cmd, capture_output=True, timeout=5)
return result.returncode == 0
except:
return False
def _gather_ssh_info(self, device: Device):
"""Gather detailed information via SSH"""
logger.info(f"Gathering SSH info from {device.ip}")
# Get OS info
device.os_type, device.os_version = self._get_os_info(device.ip)
# Get network interfaces
device.interfaces = self._get_interfaces(device.ip)
# Get routing table
device.routes = self._get_routes(device.ip)
# Get running services
device.services = self._get_services(device.ip)
def _ssh_exec(self, ip: str, command: str) -> Optional[str]:
"""Execute command via SSH"""
try:
cmd = ['ssh', '-o', 'ConnectTimeout=3',
'-o', 'StrictHostKeyChecking=no',
'-o', 'BatchMode=yes']
if self.ssh_key:
cmd.extend(['-i', self.ssh_key])
cmd.extend([f'{self.ssh_user}@{ip}', command])
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
return result.stdout
except Exception as e:
logger.debug(f"SSH exec error on {ip}: {e}")
return None
def _get_os_info(self, ip: str) -> Tuple[Optional[str], Optional[str]]:
"""Get OS type and version"""
output = self._ssh_exec(ip, 'cat /etc/os-release 2>/dev/null || uname -a')
if output:
if 'NAME=' in output:
match = re.search(r'NAME="?([^"\n]+)"?', output)
name = match.group(1) if match else None
match = re.search(r'VERSION="?([^"\n]+)"?', output)
version = match.group(1) if match else None
return name, version
else:
return 'Unix-like', output.strip()
return None, None
def _get_interfaces(self, ip: str) -> List[Dict]:
"""Get network interfaces"""
interfaces = []
output = self._ssh_exec(ip, 'ip -j addr show 2>/dev/null || ip addr show')
if output:
try:
# Try JSON format first
data = json.loads(output)
for iface in data:
interfaces.append({
'name': iface.get('ifname'),
'state': iface.get('operstate'),
'mac': iface.get('address'),
'addresses': [addr.get('local') for addr in iface.get('addr_info', [])]
})
except json.JSONDecodeError:
# Parse text format
current_iface = None
for line in output.splitlines():
if not line.startswith(' '):
match = re.match(r'\d+: (\S+):', line)
if match:
current_iface = {'name': match.group(1), 'addresses': []}
interfaces.append(current_iface)
elif 'inet ' in line and current_iface:
match = re.search(r'inet (\S+)', line)
if match:
current_iface['addresses'].append(match.group(1))
return interfaces
def _get_routes(self, ip: str) -> List[Dict]:
"""Get routing table"""
routes = []
output = self._ssh_exec(ip, 'ip route show')
if output:
for line in output.splitlines():
route = {'raw': line}
# Parse destination
if line.startswith('default'):
route['destination'] = 'default'
match = re.search(r'via (\S+)', line)
if match:
route['gateway'] = match.group(1)
else:
match = re.match(r'(\S+)', line)
if match:
route['destination'] = match.group(1)
# Parse interface
match = re.search(r'dev (\S+)', line)
if match:
route['interface'] = match.group(1)
routes.append(route)
return routes
def _get_services(self, ip: str) -> List[str]:
"""Get running services"""
services = []
output = self._ssh_exec(ip, 'systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null')
if output:
for line in output.splitlines():
match = re.match(r'(\S+\.service)', line)
if match:
services.append(match.group(1))
return services[:20] # Limit to top 20
def _identify_device_type(self, device: Device) -> str:
"""Identify device type based on available info"""
if device.routes and len(device.routes) > 5:
return 'router'
elif 80 in device.open_ports or 443 in device.open_ports:
if device.hostname and 'pfsense' in device.hostname.lower():
return 'firewall'
return 'server'
elif 3389 in device.open_ports:
return 'windows_client'
elif 22 in device.open_ports:
return 'linux_server'
else:
return 'client'
def scan_network(self, network: str, name: str = None, is_vpn: bool = False) -> NetworkSegment:
"""Scan a complete network segment"""
logger.info(f"Scanning network segment: {network}")
segment = NetworkSegment(
name=name or network,
cidr=network,
is_vpn=is_vpn
)
# Find live hosts
live_hosts = self.ping_sweep(network)
# Gather device info
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_ip = {
executor.submit(self.get_device_info, ip): ip
for ip in live_hosts
}
for future in as_completed(future_to_ip):
try:
device = future.result()
segment.devices.append(device)
except Exception as e:
logger.error(f"Error getting device info: {e}")
return segment
def scan_all(self):
"""Scan all configured networks"""
logger.info("Starting comprehensive network scan...")
# Discover networks
networks = self.discover_networks()
# Scan each network
for network in networks:
try:
segment = self.scan_network(network)
self.segments.append(segment)
except Exception as e:
logger.error(f"Error scanning {network}: {e}")
logger.info(f"Scan complete. Found {len(self.segments)} segments")
def export_json(self, filename: str):
"""Export results to JSON"""
data = {
'scan_timestamp': None, # Will be set by caller
'segments': [
{
'name': seg.name,
'cidr': seg.cidr,
'gateway': seg.gateway,
'is_vpn': seg.is_vpn,
'devices': [asdict(dev) for dev in seg.devices]
}
for seg in self.segments
]
}
with open(filename, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"Exported results to {filename}")
def print_summary(self):
"""Print a human-readable summary"""
print("\n" + "="*80)
print("NETWORK SCAN SUMMARY")
print("="*80)
for segment in self.segments:
print(f"\n📡 Network: {segment.name} ({segment.cidr})")
print(f" Devices found: {len(segment.devices)}")
for device in segment.devices:
print(f"\n 🖥️ {device.ip}")
if device.hostname:
print(f" Hostname: {device.hostname}")
if device.mac:
print(f" MAC: {device.mac}")
if device.device_type:
print(f" Type: {device.device_type}")
if device.os_type:
print(f" OS: {device.os_type} {device.os_version or ''}")
if device.open_ports:
print(f" Open Ports: {', '.join(map(str, device.open_ports))}")
if device.ssh_accessible:
print(f" ✓ SSH Accessible")
if device.routes:
print(f" Routes: {len(device.routes)}")
if device.interfaces:
print(f" Interfaces: {len(device.interfaces)}")
def main():
parser = argparse.ArgumentParser(description='Network Scanner for Diagram Generation')
parser.add_argument('-c', '--config', default='config.json',
help='Configuration file (default: config.json)')
parser.add_argument('-o', '--output', default='network_scan.json',
help='Output JSON file (default: network_scan.json)')
parser.add_argument('-v', '--verbose', action='store_true',
help='Verbose output')
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
# Load configuration
try:
with open(args.config, 'r') as f:
config = json.load(f)
except FileNotFoundError:
logger.warning(f"Config file {args.config} not found, using defaults")
config = {}
# Run scanner
scanner = NetworkScanner(config)
scanner.scan_all()
# Print summary
scanner.print_summary()
# Export results
from datetime import datetime
scanner.export_json(args.output)
print(f"\n✓ Scan complete! Results saved to {args.output}")
if __name__ == '__main__':
main()