Monitoring password changes is crucial for maintaining security in Active Directory (AD) environments. The Get-ADUser cmdlet provides access to password-related properties that help administrators track when users last updated their credentials. This information is vital for compliance audits, identifying accounts with outdated passwords, and generating password expiration reports. By leveraging PowerShell's capabilities with specific AD attributes, administrators can effectively monitor password hygiene across the organization.
Active Directory stores password change information through two related but distinct characteristics:
The pwdLastSet attribute exhibits specific behaviors in certain scenarios:
These special cases require careful handling in scripts to avoid errors and ensure accurate reporting.
Using the Get-ADUser command to get AD users' last password set date:
Get-ADUser -identity robert.allen -properties PwdLastSet | sort Name | ft Name,@{Name='PwdLastSet';Expression={[DateTime]::FromFileTime($_.PwdLastSet)}}
Using ADManager Plus to get AD users' last password set date:
Retrieve the date when a specific user last changed their password to verify compliance with password policies.
Get-ADUser -Identity "john.doe" -Properties PasswordLastSet | Select Name,PasswordLastSet
This command shows the exact date and time when the specified user's password was last changed.
Create an AD user password age script to identify users with passwords older than 90 days.
Get-ADUser -Filter {PasswordLastSet -lt (Get-Date).AddDays(-90)} -Properties PasswordLastSet | Select Name,PasswordLastSet
This PowerShell last password change query helps identify security risks from stale passwords.
Focus security reviews on active accounts by filtering for enabled users only.
Get-ADUser -Filter {Enabled -eq $true} -Properties PasswordLastSet,PasswordNeverExpires | Select Name,PasswordLastSet,PasswordNeverExpires
This command helps identify active accounts that may need password policy enforcement.
| Parameters | Description |
|---|---|
| -Identity | Specifies an AD user object by distinguished name, GUID, security identifier, or SAM account name. |
| -Filter | Specifies a query string using PowerShell Expression Language to retrieve multiple objects based on PasswordLastSet. |
| -SearchBase | Specifies the AD path to search for users with specific password change dates. |
| -Properties | Must include PasswordLastSet to retrieve password change information (not returned by default). |
| -LDAPFilter | Uses LDAP syntax for complex date-based password queries. |
| -ResultPageSize | Controls pagination when retrieving large numbers of users for password reports. |
Get-ADUser -Filter * -Properties PasswordLastSet |
Select Name,@{N='PasswordLastSet';E={if($_.PasswordLastSet){$_.PasswordLastSet}else{'Never Set'}}}
Get-ADUser -Identity "user" -Properties PasswordLastSet |
Select Name,@{N='LastSet';E={[DateTime]::FromFileTime($_.PasswordLastSet)}}
$maxPwdAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
Get-ADUser -Filter * -Properties PasswordLastSet |
Select Name,@{N='ExpiresOn';E={$_.PasswordLastSet.AddDays($maxPwdAge)}}
Get-ADUser -Identity "user" -Properties PasswordLastSet,msDS-ResultantPSO |
Select Name,PasswordLastSet,"msDS-ResultantPSO"
PasswordLastSet is stored in Active Directory as a FileTime value (64-bit integer representing 100-nanosecond intervals since January 1, 1601). You need to convert it to a readable DateTime format:
Incorrect: Shows raw FileTime value
Get-ADUser -Identity "john.doe" -Properties PasswordLastSet | Select PasswordLastSet
Correct: Converts to readable date
Get-ADUser -Identity "john.doe" -Properties PasswordLastSet |
Select Name, @{Name='PasswordLastSet';Expression={[DateTime]::FromFileTime($_.PasswordLastSet)}}
For PowerShell 3.0 and above, the PasswordLastSet property is automatically converted to DateTime when using -Properties parameter correctly.
To create a PowerShell password expiration report, you need to combine the PasswordLastSet date with your domain's MaxPasswordAge policy:
To get the domain password policy:
$maxPwdAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
To calculate expiration for all users:
Get-ADUser -Filter {Enabled -eq $true} -Properties PasswordLastSet, PasswordNeverExpires |
Select Name, PasswordLastSet,
@{Name='PasswordExpires';Expression={
if($_.PasswordNeverExpires -or $_.PasswordLastSet -eq $null) {
"Never"
} else {
$_.PasswordLastSet.AddDays($maxPwdAge.Days)
}
}},
@{Name='DaysUntilExpiry';Expression={
if($_.PasswordNeverExpires -or $_.PasswordLastSet -eq $null) {
"N/A"
} else {
(New-TimeSpan -Start (Get-Date) -End $_.PasswordLastSet.AddDays($maxPwdAge.Days)).Days
}
}} | Sort DaysUntilExpiry
Some accounts may have null PasswordLastSet values, indicating the password was never set or the account was created with User must change password at next logon option.
To find users with no password set:
Get-ADUser -Filter * -Properties PasswordLastSet |
Where-Object {$_.PasswordLastSet -eq $null} |
Select Name, SamAccountName, Enabled
To include proper handling in reports:
Get-ADUser -Filter * -Properties PasswordLastSet, whenCreated |
Select Name,
@{Name='PasswordStatus';Expression={
if($_.PasswordLastSet -eq $null) {
"Never Set (Created: $($_.whenCreated))"
} else {
"Last Changed: $($_.PasswordLastSet)"
}
}}
FGPPs override the default domain policy. You need to check for applicable PSOs.
To check if user has a specific PSO applied:
Get-ADUser -Identity "john.doe" -Properties PasswordLastSet, "msDS-ResultantPSO", "msDS-UserPasswordExpiryTimeComputed" |
Select Name, PasswordLastSet,
@{Name='PasswordPolicy';Expression={
if($_."msDS-ResultantPSO") {
(Get-ADObject $_."msDS-ResultantPSO" -Properties name).name
} else {
"Default Domain Policy"
}
}},
@{Name='ExpiryDate';Expression={
[DateTime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")
}}
To get all users with FGPPs and their expiration dates:
Get-ADUser -Filter * -Properties PasswordLastSet, "msDS-UserPasswordExpiryTimeComputed", "msDS-ResultantPSO" |
Where-Object {$_."msDS-ResultantPSO" -ne $null} |
Select Name, PasswordLastSet,
@{Name='ExpiryDate';Expression={[DateTime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}}
This discrepancy usually occurs due to time zone differences or how the tools calculate password age.
To ensure consistent time zone handling:
Get-ADUser -Identity "john.doe" -Properties PasswordLastSet |
Select Name,
@{Name='PasswordLastSet (UTC)';Expression={$_.PasswordLastSet.ToUniversalTime()}},
@{Name='PasswordLastSet (Local)';Expression={$_.PasswordLastSet.ToLocalTime()}},
@{Name='Password Age (Days)';Expression={
[Math]::Round(((Get-Date) - $_.PasswordLastSet).TotalDays, 2)
}}
For precise matching with Active Directory Users and Computers, use the same calculation method:
Get-ADUser -Identity "john.doe" -Properties PasswordLastSet, pwdLastSet |
Select Name,
@{Name='PasswordLastSet';Expression={$_.PasswordLastSet}},
@{Name='Exact Age';Expression={
$now = [DateTime]::Now
$age = $now - $_.PasswordLastSet
"$($age.Days) days, $($age.Hours) hours, $($age.Minutes) minutes"
}}
Create comprehensive AD user password age scripts for security audits across multiple organizational units.
To define target OUs:
$OUs = @(
"OU=Sales,DC=contoso,DC=com",
"OU=IT,DC=contoso,DC=com",
"OU=Finance,DC=contoso,DC=com"
)
To generate a comprehensive password audit report:
$Report = foreach ($OU in $OUs) {
Get-ADUser -Filter {Enabled -eq $true} -SearchBase $OU -Properties PasswordLastSet, PasswordNeverExpires,LastLogonDate |
Select @{Name='OU';Expression={$OU.Split(',')[0].Replace('OU=','')}},
Name,
SamAccountName,
@{Name='PasswordLastSet';Expression={
if($_.PasswordLastSet) {$_.PasswordLastSet} else {"Never"}
}},
@{Name='PasswordAge';Expression={
if($_.PasswordLastSet) {
[Math]::Round(((Get-Date) - $_.PasswordLastSet).TotalDays)
} else {"N/A"}
}},
PasswordNeverExpires,
LastLogonDate,
@{Name='Risk Level';Expression={
if($_.PasswordNeverExpires) {"High - Never Expires"}
elseif(-not $_.PasswordLastSet) {"High - Never Set"}
elseif(((Get-Date) - $_.PasswordLastSet).TotalDays -gt 180) {"High - Very Old"}
elseif(((Get-Date) - $_.PasswordLastSet).TotalDays -gt 90) {"Medium - Old"}
else {"Low"}
}}
}
To export the report with a timestamp:
$Report | Export-CSV "C:\PasswordAudit_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" -NoTypeInformation
# Create summary statistics
$Report | Group-Object 'Risk Level' | Select Name, Count
Set up proactive monitoring with a PowerShell last password change script for upcoming expirations.
To get the default domain password policy:
$maxPwdAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
# Find users with passwords expiring within 7 days
$ExpiringUsers = Get-ADUser -Filter {Enabled -eq $true -and PasswordNeverExpires -eq $false} -Properties PasswordLastSet, EmailAddress, Manager |
Where-Object {$_.PasswordLastSet -ne $null} |
Select Name, EmailAddress,
@{Name='Manager';Expression={(Get-ADUser $_.Manager -ErrorAction SilentlyContinue).Name}},
PasswordLastSet,
@{Name='ExpiryDate';Expression={$_.PasswordLastSet.Add($maxPwdAge)}},
@{Name='DaysUntilExpiry';Expression={
($_.PasswordLastSet.Add($maxPwdAge) - (Get-Date)).Days
}} |
Where-Object {$_.DaysUntilExpiry -le 7 -and $_.DaysUntilExpiry -ge 0} |
Sort DaysUntilExpiry
To display the results:
$ExpiringUsers | Format-Table -AutoSize
PowerShell cannot directly access password history, but you can track password change frequency using AdminSDHolder or by comparing PasswordLastSet over time.
To check current password age and estimate change frequency:
Get-ADUser -Filter * -Properties PasswordLastSet, whenCreated |
Where-Object {$_.PasswordLastSet -ne $null} |
Select Name,
@{Name='AccountAge(Days)';Expression={((Get-Date) - $_.whenCreated).Days}},
@{Name='PasswordAge(Days)';Expression={((Get-Date) - $_.PasswordLastSet).Days}},
@{Name='EstimatedChangesPerYear';Expression={
$accountAge = ((Get-Date) - $_.whenCreated).Days
$passwordAge = ((Get-Date) - $_.PasswordLastSet).Days
if($accountAge -gt 365) {
[Math]::Round(365 / $passwordAge, 2)
} else {
"Account less than 1 year old"
}
}} |
Sort 'PasswordAge(Days)' -Descending
For parentheses or other special characters, use single quotes and escape as needed.
Note: For actual password history and detailed change tracking, consider using ADManager Plus, which maintains historical data.