1054 lines
32 KiB
PowerShell
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}"
|
|
}
|