Add Get-PasswordChangeHistory script to display password change history for Active Directory accounts
This commit is contained in:
@@ -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 >= 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 = (
|
||||
'<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="TargetUserName"]="' + $SamAccountName + '"]]' +
|
||||
'</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
|
||||
Reference in New Issue
Block a user