#!/usr/bin/env python3
"""CAPE Sandbox automated malware analysis agent.

Submits malware samples to a CAPE Sandbox instance via its REST API,
polls for analysis completion, and retrieves behavioral reports including
process trees, network activity, dropped files, and extracted configs.
"""
import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone

try:
    import requests
except ImportError:
    print("[!] 'requests' library required: pip install requests", file=sys.stderr)
    sys.exit(1)


def get_cape_config():
    """Return CAPE server URL and optional API token."""
    server = os.environ.get("CAPE_URL", "http://localhost:8090")
    token = os.environ.get("CAPE_API_TOKEN", "")
    return server.rstrip("/"), token


def submit_file(server, token, file_path, timeout_minutes=5, machine=None):
    """Submit a file to CAPE for analysis."""
    url = f"{server}/apiv2/tasks/create/file/"
    headers = {"Authorization": f"Token {token}"} if token else {}
    if not os.path.isfile(file_path):
        print(f"[!] File not found: {file_path}", file=sys.stderr)
        sys.exit(1)
    data = {"timeout": timeout_minutes * 60}
    if machine:
        data["machine"] = machine
    print(f"[*] Submitting {os.path.basename(file_path)} to CAPE...")
    with open(file_path, "rb") as f:
        resp = requests.post(
            url,
            files={"file": (os.path.basename(file_path), f)},
            data=data,
            headers=headers,
            timeout=120,
        )
    resp.raise_for_status()
    result = resp.json()
    if result.get("error"):
        print(f"[!] Submission error: {result.get('error_value', result['error'])}", file=sys.stderr)
        sys.exit(1)
    task_id = result.get("data", {}).get("task_ids", [None])[0]
    if not task_id:
        task_id = result.get("task_id") or result.get("data", {}).get("task_id")
    print(f"[+] Submitted successfully, task ID: {task_id}")
    return task_id


def submit_url(server, token, target_url, timeout_minutes=5):
    """Submit a URL to CAPE for analysis."""
    url = f"{server}/apiv2/tasks/create/url/"
    headers = {"Authorization": f"Token {token}"} if token else {}
    data = {"url": target_url, "timeout": timeout_minutes * 60}
    print(f"[*] Submitting URL: {target_url}")
    resp = requests.post(url, data=data, headers=headers, timeout=60)
    resp.raise_for_status()
    result = resp.json()
    task_id = result.get("data", {}).get("task_ids", [None])[0]
    if not task_id:
        task_id = result.get("task_id")
    print(f"[+] Submitted, task ID: {task_id}")
    return task_id


def poll_task_status(server, token, task_id, max_wait=600, interval=15):
    """Poll CAPE until the analysis task completes or times out."""
    url = f"{server}/apiv2/tasks/status/{task_id}/"
    headers = {"Authorization": f"Token {token}"} if token else {}
    print(f"[*] Waiting for analysis to complete (max {max_wait}s)...")
    elapsed = 0
    while elapsed < max_wait:
        try:
            resp = requests.get(url, headers=headers, timeout=30)
            resp.raise_for_status()
            data = resp.json()
            status = data.get("data", data.get("status", "unknown"))
            if isinstance(status, dict):
                status = status.get("status", "unknown")
            if status in ("reported", "completed"):
                print(f"[+] Analysis complete (status: {status})")
                return True
            if status in ("failed_analysis", "failed_processing"):
                print(f"[!] Analysis failed: {status}", file=sys.stderr)
                return False
            print(f"    Status: {status} ({elapsed}s elapsed)")
        except requests.RequestException as e:
            print(f"    Connection error: {e}")
        time.sleep(interval)
        elapsed += interval
    print("[!] Timed out waiting for analysis", file=sys.stderr)
    return False


def get_report(server, token, task_id):
    """Retrieve the full analysis report for a completed task."""
    url = f"{server}/apiv2/tasks/get/report/{task_id}/"
    headers = {"Authorization": f"Token {token}"} if token else {}
    resp = requests.get(url, headers=headers, timeout=120)
    resp.raise_for_status()
    return resp.json()


def extract_findings(report):
    """Extract structured findings from CAPE report."""
    findings = []

    # Signatures (behavioral detections)
    for sig in report.get("signatures", []):
        findings.append({
            "category": "signature",
            "name": sig.get("name", "Unknown"),
            "severity": sig.get("severity", 0),
            "description": sig.get("description", ""),
            "families": sig.get("families", []),
            "ttp": sig.get("ttp", {}),
        })

    # Network IOCs
    network = report.get("network", {})
    for dns_entry in network.get("dns", []):
        findings.append({
            "category": "network_dns",
            "request": dns_entry.get("request", ""),
            "type": dns_entry.get("type", ""),
            "answers": [a.get("data", "") for a in dns_entry.get("answers", [])],
        })
    for http_entry in network.get("http", []):
        findings.append({
            "category": "network_http",
            "method": http_entry.get("method", ""),
            "uri": http_entry.get("uri", ""),
            "host": http_entry.get("host", ""),
            "port": http_entry.get("port", 80),
        })

    # Dropped files
    for dropped in report.get("dropped", []):
        findings.append({
            "category": "dropped_file",
            "name": dropped.get("name", ""),
            "path": dropped.get("filepath", ""),
            "type": dropped.get("type", ""),
            "size": dropped.get("size", 0),
            "sha256": dropped.get("sha256", ""),
        })

    # CAPE extracted payloads and configs
    cape_data = report.get("CAPE", {})
    if isinstance(cape_data, dict):
        for cape_item in cape_data.get("payloads", []):
            findings.append({
                "category": "cape_payload",
                "name": cape_item.get("name", ""),
                "module": cape_item.get("module_path", ""),
                "sha256": cape_item.get("sha256", ""),
                "cape_type": cape_item.get("cape_type", ""),
            })
        for config in cape_data.get("configs", []):
            findings.append({
                "category": "cape_config",
                "family": config.get("family", "unknown"),
                "config_data": config,
            })

    return findings


def format_summary(report, findings):
    """Print human-readable analysis summary."""
    info = report.get("info", {})
    target = report.get("target", {})
    score = report.get("malscore", info.get("score", 0))

    print(f"\n{'='*60}")
    print(f"  CAPE Sandbox Analysis Report")
    print(f"{'='*60}")
    print(f"  Task ID     : {info.get('id', 'N/A')}")
    print(f"  Duration    : {info.get('duration', 0)}s")
    print(f"  Malscore    : {score}/10")

    file_info = target.get("file", {})
    if file_info:
        print(f"  File        : {file_info.get('name', 'N/A')}")
        print(f"  SHA256      : {file_info.get('sha256', 'N/A')}")
        print(f"  Type        : {file_info.get('type', 'N/A')}")

    sig_findings = [f for f in findings if f["category"] == "signature"]
    net_dns = [f for f in findings if f["category"] == "network_dns"]
    net_http = [f for f in findings if f["category"] == "network_http"]
    dropped = [f for f in findings if f["category"] == "dropped_file"]
    configs = [f for f in findings if f["category"] == "cape_config"]

    print(f"\n  Signatures  : {len(sig_findings)}")
    print(f"  DNS Queries : {len(net_dns)}")
    print(f"  HTTP Reqs   : {len(net_http)}")
    print(f"  Dropped     : {len(dropped)}")
    print(f"  CAPE Configs: {len(configs)}")

    if sig_findings:
        print(f"\n  Top Signatures:")
        for s in sorted(sig_findings, key=lambda x: x.get("severity", 0), reverse=True)[:10]:
            sev = s.get("severity", 0)
            label = "HIGH" if sev >= 3 else "MEDIUM" if sev >= 2 else "LOW"
            print(f"    [{label:6s}] {s['name']}: {s['description'][:80]}")

    if configs:
        print(f"\n  Extracted Configs:")
        for c in configs:
            print(f"    Family: {c.get('family', 'unknown')}")

    return score


def main():
    parser = argparse.ArgumentParser(
        description="CAPE Sandbox malware analysis agent"
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--file", help="Path to malware sample to submit")
    group.add_argument("--url", help="URL to submit for analysis")
    group.add_argument("--task-id", type=int, help="Retrieve report for existing task")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--server", help="CAPE server URL (or set CAPE_URL env var)")
    parser.add_argument("--token", help="API token (or set CAPE_API_TOKEN env var)")
    parser.add_argument("--machine", help="Specific VM to use for analysis")
    parser.add_argument("--timeout", type=int, default=5, help="Analysis timeout in minutes")
    parser.add_argument("--wait", type=int, default=600, help="Max seconds to wait for completion")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    if args.server:
        os.environ["CAPE_URL"] = args.server
    if args.token:
        os.environ["CAPE_API_TOKEN"] = args.token

    server, token = get_cape_config()

    if args.task_id:
        task_id = args.task_id
    elif args.file:
        task_id = submit_file(server, token, args.file, args.timeout, args.machine)
    else:
        task_id = submit_url(server, token, args.url, args.timeout)

    if not args.task_id:
        if not poll_task_status(server, token, task_id, args.wait):
            sys.exit(1)

    report = get_report(server, token, task_id)
    findings = extract_findings(report)
    score = format_summary(report, findings)

    output_report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "CAPE Sandbox",
        "task_id": task_id,
        "malscore": score,
        "findings_count": len(findings),
        "findings": findings,
        "risk_level": (
            "CRITICAL" if score >= 8
            else "HIGH" if score >= 5
            else "MEDIUM" if score >= 3
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(output_report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(output_report, indent=2))


if __name__ == "__main__":
    main()
