Public/activedirectory/Get-ADPasswordStatus.ps1

#Requires -Version 5.1

function Get-ADPasswordStatus {
    <#
    .SYNOPSIS
        Audits password status of Active Directory user accounts
 
    .DESCRIPTION
        Scans Active Directory for user accounts with specific password conditions such as
        expired passwords, passwords set to never expire, or accounts that must change
        password at next logon. Results are sorted by password age in descending order.
 
    .PARAMETER Status
        One or more password status filters to apply. Valid values are Expired, NeverExpires,
        MustChange, and All. Defaults to All. Multiple values can be combined.
 
    .PARAMETER SearchBase
        The distinguished name of the OU to search within. If omitted, searches the entire domain.
 
    .PARAMETER Server
        Specifies the Active Directory Domain Services instance to connect to.
 
    .PARAMETER Credential
        Specifies the credentials to use for the Active Directory query.
 
    .EXAMPLE
        Get-ADPasswordStatus
 
        Returns all enabled users with any password status concern.
 
    .EXAMPLE
        Get-ADPasswordStatus -Status 'NeverExpires' -Server 'dc01.contoso.com'
 
        Finds accounts with passwords set to never expire from a specific DC.
 
    .EXAMPLE
        Get-ADPasswordStatus -Status 'Expired', 'MustChange' -SearchBase 'OU=Users,DC=contoso,DC=com'
 
        Finds accounts with expired passwords or must-change flags in a specific OU.
 
    .OUTPUTS
        PSWinOps.ADPasswordStatus
        Returns objects with account identity, password state flags, password age,
        and last set date sorted by oldest password first.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-04-03
        Requires: PowerShell 5.1+ / Windows only
        Requires: ActiveDirectory module
 
    .LINK
        https://github.com/k9fr4n/PSWinOps
 
    .LINK
        https://learn.microsoft.com/en-us/powershell/module/activedirectory/get-aduser
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('Expired', 'NeverExpires', 'MustChange', 'All')]
        [string[]]$Status = 'All',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$SearchBase,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Server,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]$Credential
    )

    begin {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Starting"

        try {
            Import-Module -Name 'ActiveDirectory' -ErrorAction Stop
        }
        catch {
            throw "[$($MyInvocation.MyCommand)] Failed to import ActiveDirectory module: $_"
        }

        $adParams = @{}
        if ($PSBoundParameters.ContainsKey('Server')) {
            $adParams['Server'] = $Server
        }
        if ($PSBoundParameters.ContainsKey('Credential')) {
            $adParams['Credential'] = $Credential
        }

        $searchBaseParam = @{}
        if ($PSBoundParameters.ContainsKey('SearchBase')) {
            $searchBaseParam['SearchBase'] = $SearchBase
        }

        $adProperties = @(
            'PasswordLastSet'
            'PasswordExpired'
            'PasswordNeverExpires'
            'PasswordNotRequired'
            'CannotChangePassword'
            'Enabled'
            'Description'
            'DistinguishedName'
        )
    }

    process {
        try {
            Write-Verbose -Message "[$($MyInvocation.MyCommand)] Querying enabled users with password properties"

            $users = Get-ADUser -Filter "Enabled -eq `$true" -Properties $adProperties @searchBaseParam @adParams -ErrorAction Stop

            $results = [System.Collections.Generic.List[object]]::new()

            foreach ($user in $users) {
                $isExpired = $user.PasswordExpired
                $neverExpires = $user.PasswordNeverExpires
                $mustChange = ($null -eq $user.PasswordLastSet)

                # Apply status filter
                $include = $false
                if ('All' -in $Status) {
                    $include = ($isExpired -or $neverExpires -or $mustChange)
                }
                else {
                    if ('Expired' -in $Status -and $isExpired) { $include = $true }
                    if ('NeverExpires' -in $Status -and $neverExpires) { $include = $true }
                    if ('MustChange' -in $Status -and $mustChange) { $include = $true }
                }

                if ($include) {
                    $passwordAge = if ($user.PasswordLastSet) {
                        [math]::Round(((Get-Date) - $user.PasswordLastSet).TotalDays)
                    }
                    else { $null }

                    $results.Add([PSCustomObject]@{
                        PSTypeName           = 'PSWinOps.ADPasswordStatus'
                        Name                 = $user.Name
                        SamAccountName       = $user.SamAccountName
                        Enabled              = $user.Enabled
                        PasswordExpired      = $isExpired
                        PasswordNeverExpires = $neverExpires
                        MustChangePassword   = $mustChange
                        PasswordNotRequired  = $user.PasswordNotRequired
                        PasswordLastSet      = $user.PasswordLastSet
                        PasswordAgeDays      = $passwordAge
                        Description          = $user.Description
                        DistinguishedName    = $user.DistinguishedName
                        Timestamp            = Get-Date -Format 'o'
                    })
                }
            }

            # Sort by password age descending (oldest first, nulls first)
            $results | Sort-Object -Property @{Expression = { if ($null -eq $_.PasswordAgeDays) { [int]::MaxValue } else { $_.PasswordAgeDays } }; Descending = $true }
        }
        catch {
            Write-Error -Message "[$($MyInvocation.MyCommand)] Search failed: $_"
        }
    }

    end {
        Write-Verbose -Message "[$($MyInvocation.MyCommand)] Completed — found $($results.Count) account(s)"
    }
}