Dargslan.LocalAdminAudit.psm1
|
<# .SYNOPSIS Audit Windows local users and the Administrators group: members, dormant accounts, Guest state, unknown SIDs. .DESCRIPTION Part of the Dargslan Windows Admin Tools collection. Free Cheat Sheet: https://dargslan.com/cheat-sheets/windows-local-admin-audit-2026 Full Guide: https://dargslan.com/blog/windows-local-admin-audit-powershell-2026 More tools: https://dargslan.com .LINK https://dargslan.com .LINK https://github.com/Dargslan/powershell-admin-scripts #> $script:Banner = @" +----------------------------------------------------------+ | Dargslan Local Admin + User Audit | https://dargslan.com - Free cheat sheets & eBooks | +----------------------------------------------------------+ "@ function Get-DargslanLocalAdmins { <# .SYNOPSIS Return the members of the local Administrators group with SID and source. #> [CmdletBinding()] param() try { $members = Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop } catch { $members = ([ADSI]'WinNT://./Administrators,group').Invoke('Members') | ForEach-Object { [pscustomobject]@{ Name = ([ADSI]$_).Path.Replace('WinNT://',''); ObjectClass = 'Unknown'; PrincipalSource = 'ADSI' } } } $members | ForEach-Object { [pscustomobject]@{ ComputerName = $env:COMPUTERNAME Name = $_.Name ObjectClass = $_.ObjectClass PrincipalSource = $_.PrincipalSource SID = $_.SID } } } function Get-DargslanLocalUsers { <# .SYNOPSIS Enumerate every local user with enabled state and last logon. #> [CmdletBinding()] param() Get-LocalUser | Select Name, Enabled, LastLogon, PasswordRequired, PasswordLastSet, PasswordExpires, Description, SID } function Get-DargslanInactiveLocalAccounts { <# .SYNOPSIS Find enabled local users with no logon for N days (default 90). #> [CmdletBinding()] param([int]$Days = 90) $cut = (Get-Date).AddDays(-$Days) Get-LocalUser | Where-Object { $_.Enabled -and $_.LastLogon -and $_.LastLogon -lt $cut } | Select Name, LastLogon, Description } function Get-DargslanLocalAdminAuditReport { <# .SYNOPSIS Combined report with PASS / WARN / FAIL verdict. #> [CmdletBinding()] param([int]$InactiveDays = 90) $adm = @(Get-DargslanLocalAdmins) $users = @(Get-DargslanLocalUsers) $inact = @(Get-DargslanInactiveLocalAccounts -Days $InactiveDays) $guest = ($users | Where-Object Name -eq 'Guest') $unknownSid = ($adm | Where-Object { $_.Name -match '^S-1-5-' }).Count $score = 0 if ($adm.Count -le 5) { $score++ } if ((-not $guest) -or -not $guest.Enabled) { $score++ } if ($unknownSid -eq 0) { $score++ } if ($inact.Count -eq 0) { $score++ } $verdict = if ($score -eq 4) { 'PASS' } elseif ($score -ge 2) { 'WARN' } else { 'FAIL' } [pscustomobject]@{ ComputerName = $env:COMPUTERNAME Admins = $adm Users = $users Inactive = $inact AdminCount = $adm.Count InactiveCount = $inact.Count UnknownSidAdmins = $unknownSid GuestEnabled = [bool]$guest.Enabled Score = $score Verdict = $verdict TimeStamp = (Get-Date).ToString('s') } } function Export-DargslanLocalAdminAuditReport { <# .SYNOPSIS Export the report to HTML and JSON. #> [CmdletBinding()] param([int]$InactiveDays = 90, [string]$OutDir = (Join-Path $env:TEMP 'DargslanLocalAdminAudit')) if (-not (Test-Path $OutDir)) { New-Item -Type Directory -Path $OutDir | Out-Null } $r = Get-DargslanLocalAdminAuditReport -InactiveDays $InactiveDays $json = Join-Path $OutDir ('localadmin-' + $env:COMPUTERNAME + '.json') $html = Join-Path $OutDir ('localadmin-' + $env:COMPUTERNAME + '.html') $r | ConvertTo-Json -Depth 6 | Set-Content $json -Encoding UTF8 $body = "<h1>Local Admin Audit - $($r.ComputerName)</h1>" $body += "<p>Verdict: <b>$($r.Verdict)</b> ($($r.Score)/4)</p>" $body += '<h2>Administrators</h2>' + ($r.Admins | ConvertTo-Html -Fragment) $body += '<h2>Local Users</h2>' + ($r.Users | ConvertTo-Html -Fragment) $body += '<h2>Inactive</h2>' + ($r.Inactive | ConvertTo-Html -Fragment) ConvertTo-Html -Body $body -Title 'Local Admin Audit' | Set-Content $html -Encoding UTF8 [pscustomobject]@{ Json = $json; Html = $html; Verdict = $r.Verdict } } |