#!/usr/bin/env python3
"""Agent for hunting C2 beaconing across multiple data sources."""

import argparse
import json
import re
from collections import defaultdict
from datetime import datetime, timezone


C2_INDICATORS = {
    "known_ports": {443, 8443, 8080, 4444, 5555, 8888, 9090, 1337},
    "suspicious_user_agents": [
        "mozilla/4.0", "python-requests", "curl/", "wget/",
        "java/", "go-http-client",
    ],
    "dns_c2_patterns": [
        r'^[a-z0-9]{30,}\.',  # Long random subdomain (DNS tunneling)
        r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$',  # Direct IP
    ],
}


def analyze_dns_queries(dns_log_path):
    """Analyze DNS query logs for C2 indicators."""
    findings = []
    domain_counts = defaultdict(int)
    try:
        with open(dns_log_path, "r") as f:
            for line in f:
                if line.startswith("#"):
                    continue
                fields = line.strip().split("\t")
                if len(fields) < 10:
                    continue
                query = fields[9] if len(fields) > 9 else ""
                domain_counts[query] += 1
                for pattern in C2_INDICATORS["dns_c2_patterns"]:
                    if re.match(pattern, query):
                        findings.append({
                            "type": "suspicious_dns",
                            "query": query,
                            "pattern": pattern,
                        })
    except FileNotFoundError:
        pass

    high_freq = sorted(domain_counts.items(), key=lambda x: x[1], reverse=True)[:20]
    for domain, count in high_freq:
        if count > 100 and len(domain) > 20:
            findings.append({
                "type": "high_frequency_dns",
                "domain": domain,
                "query_count": count,
            })
    return findings


def analyze_http_logs(http_log_path):
    """Analyze HTTP logs for C2-like traffic patterns."""
    findings = []
    try:
        with open(http_log_path, "r") as f:
            for line in f:
                if line.startswith("#"):
                    continue
                fields = line.strip().split("\t")
                if len(fields) < 13:
                    continue
                host = fields[8] if len(fields) > 8 else ""
                uri = fields[9] if len(fields) > 9 else ""
                user_agent = fields[12] if len(fields) > 12 else ""
                for ua in C2_INDICATORS["suspicious_user_agents"]:
                    if ua in user_agent.lower():
                        findings.append({
                            "type": "suspicious_user_agent",
                            "host": host,
                            "uri": uri[:100],
                            "user_agent": user_agent[:100],
                        })
                        break
                if re.match(r'^/[a-zA-Z0-9]{4,8}$', uri):
                    findings.append({
                        "type": "c2_uri_pattern",
                        "host": host,
                        "uri": uri,
                        "note": "Short random URI typical of C2 frameworks",
                    })
    except FileNotFoundError:
        pass
    return findings


def analyze_connection_patterns(conn_log_path):
    """Detect persistent long-duration connections typical of C2."""
    findings = []
    try:
        with open(conn_log_path, "r") as f:
            for line in f:
                if line.startswith("#"):
                    continue
                fields = line.strip().split("\t")
                if len(fields) < 10:
                    continue
                src = fields[2]
                dst = fields[4]
                dst_port = fields[5]
                duration = fields[8] if len(fields) > 8 else "0"
                orig_bytes = fields[9] if len(fields) > 9 else "0"
                resp_bytes = fields[10] if len(fields) > 10 else "0"
                try:
                    dur = float(duration) if duration != "-" else 0
                    ob = int(orig_bytes) if orig_bytes != "-" else 0
                    rb = int(resp_bytes) if resp_bytes != "-" else 0
                except ValueError:
                    continue
                if dur > 3600 and ob > 0 and rb > 0:
                    ratio = ob / rb if rb > 0 else 999
                    if 0.8 < ratio < 1.2:
                        findings.append({
                            "type": "persistent_symmetric",
                            "src": src, "dst": dst, "port": dst_port,
                            "duration_hours": round(dur / 3600, 1),
                            "data_ratio": round(ratio, 2),
                        })
    except FileNotFoundError:
        pass
    return findings


def main():
    parser = argparse.ArgumentParser(
        description="Hunt for C2 beaconing across network data sources"
    )
    parser.add_argument("--conn-log", help="Zeek conn.log")
    parser.add_argument("--dns-log", help="Zeek dns.log")
    parser.add_argument("--http-log", help="Zeek http.log")
    parser.add_argument("--output", "-o", help="Output JSON report")
    args = parser.parse_args()

    print("[*] C2 Beaconing Hunting Agent")
    report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": {}}

    if args.dns_log:
        dns = analyze_dns_queries(args.dns_log)
        report["findings"]["dns"] = dns
        print(f"[*] DNS findings: {len(dns)}")

    if args.http_log:
        http = analyze_http_logs(args.http_log)
        report["findings"]["http"] = http
        print(f"[*] HTTP findings: {len(http)}")

    if args.conn_log:
        conn = analyze_connection_patterns(args.conn_log)
        report["findings"]["connections"] = conn
        print(f"[*] Connection findings: {len(conn)}")

    total = sum(len(v) for v in report["findings"].values())
    report["risk_level"] = "CRITICAL" if total >= 10 else "HIGH" if total >= 5 else "MEDIUM" if total > 0 else "LOW"

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"[*] Report saved to {args.output}")
    else:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
