Public/Authentication/Test-PasswordComplexity.ps1
|
function Test-PasswordComplexity { <# .SYNOPSIS Validates password length and complexity against AD policy. .DESCRIPTION Evaluates a SecureString password for minimum length and character category complexity. If a user is provided and a Fine-Grained Password Policy (PSO) applies, that policy is used; otherwise the default domain password policy is used. .PARAMETER Password The password to evaluate as a SecureString. .PARAMETER User Optional user identifier (samAccountName, DN, or UPN) used to resolve the effective password policy (PSO) for that user. .PARAMETER RequiredCategories The minimum number of character categories required when complexity is enabled. Default is 3 (Windows default). .EXAMPLE $secure = Read-Host -AsSecureString "Password" Test-PasswordComplexity -Password $secure .EXAMPLE $secure = ConvertTo-SecureString "P@ssw0rd!" -AsPlainText -Force Test-PasswordComplexity -Password $secure -User "jdoe" .OUTPUTS PSCustomObject with compliance details and policy metadata. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [SecureString] $Password, # Optional: if you pass a user (samAccountName, DN, UPN), # we’ll use Fine-Grained Password Policy (PSO) when present. [Parameter()] [string] $User, # Default Windows complexity rule is 3 of 4 categories. [ValidateRange(1,4)] [int] $RequiredCategories = 3 ) begin { # Helper: get effective policy (PSO if User is provided and has one) function Get-EffectivePasswordPolicy { param([string] $User) if ($User) { try { # Get-ADUserResultantPasswordPolicy returns the effective policy (PSO) if applicable. $pso = Get-ADUserResultantPasswordPolicy -Identity $User -ErrorAction Stop if ($pso) { return [pscustomobject]@{ PolicySource = "PSO" Name = $pso.Name MinPasswordLength = $pso.MinPasswordLength ComplexityEnabled = $pso.ComplexityEnabled } } } catch { # If we can’t resolve PSO, fall back to default domain policy } } $d = Get-ADDefaultDomainPasswordPolicy [pscustomobject]@{ PolicySource = "DefaultDomain" Name = $d.Name MinPasswordLength = $d.MinPasswordLength ComplexityEnabled = $d.ComplexityEnabled } } } process { $policy = Get-EffectivePasswordPolicy -User $User # Convert SecureString -> plaintext (unfortunately needed for regex checks) $plain = (New-Object PSCredential 'x', $Password).GetNetworkCredential().Password try { $lenOk = ($plain.Length -ge $policy.MinPasswordLength) # Character category checks (Unicode-aware-ish: upper/lower relies on .NET char properties) $hasLower = $plain -cmatch '\p{Ll}' $hasUpper = $plain -cmatch '\p{Lu}' $hasDigit = $plain -cmatch '\p{Nd}' # “Special” = not letter or digit $hasSpecial = $plain -cmatch '[^\p{L}\p{Nd}]' $met = @( [pscustomobject]@{ Name = 'Lower'; Met = [bool]$hasLower } [pscustomobject]@{ Name = 'Upper'; Met = [bool]$hasUpper } [pscustomobject]@{ Name = 'Digit'; Met = [bool]$hasDigit } [pscustomobject]@{ Name = 'Special'; Met = [bool]$hasSpecial } ) $categoryCount = ($met | Where-Object Met).Count $complexityOk = if ($policy.ComplexityEnabled) { $categoryCount -ge $RequiredCategories } else { $true } $isCompliant = $lenOk -and $complexityOk [pscustomobject]@{ IsCompliant = $isCompliant PolicySource = $policy.PolicySource PolicyName = $policy.Name MinPasswordLength = $policy.MinPasswordLength ComplexityEnabled = $policy.ComplexityEnabled RequiredCategories = if ($policy.ComplexityEnabled) { $RequiredCategories } else { 0 } CategoryCount = $categoryCount CategoriesMet = ($met | Where-Object Met | Select-Object -ExpandProperty Name) CategoriesMissing = ($met | Where-Object { -not $_.Met } | Select-Object -ExpandProperty Name) Length = $plain.Length LengthOk = $lenOk ComplexityOk = $complexityOk } } finally { # Best-effort reduce lifetime of plaintext in memory $plain = $null } } } |