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 CSV Report Columns: Date, Username, Display Name, PC Name, DNS, IP
PERFORMANCE: Uses regex-based XML extraction instead of DOM parsing PERFORMANCE: Uses event .Properties[] index access (no XML parsing)
and bulk AD user preloading for significantly faster execution. and bulk AD user preloading for fast execution.
.PARAMETER OutputPath .PARAMETER OutputPath
Path for the output CSV file. Defaults to current directory. 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) # Build logon types lookup (HashSet for fast contains-check using int)
$LogonTypesAllowed = [System.Collections.Generic.HashSet[string]]::new() $LogonTypesAllowed = [System.Collections.Generic.HashSet[int]]::new()
@('2', '10', '11') | ForEach-Object { [void]$LogonTypesAllowed.Add($_) } @(2, 10, 11) | ForEach-Object { [void]$LogonTypesAllowed.Add($_) }
if (-not $ExcludeNetworkLogons) { if (-not $ExcludeNetworkLogons) {
[void]$LogonTypesAllowed.Add('3') [void]$LogonTypesAllowed.Add(3)
} }
# System accounts to exclude (HashSet for O(1) lookup) # 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 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 = @{} $UserLogons = @{}
$ProcessedCount = 0 $ProcessedCount = 0
$SkippedCount = 0 $SkippedCount = 0
$FilteredCount = 0
$i = 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) { foreach ($Event in $Events) {
$i++ $i++
if ($i % 10000 -eq 0) { 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 $props = $Event.Properties
$xmlStr = $Event.ToXml()
# Safety check - skip events with unexpected property count
if ($props.Count -lt 19) {
$FilteredCount++
continue
}
# 1. Check logon type FIRST (cheapest filter) # 1. Check logon type FIRST (cheapest filter)
$m = $rxLogonType.Match($xmlStr) $LogonType = [int]$props[8].Value
if (-not $m.Success) { continue } if (-not $LogonTypesAllowed.Contains($LogonType)) {
$LogonType = $m.Groups[1].Value $FilteredCount++
if (-not $LogonTypesAllowed.Contains($LogonType)) { continue } continue
}
# 2. Extract username # 2. Extract username
$m = $rxTargetUser.Match($xmlStr) $Username = [string]$props[5].Value
if (-not $m.Success) { continue }
$Username = $m.Groups[1].Value
# 3. Quick filters before extracting more fields # 3. Quick filters
if ([string]::IsNullOrWhiteSpace($Username)) { continue } if ([string]::IsNullOrWhiteSpace($Username)) { $FilteredCount++; continue }
if ($Username.EndsWith('$')) { continue } if ($Username.EndsWith('$')) { $FilteredCount++; continue }
if ($ExcludeUsers.Contains($Username)) { continue } if ($ExcludeUsers.Contains($Username)) { $FilteredCount++; continue }
$m = $rxTargetDomain.Match($xmlStr) $Domain = [string]$props[6].Value
$Domain = if ($m.Success) { $m.Groups[1].Value } else { '' } if ($ExcludeDomains.Contains($Domain)) { $FilteredCount++; continue }
if ($ExcludeDomains.Contains($Domain)) { continue }
# 4. Already seen this user? Skip (events are newest-first) # 4. Already seen this user? Skip (events are newest-first)
$UserKey = "$Domain\$Username" $UserKey = "$Domain\$Username"
@@ -217,13 +241,9 @@ try {
continue continue
} }
# 5. Extract workstation and IP # 5. Extract workstation and IP (only for first occurrence)
$m = $rxWorkstation.Match($xmlStr) $Workstation = [string]$props[11].Value
$Workstation = if ($m.Success) { $m.Groups[1].Value } else { '' } $IPAddress = [string]$props[18].Value
$m = $rxIPAddress.Match($xmlStr)
$IPAddress = if ($m.Success) { $m.Groups[1].Value } else { '' }
$LogonTime = $Event.TimeCreated $LogonTime = $Event.TimeCreated
# Determine PC name and DNS name # Determine PC name and DNS name
@@ -265,7 +285,9 @@ try {
$ProcessTime = (Get-Date) - $ScriptStart $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 # PHASE 4: Export CSV