#!/usr/bin/env python3 """Threat actor TTP analysis agent using MITRE ATT&CK framework. Maps threat actor behaviors to ATT&CK techniques, performs coverage analysis, and generates detection gap reports. """ import os import sys import json from collections import Counter, defaultdict try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False ATTACK_ENTERPRISE_URL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" def load_attack_bundle(filepath=None): if filepath and os.path.exists(filepath): with open(filepath, "r", encoding="utf-8") as f: return json.load(f) if HAS_REQUESTS: resp = requests.get(ATTACK_ENTERPRISE_URL, timeout=60) resp.raise_for_status() return resp.json() return None def get_techniques(bundle): techniques = {} for obj in bundle.get("objects", []): if obj.get("type") == "attack-pattern" and not obj.get("revoked"): ext_id = "" for ref in obj.get("external_references", []): if ref.get("source_name") == "mitre-attack": ext_id = ref.get("external_id", "") if ext_id: tactics = [p["phase_name"] for p in obj.get("kill_chain_phases", [])] techniques[obj["id"]] = { "id": ext_id, "name": obj.get("name", ""), "tactics": tactics, "platforms": obj.get("x_mitre_platforms", []), "detection": obj.get("x_mitre_detection", "")[:200], } return techniques def get_groups(bundle): groups = {} for obj in bundle.get("objects", []): if obj.get("type") == "intrusion-set": ext_id = "" for ref in obj.get("external_references", []): if ref.get("source_name") == "mitre-attack": ext_id = ref.get("external_id", "") groups[obj["id"]] = { "id": ext_id, "name": obj.get("name", ""), "aliases": obj.get("aliases", []), "description": obj.get("description", "")[:300], } return groups def map_group_techniques(bundle, group_id, techniques): ttps = [] for obj in bundle.get("objects", []): if (obj.get("type") == "relationship" and obj.get("relationship_type") == "uses" and obj.get("source_ref") == group_id): target = obj.get("target_ref", "") if target in techniques: ttps.append(techniques[target]) return ttps def tactic_coverage(ttps): tactic_map = defaultdict(list) for t in ttps: for tactic in t["tactics"]: tactic_map[tactic].append(t["id"]) return {k: {"count": len(v), "techniques": v} for k, v in tactic_map.items()} def detection_gaps(ttps, existing_detections): covered = set(existing_detections) gaps = [t for t in ttps if t["id"] not in covered] coverage = 1 - (len(gaps) / len(ttps)) if ttps else 0 return gaps, round(coverage * 100, 1) def find_group(groups, query): query_lower = query.lower() for gid, g in groups.items(): if (g["name"].lower() == query_lower or g["id"].lower() == query_lower or query_lower in [a.lower() for a in g["aliases"]]): return gid, g return None, None if __name__ == "__main__": print("=" * 60) print("Threat Actor TTP Analysis Agent (MITRE ATT&CK)") print("TTP mapping, tactic coverage, detection gap analysis") print("=" * 60) group_query = sys.argv[1] if len(sys.argv) > 1 else None bundle_path = sys.argv[2] if len(sys.argv) > 2 else None bundle = load_attack_bundle(bundle_path) if not bundle: print("[!] Cannot load ATT&CK data") print("[DEMO] Usage: python agent.py APT29 [enterprise-attack.json]") sys.exit(1) techniques = get_techniques(bundle) groups = get_groups(bundle) print(f"[*] Loaded {len(techniques)} techniques, {len(groups)} groups") if not group_query: print("\n--- Available Groups (sample) ---") for gid, g in list(groups.items())[:15]: print(f" {g['id']:8s} {g['name']}") sys.exit(0) gid, ginfo = find_group(groups, group_query) if not ginfo: print(f"[!] Group not found: {group_query}") sys.exit(1) print(f"\n[*] Group: {ginfo['name']} ({ginfo['id']})") print(f" Aliases: {', '.join(ginfo['aliases'][:5])}") ttps = map_group_techniques(bundle, gid, techniques) print(f" Techniques: {len(ttps)}") coverage = tactic_coverage(ttps) print("\n--- Tactic Coverage ---") for tactic, info in sorted(coverage.items(), key=lambda x: -x[1]["count"]): bar = "#" * info["count"] print(f" {tactic:35s} {info['count']:3d} {bar}") sample_detections = [t["id"] for t in ttps[:len(ttps)//2]] gaps, pct = detection_gaps(ttps, sample_detections) print(f"\n--- Detection Gaps (demo: {pct}% coverage) ---") for g in gaps[:10]: print(f" [GAP] {g['id']:12s} {g['name']}")