Public/User/Get-ADUserPasswordInfo.ps1

<#
    .SYNOPSIS
    Get the password state of Active Directory users, together with the password policy that actually applies to them.
 
    .DESCRIPTION
    For each user, resolves the effective password policy (the default domain policy, or the Fine Grained Password
    Policy / PSO that wins over it) and reports the password expiration date computed by the domain controller
    through msDS-UserPasswordExpiryTimeComputed, plus the lockout settings and the bad password counters.
 
    The special cases that make a raw msDS-UserPasswordExpiryTimeComputed misleading are handled explicitly:
    a password flagged "must change at next logon" (pwdLastSet = 0), an account with PasswordNeverExpires, a
    domain with no maximum password age, and an account with PasswordNotRequired, which lets the user bypass
    any policy and set an empty password.
 
    Queries the PDC emulator by default: it is the only domain controller holding an up to date value for
    badPwdCount and lockout state.
 
    Requires the ActiveDirectory module (RSAT).
 
    .PARAMETER SamAccountName
    One or more sAMAccountNames. When omitted, every user of the domain is processed.
 
    .PARAMETER Server
    Domain controller to query. Defaults to the PDC emulator of the current domain.
 
    .PARAMETER SimulatedMaxPasswordAgeDays
    Simulates a different maximum password age, in days, and adds SimulatedPasswordExpirationDateUTC and
    SimulatedPasswordExpired to the output. Useful to measure the impact of a policy change before applying it.
 
    .EXAMPLE
    Get-ADUserPasswordInfo
 
    Reports the password state of every user of the domain.
 
    .EXAMPLE
    Get-ADUserPasswordInfo -SamAccountName 'jdoe', 'asmith'
 
    Reports the password state of two users only.
 
    .EXAMPLE
    Get-ADUserPasswordInfo -SimulatedMaxPasswordAgeDays 180 | Where-Object SimulatedPasswordExpired
 
    Lists the users whose password would already be expired if the maximum password age was set to 180 days.
 
    .LINK
    https://github.com/bastienperez/PSADDS
#>

function Get-ADUserPasswordInfo {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string[]]$SamAccountName,

        [Parameter(Mandatory = $false)]
        [Alias('DomainController')]
        [string]$Server,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$SimulatedMaxPasswordAgeDays
    )

    [System.Collections.Generic.List[PSObject]]$passwordSettingsByUser = @()

    if (-not $Server) {
        # the PDC emulator is the only DC holding an up to date badPwdCount and lockout state
        $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
        $Server = $domain.PdcRoleOwner.Name
        Write-Verbose "For accurate results, the domain controller with the PDC emulator role will be used: $Server"
    }

    $defautPasswordPolicyObject = Get-ADDefaultDomainPasswordPolicy -Server $Server
    $defautPasswordPolicyDays = $defautPasswordPolicyObject.MaxPasswordAge.Days

    # Get-ADUserResultantPasswordPolicy is one LDAP call per user: skip it entirely when the domain has no PSO
    $fineGrainedPasswordPolicyExists = [bool](Get-ADFineGrainedPasswordPolicy -Filter * -Server $Server -ErrorAction SilentlyContinue)

    if (-not $fineGrainedPasswordPolicyExists) {
        Write-Verbose 'No Fine Grained Password Policy in this domain, the default domain password policy applies to every user'
    }

    $attributes = 'DisplayName', 'msDS-UserPasswordExpiryTimeComputed', 'PasswordNeverExpires', 'pwdLastSet', 'Enabled', 'badPwdCount', 'badPasswordTime', 'LastLogonDate', 'PasswordNotRequired', 'mail', 'UserPrincipalName'

    if ($SamAccountName) {
        [System.Collections.Generic.List[PSObject]]$users = @()

        foreach ($sam in $SamAccountName) {
            Write-Verbose "Processing user: $sam"
            try {
                $u = Get-ADUser -Identity $sam -Properties $attributes -ErrorAction Stop -Server $Server
            }
            catch {
                Write-Warning "$($_.Exception.Message)"
                return
            }

            $users.Add($u)
        }
    }
    else {
        Write-Verbose 'Processing all users'
        try {
            $users = Get-ADUser -Filter * -Properties $attributes -ErrorAction Stop -Server $Server
        }
        catch {
            Write-Warning "$($_.Exception.Message)"
            return
        }
    }

    $i = 0
    foreach ($user in $users) {
        $i++
        Write-Verbose "Processing user $i/$($users.Count): $($user.SamAccountName)"
        $policy = $null
        $passwordPolicyMaxPasswordAge = $null

        if ($fineGrainedPasswordPolicyExists) {
            Write-Verbose "Getting resultant password policy for $($user.SamAccountName)"
            $fineGrainedPassword = Get-ADUserResultantPasswordPolicy -Identity $user.SamAccountName -Server $Server
        }
        else {
            $fineGrainedPassword = $null
        }

        switch ($fineGrainedPassword.Name) {
            $null {
                $policy = 'GPO or domain settings'
                $passwordPolicyMaxPasswordAge = $defautPasswordPolicyDays
                $lockoutDuration = $defautPasswordPolicyObject.LockoutDuration
                $lockoutObservationWindow = $defautPasswordPolicyObject.LockoutObservationWindow
                $lockoutThreshold = $defautPasswordPolicyObject.LockoutThreshold
                $passwordMinimumLength = $defautPasswordPolicyObject.MinPasswordLength
                $passwordComplexityEnabled = $defautPasswordPolicyObject.ComplexityEnabled
                $passwordHistoryCount = $defautPasswordPolicyObject.PasswordHistoryCount
                break
            }
            default {
                $policy = $fineGrainedPassword.Name + ' (Fine Grained Password)'
                $passwordPolicyMaxPasswordAge = $fineGrainedPassword.MaxPasswordAge.Days
                $lockoutDuration = $fineGrainedPassword.LockoutDuration
                $lockoutObservationWindow = $fineGrainedPassword.LockoutObservationWindow
                $lockoutThreshold = $fineGrainedPassword.LockoutThreshold
                $passwordMinimumLength = $fineGrainedPassword.MinPasswordLength
                $passwordComplexityEnabled = $fineGrainedPassword.ComplexityEnabled
                $passwordHistoryCount = $fineGrainedPassword.PasswordHistoryCount
                break
            }
        }

        if ($user.PasswordNotRequired) {
            $policy = 'None - User has "PasswordNotRequired" flag set. This setting allows a user in AD to bypass any password policy and set a blank password if they want to.'
        }

        if ($user.pwdLastSet -eq 0) {
            $pwdLastSet = $null
        }
        else {
            $convertedDate = [datetime]::FromFileTime($user.pwdLastSet).ToUniversalTime()
            if ($convertedDate -eq [datetime]::new(1601, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)) {
                $pwdLastSet = $null
            }
            else {
                $pwdLastSet = $convertedDate
            }
        }

        if ($user.'msDS-UserPasswordExpiryTimeComputed' -eq 9223372036854775807 -and $user.PasswordNeverExpires -eq $false) {
            $expirationDate = 'Never (no password policy in GPO or never set)'
            $daysLeft = '-'
        }
        elseif ($user.PasswordNeverExpires -and $user.'msDS-UserPasswordExpiryTimeComputed' -ne 0) {
            $expirationDate = "Never (configured as 'Never expires')"
            $daysLeft = '-'
        }
        elseif ($user.'msDS-UserPasswordExpiryTimeComputed' -eq 0) {
            if ($defautPasswordPolicyDays -eq 0) {
                $expirationDate = 'Never (no password policy in GPO)'
            }
            else {
                $expirationDate = "Password is set to be changed at 'next logon' so no way to calculate the password expiration date"
            }

            $daysLeft = '-'
        }
        else {
            $expirationDate = $([datetime]::FromFileTime($user.'msDS-UserPasswordExpiryTimeComputed').ToUniversalTime())

            if ($expirationDate -eq [datetime]::new(1601, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)) {
                $expirationDate = $null
                $daysLeft = '-'
            }
            else {
                $daysLeft = New-TimeSpan (Get-Date).ToUniversalTime() $expirationDate

                if ($daysLeft -le 0 -and $null -ne $daysLeft) {
                    $daysLeft = 'Already expired'
                }
                else {
                    $daysLeft = $daysLeft.Days
                }
            }
        }

        if ($SimulatedMaxPasswordAgeDays -and $user.pwdLastSet -and $pwdLastSet -ne [datetime]::new(1601, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)) {
            # Calculate simulated password expiration if SimulatedMaxPasswordAgeDays is provided
            $simulatedPasswordExpirationDateUTC = $null
            $simulatedPasswordExpired = $false
            $simulatedPasswordExpirationDateUTC = $pwdLastSet.AddDays($SimulatedMaxPasswordAgeDays)
            if ($pwdLastSet -lt (Get-Date).AddDays(-$SimulatedMaxPasswordAgeDays)) {
                $simulatedPasswordExpired = $true
            }
        }

        $object = [PSCustomObject][ordered]@{
            Identity                        = $user.SamAccountName
            DisplayName                     = $user.DisplayName
            Enabled                         = $user.Enabled
            UserPrincipalName               = $user.UserPrincipalName
            Mail                            = $user.mail
            PasswordLastSetUTCTime          = $pwdLastSet
            PasswordPolicy                  = $policy
            PasswordPolicyMaxPasswordAge    = $passwordPolicyMaxPasswordAge
            PasswordMinimumLength           = $passwordMinimumLength
            PasswordHistoryCount            = $passwordHistoryCount
            PasswordComplexityEnabled       = $passwordComplexityEnabled
            PasswordExpirationDateUTC       = $expirationDate
            DaysLeftBeforePasswordChangeUTC = $daysLeft
            PasswordExpired                 = if ($daysLeft -eq 'Already expired') { $true } else { $false }
            LockoutDuration                 = $lockoutDuration
            LockoutObservationWindow        = $lockoutObservationWindow
            LockoutThreshold                = $lockoutThreshold
            LastLogonDate                   = if ($user.LastLogonDate) { $user.LastLogonDate } else { 'Never logged in' }
            BadPwdCount                     = $user.BadPwdCount
            BadPasswordTime                 = if ($user.BadPasswordTime -eq 0 -or [datetime]::FromFileTimeUTC($user.BadPasswordTime) -eq [datetime]::new(1601, 1, 1, 0, 0, 0, [DateTimeKind]::Utc)) { $null } else { [datetime]::FromFileTimeUTC($user.BadPasswordTime) }
            FromDomainController            = $Server
            DistinguishedName               = $user.DistinguishedName
        }

        if ($SimulatedMaxPasswordAgeDays) {
            $object | Add-Member -MemberType NoteProperty -Name 'SimulatedPasswordExpirationDateUTC' -Value $simulatedPasswordExpirationDateUTC
            $object | Add-Member -MemberType NoteProperty -Name 'SimulatedPasswordExpired' -Value $simulatedPasswordExpired
        }

        $passwordSettingsByUser.Add($object)
    }

    $passwordSettingsByUser | Sort-Object PasswordExpirationDate* -Descending
}