Files
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

245 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""APT group analysis agent using MITRE ATT&CK Navigator layers.
Queries ATT&CK data, maps APT techniques to Navigator layers,
performs detection gap analysis, and generates threat-informed reports.
"""
import json
import os
import sys
from collections import Counter
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"
NAVIGATOR_LAYER_TEMPLATE = {
"name": "",
"versions": {"attack": "14", "navigator": "4.9.1", "layer": "4.5"},
"domain": "enterprise-attack",
"description": "",
"filters": {"platforms": ["Windows", "Linux", "macOS", "Cloud"]},
"sorting": 0,
"layout": {"layout": "side", "aggregateFunction": "average", "showID": False,
"showName": True, "showAggregateScores": False, "countUnscored": False},
"hideDisabled": False,
"techniques": [],
"gradient": {"colors": ["#ffffff", "#ff6666"], "minValue": 0, "maxValue": 100},
"legendItems": [],
"metadata": [],
"links": [],
"showTacticRowBackground": False,
"tacticRowBackground": "#dddddd",
"selectTechniquesAcrossTactics": True,
"selectSubtechniquesWithParent": False,
"selectVisibleTechniques": False,
}
def load_attack_data(filepath=None):
"""Load ATT&CK STIX bundle from file or download."""
if filepath and os.path.exists(filepath):
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
if HAS_REQUESTS:
print("[*] Downloading ATT&CK Enterprise data...")
resp = requests.get(ATTACK_ENTERPRISE_URL, timeout=60)
resp.raise_for_status()
return resp.json()
return None
def extract_groups(bundle):
"""Extract intrusion-set (APT group) objects from STIX bundle."""
groups = {}
for obj in bundle.get("objects", []):
if obj.get("type") == "intrusion-set":
name = obj.get("name", "Unknown")
aliases = obj.get("aliases", [])
ext_refs = obj.get("external_references", [])
attack_id = ""
for ref in ext_refs:
if ref.get("source_name") == "mitre-attack":
attack_id = ref.get("external_id", "")
break
groups[obj["id"]] = {
"name": name, "id": attack_id, "aliases": aliases,
"description": obj.get("description", "")[:200],
}
return groups
def extract_techniques(bundle):
"""Extract attack-pattern (technique) objects from STIX bundle."""
techniques = {}
for obj in bundle.get("objects", []):
if obj.get("type") == "attack-pattern" and not obj.get("revoked", False):
ext_refs = obj.get("external_references", [])
attack_id = ""
for ref in ext_refs:
if ref.get("source_name") == "mitre-attack":
attack_id = ref.get("external_id", "")
break
if attack_id:
tactics = [p["phase_name"] for p in obj.get("kill_chain_phases", [])]
techniques[obj["id"]] = {
"id": attack_id, "name": obj.get("name", ""),
"tactics": tactics, "platforms": obj.get("x_mitre_platforms", []),
}
return techniques
def map_group_techniques(bundle, group_stix_id, techniques):
"""Map techniques used by a specific group via relationship objects."""
group_techniques = []
for obj in bundle.get("objects", []):
if (obj.get("type") == "relationship" and
obj.get("relationship_type") == "uses" and
obj.get("source_ref") == group_stix_id and
obj.get("target_ref", "").startswith("attack-pattern--")):
tech_id = obj["target_ref"]
if tech_id in techniques:
group_techniques.append(techniques[tech_id])
return group_techniques
def build_navigator_layer(group_name, group_techniques, color="#ff6666", score=100):
"""Build ATT&CK Navigator JSON layer for a group's techniques."""
layer = json.loads(json.dumps(NAVIGATOR_LAYER_TEMPLATE))
layer["name"] = f"{group_name} - TTPs"
layer["description"] = f"ATT&CK techniques attributed to {group_name}"
for tech in group_techniques:
entry = {
"techniqueID": tech["id"],
"tactic": tech["tactics"][0] if tech["tactics"] else "",
"color": color,
"comment": f"Used by {group_name}",
"enabled": True,
"metadata": [],
"links": [],
"showSubtechniques": False,
"score": score,
}
layer["techniques"].append(entry)
return layer
def detection_gap_analysis(group_techniques, detection_rules):
"""Compare group TTPs against existing detection rules to find gaps."""
covered = set()
for rule in detection_rules:
tech_id = rule.get("technique_id", "")
if tech_id:
covered.add(tech_id)
gaps = []
for tech in group_techniques:
if tech["id"] not in covered:
gaps.append({
"technique_id": tech["id"],
"technique_name": tech["name"],
"tactics": tech["tactics"],
"status": "NO DETECTION",
})
coverage_pct = (len(covered & {t["id"] for t in group_techniques}) /
len(group_techniques) * 100) if group_techniques else 0
return gaps, round(coverage_pct, 1)
def tactic_heatmap(group_techniques):
"""Generate tactic-level heatmap showing technique distribution."""
tactic_counts = Counter()
for tech in group_techniques:
for tactic in tech["tactics"]:
tactic_counts[tactic] += 1
return dict(tactic_counts.most_common())
def compare_groups(group_a_techs, group_b_techs):
"""Compare two groups' technique sets for overlap analysis."""
set_a = {t["id"] for t in group_a_techs}
set_b = {t["id"] for t in group_b_techs}
overlap = set_a & set_b
only_a = set_a - set_b
only_b = set_b - set_a
jaccard = len(overlap) / len(set_a | set_b) if (set_a | set_b) else 0
return {
"overlap_count": len(overlap), "overlap_ids": sorted(overlap),
"only_group_a": len(only_a), "only_group_b": len(only_b),
"jaccard_similarity": round(jaccard, 4),
}
def save_layer(layer, output_path):
"""Save Navigator layer to JSON file."""
with open(output_path, "w", encoding="utf-8") as f:
json.dump(layer, f, indent=2)
print(f"[+] Layer saved: {output_path}")
if __name__ == "__main__":
print("=" * 60)
print("APT Group Analysis Agent - MITRE ATT&CK Navigator")
print("TTP mapping, detection gap analysis, group comparison")
print("=" * 60)
group_name = sys.argv[1] if len(sys.argv) > 1 else None
attack_file = sys.argv[2] if len(sys.argv) > 2 else None
bundle = load_attack_data(attack_file)
if not bundle:
print("\n[!] Cannot load ATT&CK data. Provide STIX bundle path or install requests.")
print("[DEMO] Usage:")
print(" python agent.py APT29 enterprise-attack.json")
print(" python agent.py APT28 # downloads from GitHub")
sys.exit(1)
groups = extract_groups(bundle)
techniques = extract_techniques(bundle)
print(f"[*] Loaded {len(groups)} groups, {len(techniques)} techniques")
if not group_name:
print("\n--- Available APT Groups (sample) ---")
for gid, g in list(groups.items())[:20]:
print(f" {g['id']:8s} {g['name']:30s} aliases={g['aliases'][:3]}")
sys.exit(0)
target_group = None
for gid, g in groups.items():
if (g["name"].lower() == group_name.lower() or
g["id"].lower() == group_name.lower() or
group_name.lower() in [a.lower() for a in g["aliases"]]):
target_group = (gid, g)
break
if not target_group:
print(f"[!] Group '{group_name}' not found")
sys.exit(1)
gid, ginfo = target_group
print(f"\n[*] Group: {ginfo['name']} ({ginfo['id']})")
print(f" Aliases: {', '.join(ginfo['aliases'][:5])}")
group_techs = map_group_techniques(bundle, gid, techniques)
print(f" Techniques: {len(group_techs)}")
heatmap = tactic_heatmap(group_techs)
print("\n--- Tactic Heatmap ---")
for tactic, count in heatmap.items():
bar = "#" * count
print(f" {tactic:35s} {count:3d} {bar}")
layer = build_navigator_layer(ginfo["name"], group_techs)
out_file = f"{ginfo['name'].replace(' ', '_')}_layer.json"
save_layer(layer, out_file)
sample_rules = [{"technique_id": t["id"]} for t in group_techs[:len(group_techs)//2]]
gaps, coverage = detection_gap_analysis(group_techs, sample_rules)
print(f"\n--- Detection Gap Analysis (demo: {coverage}% coverage) ---")
for gap in gaps[:10]:
print(f" [GAP] {gap['technique_id']:12s} {gap['technique_name']}")