304 lines
8.6 KiB
PowerShell
304 lines
8.6 KiB
PowerShell
#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
|
|
}
|