"""Original offline lab heuristic for Sysmon Event 3 XML. No log configuration changes."""
import argparse
import ipaddress
import json
import ntpath
import sys
import xml.etree.ElementTree as ET

NS = {'e': 'http://schemas.microsoft.com/win/2004/08/events/event'}
WATCH = {'powershell.exe', 'pwsh.exe', 'cmd.exe', 'python.exe', 'python3.exe',
         'sharphound.exe', 'bloodhound.exe'}
PORTS = {'389', '636', '3268', '3269'}


def detect(xml_text, dc_ips):
    targets = {str(ipaddress.ip_address(ip)) for ip in dc_ips}
    if not targets:
        raise ValueError('Supply at least one domain-controller IP')
    # Parse exported XML only; ElementTree does not fetch external resources.
    if '<!DOCTYPE' in xml_text.upper() or '<!ENTITY' in xml_text.upper():
        raise ValueError('DTD/entity declarations are not supported')
    root = ET.fromstring(xml_text)
    event_tag = '{' + NS['e'] + '}Event'
    events = [root] if root.tag == event_tag else list(root.findall('e:Event', NS))
    if root.tag != event_tag and root.tag not in ('Events', '{' + NS['e'] + '}Events'):
        raise ValueError('Expected an Event or Events XML export')
    alerts, seen = [], {}
    for event in events:
        provider = event.find('e:System/e:Provider', NS)
        if provider is None or provider.get('Name') != 'Microsoft-Windows-Sysmon':
            continue
        if event.findtext('e:System/e:EventID', namespaces=NS) != '3':
            continue
        data = {item.get('Name'): item.text or '' for item in event.findall('e:EventData/e:Data', NS)}
        required = ('Image', 'DestinationIp', 'DestinationPort', 'Initiated', 'Protocol', 'ProcessGuid')
        if any(not data.get(name) for name in required):
            raise ValueError('Sysmon Event 3 is missing required named fields')
        try:
            destination = str(ipaddress.ip_address(data['DestinationIp']))
        except ValueError as exc:
            raise ValueError('Invalid DestinationIp in Sysmon Event 3') from exc
        if data['Initiated'].lower() != 'true' or data['Protocol'].lower() != 'tcp':
            continue
        if destination not in targets or data['DestinationPort'] not in PORTS:
            continue
        process = ntpath.basename(data['Image']).lower()
        if process not in WATCH:
            continue
        computer = event.findtext('e:System/e:Computer', namespaces=NS)
        record = event.findtext('e:System/e:EventRecordID', namespaces=NS)
        time = event.find('e:System/e:TimeCreated', NS)
        if not computer or not record or time is None or not time.get('SystemTime'):
            raise ValueError('Matching event lacks source computer, record ID, or timestamp')
        result = {'rule': 'review_process_ldap_connection', 'computer': computer,
                  'timestamp': time.get('SystemTime'), 'record_id': record,
                  'process': data['Image'], 'process_guid': data['ProcessGuid'],
                  'destination_ip': destination, 'destination_port': int(data['DestinationPort']),
                  'assessment': 'Connection metadata only; investigate process purpose and host role.'}
        identity = (computer, record)
        if identity in seen:
            if seen[identity] != result:
                raise ValueError('Conflicting duplicate Sysmon record ID')
            continue
        seen[identity] = result
        alerts.append(result)
    return alerts


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('xml_file', help='Event Viewer XML export, not binary EVTX')
    parser.add_argument('--dc-ip', action='append', required=True, help='Repeat for additional DC IPs')
    args = parser.parse_args()
    try:
        with open(args.xml_file, encoding='utf-8-sig') as stream:
            alerts = detect(stream.read(), args.dc_ip)
        print(json.dumps({'alerts': alerts}, indent=2))
    except (OSError, ValueError, ET.ParseError) as exc:
        print(f'Error: {exc}', file=sys.stderr)
        return 2
    return 0


if __name__ == '__main__':
    sys.exit(main())
