MyModule PowerShell · Active Directory Identity Reporting

Test-MyUserStatus

Performs a comprehensive account health check for an Active Directory user, evaluating disabled state, lockout, expiry, password status, smart card requirement, and access restrictions — with optional interactive credential validation against the domain.

Version 1.5.0
Lines ~260
Module MyModule
File Functions\AD\Test-MyUserStatus.ps1
Depends On Get-MyUser · Get-ADTargetServer

Parameters

Parameter Type Required Description
-Username [string] Yes · Pipeline The identity to evaluate. Accepts SamAccountName, UPN, email address, display name, DistinguishedName, or CanonicalName — same formats as Get-MyUser.
-SkipCredentialTest [switch] Optional Suppresses the interactive password validation prompt entirely. Use when calling from scripts or automation where interactive prompts are not appropriate.
-PreferredSite [string] Optional Prefer a DC in this AD site for the attribute query. Falls back to any available writable DC if the site has no DC for the user's domain.

Check Severity Levels

Critical
Account disabled
Locked out
Account expired
Password expired
Smart card required
Warning
Password expiring within 14 days
No logon in 90+ days
Password never expires
Must change at next logon
Restriction
Workstation restrictions
Logon hour restrictions
Cannot change password
OK
Account enabled
Not locked out
Password healthy
Recent logon recorded

Output — PSCustomObject

SamAccountName[string]
UserPrincipalName[string]
DisplayName[string]
Domain[string]
ResolvedDC[string]
Enabled[bool]
LockedOut[bool]
AccountExpirationDate[datetime]
PasswordLastSet[datetime]
PasswordNeverExpires[bool]
PasswordExpired[bool]
MustChangePassword[bool]
SmartcardRequired[bool]
WorkstationRestricted[bool]
LogonHoursRestricted[bool]
CannotChangePassword[bool]
LastLogonDate[datetime]
HasCriticalIssues[bool]
CredentialResult[string]
Checks[List[PSCustomObject]]
Credential validation: The interactive password prompt is only offered when no Critical severity checks are present. When critical issues exist, validation is automatically skipped to avoid misleading results — a locked or disabled account will fail authentication regardless of password correctness.
logonHours detection: The 21-byte logonHours array is evaluated by checking for any byte not equal to 0xFF. All-0xFF means unrestricted. Any restricted byte pattern sets severity to Restriction.

Examples

Example 1 — Basic check with credential prompt
PowerShell
Test-MyUserStatus -Username "jdoe"

Resolves jdoe via Get-MyUser, runs all checks, prints the status report, then prompts to validate credentials if no critical issues are found.

Example 2 — Suppress credential prompt (automation)
PowerShell
Test-MyUserStatus -Username "john.doe@child1.corp.local" -SkipCredentialTest

Accepts UPN or email as identity. Skips the interactive prompt — suitable for scheduled tasks, runbooks, or pipeline use.

Example 3 — Bulk check across multiple accounts, export to CSV
PowerShell
"jdoe","asmith","bwilson" |
    Test-MyUserStatus -SkipCredentialTest |
    Export-Csv C:\Reports\AccountStatus.csv -NoTypeInformation

Pipeline-friendly. The Checks array won't serialize cleanly to CSV — use HasCriticalIssues and the flat boolean fields for tabular reporting.

Example 4 — Site-aware DC targeting
PowerShell
Test-MyUserStatus -Username "jdoe" -PreferredSite "Doha-Site" -SkipCredentialTest

Pins DC resolution to the Doha-Site AD site. Falls back to any writable DC in the user's domain if no DC exists in the preferred site.

Full Source

PowerShell · Test-MyUserStatus.ps1
function Test-MyUserStatus {
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Username,

        [Parameter(Mandatory = $false)]
        [switch]$SkipCredentialTest,

        [Parameter(Mandatory = $false)]
        [string]$PreferredSite
    )

    process {

        # ── Step 1: Resolve identity and DC via Get-MyUser ────────────────────
        Write-Verbose "Resolving identity '$Username' via Get-MyUser."

        $resolveParams = @{ Username = $Username }
        if ($PreferredSite) { $resolveParams['PreferredSite'] = $PreferredSite }

        try {
            $adUser = Get-MyUser @resolveParams -ErrorAction Stop
        }
        catch {
            Write-Error "Failed to resolve identity '$Username': $_"
            return
        }

        if (-not $adUser) {
            Write-Warning "User '$Username' not found in the forest."
            return
        }

        $samAccount = $adUser.SamAccountName
        $targetDC   = $adUser.ResolvedDC

        # ── Step 2: Full attribute retrieval from the resolved DC ─────────────
        try {
            $raw = Get-ADUser -Identity $samAccount `
                -Properties AccountExpirationDate,
                            Enabled,
                            LockedOut,
                            pwdLastSet,
                            'msDS-UserPasswordExpiryTimeComputed',
                            PasswordLastSet,
                            PasswordExpired,
                            PasswordNeverExpires,
                            CannotChangePassword,
                            userWorkstations,
                            logonHours,
                            LastLogonDate,
                            SmartcardLogonRequired,
                            UserAccountControl `
                -Server $targetDC `
                -ErrorAction Stop
        }
        catch {
            Write-Error "Failed to retrieve account attributes for '$samAccount' from '$targetDC': $_"
            return
        }

        # ── Step 3: Evaluate account conditions ───────────────────────────────
        $checks      = [System.Collections.Generic.List[PSCustomObject]]::new()
        $currentTime = Get-Date
        $hasCritical = $false

        function Add-Check {
            param([string]$Severity, [string]$Message)
            $checks.Add([PSCustomObject]@{
                Severity = $Severity
                Message  = $Message
            })
        }

        # Account enabled
        if (-not $raw.Enabled) {
            Add-Check 'Critical' 'Account is disabled.'
            $hasCritical = $true
        } else { Add-Check 'OK' 'Account is enabled.' }

        # Lockout
        if ($raw.LockedOut) {
            Add-Check 'Critical' 'Account is locked out.'
            $hasCritical = $true
        } else { Add-Check 'OK' 'Account is not locked out.' }

        # Account expiration
        if ($raw.AccountExpirationDate -and $currentTime -gt $raw.AccountExpirationDate) {
            Add-Check 'Critical' "Account expired on $($raw.AccountExpirationDate.ToString('yyyy-MM-dd HH:mm'))."
            $hasCritical = $true
        } elseif ($raw.AccountExpirationDate -and
                $raw.AccountExpirationDate -gt $currentTime -and
                $raw.AccountExpirationDate -lt $currentTime.AddDays(14)) {
            Add-Check 'Warning' "Account expires soon: $($raw.AccountExpirationDate.ToString('yyyy-MM-dd HH:mm'))."
        } elseif (-not $raw.AccountExpirationDate) {
            Add-Check 'OK' 'Account has no expiration date.'
        } else {
            Add-Check 'OK' "Account expires: $($raw.AccountExpirationDate.ToString('yyyy-MM-dd HH:mm'))."
        }

        # Must change password at next logon
        if ($raw.pwdLastSet -eq 0) {
            Add-Check 'Warning' 'User must change password at next logon.'
        }

        # Password expiry
        if ($raw.PasswordNeverExpires) {
            Add-Check 'Warning' 'Password is set to never expire.'
        } elseif ($raw.pwdLastSet -ne 0) {
            $expiryRaw = $raw.'msDS-UserPasswordExpiryTimeComputed'
            if ($expiryRaw -ne [long]::MaxValue -and $expiryRaw -ne 0) {
                $passwordExpiry = [datetime]::FromFileTime($expiryRaw)
                if ($currentTime -ge $passwordExpiry) {
                    Add-Check 'Critical' "Password expired on $($passwordExpiry.ToString('yyyy-MM-dd HH:mm'))."
                    $hasCritical = $true
                } elseif ($passwordExpiry -lt $currentTime.AddDays(14)) {
                    Add-Check 'Warning' "Password expires soon: $($passwordExpiry.ToString('yyyy-MM-dd HH:mm'))."
                } else {
                    Add-Check 'OK' "Password expires: $($passwordExpiry.ToString('yyyy-MM-dd HH:mm'))."
                }
            }
        }

        # Smart card required
        if (($raw.UserAccountControl -band 0x40000) -eq 0x40000) {
            Add-Check 'Critical' 'Smart card is required for logon.'
            $hasCritical = $true
        }

        # Workstation restrictions
        if ($raw.userWorkstations) {
            Add-Check 'Restriction' "Logon restricted to workstations: $($raw.userWorkstations)."
        }

        # Logon hour restrictions (21-byte array; all 0xFF = unrestricted)
        if ($raw.logonHours) {
            $restricted = ($raw.logonHours | Where-Object { $_ -ne 0xFF }).Count -gt 0
            if ($restricted) { Add-Check 'Restriction' 'Logon hours are restricted.' }
        }

        # Cannot change password
        if ($raw.CannotChangePassword) {
            Add-Check 'Restriction' 'User cannot change their own password.'
        }

        # Last logon recency
        if (-not $raw.LastLogonDate) {
            Add-Check 'Warning' 'No logon date recorded — account may never have logged on.'
        } elseif ($raw.LastLogonDate -lt $currentTime.AddDays(-90)) {
            Add-Check 'Warning' "No recent logon — last logon: $($raw.LastLogonDate.ToString('yyyy-MM-dd HH:mm'))."
        } else {
            Add-Check 'OK' "Last logon: $($raw.LastLogonDate.ToString('yyyy-MM-dd HH:mm'))."
        }

        # ── Step 4: Display status report ─────────────────────────────────────
        Write-Host ''
        Write-Host '────────────────────────────────────────────────────────' -ForegroundColor DarkCyan
        Write-Host ' Account Status Report' -ForegroundColor Cyan
        Write-Host '────────────────────────────────────────────────────────' -ForegroundColor DarkCyan
        Write-Host " User        : $($adUser.DisplayName)  ($samAccount)" -ForegroundColor Green
        Write-Host " UPN         : $($adUser.UserPrincipalName)" -ForegroundColor Green
        Write-Host " Domain      : $($adUser.Domain)" -ForegroundColor Green
        Write-Host " Resolved DC : $targetDC" -ForegroundColor DarkGray
        Write-Host ''

        foreach ($check in $checks) {
            switch ($check.Severity) {
                'Critical'    { Write-Host "  [CRITICAL]    $($check.Message)" -ForegroundColor Red    }
                'Warning'     { Write-Host "  [WARNING]     $($check.Message)" -ForegroundColor Yellow }
                'Restriction' { Write-Host "  [RESTRICTION] $($check.Message)" -ForegroundColor Cyan   }
                'OK'          { Write-Host "  [OK]          $($check.Message)" -ForegroundColor DarkGray }
            }
        }
        Write-Host ''

        if ($hasCritical) {
            Write-Host ' ✖  Critical issues detected.' -ForegroundColor Red
        } else {
            Write-Host ' ✔  No critical issues detected.' -ForegroundColor Green
        }
        Write-Host '────────────────────────────────────────────────────────' -ForegroundColor DarkCyan
        Write-Host ''

        # ── Step 5: Optional credential validation ────────────────────────────
        $credentialResult = 'NotTested'

        if (-not $hasCritical -and -not $SkipCredentialTest) {
            $testCred = (Read-Host "Validate credentials for '$samAccount'? (y/n)").ToLower().Trim()
            if ($testCred -eq 'y') {
                $securePassword = Read-Host "Enter password for $samAccount" -AsSecureString
                Add-Type -AssemblyName System.DirectoryServices.AccountManagement
                $bstr = [IntPtr]::Zero
                $context = $null
                try {
                    $bstr          = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword)
                    $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
                    $context       = New-Object System.DirectoryServices.AccountManagement.PrincipalContext(
                        [System.DirectoryServices.AccountManagement.ContextType]::Domain,
                        $adUser.Domain
                    )
                    if ($context.ValidateCredentials($samAccount, $plainPassword)) {
                        Write-Host '  [SUCCESS] Credentials are valid.' -ForegroundColor Green
                        $credentialResult = 'Valid'
                    } else {
                        Write-Host '  [FAILURE] Authentication failed — incorrect password.' -ForegroundColor Red
                        $credentialResult = 'Invalid'
                    }
                }
                catch { Write-Error "Credential validation error: $_"; $credentialResult = 'Error' }
                finally {
                    if ($context) { $context.Dispose() }
                    if ($bstr -ne [IntPtr]::Zero) {
                        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
                    }
                    Remove-Variable plainPassword -ErrorAction SilentlyContinue
                }
            }
        }

        # ── Step 6: Return structured output to pipeline ──────────────────────
        [PSCustomObject]@{
            SamAccountName        = $samAccount
            UserPrincipalName     = $adUser.UserPrincipalName
            DisplayName           = $adUser.DisplayName
            Domain                = $adUser.Domain
            ResolvedDC            = $targetDC
            Enabled               = $raw.Enabled
            LockedOut             = $raw.LockedOut
            AccountExpirationDate = $raw.AccountExpirationDate
            PasswordLastSet       = $raw.PasswordLastSet
            PasswordNeverExpires  = $raw.PasswordNeverExpires
            PasswordExpired       = $raw.PasswordExpired
            MustChangePassword    = ($raw.pwdLastSet -eq 0)
            SmartcardRequired     = (($raw.UserAccountControl -band 0x40000) -eq 0x40000)
            WorkstationRestricted = [bool]$raw.userWorkstations
            LogonHoursRestricted  = ($raw.logonHours -and ($raw.logonHours | Where-Object { $_ -ne 0xFF }).Count -gt 0)
            CannotChangePassword  = $raw.CannotChangePassword
            LastLogonDate         = $raw.LastLogonDate
            HasCriticalIssues     = $hasCritical
            CredentialResult      = $credentialResult
            Checks                = $checks
        }
    }
}