#!/usr/bin/env python3
"""LOLBin (Living Off the Land Binary) detection agent.

Parses Windows Sysmon Event ID 1 (Process Create) and Event ID 3
(Network Connection) logs in EVTX or JSON format to detect suspicious
LOLBin execution patterns, anomalous parent-child relationships, and
LOLBin network activity.
"""

import json
import os
import re
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from datetime import datetime

try:
    import Evtx.Evtx as evtx
    HAS_EVTX = True
except ImportError:
    HAS_EVTX = False


# LOLBin signatures: binary name -> suspicious command-line patterns + MITRE mapping
LOLBIN_SIGNATURES = {
    "certutil.exe": {
        "patterns": [
            r"-urlcache\s+-f\s+https?://",
            r"-decode\s+",
            r"-encode\s+",
            r"-verifyctl\s+.*https?://",
            r"-ping\s+https?://",
        ],
        "mitre": ["T1218", "T1105"],
        "severity": "high",
        "description": "Certificate utility used for file download/decode",
    },
    "mshta.exe": {
        "patterns": [
            r"https?://",
            r"vbscript:",
            r"javascript:",
            r"about:",
        ],
        "mitre": ["T1218.005"],
        "severity": "high",
        "description": "HTML Application host executing remote/inline scripts",
    },
    "rundll32.exe": {
        "patterns": [
            r"javascript:",
            r"shell32\.dll.*ShellExec_RunDLL",
            r"\\\\.*\\.*\.dll",
            r"comsvcs\.dll.*MiniDump",
            r"comsvcs\.dll.*#24",
            r"url\.dll.*OpenURL",
            r"url\.dll.*FileProtocolHandler",
            r"advpack\.dll.*RegisterOCX",
        ],
        "mitre": ["T1218.011"],
        "severity": "critical",
        "description": "DLL proxy execution via rundll32",
    },
    "regsvr32.exe": {
        "patterns": [
            r"/s\s+/n\s+/u\s+/i:",
            r"scrobj\.dll",
            r"https?://",
        ],
        "mitre": ["T1218.010"],
        "severity": "critical",
        "description": "Squiblydoo scriptlet execution via regsvr32",
    },
    "bitsadmin.exe": {
        "patterns": [
            r"/transfer\s+.*https?://",
            r"/create\s+",
            r"/addfile\s+.*https?://",
            r"/SetNotifyCmdLine",
            r"/Resume",
        ],
        "mitre": ["T1197"],
        "severity": "high",
        "description": "BITS job abuse for file download/persistence",
    },
    "wmic.exe": {
        "patterns": [
            r"process\s+call\s+create",
            r"/node:",
            r"os\s+get\s+/format:.*https?://",
            r"/format:.*\.xsl",
        ],
        "mitre": ["T1047"],
        "severity": "high",
        "description": "WMI command-line for remote execution or XSL script",
    },
    "msbuild.exe": {
        "patterns": [
            r"\.xml\b",
            r"\.csproj\b",
            r"\\temp\\",
            r"\\appdata\\",
            r"\\users\\.*\\desktop\\",
        ],
        "mitre": ["T1127.001"],
        "severity": "high",
        "description": "MSBuild executing project from unusual location",
    },
    "installutil.exe": {
        "patterns": [
            r"/logfile=",
            r"/LogToConsole=false",
            r"\\temp\\",
            r"\\appdata\\",
        ],
        "mitre": ["T1218.004"],
        "severity": "high",
        "description": "InstallUtil executing assembly from unusual path",
    },
    "cmstp.exe": {
        "patterns": [
            r"/ni\s+/s\s+",
            r"\.inf\b",
        ],
        "mitre": ["T1218.003"],
        "severity": "high",
        "description": "CMSTP INF file execution for UAC bypass",
    },
    "mavinject.exe": {
        "patterns": [
            r"/INJECTRUNNING\s+\d+",
        ],
        "mitre": ["T1218.013"],
        "severity": "critical",
        "description": "DLL injection via mavinject",
    },
    "powershell.exe": {
        "patterns": [
            r"-e(nc|ncodedcommand)?\s+[A-Za-z0-9+/=]{40,}",
            r"IEX\s*\(",
            r"Invoke-Expression",
            r"Net\.WebClient",
            r"DownloadString",
            r"DownloadFile",
            r"-nop\s+-w\s+hidden",
            r"FromBase64String",
        ],
        "mitre": ["T1059.001"],
        "severity": "high",
        "description": "PowerShell executing encoded/download commands",
    },
    "pwsh.exe": {
        "patterns": [
            r"-e(nc|ncodedcommand)?\s+[A-Za-z0-9+/=]{40,}",
            r"IEX\s*\(",
            r"Invoke-Expression",
            r"DownloadString",
        ],
        "mitre": ["T1059.001"],
        "severity": "high",
        "description": "PowerShell Core executing encoded/download commands",
    },
    "cscript.exe": {
        "patterns": [
            r"https?://",
            r"\\temp\\",
            r"\\appdata\\",
        ],
        "mitre": ["T1059.005"],
        "severity": "medium",
        "description": "Console script host executing from unusual location",
    },
    "wscript.exe": {
        "patterns": [
            r"https?://",
            r"\\temp\\",
            r"\\appdata\\",
        ],
        "mitre": ["T1059.005"],
        "severity": "medium",
        "description": "Windows script host executing from unusual location",
    },
}

# Suspicious parent-child process relationships
PARENT_CHILD_RULES = [
    {
        "parents": ["winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe",
                     "msaccess.exe", "mspub.exe", "visio.exe", "onenote.exe"],
        "children": ["cmd.exe", "powershell.exe", "pwsh.exe", "mshta.exe",
                      "wscript.exe", "cscript.exe", "certutil.exe", "regsvr32.exe",
                      "rundll32.exe", "bitsadmin.exe"],
        "severity": "critical",
        "mitre": "T1204.002",
        "description": "Office application spawned command interpreter or LOLBin",
    },
    {
        "parents": ["wmiprvse.exe"],
        "children": ["cmd.exe", "powershell.exe", "pwsh.exe", "mshta.exe",
                      "rundll32.exe"],
        "severity": "critical",
        "mitre": "T1047",
        "description": "WMI Provider Host spawned process (possible remote WMI execution)",
    },
    {
        "parents": ["svchost.exe"],
        "children": ["cmd.exe", "powershell.exe", "mshta.exe", "certutil.exe"],
        "severity": "high",
        "mitre": "T1543.003",
        "description": "Service Host spawned suspicious child process",
    },
    {
        "parents": ["explorer.exe"],
        "children": ["mshta.exe", "regsvr32.exe", "msbuild.exe", "installutil.exe",
                      "cmstp.exe", "mavinject.exe"],
        "severity": "high",
        "mitre": "T1218",
        "description": "Explorer spawned proxy execution binary",
    },
    {
        "parents": ["taskeng.exe", "taskhostw.exe"],
        "children": ["cmd.exe", "powershell.exe", "mshta.exe", "certutil.exe",
                      "wscript.exe", "cscript.exe"],
        "severity": "high",
        "mitre": "T1053.005",
        "description": "Scheduled task spawned suspicious process",
    },
]

# LOLBins that should not make outbound network connections
NETWORK_SUSPICIOUS_LOLBINS = {
    "certutil.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe",
    "msbuild.exe", "installutil.exe", "bitsadmin.exe", "esentutl.exe",
    "expand.exe", "replace.exe", "cmstp.exe", "presentationhost.exe",
    "mavinject.exe",
}


def parse_sysmon_evtx(evtx_path):
    """Parse a Sysmon EVTX file and extract Event ID 1 and 3 records."""
    if not HAS_EVTX:
        print("[WARN] python-evtx not installed. Use: pip install python-evtx")
        return [], []
    process_events = []
    network_events = []
    ns = {"e": "http://schemas.microsoft.com/win/2004/08/events/event"}

    with evtx.Evtx(evtx_path) as log:
        for record in log.records():
            try:
                root = ET.fromstring(record.xml())
            except ET.ParseError:
                continue
            event_id_el = root.find(".//e:System/e:EventID", ns)
            if event_id_el is None:
                continue
            event_id = event_id_el.text

            event_data = {}
            for data_el in root.findall(".//e:EventData/e:Data", ns):
                name = data_el.get("Name", "")
                event_data[name] = data_el.text or ""

            time_el = root.find(".//e:System/e:TimeCreated", ns)
            if time_el is not None:
                event_data["UtcTime"] = time_el.get("SystemTime", "")

            computer_el = root.find(".//e:System/e:Computer", ns)
            if computer_el is not None:
                event_data["Computer"] = computer_el.text or ""

            if event_id == "1":
                process_events.append(event_data)
            elif event_id == "3":
                network_events.append(event_data)

    return process_events, network_events


def parse_sysmon_json(json_path):
    """Parse Sysmon events from a JSON lines file."""
    process_events = []
    network_events = []
    with open(json_path, "r", errors="replace") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                event = json.loads(line)
            except json.JSONDecodeError:
                continue
            event_id = str(event.get("EventID", event.get("event_id", "")))
            if event_id == "1":
                process_events.append(event)
            elif event_id == "3":
                network_events.append(event)
    return process_events, network_events


def load_events(path):
    """Load Sysmon events from EVTX or JSON file."""
    if path.lower().endswith(".evtx"):
        return parse_sysmon_evtx(path)
    elif path.lower().endswith(".json") or path.lower().endswith(".jsonl"):
        return parse_sysmon_json(path)
    else:
        # Try JSON first, fall back to EVTX
        try:
            return parse_sysmon_json(path)
        except Exception:
            return parse_sysmon_evtx(path)


def detect_lolbin_abuse(process_events):
    """Detect suspicious LOLBin command-line patterns."""
    alerts = []
    for event in process_events:
        image = event.get("Image", event.get("image", "")).lower()
        cmdline = event.get("CommandLine", event.get("command_line", ""))
        if not cmdline:
            continue

        binary_name = image.split("\\")[-1] if "\\" in image else image.split("/")[-1]

        for lolbin, config in LOLBIN_SIGNATURES.items():
            if binary_name == lolbin.lower():
                for pattern in config["patterns"]:
                    if re.search(pattern, cmdline, re.IGNORECASE):
                        alerts.append({
                            "type": "lolbin_suspicious_cmdline",
                            "severity": config["severity"],
                            "lolbin": lolbin,
                            "description": config["description"],
                            "mitre": config["mitre"],
                            "command_line": cmdline[:500],
                            "image": event.get("Image", event.get("image", "")),
                            "parent_image": event.get("ParentImage", event.get("parent_image", "")),
                            "user": event.get("User", event.get("user", "")),
                            "hostname": event.get("Computer", event.get("hostname", "")),
                            "timestamp": event.get("UtcTime", event.get("timestamp", "")),
                            "pid": event.get("ProcessId", event.get("process_id", "")),
                            "ppid": event.get("ParentProcessId", event.get("parent_process_id", "")),
                            "matched_pattern": pattern,
                        })
                        break
    return alerts


def detect_parent_child_anomalies(process_events):
    """Detect suspicious parent-child process relationships."""
    alerts = []
    for event in process_events:
        parent = event.get("ParentImage", event.get("parent_image", "")).lower()
        child = event.get("Image", event.get("image", "")).lower()
        parent_name = parent.split("\\")[-1] if "\\" in parent else parent.split("/")[-1]
        child_name = child.split("\\")[-1] if "\\" in child else child.split("/")[-1]

        for rule in PARENT_CHILD_RULES:
            if parent_name in rule["parents"] and child_name in rule["children"]:
                alerts.append({
                    "type": "suspicious_parent_child",
                    "severity": rule["severity"],
                    "mitre": rule["mitre"],
                    "description": rule["description"],
                    "parent_process": parent,
                    "child_process": child,
                    "command_line": event.get("CommandLine", event.get("command_line", ""))[:500],
                    "user": event.get("User", event.get("user", "")),
                    "hostname": event.get("Computer", event.get("hostname", "")),
                    "timestamp": event.get("UtcTime", event.get("timestamp", "")),
                })
                break
    return alerts


def detect_lolbin_network(network_events):
    """Detect LOLBins making outbound network connections."""
    alerts = []
    for event in network_events:
        image = event.get("Image", event.get("image", "")).lower()
        binary_name = image.split("\\")[-1] if "\\" in image else image.split("/")[-1]

        if binary_name in NETWORK_SUSPICIOUS_LOLBINS:
            dest_ip = event.get("DestinationIp", event.get("destination_ip", ""))
            if dest_ip.startswith("127.") or dest_ip == "::1":
                continue
            alerts.append({
                "type": "lolbin_network_connection",
                "severity": "critical",
                "binary": binary_name,
                "image": event.get("Image", event.get("image", "")),
                "destination_ip": dest_ip,
                "destination_port": event.get("DestinationPort", event.get("destination_port", "")),
                "destination_hostname": event.get("DestinationHostname", event.get("destination_hostname", "")),
                "source_ip": event.get("SourceIp", event.get("source_ip", "")),
                "user": event.get("User", event.get("user", "")),
                "hostname": event.get("Computer", event.get("hostname", "")),
                "timestamp": event.get("UtcTime", event.get("timestamp", "")),
            })
    return alerts


def generate_statistics(process_events, all_alerts):
    """Generate summary statistics."""
    lolbin_exec_counts = defaultdict(int)
    for event in process_events:
        image = event.get("Image", event.get("image", "")).lower()
        binary_name = image.split("\\")[-1] if "\\" in image else image.split("/")[-1]
        if binary_name in [k.lower() for k in LOLBIN_SIGNATURES]:
            lolbin_exec_counts[binary_name] += 1

    severity_counts = defaultdict(int)
    type_counts = defaultdict(int)
    mitre_counts = defaultdict(int)
    for alert in all_alerts:
        severity_counts[alert.get("severity", "unknown")] += 1
        type_counts[alert.get("type", "unknown")] += 1
        mitre_ids = alert.get("mitre", [])
        if isinstance(mitre_ids, str):
            mitre_ids = [mitre_ids]
        for mid in mitre_ids:
            mitre_counts[mid] += 1

    return {
        "lolbin_execution_counts": dict(lolbin_exec_counts),
        "alert_severity_counts": dict(severity_counts),
        "alert_type_counts": dict(type_counts),
        "mitre_technique_counts": dict(mitre_counts),
        "total_alerts": len(all_alerts),
    }


if __name__ == "__main__":
    print("=" * 60)
    print("Living Off the Land (LOLBin) Detection Agent")
    print("Sysmon log analysis for LOLBin abuse, parent-child")
    print("anomalies, and LOLBin network connections")
    print("=" * 60)

    input_path = sys.argv[1] if len(sys.argv) > 1 else None

    if not input_path or not os.path.exists(input_path):
        print(f"\n[DEMO] Usage: python agent.py <sysmon_events.evtx|json|jsonl>")
        print("[*] Provide Sysmon event logs (EVTX or JSON) for LOLBin analysis.")
        print(f"[*] Monitors {len(LOLBIN_SIGNATURES)} LOLBins with "
              f"{sum(len(v['patterns']) for v in LOLBIN_SIGNATURES.values())} detection patterns")
        print(f"[*] {len(PARENT_CHILD_RULES)} parent-child anomaly rules")
        print(f"[*] {len(NETWORK_SUSPICIOUS_LOLBINS)} LOLBins monitored for network activity")
        sys.exit(0)

    print(f"\n[*] Loading events from: {input_path}")
    process_events, network_events = load_events(input_path)
    print(f"  Process creation events (EID 1): {len(process_events)}")
    print(f"  Network connection events (EID 3): {len(network_events)}")

    all_alerts = []

    print("\n--- LOLBin Command-Line Detection ---")
    cmdline_alerts = detect_lolbin_abuse(process_events)
    print(f"  Suspicious LOLBin executions: {len(cmdline_alerts)}")
    for a in cmdline_alerts[:15]:
        print(f"  [{a['severity'].upper()}] {a['lolbin']} on {a.get('hostname', '?')}")
        print(f"    MITRE: {', '.join(a['mitre']) if isinstance(a['mitre'], list) else a['mitre']}")
        print(f"    Cmd: {a['command_line'][:120]}")
        print(f"    Parent: {a['parent_image']}")
        print(f"    User: {a['user']}")
    all_alerts.extend(cmdline_alerts)

    print("\n--- Parent-Child Anomaly Detection ---")
    pc_alerts = detect_parent_child_anomalies(process_events)
    print(f"  Suspicious parent-child pairs: {len(pc_alerts)}")
    for a in pc_alerts[:15]:
        print(f"  [{a['severity'].upper()}] {a['description']}")
        print(f"    Parent: {a['parent_process']} -> Child: {a['child_process']}")
        print(f"    Cmd: {a['command_line'][:120]}")
    all_alerts.extend(pc_alerts)

    print("\n--- LOLBin Network Connections ---")
    net_alerts = detect_lolbin_network(network_events)
    print(f"  LOLBin network connections: {len(net_alerts)}")
    for a in net_alerts[:15]:
        print(f"  [CRITICAL] {a['binary']} -> {a['destination_ip']}:{a['destination_port']}"
              f" ({a.get('destination_hostname', 'N/A')})")
    all_alerts.extend(net_alerts)

    stats = generate_statistics(process_events, all_alerts)

    print(f"\n{'=' * 60}")
    print(f"SUMMARY: {stats['total_alerts']} total alerts")
    for sev, count in sorted(stats["alert_severity_counts"].items()):
        print(f"  {sev.upper()}: {count}")
    if stats["mitre_technique_counts"]:
        print("\nMITRE ATT&CK techniques triggered:")
        for tech, count in sorted(stats["mitre_technique_counts"].items(), key=lambda x: -x[1]):
            print(f"  {tech}: {count}")
    if stats["lolbin_execution_counts"]:
        print("\nLOLBin execution counts:")
        for binary, count in sorted(stats["lolbin_execution_counts"].items(), key=lambda x: -x[1])[:20]:
            print(f"  {binary}: {count}")
