Public/eventlog/Get-ADLockoutSource.ps1
|
#Requires -Version 5.1 function Get-ADLockoutSource { <# .SYNOPSIS Find the source machine for AD account lockout events .DESCRIPTION Reads Security event 4740 from the PDC Emulator or a specified domain controller and correlates events with Active Directory users by SID. Returns typed, pipeline-friendly records sorted with the newest lockout first. .PARAMETER Identity One or more Active Directory user identities (SamAccountName, DN, GUID, or SID) to look up lockout sources for. Accepts pipeline input by value and by property name (alias 'SamAccountName'). .PARAMETER Server The domain controller whose Security event log is queried. Defaults to the discovered PDC Emulator when omitted. .PARAMETER MaxEvents The maximum number of 4740 events to retrieve from the Security log. Defaults to 1000. .PARAMETER After Only include lockout events with a TimeCreated after this timestamp. .PARAMETER Credential Specifies the credentials to use for the Get-ADUser and Get-WinEvent queries. .EXAMPLE Get-ADLockoutSource -Identity 'jsmith' Finds the lockout source for 'jsmith' using the discovered PDC Emulator. .EXAMPLE Get-ADLockoutSource -Identity 'jsmith' -Server 'DC01.contoso.com' Finds the lockout source for 'jsmith' by querying the Security log on a specific domain controller. .EXAMPLE Get-ADLockedAccount | Get-ADLockoutSource Pipes currently locked accounts into Get-ADLockoutSource to identify where each lockout originated. .OUTPUTS PSWinOps.ADLockoutSource One object per matching 4740 event, sorted by LockoutTime descending. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-09-04 Requires: PowerShell 5.1+ / Windows only Requires: ActiveDirectory module (RSAT-AD-PowerShell) Requires: Read access to the PDC Security log (Administrator or Event Log Readers) Requires: Account Lockout auditing enabled on domain controllers .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4740 #> [CmdletBinding()] [OutputType('PSWinOps.ADLockoutSource')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('SamAccountName')] [string[]]$Identity, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string]$Server, [Parameter(Mandatory = $false)] [ValidateRange(1, 2147483647)] [int]$MaxEvents = 1000, [Parameter(Mandatory = $false)] [datetime]$After, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$Credential ) begin { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting" try { Import-Module -Name 'ActiveDirectory' -ErrorAction Stop } catch { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new( 'ActiveDirectory module is not available. Install RSAT-AD-PowerShell.', $_.Exception ), 'ActiveDirectoryModuleMissing', [System.Management.Automation.ErrorCategory]::NotInstalled, 'ActiveDirectory' ) ) } $adParams = @{} if ($PSBoundParameters.ContainsKey('Credential')) { $adParams['Credential'] = $Credential } $targetServer = $Server if (-not $PSBoundParameters.ContainsKey('Server')) { try { $pdc = Get-ADDomainController -Discover -Service 'PrimaryDC' -ErrorAction Stop $targetServer = $pdc.HostName[0] Write-Verbose -Message "[$($MyInvocation.MyCommand)] Discovered PDC Emulator '$targetServer'" } catch { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new( "Failed to discover the PDC Emulator: $_", $_.Exception ), 'PdcDiscoveryFailed', [System.Management.Automation.ErrorCategory]::ResourceUnavailable, $Server ) ) } } if ($Server) { $adParams['Server'] = $Server } $eventParams = @{} if ($PSBoundParameters.ContainsKey('Credential')) { $eventParams['Credential'] = $Credential } } process { foreach ($targetIdentity in $Identity) { try { $userDetail = Get-ADUser -Identity $targetIdentity -Properties 'SID' @adParams -ErrorAction Stop } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to resolve identity '$targetIdentity': $_" continue } $targetSid = $userDetail.SID.Value $filterHashtable = @{ LogName = 'Security' Id = 4740 } if ($PSBoundParameters.ContainsKey('After')) { $filterHashtable['StartTime'] = $After } try { $events = Get-WinEvent -ComputerName $targetServer -FilterHashtable $filterHashtable -MaxEvents $MaxEvents @eventParams -ErrorAction Stop } catch { if ($_.CategoryInfo.Category -eq 'ObjectNotFound') { Write-Verbose -Message "[$($MyInvocation.MyCommand)] No lockout (4740) events found on '$targetServer' for '$targetIdentity'" continue } if ($_.Exception.Message -match 'No events were found' -or $_.FullyQualifiedErrorId -match 'NoMatchingEventsFound') { Write-Verbose -Message "[$($MyInvocation.MyCommand)] No lockout (4740) events found on '$targetServer' for '$targetIdentity'" continue } Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to query Security log on '$targetServer' for '$targetIdentity': $_" continue } if (-not $events) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] No lockout (4740) events found on '$targetServer' for '$targetIdentity'" continue } $results = [System.Collections.Generic.List[object]]::new() foreach ($lockoutEvent in $events) { try { if ($lockoutEvent.Properties.Count -lt 3) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Skipping event with unexpected property count on '$targetServer'" continue } $eventSid = $lockoutEvent.Properties[2].Value if (-not $eventSid -or $eventSid.ToString() -ne $targetSid) { continue } $results.Add([PSCustomObject]@{ PSTypeName = 'PSWinOps.ADLockoutSource' ComputerName = $targetServer DomainController = $lockoutEvent.MachineName UserName = $lockoutEvent.Properties[0].Value SamAccountName = $userDetail.SamAccountName LockoutSource = $lockoutEvent.Properties[1].Value LockoutTime = [datetime]$lockoutEvent.TimeCreated EventId = [int]$lockoutEvent.Id Timestamp = Get-Date -Format 'o' }) } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] Failed to parse event for '$targetIdentity' on '$targetServer': $_" continue } } $results | Sort-Object -Property 'LockoutTime' -Descending } } end { Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed" } } |