# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

""" Gather script for Windows remote login info.

    ****Needs AuditLogon setting enabled at least on Success in 
        Local Policies - Audit Policy.

    ****Parsing the event message output is not very good but there is no 
        other option known at this time.
"""

import os
import time
import logging
import re
from win32com.client import GetObject
import pythoncom
import pywintypes
import tempfile
import logging

logger = logging.getLogger("gather.winlogin")

from sigma.runtime import rtlib

WINERRORS = (pythoncom.com_error, pywintypes.com_error)
USER_NAME = "User Name:"
DOMAIN = "Domain:"
LOGON_TYPE = "Logon Type:"
WORKSTATION_NAME = "Workstation Name:"
CLIENT_NAME = "Client Name:"
CLIENT_ADDRESS = "Client Address:"
INF1 = "[version]\r\n"
INF2 = 'signature="$CHICAGO$"\r\n'
INF3 = "DriverVer=07/01/2001,5.1.2600.0\r\n"
SECTION = "[Event Audit]"
ENTRY1 = "AuditLogonEvents = 1" # 1 - success
ENTRY2 = "AuditLogonEvents = 3" # 3 - success and failure
ENTRY3 = "AuditAccountLogon = 1" # 1 - success
ENTRY4 = "AuditAccountLogon = 3" # 3 - success and failure
AREA = "SECURITYPOLICY"
ERRMSG = """\
Agent may be unable to get complete user log-in history because 
Audit Logon Events policy is currently disabled. To enable it, 
open the Administrative tools in Control Panel, go to 
Local Security Settings/Audit Policy/Audit Logon Events, check 
Success and Failure and click OK. 
"""

# Logon type
logon_map = {
    "2": "Interactive",
    "3": "Network",
    "4": "Batch",
    "5": "Service",
    "7": "Unlock",
    "8": "Network Clear Text",
    "9": "New Credentials",
    "10": "Remote Interactive",
    "11": "Cached Interactive",
    "12": "Cached Remote Interactive",
    "13": "Cached Unlock",
    }

# Logon events
event_map = {
    528: "Successful Logon",
    529: "Unknown user name or bad password", 
    530: "Account logon time violation",
    531: "Account currently disabled", 
    532: "User account has expired", 
    533: "User not allowed at this computer", 
    534: "Logon type denied on this machine", 
    535: "Account password has expired", 
    536: "NetLogon component is not active",
    537: "Logon failed for unknown reasons", 
    539: "Account locked out", 
    540: "Successful Network Logon",
    }

crlf_pattern = re.compile(r"(\r\n)+")

logger = logging.getLogger("gather.gather_winlogin")

FORMAT = "%-17s %-33s %-20s %-20s %-20s %-20s %-20s"

# checks the settings thru secedit.exe
def check_through_secedit(entry):
    # create input configuration file
    host = rtlib.Host("localhost")
    temppath = os.path.join(host.getconfig("agentdata"), "temp")
    if not os.path.exists(temppath):
        os.makedirs(temppath)
    h, conf_file = tempfile.mkstemp(".inf", "", temppath)
    os.close(h)
    #conf_file = tempfile.mktemp()
    try:
        fout = open(conf_file, "w")
    except:
        return -1
    try:
        fout.write(INF1)
        fout.write(INF2)
        fout.write(INF3)
        fout.write(SECTION)
        fout.write("\r\n")
        fout.write(entry)
        fout.write("\r\n")
        fout.close()
    except:
        fout.close()
        return -1

    # run the secedit utility
    h, out_file = tempfile.mkstemp(".log", "", temppath)
    os.close(h)
    #out_file = tempfile.mktemp()
    command = 'secedit /analyze /db temp.pdb /cfg "%s" /log "%s" /verbose' % (conf_file, out_file)
    p = os.popen(command)
    p.close()
    os.remove(conf_file)

    # read the output file
    try:
        f = open (out_file, "rb")
    except:
        return -1
    try:
        try:
            bytes = f.read()
        except:
            return -1
    finally:
        f.close()
        os.remove(out_file)

    # decode unicode
    u = bytes.decode("utf-16")
    k = u.find("Errors occurred")
    if k >= 0:
        k = u.find("Analyze event audit settings.")
        if k < 0:
            return -1

    # find the string 'Mismatch' and get the whole line
    i = u.find("Mismatch")
    if i >= 0:
        return -1
    return 0

def main():
    logon_enabled = True
    # check if audit logon events policy is enabled for success
    if check_through_secedit(ENTRY2) < 0:
            logon_enabled = False         
    
    # Removed account logon check because auditlogonevents generates 
    # records for local logins as well as domain logins.
    #if logon_enabled:
    #    # check if audit account logon events policy is enabled for success
    #    if check_through_secedit(ENTRY4) < 0:
    #        logon_enabled = False 
    
    if not logon_enabled:
         print(ERRMSG)

    try:
        objItems = GetObject("winmgmts:{impersonationLevel=impersonate, \
            (Security, Audit)}\\\\.\\root\\cimv2").ExecQuery("Select * from \
            Win32_NTLogEvent where logfile = 'Security' \
            and (EventCode != 538 and EventCode >= 528 and EventCode <= 540)")
    except Exception as err:
        logger.exception("winlogin: GetObject on Win32_NTLogEvent failed")
        print("Can't get login audit info:", err)
        return
    
    print(FORMAT % ("Date     Time", "Event", "Type", "User", "Domain",
                    "Client machine", "Client address"))
    loginCount = 0
    try:
        for item in objItems:
            user = ""
            domain = ""
            type = ""
            clientmachine = ""
            clientaddress = ""
            
            try:
                timegenerated = item.TimeGenerated
                eventcode = item.EventCode
                code = event_map[eventcode]
                msg = item.Message
            except Exception as err:
                logger.exception("Unexpected error in winlogin")
                print("Unexpected error:", err)
                return

            # message has multiple \r\n pairs and \t at random places
            # get rid of these
            msg = crlf_pattern.sub("\r\n", msg)
            msg = msg.replace('\t', "")

            i = msg.find(USER_NAME)
            if i >= 0:
                j = msg.find("\r\n", i + 1)
                if j > i + len(USER_NAME):
                    user = msg[i + len(USER_NAME):j]
            if user == "" and item.User != None:
                user = item.User

            # ignore events in which user name ends with $, which indicates 
            # that system services on a remote system are connected to the 
            # system services on this system
            if user.endswith("$"):
                continue

            # ignore events in which the User Name is SYSTEM, which indicates 
            # that one system service was connecting to another service on 
            # the same system
            # Anonymous logons identify network logons for which credentials 
            # are not provided, such as connections to web server
            if user.upper() in ("NT AUTHORITY\\SYSTEM", "NT AUTHORITY\\ANONYMOUS LOGON", "NT AUTHORITY\\ANONYMOUS", "NETWORK SERVICE", "LOCAL SERVICE", "SYSTEM", "LOCAL SYSTEM", "ANONYMOUS LOGON"):
                continue

            i = msg.find(DOMAIN)
            if i >= 0:
                j = msg.find("\r\n", i + 1)
                if j > i + len(DOMAIN):
                    domain = msg[i + len(DOMAIN):j]

            # not every logon has Logon Type
            i = msg.find(LOGON_TYPE)
            if i >= 0:
                j = msg.find("\r\n", i + 1)
                if j > i + len(LOGON_TYPE):
                    type = str(msg[i + len(LOGON_TYPE):j])
                    #if type == '4' or type == '5' or type == '8' or type == '11': 
                    #    continue
                    if type == '7' or type == '11' or type == '12' or type == '13':
                        continue
                    if type in logon_map:
                        type = logon_map[type]

            # event 682 is remote interactive
            #if eventcode == 682 and type == "":
            #    type = logon_map['10']

            # some logons have Workstation Name and some have Client name
            # never seen both in the same logon
            i = msg.find(WORKSTATION_NAME)
            if i >= 0:
                j = msg.find("\r\n", i + 1)
                if j > i + len(WORKSTATION_NAME):
                    clientmachine = msg[i + len(WORKSTATION_NAME):j]
            i = msg.find(CLIENT_NAME)
            if i >= 0:
                j = msg.find("\r\n", i + 1)
                if j > i + len(CLIENT_NAME):
                    clientmachine = msg[i + len(CLIENT_NAME):j]

            # not every logon has Client Address
            i = msg.find(CLIENT_ADDRESS)
            if i >= 0:
                j = msg.find("\r\n", i + 1)
                if j > i + len(CLIENT_ADDRESS):
                    clientaddress = msg[i + len(CLIENT_ADDRESS):j]

            print(FORMAT % (
                fixtime(timegenerated), code, type,
                user, domain, clientmachine, clientaddress))

            loginCount = loginCount + 1
            # we don't want more than last 20 logins
            if loginCount >= 20:
                break
    except:
        logger.exception("winlogin failed")

def fixtime(s):
    # Input is of the form YYYYMMDDhhmmss.uuuuuu-zzz
    # what is uuuuuu?
    assert len(s) == 25
    YYYY, MM, DD, hh, mm, ss = map(int, 
        [s[0:4], s[4:6], s[6:8], s[8:10], s[10:12], s[12:14]])
    #dot_uuuuuu = s[14:21]
    #assert dot_uuuuuu.startswith("."), dot_uuuuuu
    #sgn_zzz = s[21:25]
    #assert sgn_zzz.startswith("+") or sgn_zzz.startswith("-"), sgn_zzz
    t = (YYYY, MM, DD, hh, mm, ss, 0, 1, -1) # Assume it's local time
    return time.strftime("%c", t)

if __name__ == "__main__":
    main()
