Files
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

172 lines
6.1 KiB
Python

#!/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()