Skip to content

Commit 2bcc027

Browse files
committed
Evaluation-edition licensing + lower-friction admin/reboot confirmations (v1.119.4)
- Licensing: detect Windows Server Evaluation editions and convert them to the full edition via DISM /Set-Edition before activating. An Eval SKU rejects slmgr /ipk (0xC004F069 product-SKU-not-found), so conversion is the only path. The conversion key is resolved from the same merged key table, so an operator's configured key is preferred (a MAK/retail key converts AND activates on reboot; a GVLK converts and then activates against a KMS server). One-way, reboot-flagged, Dry-Run aware. - Disable built-in Administrator: the typed LOGGED-IN-AS-BUILTIN confirmation is now only required when there's no verified alternate local admin (real lockout risk); with a fallback admin present it's a plain y/N. When no alternate admin exists, offer to create one inline instead of dead-ending. - Exit self-removal reboot: replace the type-the-computer-name confirmation with a plain y/N (same warning). Adds 8 structural tests (5202 total). PSSA 0 errors.
1 parent 8e1d3b9 commit 2bcc027

10 files changed

Lines changed: 200 additions & 26 deletions

‎Changelog.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## v1.119.4
4+
5+
Evaluation-edition licensing, and less friction in the admin/reboot confirmations.
6+
7+
- **Evaluation editions can now be licensed.** Activating a Windows Server *Evaluation* edition failed with "product key installation failed / product SKU is not found" because an Eval SKU can't take a product key directly. RackStack now detects the Evaluation edition and offers to convert it to the full edition with `DISM /Set-Edition` (one-way, requires a reboot). If you've configured a product key for that edition, it's used as the conversion key — a MAK/retail key both converts **and** activates after the reboot; otherwise the built-in KMS client key converts to full edition for activation against your KMS server.
8+
- **Fewer "type this exact phrase" confirmations.** Disabling the built-in Administrator while you're logged in as it now only asks for a simple yes/no when another verified local admin already exists (the long typed confirmation is reserved for the genuine lock-yourself-out case). If no alternate admin exists, RackStack offers to create one on the spot instead of just stopping.
9+
- **Reboot confirmation simplified.** The self-removal reboot on exit no longer makes you type the full computer name — it's a plain yes/no with the same warning.
10+
11+
No module or CLI action changes (81 modules, 201 actions).
12+
313
## v1.119.3
414

515
Real-server robustness — a batch of first-deployment reliability fixes for the agent install, file download, self-update, and connectivity paths.

‎Header.ps1‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
7h3 4b1d3r
3131
3232
.VERSION
33-
1.119.3
33+
1.119.4
3434
3535
.LAST UPDATED
3636
06/25/2026

‎Modules/00-Initialization.ps1‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ if (-not $PSCommandPath -and $script:ScriptPath) {
233233
if (-not $script:ModuleRoot -and $script:ScriptPath) {
234234
$script:ModuleRoot = [System.IO.Path]::GetDirectoryName($script:ScriptPath)
235235
}
236-
$script:ScriptVersion = "1.119.3"
236+
$script:ScriptVersion = "1.119.4"
237237
$script:ScriptStartTime = Get-Date
238238

239239
# Post-update cleanup: UpdateSelf / Rollback leave a `.pending-delete` sibling next to RackStack.exe.

‎Modules/21-Licensing.ps1‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ function Get-WindowsVersionInfo {
8282
return @{
8383
"WindowsVersion" = $windowsVersion
8484
"WindowsEdition" = $windowsEdition
85+
"EditionId" = $edition # raw registry EditionID (e.g. ServerStandardEval) for eval detection + DISM conversion
8586
"IsServer" = $isServer
8687
"BuildNumber" = $buildNumber
8788
"DisplayVersion" = $displayVersion
@@ -91,6 +92,7 @@ function Get-WindowsVersionInfo {
9192
return @{
9293
WindowsVersion = "Unknown"
9394
WindowsEdition = "Unknown"
95+
EditionId = "Unknown"
9496
IsServer = $false
9597
BuildNumber = "Unknown"
9698
DisplayVersion = "Unknown"
@@ -291,6 +293,105 @@ function Show-LicenseStatus {
291293
}
292294
}
293295

296+
# Run DISM /Set-Edition to convert an evaluation edition to its full edition. Shared by the
297+
# live path and the Dry-Run apply. /NoRestart so RackStack controls the reboot prompt.
298+
function Invoke-DismSetEdition {
299+
param(
300+
[Parameter(Mandatory)][string]$TargetEditionId,
301+
[Parameter(Mandatory)][string]$ProductKey,
302+
[Parameter(Mandatory)][string]$TargetFriendly
303+
)
304+
305+
Write-OutputColor " Running DISM /Set-Edition (this can take several minutes)..." -color "Info"
306+
$dismOut = & dism.exe /online /Set-Edition:$TargetEditionId /ProductKey:$ProductKey /AcceptEula /NoRestart 2>&1
307+
$dismCode = $LASTEXITCODE
308+
$dismText = ($dismOut | Out-String)
309+
310+
if ($dismCode -eq 0 -or $dismText -match '(?i)operation completed successfully') {
311+
Write-OutputColor " Edition conversion staged successfully." -color "Success"
312+
Write-OutputColor " Reboot to finish converting to $TargetFriendly. After reboot it activates" -color "Warning"
313+
Write-OutputColor " automatically (MAK/retail key) or via your KMS server (KMS client key)." -color "Info"
314+
$script:RebootNeeded = $true
315+
Add-SessionChange -Category "Licensing" -Description "Converted evaluation edition to $TargetFriendly (reboot pending)"
316+
} else {
317+
Write-OutputColor " Edition conversion failed (DISM exit $dismCode)." -color "Error"
318+
$errLine = ($dismOut | Where-Object { $_ -match '(?i)error|0x[0-9a-fA-F]{8}' } | Select-Object -First 1)
319+
if ($errLine) { Write-OutputColor " $($errLine.ToString().Trim())" -color "Error" }
320+
Write-OutputColor " Tip: run 'DISM /online /Get-TargetEditions' to list valid targets, and confirm" -color "Warning"
321+
Write-OutputColor " the key matches $TargetFriendly." -color "Warning"
322+
}
323+
}
324+
325+
# Convert a Windows Server Evaluation edition to its full edition. Eval SKUs reject
326+
# slmgr /ipk (0xC004F069 product-SKU-not-found), so DISM /Set-Edition is the only path.
327+
# Prefers the operator's configured key (a MAK/retail key converts AND activates on reboot);
328+
# a built-in GVLK converts to full and then needs a KMS server. Irreversible + needs reboot.
329+
function Invoke-EvalEditionConversion {
330+
param(
331+
[Parameter(Mandatory)][string]$WindowsVersion,
332+
[Parameter(Mandatory)][string]$TargetFriendly,
333+
[Parameter(Mandatory)][string]$TargetEditionId,
334+
[string]$ConversionKey,
335+
[bool]$UsingCustomKey
336+
)
337+
338+
Write-OutputColor "" -color "Info"
339+
Write-OutputColor " ┌────────────────────────────────────────────────────────────────────────┐" -color "Info"
340+
Write-OutputColor " │$(" EVALUATION EDITION DETECTED".PadRight(72))│" -color "Warning"
341+
Write-OutputColor " └────────────────────────────────────────────────────────────────────────┘" -color "Info"
342+
Write-OutputColor " Evaluation editions can't take a product key directly — that's the" -color "Info"
343+
Write-OutputColor " '0xC004F069 / product SKU not found' error you saw. They must first be" -color "Info"
344+
Write-OutputColor " converted to the full edition, which is one-way and needs a reboot." -color "Info"
345+
Write-OutputColor "" -color "Info"
346+
347+
# Resolve a conversion key — prompt if neither a configured nor built-in key is available.
348+
if ([string]::IsNullOrWhiteSpace($ConversionKey)) {
349+
Write-OutputColor " No $TargetFriendly key is configured. Enter a $TargetFriendly product key" -color "Info"
350+
Write-OutputColor " (MAK, retail, or KMS client key) to convert with:" -color "Info"
351+
$attempts = 0
352+
while ($attempts -lt $script:MaxRetryAttempts) {
353+
$entered = Read-Host " Product key"
354+
$nav = Test-NavigationCommand -UserInput $entered
355+
if ($nav.ShouldReturn) { return }
356+
if (-not [string]::IsNullOrWhiteSpace($entered) -and (Test-ValidLicenseKey -licenseKey $entered.ToUpper())) {
357+
$ConversionKey = $entered.ToUpper(); break
358+
}
359+
Write-OutputColor " Invalid key format (expected XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)." -color "Error"
360+
$attempts++
361+
}
362+
if ([string]::IsNullOrWhiteSpace($ConversionKey)) { Write-OutputColor " No valid key entered." -color "Error"; return }
363+
}
364+
365+
$keyTail = if ($ConversionKey.Length -ge 5) { $ConversionKey.Substring($ConversionKey.Length - 5) } else { $ConversionKey }
366+
Write-OutputColor " Target edition : $TargetFriendly ($TargetEditionId)" -color "Info"
367+
if ($UsingCustomKey) {
368+
Write-OutputColor " Using key : your configured $TargetFriendly key (*****-$keyTail)" -color "Success"
369+
Write-OutputColor " a MAK/retail key activates automatically after reboot." -color "Info"
370+
} else {
371+
Write-OutputColor " Using key : built-in KMS client key (*****-$keyTail)" -color "Info"
372+
Write-OutputColor " converts to full edition; needs a KMS server to activate." -color "Warning"
373+
}
374+
Write-OutputColor " This is IRREVERSIBLE (no going back to evaluation) and requires a reboot." -color "Warning"
375+
Write-OutputColor "" -color "Info"
376+
377+
if (-not (Confirm-UserAction -Message "Convert this server to $TargetFriendly now?")) {
378+
Write-OutputColor " Conversion cancelled." -color "Info"
379+
return
380+
}
381+
382+
if ($script:DryRunMode -and -not $script:ApplyingDryRunQueue) {
383+
$capId = $TargetEditionId; $capKey = $ConversionKey; $capName = $TargetFriendly
384+
Push-DryRunStep -Label "Convert evaluation edition to $capName (DISM /Set-Edition)" -Category "Licensing" -OneWay $true `
385+
-Params @{ TargetEdition = $capId } `
386+
-Apply { Invoke-DismSetEdition -TargetEditionId $capId -ProductKey $capKey -TargetFriendly $capName }.GetNewClosure()
387+
Write-OutputColor " Queued (Dry-Run): convert to $capName." -color "Warning"
388+
Add-SessionChange -Category "DryRun" -Description "Queued eval->$capName edition conversion"
389+
return
390+
}
391+
392+
Invoke-DismSetEdition -TargetEditionId $TargetEditionId -ProductKey $ConversionKey -TargetFriendly $TargetFriendly
393+
}
394+
294395
# Function to license the server
295396
function Register-ServerLicense {
296397
# --- Section: Initialization & Status Display ---
@@ -445,6 +546,27 @@ function Register-ServerLicense {
445546
}
446547
}
447548

549+
# --- Section: Evaluation Edition Conversion ---
550+
# Evaluation editions can't be activated with a product key — slmgr /ipk returns
551+
# 0xC004F069 "product SKU not found". They must first be converted to the full edition
552+
# with DISM /Set-Edition (which also stamps the key), then reboot. Route eval SKUs here
553+
# instead of the normal activation menu so the operator isn't stuck on that error. The
554+
# conversion key is resolved from the SAME merged table, so a configured custom key
555+
# (a MAK/retail key) both converts AND activates on reboot.
556+
if ($windowsInfo.EditionId -match 'Eval$') {
557+
$targetFriendly = ($windowsInfo.WindowsEdition -replace '\s*Evaluation$', '').Trim()
558+
$targetEditionId = $windowsInfo.EditionId -replace 'Eval$', ''
559+
$convKey = $null
560+
if ($keys.ContainsKey($windowsInfo.WindowsVersion) -and $keys[$windowsInfo.WindowsVersion].ContainsKey($targetFriendly)) {
561+
$convKey = $keys[$windowsInfo.WindowsVersion][$targetFriendly]
562+
}
563+
$usingCustom = ($script:CustomKMSKeys.ContainsKey($windowsInfo.WindowsVersion) -and
564+
$script:CustomKMSKeys[$windowsInfo.WindowsVersion].ContainsKey($targetFriendly))
565+
Invoke-EvalEditionConversion -WindowsVersion $windowsInfo.WindowsVersion -TargetFriendly $targetFriendly `
566+
-TargetEditionId $targetEditionId -ConversionKey $convKey -UsingCustomKey $usingCustom
567+
return
568+
}
569+
448570
# --- Section: Helper Function - Manual Key Entry ---
449571
# Helper function for manual key entry with retry logic
450572
function Enter-ManualKey {

‎Modules/24-DisableAdmin.ps1‎

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,20 @@ function Disable-BuiltInAdminAccount {
115115

116116
if (-not $hasVerifiedLocal -and -not $hasDomainPath -and $unverifiedExternalAdmins.Count -eq 0) {
117117
Write-OutputColor " ╔════════════════════════════════════════════════════════════════════════╗" -color "Error"
118-
Write-OutputColor " ║$(" BLOCKED: No alternate admin account detected!".PadRight(72))║" -color "Error"
118+
Write-OutputColor " ║$(" No alternate admin account detected.".PadRight(72))║" -color "Error"
119119
Write-OutputColor " ╠════════════════════════════════════════════════════════════════════════╣" -color "Error"
120-
Write-OutputColor " ║$(" Disabling the only admin account will LOCK YOU OUT.".PadRight(72))║" -color "Error"
121-
Write-OutputColor " ║$(" Create another local admin account first, or join a domain.".PadRight(72))║" -color "Error"
120+
Write-OutputColor " ║$(" Disabling the only admin account would LOCK YOU OUT.".PadRight(72))║" -color "Error"
122121
Write-OutputColor " ╚════════════════════════════════════════════════════════════════════════╝" -color "Error"
123122
Write-OutputColor "" -color "Info"
123+
# Offer to create one right here instead of just dead-ending, then come back.
124+
if (Confirm-UserAction -Message "Create a local admin account now?" -DefaultYes) {
125+
Add-LocalAdminAccount
126+
Write-OutputColor "" -color "Info"
127+
Write-OutputColor " Local admin step complete. Re-run 'Disable built-in Administrator'" -color "Info"
128+
Write-OutputColor " to finish — it will detect the new account as your fallback." -color "Info"
129+
} else {
130+
Write-OutputColor " Create another local admin account (or join a domain) first." -color "Warning"
131+
}
124132
Write-PressEnter
125133
return
126134
}
@@ -169,17 +177,22 @@ function Disable-BuiltInAdminAccount {
169177
$currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
170178
$builtInSid = $adminAccount.SID.Value
171179
if ($currentSid -eq $builtInSid) {
172-
Write-OutputColor "" -color "Critical"
173-
Write-OutputColor " CAUTION: you are currently logged in AS the built-in Administrator." -color "Critical"
174-
Write-OutputColor " Account name in use: $($adminAccount.Name) (SID $builtInSid)" -color "Warning"
175-
Write-OutputColor " Disabling it keeps your CURRENT session alive but blocks all future logons" -color "Warning"
176-
Write-OutputColor " to this account. You must verify the alternate admin works BEFORE rebooting." -color "Warning"
177-
Write-OutputColor " Type LOGGED-IN-AS-BUILTIN to confirm you understand:" -color "Critical"
178-
$selfConfirm = Read-Host
179-
if ($selfConfirm -ne 'LOGGED-IN-AS-BUILTIN') {
180-
Write-OutputColor " Cancelled." -color "Info"
181-
Write-PressEnter
182-
return
180+
Write-OutputColor "" -color "Warning"
181+
Write-OutputColor " Note: you're logged in AS the built-in Administrator ($($adminAccount.Name))." -color "Warning"
182+
Write-OutputColor " Disabling it keeps THIS session alive but blocks future logons to it —" -color "Warning"
183+
Write-OutputColor " sign in as the alternate admin shown above before rebooting." -color "Warning"
184+
# Only demand the typed confirmation when there's no locally-verified fallback admin
185+
# (real lockout risk). With a verified alternate local admin present, the plain y/N
186+
# below is enough — you can simply log in as that account. (Operators found the long
187+
# typed phrase needless busywork in the common "I already made another admin" case.)
188+
if (-not $hasVerifiedLocal) {
189+
Write-OutputColor " Type LOGGED-IN-AS-BUILTIN to confirm you understand:" -color "Critical"
190+
$selfConfirm = Read-Host
191+
if ($selfConfirm -ne 'LOGGED-IN-AS-BUILTIN') {
192+
Write-OutputColor " Cancelled." -color "Info"
193+
Write-PressEnter
194+
return
195+
}
183196
}
184197
}
185198

‎Modules/47-ExitCleanup.ps1‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,11 @@ function Exit-Script {
9090
} catch { }
9191

9292
Write-OutputColor "" -color "Critical"
93-
Write-OutputColor " WARNING: about to schedule a post-reboot SYSTEM deletion of tool files." -color "Critical"
94-
Write-OutputColor " This is a destructive operation that cannot be undone after reboot." -color "Warning"
95-
Write-OutputColor " Type the computer name '$env:COMPUTERNAME' to confirm:" -color "Critical"
96-
$hostConfirm = Read-Host
97-
if ($hostConfirm -ne $env:COMPUTERNAME) {
98-
Write-OutputColor " Cancelled (hostname did not match)." -color "Info"
93+
Write-OutputColor " About to reboot $env:COMPUTERNAME and schedule a post-reboot SYSTEM cleanup" -color "Critical"
94+
Write-OutputColor " that removes the tool's files from this server. This can't be undone after" -color "Warning"
95+
Write-OutputColor " reboot, and any unsaved work on this server will be lost." -color "Warning"
96+
if (-not (Confirm-UserAction -Message "Reboot now and remove tool files?")) {
97+
Write-OutputColor " Cancelled — no reboot." -color "Info"
9998
return
10099
}
101100

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<a href="https://www.bestpractices.dev/projects/12921"><img alt="OpenSSF Best Practices" src="https://www.bestpractices.dev/projects/12921/badge"></a>
2929
<a href="https://codecov.io/gh/TheAbider/RackStack"><img alt="codecov" src="https://codecov.io/gh/TheAbider/RackStack/branch/master/graph/badge.svg"></a>
3030
<img alt="PSScriptAnalyzer 0 errors" src="https://img.shields.io/badge/PSScriptAnalyzer-0%20errors-brightgreen">
31-
<img alt="5194 structural tests" src="https://img.shields.io/badge/structural%20tests-5194-brightgreen">
31+
<img alt="5202 structural tests" src="https://img.shields.io/badge/structural%20tests-5202-brightgreen">
3232
<img alt="Pester 312 tests" src="https://img.shields.io/badge/Pester-312%20tests-brightgreen">
3333
<img alt="SLSA Level 3" src="https://slsa.dev/images/gh-badge-level3.svg">
3434
</p>

‎RackStack.ps1‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
Environment-specific settings are configured via defaults.json.
1414
1515
.VERSION
16-
1.119.3
16+
1.119.4
1717
1818
.NOTES
1919
- Requires Windows Server 2012 R2 or later (or Windows 10/11 for testing)

‎RackStack.psd1‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@{
22
RootModule = 'RackStack.psm1'
3-
ModuleVersion = '1.119.3'
3+
ModuleVersion = '1.119.4'
44
GUID = 'c19b8e71-4a35-4f2b-9d06-8a24f7bc0e91'
55
Author = 'TheAbider'
66
CompanyName = 'TheAbider'

0 commit comments

Comments
 (0)