#!/usr/bin/env python3 """Agent for detecting C2 beaconing through network traffic frequency analysis.""" import argparse import json import math from collections import defaultdict from datetime import datetime, timezone def parse_zeek_conn_log(log_path): """Parse Zeek conn.log and extract connection timestamps per src-dst pair.""" connections = defaultdict(list) try: with open(log_path, "r") as f: for line in f: if line.startswith("#"): continue fields = line.strip().split("\t") if len(fields) < 7: continue ts = float(fields[0]) src, dst = fields[2], fields[4] dst_port = fields[5] key = f"{src}->{dst}:{dst_port}" connections[key].append(ts) except (FileNotFoundError, ValueError): pass return connections def calculate_jitter(intervals): """Calculate jitter (standard deviation of intervals).""" if len(intervals) < 2: return 0 mean = sum(intervals) / len(intervals) variance = sum((x - mean) ** 2 for x in intervals) / len(intervals) return math.sqrt(variance) def detect_beaconing(connections, min_connections=10, max_jitter_percent=15): """Detect beaconing patterns based on interval regularity.""" beacons = [] for key, timestamps in connections.items(): if len(timestamps) < min_connections: continue timestamps.sort() intervals = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)] if not intervals: continue mean_interval = sum(intervals) / len(intervals) if mean_interval == 0: continue jitter = calculate_jitter(intervals) jitter_percent = (jitter / mean_interval) * 100 if jitter_percent <= max_jitter_percent: parts = key.split("->") src = parts[0] dst_port = parts[1] if len(parts) > 1 else "" beacons.append({ "flow": key, "connection_count": len(timestamps), "mean_interval_seconds": round(mean_interval, 2), "jitter_seconds": round(jitter, 2), "jitter_percent": round(jitter_percent, 2), "duration_hours": round((timestamps[-1] - timestamps[0]) / 3600, 2), "confidence": "HIGH" if jitter_percent < 5 else "MEDIUM", }) return sorted(beacons, key=lambda x: x["jitter_percent"]) def parse_csv_log(csv_path): """Parse generic CSV log with timestamp, src, dst, port columns.""" connections = defaultdict(list) try: import csv with open(csv_path, "r") as f: reader = csv.DictReader(f) for row in reader: ts = row.get("timestamp") or row.get("ts") or row.get("time") src = row.get("src") or row.get("source") or row.get("src_ip") dst = row.get("dst") or row.get("destination") or row.get("dst_ip") port = row.get("dst_port") or row.get("port") or "" if ts and src and dst: try: ts_float = float(ts) except ValueError: from datetime import datetime as dt try: ts_float = dt.fromisoformat(ts.replace("Z", "+00:00")).timestamp() except ValueError: continue connections[f"{src}->{dst}:{port}"].append(ts_float) except (FileNotFoundError, KeyError): pass return connections def main(): parser = argparse.ArgumentParser( description="Detect C2 beaconing via frequency analysis" ) parser.add_argument("--conn-log", help="Zeek conn.log path") parser.add_argument("--csv", help="CSV log with timestamp, src, dst columns") parser.add_argument("--min-connections", type=int, default=10) parser.add_argument("--max-jitter", type=float, default=15, help="Max jitter percent") parser.add_argument("--output", "-o", help="Output JSON report") args = parser.parse_args() print("[*] Beaconing Detection via Frequency Analysis") connections = {} if args.conn_log: connections = parse_zeek_conn_log(args.conn_log) elif args.csv: connections = parse_csv_log(args.csv) print(f"[*] Unique flows: {len(connections)}") beacons = detect_beaconing(connections, args.min_connections, args.max_jitter) report = { "timestamp": datetime.now(timezone.utc).isoformat(), "flows_analyzed": len(connections), "beacons_detected": len(beacons), "beacons": beacons[:50], "risk_level": "CRITICAL" if beacons else "LOW", } print(f"[*] Beacons detected: {len(beacons)}") 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()