diff --git a/Get-UserLastLogonComputer.ps1 b/Get-UserLastLogonComputer.ps1 index 5261a66..b21a817 100644 --- a/Get-UserLastLogonComputer.ps1 +++ b/Get-UserLastLogonComputer.ps1 @@ -10,8 +10,8 @@ CSV Report Columns: Date, Username, Display Name, PC Name, DNS, IP - PERFORMANCE: Uses regex-based XML extraction instead of DOM parsing - and bulk AD user preloading for significantly faster execution. + PERFORMANCE: Uses event .Properties[] index access (no XML parsing) + and bulk AD user preloading for fast execution. .PARAMETER OutputPath Path for the output CSV file. Defaults to current directory. @@ -67,11 +67,11 @@ if (-not (Test-Path $OutputPath)) { } } -# Build logon types lookup (HashSet for fast contains-check) -$LogonTypesAllowed = [System.Collections.Generic.HashSet[string]]::new() -@('2', '10', '11') | ForEach-Object { [void]$LogonTypesAllowed.Add($_) } +# Build logon types lookup (HashSet for fast contains-check using int) +$LogonTypesAllowed = [System.Collections.Generic.HashSet[int]]::new() +@(2, 10, 11) | ForEach-Object { [void]$LogonTypesAllowed.Add($_) } if (-not $ExcludeNetworkLogons) { - [void]$LogonTypesAllowed.Add('3') + [void]$LogonTypesAllowed.Add(3) } # System accounts to exclude (HashSet for O(1) lookup) @@ -167,48 +167,72 @@ try { Write-Host "Found $($Events.Count) total 4624 events. Processing..." -ForegroundColor Yellow # ========================================== - # PHASE 3: Process events with regex (FAST) + # PHASE 3: Process events using Properties[] index # ========================================== + # Event 4624 Properties index map: + # [0] SubjectUserSid [1] SubjectUserName + # [2] SubjectDomainName [3] SubjectLogonId + # [4] TargetUserSid [5] TargetUserName + # [6] TargetDomainName [7] TargetLogonId + # [8] LogonType [9] LogonProcessName + # [10] AuthenticationPackageName + # [11] WorkstationName [12] LogonGuid + # [13] TransmittedServices [14] LmPackageName + # [15] KeyLength [16] ProcessId + # [17] ProcessName [18] IpAddress + # [19] IpPort + # + # Using .Properties[n].Value is MUCH faster than XML parsing + + # Dump first event to verify field mapping + $firstEvent = $Events[0] + $props = $firstEvent.Properties + Write-Host "`n DEBUG: First event field check:" -ForegroundColor DarkGray + Write-Host " [5] TargetUserName = '$($props[5].Value)'" -ForegroundColor DarkGray + Write-Host " [6] TargetDomainName = '$($props[6].Value)'" -ForegroundColor DarkGray + Write-Host " [8] LogonType = '$($props[8].Value)'" -ForegroundColor DarkGray + Write-Host " [11] WorkstationName = '$($props[11].Value)'" -ForegroundColor DarkGray + Write-Host " [18] IpAddress = '$($props[18].Value)'" -ForegroundColor DarkGray + Write-Host " Total properties count = $($props.Count)" -ForegroundColor DarkGray + Write-Host "" + $UserLogons = @{} $ProcessedCount = 0 $SkippedCount = 0 + $FilteredCount = 0 $i = 0 - # Pre-compiled regex patterns for extracting fields from raw XML - $rxLogonType = [regex]'Name="LogonType">(\d+)<' - $rxTargetUser = [regex]'Name="TargetUserName">([^<]+)<' - $rxTargetDomain = [regex]'Name="TargetDomainName">([^<]+)<' - $rxWorkstation = [regex]'Name="WorkstationName">([^<]*)<' - $rxIPAddress = [regex]'Name="IpAddress">([^<]*)<' - foreach ($Event in $Events) { $i++ if ($i % 10000 -eq 0) { - Write-Host " Processing event $i of $($Events.Count)... ($ProcessedCount matched, $SkippedCount skipped as already seen)" -ForegroundColor Gray + Write-Host " Processing event $i of $($Events.Count)... ($ProcessedCount matched, $SkippedCount already seen, $FilteredCount filtered)" -ForegroundColor Gray } - # Get raw XML string - regex instead of [xml] DOM parsing - $xmlStr = $Event.ToXml() + $props = $Event.Properties + # Safety check - skip events with unexpected property count + if ($props.Count -lt 19) { + $FilteredCount++ + continue + } + # 1. Check logon type FIRST (cheapest filter) - $m = $rxLogonType.Match($xmlStr) - if (-not $m.Success) { continue } - $LogonType = $m.Groups[1].Value - if (-not $LogonTypesAllowed.Contains($LogonType)) { continue } + $LogonType = [int]$props[8].Value + if (-not $LogonTypesAllowed.Contains($LogonType)) { + $FilteredCount++ + continue + } # 2. Extract username - $m = $rxTargetUser.Match($xmlStr) - if (-not $m.Success) { continue } - $Username = $m.Groups[1].Value + $Username = [string]$props[5].Value - # 3. Quick filters before extracting more fields - if ([string]::IsNullOrWhiteSpace($Username)) { continue } - if ($Username.EndsWith('$')) { continue } - if ($ExcludeUsers.Contains($Username)) { continue } + # 3. Quick filters + if ([string]::IsNullOrWhiteSpace($Username)) { $FilteredCount++; continue } + if ($Username.EndsWith('$')) { $FilteredCount++; continue } + if ($ExcludeUsers.Contains($Username)) { $FilteredCount++; continue } - $m = $rxTargetDomain.Match($xmlStr) - $Domain = if ($m.Success) { $m.Groups[1].Value } else { '' } - if ($ExcludeDomains.Contains($Domain)) { continue } + $Domain = [string]$props[6].Value + if ($ExcludeDomains.Contains($Domain)) { $FilteredCount++; continue } # 4. Already seen this user? Skip (events are newest-first) $UserKey = "$Domain\$Username" @@ -217,13 +241,9 @@ try { continue } - # 5. Extract workstation and IP - $m = $rxWorkstation.Match($xmlStr) - $Workstation = if ($m.Success) { $m.Groups[1].Value } else { '' } - - $m = $rxIPAddress.Match($xmlStr) - $IPAddress = if ($m.Success) { $m.Groups[1].Value } else { '' } - + # 5. Extract workstation and IP (only for first occurrence) + $Workstation = [string]$props[11].Value + $IPAddress = [string]$props[18].Value $LogonTime = $Event.TimeCreated # Determine PC name and DNS name @@ -265,7 +285,9 @@ try { $ProcessTime = (Get-Date) - $ScriptStart - Write-Host "Processed $ProcessedCount unique user logons ($SkippedCount duplicate events skipped)." -ForegroundColor Yellow + Write-Host "`nProcessed $ProcessedCount unique user logons." -ForegroundColor Yellow + Write-Host " $SkippedCount duplicate events skipped (already seen user)." -ForegroundColor Gray + Write-Host " $FilteredCount events filtered (system/computer accounts, wrong logon type)." -ForegroundColor Gray # ========================================== # PHASE 4: Export CSV