diff --git a/Data/Agent/Roles/Device_Audit.yml b/Data/Agent/Roles/Device_Audit.yml index 6eafba7..777c152 100644 --- a/Data/Agent/Roles/Device_Audit.yml +++ b/Data/Agent/Roles/Device_Audit.yml @@ -156,56 +156,40 @@ ansible.builtin.set_fact: device_details: "{{ device_details | combine({'summary': (device_details.summary | combine({'external_ip': (external_ip_raw.stdout | default('') | trim) })) }) }}" - - name: Collect currently logged-in users (interactive + RDP) + - name: Collect last logged-on user from registry (SAM/UPN) ansible.builtin.shell: | $ErrorActionPreference = 'SilentlyContinue' - function Get-InteractiveUsers { - $users = @() - try { - $ls = Get-CimInstance Win32_LogonSession | Where-Object { $_.LogonType -in 2,10 } - foreach ($sess in $ls) { - $accs = Get-CimAssociatedInstance -InputObject $sess -Association Win32_LoggedOnUser -ResultClassName Win32_Account - foreach ($a in $accs) { - if (-not $a -or -not $a.Name) { continue } - $nm = [string]$a.Name - $dm = [string]$a.Domain - if ($nm -match '\$$') { continue } - if ($dm -eq 'NT AUTHORITY' -or $dm -eq 'NT SERVICE') { continue } - if ($nm -like 'DWM-*' -or $nm -like 'UMFD-*') { continue } - if ($dm) { $users += ("{0}\\{1}" -f $dm,$nm) } else { $users += $nm } - } - } - } catch {} - $users | Sort-Object -Unique + function Normalize-Sam([string]$s) { + if ([string]::IsNullOrWhiteSpace($s)) { return '' } + if ($s -match '\$$') { return '' } # exclude machine accounts + if ($s -like 'DWM-*' -or $s -like 'UMFD-*') { return '' } + if ($s -eq 'SYSTEM' -or $s -eq 'LOCAL SERVICE' -or $s -eq 'NETWORK SERVICE' -or $s -eq 'ANONYMOUS LOGON') { return '' } + return $s } - function Get-QuserUsers { - $list=@() - try { - $q = (quser 2>$null) -split "`r?`n" - foreach ($line in $q) { - if (-not $line) { continue } - if ($line -match '^USERNAME') { continue } - $s = ($line -replace '^>','').Trim() - if (-not $s) { continue } - $parts = $s -split '\s+' - if ($parts.Length -lt 1) { continue } + $regPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI' + $sam = '' + $upn = '' + try { $sam = (Get-ItemProperty -Path $regPath -Name 'LastLoggedOnSAMUser' -ErrorAction Stop).LastLoggedOnSAMUser } catch {} + try { $upn = (Get-ItemProperty -Path $regPath -Name 'LastLoggedOnUser' -ErrorAction Stop).LastLoggedOnUser } catch {} + + $user = Normalize-Sam $sam + if (-not $user) { + $user = Normalize-Sam $upn + if ($user -and $user -like '*@*') { + # Convert UPN to DOMAIN\user using machine domain (best effort) + $domDns = (Get-WmiObject Win32_ComputerSystem).Domain + $domShort = '' + if ($domDns) { $domShort = ($domDns -split '\.')[0].ToUpper() } + $parts = $user -split '@' + if ($parts.Length -ge 1) { $u = $parts[0] - if (-not $u) { continue } - if ($u -match '\$$') { continue } - if ($u -like 'DWM-*' -or $u -like 'UMFD-*') { continue } - $list += $u + if ($domShort) { $user = "$domShort\$u" } } - } catch {} - $list | Sort-Object -Unique + } } - $u1 = Get-InteractiveUsers - $u2 = Get-QuserUsers - $combined = @() - foreach ($u in $u1) { if ($combined -notcontains $u) { $combined += $u } } - foreach ($u in $u2) { if ($combined -notcontains $u) { $combined += $u } } - if ($combined.Count -eq 0) { 'No Users Logged In' } else { $combined -join ', ' } + if ($user) { $user } else { 'No Users Logged In' } register: last_user_raw changed_when: false diff --git a/Data/Agent/Roles/role_DeviceAudit.py b/Data/Agent/Roles/role_DeviceAudit.py index 1c288ed..230fe07 100644 --- a/Data/Agent/Roles/role_DeviceAudit.py +++ b/Data/Agent/Roles/role_DeviceAudit.py @@ -495,6 +495,83 @@ else { 'Workstation' } return '' +def _collect_last_user_registry() -> str: + if not IS_WINDOWS: + return '' + # Registry-first approach: LogonUI LastLoggedOnSAMUser / LastLoggedOnUser + try: + ps = r""" +$ErrorActionPreference = 'SilentlyContinue' +function Normalize-Sam([string]$s) { + if ([string]::IsNullOrWhiteSpace($s)) { return '' } + if ($s -match '\$$') { return '' } + if ($s -like 'DWM-*' -or $s -like 'UMFD-*') { return '' } + if ($s -eq 'SYSTEM' -or $s -eq 'LOCAL SERVICE' -or $s -eq 'NETWORK SERVICE' -or $s -eq 'ANONYMOUS LOGON') { return '' } + return $s +} +$regPath = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Authentication\\LogonUI' +$sam = ''; $upn = '' +try { $sam = (Get-ItemProperty -Path $regPath -Name 'LastLoggedOnSAMUser' -ErrorAction Stop).LastLoggedOnSAMUser } catch {} +try { $upn = (Get-ItemProperty -Path $regPath -Name 'LastLoggedOnUser' -ErrorAction Stop).LastLoggedOnUser } catch {} +$user = Normalize-Sam $sam +if (-not $user) { + $user = Normalize-Sam $upn + if ($user -and $user -like '*@*') { + $domDns = (Get-WmiObject Win32_ComputerSystem).Domain + $domShort = '' + if ($domDns) { $domShort = ($domDns -split '\\.')[0].ToUpper() } + $parts = $user -split '@' + if ($parts.Length -ge 1) { + $u = $parts[0] + if ($domShort) { $user = "$domShort\\$u" } + } + } +} +if ($user) { $user } else { '' } +""" + out = subprocess.run(["powershell", "-NoProfile", "-Command", ps], capture_output=True, text=True, timeout=10) + s = (out.stdout or '').strip() + if s: + return s.splitlines()[0].strip() + except Exception: + pass + # Fallback to Python winreg lookup + try: + import winreg # type: ignore + key_path = r"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Authentication\\LogonUI" + def _qval(name): + try: + k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path, 0, winreg.KEY_READ | getattr(winreg, 'KEY_WOW64_64KEY', 0)) + try: + val, _ = winreg.QueryValueEx(k, name) + finally: + winreg.CloseKey(k) + return str(val or '').strip() + except Exception: + return '' + sam = _qval('LastLoggedOnSAMUser') + upn = _qval('LastLoggedOnUser') + def _ok(s: str) -> bool: + if not s: + return False + su = s.upper() + return not (s.endswith('$') or su in ('SYSTEM','LOCAL SERVICE','NETWORK SERVICE','ANONYMOUS LOGON') or s.startswith('DWM-') or s.startswith('UMFD-')) + if _ok(sam): + return sam + if _ok(upn): + if '@' in upn: + try: + user, dom = upn.split('@', 1) + dom_short = (dom.split('.')[0] or dom).upper() + return f"{dom_short}\\{user}" + except Exception: + pass + return upn + except Exception: + pass + return '' + + def _collect_last_user_string() -> str: if not IS_WINDOWS: return '' @@ -624,7 +701,7 @@ def _build_details_fallback() -> dict: pass # Last user(s) try: - last_user = _collect_last_user_string() + last_user = _collect_last_user_registry() if last_user: summary['last_user'] = last_user except Exception: @@ -812,7 +889,7 @@ class Role: pass return False if (not lu) or (lu.lower() == 'unknown') or _contains_machine_accounts(lu): - lu2 = _collect_last_user_string().strip() + lu2 = _collect_last_user_registry().strip() summary['last_user'] = lu2 if lu2 else 'No Users Logged In' except Exception: pass