Skip to content

Commit 2449fe9

Browse files
committed
v1.9.64 — Account audit, service dependencies & error handling
1 parent b48a318 commit 2449fe9

13 files changed

Lines changed: 173 additions & 12 deletions

Changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## v1.9.64
4+
5+
- **New Feature:** Local Account Audit — scans all local user accounts and displays password age, last login, password expiry status, and enabled/disabled state. Flags accounts with passwords older than 365 days, expired passwords, and accounts with no login activity in 90+ days. Accessible from Security & Access > option [8] (22-Password, 48-MenuDisplay, 49-MenuRunner).
6+
- **New Feature:** Service Dependency Viewer — shows the full dependency tree for any service in Service Manager. Displays both "depends on" (required services) and "depended on by" (dependent services) with live status indicators. Accessible via option [D] in Service Manager (30-ServiceManager).
7+
- **Bug Fix:** VM RAM validation pre-check uses `-ErrorAction Stop` on `Get-VM` — previously, `-ErrorAction SilentlyContinue` inside a `try/catch` made the catch block unreachable dead code, meaning Hyper-V failures would silently produce an empty VM list instead of being caught (44-VMDeployment).
8+
- **Bug Fix:** Batch config Defender exclusion idempotency check uses `-ErrorAction Stop` on `Get-MpPreference` — previously, `-ErrorAction SilentlyContinue` prevented error detection when Windows Defender is unavailable or the module fails to load (50-EntryPoint).
9+
- **Bug Fix:** HTML report NIC statistics collection uses `-ErrorAction Stop` on `Get-NetAdapterStatistics` — previously, `-ErrorAction SilentlyContinue` inside `try/catch` swallowed all errors silently, producing a report with missing NIC data and no indication of the failure (54-HTMLReports).
10+
- **Bug Fix:** Storage backend auto-detection uses `-ErrorAction Stop` on `Get-ClusterS2D` and `Get-ClusterResource` in 3 locations — previously, `-ErrorAction SilentlyContinue` defeated the `try/catch` error handling, making it impossible to distinguish "cmdlet not available" from "S2D/SMB3 not configured" (59-StorageBackends).
11+
- 63 modules, 1854 tests
12+
313
## v1.9.63
414

515
- **New Feature:** Server readiness dashboard now includes a certificate expiration check — scans `LocalMachine\My` store for expired and soon-to-expire certificates (within 30 days), reports count and soonest expiry date, and counts toward the readiness score (37-HealthCheck).

Header.ps1

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,20 @@
3030
7h3 4b1d3r
3131
3232
.VERSION
33-
1.9.63
33+
1.9.64
3434
3535
.LAST UPDATED
3636
03/03/2026
3737
38+
.CHANGELOG v1.9.64
39+
ACCOUNT AUDIT, DEPENDENCIES & ERROR HANDLING:
40+
- NEW: Local Account Audit — scans all local users showing password age, last login, expiry status, and flags stale/expired accounts (Security & Access menu option 8)
41+
- NEW: Service Dependency Viewer — shows full dependency tree (depends on + depended on by) for any service in Service Manager (option D)
42+
- FIX: VM RAM validation uses -ErrorAction Stop on Get-VM — previously, SilentlyContinue inside try/catch made the catch block unreachable dead code
43+
- FIX: Batch config Defender exclusion check uses -ErrorAction Stop on Get-MpPreference — previously, SilentlyContinue prevented error detection when Defender is unavailable
44+
- FIX: HTML report NIC statistics uses -ErrorAction Stop — previously, SilentlyContinue inside try/catch swallowed errors silently
45+
- FIX: Storage backend auto-detection uses -ErrorAction Stop on Get-ClusterS2D and Get-ClusterResource — 3 instances where SilentlyContinue defeated try/catch error handling
46+
3847
.CHANGELOG v1.9.63
3948
CERTIFICATE CHECKS, READINESS & SAFETY:
4049
- NEW: Server readiness dashboard now checks for expired and soon-to-expire SSL/TLS certificates in LocalMachine\My store

Modules/00-Initialization.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ if (-not $script:ModuleRoot) { $script:ModuleRoot = $PSScriptRoot }
135135
if (-not $script:ModuleRoot -and $script:ScriptPath) {
136136
$script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath)
137137
}
138-
$script:ScriptVersion = "1.9.63"
138+
$script:ScriptVersion = "1.9.64"
139139
$script:ScriptStartTime = Get-Date
140140

141141
# OS version detection (for feature compatibility)

Modules/22-Password.ps1

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,98 @@ function Get-SecurePassword {
162162
Write-OutputColor "Maximum attempts reached." -color "Critical"
163163
return $null
164164
}
165+
# Function to audit local user accounts for password and login status
166+
function Show-LocalAccountAudit {
167+
Clear-Host
168+
Write-OutputColor "" -color "Info"
169+
Write-OutputColor " ╔════════════════════════════════════════════════════════════════════════╗" -color "Info"
170+
Write-OutputColor "$((" LOCAL ACCOUNT AUDIT").PadRight(72))" -color "Info"
171+
Write-OutputColor " ╚════════════════════════════════════════════════════════════════════════╝" -color "Info"
172+
Write-OutputColor "" -color "Info"
173+
174+
try {
175+
$users = @(Get-LocalUser -ErrorAction Stop)
176+
}
177+
catch {
178+
Write-OutputColor " Error retrieving local accounts: $_" -color "Error"
179+
return
180+
}
181+
182+
if ($users.Count -eq 0) {
183+
Write-OutputColor " No local user accounts found." -color "Warning"
184+
return
185+
}
186+
187+
$now = Get-Date
188+
$issues = 0
189+
190+
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
191+
$acctHeader = " LOCAL USER ACCOUNTS ($($users.Count))"
192+
Write-OutputColor "$($acctHeader.PadRight(72))" -color "Info"
193+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
194+
195+
foreach ($user in ($users | Sort-Object Name)) {
196+
$enabled = $user.Enabled
197+
$statusTag = if ($enabled) { "Enabled " } else { "Disabled" }
198+
199+
# Password age
200+
$pwdAge = if ($null -ne $user.PasswordLastSet) {
201+
$days = [math]::Floor(($now - $user.PasswordLastSet).TotalDays)
202+
"${days}d ago"
203+
} else { "Never set" }
204+
205+
# Last logon
206+
$lastLogon = if ($null -ne $user.LastLogon) {
207+
$logonDays = [math]::Floor(($now - $user.LastLogon).TotalDays)
208+
if ($logonDays -eq 0) { "Today" } else { "${logonDays}d ago" }
209+
} else { "Never" }
210+
211+
# Password expiry
212+
$pwdExpires = if ($user.PasswordNeverExpires) {
213+
"Never"
214+
} elseif ($null -ne $user.PasswordLastSet) {
215+
try {
216+
$maxPwdAge = (Get-LocalUser $user.Name -ErrorAction SilentlyContinue).PasswordExpires
217+
if ($null -ne $maxPwdAge) {
218+
$expiryDays = [math]::Floor(($maxPwdAge - $now).TotalDays)
219+
if ($expiryDays -lt 0) { "EXPIRED" } else { "${expiryDays}d" }
220+
} else { "N/A" }
221+
} catch { "N/A" }
222+
} else { "N/A" }
223+
224+
# Determine color based on issues
225+
$color = "Success"
226+
$flags = @()
227+
if (-not $enabled) { $color = "Info" }
228+
if ($null -ne $user.PasswordLastSet) {
229+
$pwdDays = [math]::Floor(($now - $user.PasswordLastSet).TotalDays)
230+
if ($pwdDays -gt 365) { $color = "Error"; $flags += "OLD PWD"; $issues++ }
231+
elseif ($pwdDays -gt 90) { $color = "Warning"; $flags += "AGING" }
232+
}
233+
if ($pwdExpires -eq "EXPIRED") { $color = "Error"; $flags += "EXPIRED"; $issues++ }
234+
if ($lastLogon -eq "Never" -and $enabled) { $flags += "NO LOGIN" }
235+
if ($null -ne $user.LastLogon) {
236+
$logonDays = [math]::Floor(($now - $user.LastLogon).TotalDays)
237+
if ($logonDays -gt 90 -and $enabled) { $flags += "STALE"; $issues++ }
238+
}
239+
240+
$flagStr = if ($flags.Count -gt 0) { " [" + ($flags -join ", ") + "]" } else { "" }
241+
$nameStr = $user.Name
242+
if ($nameStr.Length -gt 20) { $nameStr = $nameStr.Substring(0, 17) + "..." }
243+
$line = " $($statusTag) $($nameStr.PadRight(20)) Pwd: $($pwdAge.PadRight(10)) Login: $($lastLogon.PadRight(8))$flagStr"
244+
if ($line.Length -gt 72) { $line = $line.Substring(0, 72) }
245+
Write-OutputColor "$($line.PadRight(72))" -color $color
246+
}
247+
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
248+
249+
# Summary
250+
Write-OutputColor "" -color "Info"
251+
if ($issues -gt 0) {
252+
Write-OutputColor " $issues issue(s) found — review flagged accounts above." -color "Warning"
253+
} else {
254+
Write-OutputColor " All accounts look healthy." -color "Success"
255+
}
256+
257+
Add-SessionChange -Category "Security" -Description "Ran local account audit ($($users.Count) accounts, $issues issues)"
258+
}
165259
#endregion

Modules/30-ServiceManager.ps1

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function Show-ServiceManager {
7777
Write-MenuItem -Text "[R] Restart a Service (enter number)"
7878
Write-MenuItem -Text "[C] Change Startup Type (enter number)"
7979
Write-MenuItem -Text "[A] Search All Services"
80+
Write-MenuItem -Text "[D] View Service Dependencies"
8081
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
8182
Write-OutputColor "" -color "Info"
8283
Write-OutputColor " [B] ◄ Back" -color "Info"
@@ -202,6 +203,51 @@ function Show-ServiceManager {
202203
}
203204
}
204205
}
206+
"^[Dd]$" {
207+
$num = Read-Host " Enter service number to view dependencies"
208+
if ($num -match '^\d+$' -and [int]$num -ge 1 -and [int]$num -le $serviceList.Count) {
209+
$svc = $serviceList[[int]$num - 1]
210+
Write-OutputColor "" -color "Info"
211+
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
212+
$treeHeader = " DEPENDENCY TREE: $($svc.DisplayName)"
213+
Write-OutputColor "$($treeHeader.PadRight(72))" -color "Info"
214+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
215+
216+
# Services this one depends on (required by)
217+
$requires = @(Get-Service -Name $svc.Name -RequiredServices -ErrorAction SilentlyContinue)
218+
if ($requires.Count -gt 0) {
219+
$reqHeader = " DEPENDS ON ($($requires.Count)):"
220+
Write-OutputColor "$($reqHeader.PadRight(72))" -color "Info"
221+
foreach ($req in $requires) {
222+
$reqColor = if ($req.Status -eq "Running") { "Success" } else { "Warning" }
223+
$reqLine = " $($req.DisplayName) ($($req.Name)) - $($req.Status)"
224+
if ($reqLine.Length -gt 70) { $reqLine = $reqLine.Substring(0, 67) + "..." }
225+
Write-OutputColor "$($reqLine.PadRight(72))" -color $reqColor
226+
}
227+
} else {
228+
Write-OutputColor "$(" DEPENDS ON: (none)".PadRight(72))" -color "Info"
229+
}
230+
231+
Write-OutputColor "$(' '.PadRight(72))" -color "Info"
232+
233+
# Services that depend on this one
234+
$dependents = @(Get-Service -Name $svc.Name -DependentServices -ErrorAction SilentlyContinue)
235+
if ($dependents.Count -gt 0) {
236+
$depHeader = " DEPENDED ON BY ($($dependents.Count)):"
237+
Write-OutputColor "$($depHeader.PadRight(72))" -color "Info"
238+
foreach ($dep in $dependents) {
239+
$depColor = if ($dep.Status -eq "Running") { "Success" } else { "Warning" }
240+
$depLine = " $($dep.DisplayName) ($($dep.Name)) - $($dep.Status)"
241+
if ($depLine.Length -gt 70) { $depLine = $depLine.Substring(0, 67) + "..." }
242+
Write-OutputColor "$($depLine.PadRight(72))" -color $depColor
243+
}
244+
} else {
245+
Write-OutputColor "$(" DEPENDED ON BY: (none)".PadRight(72))" -color "Info"
246+
}
247+
248+
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
249+
}
250+
}
205251
"^[Bb]$" { return }
206252
}
207253

Modules/44-VMDeployment.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2990,7 +2990,7 @@ function Test-VMDeploymentPreFlight {
29902990
if ($vm.MemoryGB) { $requiredRAMGB += $vm.MemoryGB }
29912991
}
29922992
$runningVMs = @()
2993-
try { $runningVMs = @(Get-VM -ErrorAction SilentlyContinue | Where-Object { $_.State -eq "Running" }) } catch {}
2993+
try { $runningVMs = @(Get-VM -ErrorAction Stop | Where-Object { $_.State -eq "Running" }) } catch { $runningVMs = @() }
29942994
$existingRAMGB = 0
29952995
foreach ($rv in $runningVMs) { $existingRAMGB += [math]::Round($rv.MemoryAssigned / 1GB, 1) }
29962996
$ramStatus = if (($requiredRAMGB + $existingRAMGB) -gt ($totalRAMGB * 0.95)) { "FAIL" }

Modules/48-MenuDisplay.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ function Show-SecurityAccessMenu {
384384
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
385385
Write-MenuItem "[6] Add Local Admin Account"
386386
Write-MenuItem "[7] Disable Built-in Admin" -Status $adminDisplay -StatusColor $adminColor
387+
Write-MenuItem "[8] Local Account Audit"
387388
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
388389
Write-OutputColor "" -color "Info"
389390
Write-OutputColor " [B] ◄ Back to Server Config" -color "Info"

Modules/49-MenuRunner.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,10 @@ function Start-Show-SecurityAccessMenu {
238238
"5" { Set-DefenderExclusions; Write-PressEnter }
239239
"6" { Add-LocalAdminAccount; Write-PressEnter }
240240
"7" { Disable-BuiltInAdminAccount; Write-PressEnter }
241+
"8" { Show-LocalAccountAudit; Write-PressEnter }
241242
"back" { return }
242243
default {
243-
Write-OutputColor "Invalid choice. Please enter 1-7 or B." -color "Error"
244+
Write-OutputColor "Invalid choice. Please enter 1-8 or B." -color "Error"
244245
Start-Sleep -Seconds 2
245246
}
246247
}

Modules/50-EntryPoint.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1297,7 +1297,7 @@ function Start-BatchMode {
12971297
if ($Config.ConfigureDefenderExclusions -and $configType -eq "HOST") {
12981298
# Idempotency: check if exclusion paths are already configured
12991299
$currentExclusions = @()
1300-
try { $currentExclusions = @((Get-MpPreference -ErrorAction SilentlyContinue).ExclusionPath) } catch {}
1300+
try { $currentExclusions = @((Get-MpPreference -ErrorAction Stop).ExclusionPath) } catch { $currentExclusions = @() }
13011301
$allPaths = @($script:DefenderExclusionPaths) + @($script:DefenderCommonVMPaths) | Where-Object { $_ }
13021302
$missingPaths = @($allPaths | Where-Object { $_ -notin $currentExclusions })
13031303

Modules/54-HTMLReports.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,11 +743,11 @@ function Save-PerformanceSnapshot {
743743
# Network bytes
744744
$netInfo = @()
745745
try {
746-
$netStats = Get-NetAdapterStatistics -ErrorAction SilentlyContinue
746+
$netStats = Get-NetAdapterStatistics -ErrorAction Stop
747747
foreach ($ns in $netStats) {
748748
$netInfo += @{ Name = $ns.Name; BytesSent = $ns.SentBytes; BytesReceived = $ns.ReceivedBytes; InErrors = $ns.InErrors; OutErrors = $ns.OutErrors }
749749
}
750-
} catch {}
750+
} catch { $netInfo = @() }
751751

752752
$snapshot = [ordered]@{
753753
Hostname = $hostname

0 commit comments

Comments
 (0)