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
11 changes: 11 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -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.
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.7
1.121.8
.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.7"
$script:ScriptVersion = "1.121.8"
$script:ScriptStartTime = Get-Date

# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.
Expand Down
26 changes: 22 additions & 4 deletions Modules/20-DiskCleanup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -430,16 +430,23 @@ 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
$cleaned += $fileLen
}
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions Modules/31-BitLocker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
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="5284 structural tests" src="https://img.shields.io/badge/structural%20tests-5284-brightgreen">
<img alt="5291 structural tests" src="https://img.shields.io/badge/structural%20tests-5291-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.7
1.121.8
.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.7'
ModuleVersion = '1.121.8'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
Expand Down
47 changes: 46 additions & 1 deletion Tests/Run-Tests.ps1
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
# ============================================================================
Expand Down
Loading