Add Get-PasswordChangeHistory script to display password change history for Active Directory accounts

This commit is contained in:
Petr Štěpán
2026-07-21 13:59:17 +02:00
commit 38932b0384
+215
View File
@@ -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_<SamAccountName>_<timestamp>.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 &gt;= and &lt;=.
# 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 = '&gt;='
$lte = '&lt;='
$xmlFilter = (
'<QueryList><Query Id="0" Path="Security">' +
'<Select Path="Security">' +
'*[System[' +
'(EventID=4723 or EventID=4724)' +
' and TimeCreated[@SystemTime' + $gte + '"' + $startUtc + '"' +
' and @SystemTime' + $lte + '"' + $endUtc + '"]]' +
' and EventData[Data[@Name=&quot;TargetUserName&quot;]=&quot;' + $SamAccountName + '&quot;]]' +
'</Select></Query></QueryList>'
)
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