#!/usr/bin/env python3 """Threat campaign correlation agent using MISP and STIX.""" import json import sys import urllib.request import ssl from collections import Counter from datetime import datetime class MISPClient: """Client for MISP REST API for campaign correlation.""" def __init__(self, url, api_key, verify_ssl=False): self.base_url = url.rstrip("/") self.headers = { "Authorization": api_key, "Content-Type": "application/json", "Accept": "application/json", } self.ctx = ssl.create_default_context() if not verify_ssl: self.ctx.check_hostname = False self.ctx.verify_mode = ssl.CERT_NONE def _request(self, method, path, data=None): url = f"{self.base_url}{path}" body = json.dumps(data).encode() if data else None req = urllib.request.Request(url, data=body, headers=self.headers, method=method) try: with urllib.request.urlopen(req, context=self.ctx, timeout=60) as resp: return json.loads(resp.read().decode()) except Exception as e: return {"error": str(e)} def search_attributes(self, attr_type, value): """Search MISP for attributes matching type and value.""" data = {"type": attr_type, "value": value, "searchall": 1} return self._request("POST", "/attributes/restSearch", data) def search_events(self, tags=None, date_from=None, date_to=None): """Search MISP events with filters.""" data = {} if tags: data["tags"] = tags if date_from: data["from"] = date_from if date_to: data["to"] = date_to return self._request("POST", "/events/restSearch", data) def get_event(self, event_id): """Get full event details by ID.""" return self._request("GET", f"/events/view/{event_id}") def get_correlations(self, event_id): """Retrieve correlation data for a MISP event.""" event = self.get_event(event_id) if "error" in event: return event correlations = [] ev = event.get("Event", event) for attr in ev.get("Attribute", []): if attr.get("RelatedAttribute"): for rel in attr["RelatedAttribute"]: correlations.append({ "source_event": event_id, "source_attr": attr.get("value"), "source_type": attr.get("type"), "related_event": rel.get("event_id"), "related_value": rel.get("value"), }) return correlations def calculate_campaign_confidence(events): """Calculate campaign attribution confidence from correlated events.""" if not events or len(events) < 2: return {"confidence": "LOW", "score": 0, "reason": "Insufficient events"} all_ips = [] all_domains = [] all_hashes = [] all_tags = [] for event in events: ev = event.get("Event", event) for attr in ev.get("Attribute", []): atype = attr.get("type", "") val = attr.get("value", "") if atype in ("ip-src", "ip-dst"): all_ips.append(val) elif atype in ("domain", "hostname"): all_domains.append(val) elif "hash" in atype or "md5" in atype or "sha" in atype: all_hashes.append(val) for tag in ev.get("Tag", []): all_tags.append(tag.get("name", "")) ip_counts = Counter(all_ips) domain_counts = Counter(all_domains) hash_counts = Counter(all_hashes) shared_ips = sum(1 for c in ip_counts.values() if c > 1) shared_domains = sum(1 for c in domain_counts.values() if c > 1) shared_hashes = sum(1 for c in hash_counts.values() if c > 1) num_events = len(events) infra_score = min(40, (shared_ips + shared_domains) / max(num_events, 1) * 40) capability_score = min(35, shared_hashes / max(num_events, 1) * 35) tag_overlap = len(set(all_tags)) / max(len(all_tags), 1) ttp_score = min(15, tag_overlap * 15) total = infra_score + capability_score + ttp_score if total >= 70: confidence = "HIGH" elif total >= 45: confidence = "MEDIUM" else: confidence = "LOW" return { "confidence": confidence, "score": round(total, 1), "shared_infrastructure": {"ips": shared_ips, "domains": shared_domains}, "shared_capabilities": {"hashes": shared_hashes}, "events_analyzed": num_events, } def extract_campaign_iocs(events): """Extract shared IOCs across correlated events for blocking.""" ioc_events = {} for event in events: ev = event.get("Event", event) eid = ev.get("id", "unknown") for attr in ev.get("Attribute", []): val = attr.get("value", "") atype = attr.get("type", "") key = f"{atype}:{val}" if key not in ioc_events: ioc_events[key] = [] ioc_events[key].append(eid) shared = {k: v for k, v in ioc_events.items() if len(v) > 1} return { "total_unique_iocs": len(ioc_events), "shared_iocs": len(shared), "shared_indicators": [ {"type": k.split(":")[0], "value": ":".join(k.split(":")[1:]), "event_count": len(v)} for k, v in sorted(shared.items(), key=lambda x: len(x[1]), reverse=True) ][:50], } def build_campaign_report(campaign_name, events, attribution=None): """Build a structured campaign intelligence report.""" confidence = calculate_campaign_confidence(events) iocs = extract_campaign_iocs(events) dates = [] targets = [] for event in events: ev = event.get("Event", event) if ev.get("date"): dates.append(ev["date"]) info = ev.get("info", "") if info: targets.append(info) return { "campaign_name": campaign_name, "report_date": datetime.utcnow().isoformat() + "Z", "timeline": {"first_seen": min(dates) if dates else None, "last_seen": max(dates) if dates else None}, "attribution": attribution or "Unattributed", "confidence": confidence, "shared_indicators": iocs, "events_correlated": len(events), "target_summary": targets[:10], } if __name__ == "__main__": import os misp_url = os.environ.get("MISP_URL", "https://misp.example.com") misp_key = os.environ.get("MISP_KEY", "") action = sys.argv[1] if len(sys.argv) > 1 else "help" if action == "search" and len(sys.argv) > 3: client = MISPClient(misp_url, misp_key) result = client.search_attributes(sys.argv[2], sys.argv[3]) print(json.dumps(result, indent=2, default=str)) elif action == "correlations" and len(sys.argv) > 2: client = MISPClient(misp_url, misp_key) result = client.get_correlations(sys.argv[2]) print(json.dumps(result, indent=2, default=str)) elif action == "events": client = MISPClient(misp_url, misp_key) tags = sys.argv[2] if len(sys.argv) > 2 else None result = client.search_events(tags=tags) print(json.dumps(result, indent=2, default=str)) else: print("Usage: agent.py [search |correlations |events [tag]]") print("Env: MISP_URL, MISP_KEY")