247 lines
8.4 KiB
PowerShell
247 lines
8.4 KiB
PowerShell
#Requires -Modules ActiveDirectory
|
|
<#
|
|
.SYNOPSIS
|
|
Exports a list 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.
|
|
|
|
.PARAMETER OutputPath
|
|
Path for the output CSV file. Defaults to current directory.
|
|
|
|
.PARAMETER DaysBack
|
|
Number of days of event logs to search. Default is 7.
|
|
|
|
.PARAMETER ExcludeNetworkLogons
|
|
Exclude Type 3 (Network) logons. Default includes them.
|
|
|
|
.PARAMETER SkipIPResolve
|
|
Skip IP to hostname resolution. Faster but less useful.
|
|
|
|
.EXAMPLE
|
|
.\Get-UserLastLogonComputer.ps1
|
|
|
|
.EXAMPLE
|
|
.\Get-UserLastLogonComputer.ps1 -OutputPath "C:\Reports" -DaysBack 30
|
|
|
|
.NOTES
|
|
Must be run on a Domain Controller with appropriate permissions to read Security logs.
|
|
Logon Type 2 = Interactive (console)
|
|
Logon Type 3 = Network (file shares, etc.)
|
|
Logon Type 10 = RemoteInteractive (RDP)
|
|
Logon Type 11 = CachedInteractive
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter()]
|
|
[string]$OutputPath = (Get-Location).Path,
|
|
|
|
[Parameter()]
|
|
[int]$DaysBack = 7,
|
|
|
|
[Parameter()]
|
|
[switch]$ExcludeNetworkLogons,
|
|
|
|
[Parameter()]
|
|
[switch]$SkipIPResolve
|
|
)
|
|
|
|
# Build logon types list
|
|
$LogonTypes = @(2, 10, 11) # Interactive, RDP, Cached
|
|
if (-not $ExcludeNetworkLogons) {
|
|
$LogonTypes += 3 # Network
|
|
}
|
|
|
|
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
|
$CsvFile = Join-Path $OutputPath "UserLastLogonComputer_$Timestamp.csv"
|
|
|
|
Write-Host "`nQuerying security event logs for the last $DaysBack days..." -ForegroundColor Cyan
|
|
Write-Host "Looking for logon types: $($LogonTypes -join ', ')" -ForegroundColor Cyan
|
|
Write-Host ""
|
|
|
|
$StartDate = (Get-Date).AddDays(-$DaysBack)
|
|
|
|
# Cache for IP to DNS name resolution
|
|
$DNSCache = @{}
|
|
# Cache for user display names
|
|
$UserCache = @{}
|
|
|
|
function Resolve-IPToDNSName {
|
|
param([string]$IP)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($IP) -or $IP -eq '-' -or $IP -eq '::1' -or $IP -eq '127.0.0.1') {
|
|
return @{ Short = $null; FQDN = $null }
|
|
}
|
|
|
|
if ($DNSCache.ContainsKey($IP)) {
|
|
return $DNSCache[$IP]
|
|
}
|
|
|
|
try {
|
|
# DNS reverse lookup returns FQDN
|
|
$fqdn = [System.Net.Dns]::GetHostEntry($IP).HostName
|
|
$shortName = ($fqdn -split '\.')[0].ToUpper()
|
|
$result = @{ Short = $shortName; FQDN = $fqdn.ToLower() }
|
|
$DNSCache[$IP] = $result
|
|
return $result
|
|
} catch {
|
|
try {
|
|
$computer = Get-ADComputer -Filter "IPv4Address -eq '$IP'" -Properties DNSHostName, Name -ErrorAction SilentlyContinue
|
|
if ($computer) {
|
|
$result = @{ Short = $computer.Name; FQDN = if ($computer.DNSHostName) { $computer.DNSHostName.ToLower() } else { '' } }
|
|
$DNSCache[$IP] = $result
|
|
return $result
|
|
}
|
|
} catch {}
|
|
|
|
$result = @{ Short = $null; FQDN = $null }
|
|
$DNSCache[$IP] = $result
|
|
return $result
|
|
}
|
|
}
|
|
|
|
function Get-UserDisplayName {
|
|
param([string]$Username, [string]$Domain)
|
|
|
|
$UserKey = "$Domain\$Username"
|
|
|
|
if ($UserCache.ContainsKey($UserKey)) {
|
|
return $UserCache[$UserKey]
|
|
}
|
|
|
|
try {
|
|
$User = Get-ADUser -Identity $Username -Properties DisplayName -ErrorAction SilentlyContinue
|
|
if ($User -and $User.DisplayName) {
|
|
$UserCache[$UserKey] = $User.DisplayName
|
|
return $User.DisplayName
|
|
}
|
|
} catch {}
|
|
|
|
$UserCache[$UserKey] = ''
|
|
return ''
|
|
}
|
|
|
|
try {
|
|
Write-Host "Retrieving logon events from Security log..." -ForegroundColor Yellow
|
|
|
|
$FilterHash = @{
|
|
LogName = 'Security'
|
|
ID = 4624
|
|
StartTime = $StartDate
|
|
}
|
|
|
|
$Events = Get-WinEvent -FilterHashtable $FilterHash -ErrorAction Stop
|
|
|
|
Write-Host "Found $($Events.Count) total 4624 events. Filtering..." -ForegroundColor Yellow
|
|
|
|
$UserLogons = @{}
|
|
$ProcessedCount = 0
|
|
$i = 0
|
|
|
|
foreach ($Event in $Events) {
|
|
$i++
|
|
if ($i % 5000 -eq 0) {
|
|
Write-Host " Processing event $i of $($Events.Count)..." -ForegroundColor Gray
|
|
}
|
|
|
|
$xml = [xml]$Event.ToXml()
|
|
|
|
$LogonType = [int]($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'LogonType' }).'#text'
|
|
|
|
if ($LogonType -notin $LogonTypes) { continue }
|
|
|
|
$Username = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
|
|
$Domain = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetDomainName' }).'#text'
|
|
$Workstation = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'WorkstationName' }).'#text'
|
|
$IPAddress = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
|
|
$LogonTime = $Event.TimeCreated
|
|
|
|
# Filter out computer accounts and system accounts
|
|
if ($Username -match '\$$') { continue }
|
|
if ($Username -in @('SYSTEM', 'LOCAL SERVICE', 'NETWORK SERVICE', 'DWM-1', 'DWM-2', 'DWM-3', 'DWM-4', 'UMFD-0', 'UMFD-1', 'UMFD-2', 'UMFD-3', 'ANONYMOUS LOGON', '-')) { continue }
|
|
if ($Domain -in @('Window Manager', 'Font Driver Host', 'NT AUTHORITY')) { continue }
|
|
if ([string]::IsNullOrWhiteSpace($Username)) { continue }
|
|
|
|
# Determine computer name and DNS name
|
|
$Computer = $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.FQDN) { $DNSName = $resolved.FQDN }
|
|
}
|
|
|
|
# Fall back to WorkstationName if IP didn't resolve
|
|
if ([string]::IsNullOrWhiteSpace($Computer)) {
|
|
if (-not [string]::IsNullOrWhiteSpace($Workstation) -and $Workstation -ne '-') {
|
|
$Computer = $Workstation.ToUpper()
|
|
} else {
|
|
$Computer = "Unknown"
|
|
}
|
|
}
|
|
|
|
$UserKey = "$Domain\$Username"
|
|
$ProcessedCount++
|
|
|
|
# Keep only the most recent logon for each user
|
|
if (-not $UserLogons.ContainsKey($UserKey) -or $LogonTime -gt $UserLogons[$UserKey].LogonTime) {
|
|
$UserLogons[$UserKey] = [PSCustomObject]@{
|
|
Domain = $Domain
|
|
Username = $Username
|
|
DisplayName = '' # Will populate after
|
|
Computer = $Computer
|
|
DNSName = $DNSName
|
|
LogonTime = $LogonTime
|
|
LogonType = switch ($LogonType) {
|
|
2 { "Interactive" }
|
|
3 { "Network" }
|
|
10 { "RDP" }
|
|
11 { "Cached" }
|
|
default { "Type $LogonType" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host "Processed $ProcessedCount matching logon events." -ForegroundColor Yellow
|
|
Write-Host "Looking up display names for $($UserLogons.Count) users..." -ForegroundColor Yellow
|
|
|
|
# Populate display names
|
|
foreach ($Key in $UserLogons.Keys) {
|
|
$User = $UserLogons[$Key]
|
|
$User.DisplayName = Get-UserDisplayName -Username $User.Username -Domain $User.Domain
|
|
}
|
|
|
|
$Results = $UserLogons.Values | Sort-Object Domain, Username
|
|
|
|
if ($Results.Count -eq 0) {
|
|
Write-Host "`nNo user logon events found matching criteria." -ForegroundColor Yellow
|
|
return
|
|
}
|
|
|
|
# Export to CSV
|
|
$Results | Select-Object Domain, Username, DisplayName, Computer, DNSName, LogonTime, LogonType |
|
|
Export-Csv -Path $CsvFile -NoTypeInformation -Encoding UTF8
|
|
|
|
Write-Host "`n===== Results =====" -ForegroundColor Green
|
|
Write-Host "Total users found: $($Results.Count)"
|
|
Write-Host "Output saved to: $CsvFile"
|
|
Write-Host ""
|
|
|
|
# Display summary table
|
|
$Results | Format-Table Domain, Username, DisplayName, Computer, DNSName, LogonTime, LogonType -AutoSize
|
|
|
|
} catch [System.Exception] {
|
|
if ($_.Exception.Message -match "No events were found") {
|
|
Write-Host "`nNo logon events (Event ID 4624) found in the last $DaysBack days." -ForegroundColor Yellow
|
|
} elseif ($_.Exception.Message -match "Access is denied") {
|
|
Write-Host "`nError: Access denied. Run PowerShell as Administrator." -ForegroundColor Red
|
|
} else {
|
|
Write-Host "`nError: $($_.Exception.Message)" -ForegroundColor Red
|
|
}
|
|
}
|