From f56ae0caae097d164b148cfcd3f7428167438338 Mon Sep 17 00:00:00 2001
From: TheAbider <51920546+TheAbider@users.noreply.github.com>
Date: Fri, 29 May 2026 14:30:27 -0700
Subject: [PATCH] Add Windows Admin Center module (77-WindowsAdminCenter)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New module 77 installs and configures the Windows Admin Center gateway on
this host. Reachable from Roles & Features -> [13] Windows Admin Center and
via the WACSetup / WACStatus CLI actions.
* Install the gateway from an operator-provided MSI (default) or, opt-in and
confirmed, a download from Microsoft's official aka.ms URL. In BOTH cases
the MSI must pass an Authenticode check (signature Valid AND signed by
"O=Microsoft Corporation") before msiexec runs, with an optional SHA-256
pin — re-verified inside the Dry-Run Apply closure so a swapped file can't
slip through (no TOCTOU). The install binds a port (443 default, 6516
offered) + a TLS cert (existing LocalMachine thumbprint, or installer-
generated self-signed) and opens the firewall.
* Status (WACStatus): JSON-aware service / port / listening state.
* Uninstall: reversible (resolves the product code, msiexec /x).
Every state change is Dry-Run-aware and reversible — the install undo runs
msiexec /x and removes the firewall rule RackStack opened (Domain+Private
only; never operator/system rules). RackStack installs no unverified binary.
Wired into the loader, Header action set, entry-point dispatch (WACSetup /
WACStatus), Roles & Features menu (item 13), and menu runner. Module count
77 -> 78; CLI actions 189 -> 191; README / Help / dist counts updated in
lockstep (now enforced by the doc-freshness guard). 5025 tests, 0 failures;
PSScriptAnalyzer clean.
---
CONTRIBUTING.md | 2 +-
Changelog.md | 13 +
Header.ps1 | 6 +-
Modules/00-Initialization.ps1 | 2 +-
Modules/34-Help.ps1 | 2 +-
Modules/48-MenuDisplay.ps1 | 7 +
Modules/49-MenuRunner.ps1 | 3 +-
Modules/50-EntryPoint.ps1 | 12 +
Modules/77-WindowsAdminCenter.ps1 | 414 ++++++++++++++++++
README.md | 29 +-
RackStack.ps1 | 5 +-
RackStack.psd1 | 2 +-
Tests/Run-Tests.ps1 | 77 +++-
dist/chocolatey/rackstack.nuspec | 2 +-
dist/scoop/rackstack.json | 2 +-
.../TheAbider.RackStack.locale.en-US.yaml | 2 +-
sync-to-monolithic.ps1 | 4 +-
17 files changed, 544 insertions(+), 40 deletions(-)
create mode 100644 Modules/77-WindowsAdminCenter.ps1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 95aa42c..27f938b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,7 +30,7 @@ powershell -ExecutionPolicy Bypass -File Tests\pssa-check.ps1
## Pull Request Checklist
-- [ ] All 5,002 tests pass (`Run-Tests.ps1` exits with code 0)
+- [ ] All 5,025 tests pass (`Run-Tests.ps1` exits with code 0)
- [ ] PSScriptAnalyzer reports 0 errors (`pssa-check.ps1`)
- [ ] Monolithic synced (`sync-to-monolithic.ps1` shows 0 parse errors)
- [ ] New functions follow PowerShell verb-noun naming (`Get-`, `Set-`, `Test-`, `Show-`)
diff --git a/Changelog.md b/Changelog.md
index 78e255f..824d199 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,18 @@
# Changelog
+## v1.108.0
+
+Windows Admin Center — new module (77-WindowsAdminCenter.ps1), reachable from **Roles & Features → [13] Windows Admin Center (WAC)** and via the `WACSetup` / `WACStatus` CLI actions. Installs and configures the WAC gateway on this host.
+
+**What it does:**
+- **Install the gateway**: provide the WAC MSI (operator-staged path, or — opt-in and confirmed — download from Microsoft's official `aka.ms` URL). In **both** cases the MSI must pass an Authenticode check (signature valid **and** signed by Microsoft) before it runs, with an optional SHA-256 pin. The install binds the gateway to a port (443 by default; 6516 offered) and a TLS certificate — pick an existing LocalMachine certificate by thumbprint, or let the installer generate a self-signed one — and opens the chosen port in the firewall.
+- **Status** (`-Action WACStatus`): JSON-aware — service state, port, and whether the gateway is listening.
+- **Uninstall**: reversible removal (resolves the product code and runs `msiexec /x`).
+
+All state changes are Dry-Run-aware and reversible (install registers an `msiexec /x` undo; the firewall rule undo removes only the rule RackStack created). RackStack installs **no** unverified binary — the Authenticode-Microsoft check is mandatory regardless of source. WAC extension management and Azure-connected WAC are out of scope for this release.
+
+Module count: 77 → 78.
+
## v1.107.0
SIEM log forwarder — new module (76-SIEMForwarder.ps1), reachable from **Roles & Features → [12] SIEM Log Forwarder** and via the `SIEMSetup` / `SIEMStatus` CLI actions. This completes the security-operations workflow (NPS authentication → Always-On VPN gateway → CIS compliance scan → **log shipping**).
diff --git a/Header.ps1 b/Header.ps1
index c007369..ce09a70 100644
--- a/Header.ps1
+++ b/Header.ps1
@@ -3,7 +3,7 @@
RackStack - All-in-one Windows Server setup utility (Monolithic Build).
.DESCRIPTION
- This is the MONOLITHIC BUILD -- all 77 modules combined into a single file.
+ This is the MONOLITHIC BUILD -- all 78 modules combined into a single file.
Generated by sync-to-monolithic.ps1 from the modular source in Modules/.
The .exe is compiled from this file via ps2exe.
@@ -30,7 +30,7 @@
7h3 4b1d3r
.VERSION
- 1.107.0
+ 1.108.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')]
+ [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')]
[string]$Action,
[ValidateSet('Light', 'Standard', 'Aggressive')]
diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1
index c4d463e..5e6f97d 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.107.0"
+$script:ScriptVersion = "1.108.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 81e2ae1..490c740 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 = "189 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 = "191 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 af3df6f..7372da5 100644
--- a/Modules/48-MenuDisplay.ps1
+++ b/Modules/48-MenuDisplay.ps1
@@ -438,6 +438,12 @@ function Show-RolesFeaturesMenu {
} -CacheSeconds 120
$siemColor = if ($siemStatusText -eq "WEF configured") { "Success" } elseif ($siemStatusText -eq "Agent present") { "Warning" } else { "Warning" }
+ $wacStatusText = Get-CachedValue -Key "WACState" -FetchScript {
+ $w = Get-WACStatus
+ if (-not $w.Installed) { "Not Installed" } elseif ($w.ServiceStatus -eq 'Running') { "Running" } else { "Installed (stopped)" }
+ } -CacheSeconds 120
+ $wacColor = if ($wacStatusText -eq "Running") { "Success" } else { "Warning" }
+
Write-OutputColor "" -color "Info"
Write-OutputColor " ╔════════════════════════════════════════════════════════════════════════╗" -color "Info"
Write-OutputColor " ║$((" ROLES & FEATURES").PadRight(72))║" -color "Info"
@@ -457,6 +463,7 @@ function Show-RolesFeaturesMenu {
Write-MenuItem "[10] Network Policy Server (RADIUS) ►" -Status $npsStatusText -StatusColor $npsColor
Write-MenuItem "[11] Remote Access / Always-On VPN ►" -Status $aovpnStatusText -StatusColor $aovpnColor
Write-MenuItem "[12] SIEM Log Forwarder ►" -Status $siemStatusText -StatusColor $siemColor
+ Write-MenuItem "[13] Windows Admin Center (WAC) ►" -Status $wacStatusText -StatusColor $wacColor
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 80c112c..3545ea0 100644
--- a/Modules/49-MenuRunner.ps1
+++ b/Modules/49-MenuRunner.ps1
@@ -255,9 +255,10 @@ function Start-Show-RolesFeaturesMenu {
"10" { Show-NPSManagement }
"11" { Show-RemoteAccessManagement }
"12" { Show-SIEMForwarderManagement }
+ "13" { Show-WindowsAdminCenterManagement }
"back" { return }
default {
- Write-OutputColor " Invalid choice. Enter 1-12 or B." -color "Error"
+ Write-OutputColor " Invalid choice. Enter 1-13 or B." -color "Error"
Start-Sleep -Milliseconds 500
}
}
diff --git a/Modules/50-EntryPoint.ps1 b/Modules/50-EntryPoint.ps1
index d0b78b9..708b19f 100644
--- a/Modules/50-EntryPoint.ps1
+++ b/Modules/50-EntryPoint.ps1
@@ -407,6 +407,8 @@ function Assert-Elevation {
@{ Action = 'CISScan'; Description = 'Read-only CIS Windows Server L1 benchmark scan (scored subset; HTML + JSON report)' }
@{ Action = 'SIEMSetup'; Description = 'Configure SIEM log forwarding (WEF / Splunk / Winlogbeat); interactive, JSON-aware status' }
@{ Action = 'SIEMStatus'; Description = 'Show SIEM log-forwarder readiness (WEF / WinRM / agents / Arc)' }
+ @{ Action = 'WACSetup'; Description = 'Install + configure the Windows Admin Center gateway (verified MSI, port, certificate)' }
+ @{ Action = 'WACStatus'; Description = 'Show Windows Admin Center gateway status (service, port, listening)' }
@{ Action = 'Batch'; Description = 'JSON-driven full configuration' }
)
if ($script:CLIOutputFormat -eq 'JSON') {
@@ -2029,6 +2031,16 @@ footer{text-align:center;color:#999;font-size:12px;padding:16px}
$siemOk = Start-SIEMSetup
[Environment]::Exit([int](-not $siemOk))
}
+ 'WACStatus' {
+ # Read-only Windows Admin Center gateway status (JSON-aware).
+ $wacOk = Start-WACStatus
+ [Environment]::Exit([int](-not $wacOk))
+ }
+ 'WACSetup' {
+ # Install/configure the WAC gateway (interactive console; JSON status headless).
+ $wacOk = Start-WACSetup
+ [Environment]::Exit([int](-not $wacOk))
+ }
'Batch' {
if (-not $script:CLIConfig) {
Write-OutputColor " ERROR: -Action Batch requires -Config " -color "Error"
diff --git a/Modules/77-WindowsAdminCenter.ps1 b/Modules/77-WindowsAdminCenter.ps1
new file mode 100644
index 0000000..0178f80
--- /dev/null
+++ b/Modules/77-WindowsAdminCenter.ps1
@@ -0,0 +1,414 @@
+#region ===== WINDOWS ADMIN CENTER (WAC GATEWAY) =====
+# Install and configure the Windows Admin Center gateway on this host. WAC is
+# Microsoft's browser-based management surface; this module covers the parts
+# that script cleanly: install the gateway MSI, bind it to a port + TLS cert,
+# open the firewall, report status, and uninstall — every step reversible.
+#
+# Supply chain: RackStack never runs an installer it hasn't verified. The MSI
+# source is operator-controlled — an operator-provided local path (default), or
+# (opt-in, confirmed) a download from Microsoft's official https://aka.ms URL —
+# and in BOTH cases the file must pass an Authenticode check (signature Valid
+# AND signed by "O=Microsoft Corporation") before msiexec runs, with an optional
+# SHA-256 pin. A failed check deletes any downloaded file and refuses to install.
+#
+# Out of scope for v1 (WAC packaging/direction is in flux): extension management,
+# Azure-connected / Arc-integrated WAC, post-install cert re-binding automation
+# (printed as guidance), and managed-node setup (node readiness already lives in
+# the WACConnectivityAudit action). This module is gateway-host-only.
+#
+# Secrets: none — WAC uses Windows / Azure AD auth; nothing SecureString-worthy
+# is collected, so no hardened-state staging is needed.
+
+$script:WACServiceName = 'ServerManagementGateway'
+$script:WACDownloadUrl = 'https://aka.ms/WACDownload'
+
+# Is the WAC gateway installed? Service presence is authoritative.
+function Test-WACInstalled {
+ return ($null -ne (Get-Service -Name $script:WACServiceName -ErrorAction SilentlyContinue))
+}
+
+# Resolve the WAC product code (GUID) from the Uninstall registry, for /x.
+function Get-WACProductCode {
+ foreach ($root in @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
+ 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall')) {
+ try {
+ $match = Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue | Where-Object {
+ $dn = (Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction SilentlyContinue).DisplayName
+ $dn -match 'Windows Admin Center'
+ } | Select-Object -First 1
+ if ($match) { return $match.PSChildName }
+ }
+ catch { }
+ }
+ return $null
+}
+
+# Status object (peer of Get-NPSStatus / Get-SIEMForwarderStatus).
+function Get-WACStatus {
+ $svc = Get-Service -Name $script:WACServiceName -ErrorAction SilentlyContinue
+ $installed = ($null -ne $svc)
+ $port = $null; $listening = $false
+ if ($installed) {
+ try { $port = (Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\ServerManagementExperience' -ErrorAction Stop).SmePort } catch { }
+ if ($port) {
+ try { $listening = [bool](Get-NetTCPConnection -State Listen -LocalPort ([int]$port) -ErrorAction SilentlyContinue) } catch { }
+ }
+ }
+ return [PSCustomObject]@{
+ Installed = $installed
+ ServiceStatus = if ($svc) { "$($svc.Status)" } else { "" }
+ Port = $port
+ PortListening = $listening
+ }
+}
+
+# Validate an operator-supplied MSI path (rooted, exists, .msi, no traversal/reparse).
+function Test-WACSafeMsiPath {
+ param([string]$Path)
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
+ if ($Path -match '\.\.' -or $Path.Contains([char]0) -or $Path.Length -gt 255) { return $false }
+ if (-not [System.IO.Path]::IsPathRooted($Path)) { return $false }
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false }
+ if ([System.IO.Path]::GetExtension($Path).ToLower() -ne '.msi') { return $false }
+ try {
+ $item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop
+ if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { return $false }
+ }
+ catch { return $false }
+ return $true
+}
+
+# Fail-closed Authenticode (+ optional SHA-256) gate. The MSI must be Valid and
+# signed by Microsoft before it is ever executed.
+function Test-WACMsiSignature {
+ param([Parameter(Mandatory = $true)][string]$Path, [string]$ExpectedHash)
+ try {
+ $sig = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction Stop
+ if ("$($sig.Status)" -ne 'Valid') {
+ Write-OutputColor " Authenticode signature is not Valid (status: $($sig.Status)) — refusing to install." -color "Error"
+ return $false
+ }
+ if (($null -eq $sig.SignerCertificate) -or ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation')) {
+ Write-OutputColor " MSI is not signed by Microsoft Corporation — refusing to install." -color "Error"
+ return $false
+ }
+ }
+ catch {
+ Write-OutputColor " Could not verify the MSI signature: $($_.Exception.Message)" -color "Error"
+ return $false
+ }
+ if (-not [string]::IsNullOrWhiteSpace($ExpectedHash)) {
+ $h = (Get-FileHash -LiteralPath $Path -Algorithm SHA256 -ErrorAction SilentlyContinue).Hash
+ if ($h -ne $ExpectedHash.Trim()) {
+ Write-OutputColor " SHA-256 does not match the expected hash — refusing to install." -color "Error"
+ return $false
+ }
+ }
+ return $true
+}
+
+# Download the WAC MSI from Microsoft's official URL (opt-in, confirmed by caller).
+# Returns the local temp path, or $null on failure (temp file deleted on failure).
+function Get-WACMsiDownload {
+ $dest = Join-Path $script:TempPath ("WindowsAdminCenter-" + [System.Guid]::NewGuid().ToString("N") + ".msi")
+ try {
+ [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
+ Write-OutputColor " Downloading the WAC MSI from $($script:WACDownloadUrl) ..." -color "Info"
+ Invoke-WebRequest -Uri $script:WACDownloadUrl -OutFile $dest -UseBasicParsing -ErrorAction Stop
+ return $dest
+ }
+ catch {
+ Write-OutputColor " Download failed: $($_.Exception.Message)" -color "Error"
+ if (Test-Path -LiteralPath $dest) { Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue }
+ return $null
+ }
+}
+
+# Let the operator pick an existing Server-Auth certificate by thumbprint, or
+# return $null to let the installer generate a self-signed certificate.
+function Select-WACCertificate {
+ $serverAuth = '1.3.6.1.5.5.7.3.1'
+ $certs = @(Get-ChildItem -Path Cert:\LocalMachine\My -ErrorAction SilentlyContinue | Where-Object {
+ $_.HasPrivateKey -and $_.NotAfter -gt (Get-Date) -and
+ ((@($_.EnhancedKeyUsageList).Count -eq 0) -or (@($_.EnhancedKeyUsageList | ForEach-Object { $_.ObjectId }) -contains $serverAuth))
+ })
+ if ($certs.Count -eq 0) {
+ Write-OutputColor " No suitable Server-Authentication certificate in LocalMachine\My." -color "Warning"
+ Write-OutputColor " The installer will generate a self-signed certificate." -color "Info"
+ return $null
+ }
+ Write-OutputColor " Certificates available for the WAC gateway:" -color "Info"
+ for ($i = 0; $i -lt $certs.Count; $i++) {
+ Write-OutputColor (" [{0}] {1} (expires {2})" -f ($i + 1), $certs[$i].Subject, $certs[$i].NotAfter.ToString('yyyy-MM-dd')) -color "Info"
+ }
+ Write-OutputColor " [0] Generate a self-signed certificate instead" -color "Info"
+ $pick = (Read-Host " Select").Trim()
+ if ($pick -eq '0' -or [string]::IsNullOrWhiteSpace($pick)) { return $null }
+ if ($pick -match '^\d+$' -and [int]$pick -ge 1 -and [int]$pick -le $certs.Count) {
+ return $certs[[int]$pick - 1].Thumbprint
+ }
+ Write-OutputColor " Invalid selection; the installer will generate a self-signed certificate." -color "Warning"
+ return $null
+}
+
+# Open the chosen WAC TCP port (reversible — undo removes only our rule).
+function Enable-WACFirewallRule {
+ param([Parameter(Mandatory = $true)][int]$Port)
+ $ruleName = "RackStack WAC HTTPS $Port"
+ if (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue) { return }
+ try {
+ New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Protocol TCP -LocalPort $Port `
+ -Action Allow -Profile Domain, Private -ErrorAction Stop | Out-Null
+ Write-OutputColor " Opened TCP $Port (Domain + Private profiles)." -color "Success"
+ Add-UndoAction -Category "Security" -Description "Opened WAC firewall port $Port" -UndoScript {
+ param($Name)
+ Remove-NetFirewallRule -DisplayName $Name -ErrorAction SilentlyContinue
+ } -UndoParams @{ Name = $ruleName }
+ }
+ catch {
+ Write-OutputColor " Could not open the firewall port: $($_.Exception.Message)" -color "Warning"
+ }
+}
+
+# Run msiexec to install the (already-verified) WAC MSI. Returns the exit code.
+function Invoke-WACInstaller {
+ param([string]$Msi, [int]$Port, [string]$CertOption, [string]$Thumbprint)
+ $log = Join-Path $script:TempPath ("wac-install-" + (Get-Date -Format 'yyyyMMddHHmmss') + ".log")
+ $msiArgs = @('/i', $Msi, '/qn', '/norestart', "SME_PORT=$Port", "SSL_CERTIFICATE_OPTION=$CertOption")
+ if ($CertOption -eq 'installed' -and -not [string]::IsNullOrWhiteSpace($Thumbprint)) {
+ $msiArgs += "SME_THUMBPRINT=$Thumbprint"
+ }
+ $msiArgs += @('/l*v', $log)
+ $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList $msiArgs -Wait -PassThru
+ if ($proc.ExitCode -notin @(0, 3010, 1641)) {
+ Write-OutputColor " msiexec exited $($proc.ExitCode). See log: $log" -color "Error"
+ }
+ return $proc.ExitCode
+}
+
+# Interactive: install + configure the WAC gateway.
+function Install-WAC {
+ Clear-Host
+ Write-CenteredOutput "Install Windows Admin Center" -color "Info"
+ if (Test-WACInstalled) {
+ Write-OutputColor " Windows Admin Center is already installed." -color "Info"; return
+ }
+
+ # --- Source: operator-provided MSI, or opt-in Microsoft download. ---
+ Write-OutputColor " Path to the Windows Admin Center MSI (blank to download from Microsoft):" -color "Info"
+ $msiPath = (Read-Host).Trim().Trim('"')
+ $navResult = Test-NavigationCommand -UserInput $msiPath
+ if ($navResult.ShouldReturn) { return }
+ $downloaded = $false
+ if ([string]::IsNullOrWhiteSpace($msiPath)) {
+ if (-not (Confirm-UserAction -Message "Download the WAC MSI from $($script:WACDownloadUrl)?")) {
+ Write-OutputColor " Cancelled — provide an MSI path to install offline." -color "Info"; return
+ }
+ $msiPath = Get-WACMsiDownload
+ if (-not $msiPath) { return }
+ $downloaded = $true
+ }
+ elseif (-not (Test-WACSafeMsiPath $msiPath)) {
+ Write-OutputColor " MSI path is invalid (must be a rooted .msi file that exists)." -color "Error"; return
+ }
+
+ # --- Verify before doing anything else. ---
+ if (-not (Test-WACMsiSignature -Path $msiPath)) {
+ if ($downloaded -and (Test-Path -LiteralPath $msiPath)) { Remove-Item -LiteralPath $msiPath -Force -ErrorAction SilentlyContinue }
+ return
+ }
+ Write-OutputColor " MSI verified (Microsoft-signed)." -color "Success"
+
+ # --- Port + certificate. ---
+ Write-OutputColor " Gateway port [default 443; 6516 is the common alternative]:" -color "Info"
+ $portIn = (Read-Host).Trim()
+ $port = 443
+ if ($portIn -match '^\d+$') { $port = [int]$portIn }
+ if ($port -lt 1 -or $port -gt 65535) { Write-OutputColor " Invalid port number. Enter 1-65535." -color "Error"; return }
+
+ $thumb = Select-WACCertificate
+ $certOption = if ([string]::IsNullOrWhiteSpace($thumb)) { 'generate' } else { 'installed' }
+
+ if (-not (Confirm-UserAction -Message "Install WAC on port $port (certificate: $certOption)?")) {
+ Write-OutputColor " Cancelled." -color "Info"
+ if ($downloaded -and (Test-Path -LiteralPath $msiPath)) { Remove-Item -LiteralPath $msiPath -Force -ErrorAction SilentlyContinue }
+ return
+ }
+
+ if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) {
+ $capMsi = $msiPath; $capPort = $port; $capOpt = $certOption; $capThumb = $thumb; $capDl = $downloaded
+ Push-DryRunStep -Label "Install Windows Admin Center (port $capPort)" -Category "Roles" -OneWay $false `
+ -Params @{ Port = $capPort; CertOption = $capOpt } `
+ -Preflight { if (Test-WACInstalled) { "WAC already installed" } else { $true } }.GetNewClosure() `
+ -Apply {
+ # Re-verify the MSI at commit time — the file could have been swapped
+ # between queueing and apply (TOCTOU). Never install an unverified MSI.
+ if (-not (Test-WACMsiSignature -Path $capMsi)) {
+ Write-OutputColor " MSI failed verification at apply time — aborting install." -color "Error"
+ if ($capDl -and (Test-Path -LiteralPath $capMsi)) { Remove-Item -LiteralPath $capMsi -Force -ErrorAction SilentlyContinue }
+ return
+ }
+ $code = Invoke-WACInstaller -Msi $capMsi -Port $capPort -CertOption $capOpt -Thumbprint $capThumb
+ if ($code -in @(0, 3010, 1641)) { Enable-WACFirewallRule -Port $capPort }
+ if ($capDl -and (Test-Path -LiteralPath $capMsi)) { Remove-Item -LiteralPath $capMsi -Force -ErrorAction SilentlyContinue }
+ }.GetNewClosure() `
+ -Undo {
+ # Reverse both the install AND the firewall rule we open on apply.
+ $pc = Get-WACProductCode
+ if ($pc) { Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/x', $pc, '/qn', '/norestart') -Wait -PassThru | Out-Null }
+ Remove-NetFirewallRule -DisplayName "RackStack WAC HTTPS $capPort" -ErrorAction SilentlyContinue
+ }.GetNewClosure()
+ Write-OutputColor " Queued (Dry-Run): install Windows Admin Center." -color "Warning"
+ Add-SessionChange -Category "DryRun" -Description "Queued WAC install (port $capPort)"
+ return
+ }
+
+ $code = Invoke-WACInstaller -Msi $msiPath -Port $port -CertOption $certOption -Thumbprint $thumb
+ if ($downloaded -and (Test-Path -LiteralPath $msiPath)) { Remove-Item -LiteralPath $msiPath -Force -ErrorAction SilentlyContinue }
+ if ($code -notin @(0, 3010, 1641)) {
+ Write-OutputColor " WAC installation failed (exit $code)." -color "Error"; return
+ }
+
+ # Post-install verification.
+ $ok = $false
+ for ($w = 0; $w -lt 6; $w++) { if (Test-WACInstalled) { $ok = $true; break }; Start-Sleep -Seconds 5 }
+ if (-not $ok) {
+ Write-OutputColor " Installer finished but the gateway service was not detected." -color "Warning"
+ }
+ else {
+ Write-OutputColor " Windows Admin Center installed (port $port)." -color "Success"
+ }
+ if ($code -in @(3010, 1641)) {
+ $script:RebootNeeded = $true
+ Write-OutputColor " A restart is required to complete installation." -color "Warning"
+ }
+ Enable-WACFirewallRule -Port $port
+ Add-SessionChange -Category "Roles" -Description "Installed Windows Admin Center (port $port)"
+ Add-UndoAction -Category "Roles" -Description "Installed Windows Admin Center" -UndoScript {
+ $pc = Get-WACProductCode
+ if ($pc) { Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/x', $pc, '/qn', '/norestart') -Wait -PassThru | Out-Null }
+ }
+ Clear-MenuCache
+}
+
+# Interactive: uninstall the WAC gateway.
+function Uninstall-WAC {
+ Clear-Host
+ Write-CenteredOutput "Uninstall Windows Admin Center" -color "Info"
+ if (-not (Test-WACInstalled)) {
+ Write-OutputColor " Windows Admin Center is not installed." -color "Info"; return
+ }
+ $pc = Get-WACProductCode
+ if (-not $pc) {
+ Write-OutputColor " Could not resolve the WAC product code — remove it from Programs & Features." -color "Error"; return
+ }
+ if (-not (Confirm-UserAction -Message "Uninstall Windows Admin Center?")) {
+ Write-OutputColor " Cancelled." -color "Info"; return
+ }
+
+ if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) {
+ $capPc = $pc
+ Push-DryRunStep -Label "Uninstall Windows Admin Center" -Category "Roles" -OneWay $false `
+ -Params @{ ProductCode = $capPc } `
+ -Preflight { if (Test-WACInstalled) { $true } else { "WAC not installed" } }.GetNewClosure() `
+ -Apply { Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/x', $capPc, '/qn', '/norestart') -Wait -PassThru | Out-Null }.GetNewClosure() `
+ -Undo { Write-OutputColor " (WAC re-install is not automatic — re-run Install Windows Admin Center.)" -color "Info" }.GetNewClosure()
+ Write-OutputColor " Queued (Dry-Run): uninstall Windows Admin Center." -color "Warning"
+ Add-SessionChange -Category "DryRun" -Description "Queued WAC uninstall"
+ return
+ }
+
+ $proc = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/x', $pc, '/qn', '/norestart') -Wait -PassThru
+ if ($proc.ExitCode -in @(0, 3010, 1641)) {
+ Write-OutputColor " Windows Admin Center uninstalled." -color "Success"
+ if ($proc.ExitCode -in @(3010, 1641)) { $script:RebootNeeded = $true }
+ Add-SessionChange -Category "Roles" -Description "Uninstalled Windows Admin Center"
+ Clear-MenuCache
+ }
+ else {
+ Write-OutputColor " Uninstall failed (exit $($proc.ExitCode))." -color "Error"
+ }
+}
+
+# Show the current WAC configuration (read-only).
+function Show-WACConfig {
+ Clear-Host
+ Write-CenteredOutput "Windows Admin Center Status" -color "Info"
+ $s = Get-WACStatus
+ Write-OutputColor " Installed : $($s.Installed)" -color "Info"
+ if ($s.Installed) {
+ Write-OutputColor " Service status : $($s.ServiceStatus)" -color "Info"
+ Write-OutputColor " Gateway port : $(if ($s.Port) { $s.Port } else { 'unknown' })" -color "Info"
+ Write-OutputColor " Port listening : $($s.PortListening)" -color "Info"
+ if ($s.Port) { Write-OutputColor " URL : https://$($env:COMPUTERNAME):$($s.Port)" -color "Info" }
+ }
+}
+
+# CLI: WACStatus — read-only readiness (JSON-aware).
+function Start-WACStatus {
+ $s = Get-WACStatus
+ if ($script:CLIOutputFormat -eq 'JSON') {
+ Write-Output (@{
+ Tool = $script:ToolFullName; Version = $script:ScriptVersion; Action = 'WACStatus'
+ Timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss"); Hostname = $env:COMPUTERNAME
+ Installed = $s.Installed; ServiceStatus = $s.ServiceStatus; Port = $s.Port; PortListening = $s.PortListening
+ } | ConvertTo-Json)
+ }
+ else {
+ Show-WACConfig
+ }
+ return $true
+}
+
+# CLI: WACSetup — JSON status for automation; interactive menu on console.
+function Start-WACSetup {
+ if ($script:CLIOutputFormat -eq 'JSON') { return (Start-WACStatus) }
+ Show-WindowsAdminCenterManagement
+ return $true
+}
+
+# Windows Admin Center submenu (Roles & Features > [13]).
+function Show-WindowsAdminCenterManagement {
+ while ($true) {
+ if ($script:ReturnToMainMenu) { return }
+ Clear-Host
+ Write-OutputColor "" -color "Info"
+ Write-OutputColor " ╔════════════════════════════════════════════════════════════════════════╗" -color "Info"
+ Write-OutputColor " ║$((" WINDOWS ADMIN CENTER (WAC)").PadRight(72))║" -color "Info"
+ Write-OutputColor " ╚════════════════════════════════════════════════════════════════════════╝" -color "Info"
+ Write-OutputColor "" -color "Info"
+
+ $s = Get-WACStatus
+ $statusText = if (-not $s.Installed) { "Not Installed" } elseif ($s.ServiceStatus -eq 'Running') { "Running" } else { "Installed (stopped)" }
+ $statusColor = if ($s.ServiceStatus -eq 'Running') { "Success" } else { "Warning" }
+
+ Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
+ Write-MenuItem " Gateway" -Status $statusText -StatusColor $statusColor
+ Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
+ Write-MenuItem "[1] Install Windows Admin Center" -Status "Verified MSI + port + certificate" -StatusColor "Info"
+ Write-MenuItem "[2] Show Status" -Status "Service, port, URL" -StatusColor "Info"
+ Write-MenuItem "[3] Uninstall" -Status "Reversible removal" -StatusColor "Info"
+ Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
+ Write-OutputColor "" -color "Info"
+ Write-OutputColor " [B] ◄ Back" -color "Info"
+ Write-OutputColor "" -color "Info"
+
+ $choice = Read-Host " Select"
+ $navResult = Test-NavigationCommand -UserInput $choice
+ if ($navResult.ShouldReturn) {
+ if (Invoke-NavigationAction -NavResult $navResult) { return }
+ return
+ }
+ switch ("$choice".ToUpper()) {
+ "1" { Install-WAC; Write-PressEnter }
+ "2" { Show-WACConfig; Write-PressEnter }
+ "3" { Uninstall-WAC; Write-PressEnter }
+ "B" { return }
+ default {
+ Write-OutputColor " Invalid choice. Enter 1-3 or B." -color "Error"
+ Start-Sleep -Seconds 1
+ }
+ }
+ }
+}
+#endregion
diff --git a/README.md b/README.md
index 42f4c20..4feb343 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
-
+
@@ -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 189 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 191 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,12 +61,14 @@ 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** -- 189 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** -- 191 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)
**Security Operations** -- Network Policy Server (RADIUS) for 802.1X / VPN authentication, Always-On VPN (RRAS gateway + device/user-tunnel ProfileXML generation), CIS Windows Server L1 compliance scanning (read-only, severity-weighted score + HTML/JSON report), and SIEM log forwarding (Windows Event Forwarding plus Splunk Universal Forwarder / Elastic Winlogbeat config); plus Group Policy backup/restore/drift and Just Enough Administration (JEA) constrained endpoints
+**Windows Admin Center** -- Install and configure the WAC gateway (Authenticode-verified MSI, gateway port + TLS certificate selection, firewall), with status and reversible uninstall
+
**Monitoring & Diagnostics** -- Health dashboard (disk I/O latency, NIC errors, memory pressure, Hyper-V guest health, top CPU processes), performance snapshots with trend reports and "days until full" estimates, event log viewer, service manager, network diagnostics (ping, traceroute, port test, subnet sweep, DNS, ARP)
**Drift Detection** -- Save configuration baselines, compare snapshots over time, track setting changes across baselines, auto-baseline after batch mode
@@ -139,7 +141,7 @@ Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
.\RackStack.ps1
```
-> **`RackStack.ps1`** is the **modular loader** (~130 lines). It dot-sources all 77 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 78 modules from `Modules/` and starts the tool. Use this for development -- edit individual module files, then run.
### Single-File Deployment (Production)
@@ -150,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 77 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 78 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.
@@ -437,7 +439,7 @@ $report.Issues
**Tiers:** `Light` (minimal, safe for prod), `Standard` (recommended), `Aggressive` (maximum cleanup/debloat).
-### 189 CLI Actions
+### 191 CLI Actions
| Category | Actions |
|----------|---------|
@@ -473,7 +475,7 @@ Run `RackStack.exe -ListActions` or `RackStack.exe -ListActions -OutputFormat JS
```
RackStack/
-├── RackStack.ps1 # Modular loader -- dot-sources 77 modules (dev use)
+├── RackStack.ps1 # Modular loader -- dot-sources 78 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)
@@ -482,10 +484,10 @@ RackStack/
├── Modules/
│ ├── 00-Initialization.ps1 # Constants, variables, config loading
│ ├── 01-Console.ps1 # Console window management
-│ ├── ... # 74 more modules
-│ └── 76-SIEMForwarder.ps1
+│ ├── ... # 75 more modules
+│ └── 77-WindowsAdminCenter.ps1
├── Tests/
-│ ├── Run-Tests.ps1 # 5,002 automated tests
+│ ├── Run-Tests.ps1 # 5,025 automated tests
│ ├── Validate-Release.ps1 # Pre-release validation suite
│ └── ...
└── docs/
@@ -494,7 +496,7 @@ RackStack/
### Module Architecture
-77 modules numbered for load order. Dependencies flow downward.
+78 modules numbered for load order. Dependencies flow downward.
| Range | Category | Highlights |
|---|---|---|
@@ -518,14 +520,15 @@ RackStack/
| 74 | **Always-On VPN** | Remote Access (RRAS) VPN gateway + ProfileXML generation |
| 75 | **Compliance** | CIS Windows Server L1 benchmark scanner (read-only, scored) |
| 76 | **SIEM Forwarder** | WEF / Splunk / Winlogbeat log forwarding + Sentinel readiness |
+| 77 | **Windows Admin Center** | WAC gateway install (verified MSI) + port / TLS cert config |
## Testing
```powershell
-# Full test suite (5,002 tests, ~4 minutes)
+# Full test suite (5,025 tests, ~4 minutes)
powershell -ExecutionPolicy Bypass -File Tests\Run-Tests.ps1
-# PSScriptAnalyzer (0 errors on all 77 modules + monolithic)
+# PSScriptAnalyzer (0 errors on all 78 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 e758d6f..b37577a 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 77 modules from the Modules/
+ This is the MODULAR LOADER -- it dot-sources all 78 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.107.0
+ 1.108.0
.NOTES
- Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing)
@@ -114,6 +114,7 @@ $moduleFiles = @(
"74-AlwaysOnVPN.ps1"
"75-Compliance.ps1"
"76-SIEMForwarder.ps1"
+ "77-WindowsAdminCenter.ps1"
)
# Load all modules
diff --git a/RackStack.psd1 b/RackStack.psd1
index e78e770..b0e8cb0 100644
--- a/RackStack.psd1
+++ b/RackStack.psd1
@@ -1,6 +1,6 @@
@{
RootModule = 'RackStack.psm1'
- ModuleVersion = '1.107.0'
+ ModuleVersion = '1.108.0'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1
index 4ccec86..a9f8fe1 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 = 77 # 00-76 inclusive
+$expectedModuleCount = 78 # 00-77 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 "76-SIEMForwarder.ps1"
- Write-TestResult "Module range 00-Initialization to 76-SIEMForwarder" $pass "First=$firstName, Last=$lastName"
+ $pass = $firstName -eq "00-Initialization.ps1" -and $lastName -eq "77-WindowsAdminCenter.ps1"
+ Write-TestResult "Module range 00-Initialization to 77-WindowsAdminCenter" $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 76 #region tags" ($regionStartCount -eq 76) "Found: $regionStartCount"
- Write-TestResult "Monolithic has 76 #endregion tags" ($regionEndCount -eq 76) "Found: $regionEndCount"
+ Write-TestResult "Monolithic has 77 #region tags" ($regionStartCount -eq 77) "Found: $regionStartCount"
+ Write-TestResult "Monolithic has 77 #endregion tags" ($regionEndCount -eq 77) "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 77 modules" ($readmeContent -match '77 module')
+ Write-TestResult "README: mentions 78 modules" ($readmeContent -match '78 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 77 modules" ($loaderContent -match '77 modules')
+ Write-TestResult "RackStack.ps1: mentions 78 modules" ($loaderContent -match '78 modules')
# Module count verification
$moduleCount = (Get-ChildItem -Path $modulesPath -Filter "*.ps1").Count
- Write-TestResult "Module count is 77" ($moduleCount -eq 77) "Found $moduleCount modules"
+ Write-TestResult "Module count is 78" ($moduleCount -eq 78) "Found $moduleCount modules"
# Changelog mentions v1.4.0
$changelogPath = Join-Path $script:ModuleRoot "Changelog.md"
@@ -8970,6 +8970,59 @@ catch {
Write-TestResult "SIEM Forwarder Tests" $false $_.Exception.Message
}
+# ============================================================================
+# SECTION 175: WINDOWS ADMIN CENTER (Module 77)
+# ============================================================================
+Write-SectionHeader "SECTION 175: WINDOWS ADMIN CENTER (Module 77)"
+
+try {
+ $wacPath = "$modulesPath\77-WindowsAdminCenter.ps1"
+ if (Test-Path $wacPath) {
+ $wacContent = Get-Content $wacPath -Raw
+ Write-TestResult "77-WAC: region header present" ($wacContent -match '#region ===== WINDOWS ADMIN CENTER')
+ Write-TestResult "77-WAC: Test-WACInstalled exists" ($wacContent -match 'function\s+Test-WACInstalled\b')
+ Write-TestResult "77-WAC: Get-WACStatus exists" ($wacContent -match 'function\s+Get-WACStatus\b')
+ Write-TestResult "77-WAC: Install-WAC exists" ($wacContent -match 'function\s+Install-WAC\b')
+ Write-TestResult "77-WAC: Uninstall-WAC exists" ($wacContent -match 'function\s+Uninstall-WAC\b')
+ Write-TestResult "77-WAC: Start-WACSetup exists" ($wacContent -match 'function\s+Start-WACSetup\b')
+ Write-TestResult "77-WAC: Start-WACStatus exists" ($wacContent -match 'function\s+Start-WACStatus\b')
+ Write-TestResult "77-WAC: Show-WindowsAdminCenterManagement exists" ($wacContent -match 'function\s+Show-WindowsAdminCenterManagement\b')
+ # MSI is Authenticode-verified (Microsoft signer) before install — fail-closed.
+ Write-TestResult "77-WAC: verifies Authenticode before install" (($wacContent -match 'Get-AuthenticodeSignature') -and ($wacContent -match 'O=Microsoft Corporation'))
+ # Operator-supplied MSI path is validated (rooted, no traversal/reparse).
+ Write-TestResult "77-WAC: validates operator MSI path" ($wacContent -match 'function\s+Test-WACSafeMsiPath\b')
+ # State changes are Dry-Run gated.
+ Write-TestResult "77-WAC: state changes have a Dry-Run gate" ($wacContent -match '\$script:DryRunMode\s+-and\s+-not\s+\$script:ApplyingDryRunQueue')
+ # Reversible: install registers an msiexec /x undo.
+ Write-TestResult "77-WAC: install is reversible (msiexec /x undo)" (($wacContent -match 'Add-UndoAction') -and ($wacContent -match "'/x'"))
+ # Feature installs (if any) use the timeout wrapper, never a bare Install-WindowsFeature.
+ Write-TestResult "77-WAC: no bare Install-WindowsFeature" (-not ($wacContent -match '(?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." 189 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." 191 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 ce95378..5a1f1ca 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 189 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 191 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 1292041..a24cc85 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 189 CLI
+ "Windows is installed" and "server is in production." It provides 191 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 fbca625..03c7e45 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 = 77
+ $expectedModuleCount = 78
$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 = 77
+$expectedModuleCount = 78
$actualModuleCount = @($moduleFiles).Count
if ($actualModuleCount -ne $expectedModuleCount) {
Write-Warning "Expected $expectedModuleCount modules but found $actualModuleCount"