Add comprehensive network mapper and workflow script
- comprehensive_mapper.py: Combines network scanning with pfSense XML parsing - run_network_mapping.sh: Complete workflow script for network discovery - Successfully tested with both pfSense XML files and live network scan - Generates comprehensive JSON data and SVG network diagrams - Includes WireGuard VPN topology, static routes, and DHCP mappings
This commit is contained in:
355
comprehensive_mapper.py
Executable file
355
comprehensive_mapper.py
Executable file
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Network Diagram Generator
|
||||
Combines network scanning with pfSense XML parsing for complete network topology
|
||||
"""
|
||||
|
||||
import json
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
from network_scanner import NetworkScanner
|
||||
from pfsense_xml_parser import PfSenseXMLParser
|
||||
from svg_generator import NetworkDiagramGenerator
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComprehensiveNetworkMapper:
|
||||
"""Complete network mapping solution combining multiple data sources"""
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
self.config = config
|
||||
self.network_data = {}
|
||||
self.pfsense_data = {}
|
||||
self.combined_data = {
|
||||
'scan_timestamp': None,
|
||||
'data_sources': [],
|
||||
'segments': [],
|
||||
'pfsense_firewalls': [],
|
||||
'wireguard_networks': [],
|
||||
'openvpn_networks': [],
|
||||
'routing_table': [],
|
||||
'dhcp_leases': [],
|
||||
'static_mappings': []
|
||||
}
|
||||
|
||||
def load_network_scan(self, scan_file: str):
|
||||
"""Load network scan data"""
|
||||
logger.info(f"Loading network scan data from {scan_file}")
|
||||
try:
|
||||
with open(scan_file, 'r') as f:
|
||||
self.network_data = json.load(f)
|
||||
self.combined_data['data_sources'].append('network_scan')
|
||||
logger.info("Network scan data loaded successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading network scan: {e}")
|
||||
|
||||
def load_pfsense_configs(self, xml_files: List[str]):
|
||||
"""Load and parse pfSense XML configuration files"""
|
||||
for xml_file in xml_files:
|
||||
if Path(xml_file).exists():
|
||||
logger.info(f"Parsing pfSense config: {xml_file}")
|
||||
parser = PfSenseXMLParser(xml_file)
|
||||
data = parser.parse_all()
|
||||
|
||||
if data:
|
||||
hostname = data.get('hostname', f'pfsense_{len(self.pfsense_data)}')
|
||||
self.pfsense_data[hostname] = data
|
||||
self.combined_data['data_sources'].append(f'pfsense_{hostname}')
|
||||
self.combined_data['pfsense_firewalls'].append(data)
|
||||
logger.info(f"pfSense config {hostname} loaded successfully")
|
||||
else:
|
||||
logger.error(f"Failed to parse pfSense config: {xml_file}")
|
||||
else:
|
||||
logger.warning(f"pfSense config file not found: {xml_file}")
|
||||
|
||||
def merge_network_data(self):
|
||||
"""Merge all data sources into comprehensive network map"""
|
||||
logger.info("Merging network data sources...")
|
||||
|
||||
# Start with network scan data if available
|
||||
if self.network_data:
|
||||
self.combined_data.update({
|
||||
'scan_timestamp': self.network_data.get('scan_timestamp'),
|
||||
'segments': self.network_data.get('segments', [])
|
||||
})
|
||||
|
||||
# Process pfSense data
|
||||
self._process_pfsense_data()
|
||||
|
||||
# Extract additional network information
|
||||
self._extract_network_topology()
|
||||
|
||||
logger.info("Network data merging complete")
|
||||
|
||||
def _process_pfsense_data(self):
|
||||
"""Process and integrate pfSense configuration data"""
|
||||
for hostname, pfsense in self.pfsense_data.items():
|
||||
logger.info(f"Processing pfSense data for {hostname}")
|
||||
|
||||
# Add interfaces as network segments
|
||||
interfaces = pfsense.get('interfaces', {})
|
||||
for iface_name, iface_data in interfaces.items():
|
||||
if iface_data.get('ipaddr') and iface_data.get('ipaddr') != 'dhcp':
|
||||
try:
|
||||
# Create network segment from interface
|
||||
ip = iface_data['ipaddr']
|
||||
subnet = iface_data.get('subnet', '24')
|
||||
network_cidr = f"{ip.rsplit('.', 1)[0]}.0/{subnet}"
|
||||
|
||||
segment = {
|
||||
'name': f"{hostname}_{iface_name}",
|
||||
'cidr': network_cidr,
|
||||
'gateway': iface_data.get('gateway', ip),
|
||||
'interface': iface_data.get('interface'),
|
||||
'pfsense_host': hostname,
|
||||
'is_vpn': iface_data.get('type') in ['wireguard', 'openvpn'],
|
||||
'devices': []
|
||||
}
|
||||
|
||||
# Add gateway device
|
||||
gateway_device = {
|
||||
'ip': ip,
|
||||
'hostname': hostname,
|
||||
'device_type': 'firewall',
|
||||
'os_type': 'pfSense',
|
||||
'interface': iface_name,
|
||||
'pfsense_config': True
|
||||
}
|
||||
segment['devices'].append(gateway_device)
|
||||
|
||||
self.combined_data['segments'].append(segment)
|
||||
|
||||
# Track VPN networks
|
||||
if iface_data.get('type') == 'wireguard':
|
||||
self.combined_data['wireguard_networks'].append({
|
||||
'network': network_cidr,
|
||||
'interface': iface_data.get('interface'),
|
||||
'pfsense': hostname,
|
||||
'peers': pfsense.get('wireguard', {}).get('peers', [])
|
||||
})
|
||||
elif iface_data.get('type') == 'openvpn':
|
||||
self.combined_data['openvpn_networks'].append({
|
||||
'network': network_cidr,
|
||||
'interface': iface_data.get('interface'),
|
||||
'pfsense': hostname
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error processing interface {iface_name}: {e}")
|
||||
|
||||
# Add static routes
|
||||
static_routes = pfsense.get('static_routes', [])
|
||||
for route in static_routes:
|
||||
route_info = {
|
||||
'network': route.get('network'),
|
||||
'gateway': route.get('gateway'),
|
||||
'description': route.get('description'),
|
||||
'pfsense': hostname
|
||||
}
|
||||
self.combined_data['routing_table'].append(route_info)
|
||||
|
||||
# Add DHCP information
|
||||
dhcp_config = pfsense.get('dhcp', {})
|
||||
for iface_name, dhcp_data in dhcp_config.items():
|
||||
if dhcp_data.get('static_mappings'):
|
||||
for mapping in dhcp_data['static_mappings']:
|
||||
mapping_info = {
|
||||
'ip': mapping.get('ipaddr'),
|
||||
'mac': mapping.get('mac'),
|
||||
'hostname': mapping.get('hostname'),
|
||||
'description': mapping.get('description'),
|
||||
'interface': iface_name,
|
||||
'pfsense': hostname
|
||||
}
|
||||
self.combined_data['static_mappings'].append(mapping_info)
|
||||
|
||||
def _extract_network_topology(self):
|
||||
"""Extract network topology information from all sources"""
|
||||
logger.info("Extracting network topology...")
|
||||
|
||||
# Process WireGuard peer information
|
||||
for wg_net in self.combined_data['wireguard_networks']:
|
||||
for peer in wg_net.get('peers', []):
|
||||
for allowed_ip in peer.get('allowed_ips', []):
|
||||
# Create segment for remote networks
|
||||
if allowed_ip.get('mask') and allowed_ip['mask'] != '32':
|
||||
remote_segment = {
|
||||
'name': f"WG_Remote_{allowed_ip['address']}_{allowed_ip['mask']}",
|
||||
'cidr': f"{allowed_ip['address']}/{allowed_ip['mask']}",
|
||||
'gateway': wg_net.get('network', '').split('/')[0].rsplit('.', 1)[0] + '.1',
|
||||
'is_vpn': True,
|
||||
'vpn_type': 'wireguard',
|
||||
'pfsense_host': wg_net.get('pfsense'),
|
||||
'devices': []
|
||||
}
|
||||
self.combined_data['segments'].append(remote_segment)
|
||||
|
||||
def generate_svg_diagram(self, output_file: str):
|
||||
"""Generate SVG network diagram"""
|
||||
logger.info(f"Generating SVG diagram: {output_file}")
|
||||
|
||||
# Use the existing SVG generator
|
||||
generator = NetworkDiagramGenerator(self.combined_data)
|
||||
generator.generate_svg(output_file)
|
||||
|
||||
logger.info(f"SVG diagram generated: {output_file}")
|
||||
|
||||
def export_comprehensive_json(self, output_file: str):
|
||||
"""Export comprehensive network data"""
|
||||
logger.info(f"Exporting comprehensive data to {output_file}")
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.combined_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Comprehensive network data exported to {output_file}")
|
||||
|
||||
def print_summary(self):
|
||||
"""Print comprehensive network summary"""
|
||||
print("\n" + "="*80)
|
||||
print("COMPREHENSIVE NETWORK MAPPING SUMMARY")
|
||||
print("="*80)
|
||||
|
||||
print(f"\nData Sources: {', '.join(self.combined_data['data_sources'])}")
|
||||
print(f"Network Segments: {len(self.combined_data['segments'])}")
|
||||
print(f"pfSense Firewalls: {len(self.combined_data['pfsense_firewalls'])}")
|
||||
print(f"WireGuard Networks: {len(self.combined_data['wireguard_networks'])}")
|
||||
print(f"OpenVPN Networks: {len(self.combined_data['openvpn_networks'])}")
|
||||
print(f"Static Routes: {len(self.combined_data['routing_table'])}")
|
||||
print(f"DHCP Static Mappings: {len(self.combined_data['static_mappings'])}")
|
||||
|
||||
# Network segments summary
|
||||
print(f"\n{'='*80}")
|
||||
print("NETWORK SEGMENTS")
|
||||
print(f"{'='*80}")
|
||||
|
||||
for segment in self.combined_data['segments']:
|
||||
vpn_indicator = " (VPN)" if segment.get('is_vpn') else ""
|
||||
pfsense_indicator = f" [{segment.get('pfsense_host')}]" if segment.get('pfsense_host') else ""
|
||||
print(f"\n📡 {segment['name']}{vpn_indicator}{pfsense_indicator}")
|
||||
print(f" CIDR: {segment['cidr']}")
|
||||
print(f" Gateway: {segment.get('gateway', 'N/A')}")
|
||||
print(f" Devices: {len(segment.get('devices', []))}")
|
||||
|
||||
# Show devices
|
||||
for device in segment.get('devices', [])[:5]: # Show first 5
|
||||
print(f" • {device.get('ip', 'N/A')} - {device.get('hostname', 'N/A')} ({device.get('device_type', 'N/A')})")
|
||||
|
||||
if len(segment.get('devices', [])) > 5:
|
||||
print(f" ... and {len(segment.get('devices', [])) - 5} more devices")
|
||||
|
||||
# pfSense summary
|
||||
if self.combined_data['pfsense_firewalls']:
|
||||
print(f"\n{'='*80}")
|
||||
print("PFSENSE FIREWALLS")
|
||||
print(f"{'='*80}")
|
||||
|
||||
for pfsense in self.combined_data['pfsense_firewalls']:
|
||||
hostname = pfsense.get('hostname', 'Unknown')
|
||||
print(f"\n🛡️ {hostname}")
|
||||
print(f" Interfaces: {len(pfsense.get('interfaces', {}))}")
|
||||
print(f" Static Routes: {len(pfsense.get('static_routes', []))}")
|
||||
|
||||
wg_config = pfsense.get('wireguard', {})
|
||||
if wg_config.get('enabled'):
|
||||
print(f" WireGuard: {len(wg_config.get('tunnels', []))} tunnels, {len(wg_config.get('peers', []))} peers")
|
||||
|
||||
dhcp_config = pfsense.get('dhcp', {})
|
||||
total_mappings = sum(len(dhcp.get('static_mappings', [])) for dhcp in dhcp_config.values())
|
||||
print(f" DHCP Static Mappings: {total_mappings}")
|
||||
|
||||
# VPN Networks
|
||||
if self.combined_data['wireguard_networks'] or self.combined_data['openvpn_networks']:
|
||||
print(f"\n{'='*80}")
|
||||
print("VPN NETWORKS")
|
||||
print(f"{'='*80}")
|
||||
|
||||
for wg_net in self.combined_data['wireguard_networks']:
|
||||
print(f"\n🔐 WireGuard: {wg_net['network']} via {wg_net['pfsense']}")
|
||||
print(f" Interface: {wg_net['interface']}")
|
||||
print(f" Peers: {len(wg_net.get('peers', []))}")
|
||||
|
||||
for ovpn_net in self.combined_data['openvpn_networks']:
|
||||
print(f"\n🔐 OpenVPN: {ovpn_net['network']} via {ovpn_net['pfsense']}")
|
||||
print(f" Interface: {ovpn_net['interface']}")
|
||||
|
||||
print(f"\n{'='*80}")
|
||||
print("NETWORK TOPOLOGY ANALYSIS COMPLETE")
|
||||
print(f"{'='*80}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Command line interface"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Comprehensive Network Diagram Generator with pfSense Integration'
|
||||
)
|
||||
parser.add_argument('-c', '--config', default='config.json',
|
||||
help='Network scanner configuration file')
|
||||
parser.add_argument('-s', '--scan-data', help='Existing network scan JSON file')
|
||||
parser.add_argument('-p', '--pfsense-xml', nargs='+',
|
||||
help='pfSense XML configuration files')
|
||||
parser.add_argument('-o', '--output', default='comprehensive_network.json',
|
||||
help='Output comprehensive JSON file')
|
||||
parser.add_argument('--svg', help='Generate SVG diagram')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='Verbose output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
# Load configuration
|
||||
config = {}
|
||||
if Path(args.config).exists():
|
||||
try:
|
||||
with open(args.config, 'r') as f:
|
||||
config = json.load(f)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load config {args.config}: {e}")
|
||||
|
||||
# Initialize comprehensive mapper
|
||||
mapper = ComprehensiveNetworkMapper(config)
|
||||
|
||||
# Load network scan data if provided
|
||||
if args.scan_data and Path(args.scan_data).exists():
|
||||
mapper.load_network_scan(args.scan_data)
|
||||
else:
|
||||
logger.info("No network scan data provided - will use pfSense data only")
|
||||
|
||||
# Load pfSense configurations
|
||||
pfsense_files = args.pfsense_xml or []
|
||||
# Auto-detect pfSense XML files if none specified
|
||||
if not pfsense_files:
|
||||
xml_files = list(Path('.').glob('config-*.xml'))
|
||||
if xml_files:
|
||||
pfsense_files = [str(f) for f in xml_files]
|
||||
logger.info(f"Auto-detected pfSense XML files: {pfsense_files}")
|
||||
|
||||
if pfsense_files:
|
||||
mapper.load_pfsense_configs(pfsense_files)
|
||||
else:
|
||||
logger.warning("No pfSense XML files found")
|
||||
|
||||
# Merge all data
|
||||
mapper.merge_network_data()
|
||||
|
||||
# Print summary
|
||||
mapper.print_summary()
|
||||
|
||||
# Export comprehensive data
|
||||
mapper.export_comprehensive_json(args.output)
|
||||
print(f"\n✅ Comprehensive network data saved to: {args.output}")
|
||||
|
||||
# Generate SVG if requested
|
||||
if args.svg:
|
||||
svg_file = args.svg
|
||||
mapper.generate_svg_diagram(svg_file)
|
||||
print(f"✅ SVG network diagram generated: {svg_file}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user