#!/usr/bin/env python3 """ MITRE ATT&CK Threat Actor TTP Analysis Script Queries the MITRE ATT&CK STIX data to: - Map threat actor groups to their known techniques - Generate ATT&CK Navigator layers for visualization - Perform detection gap analysis - Compare TTPs across multiple threat groups - Identify high-priority detection opportunities Requirements: pip install attackcti mitreattack-python stix2 requests Usage: python process.py --group APT29 --output apt29_layer.json python process.py --compare APT28 APT29 "Lazarus Group" python process.py --gap-analysis --detections detections.json --group APT29 """ import argparse import json import sys from collections import defaultdict from typing import Optional try: from attackcti import attack_client except ImportError: print("ERROR: attackcti not installed. Run: pip install attackcti") sys.exit(1) class ATTACKAnalyzer: """Analyze threat actor TTPs using MITRE ATT&CK.""" def __init__(self): print("[*] Initializing ATT&CK client (querying MITRE TAXII server)...") self.lift = attack_client() self.groups_cache = None self.techniques_cache = None def _get_groups(self): if self.groups_cache is None: self.groups_cache = self.lift.get_groups() return self.groups_cache def _get_techniques(self): if self.techniques_cache is None: self.techniques_cache = self.lift.get_enterprise_techniques() return self.techniques_cache def find_group(self, name: str) -> Optional[dict]: """Find a threat group by name or alias.""" groups = self._get_groups() for group in groups: if name.lower() == group.get("name", "").lower(): return group aliases = group.get("aliases", []) if any(name.lower() == a.lower() for a in aliases): return group return None def get_group_techniques(self, group_name: str) -> dict: """Get all techniques used by a threat group.""" group = self.find_group(group_name) if not group: print(f"[-] Group '{group_name}' not found") return {} group_id = "" for ref in group.get("external_references", []): if ref.get("source_name") == "mitre-attack": group_id = ref.get("external_id", "") break if not group_id: print(f"[-] No ATT&CK ID found for {group_name}") return {} techniques = self.lift.get_techniques_used_by_group(group_id) technique_map = {} for tech in techniques: tech_id = "" for ref in tech.get("external_references", []): if ref.get("source_name") == "mitre-attack": tech_id = ref.get("external_id", "") break if not tech_id: continue tactics = [ phase.get("phase_name", "") for phase in tech.get("kill_chain_phases", []) ] technique_map[tech_id] = { "name": tech.get("name", ""), "tactics": tactics, "description": tech.get("description", "")[:500], "platforms": tech.get("x_mitre_platforms", []), "data_sources": tech.get("x_mitre_data_sources", []), } print(f"[+] {group_name} ({group_id}): {len(technique_map)} techniques") return technique_map def create_navigator_layer(self, group_name: str, technique_map: dict, color: str = "#ff6666") -> dict: """Generate ATT&CK Navigator layer JSON.""" techniques_list = [] for tech_id, info in technique_map.items(): for tactic in info["tactics"]: techniques_list.append({ "techniqueID": tech_id, "tactic": tactic, "color": color, "comment": info["name"], "enabled": True, "score": 100, "metadata": [ {"name": "group", "value": group_name}, {"name": "platforms", "value": ", ".join(info["platforms"])}, ], }) layer = { "name": f"{group_name} TTP Coverage", "versions": { "attack": "16.1", "navigator": "5.1.0", "layer": "4.5", }, "domain": "enterprise-attack", "description": f"Techniques attributed to {group_name}", "filters": { "platforms": [ "Linux", "macOS", "Windows", "Cloud", "Azure AD", "Office 365", "SaaS", "Google Workspace", ] }, "sorting": 0, "layout": { "layout": "side", "aggregateFunction": "average", "showID": True, "showName": True, "showAggregateScores": False, "countUnscored": False, }, "hideDisabled": False, "techniques": techniques_list, "gradient": { "colors": ["#ffffff", color], "minValue": 0, "maxValue": 100, }, "legendItems": [ {"label": f"Used by {group_name}", "color": color}, {"label": "Not observed", "color": "#ffffff"}, ], "showTacticRowBackground": True, "tacticRowBackground": "#dddddd", "selectTechniquesAcrossTactics": True, "selectSubtechniquesWithParent": False, "selectVisibleTechniques": False, } return layer def compare_groups(self, group_names: list) -> dict: """Compare TTPs across multiple threat groups.""" group_techs = {} for name in group_names: techs = self.get_group_techniques(name) group_techs[name] = set(techs.keys()) if len(group_techs) < 2: print("[-] Need at least 2 groups for comparison") return {} all_techniques = set.union(*group_techs.values()) common_to_all = set.intersection(*group_techs.values()) comparison = { "groups": group_names, "total_unique_techniques": len(all_techniques), "common_to_all": sorted(common_to_all), "common_count": len(common_to_all), "per_group": {}, } for name, techs in group_techs.items(): others = set.union(*[t for n, t in group_techs.items() if n != name]) unique = techs - others comparison["per_group"][name] = { "total": len(techs), "unique": sorted(unique), "unique_count": len(unique), "overlap_percentage": round( len(techs.intersection(others)) / len(techs) * 100, 1 ) if techs else 0, } # Technique frequency across groups tech_freq = defaultdict(list) for name, techs in group_techs.items(): for t in techs: tech_freq[t].append(name) comparison["technique_frequency"] = { t: {"count": len(g), "groups": g} for t, g in sorted(tech_freq.items(), key=lambda x: -len(x[1])) } return comparison def gap_analysis(self, group_name: str, detected_techniques: set) -> dict: """Analyze detection gaps for a specific threat group.""" actor_techs = self.get_group_techniques(group_name) actor_tech_ids = set(actor_techs.keys()) covered = actor_tech_ids.intersection(detected_techniques) gaps = actor_tech_ids - detected_techniques gap_details = [] for tech_id in sorted(gaps): info = actor_techs.get(tech_id, {}) gap_details.append({ "technique_id": tech_id, "name": info.get("name", ""), "tactics": info.get("tactics", []), "data_sources": info.get("data_sources", []), "platforms": info.get("platforms", []), }) analysis = { "group": group_name, "total_actor_techniques": len(actor_tech_ids), "detected": len(covered), "gaps": len(gaps), "coverage_percentage": round( len(covered) / len(actor_tech_ids) * 100, 1 ) if actor_tech_ids else 0, "detected_techniques": sorted(covered), "gap_details": gap_details, "recommended_data_sources": self._recommend_data_sources(gap_details), } return analysis def _recommend_data_sources(self, gaps: list) -> list: """Recommend data sources that would close the most gaps.""" ds_coverage = defaultdict(list) for gap in gaps: for ds in gap.get("data_sources", []): ds_coverage[ds].append(gap["technique_id"]) recommendations = [ {"data_source": ds, "covers_techniques": techs, "count": len(techs)} for ds, techs in sorted(ds_coverage.items(), key=lambda x: -len(x[1])) ] return recommendations[:10] def tactic_breakdown(self, group_name: str) -> dict: """Break down threat actor techniques by tactic.""" techs = self.get_group_techniques(group_name) tactic_map = defaultdict(list) for tech_id, info in techs.items(): for tactic in info["tactics"]: tactic_map[tactic].append({ "id": tech_id, "name": info["name"], }) tactic_order = [ "reconnaissance", "resource-development", "initial-access", "execution", "persistence", "privilege-escalation", "defense-evasion", "credential-access", "discovery", "lateral-movement", "collection", "command-and-control", "exfiltration", "impact", ] breakdown = {} for tactic in tactic_order: if tactic in tactic_map: breakdown[tactic] = { "count": len(tactic_map[tactic]), "techniques": tactic_map[tactic], } return breakdown def main(): parser = argparse.ArgumentParser( description="MITRE ATT&CK Threat Actor TTP Analyzer" ) parser.add_argument("--group", help="Threat group name (e.g., APT29)") parser.add_argument("--compare", nargs="+", help="Compare multiple groups") parser.add_argument( "--gap-analysis", action="store_true", help="Perform detection gap analysis" ) parser.add_argument( "--detections", help="JSON file with detected technique IDs", ) parser.add_argument("--breakdown", action="store_true", help="Tactic breakdown") parser.add_argument("--output", default="attack_layer.json", help="Output file") args = parser.parse_args() analyzer = ATTACKAnalyzer() if args.compare: comparison = analyzer.compare_groups(args.compare) print(json.dumps(comparison, indent=2)) with open(args.output, "w") as f: json.dump(comparison, f, indent=2) elif args.group and args.gap_analysis: detected = set() if args.detections: with open(args.detections) as f: detected = set(json.load(f)) analysis = analyzer.gap_analysis(args.group, detected) print(json.dumps(analysis, indent=2)) with open(args.output, "w") as f: json.dump(analysis, f, indent=2) elif args.group and args.breakdown: breakdown = analyzer.tactic_breakdown(args.group) print(json.dumps(breakdown, indent=2)) elif args.group: tech_map = analyzer.get_group_techniques(args.group) layer = analyzer.create_navigator_layer(args.group, tech_map) with open(args.output, "w") as f: json.dump(layer, f, indent=2) print(f"[+] Navigator layer saved to {args.output}") else: parser.print_help() if __name__ == "__main__": main()