#!/usr/bin/env python3
"""Agent for hunting unusual network connections from endpoint and firewall logs."""

import json
import argparse
from datetime import datetime
from collections import defaultdict, Counter


COMMON_PORTS = {80, 443, 53, 22, 25, 110, 143, 993, 995, 587, 8080, 8443, 3389}

KNOWN_BAD_PORTS = {4444, 5555, 1234, 9999, 31337, 6666, 6667, 8888, 12345}

PRIVATE_RANGES = [
    (0x0A000000, 0x0AFFFFFF),   # 10.0.0.0/8
    (0xAC100000, 0xAC1FFFFF),   # 172.16.0.0/12
    (0xC0A80000, 0xC0A8FFFF),   # 192.168.0.0/16
]


def ip_to_int(ip):
    """Convert dotted IP to integer."""
    parts = ip.split(".")
    if len(parts) != 4:
        return 0
    try:
        return (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])
    except ValueError:
        return 0


def is_private(ip):
    """Check if IP is in private RFC1918 range."""
    val = ip_to_int(ip)
    return any(start <= val <= end for start, end in PRIVATE_RANGES)


def load_connection_logs(log_path):
    """Load network connection logs from JSON lines."""
    entries = []
    with open(log_path) as f:
        for line in f:
            try:
                entries.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return entries


def detect_non_standard_ports(connections):
    """Find connections to unusual destination ports."""
    findings = []
    for conn in connections:
        dst_port = int(conn.get("dest_port", conn.get("dst_port", 0)))
        if dst_port in KNOWN_BAD_PORTS:
            findings.append({
                "src_ip": conn.get("src_ip", conn.get("source_ip", "")),
                "dst_ip": conn.get("dst_ip", conn.get("dest_ip", "")),
                "dst_port": dst_port,
                "process": conn.get("process", conn.get("image", "")),
                "severity": "CRITICAL",
                "reason": "known_bad_port",
            })
        elif dst_port not in COMMON_PORTS and dst_port > 0:
            findings.append({
                "src_ip": conn.get("src_ip", conn.get("source_ip", "")),
                "dst_ip": conn.get("dst_ip", conn.get("dest_ip", "")),
                "dst_port": dst_port,
                "process": conn.get("process", conn.get("image", "")),
                "severity": "MEDIUM",
                "reason": "non_standard_port",
            })
    return findings


def detect_rare_destinations(connections, threshold=3):
    """Find rarely contacted external destinations."""
    dest_counts = Counter()
    dest_conns = defaultdict(list)
    for conn in connections:
        dst = conn.get("dst_ip", conn.get("dest_ip", ""))
        if dst and not is_private(dst):
            dest_counts[dst] += 1
            dest_conns[dst].append(conn)
    findings = []
    for dst, count in dest_counts.items():
        if count <= threshold:
            sample = dest_conns[dst][0]
            findings.append({
                "dst_ip": dst,
                "connection_count": count,
                "src_ip": sample.get("src_ip", sample.get("source_ip", "")),
                "process": sample.get("process", sample.get("image", "")),
                "severity": "HIGH",
                "reason": "rare_destination",
            })
    return sorted(findings, key=lambda x: x["connection_count"])


def detect_long_connections(connections, duration_threshold=3600):
    """Find unusually long-lived connections (potential C2)."""
    findings = []
    for conn in connections:
        duration = conn.get("duration", conn.get("connection_duration", 0))
        try:
            duration = float(duration)
        except (TypeError, ValueError):
            continue
        if duration > duration_threshold:
            findings.append({
                "src_ip": conn.get("src_ip", conn.get("source_ip", "")),
                "dst_ip": conn.get("dst_ip", conn.get("dest_ip", "")),
                "dst_port": conn.get("dest_port", conn.get("dst_port", "")),
                "duration_seconds": duration,
                "process": conn.get("process", conn.get("image", "")),
                "severity": "HIGH",
                "reason": "long_duration_connection",
            })
    return sorted(findings, key=lambda x: x["duration_seconds"], reverse=True)


def detect_high_frequency_beaconing(connections, interval_threshold=60):
    """Detect periodic connections suggestive of beaconing."""
    by_dest = defaultdict(list)
    for conn in connections:
        dst = conn.get("dst_ip", conn.get("dest_ip", ""))
        ts = conn.get("timestamp", conn.get("ts", ""))
        if dst and ts:
            try:
                t = datetime.fromisoformat(str(ts).replace("Z", "+00:00"))
                by_dest[dst].append(t)
            except (ValueError, TypeError):
                continue
    findings = []
    for dst, times in by_dest.items():
        if len(times) < 5:
            continue
        times.sort()
        intervals = [(times[i+1] - times[i]).total_seconds() for i in range(len(times)-1)]
        avg = sum(intervals) / len(intervals)
        if avg < 1:
            continue
        std = (sum((x - avg)**2 for x in intervals) / len(intervals)) ** 0.5
        cv = std / avg if avg > 0 else 999
        if cv < 0.3 and avg < interval_threshold:
            findings.append({
                "dst_ip": dst, "connection_count": len(times),
                "avg_interval_sec": round(avg, 2), "cv": round(cv, 3),
                "severity": "CRITICAL", "reason": "periodic_beaconing",
            })
    return findings


def main():
    parser = argparse.ArgumentParser(description="Unusual Network Connection Hunter")
    parser.add_argument("--log", required=True, help="JSON lines connection log")
    parser.add_argument("--output", default="unusual_network_hunt_report.json")
    parser.add_argument("--action", choices=[
        "ports", "rare", "long", "beacon", "full_analysis"
    ], default="full_analysis")
    args = parser.parse_args()

    conns = load_connection_logs(args.log)
    report = {"generated_at": datetime.utcnow().isoformat(), "total_connections": len(conns),
              "findings": {}}
    print(f"[+] Loaded {len(conns)} connections")

    if args.action in ("ports", "full_analysis"):
        f = detect_non_standard_ports(conns)
        report["findings"]["non_standard_ports"] = f
        print(f"[+] Non-standard port connections: {len(f)}")

    if args.action in ("rare", "full_analysis"):
        f = detect_rare_destinations(conns)
        report["findings"]["rare_destinations"] = f
        print(f"[+] Rare destinations: {len(f)}")

    if args.action in ("long", "full_analysis"):
        f = detect_long_connections(conns)
        report["findings"]["long_connections"] = f
        print(f"[+] Long-lived connections: {len(f)}")

    if args.action in ("beacon", "full_analysis"):
        f = detect_high_frequency_beaconing(conns)
        report["findings"]["beaconing"] = f
        print(f"[+] Beaconing patterns: {len(f)}")

    with open(args.output, "w") as fout:
        json.dump(report, fout, indent=2, default=str)
    print(f"[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
