From 5255becc5e10cf73734de8b32e852cd65abbfd7a Mon Sep 17 00:00:00 2001 From: TheAbider <51920546+TheAbider@users.noreply.github.com> Date: Fri, 29 May 2026 19:31:14 -0700 Subject: [PATCH] v1.119.0: Remote Desktop Services (new module 80) Add 80-RemoteDesktopServices, surfaced under Roles & Features [15] Remote Desktop Services (RDS), plus a read-only CLI action. Completes the v1.109.0 -> v1.119.0 feature roadmap. - RDSAudit (read-only): RD Session Host (RDS-RD-Server) + RD Licensing (RDS-Licensing) role state, plus the configured licensing mode + license server(s). JSON-aware; makes no changes. - RDS role install (reversible): installs the roles via the timeout-guarded feature installer, capturing which were missing so undo removes only those. Dry-Run aware; server-SKU gated. - Licensing-mode config (reversible): sets Per-Device/Per-User mode + license-server name via the policy registry (the values the licensing GPOs write). Prior values captured for undo; Dry-Run aware; server name is format-validated. Scope: full session-collection deployment (New-RDSessionDeployment) and CAL key activation are deliberately deferred (heavy/reboot + GUI/sensitive). A 2-agent adversarial review confirmed no secret handling, a genuine deferral, validated input, and faithful reversibility. New module 80-RemoteDesktopServices. Modules 80 -> 81. CLI actions 200 -> 201. Section 186 added; 5167 structural tests green. --- Changelog.md | 14 + Header.ps1 | 4 +- Modules/00-Initialization.ps1 | 2 +- Modules/34-Help.ps1 | 2 +- Modules/48-MenuDisplay.ps1 | 9 + Modules/49-MenuRunner.ps1 | 3 +- Modules/50-EntryPoint.ps1 | 6 + Modules/80-RemoteDesktopServices.ps1 | 246 ++++++++++++++++++ README.md | 18 +- RackStack.ps1 | 5 +- RackStack.psd1 | 2 +- Tests/Run-Tests.ps1 | 73 +++++- dist/chocolatey/rackstack.nuspec | 2 +- dist/scoop/rackstack.json | 2 +- .../TheAbider.RackStack.locale.en-US.yaml | 2 +- sync-to-monolithic.ps1 | 4 +- 16 files changed, 359 insertions(+), 35 deletions(-) create mode 100644 Modules/80-RemoteDesktopServices.ps1 diff --git a/Changelog.md b/Changelog.md index 55da33c..ae50407 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,19 @@ # Changelog +## v1.119.0 + +Remote Desktop Services — a new module (**80-RemoteDesktopServices**) surfaced under **Roles & Features → [15] Remote Desktop Services (RDS)**, plus a read-only CLI action. + +- **`RDSAudit`** (read-only) — reports whether the RD Session Host (`RDS-RD-Server`) and RD Licensing (`RDS-Licensing`) roles are installed, and the configured licensing mode + license server(s). JSON-aware; makes no changes. +- **RDS role install** (reversible) — installs the RD Session Host + RD Licensing roles via the timeout-guarded feature installer, capturing which were missing so the session undo removes only the ones it added. Dry-Run aware; server-SKU gated. +- **Licensing-mode configuration** (reversible) — sets Per-Device / Per-User mode and the license-server name via the policy registry (the same values the licensing GPOs write). Prior values are captured for the session undo; Dry-Run aware. The license-server name is format-validated. + +Scope and safety: full **session-collection deployment** (`New-RDSessionDeployment`) and **CAL / license-key activation** are intentionally **deferred** — the deployment reconfigures the server and needs a reboot, and CAL activation is done against a license agreement in RD Licensing Manager (`licmgr`), where that key material belongs. This module therefore handles **no license key and holds no secret**; it lands the role lifecycle, the licensing **mode** (which has a 120-day grace period before CALs are required), and an at-a-glance audit. The Roles & Features menu shows a live RDS status indicator. + +New module 80-RemoteDesktopServices. Modules: 80 → 81. CLI actions: 200 → 201. + +This completes the v1.109.0 → v1.119.0 feature roadmap. + ## v1.118.0 DFS Namespaces & Replication — a new module (**79-DFS**) surfaced under **Roles & Features → [14] DFS Namespaces & Replication**, plus a read-only CLI action. diff --git a/Header.ps1 b/Header.ps1 index 1abfe30..699378b 100644 --- a/Header.ps1 +++ b/Header.ps1 @@ -30,7 +30,7 @@ 7h3 4b1d3r .VERSION - 1.118.0 + 1.119.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', 'PrintServerAudit', 'NtpHardeningAudit', 'CertBindingAudit', 'DFSAudit')] + [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', 'NtpHardeningAudit', 'CertBindingAudit', 'DFSAudit', 'RDSAudit')] [string]$Action, [ValidateSet('Light', 'Standard', 'Aggressive')] diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1 index 0c85092..aeed870 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.118.0" +$script:ScriptVersion = "1.119.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 9ffd4da..b169fdf 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 = "200 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 = "201 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/48-MenuDisplay.ps1 b/Modules/48-MenuDisplay.ps1 index d31c632..2716092 100644 --- a/Modules/48-MenuDisplay.ps1 +++ b/Modules/48-MenuDisplay.ps1 @@ -453,6 +453,14 @@ function Show-RolesFeaturesMenu { } -CacheSeconds 120 $dfsColor = if ($dfsStatusText -eq "Installed") { "Success" } else { "Warning" } + $rdsStatusText = Get-CachedValue -Key "RDSState" -FetchScript { + $rd = Get-RDSStatus + if ($rd.SessionHostInstalled -and $rd.LicensingInstalled) { "Installed" } + elseif ($rd.SessionHostInstalled -or $rd.LicensingInstalled) { "Partial" } + else { "Not Installed" } + } -CacheSeconds 120 + $rdsColor = if ($rdsStatusText -eq "Installed") { "Success" } else { "Warning" } + Write-OutputColor "" -color "Info" Write-OutputColor " ╔════════════════════════════════════════════════════════════════════════╗" -color "Info" Write-OutputColor " ║$((" ROLES & FEATURES").PadRight(72))║" -color "Info" @@ -474,6 +482,7 @@ function Show-RolesFeaturesMenu { Write-MenuItem "[12] SIEM Log Forwarder ►" -Status $siemStatusText -StatusColor $siemColor Write-MenuItem "[13] Windows Admin Center (WAC) ►" -Status $wacStatusText -StatusColor $wacColor Write-MenuItem "[14] DFS Namespaces & Replication ►" -Status $dfsStatusText -StatusColor $dfsColor + Write-MenuItem "[15] Remote Desktop Services (RDS) ►" -Status $rdsStatusText -StatusColor $rdsColor Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" Write-OutputColor "" -color "Info" Write-OutputColor " [B] ◄ Back to Server Config" -color "Info" diff --git a/Modules/49-MenuRunner.ps1 b/Modules/49-MenuRunner.ps1 index ae384c8..5a81fcb 100644 --- a/Modules/49-MenuRunner.ps1 +++ b/Modules/49-MenuRunner.ps1 @@ -257,9 +257,10 @@ function Start-Show-RolesFeaturesMenu { "12" { Show-SIEMForwarderManagement } "13" { Show-WindowsAdminCenterManagement } "14" { Show-DFSManagement } + "15" { Show-RDSManagement } "back" { return } default { - Write-OutputColor " Invalid choice. Enter 1-14 or B." -color "Error" + Write-OutputColor " Invalid choice. Enter 1-15 or B." -color "Error" Start-Sleep -Milliseconds 500 } } diff --git a/Modules/50-EntryPoint.ps1 b/Modules/50-EntryPoint.ps1 index d8351ac..e1d800f 100644 --- a/Modules/50-EntryPoint.ps1 +++ b/Modules/50-EntryPoint.ps1 @@ -418,6 +418,7 @@ function Assert-Elevation { @{ Action = 'NtpHardeningAudit'; Description = 'Read-only: report W32Time clock-tamper posture (phase-correction limits, auth mode) (JSON-aware)' } @{ Action = 'CertBindingAudit'; Description = 'Read-only: report RDP/WinRM listener certificate bindings + expiry (JSON-aware)' } @{ Action = 'DFSAudit'; Description = 'Read-only: report DFS namespace/replication role state, namespaces + replication groups (JSON-aware)' } + @{ Action = 'RDSAudit'; Description = 'Read-only: report RDS role state + licensing mode/servers (JSON-aware)' } @{ Action = 'Batch'; Description = 'JSON-driven full configuration' } ) if ($script:CLIOutputFormat -eq 'JSON') { @@ -2095,6 +2096,11 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px} $dfsAuditOk = Start-DFSAudit [Environment]::Exit([int](-not $dfsAuditOk)) } + 'RDSAudit' { + # Read-only RDS role + licensing posture (JSON-aware). + $rdsAuditOk = Start-RDSAudit + [Environment]::Exit([int](-not $rdsAuditOk)) + } 'Batch' { if (-not $script:CLIConfig) { Write-OutputColor " ERROR: -Action Batch requires -Config " -color "Error" diff --git a/Modules/80-RemoteDesktopServices.ps1 b/Modules/80-RemoteDesktopServices.ps1 new file mode 100644 index 0000000..e051725 --- /dev/null +++ b/Modules/80-RemoteDesktopServices.ps1 @@ -0,0 +1,246 @@ +#region ===== REMOTE DESKTOP SERVICES ===== +# RDS role lifecycle + Per-Device/Per-User licensing-mode configuration, plus a +# read-only audit. The licensing MODE has a 120-day grace period before CALs are +# required, so configuring it (and the license-server name) is the useful, safe +# first step for a single-host RDS. +# +# Deliberately deferred (documented, not silently dropped): +# - Full session-collection deployment (New-RDSessionDeployment): it reconfigures +# the server into an RDS deployment and requires a reboot — too heavy to ship +# without validation on a real server. +# - CAL / license-key activation: done in RD Licensing Manager against a license +# agreement; that key material is sensitive and best entered in the GUI. +# As a result this module handles NO license key and holds no secret. + +# Map of the RDS licensing-mode DWORD values used by the policy registry. +$script:RDSLicenseModeMap = @{ 2 = 'Per Device'; 4 = 'Per User' } + +# Report whether the RDS Session Host / Licensing role features are installed and +# whether the RemoteDesktop management tooling is present at all. +function Test-RDSRoleInstalled { + $r = @{ SessionHost = $false; Licensing = $false; ToolsAvailable = $false } + $r.ToolsAvailable = [bool](Get-Command Get-RDLicenseConfiguration -ErrorAction SilentlyContinue) + try { + if (Get-Command Get-WindowsFeature -ErrorAction SilentlyContinue) { + $sh = Get-WindowsFeature -Name 'RDS-RD-Server' -ErrorAction SilentlyContinue + $lic = Get-WindowsFeature -Name 'RDS-Licensing' -ErrorAction SilentlyContinue + if ($sh) { $r.SessionHost = ($sh.InstallState -eq 'Installed') } + if ($lic) { $r.Licensing = ($lic.InstallState -eq 'Installed') } + } + } catch {} + return $r +} + +# Read the RDS licensing mode + license-server list from the policy registry. +function Get-RDSLicenseMode { + $p = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + $res = [PSCustomObject]@{ Mode = 'Not configured (120-day grace)'; ModeValue = $null; LicenseServers = '' } + try { + $cfg = Get-ItemProperty -Path $p -ErrorAction SilentlyContinue + if ($cfg -and $null -ne $cfg.LicensingMode) { + $res.ModeValue = [int]$cfg.LicensingMode + $res.Mode = if ($script:RDSLicenseModeMap.ContainsKey([int]$cfg.LicensingMode)) { $script:RDSLicenseModeMap[[int]$cfg.LicensingMode] } else { "Unknown ($($cfg.LicensingMode))" } + } + if ($cfg -and $cfg.LicenseServers) { $res.LicenseServers = "$($cfg.LicenseServers)" } + } catch {} + return $res +} + +# Read-only RDS posture: role state + licensing mode/servers. +function Get-RDSStatus { + $roles = Test-RDSRoleInstalled + $lic = Get-RDSLicenseMode + return [PSCustomObject]@{ + ToolsAvailable = $roles.ToolsAvailable + SessionHostInstalled = $roles.SessionHost + LicensingInstalled = $roles.Licensing + LicenseMode = $lic.Mode + LicenseModeValue = $lic.ModeValue + LicenseServers = $lic.LicenseServers + } +} + +# Reversible: install the RD Session Host + RD Licensing role features. Captures +# which were missing so the session undo removes only the ones this action added. +function Install-RDSRoles { + $roles = Test-RDSRoleInstalled + if ($roles.SessionHost -and $roles.Licensing) { + Write-OutputColor " RD Session Host and RD Licensing roles are already installed." -color "Info" + return $true + } + if (-not (Test-WindowsServer)) { + Write-OutputColor " Remote Desktop Services is a Windows Server role — this OS is not a server SKU." -color "Error" + return $false + } + $needSh = -not $roles.SessionHost + $needLic = -not $roles.Licensing + + if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) { + $addSh = $needSh; $addLic = $needLic + Push-DryRunStep -Label "Install RDS roles (Session Host + Licensing)" -Category "Roles" -OneWay $false ` + -Preflight { if (-not (Test-WindowsServer)) { "Not a Windows Server SKU" } else { $true } }.GetNewClosure() ` + -Apply { Install-RDSRoles | Out-Null }.GetNewClosure() ` + -Undo { + if ($addLic) { Uninstall-WindowsFeature -Name 'RDS-Licensing' -ErrorAction SilentlyContinue | Out-Null } + if ($addSh) { Uninstall-WindowsFeature -Name 'RDS-RD-Server' -ErrorAction SilentlyContinue | Out-Null } + }.GetNewClosure() + Write-OutputColor " Queued (Dry-Run): install RDS roles." -color "Warning" + Add-SessionChange -Category "DryRun" -Description "Queued RDS role install" + return $true + } + + $ok = $true + if ($needSh) { + Write-OutputColor " Installing RD Session Host (RDS-RD-Server)..." -color "Info" + $r1 = Install-WindowsFeatureWithTimeout -FeatureName 'RDS-RD-Server' -DisplayName 'RD Session Host' -IncludeManagementTools + if (-not $r1.Success) { $ok = $false; Write-OutputColor " RD Session Host install failed." -color "Error"; if ($r1.Error) { Write-OutputColor " $($r1.Error.Trim())" -color "Error" } } + } + if ($needLic -and $ok) { + Write-OutputColor " Installing RD Licensing (RDS-Licensing)..." -color "Info" + $r2 = Install-WindowsFeatureWithTimeout -FeatureName 'RDS-Licensing' -DisplayName 'RD Licensing' -IncludeManagementTools + if (-not $r2.Success) { $ok = $false; Write-OutputColor " RD Licensing install failed." -color "Error"; if ($r2.Error) { Write-OutputColor " $($r2.Error.Trim())" -color "Error" } } + } + + if ($ok) { + Write-OutputColor " RDS roles installed. A reboot may be required before the Session Host is usable." -color "Success" + Add-SessionChange -Category "Roles" -Description "Installed RDS roles (Session Host + Licensing)" + Clear-MenuCache + # The Dry-Run queue owns undo when it re-invokes this during apply. + if (-not $script:ApplyingDryRunQueue) { + $undoSh = $needSh; $undoLic = $needLic + Add-UndoAction -Category "Roles" -Description "Installed RDS roles" -UndoScript { + param($RemoveSh, $RemoveLic) + if ($RemoveLic) { Uninstall-WindowsFeature -Name 'RDS-Licensing' -ErrorAction SilentlyContinue | Out-Null } + if ($RemoveSh) { Uninstall-WindowsFeature -Name 'RDS-RD-Server' -ErrorAction SilentlyContinue | Out-Null } + } -UndoParams @{ RemoveSh = $undoSh; RemoveLic = $undoLic } + } + } + return $ok +} + +# Reversible: set the RDS licensing mode + license-server list via the policy +# registry (the same values the "Set licensing mode" / "license servers" GPOs +# write, which the Session Host honours). No license key is involved. +function Set-RDSLicenseMode { + param( + [Parameter(Mandatory)] [ValidateSet('PerDevice', 'PerUser')] [string]$Mode, + [Parameter(Mandatory)] [string]$LicenseServer + ) + $server = $LicenseServer.Trim() + if ($server -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.\-]*$') { + Write-OutputColor " Invalid license server name (use a hostname/FQDN)." -color "Error"; return + } + $modeValue = if ($Mode -eq 'PerDevice') { 2 } else { 4 } + $p = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + + # Capture prior values (null = value/key absent) for a faithful undo. + $priorMode = $null; $priorServers = $null + if (Test-Path $p) { + $cur = Get-ItemProperty -Path $p -ErrorAction SilentlyContinue + if ($cur -and $null -ne $cur.LicensingMode) { $priorMode = [int]$cur.LicensingMode } + if ($cur -and $cur.LicenseServers) { $priorServers = "$($cur.LicenseServers)" } + } + + if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) { + $mv = $modeValue; $sv = $server; $pm = $priorMode; $ps = $priorServers; $path = $p + Push-DryRunStep -Label "Set RDS licensing: $Mode via $server" -Category "Roles" -OneWay $false ` + -Params @{ Mode = $Mode; LicenseServer = $server } ` + -Preflight { $true }.GetNewClosure() ` + -Apply { + if (-not (Test-Path $path)) { New-Item -Path $path -Force -ErrorAction SilentlyContinue | Out-Null } + Set-ItemProperty -Path $path -Name 'LicensingMode' -Value $mv -Type DWord -ErrorAction SilentlyContinue + Set-ItemProperty -Path $path -Name 'LicenseServers' -Value $sv -Type String -ErrorAction SilentlyContinue + }.GetNewClosure() ` + -Undo { + if ($null -ne $pm) { Set-ItemProperty -Path $path -Name 'LicensingMode' -Value $pm -Type DWord -ErrorAction SilentlyContinue } + else { Remove-ItemProperty -Path $path -Name 'LicensingMode' -ErrorAction SilentlyContinue } + if ($null -ne $ps) { Set-ItemProperty -Path $path -Name 'LicenseServers' -Value $ps -Type String -ErrorAction SilentlyContinue } + else { Remove-ItemProperty -Path $path -Name 'LicenseServers' -ErrorAction SilentlyContinue } + }.GetNewClosure() + Write-OutputColor " Queued (Dry-Run): set RDS licensing mode to $Mode ($server)." -color "Warning" + Add-SessionChange -Category "DryRun" -Description "Queued RDS licensing-mode change" + return + } + + try { + if (-not (Test-Path $p)) { New-Item -Path $p -Force -ErrorAction Stop | Out-Null } + Set-ItemProperty -Path $p -Name 'LicensingMode' -Value $modeValue -Type DWord -ErrorAction Stop + Set-ItemProperty -Path $p -Name 'LicenseServers' -Value $server -Type String -ErrorAction Stop + Write-OutputColor " RDS licensing set: $Mode via $server." -color "Success" + Write-OutputColor " Activate the license server and install CALs in RD Licensing Manager (licmgr)." -color "Info" + Add-SessionChange -Category "Roles" -Description "Set RDS licensing mode to $Mode ($server)" + Clear-MenuCache + Add-UndoAction -Category "Roles" -Description "Set RDS licensing mode to $Mode" -UndoScript { + param($Path, $PriorMode, $PriorServers) + if ($null -ne $PriorMode) { Set-ItemProperty -Path $Path -Name 'LicensingMode' -Value $PriorMode -Type DWord -ErrorAction SilentlyContinue } + else { Remove-ItemProperty -Path $Path -Name 'LicensingMode' -ErrorAction SilentlyContinue } + if ($null -ne $PriorServers) { Set-ItemProperty -Path $Path -Name 'LicenseServers' -Value $PriorServers -Type String -ErrorAction SilentlyContinue } + else { Remove-ItemProperty -Path $Path -Name 'LicenseServers' -ErrorAction SilentlyContinue } + } -UndoParams @{ Path = $p; PriorMode = $priorMode; PriorServers = $priorServers } + } + catch { + Write-OutputColor " Failed to set RDS licensing mode: $($_.Exception.Message)" -color "Error" + } +} + +# Interactive: RDS audit + optional role install + optional licensing-mode config. +function Show-RDSManagement { + Clear-Host + Write-CenteredOutput "Remote Desktop Services" -color "Info" + $s = Get-RDSStatus + $shColor = if ($s.SessionHostInstalled) { "Success" } else { "Warning" } + $licColor = if ($s.LicensingInstalled) { "Success" } else { "Warning" } + $modeColor = if ($s.LicenseModeValue) { "Success" } else { "Warning" } + Write-OutputColor "" -color "Info" + Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info" + Write-OutputColor " │$(" RDS STATUS".PadRight(72))│" -color "Info" + Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info" + Write-OutputColor " │$(" RD Session Host : $(if ($s.SessionHostInstalled) { 'Installed' } else { 'Not installed' })".PadRight(72))│" -color $shColor + Write-OutputColor " │$(" RD Licensing : $(if ($s.LicensingInstalled) { 'Installed' } else { 'Not installed' })".PadRight(72))│" -color $licColor + Write-OutputColor " │$(" Licensing mode : $($s.LicenseMode)".PadRight(72))│" -color $modeColor + if ($s.LicenseServers) { + $line = " License servers : $($s.LicenseServers)" + if ($line.Length -gt 72) { $line = $line.Substring(0, 69) + "..." } + Write-OutputColor " │$($line.PadRight(72))│" -color "Info" + } + Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info" + Write-OutputColor "" -color "Info" + Write-OutputColor " Full session-collection deployment and CAL activation are out of scope here:" -color "Info" + Write-OutputColor " use Server Manager (RDS deployment) and RD Licensing Manager (CALs) for those." -color "Info" + Write-OutputColor "" -color "Info" + + if (-not ($s.SessionHostInstalled -and $s.LicensingInstalled)) { + if (Confirm-UserAction -Message "Install the RD Session Host + RD Licensing roles?") { Install-RDSRoles } + } + + if (Confirm-UserAction -Message "Configure the RDS licensing mode now?") { + $modeChoice = (Read-Host " Licensing mode (1 = Per Device, 2 = Per User)").Trim() + $mode = switch ($modeChoice) { '1' { 'PerDevice' } '2' { 'PerUser' } default { $null } } + if (-not $mode) { + Write-OutputColor " Cancelled — enter 1 or 2." -color "Info" + } else { + $server = (Read-Host " RD license server hostname/FQDN").Trim() + if ([string]::IsNullOrWhiteSpace($server)) { Write-OutputColor " Cancelled — no server entered." -color "Info" } + else { Set-RDSLicenseMode -Mode $mode -LicenseServer $server } + } + } + Write-PressEnter +} + +# CLI: RDSAudit — read-only RDS posture (JSON-aware). +function Start-RDSAudit { + $s = Get-RDSStatus + if ($script:CLIOutputFormat -eq 'JSON') { + Write-Output (@{ + Tool = $script:ToolFullName; Version = $script:ScriptVersion; Action = 'RDSAudit' + Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); Hostname = $env:COMPUTERNAME + SessionHostInstalled = $s.SessionHostInstalled + LicensingInstalled = $s.LicensingInstalled + LicenseMode = $s.LicenseMode + LicenseServers = $s.LicenseServers + } | ConvertTo-Json) + } + else { Show-RDSManagement } + return $true +} +#endregion diff --git a/README.md b/README.md index 8130d8f..7ec812b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ OpenSSF Best Practices codecov PSScriptAnalyzer 0 errors - 5142 structural tests + 5167 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 200 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 201 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** -- 200 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** -- 201 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) @@ -141,7 +141,7 @@ Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process .\RackStack.ps1 ``` -> **`RackStack.ps1`** is the **modular loader** (~130 lines). It dot-sources all 80 modules from `Modules/` and starts the tool. Use this for development -- edit individual module files, then run. +> **`RackStack.ps1`** is the **modular loader** (~130 lines). It dot-sources all 81 modules from `Modules/` and starts the tool. Use this for development -- edit individual module files, then run. ### Single-File Deployment (Production) @@ -152,7 +152,7 @@ For production use, generate a monolithic single-file script (~66K lines) that y .\sync-to-monolithic.ps1 ``` -The output is **`RackStack v{version}.ps1`** -- a self-contained single file with all 80 modules baked in (version from `00-Initialization.ps1`). This is the file used to compile the `.exe`. +The output is **`RackStack v{version}.ps1`** -- a self-contained single file with all 81 modules baked in (version from `00-Initialization.ps1`). This is the file used to compile the `.exe`. > **Don't confuse the two:** `RackStack.ps1` = modular loader for development. `RackStack v{version}.ps1` = monolithic build for deployment/compilation. @@ -439,7 +439,7 @@ $report.Issues **Tiers:** `Light` (minimal, safe for prod), `Standard` (recommended), `Aggressive` (maximum cleanup/debloat). -### 200 CLI Actions +### 201 CLI Actions | Category | Actions | |----------|---------| @@ -475,7 +475,7 @@ Run `RackStack.exe -ListActions` or `RackStack.exe -ListActions -OutputFormat JS ``` RackStack/ -├── RackStack.ps1 # Modular loader -- dot-sources 80 modules (dev use) +├── RackStack.ps1 # Modular loader -- dot-sources 81 modules (dev use) ├── RackStack v{version}.ps1 # Monolithic build -- all modules in one file (deploy/compile) ├── RackStack.exe # Compiled from the monolithic .ps1 via ps2exe ├── defaults.json # Your environment config (gitignored) @@ -496,7 +496,7 @@ RackStack/ ### Module Architecture -80 modules numbered for load order. Dependencies flow downward. +81 modules numbered for load order. Dependencies flow downward. | Range | Category | Highlights | |---|---|---| @@ -528,7 +528,7 @@ RackStack/ # Full test suite (5,048 tests, ~4 minutes) powershell -ExecutionPolicy Bypass -File Tests\Run-Tests.ps1 -# PSScriptAnalyzer (0 errors on all 80 modules + monolithic) +# PSScriptAnalyzer (0 errors on all 81 modules + monolithic) powershell -ExecutionPolicy Bypass -File Tests\pssa-check.ps1 # Pre-release validation (parse + PSSA + structure + sync + version + tests) diff --git a/RackStack.ps1 b/RackStack.ps1 index 42a5d07..eb267d3 100644 --- a/RackStack.ps1 +++ b/RackStack.ps1 @@ -3,7 +3,7 @@ RackStack - Modular Loader (Development) .DESCRIPTION - This is the MODULAR LOADER -- it dot-sources all 80 modules from the Modules/ + This is the MODULAR LOADER -- it dot-sources all 81 modules from the Modules/ subfolder and starts RackStack. Use this file for development and testing. This is NOT the monolithic build. The monolithic single-file version is: @@ -13,7 +13,7 @@ Environment-specific settings are configured via defaults.json. .VERSION - 1.118.0 + 1.119.0 .NOTES - Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing) @@ -117,6 +117,7 @@ $moduleFiles = @( "77-WindowsAdminCenter.ps1" "78-CertificateAudit.ps1" "79-DFS.ps1" + "80-RemoteDesktopServices.ps1" ) # Load all modules diff --git a/RackStack.psd1 b/RackStack.psd1 index eebce69..14fb39b 100644 --- a/RackStack.psd1 +++ b/RackStack.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'RackStack.psm1' - ModuleVersion = '1.118.0' + ModuleVersion = '1.119.0' GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91' Author = 'TheAbider' CompanyName = 'TheAbider' diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1 index 0b64250..979f1f8 100644 --- a/Tests/Run-Tests.ps1 +++ b/Tests/Run-Tests.ps1 @@ -116,7 +116,7 @@ if (Test-Path $_testInitFile) { } } $monolithicPath = Join-Path (Join-Path $script:ModuleRoot "builds") "$_testToolFullName v$_testScriptVersion.ps1" -$expectedModuleCount = 80 # 00-79 inclusive +$expectedModuleCount = 81 # 00-80 inclusive # ============================================================================ # BANNER @@ -742,8 +742,8 @@ try { try { $firstName = $moduleFiles[0].Name $lastName = $moduleFiles[-1].Name - $pass = $firstName -eq "00-Initialization.ps1" -and $lastName -eq "79-DFS.ps1" - Write-TestResult "Module range 00-Initialization to 79-DFS" $pass "First=$firstName, Last=$lastName" + $pass = $firstName -eq "00-Initialization.ps1" -and $lastName -eq "80-RemoteDesktopServices.ps1" + Write-TestResult "Module range 00-Initialization to 80-RemoteDesktopServices" $pass "First=$firstName, Last=$lastName" } catch { Write-TestResult "Module range verification" $false $_.Exception.Message } @@ -2158,8 +2158,8 @@ try { if ($line -match '^\s*#region\s') { $regionStartCount++ } if ($line -match '^\s*#endregion') { $regionEndCount++ } } - Write-TestResult "Monolithic has 79 #region tags" ($regionStartCount -eq 79) "Found: $regionStartCount" - Write-TestResult "Monolithic has 79 #endregion tags" ($regionEndCount -eq 79) "Found: $regionEndCount" + Write-TestResult "Monolithic has 80 #region tags" ($regionStartCount -eq 80) "Found: $regionStartCount" + Write-TestResult "Monolithic has 80 #endregion tags" ($regionEndCount -eq 80) "Found: $regionEndCount" Write-TestResult "Region start/end counts match" ($regionStartCount -eq $regionEndCount) "Starts=$regionStartCount, Ends=$regionEndCount" } catch { Write-TestResult "Region count verification" $false $_.Exception.Message @@ -4490,7 +4490,7 @@ Write-TestResult "README.md exists" (Test-Path $readmePath) try { $readmeContent = Get-Content $readmePath -Raw - Write-TestResult "README: mentions 78 modules" ($readmeContent -match '80 module') + Write-TestResult "README: mentions 78 modules" ($readmeContent -match '81 module') Write-TestResult "README: has batch mode section" ($readmeContent -match 'Batch Mode') Write-TestResult "README: has testing section" ($readmeContent -match 'Testing') Write-TestResult "README: has defaults.json example" ($readmeContent -match 'defaults\.json') @@ -6898,11 +6898,11 @@ try { # RackStack.ps1 loader includes 62-HyperVReplica.ps1 $loaderContent = Get-Content $loaderPath -Raw Write-TestResult "RackStack.ps1: loads 62-HyperVReplica.ps1" ($loaderContent -match '62-HyperVReplica\.ps1') - Write-TestResult "RackStack.ps1: mentions 78 modules" ($loaderContent -match '80 modules') + Write-TestResult "RackStack.ps1: mentions 78 modules" ($loaderContent -match '81 modules') # Module count verification $moduleCount = (Get-ChildItem -Path $modulesPath -Filter "*.ps1").Count - Write-TestResult "Module count is 80" ($moduleCount -eq 80) "Found $moduleCount modules" + Write-TestResult "Module count is 81" ($moduleCount -eq 81) "Found $moduleCount modules" # Changelog mentions v1.4.0 $changelogPath = Join-Path $script:ModuleRoot "Changelog.md" @@ -9313,7 +9313,7 @@ try { Write-TestResult "48-MenuDisplay: Roles menu [14] DFS" ($menuD -match '\[14\]\s*DFS Namespaces') $runnerD = Get-Content "$modulesPath\49-MenuRunner.ps1" -Raw Write-TestResult "49-MenuRunner: Roles menu case 14 wired" ($runnerD -match '"14"\s*\{\s*Show-DFSManagement') - Write-TestResult "49-MenuRunner: Roles invalid msg bumped to 1-14" ($runnerD -match 'Enter 1-14 or B') + Write-TestResult "49-MenuRunner: Roles invalid msg uses numeric range format" ($runnerD -match 'Enter 1-\d+ or B') $dfsEntry = Get-Content "$modulesPath\50-EntryPoint.ps1" -Raw Write-TestResult "50-EntryPoint: DFSAudit dispatch case" ($dfsEntry -match "'DFSAudit'\s*\{") $dfsHeader = Get-Content (Join-Path $script:ModuleRoot "Header.ps1") -Raw @@ -9323,6 +9323,53 @@ catch { Write-TestResult "DFS Namespaces & Replication Tests" $false $_.Exception.Message } +# ============================================================================ +# SECTION 186: REMOTE DESKTOP SERVICES (v1.119.0, NEW module 80) +# ============================================================================ +Write-SectionHeader "SECTION 186: REMOTE DESKTOP SERVICES (80-RemoteDesktopServices)" + +try { + $rdsPath = "$modulesPath\80-RemoteDesktopServices.ps1" + Write-TestResult "80-RDS: module file exists" (Test-Path $rdsPath) + $rdsC = Get-Content $rdsPath -Raw + Write-TestResult "80-RDS: Test-RDSRoleInstalled exists" ($rdsC -match 'function\s+Test-RDSRoleInstalled\b') + Write-TestResult "80-RDS: Get-RDSStatus exists" ($rdsC -match 'function\s+Get-RDSStatus\b') + Write-TestResult "80-RDS: Install-RDSRoles exists" ($rdsC -match 'function\s+Install-RDSRoles\b') + Write-TestResult "80-RDS: Set-RDSLicenseMode exists" ($rdsC -match 'function\s+Set-RDSLicenseMode\b') + Write-TestResult "80-RDS: Show-RDSManagement exists" ($rdsC -match 'function\s+Show-RDSManagement\b') + Write-TestResult "80-RDS: Start-RDSAudit exists" ($rdsC -match 'function\s+Start-RDSAudit\b') + # Targets the real RDS role features. + Write-TestResult "80-RDS: targets RDS role features" ($rdsC -match 'RDS-RD-Server' -and $rdsC -match 'RDS-Licensing') + # Role install uses the mandated timeout wrapper. + Write-TestResult "80-RDS: role install uses timeout wrapper" ($rdsC -match 'Install-WindowsFeatureWithTimeout') + Write-TestResult "80-RDS: role install is reversible (undo)" ($rdsC -match 'function\s+Install-RDSRoles[\s\S]{0,3000}Add-UndoAction[\s\S]{0,400}Uninstall-WindowsFeature') + Write-TestResult "80-RDS: role install is server-SKU gated" ($rdsC -match 'Test-WindowsServer') + # Licensing mode is reversible (captures prior + undo). + Write-TestResult "80-RDS: license-mode change is reversible (undo)" ($rdsC -match 'function\s+Set-RDSLicenseMode[\s\S]{0,3500}Add-UndoAction[\s\S]{0,400}LicensingMode') + Write-TestResult "80-RDS: license-mode change is Dry-Run aware (reversible)" ($rdsC -match 'Set-RDSLicenseMode[\s\S]{0,2000}Push-DryRunStep[\s\S]{0,400}-OneWay \$false') + # Validates the license-server name before writing it. + Write-TestResult "80-RDS: validates license server name" ($rdsC -match 'LicenseServer[\s\S]{0,200}-notmatch') + # SECURITY: no license key / secret handling (deferred to RD Licensing Manager). + Write-TestResult "80-RDS: no license-key / secret handling" (-not ($rdsC -match 'AsSecureString|ConvertTo-SecureString|-Password|ProductKey|LicenseKey')) + Write-TestResult "80-RDS: documents deferred deployment + CAL activation" ($rdsC -match 'deferred' -and $rdsC -match 'New-RDSessionDeployment') + Write-TestResult "80-RDS: RDSAudit JSON-aware" ($rdsC -match "Start-RDSAudit[\s\S]{0,300}CLIOutputFormat -eq 'JSON'") + # Module + menu + CLI wiring. + $loaderE = Get-Content (Join-Path $script:ModuleRoot "RackStack.ps1") -Raw + Write-TestResult "RackStack.ps1: loads 80-RemoteDesktopServices" ($loaderE -match '80-RemoteDesktopServices\.ps1') + $menuE = Get-Content "$modulesPath\48-MenuDisplay.ps1" -Raw + Write-TestResult "48-MenuDisplay: Roles menu [15] RDS" ($menuE -match '\[15\]\s*Remote Desktop Services') + $runnerE = Get-Content "$modulesPath\49-MenuRunner.ps1" -Raw + Write-TestResult "49-MenuRunner: Roles menu case 15 wired" ($runnerE -match '"15"\s*\{\s*Show-RDSManagement') + Write-TestResult "49-MenuRunner: Roles invalid msg bumped to 1-15" ($runnerE -match 'Enter 1-15 or B') + $rdsEntry = Get-Content "$modulesPath\50-EntryPoint.ps1" -Raw + Write-TestResult "50-EntryPoint: RDSAudit dispatch case" ($rdsEntry -match "'RDSAudit'\s*\{") + $rdsHeader = Get-Content (Join-Path $script:ModuleRoot "Header.ps1") -Raw + Write-TestResult "Header.ps1: RDSAudit in -Action ValidateSet" ($rdsHeader -match "'RDSAudit'") +} +catch { + Write-TestResult "Remote Desktop Services Tests" $false $_.Exception.Message +} + # ============================================================================ # SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase) # ============================================================================ @@ -13334,7 +13381,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 200 entries" ($listActionCount -eq 200) "Found $listActionCount" + Write-TestResult "50-EntryPoint: action list has 201 entries" ($listActionCount -eq 201) "Found $listActionCount" } catch { Write-TestResult "v1.91.0 Tests" $false $_.Exception.Message } @@ -13367,7 +13414,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 200 entries" ($actionCount2 -eq 200) "Found $actionCount2" + Write-TestResult "50-EntryPoint: action list has 201 entries" ($actionCount2 -eq 201) "Found $actionCount2" } catch { Write-TestResult "v1.92.0 Tests" $false $_.Exception.Message } @@ -13393,7 +13440,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 200 entries" ($actionCount3 -eq 200) "Found $actionCount3" + Write-TestResult "50-EntryPoint: action list has 201 entries" ($actionCount3 -eq 201) "Found $actionCount3" } catch { Write-TestResult "v1.93.0 Tests" $false $_.Exception.Message } @@ -13431,7 +13478,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 200 entries" ($actionCount4 -eq 200) "Found $actionCount4" + Write-TestResult "50-EntryPoint: action list has 201 entries" ($actionCount4 -eq 201) "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 757c663..3550c02 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." 200 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." 201 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 d8405a4..7773077 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 200 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 201 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 cec5b04..dddcb88 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 200 CLI + "Windows is installed" and "server is in production." It provides 201 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, diff --git a/sync-to-monolithic.ps1 b/sync-to-monolithic.ps1 index 6caffee..4aa5225 100644 --- a/sync-to-monolithic.ps1 +++ b/sync-to-monolithic.ps1 @@ -42,7 +42,7 @@ if (-not (Test-Path $monoPath)) { $initModuleFiles = Get-ChildItem $modulesDir -Filter "*.ps1" | Sort-Object Name # Verify expected module count - $expectedModuleCount = 80 + $expectedModuleCount = 81 $actualModuleCount = @($initModuleFiles).Count if ($actualModuleCount -ne $expectedModuleCount) { Write-Warning "Expected $expectedModuleCount modules but found $actualModuleCount" @@ -120,7 +120,7 @@ Write-Host "Monolithic: $($monoLines.Count) lines" $moduleFiles = Get-ChildItem $modulesDir -Filter "*.ps1" | Sort-Object Name # Verify expected module count -$expectedModuleCount = 80 +$expectedModuleCount = 81 $actualModuleCount = @($moduleFiles).Count if ($actualModuleCount -ne $expectedModuleCount) { Write-Warning "Expected $expectedModuleCount modules but found $actualModuleCount"