diff --git a/Changelog.md b/Changelog.md index 3722e88..f8ccd9e 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,16 @@ # Changelog +## v1.121.8 + +Cleanup and encryption safety — fixes found by a deep audit of the destructive/data-affecting modules. + +- **Browser-cache cleanup can't be tricked into deleting files outside the cache.** The Chrome, Firefox, and full-cleanup sweeps now skip reparse points (junctions/symlinks) during recursion, the same protection the Edge and temp-file sweeps already had. Without it, a junction planted inside a browser profile could redirect the admin-context delete to files elsewhere on disk. +- **Enabling BitLocker now asks for a final confirmation** naming the exact volume and method before it starts encrypting — matching the confirmation the Disable path already had — so a mistyped volume number can be caught before a one-way, hours-long encryption begins. + +(The audit also confirmed the Deduplication, Storage Migration, and Debloat modules — and BitLocker's recovery-key handling — are already correct; these were the only gaps.) + +No module or CLI action changes (81 modules, 201 actions). + ## v1.121.7 Active Directory — the new-forest wizard now catches two problems up front instead of failing partway through promotion. diff --git a/Header.ps1 b/Header.ps1 index 9c20939..b555d47 100644 --- a/Header.ps1 +++ b/Header.ps1 @@ -30,7 +30,7 @@ 7h3 4b1d3r .VERSION - 1.121.7 + 1.121.8 .LAST UPDATED 07/01/2026 diff --git a/Modules/00-Initialization.ps1 b/Modules/00-Initialization.ps1 index 6c59789..b7e456d 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.121.7" +$script:ScriptVersion = "1.121.8" $script:ScriptStartTime = Get-Date # Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe. diff --git a/Modules/20-DiskCleanup.ps1 b/Modules/20-DiskCleanup.ps1 index 95644ca..f169d0b 100644 --- a/Modules/20-DiskCleanup.ps1 +++ b/Modules/20-DiskCleanup.ps1 @@ -430,8 +430,14 @@ function Clear-BrowserCaches { if ($chromeTotal -gt 0) { if (Confirm-UserAction -Message "Clear Chrome cache ($chromeMB MB)?" -DefaultYes) { + # Reparse-point filter on the recursion. The path globs into the operator's own + # LocalAppData, but a malicious or careless prior install could plant a junction + # inside the Chrome profile path; without this filter, admin-context Remove-Item + # would walk through to the link target. + $reparseAttr = [System.IO.FileAttributes]::ReparsePoint $cleaned = 0 - Get-ChildItem -Path "$chromeCachePath\*\Cache\*" -Recurse -Force -File -ErrorAction SilentlyContinue | ForEach-Object { + Get-ChildItem -Path "$chromeCachePath\*\Cache\*" -Recurse -Force -File -ErrorAction SilentlyContinue | + Where-Object { -not ($_.Attributes -band $reparseAttr) } | ForEach-Object { try { $fileLen = $_.Length Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop @@ -439,7 +445,8 @@ function Clear-BrowserCaches { } catch { $null = $_ } } - Get-ChildItem -Path "$chromeCachePath\*\Code Cache\*" -Recurse -Force -File -ErrorAction SilentlyContinue | ForEach-Object { + Get-ChildItem -Path "$chromeCachePath\*\Code Cache\*" -Recurse -Force -File -ErrorAction SilentlyContinue | + Where-Object { -not ($_.Attributes -band $reparseAttr) } | ForEach-Object { try { $fileLen = $_.Length Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop @@ -464,8 +471,14 @@ function Clear-BrowserCaches { if ($firefoxSize -gt 0) { if (Confirm-UserAction -Message "Clear Firefox cache ($firefoxMB MB)?" -DefaultYes) { + # Reparse-point filter on the recursion. The path globs into the operator's own + # LocalAppData, but a malicious or careless prior install could plant a junction + # inside the Firefox profile path; without this filter, admin-context Remove-Item + # would walk through to the link target. + $reparseAttr = [System.IO.FileAttributes]::ReparsePoint $cleaned = 0 - Get-ChildItem -Path "$firefoxProfilePath\*\cache2\*" -Recurse -Force -File -ErrorAction SilentlyContinue | ForEach-Object { + Get-ChildItem -Path "$firefoxProfilePath\*\cache2\*" -Recurse -Force -File -ErrorAction SilentlyContinue | + Where-Object { -not ($_.Attributes -band $reparseAttr) } | ForEach-Object { try { $fileLen = $_.Length Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop @@ -961,9 +974,14 @@ function Invoke-FullEnhancedCleanup { $browserPaths += "$firefoxProfilePath\*\cache2\*" } + # Reparse-point filter on the recursion. These globs walk the operator's own LocalAppData, + # but a malicious or careless prior install could plant a junction inside a browser profile + # path; without this filter, admin-context Remove-Item would walk through to the link target. + $reparseAttr = [System.IO.FileAttributes]::ReparsePoint $browserCleaned = 0 foreach ($bp in $browserPaths) { - Get-ChildItem -Path $bp -Recurse -Force -File -ErrorAction SilentlyContinue | ForEach-Object { + Get-ChildItem -Path $bp -Recurse -Force -File -ErrorAction SilentlyContinue | + Where-Object { -not ($_.Attributes -band $reparseAttr) } | ForEach-Object { try { $fileLen = $_.Length Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop diff --git a/Modules/31-BitLocker.ps1 b/Modules/31-BitLocker.ps1 index 09e9c95..c12f0cd 100644 --- a/Modules/31-BitLocker.ps1 +++ b/Modules/31-BitLocker.ps1 @@ -255,6 +255,21 @@ function Show-BitLockerManagement { continue } + # Final confirmation before the live encryption. Enabling BitLocker is a + # one-way, hours-long full-volume operation; name the target volume and the + # chosen method so the operator can catch a wrong-disk selection here. The + # Disable path has an equivalent gate; the Dry-Run path above is its own gate. + $methodLabel = switch ($method) { + "1" { "TPM only" } + "2" { "TPM + PIN" } + "3" { "Password only" } + default { "method $method" } + } + if ($method -in "1","2","3" -and -not (Confirm-UserAction -Message "Enable BitLocker on $($vol.MountPoint) ($methodLabel)? This starts a one-way encryption of the volume.")) { + Write-OutputColor " Cancelled. No changes made." -color "Info" + continue + } + try { switch ($method) { "1" { diff --git a/README.md b/README.md index 126a6dc..2600bdd 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ OpenSSF Best Practices codecov PSScriptAnalyzer 0 errors - 5284 structural tests + 5291 structural tests Pester 312 tests SLSA Level 3

diff --git a/RackStack.ps1 b/RackStack.ps1 index 62c34de..3e73ba3 100644 --- a/RackStack.ps1 +++ b/RackStack.ps1 @@ -13,7 +13,7 @@ Environment-specific settings are configured via defaults.json. .VERSION - 1.121.7 + 1.121.8 .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 75f0f4a..bcd4724 100644 --- a/RackStack.psd1 +++ b/RackStack.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'RackStack.psm1' - ModuleVersion = '1.121.7' + ModuleVersion = '1.121.8' GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91' Author = 'TheAbider' CompanyName = 'TheAbider' diff --git a/Tests/Run-Tests.ps1 b/Tests/Run-Tests.ps1 index 5d127e7..bf559fc 100644 --- a/Tests/Run-Tests.ps1 +++ b/Tests/Run-Tests.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Automated Test Runner for RackStack v1.121.7 + Automated Test Runner for RackStack v1.121.8 .DESCRIPTION Comprehensive non-interactive test suite covering: @@ -9804,6 +9804,51 @@ catch { Write-TestResult "New-Forest Validation Tests" $false $_.Exception.Message } +# ============================================================================ +# SECTION 195: DESTRUCTIVE-MODULE HARDENING (v1.121.8) +# ============================================================================ +# Guards two fixes from the destructive/data-affecting-module audit: +# DC-001/002/003 — the Chrome, Firefox, and full-cleanup browser-cache sweeps must skip reparse +# points during recursion (a junction planted in a browser profile could otherwise redirect an +# admin-context Remove-Item to files elsewhere on disk). Matches the Edge/temp-sweep pattern. +# BL-001 — enabling BitLocker must confirm the exact volume + method before the one-way encryption +# starts, mirroring the gate the Disable path already has. +Write-SectionHeader "SECTION 195: DESTRUCTIVE-MODULE HARDENING" + +try { + $dcR = Get-Content "$modulesPath\20-DiskCleanup.ps1" -Raw + + # DC-001/002: Chrome sweep filters reparse points on BOTH the Cache and Code Cache recursion. + $chromeBlock = [regex]::Match($dcR, 'Clear Chrome cache[\s\S]*?Chrome cache cleared').Value + $chromeFilters = @([regex]::Matches($chromeBlock, '-band \$reparseAttr')).Count + Write-TestResult "20-Cleanup: Chrome sweep filters reparse points (both caches)" ($chromeFilters -ge 2) "found $chromeFilters" + + # DC-003a: Firefox cache2 sweep filters reparse points. + $firefoxBlock = [regex]::Match($dcR, 'Clear Firefox cache[\s\S]*?Firefox cache cleared').Value + Write-TestResult "20-Cleanup: Firefox sweep filters reparse points" ($firefoxBlock -match '-band \$reparseAttr') + + # DC-003b: the Invoke-FullEnhancedCleanup browser loop filters reparse points. + $fullLoop = [regex]::Match($dcR, '\$browserCleaned = 0[\s\S]{0,400}').Value + Write-TestResult "20-Cleanup: full-cleanup browser loop filters reparse points" ($fullLoop -match '-band \$reparseAttr') + + # Regression: the pre-existing Edge sweep still filters (both caches) — proves we matched, not replaced. + $edgeBlock = [regex]::Match($dcR, 'Clear Edge cache[\s\S]*?Edge cache cleared').Value + $edgeFilters = @([regex]::Matches($edgeBlock, '-band \$reparseAttr')).Count + Write-TestResult "20-Cleanup: Edge sweep still filters reparse points (regression)" ($edgeFilters -ge 2) "found $edgeFilters" + + $blR = Get-Content "$modulesPath\31-BitLocker.ps1" -Raw + + # BL-001: Enable prompts a final confirmation naming the volume, before the live Enable-BitLocker. + Write-TestResult "31-BitLocker: Enable confirm names volume + one-way encryption" ($blR -match 'Enable BitLocker on \$\(\$vol\.MountPoint\)[\s\S]{0,200}one-way encryption') + Write-TestResult "31-BitLocker: Enable confirm precedes the live Enable-BitLocker call" ($blR -match 'Confirm-UserAction -Message "Enable BitLocker on \$\(\$vol\.MountPoint\)[\s\S]{0,600}Enable-BitLocker -MountPoint \$vol\.MountPoint') + + # Regression: the Disable path keeps its own confirmation gate. + Write-TestResult "31-BitLocker: Disable still confirms (regression)" ($blR -match 'Confirm-UserAction -Message "Disable BitLocker on \$\(\$vol\.MountPoint\)') +} +catch { + Write-TestResult "Destructive-Module Hardening Tests" $false $_.Exception.Message +} + # ============================================================================ # SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase) # ============================================================================