#Requires -Version 5.1 <# .SYNOPSIS Gets user logon, logoff, lock, unlock, and power activity from Windows event logs. .EXAMPLE $Start = '2026-07-03T00:00:00Z' $End = '2026-07-06T00:00:00Z' .\Get-UserLogonActivity.ps1 .EXAMPLE .\Get-UserLogonActivity.ps1 Returns activity from the last 7 days. .EXAMPLE $Start = '2026-07-03T00:00:00Z' .\Get-UserLogonActivity.ps1 Returns activity from the specified start date through now. #> $StartVariable = Get-Variable -Name Start -ErrorAction SilentlyContinue $EndVariable = Get-Variable -Name End -ErrorAction SilentlyContinue Function Test-ActivityDateRangeValue { Param( [AllowNull()] [object]$Value ) If ($null -eq $Value) { Return $false } If ($Value -is [string]) { Return -not [string]::IsNullOrWhiteSpace($Value) } Return $true } Function Get-UserLogonActivityErrorMessage { Param( [AllowNull()] [object]$ErrorRecord, [string]$FallbackMessage = 'Unknown error.' ) $Message = '' If ($null -ne $ErrorRecord) { If ($null -ne $ErrorRecord.Exception) { $Message = [string]$ErrorRecord.Exception.Message } If ([string]::IsNullOrWhiteSpace($Message)) { $Message = [string]$ErrorRecord } } If ([string]::IsNullOrWhiteSpace($Message)) { $Message = $FallbackMessage } Return $Message } $StartHasValue = ($null -ne $StartVariable -and (Test-ActivityDateRangeValue -Value $StartVariable.Value)) $EndHasValue = ($null -ne $EndVariable -and (Test-ActivityDateRangeValue -Value $EndVariable.Value)) If ($EndHasValue -and -not $StartHasValue) { Write-Error '$Start must be defined when $End is defined. If only $Start is defined, the script uses now for $End. If neither is defined, the script returns activity from the last 7 days.' Exit 1 } Function ConvertTo-ActivityDateTime { Param( [Parameter(Mandatory=$true)] [AllowNull()] [object]$Value, [Parameter(Mandatory=$true)] [string]$ParameterName ) If ($Value -is [DateTimeOffset]) { Return $Value } If ($Value -is [DateTime]) { Return [DateTimeOffset]$Value } $TrimmedValue = ([string]$Value).Trim() $Culture = [Globalization.CultureInfo]::InvariantCulture $ParsedDateTime = [DateTimeOffset]::MinValue $UtcFormats = [string[]]@( "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-dd'T'HH:mm:ss.FFFFFFF'Z'" ) $OffsetFormats = [string[]]@( "yyyy-MM-dd'T'HH:mm:sszzz", "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFzzz" ) $LocalFormats = [string[]]@( "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.FFFFFFF", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd", "MM/dd/yyyy HH:mm:ss", "MM/dd/yyyy" ) $UtcStyles = [Globalization.DateTimeStyles]::AllowWhiteSpaces -bor [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal $LocalStyles = [Globalization.DateTimeStyles]::AllowWhiteSpaces -bor [Globalization.DateTimeStyles]::AssumeLocal If ([DateTimeOffset]::TryParseExact($TrimmedValue, $UtcFormats, $Culture, $UtcStyles, [ref]$ParsedDateTime)) { Return $ParsedDateTime } If ([DateTimeOffset]::TryParseExact($TrimmedValue, $OffsetFormats, $Culture, [Globalization.DateTimeStyles]::AllowWhiteSpaces, [ref]$ParsedDateTime)) { Return $ParsedDateTime } If ([DateTimeOffset]::TryParseExact($TrimmedValue, $LocalFormats, $Culture, $LocalStyles, [ref]$ParsedDateTime)) { Return $ParsedDateTime } If ([DateTimeOffset]::TryParse($TrimmedValue, $Culture, $LocalStyles, [ref]$ParsedDateTime)) { Return $ParsedDateTime } Throw "Invalid -${ParameterName} datetime '${Value}'. Use ISO 8601 UTC format: yyyy-MM-ddTHH:mm:ssZ. Example: 2026-07-03T00:00:00Z" } Function Get-EventDataValue { Param( [Parameter(Mandatory=$true)] [xml]$EventXml, [Parameter(Mandatory=$true)] [string]$Name ) $Data = $EventXml.Event.EventData.Data | Where-Object { $_.Name -eq $Name } | Select-Object -First 1 If ($null -eq $Data) { Return $null } Return [string]$Data.'#text' } Function Get-EventDataValueFromList { Param( [Parameter(Mandatory=$true)] [xml]$EventXml, [Parameter(Mandatory=$true)] [string[]]$Names ) ForEach ($Name in $Names) { $Value = Get-EventDataValue -EventXml $EventXml -Name $Name If (-not [string]::IsNullOrWhiteSpace($Value) -and $Value -ne '-') { Return $Value } } Return $null } Function Get-UserDataEventXmlValue { Param( [Parameter(Mandatory=$true)] [xml]$EventXml, [Parameter(Mandatory=$true)] [string]$Name ) $Data = $EventXml.Event.UserData.EventXML.ChildNodes | Where-Object { $_.Name -eq $Name } | Select-Object -First 1 If ($null -eq $Data) { Return $null } Return [string]$Data.'#text' } Function Test-UserLogonActivityUser { Param( [AllowNull()] [string]$UserName ) If ([string]::IsNullOrWhiteSpace($UserName)) { Return $false } $LeafUserName = ($UserName -split '\\')[-1] If ($LeafUserName -eq '-') { Return $false } If ($LeafUserName.EndsWith('$')) { Return $false } If ($LeafUserName -in @('ANONYMOUS LOGON', 'LOCAL SERVICE', 'NETWORK SERVICE', 'SYSTEM')) { Return $false } If ($LeafUserName -match '^(DWM|UMFD)-\d+$') { Return $false } Return $true } Function Format-UserName { Param( [AllowNull()] [string]$DomainName, [Parameter(Mandatory=$true)] [string]$UserName ) If ([string]::IsNullOrWhiteSpace($DomainName) -or $DomainName -eq '-') { Return $UserName } Return "${DomainName}\${UserName}" } Function Resolve-UserLogonActivitySid { Param( [AllowNull()] [object]$Sid ) If ($null -eq $Sid) { Return $null } $SidString = ([string]$Sid).Trim() If ([string]::IsNullOrWhiteSpace($SidString) -or $SidString -eq '-') { Return $null } Try { Return (New-Object Security.Principal.SecurityIdentifier($SidString)).Translate([Security.Principal.NTAccount]).Value } Catch { $ProfilePath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\${SidString}" -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath If (-not [string]::IsNullOrWhiteSpace($ProfilePath)) { Return (Split-Path -Path $ProfilePath -Leaf) } Return $SidString } } Function Get-UserLogonActivityKey { Param( [AllowNull()] [object]$Sid, [AllowNull()] [string]$User ) $SidString = ([string]$Sid).Trim() If (-not [string]::IsNullOrWhiteSpace($SidString) -and $SidString -ne '-') { Return $SidString.ToUpperInvariant() } $UserString = ([string]$User).Trim() If (-not [string]::IsNullOrWhiteSpace($UserString)) { Return $UserString.ToUpperInvariant() } Return '' } Function Get-UserLogonActivitySidFromEvent { Param( [Parameter(Mandatory=$true)] [object]$Event ) $EventXml = [xml]$Event.ToXml() $UserSid = Get-EventDataValueFromList -EventXml $EventXml -Names @('UserSid', 'TargetUserSid', 'SubjectUserSid') If (-not [string]::IsNullOrWhiteSpace($UserSid)) { Return $UserSid } ForEach ($Property in $Event.Properties) { $PropertyValue = [string]$Property.Value If ($PropertyValue -match '^S-\d-\d+-.+') { Return $PropertyValue } } Return [string]$EventXml.Event.System.Security.UserID } Function New-UserLogonActivityRecord { Param( [Parameter(Mandatory=$true)] [datetime]$TimeCreated, [Parameter(Mandatory=$true)] [string]$User, [Parameter(Mandatory=$true)] [ValidateSet('logon', 'logoff', 'unlock', 'lock', 'remote logon', 'session disconnect', 'session reconnect', 'screensaver start', 'screensaver dismiss', 'sleep', 'wake', 'shutdown', 'restart', 'power on')] [string]$Activity, [Parameter(Mandatory=$true)] [string]$Source, [string]$Details = '', [string]$UserKey = '', [int]$SourcePriority = 50, [switch]$AllowSystemUser ) If (-not $AllowSystemUser -and -not (Test-UserLogonActivityUser -UserName $User)) { Return $null } $ResolvedUserKey = $UserKey If ([string]::IsNullOrWhiteSpace($ResolvedUserKey)) { $ResolvedUserKey = Get-UserLogonActivityKey -Sid $null -User $User } [PSCustomObject]@{ Date = $TimeCreated.ToString('yyyy-MM-dd') Time = $TimeCreated.ToString('HH:mm:ss') User = $User Activity = $Activity Source = $Source Details = $Details UserKey = $ResolvedUserKey SourcePriority = $SourcePriority TimeCreated = $TimeCreated } } Function ConvertTo-WinlogonUserActivity { Param( [Parameter(Mandatory=$true)] [object]$Event ) Switch ($Event.Id) { 7001 { $Activity = 'logon'; $Source = 'Winlogon 7001' } 7002 { $Activity = 'logoff'; $Source = 'Winlogon 7002' } Default { Return $null } } $UserSid = Get-UserLogonActivitySidFromEvent -Event $Event $User = Resolve-UserLogonActivitySid -Sid $UserSid If ([string]::IsNullOrWhiteSpace($User)) { Return $null } New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity -Source $Source -UserKey (Get-UserLogonActivityKey -Sid $UserSid -User $User) -SourcePriority 10 } Function ConvertTo-SecurityUserActivity { Param( [Parameter(Mandatory=$true)] [object]$Event ) $EventXml = [xml]$Event.ToXml() $UserName = Get-EventDataValueFromList -EventXml $EventXml -Names @('TargetUserName', 'SubjectUserName') $DomainName = Get-EventDataValueFromList -EventXml $EventXml -Names @('TargetDomainName', 'SubjectDomainName') $UserSid = Get-EventDataValueFromList -EventXml $EventXml -Names @('TargetUserSid', 'SubjectUserSid') $LogonType = Get-EventDataValue -EventXml $EventXml -Name 'LogonType' $Activity = $null $Source = $null $SourcePriority = 20 $Details = '' Switch ($Event.Id) { 4624 { Switch ($LogonType) { '7' { $Activity = 'unlock' $Source = 'Security 4624 type 7' $SourcePriority = 30 } '10' { $Activity = 'remote logon' $Source = 'Security 4624 type 10' $SourcePriority = 20 $IpAddress = Get-EventDataValue -EventXml $EventXml -Name 'IpAddress' $WorkstationName = Get-EventDataValue -EventXml $EventXml -Name 'WorkstationName' $DetailsParts = @() If (-not [string]::IsNullOrWhiteSpace($IpAddress) -and $IpAddress -ne '-') { $DetailsParts += "Address=${IpAddress}" } If (-not [string]::IsNullOrWhiteSpace($WorkstationName) -and $WorkstationName -ne '-') { $DetailsParts += "Workstation=${WorkstationName}" } $Details = $DetailsParts -join '; ' } Default { Return $null } } } 4800 { $Activity = 'lock' $Source = 'Security 4800' } 4801 { $Activity = 'unlock' $Source = 'Security 4801' } 4802 { $Activity = 'screensaver start' $Source = 'Security 4802' } 4803 { $Activity = 'screensaver dismiss' $Source = 'Security 4803' } Default { Return $null } } $User = Format-UserName -DomainName $DomainName -UserName $UserName New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity -Source $Source -Details $Details -UserKey (Get-UserLogonActivityKey -Sid $UserSid -User $User) -SourcePriority $SourcePriority } Function ConvertTo-TerminalServicesUserActivity { Param( [Parameter(Mandatory=$true)] [object]$Event ) $EventXml = [xml]$Event.ToXml() $User = Get-UserDataEventXmlValue -EventXml $EventXml -Name 'User' $SessionID = Get-UserDataEventXmlValue -EventXml $EventXml -Name 'SessionID' $Address = Get-UserDataEventXmlValue -EventXml $EventXml -Name 'Address' $DetailsParts = @() If ([string]::IsNullOrWhiteSpace($User)) { Return $null } If (-not [string]::IsNullOrWhiteSpace($SessionID)) { $DetailsParts += "Session=${SessionID}" } If (-not [string]::IsNullOrWhiteSpace($Address) -and $Address -ne '-') { $DetailsParts += "Address=${Address}" } Switch ($Event.Id) { 24 { $Activity = 'session disconnect' $Source = 'TermSvc 24' } 25 { $Activity = 'session reconnect' $Source = 'TermSvc 25' } Default { Return $null } } New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity -Source $Source -Details ($DetailsParts -join '; ') -UserKey (Get-UserLogonActivityKey -Sid $null -User $User) -SourcePriority 20 } Function ConvertTo-PowerUserActivity { Param( [Parameter(Mandatory=$true)] [object]$Event ) $EventXml = [xml]$Event.ToXml() $ProviderName = [string]$Event.ProviderName $User = 'SYSTEM' $Activity = $null If ($ProviderName -ieq 'Microsoft-Windows-Kernel-Power' -and $Event.Id -eq 42) { $Activity = 'sleep' $Source = 'Kernel-Power 42' } ElseIf ($ProviderName -ieq 'Microsoft-Windows-Power-Troubleshooter' -and $Event.Id -eq 1) { $Activity = 'wake' $Source = 'Power-Troubleshooter 1' } ElseIf ($ProviderName -ieq 'EventLog' -and $Event.Id -eq 6005) { $Activity = 'power on' $Source = 'EventLog 6005' } ElseIf ($ProviderName -ieq 'EventLog' -and $Event.Id -eq 6006) { $Activity = 'shutdown' $Source = 'EventLog 6006' } ElseIf ($ProviderName -ieq 'User32' -and $Event.Id -eq 1074) { $ShutdownType = Get-EventDataValue -EventXml $EventXml -Name 'param5' $EventUser = Get-EventDataValue -EventXml $EventXml -Name 'param7' If (-not [string]::IsNullOrWhiteSpace($EventUser) -and $EventUser -ne '-') { $User = $EventUser } If ($ShutdownType -match 'restart') { $Activity = 'restart' } Else { $Activity = 'shutdown' } $Source = 'User32 1074' } Else { Return $null } New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity -Source $Source -SourcePriority 10 -AllowSystemUser } Function Get-UserLogonActivityWinEvent { Param( [Parameter(Mandatory=$true)] [hashtable]$FilterHashtable, [int]$MaxEvents = 0, [Parameter(Mandatory=$true)] [string]$LogDescription ) $Parameters = @{ FilterHashtable = $FilterHashtable ErrorAction = 'Stop' } If ($MaxEvents -gt 0) { $Parameters.MaxEvents = $MaxEvents } Try { Return @(Get-WinEvent @Parameters) } Catch { If ($_.FullyQualifiedErrorId -like '*NoMatchingEventsFound*' -or $_.Exception.Message -like '*No events were found*') { Return @() } Write-Error "Unable to read ${LogDescription}. Run PowerShell as administrator if access is denied. $($_.Exception.Message)" Return @() } } Function Add-UserLogonActivityRecord { Param( [Parameter(Mandatory=$true)] [object]$ActivityEvents, [AllowNull()] [object]$ActivityEvent ) If ($null -ne $ActivityEvent) { $ActivityEvents.Add($ActivityEvent) | Out-Null } } Function Get-UniqueUserLogonActivity { Param( [AllowNull()] [object[]]$ActivityEvents ) $UniqueEvents = New-Object System.Collections.Generic.List[object] ForEach ($ActivityEvent in (@($ActivityEvents) | Where-Object { $null -ne $_ } | Sort-Object -Property TimeCreated,User,Activity,SourcePriority)) { $IsDuplicate = $false $ActivityEventKey = [string]$ActivityEvent.UserKey If ([string]::IsNullOrWhiteSpace($ActivityEventKey)) { $ActivityEventKey = Get-UserLogonActivityKey -Sid $null -User $ActivityEvent.User } For ($Index = 0; $Index -lt $UniqueEvents.Count; $Index++) { $UniqueEvent = $UniqueEvents[$Index] $UniqueEventKey = [string]$UniqueEvent.UserKey If ([string]::IsNullOrWhiteSpace($UniqueEventKey)) { $UniqueEventKey = Get-UserLogonActivityKey -Sid $null -User $UniqueEvent.User } If ( $UniqueEventKey -eq $ActivityEventKey -and $UniqueEvent.Activity -eq $ActivityEvent.Activity -and [Math]::Abs(($UniqueEvent.TimeCreated - $ActivityEvent.TimeCreated).TotalSeconds) -le 5 ) { If ($ActivityEvent.SourcePriority -lt $UniqueEvent.SourcePriority) { $UniqueEvents[$Index] = $ActivityEvent } $IsDuplicate = $true Break } } If (-not $IsDuplicate) { $UniqueEvents.Add($ActivityEvent) | Out-Null } } Return $UniqueEvents } Function Write-PlainTextTable { Param( [AllowNull()] [object[]]$Rows, [Parameter(Mandatory=$true)] [string[]]$Columns ) $TableRows = @($Rows | Where-Object { $null -ne $_ }) If ($TableRows.Count -eq 0) { Return } $Widths = @{} ForEach ($Column in $Columns) { $ColumnWidth = $Column.Length ForEach ($Row in $TableRows) { $Property = $Row.PSObject.Properties[$Column] $Value = '' If ($null -ne $Property -and $null -ne $Property.Value) { $Value = [string]$Property.Value } If ($Value.Length -gt $ColumnWidth) { $ColumnWidth = $Value.Length } } $Widths[$Column] = $ColumnWidth } $HeaderParts = @() $SeparatorParts = @() ForEach ($Column in $Columns) { $HeaderParts += $Column.PadRight($Widths[$Column]) $SeparatorParts += ('-' * $Widths[$Column]) } Write-Output ($HeaderParts -join ' ') Write-Output ($SeparatorParts -join ' ') ForEach ($Row in $TableRows) { $LineParts = @() ForEach ($Column in $Columns) { $Property = $Row.PSObject.Properties[$Column] $Value = '' If ($null -ne $Property -and $null -ne $Property.Value) { $Value = [string]$Property.Value } $LineParts += $Value.PadRight($Widths[$Column]) } Write-Output (($LineParts -join ' ').TrimEnd()) } } Function Write-UserLogonActivityOutput { Param( [AllowNull()] [object[]]$ActivityEvents, [Parameter(Mandatory=$true)] [DateTimeOffset]$StartDateTime, [Parameter(Mandatory=$true)] [DateTimeOffset]$EndDateTime ) Write-Output "Search window: $($StartDateTime.LocalDateTime.ToString('yyyy-MM-dd HH:mm:ss')) through $($EndDateTime.LocalDateTime.ToString('yyyy-MM-dd HH:mm:ss'))" $Rows = @($ActivityEvents | Select-Object Date,Time,User,Activity,Source,Details) If ($Rows.Count -eq 0) { Write-Output 'No matching user logon activity events found.' Return } Write-PlainTextTable -Rows $Rows -Columns @('Date', 'Time', 'User', 'Activity', 'Source', 'Details') } Function Get-LocalUnlockedWindowAction { Param( [AllowNull()] [string]$Activity ) Switch ($Activity) { 'session reconnect' { Return 'RDP reconnect' } 'session disconnect' { Return 'session disconnect' } 'screensaver dismiss' { Return 'screensaver dismiss' } 'screensaver start' { Return 'screensaver start' } Default { Return $Activity } } } Function Format-LocalUnlockedDuration { Param( [Parameter(Mandatory=$true)] [datetime]$StartTime, [Parameter(Mandatory=$true)] [datetime]$EndTime, [switch]$Maximum ) $Duration = $EndTime - $StartTime If ($Duration.TotalSeconds -lt 0) { $Duration = [TimeSpan]::Zero } If ($Duration.Days -gt 0) { $Text = '{0}d {1}h {2}m' -f $Duration.Days, $Duration.Hours, $Duration.Minutes } ElseIf ($Duration.Hours -gt 0) { $Text = '{0}h {1}m' -f $Duration.Hours, $Duration.Minutes } ElseIf ($Duration.Minutes -gt 0) { $Text = '{0}m {1}s' -f $Duration.Minutes, $Duration.Seconds } Else { $Text = '{0}s' -f $Duration.Seconds } If ($Maximum) { Return "up to ${Text}" } Return $Text } Function Test-RedundantOpeningUnlock { Param( [Parameter(Mandatory=$true)] [object]$ActivityEvent, [AllowNull()] [object[]]$ActivityEvents ) If ($ActivityEvent.Activity -ne 'unlock' -or $ActivityEvent.Source -ne 'Security 4624 type 7') { Return $false } ForEach ($CandidateEvent in @($ActivityEvents)) { If ( ( $CandidateEvent.Activity -eq 'logon' -or (Test-RemoteSessionReconnect -ActivityEvent $CandidateEvent) ) -and [Math]::Abs(($CandidateEvent.TimeCreated - $ActivityEvent.TimeCreated).TotalSeconds) -le 5 ) { Return $true } } Return $false } Function Get-ActivityEventDetailValue { Param( [Parameter(Mandatory=$true)] [object]$ActivityEvent, [Parameter(Mandatory=$true)] [string]$Name ) $Details = [string]$ActivityEvent.Details If ([string]::IsNullOrWhiteSpace($Details)) { Return $null } ForEach ($Part in ($Details -split ';')) { $TrimmedPart = $Part.Trim() If ($TrimmedPart -match "^$([regex]::Escape($Name))=(.*)$") { Return $Matches[1].Trim() } } Return $null } Function Test-RemoteSessionReconnect { Param( [Parameter(Mandatory=$true)] [object]$ActivityEvent ) If ($ActivityEvent.Activity -ne 'session reconnect') { Return $false } $Address = Get-ActivityEventDetailValue -ActivityEvent $ActivityEvent -Name 'Address' If ([string]::IsNullOrWhiteSpace($Address)) { Return $false } If ($Address -eq '-' -or $Address -ieq 'LOCAL') { Return $false } Return $true } Function Get-LocalUnlockedWindow { Param( [AllowNull()] [object[]]$ActivityEvents ) $OpenWindow = $null $UnlockedWindows = New-Object System.Collections.Generic.List[object] $OpeningActivities = @('logon', 'unlock', 'screensaver dismiss') $ClosingActivities = @('logoff', 'lock', 'screensaver start', 'session disconnect', 'session reconnect', 'remote logon', 'sleep', 'shutdown', 'restart') $RepeatedOpeningClosureDescriptions = @{ 'unlock' = 'lock time unknown' 'screensaver dismiss' = 'screensaver start unknown' } $OrderedActivityEvents = @($ActivityEvents | Where-Object { $null -ne $_ } | Sort-Object -Property TimeCreated) ForEach ($ActivityEvent in $OrderedActivityEvents) { $User = [string]$ActivityEvent.User $Activity = [string]$ActivityEvent.Activity $Source = [string]$ActivityEvent.Source If (Test-RedundantOpeningUnlock -ActivityEvent $ActivityEvent -ActivityEvents $OrderedActivityEvents) { Continue } If ($Activity -eq 'session reconnect' -and -not (Test-RemoteSessionReconnect -ActivityEvent $ActivityEvent)) { Continue } If ($Activity -in $ClosingActivities) { If ($null -ne $OpenWindow) { $Note = '' If ($Activity -eq 'session reconnect' -or $Activity -eq 'remote logon') { $Note = 'local console unavailable during remote session' } $UnlockedWindows.Add([PSCustomObject]@{ StartTime = $OpenWindow.StartTime Start = $OpenWindow.StartTime.ToString('yyyy-MM-dd HH:mm:ss') End = $ActivityEvent.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') Duration = Format-LocalUnlockedDuration -StartTime $OpenWindow.StartTime -EndTime $ActivityEvent.TimeCreated User = $OpenWindow.User Opened = $OpenWindow.Opened Closed = Get-LocalUnlockedWindowAction -Activity $Activity Note = $Note }) | Out-Null $OpenWindow = $null } Continue } If ([string]::IsNullOrWhiteSpace($User) -or $User -eq 'SYSTEM' -or $User -eq 'NT AUTHORITY\SYSTEM') { Continue } If ($Activity -in $OpeningActivities) { If ($null -ne $OpenWindow -and $RepeatedOpeningClosureDescriptions.ContainsKey($Activity)) { $UnlockedWindows.Add([PSCustomObject]@{ StartTime = $OpenWindow.StartTime Start = $OpenWindow.StartTime.ToString('yyyy-MM-dd HH:mm:ss') End = "before $($ActivityEvent.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))" Duration = Format-LocalUnlockedDuration -StartTime $OpenWindow.StartTime -EndTime $ActivityEvent.TimeCreated -Maximum User = $OpenWindow.User Opened = $OpenWindow.Opened Closed = $RepeatedOpeningClosureDescriptions[$Activity] Note = 'latest possible duration' }) | Out-Null $OpenWindow = $null } If ($null -eq $OpenWindow) { $OpenWindow = [PSCustomObject]@{ StartTime = $ActivityEvent.TimeCreated User = $User Opened = Get-LocalUnlockedWindowAction -Activity $Activity } } } } If ($null -ne $OpenWindow) { $UnlockedWindows.Add([PSCustomObject]@{ StartTime = $OpenWindow.StartTime Start = $OpenWindow.StartTime.ToString('yyyy-MM-dd HH:mm:ss') End = 'open through returned events' Duration = 'open' User = $OpenWindow.User Opened = $OpenWindow.Opened Closed = 'not returned' Note = 'no closing event returned' }) | Out-Null } Return $UnlockedWindows } Function Write-LocalUnlockedWindowOutput { Param( [AllowNull()] [object[]]$ActivityEvents ) $UnlockedWindows = @(Get-LocalUnlockedWindow -ActivityEvents $ActivityEvents | Sort-Object -Property StartTime) If ($UnlockedWindows.Count -eq 0) { Write-Output 'Local unlocked windows inferred from returned events: none' Return } Write-Output 'Local unlocked windows inferred from returned events:' ForEach ($UnlockedWindow in $UnlockedWindows) { $Start = [string]$UnlockedWindow.Start $End = [string]$UnlockedWindow.End $Duration = [string]$UnlockedWindow.Duration $Closed = [string]$UnlockedWindow.Closed If ($End -eq 'open through returned events') { Write-Output "Unlocked since ${Start} (still open through returned events)" } ElseIf ($End.StartsWith('before ')) { $EndTime = $End.Substring(7) Write-Output "Unlocked for ${Duration} (from ${Start} to before ${EndTime}; ${Closed})" } Else { Write-Output "Unlocked for ${Duration} (from ${Start} to ${End})" } } } # Winlogon 7001/7002 tracks user session logon/logoff more reliably than Security 4624/4634. $WinlogonFilter = @{ LogName = 'System' ProviderName = 'Microsoft-Windows-Winlogon' Id = @(7001, 7002) } $SecurityFilter = @{ LogName = 'Security' Id = @(4800, 4801, 4802, 4803) } # Security 4624 type 7 is unlock authentication evidence and type 10 is RDP evidence. # Query 4624 separately so it cannot crowd out 4800/4801 workstation lock/unlock events. $UnlockAuthFilter = @{ LogName = 'Security' Id = 4624 } $TerminalServicesFilter = @{ LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational' Id = @(24, 25) } $PowerFilters = @( @{ LogName = 'System' ProviderName = 'Microsoft-Windows-Kernel-Power' Id = 42 }, @{ LogName = 'System' ProviderName = 'Microsoft-Windows-Power-Troubleshooter' Id = 1 }, @{ LogName = 'System' ProviderName = 'EventLog' Id = @(6005, 6006) }, @{ LogName = 'System' ProviderName = 'User32' Id = 1074 } ) Try { If ($StartHasValue) { $StartDateTime = ConvertTo-ActivityDateTime -Value $Start -ParameterName 'Start' } If ($EndHasValue) { $EndDateTime = ConvertTo-ActivityDateTime -Value $End -ParameterName 'End' } If (-not $EndHasValue) { $EndDateTime = [DateTimeOffset]::Now } If (-not $StartHasValue) { $StartDateTime = $EndDateTime.AddDays(-7) } } Catch { Write-Error (Get-UserLogonActivityErrorMessage -ErrorRecord $_ -FallbackMessage 'Unable to resolve the activity date range.') Exit 1 } If ($EndDateTime -le $StartDateTime) { Write-Error "-End must be later than -Start." Exit 1 } $WinlogonFilter.StartTime = $StartDateTime.LocalDateTime $WinlogonFilter.EndTime = $EndDateTime.LocalDateTime $SecurityFilter.StartTime = $StartDateTime.LocalDateTime $SecurityFilter.EndTime = $EndDateTime.LocalDateTime $UnlockAuthFilter.StartTime = $StartDateTime.LocalDateTime $UnlockAuthFilter.EndTime = $EndDateTime.LocalDateTime $TerminalServicesFilter.StartTime = $StartDateTime.LocalDateTime $TerminalServicesFilter.EndTime = $EndDateTime.LocalDateTime ForEach ($PowerFilter in $PowerFilters) { $PowerFilter.StartTime = $StartDateTime.LocalDateTime $PowerFilter.EndTime = $EndDateTime.LocalDateTime } $ActivityEvents = New-Object System.Collections.Generic.List[object] ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $WinlogonFilter -LogDescription 'System Winlogon event log')) { Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-WinlogonUserActivity -Event $Event) } ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $SecurityFilter -LogDescription 'Security event log')) { Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event) } ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $UnlockAuthFilter -LogDescription 'Security unlock-authentication event log')) { Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event) } ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $TerminalServicesFilter -LogDescription 'Terminal Services Local Session Manager event log')) { Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-TerminalServicesUserActivity -Event $Event) } ForEach ($PowerFilter in $PowerFilters) { ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $PowerFilter -LogDescription 'System power event log')) { Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-PowerUserActivity -Event $Event) } } $OutputEvents = @(Get-UniqueUserLogonActivity -ActivityEvents $ActivityEvents | Sort-Object -Property TimeCreated) Write-UserLogonActivityOutput -ActivityEvents $OutputEvents -StartDateTime $StartDateTime -EndDateTime $EndDateTime Try { Write-LocalUnlockedWindowOutput -ActivityEvents $OutputEvents } Catch { $LocalUnlockedWindowError = Get-UserLogonActivityErrorMessage -ErrorRecord $_ -FallbackMessage 'Unable to infer local unlocked windows.' Write-Output "Local unlocked windows inferred from returned events: unavailable. ${LocalUnlockedWindowError}" }