From 38932b0384460e221b0589023bba2e8329db4f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20=C5=A0t=C4=9Bp=C3=A1n?= Date: Tue, 21 Jul 2026 13:59:17 +0200 Subject: [PATCH] Add Get-PasswordChangeHistory script to display password change history for Active Directory accounts --- Get-PasswordChangeHistory.ps1 | 215 ++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 Get-PasswordChangeHistory.ps1 diff --git a/Get-PasswordChangeHistory.ps1 b/Get-PasswordChangeHistory.ps1 new file mode 100644 index 0000000..2974d87 --- /dev/null +++ b/Get-PasswordChangeHistory.ps1 @@ -0,0 +1,215 @@ +<# +.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