mirror of
https://github.com/SHOGGOTH-SECTOR/sica-fondt.git
synced 2026-08-01 08:30:20 +00:00
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):
- brain/ LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
dispatch, A51 channels, and the OSINT cluster
- knowledge/ LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
MITRE ATT&CK data
- reference/ defensive threat-reference (C3, shhbruh doc) + AdaYaml parser
License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.
Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.
https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
172 lines
5.5 KiB
Python
172 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Agent for detecting DNS tunneling using Zeek log analysis."""
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import sys
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
ENTROPY_THRESHOLD = 3.5
|
|
MIN_QUERIES_PER_DOMAIN = 20
|
|
MAX_NORMAL_SUBDOMAIN_LEN = 30
|
|
TUNNEL_QUERY_TYPES = {"TXT", "NULL", "CNAME", "MX"}
|
|
|
|
|
|
def shannon_entropy(data):
|
|
"""Calculate Shannon entropy of a string."""
|
|
if not data:
|
|
return 0.0
|
|
freq = defaultdict(int)
|
|
for c in data:
|
|
freq[c] += 1
|
|
n = len(data)
|
|
return -sum((cnt/n) * math.log2(cnt/n) for cnt in freq.values())
|
|
|
|
|
|
def load_dns_log(filepath):
|
|
"""Load Zeek dns.log (TSV format)."""
|
|
entries = []
|
|
try:
|
|
with open(filepath, "r") as f:
|
|
for line in f:
|
|
if line.startswith("#"):
|
|
continue
|
|
parts = line.strip().split("\t")
|
|
if len(parts) >= 10:
|
|
entries.append({
|
|
"ts": parts[0],
|
|
"uid": parts[1],
|
|
"src": parts[2],
|
|
"src_port": parts[3],
|
|
"dst": parts[4],
|
|
"dst_port": parts[5],
|
|
"query": parts[9] if len(parts) > 9 else "",
|
|
"qtype": parts[13] if len(parts) > 13 else "",
|
|
"answers": parts[21] if len(parts) > 21 else "",
|
|
})
|
|
except (OSError, IndexError) as e:
|
|
print(f"[!] Error loading DNS log: {e}")
|
|
return entries
|
|
|
|
|
|
def analyze_domain_statistics(entries):
|
|
"""Compute per-domain statistics for tunneling detection."""
|
|
domain_data = defaultdict(lambda: {
|
|
"queries": [], "subdomains": [], "qtypes": defaultdict(int),
|
|
"sources": set(), "total_subdomain_len": 0,
|
|
})
|
|
|
|
for entry in entries:
|
|
query = entry.get("query", "")
|
|
if not query or query == "-":
|
|
continue
|
|
parts = query.rstrip(".").split(".")
|
|
if len(parts) < 2:
|
|
continue
|
|
domain = ".".join(parts[-2:])
|
|
subdomain = ".".join(parts[:-2])
|
|
d = domain_data[domain]
|
|
d["queries"].append(query)
|
|
d["subdomains"].append(subdomain)
|
|
d["qtypes"][entry.get("qtype", "")] += 1
|
|
d["sources"].add(entry.get("src", ""))
|
|
d["total_subdomain_len"] += len(subdomain)
|
|
|
|
return domain_data
|
|
|
|
|
|
def detect_tunneling(domain_data):
|
|
"""Apply tunneling detection heuristics."""
|
|
findings = []
|
|
for domain, data in domain_data.items():
|
|
query_count = len(data["queries"])
|
|
if query_count < MIN_QUERIES_PER_DOMAIN:
|
|
continue
|
|
|
|
avg_subdomain_len = data["total_subdomain_len"] / query_count
|
|
all_subdomain_text = "".join(data["subdomains"])
|
|
entropy = shannon_entropy(all_subdomain_text)
|
|
|
|
tunnel_qtype_count = sum(
|
|
data["qtypes"].get(qt, 0) for qt in TUNNEL_QUERY_TYPES
|
|
)
|
|
tunnel_qtype_ratio = tunnel_qtype_count / query_count if query_count else 0
|
|
|
|
score = 0
|
|
if entropy > ENTROPY_THRESHOLD:
|
|
score += 40
|
|
if avg_subdomain_len > MAX_NORMAL_SUBDOMAIN_LEN:
|
|
score += 30
|
|
if tunnel_qtype_ratio > 0.5:
|
|
score += 20
|
|
if query_count > 500:
|
|
score += 10
|
|
|
|
if score >= 40:
|
|
findings.append({
|
|
"domain": domain,
|
|
"query_count": query_count,
|
|
"avg_subdomain_length": round(avg_subdomain_len, 1),
|
|
"entropy": round(entropy, 3),
|
|
"tunnel_qtype_ratio": round(tunnel_qtype_ratio, 3),
|
|
"unique_sources": len(data["sources"]),
|
|
"tunnel_score": score,
|
|
"severity": "CRITICAL" if score >= 70 else "HIGH" if score >= 50 else "MEDIUM",
|
|
})
|
|
|
|
findings.sort(key=lambda f: f["tunnel_score"], reverse=True)
|
|
return findings
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="DNS tunneling detection agent using Zeek logs"
|
|
)
|
|
parser.add_argument("dns_log", help="Path to Zeek dns.log")
|
|
parser.add_argument("--min-queries", type=int, default=20)
|
|
parser.add_argument("--entropy-threshold", type=float, default=3.5)
|
|
parser.add_argument("--output", "-o", help="Output JSON report")
|
|
parser.add_argument("--verbose", "-v", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
global MIN_QUERIES_PER_DOMAIN, ENTROPY_THRESHOLD
|
|
MIN_QUERIES_PER_DOMAIN = args.min_queries
|
|
ENTROPY_THRESHOLD = args.entropy_threshold
|
|
|
|
print("[*] DNS Tunneling Detection Agent (Zeek)")
|
|
entries = load_dns_log(args.dns_log)
|
|
if not entries:
|
|
print("[!] No DNS entries loaded")
|
|
sys.exit(1)
|
|
|
|
print(f"[*] Loaded {len(entries)} DNS queries")
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"source_file": args.dns_log,
|
|
"total_queries": len(entries),
|
|
"findings": [],
|
|
}
|
|
|
|
domain_data = analyze_domain_statistics(entries)
|
|
findings = detect_tunneling(domain_data)
|
|
report["findings"] = findings
|
|
report["risk_level"] = (
|
|
"CRITICAL" if any(f["severity"] == "CRITICAL" for f in findings)
|
|
else "HIGH" if findings else "LOW"
|
|
)
|
|
|
|
print(f"[*] Detected {len(findings)} suspected DNS tunnels")
|
|
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()
|