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
13 changes: 13 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## v1.121.13

Role/service cleanup and reporting fixes — found by a deep audit of the ADCS, scheduled-task, WSUS, and Remote Desktop modules.

- **Self-destruct now removes all of the tool's scheduled tasks.** When the tool deleted itself, it left its own scheduled export and update-check tasks registered (running as SYSTEM) pointing at the now-deleted program — leftover privileged tasks that would fire and fail forever. It now unregisters those too, so it leaves nothing behind.
- **A failed RDS licensing change in Dry-Run is reported as failed**, not silently applied.
- **A WSUS setup with no valid update classifications is reported as incomplete.** Previously a misconfigured WSUS (which would sync nothing) still reported "configuration applied" — it now tells you the classifications didn't match and to fix them.
- **The scheduled-task health view stops crying wolf.** Brand-new or on-demand tasks that simply hadn't run yet were shown in red as "FAILED"; they're now shown as "Ready (not yet run)".

(The audit also confirmed the Certificate Authority setup uses strong crypto — SHA-256, RSA ≥ 2048 — and that the certificate-audit and Defender-onboarding paths are correct.)

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

## v1.121.12

Backup / undo honesty — fixes found by a deep audit of the security-feature modules (Group Policy, JEA, NPS, SIEM forwarding). Common theme: a safety-net backup that could silently fail and then be trusted anyway.
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.12
1.121.13
.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.12"
$script:ScriptVersion = "1.121.13"
$script:ScriptStartTime = Get-Date

# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.
Expand Down
8 changes: 8 additions & 0 deletions Modules/47-ExitCleanup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,14 @@ function Exit-Script {
$cleanupCommands += "Remove-Item -LiteralPath '$escapedPath' -Recurse -Force -ErrorAction SilentlyContinue`n"
}
$toolNameEsc = $script:ToolName -replace "'", "''"
# Also unregister the OTHER SYSTEM/Highest tasks the tool creates — otherwise the
# self-destruct deletes their target binaries ($script:ScriptPath / the installed exe)
# but leaves the tasks registered and firing on schedule against a missing file: a
# privileged persistence remnant (and, for a portable .ps1 run from a user-writable
# dir, a SYSTEM phantom-task EoP if someone recreates the deleted script). Harmless
# -EA SilentlyContinue when a task doesn't exist.
$cleanupCommands += "Unregister-ScheduledTask -TaskName '$($toolNameEsc)-ScheduledExport' -TaskPath '\$($toolNameEsc)\' -Confirm:`$false -ErrorAction SilentlyContinue`n"
$cleanupCommands += "Unregister-ScheduledTask -TaskName '$($toolNameEsc)_UpdateCheck' -Confirm:`$false -ErrorAction SilentlyContinue`n"
$cleanupCommands += "Unregister-ScheduledTask -TaskName '$($toolNameEsc)Cleanup' -Confirm:`$false -ErrorAction SilentlyContinue"

$bytes = [System.Text.Encoding]::Unicode.GetBytes($cleanupCommands)
Expand Down
15 changes: 11 additions & 4 deletions Modules/63-ScheduledTasks.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@ function Show-TaskHealthStatus {

$color = "Success"
$status = "OK"
if ($lastResult -ne 0 -and $lastResult -ne 267009) { # 267009 = task is running
if ($lastResult -eq 267009) { # 0x00041301 = task is currently running
$color = "Warning"
$status = "Running"
} elseif ($lastResult -eq 0x00041300 -or $lastResult -eq 0x00041303) {
# 0x00041300 = ready/never-run at its next time, 0x00041303 = has not yet run.
# These are HEALTHY not-yet-executed states, not failures — the sibling
# Show-FailedTasks excludes them too. Treating them as FAILED made a brand-new
# or on-demand task show a red error before it had ever run.
$color = "Info"
$status = "Ready (not yet run)"
} elseif ($lastResult -ne 0) {
$color = "Error"
$status = "FAILED (0x$($lastResult.ToString('X')))"
$failedCount++
} elseif ($lastResult -eq 267009) {
$color = "Warning"
$status = "Running"
}

Write-OutputColor " $taskName Last: $lastRun $status" -color $color
Expand Down
13 changes: 13 additions & 0 deletions Modules/67-WSUS.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ function Set-WSUSDefaultConfiguration {
} else {
@('Critical Updates', 'Security Updates', 'Definition Updates', 'Update Rollups')
}
$classificationsEnabled = $false
try {
$allClass = $wsus.GetUpdateClassifications()
$selected = @($allClass | Where-Object { $_.Title -in $wantClassifications })
Expand All @@ -291,6 +292,7 @@ function Set-WSUSDefaultConfiguration {
foreach ($c in $selected) { [void]$coll.Add($c) }
$sub.SetUpdateClassifications($coll)
$sub.Save()
$classificationsEnabled = $true
Write-OutputColor " Enabled classifications: $($matchedTitles -join ', ')" -color "Success"
}
}
Expand All @@ -314,6 +316,17 @@ function Set-WSUSDefaultConfiguration {
Write-OutputColor " Could not set the sync schedule: $($_.Exception.Message)" -color "Warning"
}

# If NO update classification was enabled, the WSUS server will sync nothing — a misconfigured
# server that reports "configured" is the worst outcome (per the comment above). Don't claim
# success: report the config as incomplete and return $false so callers / the CLI exit code
# reflect it.
if (-not $classificationsEnabled) {
Add-SessionChange -Category "Roles" -Description "WSUS configuration incomplete — no update classifications enabled"
Write-OutputColor " WSUS configuration is INCOMPLETE: no update classifications were enabled, so the" -color "Error"
Write-OutputColor " server will sync nothing. Fix WSUS.Classifications in defaults.json and re-run." -color "Warning"
return $false
}

Add-SessionChange -Category "Roles" -Description "Applied default WSUS configuration (language, classifications, sync schedule)"
Write-OutputColor " Default configuration applied. First sync will populate the catalog." -color "Success"
return $true
Expand Down
10 changes: 7 additions & 3 deletions Modules/80-RemoteDesktopServices.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,13 @@ function Set-RDSLicenseMode {
-Params @{ Mode = $Mode; LicenseServer = $server } `
-Preflight { $true }.GetNewClosure() `
-Apply {
if (-not (Test-Path $path)) { New-Item -Path $path -Force -ErrorAction SilentlyContinue | Out-Null }
Set-ItemProperty -Path $path -Name 'LicensingMode' -Value $mv -Type DWord -ErrorAction SilentlyContinue
Set-ItemProperty -Path $path -Name 'LicenseServers' -Value $sv -Type String -ErrorAction SilentlyContinue
# -EA Stop (matching the immediate path below) so a failed registry write THROWS
# and the Dry-Run commit engine records the step as failed + rolls back. With
# SilentlyContinue the engine (which judges success purely by "did Apply throw?")
# marked a no-op write as "Applied" and printed "all steps applied successfully".
if (-not (Test-Path $path)) { New-Item -Path $path -Force -ErrorAction Stop | Out-Null }
Set-ItemProperty -Path $path -Name 'LicensingMode' -Value $mv -Type DWord -ErrorAction Stop
Set-ItemProperty -Path $path -Name 'LicenseServers' -Value $sv -Type String -ErrorAction Stop
}.GetNewClosure() `
-Undo {
if ($null -ne $pm) { Set-ItemProperty -Path $path -Name 'LicensingMode' -Value $pm -Type DWord -ErrorAction SilentlyContinue }
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="5341 structural tests" src="https://img.shields.io/badge/structural%20tests-5341-brightgreen">
<img alt="5348 structural tests" src="https://img.shields.io/badge/structural%20tests-5348-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.12
1.121.13
.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.12'
ModuleVersion = '1.121.13'
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
Author = 'TheAbider'
CompanyName = 'TheAbider'
Expand Down
37 changes: 35 additions & 2 deletions 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.12
Automated Test Runner for RackStack v1.121.13

.DESCRIPTION
Comprehensive non-interactive test suite covering:
Expand Down Expand Up @@ -9484,7 +9484,7 @@ try {
Write-TestResult "80-RDS: role install is reversible (undo)" ($rdsC -match 'function\s+Install-RDSRoles[\s\S]{0,3000}Add-UndoAction[\s\S]{0,400}Uninstall-WindowsFeature')
Write-TestResult "80-RDS: role install is server-SKU gated" ($rdsC -match 'Test-WindowsServer')
# Licensing mode is reversible (captures prior + undo).
Write-TestResult "80-RDS: license-mode change is reversible (undo)" ($rdsC -match 'function\s+Set-RDSLicenseMode[\s\S]{0,3500}Add-UndoAction[\s\S]{0,400}LicensingMode')
Write-TestResult "80-RDS: license-mode change is reversible (undo)" ($rdsC -match 'function\s+Set-RDSLicenseMode[\s\S]{0,4000}Add-UndoAction[\s\S]{0,400}LicensingMode')
Write-TestResult "80-RDS: license-mode change is Dry-Run aware (reversible)" ($rdsC -match 'Set-RDSLicenseMode[\s\S]{0,2000}Push-DryRunStep[\s\S]{0,400}-OneWay \$false')
# Validates the license-server name before writing it.
Write-TestResult "80-RDS: validates license server name" ($rdsC -match 'LicenseServer[\s\S]{0,200}-notmatch')
Expand Down Expand Up @@ -10056,6 +10056,39 @@ catch {
Write-TestResult "Backup/Undo Safety-Net Tests" $false $_.Exception.Message
}

# ============================================================================
# SECTION 200: ROLE/SERVICE MODULE HARDENING (v1.121.13)
# ============================================================================
# Guards the fixes from the role/service-module audit (ADCS/cert/scheduled-task/Defender/WSUS/RDS):
# [0] Self-destruct also unregisters the tool's other SYSTEM/Highest tasks (ScheduledExport,
# UpdateCheck) so it doesn't leave privileged tasks orphaned against deleted binaries.
# [1] RDS licensing Dry-Run apply uses -EA Stop so a failed registry write isn't reported applied.
# [2] WSUS default-config returns $false when no update classification was enabled (syncs nothing).
# [3] Task Health no longer flags healthy never-run tasks (0x00041300 / 0x00041303) as FAILED.
Write-SectionHeader "SECTION 200: ROLE/SERVICE MODULE HARDENING"

try {
$ecRaw = Get-Content "$modulesPath\47-ExitCleanup.ps1" -Raw
$rdsRaw = Get-Content "$modulesPath\80-RemoteDesktopServices.ps1" -Raw
$wsusRaw = Get-Content "$modulesPath\67-WSUS.ps1" -Raw
$schRaw = Get-Content "$modulesPath\63-ScheduledTasks.ps1" -Raw

# [0]
Write-TestResult "47-ExitCleanup: self-destruct unregisters the ScheduledExport task [0]" ($ecRaw -match "Unregister-ScheduledTask -TaskName '\`$\(\`$toolNameEsc\)-ScheduledExport'")
Write-TestResult "47-ExitCleanup: self-destruct unregisters the UpdateCheck task [0]" ($ecRaw -match "Unregister-ScheduledTask -TaskName '\`$\(\`$toolNameEsc\)_UpdateCheck'")
# [1]
Write-TestResult "80-RDS: licensing Dry-Run apply uses -EA Stop [1]" ($rdsRaw -match "Set-ItemProperty -Path \`$path -Name 'LicensingMode' -Value \`$mv -Type DWord -ErrorAction Stop")
Write-TestResult "80-RDS: licensing Dry-Run apply no longer swallows write errors [1]" (-not ($rdsRaw -match "Name 'LicensingMode' -Value \`$mv -Type DWord -ErrorAction SilentlyContinue"))
# [2]
Write-TestResult "67-WSUS: tracks whether classifications were enabled [2]" (($wsusRaw -match '\$classificationsEnabled = \$false') -and ($wsusRaw -match '\$classificationsEnabled = \$true'))
Write-TestResult "67-WSUS: returns false when no classification enabled [2]" ($wsusRaw -match 'if \(-not \$classificationsEnabled\)[\s\S]{0,400}return \$false')
# [3]
Write-TestResult "63-ScheduledTasks: never-run tasks are not flagged FAILED [3]" ($schRaw -match '\$lastResult -eq 0x00041300 -or \$lastResult -eq 0x00041303')
}
catch {
Write-TestResult "Role/Service Module Hardening Tests" $false $_.Exception.Message
}

# ============================================================================
# SECTION 174: DOCUMENTATION FRESHNESS (counts must match the codebase)
# ============================================================================
Expand Down
Loading