2026-07-07 14:45:18 -04:00
# Requires -Version 5.1
<#
. SYNOPSIS
2026-07-07 15:39:57 -04:00
Gets user logon, logoff, lock, unlock, and power activity from Windows event logs.
2026-07-07 14:45:18 -04:00
. EXAMPLE
$Start = '2026-07-03T00:00:00Z'
$End = '2026-07-06T00:00:00Z'
.\Get-UserLogonActivity.ps1
2026-07-07 14:56:56 -04:00
. EXAMPLE
.\Get-UserLogonActivity.ps1
2026-07-08 11:36:27 -04:00
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.
2026-07-07 14:45:18 -04:00
#>
2026-07-07 14:56:56 -04:00
$StartVariable = Get-Variable -Name Start -ErrorAction SilentlyContinue
$EndVariable = Get-Variable -Name End -ErrorAction SilentlyContinue
2026-07-07 14:45:18 -04:00
2026-07-08 11:36:27 -04:00
Function Test-ActivityDateRangeValue {
Param (
[ AllowNull ()]
[ object ] $Value
)
If ( $null -eq $Value ) { Return $false }
If ( $Value -is [ string ]) { Return -not [ string ]:: IsNullOrWhiteSpace ( $Value ) }
Return $true
}
2026-07-08 11:56:10 -04:00
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
}
2026-07-08 11:36:27 -04:00
$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.'
2026-07-07 14:45:18 -04:00
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'
}
2026-07-07 15:06:27 -04:00
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
}
2026-07-08 11:04:22 -04:00
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'
}
2026-07-07 14:45:18 -04:00
Function Test-UserLogonActivityUser {
Param (
[ AllowNull ()]
[ string ] $UserName
)
If ([ string ]:: IsNullOrWhiteSpace ( $UserName )) { Return $false }
2026-07-07 15:23:15 -04:00
$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 }
2026-07-07 14:45:18 -04:00
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} "
}
2026-07-07 15:23:15 -04:00
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 }
2026-07-07 15:06:27 -04:00
Try {
2026-07-07 15:23:15 -04:00
Return ( New-Object Security . Principal . SecurityIdentifier ( $SidString )). Translate ([ Security.Principal.NTAccount ]). Value
2026-07-07 15:06:27 -04:00
}
Catch {
2026-07-07 15:23:15 -04:00
$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
}
}
2026-07-08 11:27:51 -04:00
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 ''
}
2026-07-07 15:23:15 -04:00
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 )]
2026-07-08 11:04:22 -04:00
[ ValidateSet ( 'logon' , 'logoff' , 'unlock' , 'lock' , 'remote logon' , 'session disconnect' , 'session reconnect' , 'screensaver start' , 'screensaver dismiss' , 'sleep' , 'wake' , 'shutdown' , 'restart' , 'power on' )]
2026-07-07 15:39:57 -04:00
[ string ] $Activity ,
2026-07-07 17:30:11 -04:00
[ Parameter ( Mandatory = $true )]
[ string ] $Source ,
2026-07-08 11:04:22 -04:00
[ string ] $Details = '' ,
2026-07-08 11:27:51 -04:00
[ string ] $UserKey = '' ,
2026-07-07 17:30:11 -04:00
[ int ] $SourcePriority = 50 ,
2026-07-07 15:39:57 -04:00
[ switch ] $AllowSystemUser
2026-07-07 15:23:15 -04:00
)
2026-07-07 15:39:57 -04:00
If ( -not $AllowSystemUser -and -not ( Test-UserLogonActivityUser -UserName $User )) { Return $null }
2026-07-07 15:23:15 -04:00
2026-07-08 11:27:51 -04:00
$ResolvedUserKey = $UserKey
If ([ string ]:: IsNullOrWhiteSpace ( $ResolvedUserKey )) {
$ResolvedUserKey = Get-UserLogonActivityKey -Sid $null -User $User
}
2026-07-07 15:23:15 -04:00
[ PSCustomObject ] @ {
Date = $TimeCreated . ToString ( 'yyyy-MM-dd' )
Time = $TimeCreated . ToString ( 'HH:mm:ss' )
User = $User
Activity = $Activity
2026-07-07 17:30:11 -04:00
Source = $Source
2026-07-08 11:04:22 -04:00
Details = $Details
2026-07-08 11:27:51 -04:00
UserKey = $ResolvedUserKey
2026-07-07 17:30:11 -04:00
SourcePriority = $SourcePriority
2026-07-07 15:23:15 -04:00
TimeCreated = $TimeCreated
}
}
Function ConvertTo-WinlogonUserActivity {
Param (
[ Parameter ( Mandatory = $true )]
[ object ] $Event
)
Switch ( $Event . Id ) {
2026-07-07 17:30:11 -04:00
7001 { $Activity = 'logon' ; $Source = 'Winlogon 7001' }
7002 { $Activity = 'logoff' ; $Source = 'Winlogon 7002' }
2026-07-07 15:23:15 -04:00
Default { Return $null }
2026-07-07 15:06:27 -04:00
}
2026-07-07 15:23:15 -04:00
2026-07-08 11:27:51 -04:00
$UserSid = Get-UserLogonActivitySidFromEvent -Event $Event
$User = Resolve-UserLogonActivitySid -Sid $UserSid
2026-07-07 15:23:15 -04:00
If ([ string ]:: IsNullOrWhiteSpace ( $User )) { Return $null }
2026-07-08 11:27:51 -04:00
New-UserLogonActivityRecord -TimeCreated $Event . TimeCreated -User $User -Activity $Activity -Source $Source -UserKey ( Get-UserLogonActivityKey -Sid $UserSid -User $User ) -SourcePriority 10
2026-07-07 15:06:27 -04:00
}
2026-07-07 15:23:15 -04:00
Function ConvertTo-SecurityUserActivity {
2026-07-07 14:45:18 -04:00
Param (
[ Parameter ( Mandatory = $true )]
[ object ] $Event
)
$EventXml = [ xml ] $Event . ToXml ()
2026-07-07 15:06:27 -04:00
$UserName = Get-EventDataValueFromList -EventXml $EventXml -Names @ ( 'TargetUserName' , 'SubjectUserName' )
$DomainName = Get-EventDataValueFromList -EventXml $EventXml -Names @ ( 'TargetDomainName' , 'SubjectDomainName' )
2026-07-08 11:27:51 -04:00
$UserSid = Get-EventDataValueFromList -EventXml $EventXml -Names @ ( 'TargetUserSid' , 'SubjectUserSid' )
2026-07-07 17:30:11 -04:00
$LogonType = Get-EventDataValue -EventXml $EventXml -Name 'LogonType'
2026-07-07 14:45:18 -04:00
$Activity = $null
2026-07-07 17:30:11 -04:00
$Source = $null
$SourcePriority = 20
2026-07-08 11:04:22 -04:00
$Details = ''
2026-07-07 14:45:18 -04:00
Switch ( $Event . Id ) {
2026-07-07 17:30:11 -04:00
4624 {
2026-07-08 11:04:22 -04:00
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
}
}
2026-07-07 17:30:11 -04:00
}
2026-07-07 14:45:18 -04:00
4800 {
$Activity = 'lock'
2026-07-07 17:30:11 -04:00
$Source = 'Security 4800'
2026-07-07 14:45:18 -04:00
}
4801 {
$Activity = 'unlock'
2026-07-07 17:30:11 -04:00
$Source = 'Security 4801'
2026-07-07 14:45:18 -04:00
}
2026-07-08 11:04:22 -04:00
4802 {
$Activity = 'screensaver start'
$Source = 'Security 4802'
}
4803 {
$Activity = 'screensaver dismiss'
$Source = 'Security 4803'
}
2026-07-07 14:45:18 -04:00
Default {
Return $null
}
}
2026-07-08 11:27:51 -04:00
$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
2026-07-08 11:04:22 -04:00
}
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
}
}
2026-07-08 11:27:51 -04:00
New-UserLogonActivityRecord -TimeCreated $Event . TimeCreated -User $User -Activity $Activity -Source $Source -Details ( $DetailsParts -join '; ' ) -UserKey ( Get-UserLogonActivityKey -Sid $null -User $User ) -SourcePriority 20
2026-07-07 15:23:15 -04:00
}
2026-07-07 15:39:57 -04:00
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'
2026-07-07 17:30:11 -04:00
$Source = 'Kernel-Power 42'
2026-07-07 15:39:57 -04:00
}
ElseIf ( $ProviderName -ieq 'Microsoft-Windows-Power-Troubleshooter' -and $Event . Id -eq 1 ) {
$Activity = 'wake'
2026-07-07 17:30:11 -04:00
$Source = 'Power-Troubleshooter 1'
2026-07-07 15:39:57 -04:00
}
ElseIf ( $ProviderName -ieq 'EventLog' -and $Event . Id -eq 6005 ) {
$Activity = 'power on'
2026-07-07 17:30:11 -04:00
$Source = 'EventLog 6005'
2026-07-07 15:39:57 -04:00
}
ElseIf ( $ProviderName -ieq 'EventLog' -and $Event . Id -eq 6006 ) {
$Activity = 'shutdown'
2026-07-07 17:30:11 -04:00
$Source = 'EventLog 6006'
2026-07-07 15:39:57 -04:00
}
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'
}
2026-07-07 17:30:11 -04:00
$Source = 'User32 1074'
2026-07-07 15:39:57 -04:00
}
Else {
Return $null
}
2026-07-07 17:30:11 -04:00
New-UserLogonActivityRecord -TimeCreated $Event . TimeCreated -User $User -Activity $Activity -Source $Source -SourcePriority 10 -AllowSystemUser
2026-07-07 15:39:57 -04:00
}
2026-07-07 15:23:15 -04:00
Function Get-UserLogonActivityWinEvent {
Param (
[ Parameter ( Mandatory = $true )]
[ hashtable ] $FilterHashtable ,
2026-07-07 14:45:18 -04:00
2026-07-07 15:23:15 -04:00
[ int ] $MaxEvents = 0 ,
2026-07-07 14:45:18 -04:00
2026-07-07 15:23:15 -04:00
[ Parameter ( Mandatory = $true )]
[ string ] $LogDescription
)
$Parameters = @ {
Filter Hashtable = $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 @ ()
2026-07-07 14:45:18 -04:00
}
}
2026-07-07 15:23:15 -04:00
Function Add-UserLogonActivityRecord {
Param (
[ Parameter ( Mandatory = $true )]
[ object ] $ActivityEvents ,
[ AllowNull ()]
[ object ] $ActivityEvent
)
If ( $null -ne $ActivityEvent ) {
$ActivityEvents . Add ( $ActivityEvent ) | Out-Null
}
2026-07-07 15:06:27 -04:00
}
2026-07-07 15:23:15 -04:00
Function Get-UniqueUserLogonActivity {
Param (
[ AllowNull ()]
[ object[] ] $ActivityEvents
)
$UniqueEvents = New-Object System . Collections . Generic . List [ object ]
2026-07-07 17:30:11 -04:00
ForEach ( $ActivityEvent in ( @ ( $ActivityEvents ) | Where-Object { $null -ne $_ } | Sort-Object -Property TimeCreated , User , Activity , SourcePriority )) {
2026-07-07 15:23:15 -04:00
$IsDuplicate = $false
2026-07-08 11:27:51 -04:00
$ActivityEventKey = [ string ] $ActivityEvent . UserKey
If ([ string ]:: IsNullOrWhiteSpace ( $ActivityEventKey )) {
$ActivityEventKey = Get-UserLogonActivityKey -Sid $null -User $ActivityEvent . User
}
2026-07-07 15:23:15 -04:00
2026-07-07 17:30:11 -04:00
For ( $Index = 0 ; $Index -lt $UniqueEvents . Count ; $Index ++) {
$UniqueEvent = $UniqueEvents [ $Index ]
2026-07-08 11:27:51 -04:00
$UniqueEventKey = [ string ] $UniqueEvent . UserKey
If ([ string ]:: IsNullOrWhiteSpace ( $UniqueEventKey )) {
$UniqueEventKey = Get-UserLogonActivityKey -Sid $null -User $UniqueEvent . User
}
2026-07-07 17:30:11 -04:00
2026-07-07 15:23:15 -04:00
If (
2026-07-08 11:27:51 -04:00
$UniqueEventKey -eq $ActivityEventKey -and
2026-07-07 15:23:15 -04:00
$UniqueEvent . Activity -eq $ActivityEvent . Activity -and
[ Math ]:: Abs (( $UniqueEvent . TimeCreated - $ActivityEvent . TimeCreated ). TotalSeconds ) -le 5
) {
2026-07-07 17:30:11 -04:00
If ( $ActivityEvent . SourcePriority -lt $UniqueEvent . SourcePriority ) {
$UniqueEvents [ $Index ] = $ActivityEvent
}
2026-07-07 15:23:15 -04:00
$IsDuplicate = $true
Break
}
}
If ( -not $IsDuplicate ) {
$UniqueEvents . Add ( $ActivityEvent ) | Out-Null
}
}
Return $UniqueEvents
}
2026-07-08 12:13:48 -04:00
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 ())
}
}
2026-07-07 15:23:15 -04:00
Function Write-UserLogonActivityOutput {
Param (
[ AllowNull ()]
2026-07-08 12:00:25 -04:00
[ object[] ] $ActivityEvents ,
[ Parameter ( Mandatory = $true )]
[ DateTimeOffset ] $StartDateTime ,
[ Parameter ( Mandatory = $true )]
[ DateTimeOffset ] $EndDateTime
2026-07-07 15:23:15 -04:00
)
2026-07-08 12:00:25 -04:00
Write-Output "Search window: $( $StartDateTime . LocalDateTime . ToString ( 'yyyy-MM-dd HH:mm:ss' )) through $( $EndDateTime . LocalDateTime . ToString ( 'yyyy-MM-dd HH:mm:ss' )) "
2026-07-08 11:04:22 -04:00
$Rows = @ ( $ActivityEvents | Select-Object Date , Time , User , Activity , Source , Details )
2026-07-07 15:23:15 -04:00
If ( $Rows . Count -eq 0 ) {
Write-Output 'No matching user logon activity events found.'
Return
}
2026-07-08 12:13:48 -04:00
Write-PlainTextTable -Rows $Rows -Columns @ ( 'Date' , 'Time' , 'User' , 'Activity' , 'Source' , 'Details' )
2026-07-07 15:23:15 -04:00
}
2026-07-08 16:44:12 -04:00
Function Get-LocalUnlockedWindowAction {
2026-07-08 12:20:45 -04:00
Param (
[ AllowNull ()]
[ string ] $Activity
)
Switch ( $Activity ) {
2026-07-08 16:44:12 -04:00
'session reconnect' { Return 'RDP reconnect' }
'session disconnect' { Return 'session disconnect' }
2026-07-08 12:20:45 -04:00
'screensaver dismiss' { Return 'screensaver dismiss' }
'screensaver start' { Return 'screensaver start' }
Default { Return $Activity }
}
}
2026-07-08 16:44:12 -04:00
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
}
2026-07-08 12:20:45 -04:00
Function Test-RedundantOpeningUnlock {
2026-07-08 11:56:10 -04:00
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 (
2026-07-08 16:44:12 -04:00
(
$CandidateEvent . Activity -eq 'logon' -or
( Test-RemoteSessionReconnect -ActivityEvent $CandidateEvent )
) -and
2026-07-08 11:56:10 -04:00
[ Math ]:: Abs (( $CandidateEvent . TimeCreated - $ActivityEvent . TimeCreated ). TotalSeconds ) -le 5
) {
Return $true
}
}
Return $false
}
2026-07-08 16:44:12 -04:00
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 {
2026-07-08 11:27:51 -04:00
Param (
[ AllowNull ()]
[ object[] ] $ActivityEvents
)
2026-07-08 11:56:10 -04:00
$OpenWindow = $null
2026-07-08 16:44:12 -04:00
$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 = @ {
2026-07-08 12:20:45 -04:00
'unlock' = 'lock time unknown'
'screensaver dismiss' = 'screensaver start unknown'
2026-07-08 11:27:51 -04:00
}
2026-07-08 11:56:10 -04:00
$OrderedActivityEvents = @ ( $ActivityEvents | Where-Object { $null -ne $_ } | Sort-Object -Property TimeCreated )
ForEach ( $ActivityEvent in $OrderedActivityEvents ) {
2026-07-08 11:27:51 -04:00
$User = [ string ] $ActivityEvent . User
$Activity = [ string ] $ActivityEvent . Activity
$Source = [ string ] $ActivityEvent . Source
2026-07-08 12:20:45 -04:00
If ( Test-RedundantOpeningUnlock -ActivityEvent $ActivityEvent -ActivityEvents $OrderedActivityEvents ) {
2026-07-08 11:56:10 -04:00
Continue
2026-07-08 11:27:51 -04:00
}
2026-07-08 16:44:12 -04:00
If ( $Activity -eq 'session reconnect' -and -not ( Test-RemoteSessionReconnect -ActivityEvent $ActivityEvent )) {
Continue
}
If ( $Activity -in $ClosingActivities ) {
2026-07-08 11:56:10 -04:00
If ( $null -ne $OpenWindow ) {
2026-07-08 16:44:12 -04:00
$Note = ''
If ( $Activity -eq 'session reconnect' -or $Activity -eq 'remote logon' ) {
$Note = 'local console unavailable during remote session'
}
$UnlockedWindows . Add ([ PSCustomObject ] @ {
2026-07-08 11:27:51 -04:00
StartTime = $OpenWindow . StartTime
Start = $OpenWindow . StartTime . ToString ( 'yyyy-MM-dd HH:mm:ss' )
End = $ActivityEvent . TimeCreated . ToString ( 'yyyy-MM-dd HH:mm:ss' )
2026-07-08 16:44:12 -04:00
Duration = Format-LocalUnlockedDuration -StartTime $OpenWindow . StartTime -EndTime $ActivityEvent . TimeCreated
2026-07-08 11:27:51 -04:00
User = $OpenWindow . User
2026-07-08 12:20:45 -04:00
Opened = $OpenWindow . Opened
2026-07-08 16:44:12 -04:00
Closed = Get-LocalUnlockedWindowAction -Activity $Activity
Note = $Note
2026-07-08 11:27:51 -04:00
}) | Out-Null
2026-07-08 11:56:10 -04:00
$OpenWindow = $null
2026-07-08 11:27:51 -04:00
}
Continue
}
If ([ string ]:: IsNullOrWhiteSpace ( $User ) -or $User -eq 'SYSTEM' -or $User -eq 'NT AUTHORITY\SYSTEM' ) {
Continue
}
If ( $Activity -in $OpeningActivities ) {
2026-07-08 16:44:12 -04:00
If ( $null -ne $OpenWindow -and $RepeatedOpeningClosureDescriptions . ContainsKey ( $Activity )) {
$UnlockedWindows . Add ([ PSCustomObject ] @ {
2026-07-08 11:27:51 -04:00
StartTime = $OpenWindow . StartTime
Start = $OpenWindow . StartTime . ToString ( 'yyyy-MM-dd HH:mm:ss' )
End = "before $( $ActivityEvent . TimeCreated . ToString ( 'yyyy-MM-dd HH:mm:ss' )) "
2026-07-08 16:44:12 -04:00
Duration = Format-LocalUnlockedDuration -StartTime $OpenWindow . StartTime -EndTime $ActivityEvent . TimeCreated -Maximum
2026-07-08 11:27:51 -04:00
User = $OpenWindow . User
2026-07-08 12:20:45 -04:00
Opened = $OpenWindow . Opened
2026-07-08 16:44:12 -04:00
Closed = $RepeatedOpeningClosureDescriptions [ $Activity ]
Note = 'latest possible duration'
2026-07-08 11:27:51 -04:00
}) | Out-Null
2026-07-08 11:56:10 -04:00
$OpenWindow = $null
2026-07-08 11:27:51 -04:00
}
2026-07-08 11:56:10 -04:00
If ( $null -eq $OpenWindow ) {
$OpenWindow = [ PSCustomObject ] @ {
2026-07-08 11:27:51 -04:00
StartTime = $ActivityEvent . TimeCreated
User = $User
2026-07-08 16:44:12 -04:00
Opened = Get-LocalUnlockedWindowAction -Activity $Activity
2026-07-08 11:27:51 -04:00
}
}
}
}
2026-07-08 11:56:10 -04:00
If ( $null -ne $OpenWindow ) {
2026-07-08 16:44:12 -04:00
$UnlockedWindows . Add ([ PSCustomObject ] @ {
2026-07-08 11:27:51 -04:00
StartTime = $OpenWindow . StartTime
Start = $OpenWindow . StartTime . ToString ( 'yyyy-MM-dd HH:mm:ss' )
End = 'open through returned events'
2026-07-08 16:44:12 -04:00
Duration = 'open'
2026-07-08 11:27:51 -04:00
User = $OpenWindow . User
2026-07-08 12:20:45 -04:00
Opened = $OpenWindow . Opened
Closed = 'not returned'
Note = 'no closing event returned'
2026-07-08 11:27:51 -04:00
}) | Out-Null
}
2026-07-08 16:44:12 -04:00
Return $UnlockedWindows
2026-07-08 11:27:51 -04:00
}
2026-07-08 16:44:12 -04:00
Function Write-LocalUnlockedWindowOutput {
2026-07-08 11:27:51 -04:00
Param (
[ AllowNull ()]
[ object[] ] $ActivityEvents
)
2026-07-08 16:44:12 -04:00
$UnlockedWindows = @ ( Get-LocalUnlockedWindow -ActivityEvents $ActivityEvents | Sort-Object -Property StartTime )
2026-07-08 11:27:51 -04:00
2026-07-08 16:44:12 -04:00
If ( $UnlockedWindows . Count -eq 0 ) {
Write-Output 'Local unlocked windows inferred from returned events: none'
2026-07-08 11:27:51 -04:00
Return
}
2026-07-08 16:44:12 -04:00
$Rows = @ ( $UnlockedWindows | Select-Object Start , End , Duration , User , Opened , Closed , Note )
Write-Output 'Local unlocked windows inferred from returned events:'
Write-PlainTextTable -Rows $Rows -Columns @ ( 'Start' , 'End' , 'Duration' , 'User' , 'Opened' , 'Closed' , 'Note' )
2026-07-08 11:27:51 -04:00
}
2026-07-07 15:23:15 -04:00
# 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 = @ {
2026-07-07 14:45:18 -04:00
LogName = 'Security'
2026-07-08 11:04:22 -04:00
Id = @ ( 4800 , 4801 , 4802 , 4803 )
2026-07-07 14:45:18 -04:00
}
2026-07-08 11:04:22 -04:00
# 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.
2026-07-07 17:30:11 -04:00
$UnlockAuthFilter = @ {
LogName = 'Security'
Id = 4624
}
2026-07-08 11:04:22 -04:00
$TerminalServicesFilter = @ {
LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
Id = @ ( 24 , 25 )
}
2026-07-07 15:39:57 -04:00
$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
}
)
2026-07-08 11:36:27 -04:00
Try {
If ( $StartHasValue ) {
2026-07-07 14:56:56 -04:00
$StartDateTime = ConvertTo-ActivityDateTime -Value $Start -ParameterName 'Start'
}
2026-07-07 15:23:15 -04:00
2026-07-08 11:36:27 -04:00
If ( $EndHasValue ) {
$EndDateTime = ConvertTo-ActivityDateTime -Value $End -ParameterName 'End'
2026-07-07 17:30:11 -04:00
}
2026-07-08 11:36:27 -04:00
If ( -not $EndHasValue ) {
$EndDateTime = [ DateTimeOffset ]:: Now
2026-07-08 11:04:22 -04:00
}
2026-07-08 11:36:27 -04:00
If ( -not $StartHasValue ) {
$StartDateTime = $EndDateTime . AddDays ( -7 )
2026-07-07 15:39:57 -04:00
}
2026-07-07 14:45:18 -04:00
}
2026-07-08 11:36:27 -04:00
Catch {
2026-07-08 11:56:10 -04:00
Write-Error ( Get-UserLogonActivityErrorMessage -ErrorRecord $_ -FallbackMessage 'Unable to resolve the activity date range.' )
2026-07-08 11:36:27 -04:00
Exit 1
}
2026-07-07 17:30:11 -04:00
2026-07-08 11:36:27 -04:00
If ( $EndDateTime -le $StartDateTime ) {
Write-Error "-End must be later than -Start."
Exit 1
}
2026-07-08 11:04:22 -04:00
2026-07-08 11:36:27 -04:00
$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
}
2026-07-07 15:39:57 -04:00
2026-07-08 11:36:27 -04:00
$ActivityEvents = New-Object System . Collections . Generic . List [ object ]
2026-07-07 15:39:57 -04:00
2026-07-08 11:36:27 -04:00
ForEach ( $Event in ( Get-UserLogonActivityWinEvent -FilterHashtable $WinlogonFilter -LogDescription 'System Winlogon event log' )) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent ( ConvertTo-WinlogonUserActivity -Event $Event )
}
2026-07-07 15:39:57 -04:00
2026-07-08 11:36:27 -04:00
ForEach ( $Event in ( Get-UserLogonActivityWinEvent -FilterHashtable $SecurityFilter -LogDescription 'Security event log' )) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent ( ConvertTo-SecurityUserActivity -Event $Event )
}
2026-07-07 15:23:15 -04:00
2026-07-08 11:36:27 -04:00
ForEach ( $Event in ( Get-UserLogonActivityWinEvent -FilterHashtable $UnlockAuthFilter -LogDescription 'Security unlock-authentication event log' )) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent ( ConvertTo-SecurityUserActivity -Event $Event )
}
2026-07-07 14:56:56 -04:00
2026-07-08 11:36:27 -04:00
ForEach ( $Event in ( Get-UserLogonActivityWinEvent -FilterHashtable $TerminalServicesFilter -LogDescription 'Terminal Services Local Session Manager event log' )) {
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent ( ConvertTo-TerminalServicesUserActivity -Event $Event )
}
2026-07-07 14:45:18 -04:00
2026-07-08 11:36:27 -04:00
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 )
}
2026-07-07 14:45:18 -04:00
}
2026-07-08 11:36:27 -04:00
$OutputEvents = @ ( Get-UniqueUserLogonActivity -ActivityEvents $ActivityEvents | Sort-Object -Property TimeCreated )
2026-07-08 12:00:25 -04:00
Write-UserLogonActivityOutput -ActivityEvents $OutputEvents -StartDateTime $StartDateTime -EndDateTime $EndDateTime
2026-07-08 11:56:10 -04:00
Try {
2026-07-08 16:44:12 -04:00
Write-LocalUnlockedWindowOutput -ActivityEvents $OutputEvents
2026-07-08 11:56:10 -04:00
}
Catch {
2026-07-08 16:44:12 -04:00
$LocalUnlockedWindowError = Get-UserLogonActivityErrorMessage -ErrorRecord $_ -FallbackMessage 'Unable to infer local unlocked windows.'
Write-Output "Local unlocked windows inferred from returned events: unavailable. ${LocalUnlockedWindowError} "
2026-07-08 11:56:10 -04:00
}