#!/usr/bin/env python3
"""SIEM Correlation Rules Agent - Builds and deploys multi-event APT detection rules via Splunk and Sigma."""

import json
import os
import time
import logging
import argparse
from datetime import datetime

import yaml
import requests

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

LATERAL_MOVEMENT_RULES = [
    {
        "name": "RDP Lateral Movement Chain",
        "description": "RDP logon followed by service installation on same host within 15 minutes",
        "spl": (
            'index=wineventlog (EventCode=4624 Logon_Type=10) OR (EventCode=7045) '
            '| transaction Computer maxspan=15m startswith=(EventCode=4624) endswith=(EventCode=7045) '
            '| where eventcount >= 2 '
            '| table _time Computer Account_Name ServiceName'
        ),
        "severity": "high",
        "mitre": "T1021.001",
    },
    {
        "name": "PsExec Service Installation",
        "description": "Named pipe PSEXESVC created followed by remote service install",
        "spl": (
            'index=sysmon (EventCode=17 PipeName="\\\\PSEXESVC*") OR '
            '(index=wineventlog EventCode=7045 ServiceFileName="*PSEXESVC*") '
            '| transaction Computer maxspan=5m '
            '| where eventcount >= 2 '
            '| table _time Computer User Image ServiceName'
        ),
        "severity": "high",
        "mitre": "T1021.002",
    },
    {
        "name": "NTLM Pass-the-Hash Followed by Admin Tool",
        "description": "NTLM network logon followed by admin tool execution within 10 minutes",
        "spl": (
            'index=wineventlog EventCode=4624 Logon_Type=3 Authentication_Package=NTLM '
            '| join Computer maxspan=10m [search index=sysmon EventCode=1 '
            '(Image="*\\\\net.exe" OR Image="*\\\\net1.exe" OR Image="*\\\\wmic.exe" '
            'OR Image="*\\\\psexec.exe" OR Image="*\\\\powershell.exe")] '
            '| table _time Computer Account_Name Image CommandLine'
        ),
        "severity": "critical",
        "mitre": "T1550.002",
    },
    {
        "name": "WMI Remote Execution Chain",
        "description": "WMI process creation on remote host correlated with network logon",
        "spl": (
            'index=sysmon EventCode=1 ParentImage="*\\\\WmiPrvSE.exe" '
            '| join Computer [search index=wineventlog EventCode=4624 Logon_Type=3] '
            '| where Account_Name!="-" '
            '| stats count by Computer, Account_Name, Image, CommandLine '
            '| where count > 0'
        ),
        "severity": "high",
        "mitre": "T1047",
    },
    {
        "name": "Credential Dumping After Lateral Move",
        "description": "Network logon followed by LSASS access within 30 minutes",
        "spl": (
            'index=wineventlog EventCode=4624 Logon_Type=3 '
            '| join Computer maxspan=30m [search index=sysmon EventCode=10 '
            'TargetImage="*\\\\lsass.exe" GrantedAccess=0x1010] '
            '| table _time Computer Account_Name SourceImage GrantedAccess'
        ),
        "severity": "critical",
        "mitre": "T1003.001",
    },
]


def authenticate_splunk(base_url, username, password):
    """Authenticate to Splunk and return session headers."""
    resp = requests.post(
        f"{base_url}/services/auth/login",
        data={"username": username, "password": password},
        verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        timeout=30,
    )
    resp.raise_for_status()
    session_key = resp.json()["sessionKey"]
    logger.info("Authenticated to Splunk")
    return {"Authorization": f"Splunk {session_key}"}


def deploy_correlation_search(base_url, headers, rule):
    """Deploy a correlation search to Splunk ES."""
    search_payload = {
        "search": f"search {rule['spl']}",
        "name": rule["name"],
        "description": rule["description"],
        "cron_schedule": "*/15 * * * *",
        "dispatch.earliest_time": "-15m",
        "dispatch.latest_time": "now",
        "alert_type": "always",
        "alert.severity": "4" if rule["severity"] == "critical" else "3",
        "action.notable": "1",
        "action.notable.param.security_domain": "threat",
        "action.notable.param.severity": rule["severity"],
        "action.notable.param.rule_title": rule["name"],
    }
    resp = requests.post(
        f"{base_url}/services/saved/searches",
        headers=headers,
        data=search_payload,
        verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        timeout=30,
    )
    if resp.status_code in (200, 201):
        logger.info("Deployed correlation search: %s", rule["name"])
        return True
    logger.warning("Deploy failed for %s: %d %s", rule["name"], resp.status_code, resp.text[:100])
    return False


def generate_sigma_rule(rule):
    """Generate a Sigma-format YAML rule from a correlation definition."""
    sigma = {
        "title": rule["name"],
        "id": None,
        "status": "experimental",
        "description": rule["description"],
        "references": [f"https://attack.mitre.org/techniques/{rule['mitre']}/"],
        "logsource": {"product": "windows", "service": "sysmon"},
        "detection": {
            "selection": {"EventID": [1, 3, 17, 18]},
            "condition": "selection",
        },
        "level": rule["severity"],
        "tags": [f"attack.{rule['mitre'].lower()}"],
    }
    return yaml.dump(sigma, default_flow_style=False, sort_keys=False)


def audit_existing_searches(base_url, headers):
    """Audit existing Splunk saved searches for coverage gaps."""
    resp = requests.get(
        f"{base_url}/services/saved/searches",
        headers=headers,
        params={"output_mode": "json", "count": 0},
        verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        timeout=30,
    )
    if resp.status_code != 200:
        return []
    searches = resp.json().get("entry", [])
    mitre_covered = set()
    for s in searches:
        content = s.get("content", {})
        search_text = content.get("search", "").lower()
        for technique in ["t1021", "t1047", "t1053", "t1550", "t1003"]:
            if technique in search_text or technique in s.get("name", "").lower():
                mitre_covered.add(technique)
    lateral_techniques = {"t1021", "t1047", "t1053", "t1550", "t1003", "t1059", "t1570"}
    gaps = lateral_techniques - mitre_covered
    logger.info("Coverage: %d/%d lateral movement techniques covered", len(mitre_covered), len(lateral_techniques))
    return list(gaps)


def run_test_search(base_url, headers, spl, earliest="-24h"):
    """Execute a correlation search and return matching events."""
    resp = requests.post(
        f"{base_url}/services/search/jobs",
        headers=headers,
        data={"search": f"search {spl}", "earliest_time": earliest, "output_mode": "json"},
        verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        timeout=30,
    )
    resp.raise_for_status()
    sid = resp.json()["sid"]
    for _ in range(60):
        status = requests.get(
            f"{base_url}/services/search/jobs/{sid}",
            headers=headers, params={"output_mode": "json"},
            verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
            timeout=30,
        ).json()
        if status["entry"][0]["content"]["isDone"]:
            break
        time.sleep(2)
    results = requests.get(
        f"{base_url}/services/search/jobs/{sid}/results",
        headers=headers, params={"output_mode": "json", "count": 50},
        verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true",  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        timeout=30,
    ).json()
    return results.get("results", [])


def generate_report(deployed, gaps, test_results):
    """Generate correlation rules deployment report."""
    report = {
        "timestamp": datetime.utcnow().isoformat(),
        "rules_deployed": deployed,
        "coverage_gaps": gaps,
        "test_results_summary": {r["name"]: len(r.get("hits", [])) for r in test_results},
    }
    print(f"CORRELATION RULES REPORT: {len(deployed)} deployed, {len(gaps)} gaps")
    return report


def main():
    parser = argparse.ArgumentParser(description="SIEM Correlation Rules Agent")
    parser.add_argument("--splunk-url", default=os.environ.get("SPLUNK_URL", "https://localhost:8089"))
    parser.add_argument("--username", default="admin")
    parser.add_argument("--password", required=True)
    parser.add_argument("--deploy", action="store_true", help="Deploy rules to Splunk")
    parser.add_argument("--test", action="store_true", help="Test rules against recent data")
    parser.add_argument("--sigma-export", help="Export rules as Sigma YAML to directory")
    parser.add_argument("--output", default="correlation_report.json")
    args = parser.parse_args()

    headers = authenticate_splunk(args.splunk_url, args.username, args.password)
    deployed = []
    test_results = []

    if args.deploy:
        for rule in LATERAL_MOVEMENT_RULES:
            if deploy_correlation_search(args.splunk_url, headers, rule):
                deployed.append(rule["name"])

    if args.test:
        for rule in LATERAL_MOVEMENT_RULES:
            hits = run_test_search(args.splunk_url, headers, rule["spl"])
            test_results.append({"name": rule["name"], "hits": hits})
            logger.info("Rule '%s': %d hits", rule["name"], len(hits))

    if args.sigma_export:
        import os
        os.makedirs(args.sigma_export, exist_ok=True)
        for rule in LATERAL_MOVEMENT_RULES:
            sigma_yaml = generate_sigma_rule(rule)
            fname = rule["name"].lower().replace(" ", "_") + ".yml"
            with open(os.path.join(args.sigma_export, fname), "w") as f:
                f.write(sigma_yaml)
        logger.info("Exported %d Sigma rules", len(LATERAL_MOVEMENT_RULES))

    gaps = audit_existing_searches(args.splunk_url, headers)
    report = generate_report(deployed, gaps, test_results)
    with open(args.output, "w") as f:
        json.dump(report, f, indent=2)
    logger.info("Report saved to %s", args.output)


if __name__ == "__main__":
    main()
