Fix CSV export, update columns to: Date, Username, Display Name, PC Name, DNS, IP

This commit is contained in:
2026-02-02 11:01:41 +11:00
parent 15f0ad43ca
commit b340c601c7

View File

@@ -1,13 +1,15 @@
#Requires -Modules ActiveDirectory
<#
.SYNOPSIS
Exports a list of users and the last PC they logged into.
Exports a CSV report of users and the last PC they logged into.
.DESCRIPTION
Queries Domain Controller security event logs for logon events (4624)
to determine which computer each user last authenticated from.
Resolves IP addresses to DNS names and looks up user display names.
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.
@@ -54,6 +56,17 @@ param(
$ScriptStart = Get-Date
# Validate output path exists
if (-not (Test-Path $OutputPath)) {
Write-Host "Output path '$OutputPath' does not exist. Creating it..." -ForegroundColor Yellow
try {
New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
} catch {
Write-Host "Error: Could not create output directory '$OutputPath'. $($_.Exception.Message)" -ForegroundColor Red
return
}
}
# Build logon types lookup (HashSet for fast contains-check)
$LogonTypesAllowed = [System.Collections.Generic.HashSet[string]]::new()
@('2', '10', '11') | ForEach-Object { [void]$LogonTypesAllowed.Add($_) }
@@ -76,10 +89,11 @@ $ExcludeDomains = [System.Collections.Generic.HashSet[string]]::new(
ForEach-Object { [void]$ExcludeDomains.Add($_) }
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$CsvFile = Join-Path $OutputPath "UserLastLogonComputer_$Timestamp.csv"
$CsvFile = Join-Path $OutputPath "UserLastLogonReport_$Timestamp.csv"
Write-Host "`nQuerying security event logs for the last $DaysBack days..." -ForegroundColor Cyan
Write-Host "Looking for logon types: $($LogonTypesAllowed -join ', ')" -ForegroundColor Cyan
Write-Host "CSV will be saved to: $CsvFile" -ForegroundColor Cyan
Write-Host ""
$StartDate = (Get-Date).AddDays(-$DaysBack)
@@ -136,14 +150,6 @@ function Resolve-IPToDNSName {
}
}
# Logon type display labels
$LogonTypeLabels = @{
'2' = 'Interactive'
'3' = 'Network'
'10' = 'RDP'
'11' = 'Cached'
}
try {
# ==========================================
# PHASE 2: Retrieve events
@@ -163,16 +169,12 @@ try {
# ==========================================
# PHASE 3: Process events with regex (FAST)
# ==========================================
# Events come newest-first from Get-WinEvent, so the first time we see
# a user is already their most recent logon - we can skip all later entries.
$UserLogons = @{}
$ProcessedCount = 0
$SkippedCount = 0
$i = 0
# Pre-compiled regex patterns for extracting fields from raw XML
# This is ~10-20x faster than [xml] DOM parsing + Where-Object pipelines
$rxLogonType = [regex]'Name="LogonType">(\d+)<'
$rxTargetUser = [regex]'Name="TargetUserName">([^<]+)<'
$rxTargetDomain = [regex]'Name="TargetDomainName">([^<]+)<'
@@ -185,7 +187,7 @@ try {
Write-Host " Processing event $i of $($Events.Count)... ($ProcessedCount matched, $SkippedCount skipped as already seen)" -ForegroundColor Gray
}
# Get raw XML string - DO NOT parse as [xml], just regex it
# Get raw XML string - regex instead of [xml] DOM parsing
$xmlStr = $Event.ToXml()
# 1. Check logon type FIRST (cheapest filter)
@@ -194,7 +196,7 @@ try {
$LogonType = $m.Groups[1].Value
if (-not $LogonTypesAllowed.Contains($LogonType)) { continue }
# 2. Extract username and domain
# 2. Extract username
$m = $rxTargetUser.Match($xmlStr)
if (-not $m.Success) { continue }
$Username = $m.Groups[1].Value
@@ -215,7 +217,7 @@ try {
continue
}
# 5. Only NOW extract the expensive fields (workstation, IP)
# 5. Extract workstation and IP
$m = $rxWorkstation.Match($xmlStr)
$Workstation = if ($m.Success) { $m.Groups[1].Value } else { '' }
@@ -224,39 +226,40 @@ try {
$LogonTime = $Event.TimeCreated
# Determine computer name and DNS name
$Computer = $null
# Determine PC name and DNS name
$PCName = $null
$DNSName = ''
if (-not $SkipIPResolve -and -not [string]::IsNullOrWhiteSpace($IPAddress) -and $IPAddress -ne '-') {
$resolved = Resolve-IPToDNSName -IP $IPAddress
if ($resolved.Short) { $Computer = $resolved.Short }
if ($resolved.Short) { $PCName = $resolved.Short }
if ($resolved.FQDN) { $DNSName = $resolved.FQDN }
}
# Fall back to WorkstationName if IP didn't resolve
if ([string]::IsNullOrWhiteSpace($Computer)) {
if ([string]::IsNullOrWhiteSpace($PCName)) {
if (-not [string]::IsNullOrWhiteSpace($Workstation) -and $Workstation -ne '-') {
$Computer = $Workstation.ToUpper()
$PCName = $Workstation.ToUpper()
} else {
$Computer = "Unknown"
$PCName = 'Unknown'
}
}
# Clean up IP for display
$IPDisplay = if ($IPAddress -and $IPAddress -ne '-' -and $IPAddress -ne '::1' -and $IPAddress -ne '127.0.0.1') { $IPAddress } else { '' }
$ProcessedCount++
# Store result - display name from bulk preloaded cache
# Store result with report column names
$DisplayName = if ($UserDisplayNames.ContainsKey($Username)) { $UserDisplayNames[$Username] } else { '' }
$TypeLabel = if ($LogonTypeLabels.ContainsKey($LogonType)) { $LogonTypeLabels[$LogonType] } else { "Type $LogonType" }
$UserLogons[$UserKey] = [PSCustomObject]@{
Domain = $Domain
Username = $Username
DisplayName = $DisplayName
Computer = $Computer
DNSName = $DNSName
LogonTime = $LogonTime
LogonType = $TypeLabel
'Date' = $LogonTime.ToString('yyyy-MM-dd HH:mm:ss')
'Username' = $Username
'Display Name' = $DisplayName
'PC Name' = $PCName
'DNS' = $DNSName
'IP' = $IPDisplay
}
}
@@ -264,27 +267,46 @@ try {
Write-Host "Processed $ProcessedCount unique user logons ($SkippedCount duplicate events skipped)." -ForegroundColor Yellow
$Results = $UserLogons.Values | Sort-Object Domain, Username
# ==========================================
# PHASE 4: Export CSV
# ==========================================
$Results = @($UserLogons.Values | Sort-Object Username)
if ($Results.Count -eq 0) {
Write-Host "`nNo user logon events found matching criteria." -ForegroundColor Yellow
return
}
# ==========================================
# PHASE 4: Export
# ==========================================
$Results | Select-Object Domain, Username, DisplayName, Computer, DNSName, LogonTime, LogonType |
Export-Csv -Path $CsvFile -NoTypeInformation -Encoding UTF8
# Write CSV
try {
$Results | Export-Csv -Path $CsvFile -NoTypeInformation -Encoding UTF8 -Force
if (Test-Path $CsvFile) {
$FileSize = (Get-Item $CsvFile).Length
Write-Host "`n===== Report Saved =====" -ForegroundColor Green
Write-Host "Total users: $($Results.Count)"
Write-Host "File size: $([math]::Round($FileSize / 1KB, 1)) KB"
Write-Host "Saved to: $CsvFile" -ForegroundColor Green
Write-Host "Completed in: $($ProcessTime.Minutes)m $($ProcessTime.Seconds)s" -ForegroundColor Cyan
} else {
Write-Host "`nError: CSV file was not created at '$CsvFile'" -ForegroundColor Red
}
} catch {
Write-Host "`nError saving CSV: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Attempting to save to Desktop instead..." -ForegroundColor Yellow
$FallbackPath = Join-Path ([Environment]::GetFolderPath('Desktop')) "UserLastLogonReport_$Timestamp.csv"
try {
$Results | Export-Csv -Path $FallbackPath -NoTypeInformation -Encoding UTF8 -Force
Write-Host "Saved to fallback location: $FallbackPath" -ForegroundColor Green
} catch {
Write-Host "Error: Could not save CSV. $($_.Exception.Message)" -ForegroundColor Red
}
}
Write-Host "`n===== Results =====" -ForegroundColor Green
Write-Host "Total users found: $($Results.Count)"
Write-Host "Output saved to: $CsvFile"
Write-Host "Completed in: $($ProcessTime.Minutes)m $($ProcessTime.Seconds)s" -ForegroundColor Cyan
Write-Host ""
# Display summary table
$Results | Format-Table Domain, Username, DisplayName, Computer, DNSName, LogonTime, LogonType -AutoSize
# Display summary table in console
$Results | Format-Table -AutoSize
} catch [System.Exception] {
if ($_.Exception.Message -match "No events were found") {