Created
April 22, 2025 07:01
-
-
Save hongymagic/da77a83383b5e317f3a46519bce6ba87 to your computer and use it in GitHub Desktop.
Check windows users' last logon and logoff times
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param ( | |
[Parameter(Mandatory=$true, HelpMessage="Enter the username in email format (e.g., [email protected])")] | |
[ValidatePattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")] | |
[string]$Username, | |
[Parameter(HelpMessage="Number of days to look back for events")] | |
[int]$DaysBack = 1, | |
[Parameter(HelpMessage="Use alternative methods that don't require admin privileges")] | |
[switch]$UseAlternativeMethods = $true | |
) | |
# Extract username part (before the @) | |
$usernameOnly = $Username.Split('@')[0] | |
# Display the username being searched | |
Write-Host "Searching for logon/logoff events for user: $Username" -ForegroundColor Cyan | |
Write-Host "Also searching for username part: $usernameOnly" -ForegroundColor Cyan | |
Write-Host "Looking back $DaysBack days" -ForegroundColor Cyan | |
# Check if running as administrator | |
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) | |
if (-not $isAdmin) { | |
Write-Warning "This script is not running with administrator privileges." | |
Write-Warning "Security logs may not be accessible without admin rights." | |
Write-Warning "Try running PowerShell as Administrator and run this script again." | |
} | |
# Function to check if an event matches our username | |
function Test-EventForUsername { | |
param($event, $fullUsername, $shortUsername) | |
# Convert event to XML for easier property access | |
$eventXml = [xml]$event.ToXml() | |
$eventData = $eventXml.Event.EventData.Data | |
# Check for username in various formats | |
foreach ($data in $eventData) { | |
$value = $data.'#text' | |
if ($value -eq $fullUsername -or | |
$value -eq $shortUsername -or | |
$value -like "*\$shortUsername" -or | |
$value -like "*@*" -and $value -match [regex]::Escape($shortUsername)) { | |
return $true | |
} | |
} | |
# Also check raw properties | |
foreach ($prop in $event.Properties) { | |
$value = $prop.Value | |
if ($value -is [string] -and ( | |
$value -eq $fullUsername -or | |
$value -eq $shortUsername -or | |
$value -like "*\$shortUsername" -or | |
$value -like "*@*" -and $value -match [regex]::Escape($shortUsername))) { | |
return $true | |
} | |
} | |
return $false | |
} | |
# Function to get logon type description | |
function Get-LogonTypeDescription { | |
param([int]$logonType) | |
switch ($logonType) { | |
2 { "Interactive (Local logon)" } | |
3 { "Network (Remote logon)" } | |
4 { "Batch (Scheduled task)" } | |
5 { "Service (Service startup)" } | |
7 { "Unlock (Screen unlock)" } | |
8 { "NetworkCleartext (Network logon with cleartext credentials)" } | |
9 { "NewCredentials (RunAs using alternate credentials)" } | |
10 { "RemoteInteractive (Terminal Services, Remote Desktop)" } | |
11 { "CachedInteractive (Cached domain credentials)" } | |
default { "Other ($logonType)" } | |
} | |
} | |
# Function to get user sessions using Security event logs (requires admin) | |
function Get-UserSessionsFromEventLog { | |
param( | |
$username, | |
$usernameOnly, | |
$daysBack, | |
[switch]$IncludeSystemLogons = $false | |
) | |
$startTime = (Get-Date).AddDays(-$daysBack) | |
$logonEvents = @() | |
$logoffEvents = @() | |
try { | |
Write-Host "Retrieving Security events since $($startTime.ToString('yyyy-MM-dd HH:mm:ss'))..." -ForegroundColor Yellow | |
# First get all logon events to analyze | |
$allLogonEvents = Get-WinEvent -FilterHashtable @{ | |
LogName='Security'; | |
ID=4624; | |
StartTime=$startTime | |
} -MaxEvents 200 -ErrorAction Stop | |
Write-Host "Found $($allLogonEvents.Count) total logon events to analyze" -ForegroundColor Yellow | |
# Filter events for our username and extract logon type | |
$logonEvents = @() | |
foreach ($event in $allLogonEvents) { | |
if (Test-EventForUsername -event $event -fullUsername $username -shortUsername $usernameOnly) { | |
# Extract logon type from event | |
$eventXml = [xml]$event.ToXml() | |
$logonType = $null | |
foreach ($data in $eventXml.Event.EventData.Data) { | |
if ($data.Name -eq "LogonType") { | |
$logonType = [int]$data.'#text' | |
break | |
} | |
} | |
# Skip system logons unless specifically requested | |
if (-not $IncludeSystemLogons -and ($logonType -eq 4 -or $logonType -eq 5)) { | |
continue | |
} | |
# Add logon type to event as a note property | |
$event | Add-Member -NotePropertyName LogonType -NotePropertyValue $logonType -Force | |
$event | Add-Member -NotePropertyName LogonTypeDescription -NotePropertyValue (Get-LogonTypeDescription -logonType $logonType) -Force | |
$logonEvents += $event | |
} | |
} | |
# Get logoff events | |
$allLogoffEvents = Get-WinEvent -FilterHashtable @{ | |
LogName='Security'; | |
ID=4634; | |
StartTime=$startTime | |
} -MaxEvents 200 -ErrorAction Stop | |
Write-Host "Found $($allLogoffEvents.Count) total logoff events to analyze" -ForegroundColor Yellow | |
$logoffEvents = $allLogoffEvents | Where-Object { | |
Test-EventForUsername -event $_ -fullUsername $username -shortUsername $usernameOnly | |
} | |
return @{ | |
LogonEvents = $logonEvents | |
LogoffEvents = $logoffEvents | |
Success = $true | |
} | |
} | |
catch { | |
Write-Warning "Error accessing Security event logs: $_" | |
# Try with a smaller set of events as a fallback | |
try { | |
Write-Host "No exact matches found. Analyzing sample events to help troubleshoot..." -ForegroundColor Yellow | |
# Get a few sample events to analyze | |
$sampleEvents = Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4624} -MaxEvents 5 -ErrorAction SilentlyContinue | |
if ($sampleEvents.Count -gt 0) { | |
Write-Host "Sample event analysis:" -ForegroundColor Magenta | |
$sampleEvent = $sampleEvents[0] | |
# Convert to XML for better analysis | |
$eventXml = [xml]$sampleEvent.ToXml() | |
$eventData = $eventXml.Event.EventData.Data | |
Write-Host "Event data fields from sample logon event:" -ForegroundColor Magenta | |
foreach ($data in $eventData) { | |
$name = $data.Name | |
$value = $data.'#text' | |
Write-Host " $name : $value" | |
# Check if this might be a username field | |
if ($value -like "*@*" -or $value -like "*\*") { | |
Write-Host " ^ This field might contain a username" -ForegroundColor Green | |
} | |
} | |
# Also show raw properties for comparison | |
Write-Host "`nRaw properties from sample logon event:" -ForegroundColor Magenta | |
for ($i = 0; $i -lt $sampleEvent.Properties.Count; $i++) { | |
$value = $sampleEvent.Properties[$i].Value | |
Write-Host " Property[$i]: $value" | |
} | |
} | |
} | |
catch { | |
Write-Error "Cannot access Security logs at all. Run as Administrator." | |
} | |
return @{ | |
LogonEvents = @() | |
LogoffEvents = @() | |
Success = $false | |
Error = $_ | |
} | |
} | |
} | |
# Function to get user sessions using WMI (alternative method) | |
function Get-UserSessionsFromWMI { | |
param($username, $usernameOnly, $daysBack) | |
Write-Host "Using WMI to get user session information..." -ForegroundColor Yellow | |
$sessions = @() | |
$startTime = (Get-Date).AddDays(-$daysBack) | |
try { | |
# Get all user profiles | |
$userProfiles = Get-WmiObject -Class Win32_UserProfile | Where-Object { | |
$_.LastUseTime -and [Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) -gt $startTime | |
} | |
foreach ($profile in $userProfiles) { | |
# Try to get the username from the SID | |
try { | |
$sid = New-Object System.Security.Principal.SecurityIdentifier($profile.SID) | |
$user = $sid.Translate([System.Security.Principal.NTAccount]) | |
$profileUsername = $user.Value | |
# Check if this profile matches our target user | |
if ($profileUsername -like "*$usernameOnly*" -or | |
$profileUsername -eq $username -or | |
$profileUsername -like "*\$usernameOnly") { | |
$lastUseTime = [Management.ManagementDateTimeConverter]::ToDateTime($profile.LastUseTime) | |
$sessions += [PSCustomObject]@{ | |
Username = $profileUsername | |
LogonTime = $lastUseTime | |
LogoffTime = $null # We don't have logoff time from this method | |
Source = "UserProfile" | |
} | |
} | |
} | |
catch { | |
# Skip profiles where we can't resolve the username | |
continue | |
} | |
} | |
# Also try to get currently logged on users | |
$loggedOnUsers = Get-WmiObject -Class Win32_ComputerSystem | Select-Object -ExpandProperty UserName | |
foreach ($loggedOnUser in $loggedOnUsers) { | |
if ($loggedOnUser -like "*$usernameOnly*" -or | |
$loggedOnUser -eq $username -or | |
$loggedOnUser -like "*\$usernameOnly") { | |
$sessions += [PSCustomObject]@{ | |
Username = $loggedOnUser | |
LogonTime = Get-Date # We don't know the exact logon time | |
LogoffTime = $null # Currently logged in | |
Source = "CurrentlyLoggedOn" | |
} | |
} | |
} | |
# Try to get logon sessions | |
$logonSessions = Get-WmiObject -Class Win32_LogonSession | Where-Object { $_.LogonType -eq 2 -or $_.LogonType -eq 10 } | |
foreach ($session in $logonSessions) { | |
$logonUser = Get-WmiObject -Class Win32_LoggedOnUser | Where-Object { $_.Dependent.Path -like "*$($session.LogonId)*" } | |
if ($logonUser) { | |
$userAccountPath = $logonUser.Antecedent.Path | |
$userAccountName = ($userAccountPath -split '"')[1] | |
if ($userAccountName -like "*$usernameOnly*" -or | |
$userAccountName -eq $username -or | |
$userAccountName -like "*\$usernameOnly") { | |
$startTime = [Management.ManagementDateTimeConverter]::ToDateTime($session.StartTime) | |
$sessions += [PSCustomObject]@{ | |
Username = $userAccountName | |
LogonTime = $startTime | |
LogoffTime = $null # Currently logged in | |
Source = "LogonSession" | |
} | |
} | |
} | |
} | |
return @{ | |
Sessions = $sessions | |
Success = $true | |
} | |
} | |
catch { | |
Write-Warning "Error accessing WMI information: $_" | |
return @{ | |
Sessions = @() | |
Success = $false | |
Error = $_ | |
} | |
} | |
} | |
# Try to get user sessions | |
$logonEvents = @() | |
$logoffEvents = @() | |
$wmiSessions = @() | |
$foundSessions = $false | |
# First try Security event logs if we're admin | |
if ($isAdmin -or -not $UseAlternativeMethods) { | |
Write-Host "Attempting to use Security event logs (requires admin)..." -ForegroundColor Yellow | |
$eventLogResult = Get-UserSessionsFromEventLog -username $Username -usernameOnly $usernameOnly -daysBack $DaysBack | |
if ($eventLogResult.Success -and ($eventLogResult.LogonEvents.Count -gt 0 -or $eventLogResult.LogoffEvents.Count -gt 0)) { | |
$logonEvents = $eventLogResult.LogonEvents | |
$logoffEvents = $eventLogResult.LogoffEvents | |
$foundSessions = $true | |
} | |
} | |
# If we didn't find anything or aren't admin, try alternative methods | |
if ((-not $foundSessions -or $UseAlternativeMethods) -and -not $isAdmin) { | |
Write-Host "Attempting to use alternative methods (WMI)..." -ForegroundColor Yellow | |
$wmiResult = Get-UserSessionsFromWMI -username $Username -usernameOnly $usernameOnly -daysBack $DaysBack | |
if ($wmiResult.Success -and $wmiResult.Sessions.Count -gt 0) { | |
$wmiSessions = $wmiResult.Sessions | |
$foundSessions = $true | |
} | |
} | |
# Check if we found any sessions | |
if (-not $foundSessions) { | |
Write-Warning "No sessions found for user '$Username' or '$usernameOnly'" | |
Write-Host "`nTroubleshooting tips:" -ForegroundColor Cyan | |
Write-Host "1. Try running as Administrator" -ForegroundColor Cyan | |
Write-Host "2. Try increasing the days to look back with -DaysBack parameter" -ForegroundColor Cyan | |
Write-Host "3. The username might be in a different format in the logs (domain\\username)" -ForegroundColor Cyan | |
Write-Host "4. Check if Security auditing is enabled in Group Policy" -ForegroundColor Cyan | |
exit | |
} | |
# Display results from Event Logs if available | |
if ($logonEvents.Count -gt 0 -or $logoffEvents.Count -gt 0) { | |
Write-Host "`nSuccess! Found $($logonEvents.Count) logon events and $($logoffEvents.Count) logoff events for user '$Username'" -ForegroundColor Green | |
# Show the found events | |
if ($logonEvents.Count -gt 0) { | |
Write-Host "`nLogon events found:" -ForegroundColor Green | |
foreach ($event in $logonEvents) { | |
Write-Host " $($event.TimeCreated) - ID: $($event.Id) - RecordId: $($event.RecordId) - Type: $($event.LogonTypeDescription)" | |
} | |
} | |
# Group logon events by session ID if available | |
$sessionGroups = @{} | |
foreach ($logon in $logonEvents) { | |
$eventXml = [xml]$logon.ToXml() | |
$sessionId = $null | |
foreach ($data in $eventXml.Event.EventData.Data) { | |
if ($data.Name -eq "TargetLogonId") { | |
$sessionId = $data.'#text' | |
break | |
} | |
} | |
if ($sessionId) { | |
if (-not $sessionGroups.ContainsKey($sessionId)) { | |
$sessionGroups[$sessionId] = @{ | |
Logon = $logon | |
Logoffs = @() | |
} | |
} | |
} | |
} | |
# Match logoff events to sessions | |
foreach ($logoff in $logoffEvents) { | |
$eventXml = [xml]$logoff.ToXml() | |
$sessionId = $null | |
foreach ($data in $eventXml.Event.EventData.Data) { | |
if ($data.Name -eq "TargetLogonId") { | |
$sessionId = $data.'#text' | |
break | |
} | |
} | |
if ($sessionId -and $sessionGroups.ContainsKey($sessionId)) { | |
$sessionGroups[$sessionId].Logoffs += $logoff | |
} | |
} | |
# Process logon events to match with logoff events | |
Write-Host "`nUser sessions:" -ForegroundColor Green | |
# First try to use session IDs for matching | |
$sessionsFound = $false | |
foreach ($sessionId in $sessionGroups.Keys) { | |
$session = $sessionGroups[$sessionId] | |
$logon = $session.Logon | |
$logoffs = $session.Logoffs | Sort-Object TimeCreated | |
if ($logoffs.Count -gt 0) { | |
$logoff = $logoffs[0] # Take the first logoff event | |
$duration = $logoff.TimeCreated - $logon.TimeCreated | |
# Only show sessions with a reasonable duration (more than 1 second) | |
if ($duration.TotalSeconds -gt 1) { | |
$formattedDuration = "{0:D2}:{1:D2}:{2:D2}" -f $duration.Hours, $duration.Minutes, $duration.Seconds | |
Write-Host " Session ID: $sessionId" -ForegroundColor Yellow | |
Write-Host " Logon: $($logon.TimeCreated) - Type: $($logon.LogonTypeDescription)" | |
Write-Host " Logoff: $($logoff.TimeCreated)" | |
Write-Host " Duration: $formattedDuration" | |
Write-Host "" | |
$sessionsFound = $true | |
} | |
} | |
else { | |
# No logoff found, user might still be logged in | |
Write-Host " Session ID: $sessionId" -ForegroundColor Yellow | |
Write-Host " Logon: $($logon.TimeCreated) - Type: $($logon.LogonTypeDescription)" | |
Write-Host " Status: Still logged in" | |
Write-Host "" | |
$sessionsFound = $true | |
} | |
} | |
# If no sessions were found using session IDs, fall back to time-based matching | |
if (-not $sessionsFound) { | |
Write-Host " No sessions found using session IDs. Using time-based matching:" -ForegroundColor Yellow | |
foreach ($logon in $logonEvents) { | |
# Skip system or service logons | |
if ($logon.LogonType -eq 4 -or $logon.LogonType -eq 5) { | |
continue | |
} | |
$logoff = $logoffEvents | Where-Object { $_.TimeCreated -gt $logon.TimeCreated } | Sort-Object TimeCreated | Select-Object -First 1 | |
if ($logoff) { | |
$duration = $logoff.TimeCreated - $logon.TimeCreated | |
# Only show sessions with a reasonable duration (more than 1 second) | |
if ($duration.TotalSeconds -gt 1) { | |
$formattedDuration = "{0:D2}:{1:D2}:{2:D2}" -f $duration.Hours, $duration.Minutes, $duration.Seconds | |
Write-Host " Logon: $($logon.TimeCreated) - Type: $($logon.LogonTypeDescription)" | |
Write-Host " Logoff: $($logoff.TimeCreated)" | |
Write-Host " Duration: $formattedDuration" | |
Write-Host "" | |
} | |
} | |
else { | |
Write-Host " Logon: $($logon.TimeCreated) - Type: $($logon.LogonTypeDescription)" | |
Write-Host " Status: Still logged in or no matching logoff event found" | |
Write-Host "" | |
} | |
} | |
} | |
# If we found logon events but no valid sessions, show a message | |
if ($logonEvents.Count -gt 0 -and -not $sessionsFound) { | |
Write-Host "`nNote: Found logon/logoff events but no valid user sessions with reasonable duration." -ForegroundColor Yellow | |
Write-Host "This might be because:" -ForegroundColor Yellow | |
Write-Host "1. The events are related to system processes rather than actual user logons" -ForegroundColor Yellow | |
Write-Host "2. The logon and logoff events occurred very close together (less than 1 second apart)" -ForegroundColor Yellow | |
Write-Host "3. The logoff events don't properly match with the logon events" -ForegroundColor Yellow | |
# Show raw events for debugging | |
Write-Host "`nRaw logon events for reference:" -ForegroundColor Magenta | |
foreach ($event in $logonEvents) { | |
Write-Host " $($event.TimeCreated) - ID: $($event.Id) - Type: $($event.LogonTypeDescription) - RecordId: $($event.RecordId)" | |
} | |
} | |
} | |
# Display results from WMI if available | |
if ($wmiSessions.Count -gt 0) { | |
Write-Host "`nSuccess! Found $($wmiSessions.Count) user sessions from WMI for user '$Username'" -ForegroundColor Green | |
# Show the found sessions | |
Write-Host "`nUser sessions found (from WMI):" -ForegroundColor Green | |
foreach ($session in $wmiSessions) { | |
$status = if ($session.LogoffTime) { "Logged off" } else { "Still logged in" } | |
Write-Host " Username: $($session.Username)" | |
Write-Host " Logon Time: $($session.LogonTime)" | |
Write-Host " Status: $status" | |
Write-Host " Source: $($session.Source)" | |
Write-Host "" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment