#!/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']}")
