#!/usr/bin/env python3
"""
SecRecon Remote Agent v1.0
Standalone script for executing SecRecon scans on your own infrastructure.
Supports internal network scanning, localhost targets, and air-gapped environments.

Usage:
    python sec-recon-agent.py --server https://secrecon.dev --key sra_YOUR_API_KEY

Requirements:
    pip install requests
"""

import argparse
import hashlib
import json
import logging
import os
import platform
import re
import socket
import ssl
import subprocess
import sys
import threading
import time
import urllib.request
import urllib.error
import urllib.parse
from datetime import datetime, timezone

__version__ = "1.0.0"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("secrecon-agent")

POLL_INTERVAL = 5
HEARTBEAT_INTERVAL = 30
MAX_RETRIES = 3
RETRY_DELAY = 10


class SecReconAgent:
    def __init__(self, server_url, api_key, verify_ssl=True):
        self.server_url = server_url.rstrip("/")
        self.api_key = api_key
        self._engine_module = None
        self.verify_ssl = verify_ssl
        self.running = False
        self._heartbeat_thread = None
        self._capabilities = self._detect_capabilities()

    def _detect_capabilities(self):
        caps = ["basic_recon", "port_scan", "http_probe"]
        try:
            subprocess.run(["nmap", "--version"], capture_output=True, timeout=5)
            caps.append("nmap")
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        try:
            subprocess.run(["nuclei", "--version"], capture_output=True, timeout=5)
            caps.append("nuclei")
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        try:
            subprocess.run(["curl", "--version"], capture_output=True, timeout=5)
            caps.append("curl")
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass
        return caps

    def _api_request(self, method, path, data=None, timeout=35):
        url = f"{self.server_url}{path}"
        headers = {
            "X-SecRecon-Agent-Key": self.api_key,
            "Content-Type": "application/json",
            "User-Agent": f"SecRecon-Agent/{__version__}",
        }

        body = json.dumps(data).encode("utf-8") if data else None
        req = urllib.request.Request(url, data=body, headers=headers, method=method)

        ctx = None
        if not self.verify_ssl:
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE

        try:
            resp = urllib.request.urlopen(req, timeout=timeout, context=ctx)
            return json.loads(resp.read().decode("utf-8")), resp.status
        except urllib.error.HTTPError as e:
            body_text = e.read().decode("utf-8", errors="replace")
            try:
                return json.loads(body_text), e.code
            except json.JSONDecodeError:
                return {"error": body_text}, e.code
        except urllib.error.URLError as e:
            raise ConnectionError(f"Failed to connect to {url}: {e.reason}")

    def preflight_check(self):
        logger.info("Running pre-flight connectivity check...")
        try:
            resp, status = self._api_request("GET", "/api/agent/preflight", timeout=15)
            if status == 200 and resp.get("status") == "ok":
                logger.info(f"Pre-flight OK — server engine v{resp.get('engine_version', '?')}, "
                            f"{len(resp.get('endpoints', []))} endpoints available")
                return True
            else:
                logger.error(f"Pre-flight failed (HTTP {status}): {resp}")
                return False
        except ConnectionError as e:
            logger.error(f"Pre-flight connection failed: {e}")
            return False

    def check_engine_update(self):
        try:
            resp, status = self._api_request("GET", f"/api/agent/engine?v={__version__}", timeout=20)
            if status != 200:
                return
            server_version = resp.get("version", "0.0.0")
            if not resp.get("has_update"):
                logger.info(f"Engine check: up to date (v{server_version})")
                return
            engine_code = resp.get("engine_code", "")
            expected_hash = resp.get("sha256", "")
            if not engine_code:
                return
            actual_hash = hashlib.sha256(engine_code.encode("utf-8")).hexdigest()
            if expected_hash and actual_hash != expected_hash:
                logger.error(f"Engine integrity check FAILED — expected {expected_hash[:16]}..., got {actual_hash[:16]}...")
                return
            engine_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "engine_remote.py")
            with open(engine_path, "w", encoding="utf-8") as f:
                f.write(engine_code)
            logger.info(f"Engine updated to v{server_version} (hash={actual_hash[:16]}...) — saved to {engine_path}")
            try:
                spec = __import__("importlib").util.spec_from_file_location("engine_remote", engine_path)
                mod = __import__("importlib").util.module_from_spec(spec)
                spec.loader.exec_module(mod)
                self._engine_module = mod
                logger.info("Remote engine module loaded successfully")
            except Exception as e:
                logger.warning(f"Failed to load remote engine module: {e}")
                self._engine_module = None
        except Exception as e:
            logger.debug(f"Engine update check skipped: {e}")

    def check_in(self):
        data = {
            "agent_version": __version__,
            "capabilities": self._capabilities,
            "hostname": platform.node(),
        }
        resp, status = self._api_request("POST", "/api/agent/check-in", data)
        if status == 200:
            logger.info(f"Check-in successful: agent_id={resp.get('agent_id')}, name={resp.get('agent_name')}")
            return True
        else:
            logger.error(f"Check-in failed (HTTP {status}): {resp}")
            return False

    def poll_tasks(self):
        try:
            resp, status = self._api_request("GET", "/api/agent/tasks", timeout=35)
            if status == 200:
                return resp
            else:
                logger.warning(f"Task poll returned HTTP {status}: {resp}")
                return None
        except ConnectionError as e:
            logger.warning(f"Task poll connection error: {e}")
            return None

    def send_log(self, scan_id, message, phase="scan", progress=0):
        try:
            payload = {"scan_id": scan_id, "phase": phase, "message": message, "progress": progress}
            self._api_request("POST", "/api/agent/log", payload)
        except Exception:
            pass

    def submit_results(self, scan_id, results):
        payload = {
            "scan_id": scan_id,
            "status": "complete",
            **results,
        }
        resp, status = self._api_request("POST", "/api/agent/results", payload)
        if status == 200:
            logger.info(f"Results submitted for scan {scan_id}: {resp}")
            return True
        elif status == 404:
            detail = resp.get("detail", "")
            if "Not Found" == detail:
                logger.error(
                    f"AGENT COMMUNICATION ERROR: /api/agent/results returned 404. "
                    f"Your agent script may be outdated — download the latest version from the SecRecon dashboard."
                )
                self.send_log(scan_id, "Agent Communication Error: API Endpoint Not Found. Please update your Agent script.", "error", 0)
            elif "not assigned" in detail.lower() or "not found" in detail.lower():
                logger.error(f"Scan ownership error: {detail}")
                self.send_log(scan_id, f"Scan submission rejected: {detail}", "error", 0)
            else:
                logger.error(f"Result submission 404: {detail}")
            return False
        else:
            logger.error(f"Result submission failed (HTTP {status}): {resp}")
            return False

    def execute_scan(self, task):
        scan_id = task["scan_id"]
        target = task["target"]
        logger.info(f"Executing scan {scan_id} against {target}")
        self.send_log(scan_id, f"Starting scan against {target}", "init", 5)

        findings = []
        raw_evidence = []
        services = []
        technologies = []
        cve_count = 0
        detected_ports = []

        self.send_log(scan_id, "Checking target reachability...", "preflight", 2)
        target_reachable = False
        try:
            import socket
            _probe_host = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
            _probe_port = 80
            try:
                _port_part = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[1]
                _probe_port = int(_port_part)
            except (IndexError, ValueError):
                _probe_port = 443 if target.startswith("https") else 80
            _sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            _sock.settimeout(10)
            _result = _sock.connect_ex((_probe_host, _probe_port))
            _sock.close()
            if _result == 0:
                target_reachable = True
            else:
                try:
                    socket.getaddrinfo(_probe_host, None, socket.AF_INET)
                    target_reachable = True
                except socket.gaierror:
                    target_reachable = False
        except Exception as reach_err:
            logger.warning(f"Reachability check error: {reach_err}")
            target_reachable = False

        if not target_reachable:
            self.send_log(scan_id, "Target unreachable — cannot connect", "error", 100)
            logger.warning(f"Target {target} is unreachable for scan {scan_id}")
            return {
                "risk_score": "N/A",
                "vulnerabilities_found": 0,
                "findings": [],
                "raw_evidence": [{"type": "error", "probe": "reachability_check", "target": target, "response": "Target unreachable by Agent", "timestamp": datetime.now(timezone.utc).isoformat()}],
                "services_found": 0,
                "technologies_found": 0,
                "cve_count": 0,
                "result_json": {"scan_source": "remote_agent", "agent_version": __version__, "error": "Target unreachable by Agent"},
                "ai_summary": f"Scan could not be completed: target {target} was unreachable from the agent's network. Verify the target is online and accessible from the agent host.",
            }

        try:
            self.send_log(scan_id, "Running DNS reconnaissance...", "recon", 10)
            dns_results = self._dns_recon(target)
            if dns_results:
                self.send_log(scan_id, f"Found {len(dns_results)} DNS records", "recon", 20)
                for rec in dns_results:
                    raw_evidence.append({
                        "type": "dns",
                        "probe": f"DNS {rec['type']}",
                        "target": target,
                        "response": rec["value"],
                        "timestamp": datetime.now(timezone.utc).isoformat(),
                    })
        except Exception as e:
            logger.warning(f"DNS recon error: {e}")

        try:
            self.send_log(scan_id, "Probing HTTP endpoints...", "http", 30)
            http_results = self._http_probe(target)
            if http_results:
                self.send_log(scan_id, f"HTTP probe returned {len(http_results)} result(s)", "http", 40)
                for result in http_results:
                    raw_evidence.append(result)
                    if result.get("findings"):
                        findings.extend(result["findings"])
                    if result.get("technologies"):
                        technologies.extend(result["technologies"])
                    services.append(f"HTTP/{result.get('port', 80)}")
        except Exception as e:
            logger.warning(f"HTTP probe error: {e}")

        try:
            self.send_log(scan_id, "Scanning TCP ports...", "portscan", 50)
            port_results = self._port_scan(target)
            if port_results:
                self.send_log(scan_id, f"Found {len(port_results)} open port(s)", "portscan", 60)
                for port_info in port_results:
                    services.append(f"{port_info['service']}/{port_info['port']}")
                    detected_ports.append(port_info['port'])
                    raw_evidence.append({
                        "type": "port_scan",
                        "probe": f"TCP connect {port_info['port']}",
                        "target": target,
                        "response": f"Port {port_info['port']} open: {port_info['service']}",
                        "timestamp": datetime.now(timezone.utc).isoformat(),
                    })
        except Exception as e:
            logger.warning(f"Port scan error: {e}")

        if "nmap" in self._capabilities:
            try:
                self.send_log(scan_id, "Running nmap service detection...", "nmap", 65)
                nmap_results = self._nmap_scan(target)
                if nmap_results:
                    self.send_log(scan_id, "Nmap scan complete", "nmap", 75)
                    raw_evidence.append({
                        "type": "nmap",
                        "probe": "nmap service detection",
                        "target": target,
                        "response": nmap_results,
                        "timestamp": datetime.now(timezone.utc).isoformat(),
                    })
            except Exception as e:
                logger.warning(f"Nmap error: {e}")

        if "nuclei" in self._capabilities:
            try:
                self.send_log(scan_id, "Running Nuclei vulnerability templates...", "nuclei", 80)
                nuclei_targets = set()
                nuclei_targets.add(target)
                _base_host = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
                http_ports = [p for p in detected_ports if p in (80, 443, 8080, 8443, 8888, 8000, 3000, 5000, 9090)]
                for hp in http_ports:
                    scheme = "https" if hp in (443, 8443) else "http"
                    nuclei_targets.add(f"{scheme}://{_base_host}:{hp}")
                nuclei_findings = []
                for nt in nuclei_targets:
                    try:
                        nf = self._nuclei_scan(nt)
                        nuclei_findings.extend(nf)
                    except Exception as inner_e:
                        logger.warning(f"Nuclei scan error for {nt}: {inner_e}")
                seen_templates = set()
                deduped_findings = []
                for nf in nuclei_findings:
                    key = (nf.get("template_id", ""), nf.get("matched_at", ""))
                    if key not in seen_templates:
                        seen_templates.add(key)
                        deduped_findings.append(nf)
                findings.extend(deduped_findings)
                cve_count = sum(1 for f in deduped_findings if f.get("cve_id"))
                self.send_log(scan_id, f"Nuclei found {len(deduped_findings)} finding(s) across {len(nuclei_targets)} target(s)", "nuclei", 90)
            except Exception as e:
                logger.warning(f"Nuclei error: {e}")

        oast_total_hits = 0
        oast_dns_hits = 0
        oast_http_hits = 0
        oast_interactions = []
        oast_url = task.get("oast_url", "")
        if oast_url:
            try:
                self.send_log(scan_id, "Running OAST/SSRF probes...", "oast", 92)
                if self._engine_module and hasattr(self._engine_module, "oast_ssrf_probe"):
                    oast_results = self._engine_module.oast_ssrf_probe(self, target, oast_url, scan_id=scan_id, verify_ssl=self.verify_ssl)
                else:
                    oast_results = self._oast_ssrf_probe(target, oast_url, scan_id=scan_id)
                if oast_results:
                    oast_interactions = oast_results.get("interactions", [])
                    oast_total_hits = oast_results.get("total_hits", 0)
                    oast_dns_hits = oast_results.get("dns_hits", 0)
                    oast_http_hits = oast_results.get("http_hits", 0)
                    if oast_total_hits > 0:
                        findings.append({
                            "title": "SSRF/OAST Interaction Detected",
                            "severity": "high",
                            "description": f"Out-of-band interactions detected: {oast_total_hits} total ({oast_dns_hits} DNS, {oast_http_hits} HTTP). The target made external requests to the OAST callback server.",
                            "remediation": "Investigate server-side request forgery (SSRF) vectors. Restrict outbound connections and validate user-supplied URLs.",
                            "vrt_category": "server_side_injection",
                            "evidence": oast_interactions[:5],
                            "has_hard_evidence": True,
                        })
                    for interaction in oast_interactions[:10]:
                        raw_evidence.append({
                            "type": "oast",
                            "probe": f"OAST {interaction.get('protocol', 'unknown')}",
                            "target": target,
                            "response": json.dumps(interaction),
                            "timestamp": interaction.get("timestamp", datetime.now(timezone.utc).isoformat()),
                        })
                    self.send_log(scan_id, f"OAST probes complete — {oast_total_hits} interaction(s) detected", "oast", 95)
            except Exception as oast_err:
                logger.warning(f"OAST/SSRF probe error: {oast_err}")

        vuln_count = len([f for f in findings if f.get("severity") in ("critical", "high", "medium")])
        cve_count = sum(1 for f in findings if f.get("cve_id"))
        risk_score = self._calculate_risk(findings)
        self.send_log(scan_id, f"Scan complete — {vuln_count} vulnerabilities, {cve_count} CVEs, {len(detected_ports)} open ports, risk={risk_score}", "complete", 100)

        unique_techs = list(set(technologies))
        unique_ports = sorted(set(detected_ports))

        result = {
            "risk_score": risk_score,
            "vulnerabilities_found": vuln_count,
            "findings": findings,
            "raw_evidence": raw_evidence,
            "services_found": len(set(services)),
            "technologies_found": len(unique_techs),
            "cve_count": cve_count,
            "result_json": {
                "services": list(set(services)),
                "technologies": unique_techs,
                "ports": unique_ports,
                "scan_source": "remote_agent",
                "agent_version": __version__,
            },
        }
        if oast_total_hits or oast_interactions:
            result["oast_total_hits"] = oast_total_hits
            result["oast_dns_hits"] = oast_dns_hits
            result["oast_http_hits"] = oast_http_hits
            result["oast_interactions"] = oast_interactions
        return result

    def _oast_ssrf_probe(self, target, oast_url, scan_id=None):
        hostname = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
        scheme = "https" if target.startswith("https") else "http"
        base_url = f"{scheme}://{hostname}"
        params = ["url", "redirect", "callback", "dest", "uri", "path", "next",
                  "target", "fetch", "load", "file", "page", "site", "img", "src"]

        ssrf_payloads = []
        if target.rstrip("/").endswith("="):
            ssrf_payloads.append(f"{target}{oast_url}")
            ssrf_payloads.append(f"{target}{urllib.parse.quote(oast_url, safe='')}")
        elif "?" in target:
            sep = "&" if not target.endswith("?") else ""
            ssrf_payloads.append(f"{target}{sep}url={oast_url}")
        for p in params:
            ssrf_payloads.append(f"{base_url}/?{p}={oast_url}")
        try:
            parsed = urllib.parse.urlparse(target if "://" in target else f"https://{target}")
            if parsed.path and parsed.path != "/":
                path_base = f"{base_url}{parsed.path}"
                for p in params:
                    ssrf_payloads.append(f"{path_base}?{p}={oast_url}")
        except Exception:
            pass
        seen = set()
        ssrf_payloads = [u for u in ssrf_payloads if u not in seen and not seen.add(u)]

        logger.info(f"[OAST] Sending {len(ssrf_payloads)} SSRF probes to {hostname} with OAST URL: {oast_url}")

        for payload_url in ssrf_payloads:
            try:
                req = urllib.request.Request(
                    payload_url,
                    headers={
                        "User-Agent": f"SecRecon-Agent/{__version__}",
                        "Referer": oast_url,
                        "X-Forwarded-For": oast_url,
                        "ngrok-skip-browser-warning": "true",
                        "Accept": "*/*",
                    },
                    method="GET",
                )
                ctx = None
                if not self.verify_ssl:
                    ctx = ssl.create_default_context()
                    ctx.check_hostname = False
                    ctx.verify_mode = ssl.CERT_NONE
                urllib.request.urlopen(req, timeout=5, context=ctx)
            except Exception:
                pass

        try:
            import subprocess
            oast_host = oast_url.replace("http://", "").replace("https://", "").split("/")[0]
            subprocess.run(["nslookup", oast_host], capture_output=True, timeout=5)
        except Exception:
            pass

        time.sleep(5)

        interactions = []
        dns_hits = 0
        http_hits = 0
        if scan_id:
            for attempt in range(3):
                try:
                    resp, status = self._api_request("GET", f"/api/agent/oast-poll/{scan_id}", timeout=15)
                    if status == 200:
                        interactions = resp.get("interactions", [])
                        dns_hits = resp.get("dns_hits", 0)
                        http_hits = resp.get("http_hits", 0)
                        total = resp.get("total_hits", 0)
                        if total > 0:
                            logger.info(f"[OAST] Server reported {total} interactions ({dns_hits} DNS, {http_hits} HTTP)")
                            break
                        else:
                            logger.info(f"[OAST] Poll attempt {attempt+1}/3: no interactions yet")
                except Exception as poll_err:
                    logger.warning(f"[OAST] Poll attempt {attempt+1}/3 failed: {poll_err}")
                if attempt < 2:
                    time.sleep(3)

        return {
            "interactions": interactions,
            "total_hits": dns_hits + http_hits,
            "dns_hits": dns_hits,
            "http_hits": http_hits,
        }

    def _dns_recon(self, target):
        results = []
        hostname = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
        try:
            ips = socket.getaddrinfo(hostname, None)
            seen = set()
            for info in ips:
                ip = info[4][0]
                if ip not in seen:
                    seen.add(ip)
                    family = "AAAA" if ":" in ip else "A"
                    results.append({"type": family, "value": ip})
        except socket.gaierror:
            pass

        try:
            import subprocess
            result = subprocess.run(
                ["nslookup", "-type=MX", hostname],
                capture_output=True, text=True, timeout=10
            )
            for line in result.stdout.split("\n"):
                if "mail exchanger" in line.lower():
                    results.append({"type": "MX", "value": line.strip()})
        except (FileNotFoundError, subprocess.TimeoutExpired):
            pass

        return results

    def _http_probe(self, target):
        results = []
        hostname = target.replace("http://", "").replace("https://", "").split("/")[0]

        for scheme in ["https", "http"]:
            url = f"{scheme}://{hostname}/"
            try:
                req = urllib.request.Request(url, method="GET")
                req.add_header("User-Agent", f"SecRecon-Agent/{__version__}")

                ctx = ssl.create_default_context()
                ctx.check_hostname = False
                ctx.verify_mode = ssl.CERT_NONE

                resp = urllib.request.urlopen(req, timeout=10, context=ctx)
                headers = dict(resp.headers)
                body = resp.read(8192).decode("utf-8", errors="replace")
                status_code = resp.status

                result = {
                    "type": "http_probe",
                    "probe": f"{scheme.upper()} GET /",
                    "target": url,
                    "status_code": status_code,
                    "response_headers": json.dumps(headers),
                    "response_body_preview": body[:500],
                    "timestamp": datetime.now(timezone.utc).isoformat(),
                    "findings": [],
                    "technologies": [],
                }

                security_headers = {
                    "Strict-Transport-Security": "Missing HSTS header",
                    "Content-Security-Policy": "Missing CSP header",
                    "X-Frame-Options": "Missing X-Frame-Options header",
                    "X-Content-Type-Options": "Missing X-Content-Type-Options header",
                }

                for header, desc in security_headers.items():
                    if header.lower() not in {k.lower(): v for k, v in headers.items()}:
                        result["findings"].append({
                            "name": desc,
                            "severity": "info",
                            "matched_at": url,
                            "description": f"The {header} header is not set on the response.",
                            "evidence": {"response_headers": json.dumps(headers)},
                        })

                server = headers.get("Server", headers.get("server", ""))
                if server:
                    result["technologies"].append(server)

                powered_by = headers.get("X-Powered-By", headers.get("x-powered-by", ""))
                if powered_by:
                    result["technologies"].append(powered_by)
                    result["findings"].append({
                        "name": "Technology disclosure via X-Powered-By",
                        "severity": "low",
                        "matched_at": url,
                        "description": f"Server discloses technology: {powered_by}",
                        "evidence": {"response_headers": json.dumps(headers)},
                    })

                results.append(result)

            except Exception as e:
                logger.debug(f"HTTP probe failed for {url}: {e}")

        return results

    def _port_scan(self, target):
        hostname = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
        common_ports = [21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 993, 995, 3000, 3306, 3389, 5000, 5432, 8000, 8080, 8443, 8888, 9090]
        open_ports = []

        for port in common_ports:
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(2)
                result = sock.connect_ex((hostname, port))
                if result == 0:
                    service = self._guess_service(port)
                    open_ports.append({"port": port, "service": service, "state": "open"})
                sock.close()
            except (socket.gaierror, socket.error, OSError):
                pass

        return open_ports

    def _guess_service(self, port):
        service_map = {
            21: "ftp", 22: "ssh", 23: "telnet", 25: "smtp", 53: "dns",
            80: "http", 110: "pop3", 143: "imap", 443: "https", 445: "smb",
            993: "imaps", 995: "pop3s", 3306: "mysql", 3389: "rdp",
            5432: "postgresql", 8080: "http-proxy", 8443: "https-alt", 8888: "http-alt",
        }
        return service_map.get(port, f"unknown-{port}")

    def _nmap_scan(self, target):
        hostname = target.replace("http://", "").replace("https://", "").split("/")[0].split(":")[0]
        try:
            result = subprocess.run(
                ["nmap", "-sV", "--top-ports", "100", "-T4", "--open", hostname],
                capture_output=True, text=True, timeout=120,
            )
            return result.stdout
        except (FileNotFoundError, subprocess.TimeoutExpired) as e:
            logger.warning(f"Nmap scan error: {e}")
            return None

    def _nuclei_scan(self, target):
        hostname = target.replace("http://", "").replace("https://", "").split("/")[0]
        url = f"https://{hostname}" if not target.startswith("http") else target

        try:
            result = subprocess.run(
                ["nuclei", "-u", url, "-severity", "critical,high,medium,low,info",
                 "-tags", "cve,cves,tech,misconfig,exposure",
                 "-json", "-silent"],
                capture_output=True, text=True, timeout=300,
            )
            findings = []
            for line in result.stdout.strip().split("\n"):
                if not line.strip():
                    continue
                try:
                    data = json.loads(line)
                    finding = {
                        "name": data.get("info", {}).get("name", "Unknown"),
                        "severity": data.get("info", {}).get("severity", "info"),
                        "matched_at": data.get("matched-at", url),
                        "description": data.get("info", {}).get("description", ""),
                        "template_id": data.get("template-id", ""),
                    }
                    classification = data.get("info", {}).get("classification", {})
                    cve = classification.get("cve-id")
                    if cve:
                        if isinstance(cve, list):
                            finding["cve_id"] = cve[0] if cve else None
                        else:
                            finding["cve_id"] = cve

                    cvss_metric = classification.get("cvss-score")
                    if cvss_metric:
                        try:
                            finding["cvss_score"] = float(cvss_metric) if not isinstance(cvss_metric, (int, float)) else cvss_metric
                        except (ValueError, TypeError):
                            pass
                    if not finding.get("cvss_score"):
                        cvss_metrics = data.get("info", {}).get("classification", {}).get("cvss-metrics", "")
                        if cvss_metrics and isinstance(cvss_metrics, str):
                            finding["cvss_vector"] = cvss_metrics

                    evidence = {}
                    if data.get("request"):
                        evidence["request"] = data["request"][:2000]
                    if data.get("response"):
                        evidence["response"] = data["response"][:2000]
                    if evidence:
                        finding["evidence"] = evidence

                    findings.append(finding)
                except json.JSONDecodeError:
                    continue
            return findings
        except (FileNotFoundError, subprocess.TimeoutExpired) as e:
            logger.warning(f"Nuclei scan error: {e}")
            return []

    def _calculate_risk(self, findings):
        if not findings:
            return "C"

        severity_weights = {"critical": 10, "high": 7, "medium": 4, "low": 1, "info": 0}
        total = sum(severity_weights.get(f.get("severity", "info"), 0) for f in findings)

        if total >= 20:
            return "F"
        elif total >= 15:
            return "D"
        elif total >= 10:
            return "C"
        elif total >= 5:
            return "B"
        else:
            return "A"

    def _heartbeat_loop(self):
        heartbeat_count = 0
        while self.running:
            try:
                self.check_in()
            except Exception as e:
                logger.warning(f"Heartbeat failed: {e}")
            heartbeat_count += 1
            if heartbeat_count % 10 == 0:
                self.check_engine_update()
            time.sleep(HEARTBEAT_INTERVAL)

    def run(self):
        logger.info(f"SecRecon Agent v{__version__} starting")
        logger.info(f"Server: {self.server_url}")
        logger.info(f"Capabilities: {', '.join(self._capabilities)}")
        logger.info(f"Hostname: {platform.node()}")
        logger.info(f"Platform: {platform.system()} {platform.release()}")

        if not self.preflight_check():
            logger.error("Pre-flight check failed. Server may be unreachable or API endpoints missing.")
            logger.error("Ensure your server URL is correct and the SecRecon server is running.")
            sys.exit(1)

        if not self.check_in():
            logger.error("Initial check-in failed. Verify your API key and server URL.")
            sys.exit(1)

        self.check_engine_update()

        self.running = True
        self._heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True)
        self._heartbeat_thread.start()

        logger.info("Agent online. Polling for tasks...")

        consecutive_errors = 0
        while self.running:
            try:
                result = self.poll_tasks()
                if result is None:
                    consecutive_errors += 1
                    if consecutive_errors >= MAX_RETRIES:
                        delay = min(RETRY_DELAY * consecutive_errors, 120)
                        logger.warning(f"Multiple poll failures. Backing off {delay}s...")
                        time.sleep(delay)
                    continue

                consecutive_errors = 0

                if result.get("has_task"):
                    task = result["task"]
                    logger.info(f"Task received: scan_id={task['scan_id']}, target={task['target']}")

                    try:
                        scan_results = self.execute_scan(task)
                        success = False
                        for attempt in range(MAX_RETRIES):
                            if self.submit_results(task["scan_id"], scan_results):
                                success = True
                                break
                            logger.warning(f"Result submission attempt {attempt + 1} failed, retrying...")
                            time.sleep(RETRY_DELAY)

                        if not success:
                            logger.error(f"Failed to submit results for scan {task['scan_id']} after {MAX_RETRIES} attempts")
                    except Exception as e:
                        logger.error(f"Scan execution failed: {e}")
                        try:
                            self.submit_results(task["scan_id"], {
                                "risk_score": "C",
                                "vulnerabilities_found": 0,
                                "findings": [],
                                "raw_evidence": [{"type": "error", "probe": "agent_error", "response": str(e)}],
                                "ai_summary": f"Scan failed with error: {e}",
                            })
                        except Exception:
                            pass
                else:
                    time.sleep(POLL_INTERVAL)

            except KeyboardInterrupt:
                logger.info("Shutdown signal received.")
                self.running = False
                break
            except Exception as e:
                logger.error(f"Unexpected error in main loop: {e}")
                consecutive_errors += 1
                time.sleep(RETRY_DELAY)

        logger.info("Agent shutting down.")


def main():
    parser = argparse.ArgumentParser(
        description="SecRecon Remote Agent - Execute security scans on your infrastructure",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python sec-recon-agent.py --server https://secrecon.dev --key sra_abc123...
  python sec-recon-agent.py --server https://secrecon.dev --key sra_abc123... --no-verify-ssl
  SEC_RECON_SERVER=https://secrecon.dev SEC_RECON_KEY=sra_abc123 python sec-recon-agent.py
        """,
    )

    parser.add_argument("--server", default=os.environ.get("SEC_RECON_SERVER"),
                        help="SecRecon server URL (or set SEC_RECON_SERVER env var)")
    parser.add_argument("--key", default=os.environ.get("SEC_RECON_KEY"),
                        help="Agent API key (or set SEC_RECON_KEY env var)")
    parser.add_argument("--no-verify-ssl", action="store_true",
                        help="Disable SSL certificate verification")
    parser.add_argument("--version", action="version", version=f"SecRecon Agent v{__version__}")

    args = parser.parse_args()

    if not args.server:
        parser.error("Server URL required. Use --server or set SEC_RECON_SERVER env var.")
    if not args.key:
        parser.error("API key required. Use --key or set SEC_RECON_KEY env var.")

    agent = SecReconAgent(args.server, args.key, verify_ssl=not args.no_verify_ssl)
    agent.run()


if __name__ == "__main__":
    main()
