Assistance at your fingertips...

Find Password Change Source

<# Finds the source of an Active Directory user object password reset or password change and whether or not the change was made by the local user
This is a Very good security script:
4723 = 'An attempt was made to change an account’s password '(user‑initiated change, e.g. Ctrl+Alt+Del → Change Password).

4724 = 'An attempt was made to reset an account’s password' (admin‑initiated reset).
#>
# Target user
$user = read-Host -prompt 'What SamAccountName in the present user domain?'

# Get all DCs in the domain in one line
$dcs = (Get-ADDomainController -Filter *).Hostname

# Define cutoff time
$since = (Get-Date).AddHours(-48)

$results = foreach ($dc in $dcs) {
    try {
        Get-WinEvent -ComputerName $dc -FilterHashtable @{
            LogName   = 'Security'
            ID        = 4723,4724
            StartTime = $since
        } -ErrorAction Stop |
        Where-Object { $_.Properties.Value -contains $user } |
        Select-Object @{n='DC';e={$dc}},
                      TimeCreated,
                      @{n='TargetUser';e={($_.Properties[0].Value)}},
                      @{n='Initiator';e={($_.Properties[1].Value)}},
                      Message
    } catch {
        Write-Warning "Failed to query $dc"
    }
}

$results | Format-Table DC, TimeCreated, TargetUser, Initiator

CoPIlot suggests these changes:

Centralized error handling Instead of inline try/catch with Write-Warning, capture failures into a structured object so you can see which DCs failed and why.

Timeouts and fallbacks Get-WinEvent can hang if RPC is flaky. Use Invoke-Command with -AsJob or Parallel to query DCs concurrently, with a timeout per job. That way one bad DC doesn’t stall the whole script.

Structured output Return a single object array with both successes and failures. That makes it easy to export to CSV or hand off to another admin.

Parameterization Instead of hardcoding cutoff time, expose it as a parameter (-HoursBack, -User). Makes the script reusable.

Event parsing Don’t rely on Properties[0] or [1] indexes — those can vary. Use named XML fields ($_.Properties[n].Value) mapped to event schema. Or parse the Message with regex for reliability.

param(
    [Parameter(Mandatory)]
    [string]$User,

    [int]$HoursBack = 48
)

Import-Module ActiveDirectory

$dcs   = (Get-ADDomainController -Filter *).Hostname
$since = (Get-Date).AddHours(-$HoursBack)

$results = foreach ($dc in $dcs) {
    try {
        Get-WinEvent -ComputerName $dc -FilterHashtable @{
            LogName   = 'Security'
            ID        = 4723,4724
            StartTime = $since
        } -ErrorAction Stop |
        Where-Object { $_.Properties.Value -contains $User } |
        Select-Object @{n='DomainController';e={$dc}},
                      TimeCreated,
                      @{n='TargetUser';e={($_.Properties[0].Value)}},
                      @{n='Initiator';e={($_.Properties[1].Value)}},
                      @{n='EventID';e={$_.Id}},
                      @{n='Status';e={'Success'}},
                      Message
    } catch {
        [PSCustomObject]@{
            DomainController = $dc
            TimeCreated      = $null
            TargetUser       = $User
            Initiator        = $null
            EventID          = $null
            Status           = "Failed: $($_.Exception.Message)"
            Message          = $null
        }
    }
}

$results | Format-Table DomainController, TimeCreated, TargetUser, Initiator, EventID, Status

Posted

in

, ,

by

Tags: