Files
Anthropic-Cybersecurity-Skills/skills/correlating-threat-campaigns/scripts/agent.py
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

206 lines
7.3 KiB
Python

#!/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 <type> <value>|correlations <event_id>|events [tag]]")
print("Env: MISP_URL, MISP_KEY")