Security requirements are obviously a large player in the IT landscape. One of the biggest topics in that realm are passwords. While us IT dreamers have hopes of a password-less MFA covered landscape, most organizations have yet to embrace that. For most organizations account passwords are supposed to be subjected to certain expiration and change requirements. But, just because a written policy is in place or even a literally policy implemented, how do you know if it’s actually working? Afterall, most password policies are all too easily defeated with a simple check of the password never expires box.
This question was brought to me in the perspective of Local AD, so I made a PowerShell Script to answer the question.
With this, you can quickly answer questions like…
- What accounts are set to never expire?
- What is the average time since passwords were last changed?
- Are there any accounts that have not had the password changed in X days?
- When was the last time a password was set on X account?
- How many days ago was a password last set on X account?
- Who has a password that is currently expired?
- Are those accounts actually enabled currently?
Requirements:
- (Recommended) Visual Studio Code with the PowerShell extension.
- An account with AD Read access.
- Direct line of sight (network connection) to a Domain Controller.
- Active Directory PowerShell Modules (Install RSAT)
The Script:
# set default domain
$PSDefaultParameterValues = @{"*-AD*:Server"='YOURDOMAIN.local'}
#Get all accounts
$UserList = Get-ADuser -Filter {Enabled -eq $True} -Properties SamAccountName, Name, Enabled, PasswordExpired, PasswordLastSet, PasswordNeverExpires | select-object SamAccountName, Name, Enabled, PasswordExpired, PasswordLastSet, PasswordNeverExpires
#If wishing to target accounts which match a certain string, set the string below using wildcards and use this alternate search.
#$myVar = "a-*"
#$UserList = Get-ADuser -Filter {Enabled -eq $True -and name -like $myVar} -Properties SamAccountName, Name, Enabled, PasswordExpired, PasswordLastSet, PasswordNeverExpires | select-object SamAccountName, Name, Enabled, PasswordExpired, PasswordLastSet, PasswordNeverExpires
#Get date
$date = Get-Date
#Count
$Count = 0
$UserList | ForEach-Object -Process {
#Track our user count
$Count = $Count + 1
#Time Span and date handling
$ts = $null
$PwLastSet = $null
if ($_.PasswordLastSet -ne $null) {
$ts = New-TimeSpan -Start $_.PasswordLastSet -End $date
$ts = $ts.days
#Password last set
$PwLastSet = $_.PasswordLastSet
} else {
$ts = "Never"
$PwLastSet = "Never"
}
#Add fields to original array
write-host "Count - $($Count)"
$_ | Add-Member -NotePropertyName "Password Expired" -NotePropertyValue $_.PasswordExpired
$_ | Add-Member -NotePropertyName "Password last changed on" -NotePropertyValue $PwLastSet
$_ | Add-Member -NotePropertyName "Password never expires" -NotePropertyValue $_.PasswordNeverExpires
$_ | Add-Member -NotePropertyName "Days since last changed" -NotePropertyValue $ts
} #End loop
$UserList | Select-Object "SamAccountName","Name","Enabled","Password Expired","Password never expires","Password last changed on","Days since last changed" | Export-Csv -path C:\temp\User-info.csv -NoTypeInformation
It’s been written with AAD devices in mind. This line…
$PSDefaultParameterValues = @{"*-AD*:Server"='YOURDOMAIN.local'}
…allows you to configure your local domain name such that the script can be ran from an AAD device, assuming the account logged into that AAD device is also in Local AD and has permission to it.
How does it work?
By default, the script pulls the Account Name, Name, Enabled status, Password Expired status, Password Last Set date, and Password Never Expires status for every single account.
If you wish to narrow this, you can use the $myvar line near the top to add in a filter for accounts with a name that match only a certain string. For instance, if your admin accounts start with “A-” you could do “A-*”.
That data is then processed further to determine the days since the password was last changed and that information is added to the array.
Finally, it exports all this information to a CSV in C:\Temp. Feel free to modify the export location. I then typically copy that to a proper XLSX file for massaging into something more human readable.
The script only performs once single AD query making it very fast, as opposed to having to pull this information in individual queries for each account. It takes less than 60 seconds for my machine to run that query and then process the range for each account on around 9,000 accounts. The script does include a counter so you can see how it progresses through processing each account.
Disclaimer:
The following is the disclaimer that applies to all scripts, functions, one-liners, setup examples, documentation, etc. This disclaimer supersedes any disclaimer included in any script, function, one-liner, article, post, etc.
You running this script/function or following the setup example(s) means you will not blame the author(s) if this breaks your stuff. This script/function/setup-example is provided AS IS without warranty of any kind. Author(s) disclaim all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall author(s) be held liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the script or documentation. Neither this script/function/example/documentation, nor any part of it other than those parts that are explicitly copied from others, may be republished without author(s) express written permission. Author(s) retain the right to alter this disclaimer at any time.
It is entirely up to you and/or your business to understand and evaluate the full direct and indirect consequences of using one of these examples or following this documentation.
The latest version of this disclaimer can be found at: https://azuretothemax.net/disclaimer/
