<# .SYNOPSIS Displays the password change history for the specified Active Directory account. .DESCRIPTION Searches the Security event log on all reachable domain controllers and returns password change records (Event ID 4723, 4724) for the specified account. Event ID 4723 - user changed their own password Event ID 4724 - administrator reset the user's password .PARAMETER SamAccountName SamAccountName of the target account (e.g. "jnovak"). .PARAMETER DomainController Optional specific domain controller(s). Default = all DCs in the domain. .PARAMETER StartTime Lower bound of the time range (default: last 90 days). .PARAMETER EndTime Upper bound of the time range (default: now). .PARAMETER MaxEvents Maximum number of events searched per DC (default: 10000). .PARAMETER ExportPath Path to the CSV export file. Defaults to ".\PasswordHistory__.csv" in the current directory. .EXAMPLE .\Get-PasswordChangeHistory.ps1 -SamAccountName "jnovak" .EXAMPLE .\Get-PasswordChangeHistory.ps1 -SamAccountName "jnovak" -StartTime (Get-Date).AddDays(-180) .EXAMPLE .\Get-PasswordChangeHistory.ps1 -SamAccountName "jnovak" -DomainController "DC01" .EXAMPLE .\Get-PasswordChangeHistory.ps1 -SamAccountName "jnovak" -ExportPath "C:\reports\jnovak.csv" -WhatIf .NOTES Author: Petr Stepan Created: 2026-07-21 Version: 1.0.0 Changelog: 1.0.0 - Initial version Requires permission to read the Security event log on the DC (Domain Admins or Event Log Readers). Auditing must be enabled via GPO: Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy > Account Management > Audit User Account Management = Success #> #Requires -Modules ActiveDirectory [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low')] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$SamAccountName, [Parameter()] [string[]]$DomainController, [Parameter()] [datetime]$StartTime = (Get-Date).AddDays(-90), [Parameter()] [datetime]$EndTime = (Get-Date), [Parameter()] [int]$MaxEvents = 10000, [Parameter(HelpMessage = 'Path to the CSV export file.')] [ValidateNotNullOrEmpty()] [string]$ExportPath = ".\PasswordHistory_${SamAccountName}_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # --- 1. Verify account in AD --- Write-Verbose "Verifying account '$SamAccountName' in Active Directory..." try { $adUser = Get-ADUser -Identity $SamAccountName -Properties DisplayName, UserPrincipalName, DistinguishedName } catch { Write-Error "Account '$SamAccountName' was not found in Active Directory: $_" exit 1 } Write-Host "" Write-Host "Searching for password changes for:" -ForegroundColor Cyan Write-Host " Name : $($adUser.DisplayName)" Write-Host " UPN : $($adUser.UserPrincipalName)" Write-Host " Range : $($StartTime.ToString('dd.MM.yyyy HH:mm')) - $($EndTime.ToString('dd.MM.yyyy HH:mm'))" Write-Host "" # --- 2. List of DCs --- if (-not $DomainController) { Write-Verbose "Retrieving list of domain controllers..." $DomainController = (Get-ADDomainController -Filter *).HostName } Write-Host "Searching DCs: $($DomainController -join ', ')" -ForegroundColor Cyan Write-Host "" # --- 3. Build XML filter --- # TimeCreated needs XML entities >= and <=. # Built via string concatenation so PowerShell does not interpret '&' as an operator. $startUtc = $StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.000Z') $endUtc = $EndTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.000Z') $gte = '>=' $lte = '<=' $xmlFilter = ( '' + '' ) Write-Verbose "XML filter: $xmlFilter" # --- 4. Collect events from all DCs --- $allEvents = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($dc in $DomainController) { Write-Verbose "Searching $dc..." try { $events = @(Get-WinEvent -ComputerName $dc ` -FilterXml $xmlFilter ` -MaxEvents $MaxEvents ` -ErrorAction SilentlyContinue) if (-not $events) { Write-Host " ${dc}: 0 records" -ForegroundColor DarkGray continue } foreach ($ev in $events) { $xml = [xml]$ev.ToXml() $evData = $xml.Event.EventData.Data $get = { param($n) ($evData | Where-Object { $_.Name -eq $n }).'#text' } $eventType = switch ($ev.Id) { 4723 { 'Password change (user)' } 4724 { 'Password reset (admin)' } } $allEvents.Add([PSCustomObject]@{ Time = $ev.TimeCreated EventID = $ev.Id Type = $eventType TargetAccount = & $get 'TargetUserName' TargetDomain = & $get 'TargetDomainName' PerformedByAccount = & $get 'SubjectUserName' PerformedByDomain = & $get 'SubjectDomainName' PerformedByLogonId = & $get 'SubjectLogonId' DC = $dc Result = if ($ev.Keywords -band 0x8020000000000000) { 'Success' } else { 'Failure' } }) } Write-Host " ${dc}: found $($events.Count) records" -ForegroundColor Green } catch { Write-Warning " ${dc}: error reading event log - $_" } } # --- 5. Output --- if ($allEvents.Count -eq 0) { Write-Host "" Write-Host "No password change records were found." -ForegroundColor Yellow Write-Host "Check:" -ForegroundColor Yellow Write-Host " * Is 'Audit User Account Management' auditing enabled on the DC?" Write-Host " * Was the account active during the specified time range?" Write-Host " * Was the event log trimmed (overwritten)?" exit 0 } $sorted = @($allEvents | Sort-Object Time) Write-Host "" Write-Host "=== RESULTS ($($sorted.Count) records) ===" -ForegroundColor Cyan $sorted | Format-Table -AutoSize -Property Time, Type, PerformedByAccount, PerformedByDomain, DC, Result Write-Host "=== DETAILED OVERVIEW ===" -ForegroundColor Cyan foreach ($ev in $sorted) { $color = if ($ev.EventID -eq 4724) { 'Magenta' } else { 'Green' } Write-Host "[$($ev.Time.ToString('dd.MM.yyyy HH:mm:ss'))] $($ev.Type)" -ForegroundColor $color Write-Host " Target account : $($ev.TargetDomain)\$($ev.TargetAccount)" Write-Host " Performed by : $($ev.PerformedByDomain)\$($ev.PerformedByAccount)" Write-Host " LogonID : $($ev.PerformedByLogonId)" Write-Host " DC : $($ev.DC) | EventID: $($ev.EventID) | $($ev.Result)" Write-Host "" } if ($PSCmdlet.ShouldProcess($ExportPath, "Export $($sorted.Count) records to CSV")) { $sorted | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Host "Export saved: $ExportPath" -ForegroundColor DarkGray } exit 0