#!/usr/bin/env python3 """ Simple ARP Network Scanner Discovers devices on local network using ARP protocol """ import subprocess import re import json import time from datetime import datetime from pathlib import Path class ARPScanner: def __init__(self, config_file="config.json"): self.config_file = Path(config_file) self.config = self.load_config() self.data_file = Path(self.config['database']['devices_file']) self.known_devices = self.load_devices() def load_config(self): """Load configuration from file""" if self.config_file.exists(): try: with open(self.config_file, 'r') as f: return json.load(f) except: pass # Default configuration return { "network": { "subnet": "192.168.1.0/24", "auto_detect": True }, "scanning": { "ping_timeout": 1, "ping_delay": 0.01, "max_threads": 10 }, "database": { "devices_file": "known_devices.json" }, "logging": { "enabled": False, "log_file": "network_scanner.log", "log_level": "INFO" } } def load_devices(self): """Load previously discovered devices""" if self.data_file.exists(): try: with open(self.data_file, 'r') as f: return json.load(f) except: return {} return {} def save_devices(self): """Save devices to file""" with open(self.data_file, 'w') as f: json.dump(self.known_devices, f, indent=2) def get_network_info(self): """Get network range from config or auto-detect""" print(f"Auto-detect: {self.config['network']['auto_detect']}") print(f"Config subnet: {self.config['network']['subnet']}") if not self.config['network']['auto_detect']: print(f"Using configured subnet: {self.config['network']['subnet']}") return self.config['network']['subnet'] interface = None try: # Check if we're on Windows or Linux import platform system = platform.system().lower() if system == 'windows': # Windows network info result = subprocess.run(['ipconfig'], capture_output=True, text=True, encoding='utf-8', errors='ignore') for line in result.stdout.split('\n'): if 'IPv4' in line and 'Address' in line: ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', line) if ip_match: ip = ip_match.group(1) return self.calculate_network_range(ip, 24) else: # Linux network info result = subprocess.run(['ip', 'route', 'show', 'default'], capture_output=True, text=True, encoding='utf-8', errors='ignore') if result.returncode == 0: line = result.stdout.strip() parts = line.split() for i, part in enumerate(parts): if part == 'dev': interface = parts[i+1] elif part == 'via': gateway = parts[i+1] # Get network range if interface: result = subprocess.run(['ip', 'addr', 'show', interface], capture_output=True, text=True, encoding='utf-8', errors='ignore') for line in result.stdout.split('\n'): if 'inet ' in line: ip_match = re.search(r'inet (\d+\.\d+\.\d+\.\d+)/(\d+)', line) if ip_match: ip = ip_match.group(1) cidr = int(ip_match.group(2)) return self.calculate_network_range(ip, cidr) except: pass return self.config['network']['subnet'] # fallback to config def calculate_network_range(self, ip, cidr): """Calculate network range from IP and CIDR""" parts = ip.split('.') if cidr == 24: return f"{parts[0]}.{parts[1]}.{parts[2]}.0/24" elif cidr == 16: return f"{parts[0]}.{parts[1]}.0.0/16" else: return f"{parts[0]}.{parts[1]}.{parts[2]}.0/24" def scan_arp_table(self): """Read current ARP table""" try: import platform system = platform.system().lower() if system == 'windows': result = subprocess.run(['arp', '-a'], capture_output=True, text=True, encoding='utf-8', errors='ignore') else: result = subprocess.run(['arp', '-n'], capture_output=True, text=True, encoding='utf-8', errors='ignore') devices = {} for line in result.stdout.split('\n'): if re.match(r'\d+\.\d+\.\d+\.\d+', line): parts = line.split() if len(parts) >= 2: # Windows format: " 192.168.1.1 00-11-22-33-44-55 dynamic" # Linux format: "192.168.1.1 ether 00:11:22:33:44:55 C eth0" ip = parts[0] mac = parts[1] if len(parts) > 1 else "" # Clean MAC address format mac = mac.replace('-', ':').upper() if mac and mac != '(incomplete)' and len(mac) == 17: devices[ip] = { 'mac': mac, 'last_seen': datetime.now().isoformat() } return devices except: return {} def ping_sweep(self, network_range): """Ping sweep to populate ARP table""" print("Performing ping sweep...") import platform system = platform.system().lower() # Extract network part if '/24' in network_range: base = network_range.replace('/24', '') base_parts = base.split('.') ping_timeout = self.config['scanning']['ping_timeout'] ping_delay = self.config['scanning']['ping_delay'] # Only ping first 50 IPs for faster testing for i in range(1, 51): ip = f"{base_parts[0]}.{base_parts[1]}.{base_parts[2]}.{i}" try: if system == 'windows': subprocess.run(['ping', '-n', '1', '-w', str(ping_timeout * 1000), ip], capture_output=True, timeout=1, encoding='utf-8', errors='ignore') else: subprocess.run(['ping', '-c', '1', '-W', str(ping_timeout), ip], capture_output=True, timeout=1, encoding='utf-8', errors='ignore') except: pass time.sleep(ping_delay) # Configurable delay def filter_devices_by_network(self, devices, network_range): """Filter devices to only include those in the specified network""" if '/24' in network_range: base = network_range.replace('/24', '') base_parts = base.split('.') filtered_devices = {} for ip, info in devices.items(): ip_parts = ip.split('.') if (len(ip_parts) == 4 and ip_parts[0] == base_parts[0] and ip_parts[1] == base_parts[1] and ip_parts[2] == base_parts[2]): filtered_devices[ip] = info return filtered_devices return devices def scan_network(self): """Full network scan""" print("Starting ARP network scan...") # Get network range network_range = self.get_network_info() print(f"Scanning network: {network_range}") # First read existing ARP table print("Reading existing ARP table...") current_devices = self.scan_arp_table() print(f"Found {len(current_devices)} devices in ARP table") # Filter devices to only include those in our network range if current_devices: current_devices = self.filter_devices_by_network(current_devices, network_range) print(f"Found {len(current_devices)} devices in target network") # If no devices found, try ping sweep if not current_devices: print("No devices found in target network, trying ping sweep...") self.ping_sweep(network_range) current_devices = self.scan_arp_table() current_devices = self.filter_devices_by_network(current_devices, network_range) print(f"After ping sweep: {len(current_devices)} devices found") return current_devices def compare_devices(self, current_devices): """Compare current devices with known ones""" new_devices = {} returned_devices = {} for ip, info in current_devices.items(): if ip not in self.known_devices: new_devices[ip] = info info['first_seen'] = datetime.now().isoformat() info['status'] = 'NEW' else: if self.known_devices[ip].get('status') == 'offline': returned_devices[ip] = info info['status'] = 'returned' else: info['status'] = 'online' # Check for offline devices offline_devices = {} for ip in self.known_devices: if ip not in current_devices: offline_devices[ip] = self.known_devices[ip] offline_devices[ip]['status'] = 'offline' return new_devices, returned_devices, offline_devices def update_devices(self, current_devices): """Update known devices database""" for ip, info in current_devices.items(): self.known_devices[ip] = info def run(self): """Main scan function""" print(f"Scan started at: {datetime.now()}") current_devices = self.scan_network() if current_devices: new_devices, returned_devices, offline_devices = self.compare_devices(current_devices) # Update database self.update_devices(current_devices) self.save_devices() # Results print(f"\n=== Scan Results ===") print(f"Total devices found: {len(current_devices)}") if new_devices: print(f"\nšŸ†• NEW DEVICES ({len(new_devices)}):") for ip, info in new_devices.items(): print(f" {ip} - {info['mac']}") if returned_devices: print(f"\nšŸ”„ RETURNED DEVICES ({len(returned_devices)}):") for ip, info in returned_devices.items(): print(f" {ip} - {info['mac']}") if offline_devices: print(f"\n⚫ OFFLINE DEVICES ({len(offline_devices)}):") for ip, info in offline_devices.items(): print(f" {ip} - {info['mac']}") if not new_devices and not returned_devices: print("\nāœ… No new devices detected") else: print("No devices found") if __name__ == "__main__": scanner = ARPScanner() scanner.run()