diff --git a/Changelog.md b/Changelog.md index 670dae9..187231c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,16 @@ # Changelog +## v1.114.0 + +Print server cleanup — a new Operations-menu utility for maintaining the local print spooler, plus a read-only CLI action. + +- **`PrintServerAudit`** (read-only) — reports the spooler service state, printer count, total queued jobs, orphaned TCP/IP printer ports (ports no printer references), and unused printer drivers. `-OutputFormat JSON` emits the full posture for fleet auditing; makes no changes. +- Operations Menu gains **[36] Print Server Cleanup** with two maintenance actions: + - **Flush stuck print queue** (one-way) — stops the spooler, purges `%SystemRoot%\System32\spool\PRINTERS`, and restarts it. Discarded jobs cannot be recovered, so it is confirmation-gated and marked ONE-WAY in Dry-Run; the spooler is always restarted even if the purge fails. + - **Remove orphaned printer ports** (reversible) — removes TCP/IP ports that no printer references, capturing each port's host address first so the session undo (and the Dry-Run `Undo` closure) can re-create it. + +Unused drivers are reported for visibility but not auto-removed (driver removal is failure-prone and out of scope). Addition to 35-Utilities (no new module). CLI actions: 196 → 197. + ## v1.113.0 SMB signing + encryption enforcement — two new CLI actions plus an Operations-menu item for hardening the SMB server on this host. diff --git a/Header.ps1 b/Header.ps1 index 28bc95d..4e50aa9 100644 --- a/Header.ps1 +++ b/Header.ps1 @@ -30,7 +30,7 @@ 7h3 4b1d3r .VERSION - 1.113.0 + 1.114.0 .LAST UPDATED 05/23/2026 @@ -1391,7 +1391,7 @@ param( # CLI headless mode: run a specific action without interactive menus # Usage: RackStack.exe -Action Cleanup [-Tier Standard] [-Silent] [-OutputFormat JSON] - [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', 'ProcessAudit', 'BackupAudit', 'ShareAudit', 'DNSAudit', 'PowerAudit', 'RegistryAudit', 'ProfileAudit', 'HyperVAudit', 'NetworkAudit', 'StorageAudit', 'FeatureAudit', 'AutoStartAudit', 'BIOSAudit', 'ClusterAudit', 'AuditPolicyAudit', 'EnvAudit', 'CrashAudit', 'LocalGroupAudit', 'WMIAudit', 'TempAudit', 'UpdatePolicyAudit', 'IISAudit', 'SSHAudit', 'BitLockerAudit', 'PrintAudit', 'CredGuardAudit', 'PortAudit', 'AntivirusAudit', 'DotNetAudit', 'RDPAudit', 'VPNAudit', 'HostsFileAudit', 'NetStatAudit', 'LicenseAudit', 'USBDeviceAudit', 'AppLockerAudit', 'EventSubAudit', 'HotfixAudit', 'SysInfoAudit', 'LogonAudit', 'ACLAudit', 'RecoveryAudit', 'ServiceAccountAudit', 'ProxyAudit', 'PendingRebootAudit', 'PageFileAudit', 'CPUAudit', 'DefenderExclusionAudit', 'KerberosAudit', 'DHCPAudit', 'NUMAAudit', 'SymlinkAudit', 'StartupScriptAudit', 'SecureChannelAudit', 'ComObjectAudit', 'FirewallLogAudit', 'ScheduledRebootAudit', 'PowerShellAudit', 'RouteTableAudit', 'TokenPrivilegeAudit', 'WindowsCapabilityAudit', 'ARPTableAudit', 'LocaleAudit', 'TaskHistoryAudit', 'NTFSAudit', 'Win11Cleanup', 'DarkMode', 'LightMode', 'iSCSIAudit', 'NICTeamAudit', 'SMBSessionAudit', 'WindowsUpdateAudit', 'ClusterQuorumAudit', 'S2DAudit', 'VirtualSwitchAudit', 'MPIOPathAudit', 'ServiceRecoveryAudit', 'VMOvercommitAudit', 'DedupAudit', 'ClusterNetworkAudit', 'ReplicaLagAudit', 'HandleLeakAudit', 'ShadowCopyAudit', 'QoSPolicyAudit', 'LiveMigrationAudit', 'DomainTrustAudit', 'DiskLatencyAudit', 'NICOffloadAudit', 'StorageTimeoutAudit', 'EventLogCapacityAudit', 'TcpSettingsAudit', 'WinRMAudit', 'ClusterHealthScore', 'VMInventoryExport', 'VMSnapshotAudit', 'StorageHealthScore', 'CSVSpaceAudit', 'SMBConnectionAudit', 'VolumeLabelAudit', 'NICErrorAudit', 'VMResourceWaste', 'HealthDashboard', 'SCCMClientAudit', 'SCOMAgentAudit', 'WACConnectivityAudit', 'AzureADAudit', 'ServerScore', 'FleetReport', 'PasswordPolicy', 'FirewallRuleAudit', 'GPResultAudit', 'DNSCacheAudit', 'TPMAudit', 'SecureBootAudit', 'TimeSkewAudit', 'NetworkProfileAudit', 'InsecureServiceAudit', 'SelfTest', 'CheckForUpdate', 'ExportLogs', 'UpdateSelf', 'Rollback', 'ScheduleUpdateCheck', 'Dashboard', 'History', 'Replay', 'AzureArcEnroll', 'DefenderEndpointOnboard', 'WSUSSetup', 'ADCSSetup', 'StorageMigrationSetup', 'GPOBackup', 'GPODrift', 'JEAList', 'NPSSetup', 'AlwaysOnVPNSetup', 'CISScan', 'SIEMSetup', 'SIEMStatus', 'WACSetup', 'WACStatus', 'VHDXEncryptionAudit', 'ADRecycleBin', 'ClusterValidationReport', 'SmbEnforce', 'SmbSecurityCheck')] + [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', 'ProcessAudit', 'BackupAudit', 'ShareAudit', 'DNSAudit', 'PowerAudit', 'RegistryAudit', 'ProfileAudit', 'HyperVAudit', 'NetworkAudit', 'StorageAudit', 'FeatureAudit', 'AutoStartAudit', 'BIOSAudit', 'ClusterAudit', 'AuditPolicyAudit', 'EnvAudit', 'CrashAudit', 'LocalGroupAudit', 'WMIAudit', 'TempAudit', 'UpdatePolicyAudit', 'IISAudit', 'SSHAudit', 'BitLockerAudit', 'PrintAudit', 'CredGuardAudit', 'PortAudit', 'AntivirusAudit', 'DotNetAudit', 'RDPAudit', 'VPNAudit', 'HostsFileAudit', 'NetStatAudit', 'LicenseAudit', 'USBDeviceAudit', 'AppLockerAudit', 'EventSubAudit', 'HotfixAudit', 'SysInfoAudit', 'LogonAudit', 'ACLAudit', 'RecoveryAudit', 'ServiceAccountAudit', 'ProxyAudit', 'PendingRebootAudit', 'PageFileAudit', 'CPUAudit', 'DefenderExclusionAudit', 'KerberosAudit', 'DHCPAudit', 'NUMAAudit', 'SymlinkAudit', 'StartupScriptAudit', 'SecureChannelAudit', 'ComObjectAudit', 'FirewallLogAudit', 'ScheduledRebootAudit', 'PowerShellAudit', 'RouteTableAudit', 'TokenPrivilegeAudit', 'WindowsCapabilityAudit', 'ARPTableAudit', 'LocaleAudit', 'TaskHistoryAudit', 'NTFSAudit', 'Win11Cleanup', 'DarkMode', 'LightMode', 'iSCSIAudit', 'NICTeamAudit', 'SMBSessionAudit', 'WindowsUpdateAudit', 'ClusterQuorumAudit', 'S2DAudit', 'VirtualSwitchAudit', 'MPIOPathAudit', 'ServiceRecoveryAudit', 'VMOvercommitAudit', 'DedupAudit', 'ClusterNetworkAudit', 'ReplicaLagAudit', 'HandleLeakAudit', 'ShadowCopyAudit', 'QoSPolicyAudit', 'LiveMigrationAudit', 'DomainTrustAudit', 'DiskLatencyAudit', 'NICOffloadAudit', 'StorageTimeoutAudit', 'EventLogCapacityAudit', 'TcpSettingsAudit', 'WinRMAudit', 'ClusterHealthScore', 'VMInventoryExport', 'VMSnapshotAudit', 'StorageHealthScore', 'CSVSpaceAudit', 'SMBConnectionAudit', 'VolumeLabelAudit', 'NICErrorAudit', 'VMResourceWaste', 'HealthDashboard', 'SCCMClientAudit', 'SCOMAgentAudit', 'WACConnectivityAudit', 'AzureADAudit', 'ServerScore', 'FleetReport', 'PasswordPolicy', 'FirewallRuleAudit', 'GPResultAudit', 'DNSCacheAudit', 'TPMAudit', 'SecureBootAudit', 'TimeSkewAudit', 'NetworkProfileAudit', 'InsecureServiceAudit', 'SelfTest', 'CheckForUpdate', 'ExportLogs', 'UpdateSelf', 'Rollback', 'ScheduleUpdateCheck', 'Dashboard', 'History', 'Replay', 'AzureArcEnroll', 'DefenderEndpointOnboard', 'WSUSSetup', 'ADCSSetup', 'StorageMigrationSetup', 'GPOBackup', 'GPODrift', 'JEAList', 'NPSSetup', 'AlwaysOnVPNSetup', 'CISScan', 'SIEMSetup', 'SIEMStatus', 'WACSetup', 'WACStatus', 'VHDXEncryptionAudit', 'ADRecycleBin', 'ClusterValidationReport', 'SmbEnforce', 'SmbSecurityCheck', 'PrintServerAudit')] [string]$Action, [ValidateSet('Light', 'Standard', 'Aggressive')] diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1 index 98efad3..cfbaa9e 100644 --- a/Modules/00-Initialization.ps1 +++ b/Modules/00-Initialization.ps1 @@ -225,7 +225,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) { if (-not $script:ModuleRoot -and $script:ScriptPath) { $script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath) } -$script:ScriptVersion = "1.113.0" +$script:ScriptVersion = "1.114.0" $script:ScriptStartTime = Get-Date # Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe. diff --git a/Modules/34-Help.ps1 b/Modules/34-Help.ps1 index 37afd6f..5f0f786 100644 --- a/Modules/34-Help.ps1 +++ b/Modules/34-Help.ps1 @@ -253,7 +253,7 @@ function Search-HelpTopics { @{ Title = "Performance"; Keywords = @("performance", "cpu", "memory", "disk", "io", "bandwidth", "dashboard", "process"); Description = "Live performance dashboard with CPU, memory, disk I/O, and network bandwidth monitoring" } @{ Title = "Licensing & NTP"; Keywords = @("license", "activation", "kms", "avma", "ntp", "time", "timezone", "clock"); Description = "Windows licensing status (KMS/AVMA/Retail), NTP configuration, time sync, and timezone setup" } @{ Title = "VM Management"; Keywords = @("checkpoint", "snapshot", "export", "import", "migration", "vhd", "iso"); Description = "VM checkpoints, export/import, migration readiness, VHD health, and ISO inventory" } - @{ Title = "CLI Actions"; Keywords = @("cli", "action", "headless", "automation", "fleet", "json", "audit", "scan", "score", "dashboard", "monitor", "policy", "sla", "netmap", "validate"); Description = "196 CLI actions for headless automation. Run -ListActions to see all. JSON output via -OutputFormat JSON. Key: ServerScore, HealthDashboard, FleetReport, CISScan, NPSSetup, AlwaysOnVPNSetup, SIEMStatus." } + @{ Title = "CLI Actions"; Keywords = @("cli", "action", "headless", "automation", "fleet", "json", "audit", "scan", "score", "dashboard", "monitor", "policy", "sla", "netmap", "validate"); Description = "197 CLI actions for headless automation. Run -ListActions to see all. JSON output via -OutputFormat JSON. Key: ServerScore, HealthDashboard, FleetReport, CISScan, NPSSetup, AlwaysOnVPNSetup, SIEMStatus." } @{ Title = "SelfTest Action"; Keywords = @("selftest", "self-test", "diagnose", "diagnostic", "verify", "healthcheck", "sanity"); Description = "Internal diagnostic. -Action SelfTest checks PS version, elevation, module count, version consistency, defaults.json validity, temp path writability, FileServer reachability, and agent installer config. Exit 1 on any failure. Use -OutputFormat JSON for structured output." } @{ Title = "Security Audits"; Keywords = @("security", "audit", "hardening", "compliance", "tls", "smb", "kerberos", "credguard", "applocker", "bitlockeraudit", "defenderexclusionaudit", "audit-policy", "secureboot", "tpm"); Description = "Security-focused CLI audits: TLSAudit, SMBAudit, KerberosAudit, CredGuardAudit, AppLockerAudit, BitLockerAudit, DefenderExclusionAudit, AuditPolicyAudit, SecureBootAudit, TPMAudit, UserAudit, LogonAudit, InsecureServiceAudit, RegistryAudit. All support -OutputFormat JSON." } @{ Title = "Network Audits"; Keywords = @("netaudit", "dns", "firewall-audit", "firewalllog", "arp", "route", "tcp", "netstat", "dhcp", "netprofile", "winrm", "qos", "nicoffload"); Description = "Network audits: DNSAudit, DNSCacheAudit, FirewallAudit, FirewallRuleAudit, FirewallLogAudit, ARPTableAudit, RouteTableAudit, TcpSettingsAudit, NetStatAudit, DHCPAudit, NetworkProfileAudit, WinRMAudit, QoSPolicyAudit, NICOffloadAudit, NICErrorAudit, HostsFileAudit, VPNAudit, ProxyAudit." } diff --git a/Modules/35-Utilities.ps1 b/Modules/35-Utilities.ps1 index ae82f61..1918d29 100644 --- a/Modules/35-Utilities.ps1 +++ b/Modules/35-Utilities.ps1 @@ -2694,4 +2694,181 @@ function Show-SystemBanner { Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" } + +# Read-only print-server posture: spooler state, per-printer queue depth, orphaned +# TCP/IP ports (no printer references them) and drivers no printer uses. +function Get-PrintServerStatus { + $result = [PSCustomObject]@{ + Available = $false + SpoolerStatus = 'Unknown' + Printers = @() + TotalQueuedJobs = 0 + OrphanedPorts = @() + UnusedDrivers = @() + } + if (-not (Get-Command Get-Printer -ErrorAction SilentlyContinue)) { return $result } + $result.Available = $true + $svc = Get-Service -Name Spooler -ErrorAction SilentlyContinue + if ($svc) { $result.SpoolerStatus = "$($svc.Status)" } + + $printers = @(Get-Printer -ErrorAction SilentlyContinue) + $printerInfo = foreach ($p in $printers) { + $jobs = @(Get-PrintJob -PrinterName $p.Name -ErrorAction SilentlyContinue) + [PSCustomObject]@{ Name = $p.Name; PortName = $p.PortName; DriverName = $p.DriverName; QueuedJobs = $jobs.Count } + } + $result.Printers = @($printerInfo) + $sum = ($printerInfo | Measure-Object -Property QueuedJobs -Sum).Sum + $result.TotalQueuedJobs = if ($sum) { [int]$sum } else { 0 } + + # Orphaned TCP/IP ports: a port referenced by no printer. + $usedPorts = @($printers | ForEach-Object { $_.PortName }) + $tcpPorts = @(Get-PrinterPort -ErrorAction SilentlyContinue | Where-Object { $_.Description -match 'TCP/IP' -or $_.Name -match '^IP_' }) + $result.OrphanedPorts = @($tcpPorts | Where-Object { $usedPorts -notcontains $_.Name } | ForEach-Object { $_.Name }) + + # Unused drivers (informational only — removal is risky and not automated here). + $usedDrivers = @($printers | ForEach-Object { $_.DriverName }) + $allDrivers = @(Get-PrinterDriver -ErrorAction SilentlyContinue | ForEach-Object { $_.Name }) + $result.UnusedDrivers = @($allDrivers | Where-Object { $usedDrivers -notcontains $_ }) + + return $result +} + +# OneWay: stop the spooler, purge the spool directory, restart. Abandoned/stuck +# jobs are discarded — there is no undo for a flushed queue. +function Clear-StuckPrintQueue { + if (-not (Get-Service -Name Spooler -ErrorAction SilentlyContinue)) { + Write-OutputColor " Print Spooler service is not present on this host." -color "Error"; return + } + Write-OutputColor " This stops the spooler, deletes ALL queued print jobs on this host, and restarts it." -color "Warning" + Write-OutputColor " Discarded jobs cannot be recovered." -color "Warning" + if (-not (Confirm-UserAction -Message "Flush the print queue now?")) { Write-OutputColor " Cancelled." -color "Info"; return } + + if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) { + Push-DryRunStep -Label "Flush stuck print queue (purge spooler)" -Category "Maintenance" -OneWay $true ` + -Params @{ SpoolDir = "$env:SystemRoot\System32\spool\PRINTERS" } ` + -Preflight { $null -ne (Get-Service -Name Spooler -ErrorAction SilentlyContinue) }.GetNewClosure() ` + -Apply { + Stop-Service -Name Spooler -Force -ErrorAction SilentlyContinue + Get-ChildItem -LiteralPath "$env:SystemRoot\System32\spool\PRINTERS" -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue + Start-Service -Name Spooler -ErrorAction SilentlyContinue + }.GetNewClosure() + Write-OutputColor " Queued (Dry-Run): flush print queue (ONE-WAY — jobs are lost)." -color "Warning" + Add-SessionChange -Category "DryRun" -Description "Queued print-queue flush (one-way)" + return + } + + try { + Stop-Service -Name Spooler -Force -ErrorAction Stop + Get-ChildItem -LiteralPath "$env:SystemRoot\System32\spool\PRINTERS" -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue + Start-Service -Name Spooler -ErrorAction Stop + Write-OutputColor " Print queue flushed; spooler restarted." -color "Success" + Add-SessionChange -Category "Maintenance" -Description "Flushed stuck print queue" + } + catch { + # Best-effort: make sure the spooler comes back even if the purge failed. + Start-Service -Name Spooler -ErrorAction SilentlyContinue + Write-OutputColor " Failed to flush print queue: $($_.Exception.Message)" -color "Error" + } +} + +# Reversible: remove TCP/IP printer ports that no printer references. Each removed +# port's address config is captured so the session undo can re-create it. +function Remove-OrphanedPrinterPorts { + $status = Get-PrintServerStatus + if (-not $status.Available) { Write-OutputColor " Print management is not available on this host." -color "Error"; return } + $orphans = @($status.OrphanedPorts) + if (-not $orphans.Count) { Write-OutputColor " No orphaned printer ports found." -color "Success"; return } + + Write-OutputColor " Orphaned TCP/IP ports (referenced by no printer):" -color "Info" + $orphans | ForEach-Object { Write-OutputColor " - $_" -color "Info" } + if (-not (Confirm-UserAction -Message "Remove these $($orphans.Count) orphaned port(s)?")) { Write-OutputColor " Cancelled." -color "Info"; return } + + # Capture each port's config for a faithful re-add on undo. + $portConfigs = @(foreach ($name in $orphans) { + Get-PrinterPort -Name $name -ErrorAction SilentlyContinue | + Select-Object Name, PrinterHostAddress, PortNumber, SNMPEnabled, SNMPCommunity + }) + + if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) { + $caps = $portConfigs + Push-DryRunStep -Label "Remove $($orphans.Count) orphaned printer port(s)" -Category "Maintenance" -OneWay $false ` + -Params @{ Ports = ($orphans -join ', ') } ` + -Preflight { $true }.GetNewClosure() ` + -Apply { foreach ($n in $orphans) { Remove-PrinterPort -Name $n -ErrorAction SilentlyContinue } }.GetNewClosure() ` + -Undo { + foreach ($c in $caps) { + if ($c.PrinterHostAddress) { + Add-PrinterPort -Name $c.Name -PrinterHostAddress $c.PrinterHostAddress -ErrorAction SilentlyContinue + } + } + }.GetNewClosure() + Write-OutputColor " Queued (Dry-Run): remove $($orphans.Count) orphaned port(s)." -color "Warning" + Add-SessionChange -Category "DryRun" -Description "Queued removal of $($orphans.Count) orphaned printer port(s)" + return + } + + $removed = 0 + foreach ($name in $orphans) { + try { Remove-PrinterPort -Name $name -ErrorAction Stop; $removed++ } + catch { Write-OutputColor " Could not remove $name`: $($_.Exception.Message)" -color "Warning" } + } + if ($removed -gt 0) { + Write-OutputColor " Removed $removed orphaned printer port(s)." -color "Success" + Add-SessionChange -Category "Maintenance" -Description "Removed $removed orphaned printer port(s)" + Add-UndoAction -Category "Maintenance" -Description "Removed $removed orphaned printer port(s)" -UndoScript { + param($Configs) + foreach ($c in $Configs) { + if ($c.PrinterHostAddress) { Add-PrinterPort -Name $c.Name -PrinterHostAddress $c.PrinterHostAddress -ErrorAction SilentlyContinue } + } + } -UndoParams @{ Configs = $portConfigs } + } +} + +# Interactive submenu: print-server cleanup. +function Show-PrintServerCleanup { + while ($true) { + if ($script:ReturnToMainMenu) { return } + Clear-Host + Write-CenteredOutput "Print Server Cleanup" -color "Info" + $s = Get-PrintServerStatus + if (-not $s.Available) { + Write-OutputColor " Print management is not available on this host (no Print-Services role / RSAT)." -color "Error" + return + } + Write-OutputColor " Spooler : $($s.SpoolerStatus)" -color $(if ($s.SpoolerStatus -eq 'Running') { "Success" } else { "Warning" }) + Write-OutputColor " Printers : $($s.Printers.Count)" -color "Info" + Write-OutputColor " Queued jobs : $($s.TotalQueuedJobs)" -color $(if ($s.TotalQueuedJobs -gt 0) { "Warning" } else { "Info" }) + Write-OutputColor " Orphaned ports : $($s.OrphanedPorts.Count)" -color $(if ($s.OrphanedPorts.Count -gt 0) { "Warning" } else { "Info" }) + Write-OutputColor " Unused drivers : $($s.UnusedDrivers.Count) (informational)" -color "Info" + Write-OutputColor "" -color "Info" + Write-MenuItem "[1] Flush stuck print queue (one-way)" + Write-MenuItem "[2] Remove orphaned printer ports (reversible)" + Write-OutputColor "" -color "Info" + $choice = Read-Host " Select an option (or B to go back)" + $navResult = Test-NavigationCommand -UserInput $choice + if ($navResult.ShouldReturn) { return } + switch ($choice) { + "1" { Clear-StuckPrintQueue; Write-PressEnter } + "2" { Remove-OrphanedPrinterPorts; Write-PressEnter } + "B" { return } + default { Write-OutputColor " Invalid selection. Enter 1-2 or B." -color "Error"; Start-Sleep -Seconds 1 } + } + } +} + +# CLI: PrintServerAudit — read-only print-server posture (JSON-aware). +function Start-PrintServerAudit { + $s = Get-PrintServerStatus + if ($script:CLIOutputFormat -eq 'JSON') { + Write-Output (@{ + Tool = $script:ToolFullName; Version = $script:ScriptVersion; Action = 'PrintServerAudit' + Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); Hostname = $env:COMPUTERNAME + Available = $s.Available; SpoolerStatus = $s.SpoolerStatus + PrinterCount = $s.Printers.Count; TotalQueuedJobs = $s.TotalQueuedJobs + OrphanedPorts = $s.OrphanedPorts; UnusedDrivers = $s.UnusedDrivers + } | ConvertTo-Json -Depth 4) + } + else { Show-PrintServerCleanup } + return $true +} #endregion \ No newline at end of file diff --git a/Modules/50-EntryPoint.ps1 b/Modules/50-EntryPoint.ps1 index 688ce67..f2c13c1 100644 --- a/Modules/50-EntryPoint.ps1 +++ b/Modules/50-EntryPoint.ps1 @@ -414,6 +414,7 @@ function Assert-Elevation { @{ Action = 'ClusterValidationReport'; Description = 'Run a non-disruptive failover-cluster validation and archive the HTML report (JSON-aware)' } @{ Action = 'SmbSecurityCheck'; Description = 'Read-only: report SMB server signing/encryption posture (JSON-aware)' } @{ Action = 'SmbEnforce'; Description = 'Enforce SMB server signing + encryption (reversible)' } + @{ Action = 'PrintServerAudit'; Description = 'Read-only: report print-spooler posture, queue depth, orphaned ports + unused drivers (JSON-aware)' } @{ Action = 'Batch'; Description = 'JSON-driven full configuration' } ) if ($script:CLIOutputFormat -eq 'JSON') { @@ -2071,6 +2072,11 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} $smbEnfOk = Start-SmbEnforce [Environment]::Exit([int](-not $smbEnfOk)) } + 'PrintServerAudit' { + # Read-only print-server posture (JSON-aware). + $psAuditOk = Start-PrintServerAudit + [Environment]::Exit([int](-not $psAuditOk)) + } 'Batch' { if (-not $script:CLIConfig) { Write-OutputColor " ERROR: -Action Batch requires -Config " -color "Error" diff --git a/Modules/56-OperationsMenu.ps1 b/Modules/56-OperationsMenu.ps1 index 80b19ee..2195296 100644 --- a/Modules/56-OperationsMenu.ps1 +++ b/Modules/56-OperationsMenu.ps1 @@ -71,6 +71,7 @@ function Show-OperationsMenu { Write-MenuItem "[30] Memory Pressure Diagnostics" Write-MenuItem "[34] CIS Compliance Scan (CIS L1)" Write-MenuItem "[35] SMB Signing & Encryption Enforcement" + Write-MenuItem "[36] Print Server Cleanup" Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" @@ -105,6 +106,7 @@ function Show-OperationsMenu { @{Num="31"; Name="Batch VM Cleanup"}; @{Num="32"; Name="VM Migration Pre-Flight"} @{Num="33"; Name="Logged-On Users"}; @{Num="34"; Name="CIS Compliance Scan (CIS L1)"} @{Num="35"; Name="SMB Signing & Encryption Enforcement"} + @{Num="36"; Name="Print Server Cleanup"} ) $matched = @($opsItems | Where-Object { $_.Name -match [regex]::Escape($searchTerm) }) Write-OutputColor "" -color "Info" @@ -278,10 +280,14 @@ function Show-OperationsMenu { Set-SMBServerSecurity Write-PressEnter } + "36" { + Show-PrintServerCleanup + Write-PressEnter + } "b" { return } "B" { return } default { - Write-OutputColor " Invalid choice. Enter 1-35, [/] to search, or B." -color "Error" + Write-OutputColor " Invalid choice. Enter 1-36, [/] to search, or B." -color "Error" Start-Sleep -Seconds 1 } } diff --git a/README.md b/README.md index 1363f15..b10fe1f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ OpenSSF Best Practices codecov PSScriptAnalyzer 0 errors - 5061 structural tests + 5075 structural tests Pester 312 tests SLSA Level 3

@@ -37,7 +37,7 @@ --- -RackStack is a menu-driven PowerShell tool that automates everything between "Windows is installed" and "server is in production." Where sconfig gives you 15 options, RackStack gives you 196 CLI actions and 60+ interactive menus covering networking, Hyper-V, SAN/iSCSI, clustering, VM deployment, cloud onboarding, and batch automation, all with undo, transaction rollback, and audit logging. +RackStack is a menu-driven PowerShell tool that automates everything between "Windows is installed" and "server is in production." Where sconfig gives you 15 options, RackStack gives you 197 CLI actions and 60+ interactive menus covering networking, Hyper-V, SAN/iSCSI, clustering, VM deployment, cloud onboarding, and batch automation, all with undo, transaction rollback, and audit logging. Built for MSPs, sysadmins, and infrastructure teams who build servers repeatedly and want it done right every time. @@ -61,7 +61,7 @@ Built for MSPs, sysadmins, and infrastructure teams who build servers repeatedly **Automation** -- JSON-driven batch mode (24 idempotent steps with transaction rollback), Quick Setup Wizard, configuration export/import, HTML reports, JSON audit logging with rotation -**Monitoring** -- 196 CLI actions with JSON output for fleet automation, `ServerScore` (unified 0-100 health grade), `HealthDashboard` (all-in-one monitoring endpoint), `ClusterHealthScore`, `StorageHealthScore`, System Center (SCCM/SCOM/WAC) + Azure AD/Intune integration +**Monitoring** -- 197 CLI actions with JSON output for fleet automation, `ServerScore` (unified 0-100 health grade), `HealthDashboard` (all-in-one monitoring endpoint), `ClusterHealthScore`, `StorageHealthScore`, System Center (SCCM/SCOM/WAC) + Azure AD/Intune integration **Cloud & Security** -- Azure Arc server onboarding (install the Connected Machine Agent, connect the host to Azure's hybrid management plane via service-principal auth); Microsoft Defender for Endpoint onboarding (activate the built-in EDR sensor against your tenant, with a built-in detection test) @@ -439,7 +439,7 @@ $report.Issues **Tiers:** `Light` (minimal, safe for prod), `Standard` (recommended), `Aggressive` (maximum cleanup/debloat). -### 196 CLI Actions +### 197 CLI Actions | Category | Actions | |----------|---------| diff --git a/RackStack.ps1 b/RackStack.ps1 index 5bce3a1..17527c2 100644 --- a/RackStack.ps1 +++ b/RackStack.ps1 @@ -13,7 +13,7 @@ Environment-specific settings are configured via defaults.json. .VERSION - 1.113.0 + 1.114.0 .NOTES - Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing) diff --git a/RackStack.psd1 b/RackStack.psd1 index 746e3e4..b3100ca 100644 --- a/RackStack.psd1 +++ b/RackStack.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'RackStack.psm1' - ModuleVersion = '1.113.0' + ModuleVersion = '1.114.0' GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91' Author = 'TheAbider' CompanyName = 'TheAbider' diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1 index bbed3b6..7f49dcf 100644 --- a/Tests/Run-Tests.ps1 +++ b/Tests/Run-Tests.ps1 @@ -9123,7 +9123,7 @@ try { Write-TestResult "56-Ops: SmbSecurityCheck JSON-aware" ($smbC -match "Start-SmbSecurityCheck[\s\S]{0,400}CLIOutputFormat -eq 'JSON'") $smbMenu = $smbC -match '\[35\]\s*SMB Signing' Write-TestResult "56-Ops: menu item [35] SMB Signing present" $smbMenu - Write-TestResult "56-Ops: Operations invalid msg bumped to 1-35" ($smbC -match 'Enter 1-35, \[/\] to search') + Write-TestResult "56-Ops: Operations invalid msg uses searchable range format" ($smbC -match 'Enter 1-\d+, \[/\] to search') $smbEntry = Get-Content "$modulesPath\50-EntryPoint.ps1" -Raw Write-TestResult "50-EntryPoint: SmbSecurityCheck dispatch case" ($smbEntry -match "'SmbSecurityCheck'\s*\{") Write-TestResult "50-EntryPoint: SmbEnforce dispatch case" ($smbEntry -match "'SmbEnforce'\s*\{") @@ -9135,6 +9135,40 @@ catch { Write-TestResult "SMB Security Enforcement Tests" $false $_.Exception.Message } +# ============================================================================ +# SECTION 181: PRINT SERVER CLEANUP (v1.114.0, 35-Utilities) +# ============================================================================ +Write-SectionHeader "SECTION 181: PRINT SERVER CLEANUP (35-Utilities)" + +try { + $psC = Get-Content "$modulesPath\35-Utilities.ps1" -Raw + Write-TestResult "35-Utils: Get-PrintServerStatus exists" ($psC -match 'function\s+Get-PrintServerStatus\b') + Write-TestResult "35-Utils: Clear-StuckPrintQueue exists" ($psC -match 'function\s+Clear-StuckPrintQueue\b') + Write-TestResult "35-Utils: Remove-OrphanedPrinterPorts exists" ($psC -match 'function\s+Remove-OrphanedPrinterPorts\b') + Write-TestResult "35-Utils: Show-PrintServerCleanup exists" ($psC -match 'function\s+Show-PrintServerCleanup\b') + Write-TestResult "35-Utils: Start-PrintServerAudit exists" ($psC -match 'function\s+Start-PrintServerAudit\b') + # Audit is read-only: status reader makes no Set/Remove/Stop calls. + Write-TestResult "35-Utils: Get-PrintServerStatus is read-only" (-not ($psC -match 'function\s+Get-PrintServerStatus[\s\S]{0,1400}(Remove-Printer|Stop-Service|Set-Printer)')) + # Queue flush is ONE-WAY in Dry-Run (jobs are unrecoverable). + Write-TestResult "35-Utils: queue flush is ONE-WAY in Dry-Run" ($psC -match 'function\s+Clear-StuckPrintQueue[\s\S]{0,1600}Push-DryRunStep[\s\S]{0,300}-OneWay \$true') + # Flush always restarts the spooler even on failure. + Write-TestResult "35-Utils: queue flush restarts spooler on failure" ($psC -match 'catch\s*\{[\s\S]{0,200}Start-Service -Name Spooler') + # Orphaned-port removal is reversible: captures config + registers undo that re-adds. + Write-TestResult "35-Utils: orphaned-port removal is reversible (re-add undo)" ($psC -match 'function\s+Remove-OrphanedPrinterPorts[\s\S]{0,2600}Add-UndoAction[\s\S]{0,300}Add-PrinterPort') + # Audit CLI is JSON-aware. + Write-TestResult "35-Utils: PrintServerAudit JSON-aware" ($psC -match "Start-PrintServerAudit[\s\S]{0,300}CLIOutputFormat -eq 'JSON'") + $psMenu = Get-Content "$modulesPath\56-OperationsMenu.ps1" -Raw + Write-TestResult "56-Ops: menu item [36] Print Server Cleanup present" ($psMenu -match '\[36\]\s*Print Server Cleanup') + Write-TestResult "56-Ops: Operations invalid msg covers item [36] (1-36)" ($psMenu -match 'Enter 1-36, \[/\] to search') + $psEntry = Get-Content "$modulesPath\50-EntryPoint.ps1" -Raw + Write-TestResult "50-EntryPoint: PrintServerAudit dispatch case" ($psEntry -match "'PrintServerAudit'\s*\{") + $psHeader = Get-Content (Join-Path $script:ModuleRoot "Header.ps1") -Raw + Write-TestResult "Header.ps1: PrintServerAudit in -Action ValidateSet" ($psHeader -match "'PrintServerAudit'") +} +catch { + Write-TestResult "Print Server Cleanup Tests" $false $_.Exception.Message +} + # ============================================================================ # SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase) # ============================================================================ @@ -13146,7 +13180,7 @@ try { # Action list in -ListActions block has 160 entries $listBlock = [regex]::Match($ep5, '\$actionList = @\([\s\S]*?\)[\s\S]{0,50}CLIOutputFormat').Value $listActionCount = @([regex]::Matches($listBlock, "Action\s*=\s*'")).Count - Write-TestResult "50-EntryPoint: action list has 196 entries" ($listActionCount -eq 196) "Found $listActionCount" + Write-TestResult "50-EntryPoint: action list has 197 entries" ($listActionCount -eq 197) "Found $listActionCount" } catch { Write-TestResult "v1.91.0 Tests" $false $_.Exception.Message } @@ -13179,7 +13213,7 @@ try { # Action list count (should be 167 now) $listBlock2 = [regex]::Match($ep6, '\$actionList = @\([\s\S]*?\)[\s\S]{0,50}CLIOutputFormat').Value $actionCount2 = @([regex]::Matches($listBlock2, "Action\s*=\s*'")).Count - Write-TestResult "50-EntryPoint: action list has 196 entries" ($actionCount2 -eq 196) "Found $actionCount2" + Write-TestResult "50-EntryPoint: action list has 197 entries" ($actionCount2 -eq 197) "Found $actionCount2" } catch { Write-TestResult "v1.92.0 Tests" $false $_.Exception.Message } @@ -13205,7 +13239,7 @@ try { # Action count updated $listBlock3 = [regex]::Match($ep7, '\$actionList = @\([\s\S]*?\)[\s\S]{0,50}CLIOutputFormat').Value $actionCount3 = @([regex]::Matches($listBlock3, "Action\s*=\s*'")).Count - Write-TestResult "50-EntryPoint: action list has 196 entries" ($actionCount3 -eq 196) "Found $actionCount3" + Write-TestResult "50-EntryPoint: action list has 197 entries" ($actionCount3 -eq 197) "Found $actionCount3" } catch { Write-TestResult "v1.93.0 Tests" $false $_.Exception.Message } @@ -13243,7 +13277,7 @@ try { # Action list count $listBlock4 = [regex]::Match($ep8, '\$actionList = @\([\s\S]*?\)[\s\S]{0,50}CLIOutputFormat').Value $actionCount4 = @([regex]::Matches($listBlock4, "Action\s*=\s*'")).Count - Write-TestResult "50-EntryPoint: action list has 196 entries" ($actionCount4 -eq 196) "Found $actionCount4" + Write-TestResult "50-EntryPoint: action list has 197 entries" ($actionCount4 -eq 197) "Found $actionCount4" } catch { Write-TestResult "v1.94.1 Tests" $false $_.Exception.Message } diff --git a/dist/chocolatey/rackstack.nuspec b/dist/chocolatey/rackstack.nuspec index 0d00187..bcc9cc0 100644 --- a/dist/chocolatey/rackstack.nuspec +++ b/dist/chocolatey/rackstack.nuspec @@ -18,7 +18,7 @@ windows-server hyper-v iscsi clustering powershell sysadmin automation msp admin-tools PowerShell automation toolkit for configuring Windows Server hosts -RackStack is a menu-driven PowerShell tool that automates everything between "Windows is installed" and "server is in production." 196 CLI actions and 60+ interactive menus covering networking, Hyper-V, SAN/iSCSI, clustering, VM deployment, and batch automation, all with undo, transaction rollback, and audit logging. +RackStack is a menu-driven PowerShell tool that automates everything between "Windows is installed" and "server is in production." 197 CLI actions and 60+ interactive menus covering networking, Hyper-V, SAN/iSCSI, clustering, VM deployment, and batch automation, all with undo, transaction rollback, and audit logging. Built for MSPs, sysadmins, and infrastructure teams who build servers repeatedly and want it done right every time. diff --git a/dist/scoop/rackstack.json b/dist/scoop/rackstack.json index 60fa280..fb40954 100644 --- a/dist/scoop/rackstack.json +++ b/dist/scoop/rackstack.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/ScoopInstaller/Scoop/master/schema.json", "version": "0.0.0", - "description": "PowerShell automation toolkit for configuring Windows Server hosts — Hyper-V virtualization hosts, failover cluster nodes, iSCSI storage clients, Active Directory members, and standalone servers. Ships as a code-signed EXE plus a PowerShell Gallery wrapper module exposing 196 structured CLI actions.", + "description": "PowerShell automation toolkit for configuring Windows Server hosts — Hyper-V virtualization hosts, failover cluster nodes, iSCSI storage clients, Active Directory members, and standalone servers. Ships as a code-signed EXE plus a PowerShell Gallery wrapper module exposing 197 structured CLI actions.", "homepage": "https://github.com/TheAbider/RackStack", "license": "MIT", "url": "https://github.com/TheAbider/RackStack/releases/download/v0.0.0/RackStack.exe", diff --git a/dist/winget/1.99.1/TheAbider.RackStack.locale.en-US.yaml b/dist/winget/1.99.1/TheAbider.RackStack.locale.en-US.yaml index 0e5298e..d855395 100644 --- a/dist/winget/1.99.1/TheAbider.RackStack.locale.en-US.yaml +++ b/dist/winget/1.99.1/TheAbider.RackStack.locale.en-US.yaml @@ -13,7 +13,7 @@ Copyright: Copyright (c) 2026 TheAbider ShortDescription: PowerShell automation toolkit for configuring Windows Server hosts. Description: |- RackStack is a menu-driven PowerShell tool that automates everything between - "Windows is installed" and "server is in production." It provides 196 CLI + "Windows is installed" and "server is in production." It provides 197 CLI actions and 60+ interactive menus covering networking, Hyper-V, SAN/iSCSI, clustering, VM deployment, cloud onboarding, and batch automation, all with undo, transaction rollback, and audit logging. Built for MSPs, sysadmins,