Dargslan.AdPasswordAudit.psm1
|
<# .SYNOPSIS Audit Active Directory password and lockout policy, Fine Grained Password Policy and stale / never-expire accounts. Requires the ActiveDirectory module. .DESCRIPTION Part of the Dargslan Windows Admin Tools collection. Free Cheat Sheet: https://dargslan.com/cheat-sheets/ad-password-policy-audit-2026 Full Guide: https://dargslan.com/blog/ad-password-policy-audit-powershell-2026 More tools: https://dargslan.com .LINK https://dargslan.com .LINK https://github.com/Dargslan/powershell-admin-scripts #> $script:Banner = @" +----------------------------------------------------------+ | Dargslan AD Password Policy Audit | https://dargslan.com - Free cheat sheets & eBooks | +----------------------------------------------------------+ "@ function Get-DargslanAdPasswordPolicy { <# .SYNOPSIS Return the default domain password and lockout policy. #> [CmdletBinding()] param() if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { throw 'ActiveDirectory module is required (RSAT-AD-PowerShell).' } Import-Module ActiveDirectory -ErrorAction Stop $p = Get-ADDefaultDomainPasswordPolicy [pscustomobject]@{ Domain = (Get-ADDomain).DNSRoot MinLength = $p.MinPasswordLength Complexity = $p.ComplexityEnabled MaxAgeDays = $p.MaxPasswordAge.Days MinAgeDays = $p.MinPasswordAge.Days HistoryCount = $p.PasswordHistoryCount ReversibleEncryption = $p.ReversibleEncryptionEnabled LockoutThreshold = $p.LockoutThreshold LockoutDurationMinutes = $p.LockoutDuration.TotalMinutes LockoutWindowMinutes = $p.LockoutObservationWindow.TotalMinutes } } function Get-DargslanAdFineGrainedPolicies { <# .SYNOPSIS Return Fine Grained Password Policies (PSO) and the accounts they apply to. #> [CmdletBinding()] param() Import-Module ActiveDirectory -ErrorAction Stop Get-ADFineGrainedPasswordPolicy -Filter * | ForEach-Object { [pscustomobject]@{ Name = $_.Name Precedence = $_.Precedence MinLength = $_.MinPasswordLength Complexity = $_.ComplexityEnabled MaxAgeDays = $_.MaxPasswordAge.Days HistoryCount = $_.PasswordHistoryCount AppliesTo = ($_.AppliesTo -join ', ') } } } function Get-DargslanAdStaleAccounts { <# .SYNOPSIS Find enabled accounts with no logon for N days (default 90). #> [CmdletBinding()] param([int]$Days = 90) Import-Module ActiveDirectory -ErrorAction Stop $cutoff = (Get-Date).AddDays(-$Days) Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate, PasswordLastSet, PasswordNeverExpires | Where-Object { $_.LastLogonDate -lt $cutoff } | Select SamAccountName, LastLogonDate, PasswordLastSet, PasswordNeverExpires } function Get-DargslanAdPasswordAuditReport { <# .SYNOPSIS Combined report with PASS / WARN / FAIL verdict. #> [CmdletBinding()] param([int]$StaleDays = 90) $pol = Get-DargslanAdPasswordPolicy $fgpp = @(Get-DargslanAdFineGrainedPolicies) $stale = @(Get-DargslanAdStaleAccounts -Days $StaleDays) Import-Module ActiveDirectory -ErrorAction Stop $never = @(Get-ADUser -Filter {Enabled -eq $true -and PasswordNeverExpires -eq $true} -Properties PasswordLastSet) $score = 0 if ($pol.MinLength -ge 14) { $score++ } if ($pol.Complexity) { $score++ } if ($pol.LockoutThreshold -gt 0 -and $pol.LockoutThreshold -le 10) { $score++ } if (-not $pol.ReversibleEncryption) { $score++ } if ($never.Count -le 3) { $score++ } $verdict = if ($score -ge 4) { 'PASS' } elseif ($score -ge 2) { 'WARN' } else { 'FAIL' } [pscustomobject]@{ Policy = $pol FineGrained = $fgpp StaleAccounts = $stale NeverExpire = $never | Select SamAccountName, PasswordLastSet StaleCount = $stale.Count NeverExpireCount= $never.Count Score = $score Verdict = $verdict TimeStamp = (Get-Date).ToString('s') } } function Export-DargslanAdPasswordAuditReport { <# .SYNOPSIS Export the AD password audit to HTML and JSON. #> [CmdletBinding()] param([int]$StaleDays = 90, [string]$OutDir = (Join-Path $env:TEMP 'DargslanAdAudit')) if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null } $r = Get-DargslanAdPasswordAuditReport -StaleDays $StaleDays $json = Join-Path $OutDir 'ad-password-audit.json' $html = Join-Path $OutDir 'ad-password-audit.html' $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8 $body = "<h1>AD Password Audit - $($r.Policy.Domain)</h1>" $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/5)</p>" $body += '<h2>Default Policy</h2>' + ($r.Policy | ConvertTo-Html -Fragment) $body += '<h2>FGPP</h2>' + ($r.FineGrained | ConvertTo-Html -Fragment) $body += '<h2>Never-expire accounts</h2>' + ($r.NeverExpire | ConvertTo-Html -Fragment) $body += "<h2>Stale ($StaleDays+ days) </h2>" + ($r.StaleAccounts | ConvertTo-Html -Fragment) ConvertTo-Html -Body $body -Title 'AD Password Audit' | Set-Content $html -Encoding UTF8 [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict } } |