Files
management-scripts/Get-UserLogonActivity.ps1

1054 lines
32 KiB
PowerShell

#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}"
}