Public/eventlog/Get-LogonFailure.ps1
|
#Requires -Version 5.1 function Get-LogonFailure { <# .SYNOPSIS Aggregate and decode Windows Security 4625 failed-logon events .DESCRIPTION Queries the Windows Security event log for 4625 failed-logon events over a look-back window and decodes each into a correlated diagnostic row (account, domain, logon type, decoded failure reason, source IP, workstation and process). Local and remote targets are dispatched through Invoke-RemoteOrLocal; machines with no matching events return nothing and per-machine failures do not stop the remaining targets. .PARAMETER ComputerName One or more computer names to target. Defaults to the local computer. Accepts pipeline input by value and by property name. .PARAMETER Credential Optional PSCredential for authenticating to remote machines. Ignored for local machine queries. .PARAMETER Days Look-back window in days used to bound the event log scan. StartTime is computed as (Get-Date).AddDays(-Days). Valid range is 1 to 3650. Defaults to 7. .PARAMETER MaxEvents Maximum number of 4625 events returned per machine, newest first. This value is forwarded to Get-WinEvent -MaxEvents and also caps the number of decoded rows emitted. Valid range is 1 to 10000. Defaults to 200. .PARAMETER UserName Optional filter on the decoded TargetUserName. Comparison is case-insensitive equality, applied after decoding (the 4625 TargetUserName lives in the event data, not a FilterHashtable key). Empty or absent means no filter. .EXAMPLE Get-LogonFailure -Days 1 Returns decoded failed-logon events for the local machine over the last day. .EXAMPLE Get-LogonFailure -ComputerName SRV01 -UserName jdoe -Days 30 Returns decoded failed-logon events for user 'jdoe' on SRV01 over the last 30 days via WinRM. .EXAMPLE 'SRV01','SRV02' | Get-LogonFailure -MaxEvents 500 Returns up to 500 decoded failed-logon events per machine for SRV01 and SRV02 via pipeline. .OUTPUTS PSWinOps.LogonFailure One object per 4625 failed-logon event, newest first, with decoded account, domain, logon type, failure reason, source IP, workstation and process. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-07-06 Requires: PowerShell 5.1+ / Windows only Requires: membership in the local Administrators / Event Log Readers group (Security log read access) Requires: WinRM enabled on target machines for remote queries .LINK https://github.com/k9fr4n/PSWinOps .LINK Source of truth: Windows Security event log (Event ID 4625, failed logon) #> [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')] [OutputType('PSWinOps.LogonFailure')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory = $false)] [ValidateRange(1, 3650)] [int]$Days = 7, [Parameter(Mandatory = $false)] [ValidateRange(1, 10000)] [int]$MaxEvents = 200, [Parameter(Mandatory = $false)] [string]$UserName = '' ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting logon failure query" $startTime = (Get-Date).AddDays(-$Days) $scriptBlock = { param( [datetime]$ScanStartTime, [int]$MaxEvts, [string]$UserNameFilter ) $filter = @{ LogName = 'Security' Id = 4625 StartTime = $ScanStartTime } $events = @() try { $events = @(Get-WinEvent -FilterHashtable $filter -MaxEvents $MaxEvts -ErrorAction Stop) } catch { if ($_.Exception.Message -notmatch 'No events were found') { throw } } $logonTypeMap = @{ 2 = 'Interactive' 3 = 'Network' 4 = 'Batch' 5 = 'Service' 7 = 'Unlock' 8 = 'NetworkCleartext' 9 = 'NewCredentials' 10 = 'RemoteInteractive' 11 = 'CachedInteractive' } $failureReasonMap = @{ '0xC0000064' = 'Unknown user name' '0xC000006A' = 'Bad password' '0xC0000072' = 'Account disabled' '0xC0000234' = 'Account locked out' '0xC0000070' = 'Workstation restriction / logon time restriction' '0xC0000071' = 'Password expired' '0xC0000193' = 'Account expired' '0xC000015B' = 'Logon type not granted' '0xC0000224' = 'Password must change at next logon' } $results = [System.Collections.Generic.List[psobject]]::new() foreach ($evt in $events) { if ($results.Count -ge $MaxEvts) { break } $props = $evt.Properties $targetUserName = if ($props.Count -gt 5) { [string]$props[5].Value } else { '' } $targetDomain = if ($props.Count -gt 6) { [string]$props[6].Value } else { '' } $statusHex = '' if ($props.Count -gt 7) { try { $statusHex = '0x{0:X8}' -f (([int64]$props[7].Value) -band 0xFFFFFFFFL) } catch { $statusHex = [string]$props[7].Value } } $subStatusHex = '' if ($props.Count -gt 9) { try { $subStatusHex = '0x{0:X8}' -f (([int64]$props[9].Value) -band 0xFFFFFFFFL) } catch { $subStatusHex = [string]$props[9].Value } } $logonType = 0 if ($props.Count -gt 10) { try { $logonType = [int]$props[10].Value } catch { $logonType = 0 } } $logonTypeName = if ($logonTypeMap.ContainsKey($logonType)) { $logonTypeMap[$logonType] } else { 'Unknown' } $failureReason = if ($failureReasonMap.ContainsKey($subStatusHex)) { $failureReasonMap[$subStatusHex] } else { 'Other (see SubStatus)' } $workstationName = if ($props.Count -gt 13) { [string]$props[13].Value } else { '' } $processName = if ($props.Count -gt 18) { [string]$props[18].Value } else { '' } $sourceIp = if ($props.Count -gt 19) { [string]$props[19].Value } else { '' } if (-not [string]::IsNullOrWhiteSpace($UserNameFilter) -and $targetUserName -ne $UserNameFilter) { continue } $results.Add([PSCustomObject]@{ PSTypeName = 'PSWinOps.LogonFailure' ComputerName = $env:COMPUTERNAME EventTime = $evt.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss') TargetUserName = $targetUserName TargetDomain = $targetDomain LogonType = $logonType LogonTypeName = $logonTypeName FailureReason = $failureReason Status = $statusHex SubStatus = $subStatusHex WorkstationName = $workstationName SourceIpAddress = $sourceIp ProcessName = $processName Timestamp = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') }) } $results } } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Querying logon failures on '$targetComputer'" Invoke-RemoteOrLocal -ComputerName $targetComputer -Credential $Credential ` -ScriptBlock $scriptBlock ` -ArgumentList @($startTime, $MaxEvents, $UserName) } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed logon failure query" } } |