Skip to content

Commit 1627645

Browse files
committed
v1.48.0 — GPOAudit + MemoryAudit CLI actions
New CLI actions: - GPOAudit: applied Group Policy inventory from registry, gpresult query - MemoryAudit: RAM utilization, DIMM inventory, page file health 65 modules, 57134 lines, 3368 tests, 0 PSSA errors
1 parent e15bcb6 commit 1627645

8 files changed

Lines changed: 283 additions & 7 deletions

File tree

Changelog.md

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

3+
## v1.48.0
4+
5+
- **New Feature:** GPOAudit CLI action — `RackStack.exe -Action GPOAudit -OutputFormat JSON` inventories applied Group Policies from the GP History registry. Reports machine and user policies with display names, deduplicates entries, and queries last gpupdate time. Works on both domain-joined and standalone systems (50-EntryPoint).
6+
- **New Feature:** MemoryAudit CLI action — `RackStack.exe -Action MemoryAudit -OutputFormat JSON` audits physical memory configuration. Reports total/used/available RAM with utilization percentage, enumerates DIMM slots with capacity, speed, and manufacturer. Checks page file utilization. Flags RAM usage over 90% and page file over 80%. Exits code 1 when memory issues detected (50-EntryPoint).
7+
- 65 modules, 3368 tests
8+
39
## v1.47.0
410

511
- **New Feature:** TimeAudit CLI action — `RackStack.exe -Action TimeAudit -OutputFormat JSON` audits time synchronization configuration. Checks W32Time service status, NTP source and type, last sync time, and measures time drift against time.windows.com. Flags drift over 1s as warning, over 5s as critical. Exits code 1 when time issues detected (50-EntryPoint).

Header.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
7h3 4b1d3r
3131
3232
.VERSION
33-
1.47.0
33+
1.48.0
3434
3535
.LAST UPDATED
3636
03/13/2026
@@ -1390,7 +1390,7 @@
13901390
param(
13911391
# CLI headless mode: run a specific action without interactive menus
13921392
# Usage: RackStack.exe -Action Cleanup [-Tier Standard] [-Silent] [-OutputFormat JSON]
1393-
[ValidateSet('Cleanup', 'Debloat', 'HealthCheck', 'Batch', 'QuickScan', 'Inventory', 'DriftCheck', 'Snapshot', 'Compliance', 'Harden', 'Remediate', 'Aggregate', 'Compare', 'Export', 'Trend', 'CertCheck', 'ReportHTML', 'ListeningPorts', 'SoftwareList', 'Uptime', 'ServiceAudit', 'EventAudit', 'NetInfo', 'ScheduledExport', 'ValidateConfig', 'Watch', 'Query', 'Diff', 'Baseline', 'Alert', 'FleetScan', 'PatchStatus', 'UserAudit', 'FirewallAudit', 'TaskAudit', 'DiskAudit', 'TLSAudit', 'SMBAudit', 'DriverAudit', 'TimeAudit', 'BootAudit')]
1393+
[ValidateSet('Cleanup', 'Debloat', 'HealthCheck', 'Batch', 'QuickScan', 'Inventory', 'DriftCheck', 'Snapshot', 'Compliance', 'Harden', 'Remediate', 'Aggregate', 'Compare', 'Export', 'Trend', 'CertCheck', 'ReportHTML', 'ListeningPorts', 'SoftwareList', 'Uptime', 'ServiceAudit', 'EventAudit', 'NetInfo', 'ScheduledExport', 'ValidateConfig', 'Watch', 'Query', 'Diff', 'Baseline', 'Alert', 'FleetScan', 'PatchStatus', 'UserAudit', 'FirewallAudit', 'TaskAudit', 'DiskAudit', 'TLSAudit', 'SMBAudit', 'DriverAudit', 'TimeAudit', 'BootAudit', 'GPOAudit', 'MemoryAudit')]
13941394
[string]$Action,
13951395

13961396
[ValidateSet('Light', 'Standard', 'Aggressive')]

Install-RackStack.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
#>
3737

3838
param(
39-
[ValidateSet('Cleanup', 'Debloat', 'HealthCheck', 'Batch', 'QuickScan', 'Inventory', 'DriftCheck', 'Snapshot', 'Compliance', 'Harden', 'Remediate', 'Aggregate', 'Compare', 'Export', 'Trend', 'CertCheck', 'ReportHTML', 'ListeningPorts', 'SoftwareList', 'Uptime', 'ServiceAudit', 'EventAudit', 'NetInfo', 'ScheduledExport', 'ValidateConfig', 'Watch', 'Query', 'Diff', 'Baseline', 'Alert', 'FleetScan', 'PatchStatus', 'UserAudit', 'FirewallAudit', 'TaskAudit', 'DiskAudit', 'TLSAudit', 'SMBAudit', 'DriverAudit', 'TimeAudit', 'BootAudit')]
39+
[ValidateSet('Cleanup', 'Debloat', 'HealthCheck', 'Batch', 'QuickScan', 'Inventory', 'DriftCheck', 'Snapshot', 'Compliance', 'Harden', 'Remediate', 'Aggregate', 'Compare', 'Export', 'Trend', 'CertCheck', 'ReportHTML', 'ListeningPorts', 'SoftwareList', 'Uptime', 'ServiceAudit', 'EventAudit', 'NetInfo', 'ScheduledExport', 'ValidateConfig', 'Watch', 'Query', 'Diff', 'Baseline', 'Alert', 'FleetScan', 'PatchStatus', 'UserAudit', 'FirewallAudit', 'TaskAudit', 'DiskAudit', 'TLSAudit', 'SMBAudit', 'DriverAudit', 'TimeAudit', 'BootAudit', 'GPOAudit', 'MemoryAudit')]
4040
[string]$Action = 'QuickScan',
4141

4242
[ValidateSet('Light', 'Standard', 'Aggressive')]

Modules/00-Initialization.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) {
160160
if (-not $script:ModuleRoot -and $script:ScriptPath) {
161161
$script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath)
162162
}
163-
$script:ScriptVersion = "1.47.0"
163+
$script:ScriptVersion = "1.48.0"
164164
$script:ScriptStartTime = Get-Date
165165

166166
# CLI headless mode parameters (populated from param block in monolithic/exe)

Modules/50-EntryPoint.ps1

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3604,6 +3604,244 @@ function Invoke-CLIAction {
36043604

36053605
if ($bootIssues -gt 0) { [Environment]::Exit(1) }
36063606
}
3607+
'GPOAudit' {
3608+
Write-OutputColor " Auditing Group Policy configuration..." -color "Info"
3609+
Write-OutputColor "" -color "Info"
3610+
3611+
$gpoIssues = 0
3612+
$gpoEntries = @()
3613+
3614+
# Check if domain-joined
3615+
$isDomainJoined = $false
3616+
try {
3617+
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
3618+
$isDomainJoined = $cs.PartOfDomain -eq $true
3619+
} catch { }
3620+
3621+
if (-not $isDomainJoined) {
3622+
Write-OutputColor " Not domain-joined — checking local policy only." -color "Info"
3623+
}
3624+
3625+
# Query applied GPOs via registry (works on all systems)
3626+
$gpoRegPaths = @(
3627+
@{ Scope = 'Machine'; Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' }
3628+
@{ Scope = 'User'; Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History' }
3629+
)
3630+
3631+
foreach ($gpoReg in $gpoRegPaths) {
3632+
if (Test-Path $gpoReg.Path) {
3633+
try {
3634+
$subkeys = Get-ChildItem -Path $gpoReg.Path -ErrorAction SilentlyContinue
3635+
foreach ($sk in $subkeys) {
3636+
$innerKeys = Get-ChildItem -Path $sk.PSPath -ErrorAction SilentlyContinue
3637+
foreach ($ik in $innerKeys) {
3638+
$props = Get-ItemProperty -Path $ik.PSPath -ErrorAction SilentlyContinue
3639+
if ($null -ne $props -and $null -ne $props.DisplayName) {
3640+
$gpoEntries += @{
3641+
Scope = $gpoReg.Scope
3642+
DisplayName = $props.DisplayName
3643+
GPOName = if ($null -ne $props.GPOName) { $props.GPOName } else { "" }
3644+
Extensions = if ($null -ne $props.Extensions) { $props.Extensions } else { "" }
3645+
Link = if ($null -ne $props.Link) { $props.Link } else { "" }
3646+
Status = 'Applied'
3647+
}
3648+
}
3649+
}
3650+
}
3651+
} catch { }
3652+
}
3653+
}
3654+
3655+
# Deduplicate by DisplayName+Scope
3656+
$uniqueGPOs = @()
3657+
$seen = @{}
3658+
foreach ($g in $gpoEntries) {
3659+
$key = "$($g.Scope):$($g.DisplayName)"
3660+
if (-not $seen.ContainsKey($key)) {
3661+
$seen[$key] = $true
3662+
$uniqueGPOs += $g
3663+
}
3664+
}
3665+
3666+
# Last gpupdate time
3667+
$lastGPUpdate = "Unknown"
3668+
try {
3669+
$gpResult = & gpresult /scope computer /v 2>&1
3670+
$gpStr = $gpResult -join "`n"
3671+
if ($gpStr -match 'Last time Group Policy was applied:\s*(.+)') {
3672+
$regexMatches = $Matches; $lastGPUpdate = $regexMatches[1].Trim()
3673+
}
3674+
} catch { }
3675+
3676+
$machineGPOs = @($uniqueGPOs | Where-Object { $_.Scope -eq 'Machine' })
3677+
$userGPOs = @($uniqueGPOs | Where-Object { $_.Scope -eq 'User' })
3678+
3679+
# Console output
3680+
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
3681+
Write-OutputColor "$(" GPO AUDIT - $env:COMPUTERNAME".PadRight(72))" -color "Info"
3682+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
3683+
Write-OutputColor "$(" Domain Joined: $isDomainJoined Last GP Update: $lastGPUpdate".PadRight(72))" -color "Info"
3684+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
3685+
if (@($machineGPOs).Count -gt 0) {
3686+
Write-OutputColor "$(" MACHINE POLICIES ($(@($machineGPOs).Count))".PadRight(72))" -color "Info"
3687+
foreach ($g in $machineGPOs) {
3688+
$gpoName = if ($g.DisplayName.Length -gt 65) { $g.DisplayName.Substring(0, 62) + "..." } else { $g.DisplayName }
3689+
Write-OutputColor "$(" $gpoName".PadRight(72))" -color "Success"
3690+
}
3691+
}
3692+
if (@($userGPOs).Count -gt 0) {
3693+
Write-OutputColor "$(" USER POLICIES ($(@($userGPOs).Count))".PadRight(72))" -color "Info"
3694+
foreach ($g in $userGPOs) {
3695+
$gpoName = if ($g.DisplayName.Length -gt 65) { $g.DisplayName.Substring(0, 62) + "..." } else { $g.DisplayName }
3696+
Write-OutputColor "$(" $gpoName".PadRight(72))" -color "Success"
3697+
}
3698+
}
3699+
if (@($uniqueGPOs).Count -eq 0) {
3700+
Write-OutputColor "$(" (No applied Group Policies found)".PadRight(72))" -color "Info"
3701+
}
3702+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
3703+
$summaryLine = " Machine: $(@($machineGPOs).Count) User: $(@($userGPOs).Count) Total: $(@($uniqueGPOs).Count)"
3704+
Write-OutputColor "$($summaryLine.PadRight(72))" -color "Success"
3705+
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
3706+
3707+
if ($script:CLIOutputFormat -eq 'JSON') {
3708+
$jsonResult = @{
3709+
Tool = $script:ToolFullName
3710+
Version = $script:ScriptVersion
3711+
Action = 'GPOAudit'
3712+
Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss")
3713+
Hostname = $env:COMPUTERNAME
3714+
Summary = @{
3715+
DomainJoined = $isDomainJoined
3716+
LastGPUpdate = $lastGPUpdate
3717+
MachinePolicies = @($machineGPOs).Count
3718+
UserPolicies = @($userGPOs).Count
3719+
TotalPolicies = @($uniqueGPOs).Count
3720+
Issues = $gpoIssues
3721+
}
3722+
Policies = $uniqueGPOs
3723+
}
3724+
Write-Output ($jsonResult | ConvertTo-Json -Depth 10)
3725+
}
3726+
3727+
if ($gpoIssues -gt 0) { [Environment]::Exit(1) }
3728+
}
3729+
'MemoryAudit' {
3730+
Write-OutputColor " Auditing memory configuration..." -color "Info"
3731+
Write-OutputColor "" -color "Info"
3732+
3733+
$memIssues = 0
3734+
3735+
# Physical memory
3736+
$totalPhysicalGB = 0
3737+
$availableGB = 0
3738+
$usedGB = 0
3739+
$pctUsed = 0
3740+
try {
3741+
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
3742+
$totalPhysicalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
3743+
$availableGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
3744+
$usedGB = [math]::Round($totalPhysicalGB - $availableGB, 1)
3745+
$pctUsed = if ($totalPhysicalGB -gt 0) { [math]::Round(($usedGB / $totalPhysicalGB) * 100, 1) } else { 0 }
3746+
if ($pctUsed -gt 90) { $memIssues++ }
3747+
} catch {
3748+
Write-OutputColor " ERROR: Failed to query memory: $($_.Exception.Message)" -color "Error"
3749+
[Environment]::Exit(1)
3750+
}
3751+
3752+
# Memory DIMMs
3753+
$dimms = @()
3754+
try {
3755+
$physMem = @(Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction Stop)
3756+
foreach ($m in $physMem) {
3757+
$dimms += @{
3758+
BankLabel = "$($m.BankLabel)"
3759+
DeviceLocator = "$($m.DeviceLocator)"
3760+
CapacityGB = [math]::Round($m.Capacity / 1GB, 1)
3761+
Speed = $m.Speed
3762+
Manufacturer = "$($m.Manufacturer)"
3763+
PartNumber = "$($m.PartNumber)".Trim()
3764+
}
3765+
}
3766+
} catch {
3767+
Write-OutputColor " WARNING: Could not enumerate DIMMs: $($_.Exception.Message)" -color "Warning"
3768+
}
3769+
3770+
# Page file
3771+
$pageFiles = @()
3772+
try {
3773+
$pf = @(Get-CimInstance -ClassName Win32_PageFileUsage -ErrorAction Stop)
3774+
foreach ($p in $pf) {
3775+
$pfSizeMB = $p.AllocatedBaseSize
3776+
$pfUsedMB = $p.CurrentUsage
3777+
$pfPctUsed = if ($pfSizeMB -gt 0) { [math]::Round(($pfUsedMB / $pfSizeMB) * 100, 1) } else { 0 }
3778+
$pfStatus = 'OK'
3779+
if ($pfPctUsed -gt 80) { $pfStatus = 'WARN'; $memIssues++ }
3780+
$pageFiles += @{
3781+
Name = $p.Name
3782+
SizeMB = $pfSizeMB
3783+
UsedMB = $pfUsedMB
3784+
PctUsed = $pfPctUsed
3785+
Status = $pfStatus
3786+
}
3787+
}
3788+
} catch { }
3789+
3790+
$memStatus = if ($pctUsed -gt 90) { 'CRITICAL' } elseif ($pctUsed -gt 80) { 'WARN' } else { 'OK' }
3791+
3792+
# Console output
3793+
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
3794+
Write-OutputColor "$(" MEMORY AUDIT - $env:COMPUTERNAME".PadRight(72))" -color "Info"
3795+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
3796+
$memColor = switch ($memStatus) { 'OK' { 'Success' } 'WARN' { 'Warning' } 'CRITICAL' { 'Error' } default { 'Info' } }
3797+
Write-OutputColor "$(" Physical RAM: ${usedGB}/${totalPhysicalGB} GB used (${pctUsed}%) [$memStatus]".PadRight(72))" -color $memColor
3798+
Write-OutputColor "$(" Available: ${availableGB} GB".PadRight(72))" -color "Info"
3799+
Write-OutputColor "$(" DIMMs: $(@($dimms).Count) installed".PadRight(72))" -color "Info"
3800+
if (@($dimms).Count -gt 0) {
3801+
foreach ($d in $dimms) {
3802+
$dimmLine = " $($d.DeviceLocator): $($d.CapacityGB)GB @ $($d.Speed)MHz"
3803+
if ($d.Manufacturer -and $d.Manufacturer -ne 'Unknown') { $dimmLine += " ($($d.Manufacturer))" }
3804+
if ($dimmLine.Length -gt 70) { $dimmLine = $dimmLine.Substring(0, 67) + "..." }
3805+
Write-OutputColor "$($dimmLine.PadRight(72))" -color "Info"
3806+
}
3807+
}
3808+
if (@($pageFiles).Count -gt 0) {
3809+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
3810+
foreach ($pf in $pageFiles) {
3811+
$pfColor = if ($pf.Status -eq 'OK') { 'Success' } else { 'Warning' }
3812+
$pfLine = " Page File: $($pf.Name) $($pf.UsedMB)/$($pf.SizeMB)MB ($($pf.PctUsed)%) [$($pf.Status)]"
3813+
if ($pfLine.Length -gt 72) { $pfLine = $pfLine.Substring(0, 69) + "..." }
3814+
Write-OutputColor "$($pfLine.PadRight(72))" -color $pfColor
3815+
}
3816+
}
3817+
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
3818+
Write-OutputColor "$(" Issues: $memIssues".PadRight(72))" -color $(if ($memIssues -gt 0) { "Warning" } else { "Success" })
3819+
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
3820+
3821+
if ($script:CLIOutputFormat -eq 'JSON') {
3822+
$jsonResult = @{
3823+
Tool = $script:ToolFullName
3824+
Version = $script:ScriptVersion
3825+
Action = 'MemoryAudit'
3826+
Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss")
3827+
Hostname = $env:COMPUTERNAME
3828+
Summary = @{
3829+
TotalGB = $totalPhysicalGB
3830+
UsedGB = $usedGB
3831+
AvailableGB = $availableGB
3832+
PctUsed = $pctUsed
3833+
Status = $memStatus
3834+
DIMMs = @($dimms).Count
3835+
Issues = $memIssues
3836+
}
3837+
DIMMs = $dimms
3838+
PageFiles = $pageFiles
3839+
}
3840+
Write-Output ($jsonResult | ConvertTo-Json -Depth 10)
3841+
}
3842+
3843+
if ($memIssues -gt 0) { [Environment]::Exit(1) }
3844+
}
36073845
default {
36083846
Write-OutputColor " Unknown CLI action: $($script:CLIAction)" -color "Error"
36093847
[Environment]::Exit(1)

0 commit comments

Comments
 (0)