From d0cacb7e2ed052fc03209393f7d48b5df22e1819 Mon Sep 17 00:00:00 2001 From: TheAbider <51920546+TheAbider@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:53:31 -0700 Subject: [PATCH] Make UpdateSelf refuse an update it cannot verify v1.122.1 fixed the interactive updater in 35-Utilities, but the UpdateSelf CLI action in 50-EntryPoint is a separate implementation and kept the same fail-open shape: when no SHA-256 for RackStack.exe appeared in the release body it warned and replaced the running executable anyway. It now refuses and exits, matching the policy Install-RackStack.ps1 already applies to the bootstrap download. In practice the hash has always been present, so this closes a latent gap rather than a live exposure - but v1.122.1's notes described the fix more broadly than what shipped, and this makes that description accurate. Found by verifying the shipped v1.122.2 monolithic rather than the module that was edited. The section 204 assertion was scoped to 35-Utilities, so it passed while this second path survived. That assertion is now repo-wide across every module, and verified by reintroducing the fail-open string in 50-EntryPoint and confirming the suite goes red. Also audited the remaining download-then-execute sites: Install-RackStack.ps1 fails closed with an explicit -AllowUnverified opt-out, and 77-WindowsAdminCenter re-verifies its MSI signature at apply time. UpdateSelf was the only outstanding one. Separately, the Scoop manifest described the EXE as code-signed. It is not Authenticode-signed - that is the reason antivirus engines flag it - so the description now states what is true: Sigstore cosign signatures and SLSA Level 3 build provenance. --- Changelog.md | 10 ++++++++++ Header.ps1 | 2 +- Modules/00-Initialization.ps1 | 2 +- Modules/50-EntryPoint.ps1 | 10 +++++++++- README.md | 2 +- RackStack.ps1 | 2 +- RackStack.psd1 | 2 +- Tests/Run-Tests.ps1 | 23 ++++++++++++++++++++--- dist/scoop/rackstack.json | 2 +- 9 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Changelog.md b/Changelog.md index 765ac8a..5d9de2c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,15 @@ # Changelog +## v1.122.3 + +Closes the second self-update path, and corrects a packaging claim. + +- **`UpdateSelf` now refuses an update it cannot verify.** v1.122.1 fixed the interactive updater, but the `UpdateSelf` CLI action is a separate implementation and kept the same fail-open shape: if no SHA-256 for `RackStack.exe` appeared in the release body it warned and replaced the running executable anyway. It now refuses and exits, matching the policy `Install-RackStack.ps1` already applied. In practice the hash has always been present, so this closes a latent gap rather than a live exposure -- but v1.122.1's notes described the fix more broadly than what actually shipped, and this makes that description true. +- **The test suite now asserts no module has a fail-open verification path**, rather than checking the one module that was edited. The narrower check passed while this second path survived; only verifying the shipped artifact caught it. +- **The Scoop manifest no longer claims the EXE is code-signed.** It is not Authenticode-signed -- that is precisely why antivirus engines flag it. The description now states what is actually true: Sigstore cosign signatures and SLSA Level 3 build provenance. + +No module or CLI action changes (81 modules, 201 actions). + ## v1.122.2 Packaging fix for the v1.122.1 release. diff --git a/Header.ps1 b/Header.ps1 index d761a88..41a3ed8 100644 --- a/Header.ps1 +++ b/Header.ps1 @@ -30,7 +30,7 @@ 7h3 4b1d3r .VERSION - 1.122.2 + 1.122.3 .LAST UPDATED 07/28/2026 diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1 index 295e19b..dd0a1fa 100644 --- a/Modules/00-Initialization.ps1 +++ b/Modules/00-Initialization.ps1 @@ -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.122.2" +$script:ScriptVersion = "1.122.3" $script:ScriptStartTime = Get-Date # Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe. diff --git a/Modules/50-EntryPoint.ps1 b/Modules/50-EntryPoint.ps1 index eb6c317..d0253f1 100644 --- a/Modules/50-EntryPoint.ps1 +++ b/Modules/50-EntryPoint.ps1 @@ -1078,7 +1078,15 @@ function Invoke-CLIAction { } Write-OutputColor " SHA256 verified." -color "Success" } else { - Write-OutputColor " WARNING: No SHA256 in release body — skipping verification." -color "Warning" + # Fail closed, matching Install-RackStack.ps1's bootstrap policy: refusal is the + # default outcome of any verification failure, including a missing manifest entry. + # This replaces the running EXE, so an update whose integrity cannot be + # established must not be installed. (The interactive updater in 35-Utilities + # had the same fail-open shape and was fixed in v1.122.1.) + Remove-Item -LiteralPath $stageDir -Recurse -Force -ErrorAction SilentlyContinue + Write-OutputColor " No SHA256 for RackStack.exe in the release body — refusing to install an unverified update." -color "Error" + Write-OutputColor " Download and verify manually from: https://github.com/TheAbider/RackStack/releases/tag/$latestTag" -color "Info" + [Environment]::Exit(1) } # If a previous .old still exists (user ran UpdateSelf twice without launching in between), diff --git a/README.md b/README.md index 26d0313..c443cae 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ OpenSSF Best Practices codecov PSScriptAnalyzer 0 errors - 5424 structural tests + 5426 structural tests Pester 312 tests SLSA Level 3

diff --git a/RackStack.ps1 b/RackStack.ps1 index 91e70eb..0c69c06 100644 --- a/RackStack.ps1 +++ b/RackStack.ps1 @@ -13,7 +13,7 @@ Environment-specific settings are configured via rackstack.config.json (a legacy defaults.json is still read). .VERSION - 1.122.2 + 1.122.3 .NOTES - Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing) - Must be run as Administrator diff --git a/RackStack.psd1 b/RackStack.psd1 index f8664d3..70354f8 100644 --- a/RackStack.psd1 +++ b/RackStack.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'RackStack.psm1' - ModuleVersion = '1.122.2' + ModuleVersion = '1.122.3' GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91' Author = 'TheAbider' CompanyName = 'TheAbider' diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1 index 7bf5b81..5343871 100644 --- a/Tests/Run-Tests.ps1 +++ b/Tests/Run-Tests.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Automated Test Runner for RackStack v1.122.2 + Automated Test Runner for RackStack v1.122.3 .DESCRIPTION Comprehensive non-interactive test suite covering: @@ -10333,8 +10333,25 @@ try { Write-TestResult "Update: Get-ReleaseAssetHash function exists" ($utilContent204 -match 'function\s+Get-ReleaseAssetHash\b') Write-TestResult "Update: hash lookup is delegated, not inlined" ($utilContent204 -match '\$expectedHash\s*=\s*Get-ReleaseAssetHash\s') Write-TestResult "Update: asset selection normalizes space/dot" ($utilContent204 -match '\$assetKey\s*=\s*\$assetName\s*-replace\s*''\[\\s\.\]'',\s*''\.''') - # The old fail-open text must never come back. - Write-TestResult "Update: no 'skipping verification' fail-open path" (-not ($utilContent204 -match 'skipping verification')) + # The old fail-open text must never come back — REPO-WIDE, not just in this module. + # Scoping this to 35-Utilities was a mistake: it passed while an identical fail-open + # survived in 50-EntryPoint's UpdateSelf action, and only an end-to-end check against the + # shipped monolithic caught it. Any code path that replaces a running binary or script must + # refuse an update it cannot verify, so assert across every module. + $failOpenHits204 = @( + Get-ChildItem -Path $modulesPath -Filter '*.ps1' -File | + Where-Object { (Get-Content $_.FullName -Raw) -match 'skipping verification' } | + ForEach-Object { $_.Name } + ) + Write-TestResult "Update: no 'skipping verification' fail-open path in ANY module" ` + ($failOpenHits204.Count -eq 0) $(if ($failOpenHits204.Count) { "found in: $($failOpenHits204 -join ', ')" } else { "" }) + + # Both self-update implementations must refuse rather than warn-and-continue. + $entryContent204 = Get-Content "$modulesPath\50-EntryPoint.ps1" -Raw + Write-TestResult "Update: UpdateSelf refuses an unverified EXE" ` + ($entryContent204 -match 'refusing to install an unverified update') + Write-TestResult "Update: UpdateSelf exits rather than continuing" ` + ($entryContent204 -match 'refusing to install an unverified update[\s\S]{0,400}\[Environment\]::Exit\(1\)') Write-TestResult "Update: missing hash refuses the install" ($utilContent204 -match 'refusing to install an unverified update') # A refusal must actually return, not just print. Write-TestResult "Update: refusal path returns before install" ($utilContent204 -match 'refusing to install an unverified update[\s\S]{0,400}\breturn\b') diff --git a/dist/scoop/rackstack.json b/dist/scoop/rackstack.json index f950688..de9db75 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 201 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 standalone EXE with Sigstore cosign signatures and SLSA Level 3 build provenance, 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",