Switch from regex XML to Properties[] index access - fixes 0 match issue, even faster

This commit is contained in:
2026-02-02 11:34:44 +11:00
parent b340c601c7
commit d5f82692fc

View File

@@ -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