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
166 lines
5.9 KiB
Python
166 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
DNS Tunneling Detection Script for Zeek Logs
|
|
Analyzes dns.log for tunneling indicators: query length, entropy,
|
|
unique subdomains, record types, and query volume.
|
|
"""
|
|
|
|
import json
|
|
import csv
|
|
import argparse
|
|
import datetime
|
|
import math
|
|
from collections import defaultdict, Counter
|
|
from pathlib import Path
|
|
|
|
ENTROPY_THRESHOLD = 3.5
|
|
QUERY_LENGTH_THRESHOLD = 50
|
|
SUBDOMAIN_LENGTH_THRESHOLD = 25
|
|
UNIQUE_SUBDOMAIN_THRESHOLD = 50
|
|
QUERY_COUNT_THRESHOLD = 100
|
|
|
|
SUSPICIOUS_RECORD_TYPES = {"TXT", "NULL", "CNAME", "MX", "KEY", "SRV"}
|
|
|
|
|
|
def calculate_entropy(text: str) -> float:
|
|
if not text:
|
|
return 0.0
|
|
counts = Counter(text)
|
|
length = len(text)
|
|
return -sum((c / length) * math.log2(c / length) for c in counts.values())
|
|
|
|
|
|
def parse_zeek_dns(input_path: str) -> list[dict]:
|
|
path = Path(input_path)
|
|
events = []
|
|
if path.suffix == ".json":
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
events = data if isinstance(data, list) else data.get("events", [])
|
|
elif path.suffix == ".csv":
|
|
with open(path, "r", encoding="utf-8-sig") as f:
|
|
events = [dict(row) for row in csv.DictReader(f)]
|
|
elif path.suffix == ".log":
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
headers = None
|
|
for line in f:
|
|
if line.startswith("#fields"):
|
|
headers = line.strip().split("\t")[1:]
|
|
elif line.startswith("#"):
|
|
continue
|
|
elif headers:
|
|
values = line.strip().split("\t")
|
|
if len(values) == len(headers):
|
|
events.append(dict(zip(headers, values)))
|
|
return events
|
|
|
|
|
|
def detect_dns_tunneling(queries: list[dict]) -> list[dict]:
|
|
domain_stats = defaultdict(lambda: {
|
|
"queries": 0, "unique_subdomains": set(), "total_length": 0,
|
|
"max_length": 0, "record_types": Counter(), "sources": set(),
|
|
"subdomain_entropy_samples": [],
|
|
})
|
|
|
|
for q in queries:
|
|
query = q.get("query", "")
|
|
src = q.get("id.orig_h", q.get("src_ip", q.get("source_ip", "")))
|
|
qtype = q.get("qtype_name", q.get("query_type", "A"))
|
|
if not query or len(query.split(".")) < 3:
|
|
continue
|
|
|
|
parts = query.rstrip(".").split(".")
|
|
base_domain = ".".join(parts[-2:])
|
|
subdomain = ".".join(parts[:-2])
|
|
|
|
stats = domain_stats[(src, base_domain)]
|
|
stats["queries"] += 1
|
|
stats["unique_subdomains"].add(subdomain)
|
|
stats["total_length"] += len(query)
|
|
stats["max_length"] = max(stats["max_length"], len(query))
|
|
stats["record_types"][qtype] += 1
|
|
stats["sources"].add(src)
|
|
if len(stats["subdomain_entropy_samples"]) < 100:
|
|
stats["subdomain_entropy_samples"].append(subdomain)
|
|
|
|
findings = []
|
|
for (src, base_domain), stats in domain_stats.items():
|
|
if stats["queries"] < 20:
|
|
continue
|
|
|
|
avg_len = stats["total_length"] / stats["queries"]
|
|
unique_subs = len(stats["unique_subdomains"])
|
|
|
|
avg_entropy = 0.0
|
|
if stats["subdomain_entropy_samples"]:
|
|
entropies = [calculate_entropy(s) for s in stats["subdomain_entropy_samples"]]
|
|
avg_entropy = sum(entropies) / len(entropies)
|
|
|
|
risk = 0
|
|
indicators = []
|
|
|
|
if avg_len > QUERY_LENGTH_THRESHOLD:
|
|
risk += 25
|
|
indicators.append(f"High avg query length: {avg_len:.1f}")
|
|
if unique_subs > UNIQUE_SUBDOMAIN_THRESHOLD:
|
|
risk += 25
|
|
indicators.append(f"High unique subdomains: {unique_subs}")
|
|
if avg_entropy > ENTROPY_THRESHOLD:
|
|
risk += 25
|
|
indicators.append(f"High subdomain entropy: {avg_entropy:.2f}")
|
|
if stats["queries"] > QUERY_COUNT_THRESHOLD:
|
|
risk += 10
|
|
indicators.append(f"High query volume: {stats['queries']}")
|
|
|
|
suspicious_types = sum(stats["record_types"].get(rt, 0) for rt in SUSPICIOUS_RECORD_TYPES)
|
|
if suspicious_types > stats["queries"] * 0.5:
|
|
risk += 15
|
|
indicators.append(f"Unusual record types: {dict(stats['record_types'])}")
|
|
|
|
if risk < 30:
|
|
continue
|
|
|
|
risk_level = "CRITICAL" if risk >= 70 else "HIGH" if risk >= 50 else "MEDIUM"
|
|
findings.append({
|
|
"source_ip": src,
|
|
"base_domain": base_domain,
|
|
"query_count": stats["queries"],
|
|
"unique_subdomains": unique_subs,
|
|
"avg_query_length": round(avg_len, 1),
|
|
"max_query_length": stats["max_length"],
|
|
"avg_subdomain_entropy": round(avg_entropy, 3),
|
|
"record_types": dict(stats["record_types"]),
|
|
"risk_score": risk,
|
|
"risk_level": risk_level,
|
|
"indicators": indicators,
|
|
})
|
|
|
|
return sorted(findings, key=lambda x: x["risk_score"], reverse=True)
|
|
|
|
|
|
def run_hunt(input_path: str, output_dir: str) -> None:
|
|
print(f"[*] DNS Tunneling Hunt - {datetime.datetime.now().isoformat()}")
|
|
queries = parse_zeek_dns(input_path)
|
|
print(f"[*] Loaded {len(queries)} DNS queries")
|
|
findings = detect_dns_tunneling(queries)
|
|
print(f"[!] DNS tunneling detections: {len(findings)}")
|
|
|
|
output_path = Path(output_dir)
|
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
with open(output_path / "dns_tunnel_findings.json", "w", encoding="utf-8") as f:
|
|
json.dump({"hunt_id": f"TH-DNSTUNNEL-{datetime.date.today().isoformat()}",
|
|
"total_queries": len(queries), "findings": findings}, f, indent=2)
|
|
print(f"[+] Results written to {output_dir}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="DNS Tunneling Detection")
|
|
parser.add_argument("--input", "-i", required=True)
|
|
parser.add_argument("--output", "-o", default="./dns_tunnel_hunt_output")
|
|
args = parser.parse_args()
|
|
run_hunt(args.input, args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|