fixed stability and output
This commit is contained in:
+218
-76
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Gets user logon, logoff, lock, and unlock activity from the Windows Security log.
|
Gets user logon, logoff, lock, and unlock activity from Windows event logs.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
$Start = '2026-07-03T00:00:00Z'
|
$Start = '2026-07-03T00:00:00Z'
|
||||||
@@ -126,10 +126,13 @@ Function Test-UserLogonActivityUser {
|
|||||||
)
|
)
|
||||||
|
|
||||||
If ([string]::IsNullOrWhiteSpace($UserName)) { Return $false }
|
If ([string]::IsNullOrWhiteSpace($UserName)) { Return $false }
|
||||||
If ($UserName -eq '-') { Return $false }
|
|
||||||
If ($UserName.EndsWith('$')) { Return $false }
|
$LeafUserName = ($UserName -split '\\')[-1]
|
||||||
If ($UserName -in @('ANONYMOUS LOGON', 'LOCAL SERVICE', 'NETWORK SERVICE', 'SYSTEM')) { Return $false }
|
|
||||||
If ($UserName -match '^(DWM|UMFD)-\d+$') { Return $false }
|
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
|
Return $true
|
||||||
}
|
}
|
||||||
@@ -150,17 +153,98 @@ Function Format-UserName {
|
|||||||
Return "${DomainName}\${UserName}"
|
Return "${DomainName}\${UserName}"
|
||||||
}
|
}
|
||||||
|
|
||||||
Function Get-UserLogonActivitySecurityLogAccessError {
|
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 {
|
Try {
|
||||||
Get-WinEvent -ListLog Security -ErrorAction Stop | Out-Null
|
Return (New-Object Security.Principal.SecurityIdentifier($SidString)).Translate([Security.Principal.NTAccount]).Value
|
||||||
Return $null
|
|
||||||
}
|
}
|
||||||
Catch {
|
Catch {
|
||||||
Return "Unable to read the Windows Security event log. Run PowerShell as administrator if access is denied. $($_.Exception.Message)"
|
$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 ConvertTo-UserLogonActivity {
|
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')]
|
||||||
|
[string]$Activity
|
||||||
|
)
|
||||||
|
|
||||||
|
If (-not (Test-UserLogonActivityUser -UserName $User)) { Return $null }
|
||||||
|
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Date = $TimeCreated.ToString('yyyy-MM-dd')
|
||||||
|
Time = $TimeCreated.ToString('HH:mm:ss')
|
||||||
|
User = $User
|
||||||
|
Activity = $Activity
|
||||||
|
TimeCreated = $TimeCreated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertTo-WinlogonUserActivity {
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[object]$Event
|
||||||
|
)
|
||||||
|
|
||||||
|
Switch ($Event.Id) {
|
||||||
|
7001 { $Activity = 'logon' }
|
||||||
|
7002 { $Activity = 'logoff' }
|
||||||
|
Default { Return $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
$User = Resolve-UserLogonActivitySid -Sid (Get-UserLogonActivitySidFromEvent -Event $Event)
|
||||||
|
If ([string]::IsNullOrWhiteSpace($User)) { Return $null }
|
||||||
|
|
||||||
|
New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User $User -Activity $Activity
|
||||||
|
}
|
||||||
|
|
||||||
|
Function ConvertTo-SecurityUserActivity {
|
||||||
Param(
|
Param(
|
||||||
[Parameter(Mandatory=$true)]
|
[Parameter(Mandatory=$true)]
|
||||||
[object]$Event
|
[object]$Event
|
||||||
@@ -174,20 +258,8 @@ Function ConvertTo-UserLogonActivity {
|
|||||||
|
|
||||||
Switch ($Event.Id) {
|
Switch ($Event.Id) {
|
||||||
4624 {
|
4624 {
|
||||||
Switch ($LogonType) {
|
If ($LogonType -ne '7') { Return $null }
|
||||||
'2' { $Activity = 'logon' }
|
$Activity = 'unlock'
|
||||||
'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 {
|
4800 {
|
||||||
$Activity = 'lock'
|
$Activity = 'lock'
|
||||||
@@ -200,27 +272,112 @@ Function ConvertTo-UserLogonActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
If (-not (Test-UserLogonActivityUser -UserName $UserName)) { Return $null }
|
New-UserLogonActivityRecord -TimeCreated $Event.TimeCreated -User (Format-UserName -DomainName $DomainName -UserName $UserName) -Activity $Activity
|
||||||
|
}
|
||||||
|
|
||||||
$EventTime = $Event.TimeCreated
|
Function Get-UserLogonActivityWinEvent {
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[hashtable]$FilterHashtable,
|
||||||
|
|
||||||
[PSCustomObject]@{
|
[int]$MaxEvents = 0,
|
||||||
Date = $EventTime.ToString('yyyy-MM-dd')
|
|
||||||
Time = $EventTime.ToString('HH:mm:ss')
|
[Parameter(Mandatory=$true)]
|
||||||
User = Format-UserName -DomainName $DomainName -UserName $UserName
|
[string]$LogDescription
|
||||||
Activity = $Activity
|
)
|
||||||
|
|
||||||
|
$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 @()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$SecurityLogAccessError = Get-UserLogonActivitySecurityLogAccessError
|
Function Add-UserLogonActivityRecord {
|
||||||
If (-not [string]::IsNullOrWhiteSpace($SecurityLogAccessError)) {
|
Param(
|
||||||
Write-Error $SecurityLogAccessError
|
[Parameter(Mandatory=$true)]
|
||||||
Exit 1
|
[object]$ActivityEvents,
|
||||||
|
|
||||||
|
[AllowNull()]
|
||||||
|
[object]$ActivityEvent
|
||||||
|
)
|
||||||
|
|
||||||
|
If ($null -ne $ActivityEvent) {
|
||||||
|
$ActivityEvents.Add($ActivityEvent) | Out-Null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$Filter = @{
|
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)) {
|
||||||
|
$IsDuplicate = $false
|
||||||
|
|
||||||
|
ForEach ($UniqueEvent in $UniqueEvents) {
|
||||||
|
If (
|
||||||
|
$UniqueEvent.User -eq $ActivityEvent.User -and
|
||||||
|
$UniqueEvent.Activity -eq $ActivityEvent.Activity -and
|
||||||
|
[Math]::Abs(($UniqueEvent.TimeCreated - $ActivityEvent.TimeCreated).TotalSeconds) -le 5
|
||||||
|
) {
|
||||||
|
$IsDuplicate = $true
|
||||||
|
Break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
If (-not $IsDuplicate) {
|
||||||
|
$UniqueEvents.Add($ActivityEvent) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Return $UniqueEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
Function Write-UserLogonActivityOutput {
|
||||||
|
Param(
|
||||||
|
[AllowNull()]
|
||||||
|
[object[]]$ActivityEvents
|
||||||
|
)
|
||||||
|
|
||||||
|
$Rows = @($ActivityEvents | Select-Object Date,Time,User,Activity)
|
||||||
|
|
||||||
|
If ($Rows.Count -eq 0) {
|
||||||
|
Write-Output 'No matching user logon activity events found.'
|
||||||
|
Return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output (($Rows | Format-Table -AutoSize | Out-String -Width 240).TrimEnd())
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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'
|
LogName = 'Security'
|
||||||
Id = @(4624, 4634, 4647, 4800, 4801)
|
Id = @(4624, 4800, 4801)
|
||||||
}
|
}
|
||||||
|
|
||||||
If ($UseDateRange) {
|
If ($UseDateRange) {
|
||||||
@@ -238,66 +395,51 @@ If ($UseDateRange) {
|
|||||||
Exit 1
|
Exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
$Filter.StartTime = $StartDateTime.LocalDateTime
|
$WinlogonFilter.StartTime = $StartDateTime.LocalDateTime
|
||||||
$Filter.EndTime = $EndDateTime.LocalDateTime
|
$WinlogonFilter.EndTime = $EndDateTime.LocalDateTime
|
||||||
|
$SecurityFilter.StartTime = $StartDateTime.LocalDateTime
|
||||||
|
$SecurityFilter.EndTime = $EndDateTime.LocalDateTime
|
||||||
|
|
||||||
Try {
|
$ActivityEvents = New-Object System.Collections.Generic.List[object]
|
||||||
$Events = Get-WinEvent -FilterHashtable $Filter -ErrorAction Stop
|
|
||||||
}
|
ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $WinlogonFilter -LogDescription 'System Winlogon event log')) {
|
||||||
Catch {
|
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-WinlogonUserActivity -Event $Event)
|
||||||
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)) {
|
ForEach ($Event in (Get-UserLogonActivityWinEvent -FilterHashtable $SecurityFilter -LogDescription 'Security event log')) {
|
||||||
ConvertTo-UserLogonActivity -Event $Event
|
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$OutputEvents = @(Get-UniqueUserLogonActivity -ActivityEvents $ActivityEvents | Sort-Object -Property TimeCreated)
|
||||||
|
Write-UserLogonActivityOutput -ActivityEvents $OutputEvents
|
||||||
}
|
}
|
||||||
Else {
|
Else {
|
||||||
$ActivityLimit = 15
|
$ActivityLimit = 15
|
||||||
$SearchLimit = 200
|
$SearchLimit = 200
|
||||||
$MaxSearchLimit = 10000
|
$MaxSearchLimit = 10000
|
||||||
$ActivityEvents = New-Object System.Collections.Generic.List[object]
|
$OutputEvents = @()
|
||||||
|
|
||||||
Do {
|
Do {
|
||||||
$ActivityEvents.Clear()
|
$ActivityEvents = New-Object System.Collections.Generic.List[object]
|
||||||
|
$WinlogonEvents = @(Get-UserLogonActivityWinEvent -FilterHashtable $WinlogonFilter -MaxEvents $SearchLimit -LogDescription 'System Winlogon event log')
|
||||||
|
$SecurityEvents = @(Get-UserLogonActivityWinEvent -FilterHashtable $SecurityFilter -MaxEvents $SearchLimit -LogDescription 'Security event log')
|
||||||
|
|
||||||
Try {
|
ForEach ($Event in $WinlogonEvents) {
|
||||||
$Events = Get-WinEvent -FilterHashtable $Filter -MaxEvents $SearchLimit -ErrorAction Stop
|
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-WinlogonUserActivity -Event $Event)
|
||||||
}
|
|
||||||
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) {
|
ForEach ($Event in $SecurityEvents) {
|
||||||
$ActivityEvent = ConvertTo-UserLogonActivity -Event $Event
|
Add-UserLogonActivityRecord -ActivityEvents $ActivityEvents -ActivityEvent (ConvertTo-SecurityUserActivity -Event $Event)
|
||||||
|
|
||||||
If ($null -ne $ActivityEvent) {
|
|
||||||
$ActivityEvents.Add($ActivityEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
If ($ActivityEvents.Count -ge $ActivityLimit) {
|
$OutputEvents = @(Get-UniqueUserLogonActivity -ActivityEvents $ActivityEvents | Sort-Object -Property TimeCreated -Descending | Select-Object -First $ActivityLimit)
|
||||||
Break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
If ($ActivityEvents.Count -ge $ActivityLimit -or $Events.Count -lt $SearchLimit -or $SearchLimit -ge $MaxSearchLimit) {
|
If ($OutputEvents.Count -ge $ActivityLimit -or (($WinlogonEvents.Count -lt $SearchLimit) -and ($SecurityEvents.Count -lt $SearchLimit)) -or $SearchLimit -ge $MaxSearchLimit) {
|
||||||
Break
|
Break
|
||||||
}
|
}
|
||||||
|
|
||||||
$SearchLimit = [Math]::Min(($SearchLimit * 2), $MaxSearchLimit)
|
$SearchLimit = [Math]::Min(($SearchLimit * 2), $MaxSearchLimit)
|
||||||
} While ($true)
|
} While ($true)
|
||||||
|
|
||||||
$ActivityEvents | Select-Object -First $ActivityLimit
|
Write-UserLogonActivityOutput -ActivityEvents $OutputEvents
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user