Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## v1.121.15

Final audit sweep — remaining fixes across the firewall/RDP, iSCSI, network-diagnostics, and session modules.

- **"Restrict RDP to a subnet" now actually restricts it.** If you had previously added RDP (or WinRM) firewall rules via the Firewall Templates, the subnet-restriction step left those wide-open rules enabled — so RDP/WinRM stayed reachable from any address while the tool reported it was locked down. It now disables those rules too, so the restriction holds.
- **PowerShell Remoting reports its real firewall state.** It used to list every firewall group as "enabled" regardless; it now shows each group's actual status.
- **iSCSI fixes:** the [M] Manual option in "Connect to iSCSI Targets" works (it was silently exiting the menu); the status screen shows the real iSCSI auto-claim state instead of always "Enabled"; and the dry-run undo for a connection now actually disconnects.
- **The network throughput benchmark shows the correct MB/s.** A variable-name clash made the MB/s figure display the Mbps value (8× too high).
- **Session state is saved atomically**, so a crash mid-save can't corrupt the file the tool reads on resume.

(This completes an end-to-end adversarial audit of the whole codebase; every remaining finding is fixed and verified.)

No module or CLI action changes (81 modules, 201 actions).

## v1.121.14

VM and storage safety — fixes found by a deep audit of the VM export/import, offline-VHD, VHD conversion, and Hyper-V Replica modules.
Expand Down
2 changes: 1 addition & 1 deletion Header.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
7h3 4b1d3r

.VERSION
1.121.14
1.121.15
.LAST UPDATED
07/01/2026

Expand Down
2 changes: 1 addition & 1 deletion Modules/00-Initialization.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,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.121.14"
$script:ScriptVersion = "1.121.15"
$script:ScriptStartTime = Get-Date

# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.
Expand Down
30 changes: 23 additions & 7 deletions Modules/10-iSCSI.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1032,8 +1032,13 @@ function Connect-iSCSITargets {
# portals. Doesn't unregister the portals themselves so a
# subsequent re-connect is fast.
foreach ($p in $capPortals) {
Get-IscsiSession -ErrorAction SilentlyContinue | Where-Object { $_.TargetNodeAddress -and $_.InitiatorPortalAddress -eq $p } |
ForEach-Object { Disconnect-IscsiTarget -NodeAddress $_.TargetNodeAddress -Confirm:$false -ErrorAction SilentlyContinue }
# Match on the session's CONNECTION target address (the target/SAN portal IP),
# not InitiatorPortalAddress — that's the LOCAL initiator NIC IP and never equals
# a captured target portal, so the old predicate matched nothing and the undo
# silently disconnected nothing.
Get-IscsiSession -ErrorAction SilentlyContinue | Where-Object {
$_.TargetNodeAddress -and (@($_ | Get-IscsiConnection -ErrorAction SilentlyContinue | Where-Object { $_.TargetAddress -eq $p }).Count -gt 0)
} | ForEach-Object { Disconnect-IscsiTarget -NodeAddress $_.TargetNodeAddress -Confirm:$false -ErrorAction SilentlyContinue }
}
}.GetNewClosure()
Write-OutputColor " Queued (Dry-Run): connect iSCSI portals." -color "Warning"
Expand Down Expand Up @@ -1312,11 +1317,16 @@ function Show-iSCSIStatus {
Write-OutputColor " │$(" Load Balance Policy: Unknown".PadRight(72))│" -color "Warning"
}

# Show MPIO disks
$mpioDisks = Get-MSDSMAutomaticClaimSettings -ErrorAction SilentlyContinue
if ($mpioDisks) {
# Show MPIO iSCSI auto-claim state. Get-MSDSMAutomaticClaimSettings returns a per-bus-type
# hashtable (e.g. @{ SAS = $true; iSCSI = $false }); a hashtable is ALWAYS truthy, so the old
# `if ($mpioDisks)` printed "Enabled" even when iSCSI auto-claim was off. Read the iSCSI key.
$mpioClaim = Get-MSDSMAutomaticClaimSettings -ErrorAction SilentlyContinue
if ($null -ne $mpioClaim -and $mpioClaim['iSCSI']) {
Write-OutputColor " │$(" iSCSI Auto-Claim: Enabled".PadRight(72))│" -color "Success"
}
else {
Write-OutputColor " │$(" iSCSI Auto-Claim: Disabled".PadRight(72))│" -color "Warning"
}
}
else {
Write-OutputColor " │$(" MPIO: Not Installed".PadRight(72))│" -color "Warning"
Expand Down Expand Up @@ -1556,8 +1566,14 @@ function Start-Show-iSCSISANMenu {
Write-OutputColor "" -color "Info"

$connectChoice = Read-Host " Select"
$navResult = Test-NavigationCommand -UserInput $connectChoice
if ($navResult.ShouldReturn) { return }
# Match this menu's own action letters (A/M/P) BEFORE the nav check — otherwise
# Test-NavigationCommand consumes 'M' as the home token and silently exits the
# whole iSCSI/SAN menu, making [M] Manual unreachable dead code (same class fixed
# in Set-iSCSIConfiguration). Only OTHER input (b/back/0/...) is treated as nav.
if ($connectChoice -notmatch '^[AaMmPp]$') {
$navResult = Test-NavigationCommand -UserInput $connectChoice
if ($navResult.ShouldReturn) { return }
}

switch -Regex ($connectChoice) {
'^[Aa]$' {
Expand Down
50 changes: 41 additions & 9 deletions Modules/15-RDP.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,26 @@ function Enable-RDP {
-Profile Domain,Private,Public `
-Description "Allow RDP only from $subnet" -ErrorAction Stop | Out-Null

# Disable the broad RDP rules so only the restricted one applies
# Disable the broad RDP rules so only the restricted one applies. This must ALSO
# cover RackStack's own custom RDP rules ('RDP Inbound TCP/UDP 3389' from the
# Firewall Templates), which are created with NO DisplayGroup and RemoteAddress=Any
# — the DisplayGroup query below misses them, so they'd stay enabled and (Windows
# OR-combines inbound Allow rules) keep 3389 reachable from ANY address, silently
# defeating the subnet restriction this path claims to apply.
Get-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -ne "RDP Subnet Restriction" } |
Disable-NetFirewallRule -ErrorAction SilentlyContinue
Get-NetFirewallRule -DisplayName "RDP Inbound TCP 3389", "RDP Inbound UDP 3389" -ErrorAction SilentlyContinue |
Disable-NetFirewallRule -ErrorAction SilentlyContinue

Write-OutputColor " RDP restricted to $subnet only." -color "Success"
Write-OutputColor " Broad RDP rules disabled. Only subnet rule is active." -color "Success"
Add-SessionChange -Category "Security" -Description "Restricted RDP to subnet $subnet"
Add-UndoAction -Category "Security" -Description "RDP subnet restriction to $subnet" -UndoScript {
Remove-NetFirewallRule -DisplayName "RDP Subnet Restriction" -ErrorAction SilentlyContinue
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue
Get-NetFirewallRule -DisplayName "RDP Inbound TCP 3389", "RDP Inbound UDP 3389" -ErrorAction SilentlyContinue |
Enable-NetFirewallRule -ErrorAction SilentlyContinue
}
}
catch {
Expand Down Expand Up @@ -479,13 +488,30 @@ function Enable-PowerShellRemoting {
Write-OutputColor "" -color "Info"
Write-OutputColor " PowerShell Remoting enabled successfully!" -color "Success"
Write-OutputColor "" -color "Info"
Write-OutputColor " Firewall rules enabled:" -color "Info"
Write-OutputColor " - Windows Remote Management (WinRM)" -color "Success"
Write-OutputColor " - Remote Event Log Management" -color "Success"
Write-OutputColor " - Remote Service Management" -color "Success"
Write-OutputColor " - Windows Management Instrumentation (WMI)" -color "Success"
Write-OutputColor " - Remote Scheduled Tasks Management" -color "Success"
Write-OutputColor " - File and Printer Sharing" -color "Success"
Write-OutputColor " Firewall rule groups:" -color "Info"
# Report each group's ACTUAL state rather than a hardcoded green list. The Enable calls above
# use -EA SilentlyContinue, so a group that is absent or failed to enable was previously still
# reported as enabled — false assurance about the remote-management firewall posture.
$rmGroups = @(
"Windows Remote Management",
"Remote Event Log Management",
"Remote Service Management",
"Windows Management Instrumentation (WMI)",
"Remote Scheduled Tasks Management",
"File and Printer Sharing"
)
foreach ($grp in $rmGroups) {
$grpRules = @(Get-NetFirewallRule -DisplayGroup $grp -ErrorAction SilentlyContinue)
if (@($grpRules | Where-Object { $_.Enabled -eq 'True' }).Count -gt 0) {
Write-OutputColor " - $grp" -color "Success"
}
elseif ($grpRules.Count -eq 0) {
Write-OutputColor " - $grp (no rules present on this host)" -color "Warning"
}
else {
Write-OutputColor " - $grp (could NOT be enabled)" -color "Warning"
}
}
Write-OutputColor "" -color "Info"
Write-OutputColor " You can now connect to this server using:" -color "Info"
Write-OutputColor " Enter-PSSession -ComputerName $env:COMPUTERNAME" -color "Success"
Expand Down Expand Up @@ -530,15 +556,21 @@ function Enable-PowerShellRemoting {
-Profile Domain,Private,Public `
-Description "Allow WinRM only from $subnet" -ErrorAction Stop | Out-Null

# Disable broad WinRM rules
# Disable broad WinRM rules — including RackStack's own custom 'WinRM HTTP/HTTPS
# Inbound' rules (Firewall Templates), which have no DisplayGroup + RemoteAddress=Any
# and would otherwise keep 5985/5986 open to any source, defeating the restriction.
Get-NetFirewallRule -DisplayGroup "Windows Remote Management" -ErrorAction SilentlyContinue |
Disable-NetFirewallRule -ErrorAction SilentlyContinue
Get-NetFirewallRule -DisplayName "WinRM HTTP Inbound", "WinRM HTTPS Inbound" -ErrorAction SilentlyContinue |
Disable-NetFirewallRule -ErrorAction SilentlyContinue

Write-OutputColor " WinRM restricted to $subnet." -color "Success"
Add-SessionChange -Category "Security" -Description "Restricted WinRM to subnet $subnet"
Add-UndoAction -Category "Security" -Description "WinRM subnet restriction to $subnet" -UndoScript {
Remove-NetFirewallRule -DisplayName "WinRM Subnet Restriction" -ErrorAction SilentlyContinue
Enable-NetFirewallRule -DisplayGroup "Windows Remote Management" -ErrorAction SilentlyContinue
Get-NetFirewallRule -DisplayName "WinRM HTTP Inbound", "WinRM HTTPS Inbound" -ErrorAction SilentlyContinue |
Enable-NetFirewallRule -ErrorAction SilentlyContinue
}
}
catch {
Expand Down
9 changes: 8 additions & 1 deletion Modules/55-QoLFeatures.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,14 @@ function Save-SessionState {
}

try {
$sessionState | ConvertTo-Json -Depth 10 | Out-File -LiteralPath $script:SessionStatePath -Encoding UTF8 -Force
# Atomic write — THIS is the file Restore-SessionState reads, and its load path deletes the
# file on a ConvertFrom-Json failure, so a mid-write truncation (process kill / disk full)
# would lose session state. Write to .tmp then rename so the reader only ever sees a complete
# file. (The snapshot file below already does this; the consumed file was missing it.)
$sessionJson = $sessionState | ConvertTo-Json -Depth 10
$tmpSession = "$($script:SessionStatePath).tmp"
Set-Content -LiteralPath $tmpSession -Value $sessionJson -Encoding UTF8 -ErrorAction Stop
Move-Item -LiteralPath $tmpSession -Destination $script:SessionStatePath -Force -ErrorAction Stop
}
catch {
Write-OutputColor " Warning: Could not save session state: $($_.Exception.Message)" -color "Warning"
Expand Down
11 changes: 7 additions & 4 deletions Modules/58-NetworkDiagnostics.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1179,17 +1179,20 @@ function Invoke-NetworkThroughputBenchmark {
}

if ($ok) {
$writeMbps = [math]::Round($writeMBps * 8, 0)
$readMbps = [math]::Round($readMBps * 8, 0)
# NOTE: PowerShell variable names are case-INSENSITIVE, so $writeMbps and $writeMBps are the
# SAME variable — the old code overwrote the MB/s value with the *8 Mbps value, then printed
# both columns from it (MB/s shown 8x inflated). Use case-distinct names for the Mbps values.
$writeMbits = [math]::Round($writeMBps * 8, 0)
$readMbits = [math]::Round($readMBps * 8, 0)
$title = " THROUGHPUT: $target"
if ($title.Length -gt 69) { $title = $title.Substring(0, 69) + "..." }
Write-OutputColor "" -color "Info"
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
Write-OutputColor " │$($title.PadRight(72))│" -color "Info"
Write-OutputColor " ├────────────────────────────────────────────────────────────────────────┤" -color "Info"
Write-OutputColor " │$(" Test size: ${sizeMB} MB".PadRight(72))│" -color "Info"
Write-OutputColor " │$(" Write: ${writeMBps} MB/s (${writeMbps} Mbps)".PadRight(72))│" -color "Success"
Write-OutputColor " │$(" Read: ${readMBps} MB/s (${readMbps} Mbps)".PadRight(72))│" -color "Success"
Write-OutputColor " │$(" Write: ${writeMBps} MB/s (${writeMbits} Mbps)".PadRight(72))│" -color "Success"
Write-OutputColor " │$(" Read: ${readMBps} MB/s (${readMbits} Mbps)".PadRight(72))│" -color "Success"
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
}
Write-PressEnter
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<a href="https://www.bestpractices.dev/projects/12921"><img alt="OpenSSF Best Practices" src="https://www.bestpractices.dev/projects/12921/badge"></a>
<a href="https://codecov.io/gh/TheAbider/RackStack"><img alt="codecov" src="https://codecov.io/gh/TheAbider/RackStack/branch/master/graph/badge.svg"></a>
<img alt="PSScriptAnalyzer 0 errors" src="https://img.shields.io/badge/PSScriptAnalyzer-0%20errors-brightgreen">
<img alt="5357 structural tests" src="https://img.shields.io/badge/structural%20tests-5357-brightgreen">
<img alt="5366 structural tests" src="https://img.shields.io/badge/structural%20tests-5366-brightgreen">
<img alt="Pester 312 tests" src="https://img.shields.io/badge/Pester-312%20tests-brightgreen">
<img alt="SLSA Level 3" src="https://slsa.dev/images/gh-badge-level3.svg">
</p>
Expand Down
2 changes: 1 addition & 1 deletion RackStack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Environment-specific settings are configured via defaults.json.

.VERSION
1.121.14
1.121.15
.NOTES
- Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing)
- Must be run as Administrator
Expand Down
2 changes: 1 addition & 1 deletion RackStack.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'RackStack.psm1'
ModuleVersion = '1.121.14'
ModuleVersion = '1.121.15'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
Expand Down
Loading
Loading