#Requires -Version 5.1 <# .SYNOPSIS Gets user logon, logoff, lock, and unlock activity from the Windows Security log. .EXAMPLE $Start = '2026-07-03T00:00:00Z' $End = '2026-07-06T00:00:00Z' .\Get-UserLogonActivity.ps1 .EXAMPLE .\Get-UserLogonActivity.ps1 #> $StartVariable = Get-Variable -Name Start -ErrorAction SilentlyContinue $EndVariable = Get-Variable -Name End -ErrorAction SilentlyContinue $UseDateRange = ($null -ne $StartVariable -and $null -ne $EndVariable) If (($null -ne $StartVariable) -xor ($null -ne $EndVariable)) { Write-Error '$Start and $End must either both be defined or both be omitted. If omitted, the script returns the latest 15 activity events.' 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 Test-UserLogonActivityUser { Param( [AllowNull()] [string]$UserName ) If ([string]::IsNullOrWhiteSpace($UserName)) { Return $false } If ($UserName -eq '-') { Return $false } If ($UserName.EndsWith('$')) { Return $false } If ($UserName -in @('ANONYMOUS LOGON', 'LOCAL SERVICE', 'NETWORK SERVICE', 'SYSTEM')) { Return $false } If ($UserName -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 Get-UserLogonActivitySecurityLogAccessError { Try { Get-WinEvent -ListLog Security -ErrorAction Stop | Out-Null Return $null } Catch { Return "Unable to read the Windows Security event log. Run PowerShell as administrator if access is denied. $($_.Exception.Message)" } } Function ConvertTo-UserLogonActivity { 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') $LogonType = Get-EventDataValue -EventXml $EventXml -Name 'LogonType' $Activity = $null Switch ($Event.Id) { 4624 { Switch ($LogonType) { '2' { $Activity = 'logon' } '7' { $Activity = 'unlock' } '10' { $Activity = 'logon' } '11' { $Activity = 'logon' } Default { Return $null } } } 4634 { If ($LogonType -notin @('2', '7', '10', '11')) { Return $null } $Activity = 'logoff' } 4647 { $Activity = 'logoff' } 4800 { $Activity = 'lock' } 4801 { $Activity = 'unlock' } Default { Return $null } } If (-not (Test-UserLogonActivityUser -UserName $UserName)) { Return $null } $EventTime = $Event.TimeCreated [PSCustomObject]@{ Date = $EventTime.ToString('yyyy-MM-dd') Time = $EventTime.ToString('HH:mm:ss') User = Format-UserName -DomainName $DomainName -UserName $UserName Activity = $Activity } } $SecurityLogAccessError = Get-UserLogonActivitySecurityLogAccessError If (-not [string]::IsNullOrWhiteSpace($SecurityLogAccessError)) { Write-Error $SecurityLogAccessError Exit 1 } $Filter = @{ LogName = 'Security' Id = @(4624, 4634, 4647, 4800, 4801) } If ($UseDateRange) { Try { $StartDateTime = ConvertTo-ActivityDateTime -Value $Start -ParameterName 'Start' $EndDateTime = ConvertTo-ActivityDateTime -Value $End -ParameterName 'End' } Catch { Write-Error $_.Exception.Message Exit 1 } If ($EndDateTime -le $StartDateTime) { Write-Error "-End must be later than -Start." Exit 1 } $Filter.StartTime = $StartDateTime.LocalDateTime $Filter.EndTime = $EndDateTime.LocalDateTime Try { $Events = Get-WinEvent -FilterHashtable $Filter -ErrorAction Stop } Catch { If ($_.FullyQualifiedErrorId -like '*NoMatchingEventsFound*' -or $_.Exception.Message -like '*No events were found*') { $Events = @() } Else { Write-Error "Unable to read the Windows Security event log. Run PowerShell as administrator if access is denied. $($_.Exception.Message)" Exit 1 } } ForEach ($Event in ($Events | Sort-Object -Property TimeCreated)) { ConvertTo-UserLogonActivity -Event $Event } } Else { $ActivityLimit = 15 $SearchLimit = 200 $MaxSearchLimit = 10000 $ActivityEvents = New-Object System.Collections.Generic.List[object] Do { $ActivityEvents.Clear() Try { $Events = Get-WinEvent -FilterHashtable $Filter -MaxEvents $SearchLimit -ErrorAction Stop } Catch { If ($_.FullyQualifiedErrorId -like '*NoMatchingEventsFound*' -or $_.Exception.Message -like '*No events were found*') { $Events = @() } Else { Write-Error "Unable to read the Windows Security event log. Run PowerShell as administrator if access is denied. $($_.Exception.Message)" Exit 1 } } ForEach ($Event in $Events) { $ActivityEvent = ConvertTo-UserLogonActivity -Event $Event If ($null -ne $ActivityEvent) { $ActivityEvents.Add($ActivityEvent) } If ($ActivityEvents.Count -ge $ActivityLimit) { Break } } If ($ActivityEvents.Count -ge $ActivityLimit -or $Events.Count -lt $SearchLimit -or $SearchLimit -ge $MaxSearchLimit) { Break } $SearchLimit = [Math]::Min(($SearchLimit * 2), $MaxSearchLimit) } While ($true) $ActivityEvents | Select-Object -First $ActivityLimit }