From 65c2e218b30ac4fa30c2826991226ade727094f4 Mon Sep 17 00:00:00 2001
From: TheAbider <51920546+TheAbider@users.noreply.github.com>
Date: Fri, 29 May 2026 17:11:37 -0700
Subject: [PATCH] v1.114.0: print server cleanup
Add a print-server maintenance utility to 35-Utilities plus a read-only
CLI action:
- PrintServerAudit (read-only): reports spooler state, printer count,
total queued jobs, orphaned TCP/IP ports (no printer references them),
and unused drivers. JSON-aware; makes no changes.
- Operations Menu [36] Print Server Cleanup:
- Flush stuck print queue (one-way): stop spooler, purge spool dir,
restart. Confirmation-gated, ONE-WAY in Dry-Run; spooler is always
restarted even if the purge fails.
- Remove orphaned printer ports (reversible): removes TCP/IP ports no
printer references, capturing each port's host address so the session
undo / Dry-Run Undo closure can re-create it.
Unused drivers are reported but not auto-removed (out of scope). The new
looping submenu follows the navigation convention (Test-NavigationCommand
on the Select prompt, ReturnToMainMenu in the loop).
CLI actions 196 -> 197. Section 181 added; 5075 structural tests green.
---
Changelog.md | 11 ++
Header.ps1 | 4 +-
Modules/00-Initialization.ps1 | 2 +-
Modules/34-Help.ps1 | 2 +-
Modules/35-Utilities.ps1 | 177 ++++++++++++++++++
Modules/50-EntryPoint.ps1 | 6 +
Modules/56-OperationsMenu.ps1 | 8 +-
README.md | 8 +-
RackStack.ps1 | 2 +-
RackStack.psd1 | 2 +-
Tests/Run-Tests.ps1 | 44 ++++-
dist/chocolatey/rackstack.nuspec | 2 +-
dist/scoop/rackstack.json | 2 +-
.../TheAbider.RackStack.locale.en-US.yaml | 2 +-
14 files changed, 253 insertions(+), 19 deletions(-)
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
-
+