Guard against loading, saving & executing undefined features (#665)

This commit is contained in:
Jeffrey
2026-06-23 00:41:33 +02:00
committed by GitHub
parent d1fe541b62
commit 5ebc50d36a
11 changed files with 209 additions and 84 deletions

View File

@@ -1,17 +1,10 @@
function Get-FeatureId {
param(
[Parameter(Mandatory)]
$Feature
)
$featureId = [string]$Feature.FeatureId
if ([string]::IsNullOrWhiteSpace($featureId)) {
throw 'Selected feature is missing required FeatureId.'
}
return $featureId
}
<#
.SYNOPSIS
Filters a list of features to those that have a non-empty RegistryKey.
.PARAMETER Features
An array of feature objects to filter.
#>
function Get-RegistryBackedFeatures {
param(
[object[]]$Features = @()

View File

@@ -1,3 +1,18 @@
<#
.SYNOPSIS
Creates a timestamped JSON backup of registry state for selected features.
.DESCRIPTION
Resolves selected and undo features from the provided keys, captures their
registry state, and saves the result as a JSON file in the Backups/ folder.
Returns the file path on success, $null if no registry-backed features exist.
.PARAMETER ActionableKeys
Param keys from $script:Params to resolve into apply features.
.PARAMETER ExtraFeatures
Additional synthetic feature objects (e.g. undo features) to include.
#>
function New-RegistrySettingsBackup {
param(
[string[]]$ActionableKeys,
@@ -32,6 +47,13 @@ function New-RegistrySettingsBackup {
return $backupFilePath
}
<#
.SYNOPSIS
Resolves param keys into deduplicated feature objects from the catalog.
.PARAMETER ActionableKeys
Param keys to look up in $script:Features.
#>
function Get-SelectedFeatures {
param(
[string[]]$ActionableKeys
@@ -46,8 +68,7 @@ function Get-SelectedFeatures {
$feature = $script:Features[$paramKey]
if (-not $feature) { continue }
$featureId = Get-FeatureId -Feature $feature
$featureId = [string]$feature.FeatureId
if ($selectedFeatureIds.Add($featureId)) {
$selectedFeatures.Add($feature)
}
@@ -56,6 +77,23 @@ function Get-SelectedFeatures {
return @($selectedFeatures.ToArray())
}
<#
.SYNOPSIS
Builds the full backup payload object from selected and undo features.
.DESCRIPTION
Deduplicates feature IDs, resolves registry capture plans, snapshots all
registry keys, and assembles the final backup hashtable with metadata.
.PARAMETER SelectedFeatures
Feature objects from the apply side.
.PARAMETER UndoFeatures
Synthetic feature objects from the undo side.
.PARAMETER CreatedAt
Timestamp recorded in the backup metadata.
#>
function Get-RegistryBackupPayload {
param(
[object[]]$SelectedFeatures = @(),
@@ -67,8 +105,7 @@ function Get-RegistryBackupPayload {
$selectedFeatureIds = New-Object System.Collections.Generic.List[string]
$seenSelectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($feature in $SelectedFeatures) {
$featureId = Get-FeatureId -Feature $feature
$featureId = [string]$feature.FeatureId
if ($seenSelectedFeatureIds.Add($featureId)) {
$selectedFeatureIds.Add($featureId)
}
@@ -77,8 +114,7 @@ function Get-RegistryBackupPayload {
$selectedUndoFeatureIds = New-Object System.Collections.Generic.List[string]
$seenUndoFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($feature in $UndoFeatures) {
$featureId = Get-FeatureId -Feature $feature
$featureId = [string]$feature.FeatureId
if ($seenUndoFeatureIds.Add($featureId)) {
$selectedUndoFeatureIds.Add($featureId)
}

View File

@@ -1,6 +1,10 @@
# Tests whether the registry operations in a feature's .reg file currently match the live registry.
# Returns $true if ALL operations in the apply reg file match current system state.
# Returns $false if the feature has no RegistryKey, the file is missing, or any operation mismatches.
<#
.SYNOPSIS
Maps a .reg file value type string to its RegistryValueKind enum.
.PARAMETER Operation
A parsed .reg operation object containing a ValueType property.
#>
function Get-ExpectedRegistryValueKind {
param(
[Parameter(Mandatory)]
@@ -18,13 +22,25 @@ function Get-ExpectedRegistryValueKind {
}
}
<#
.SYNOPSIS
Tests whether a feature's registry operations currently match the live registry.
.DESCRIPTION
Returns $true when ALL operations in the apply .reg file match current system
state. Returns $false if the feature has no RegistryKey, the reg file is
missing, or any operation mismatches. Special-cased features (Widgets, Store
suggestions, Windows Sandbox, WSL) bypass .reg checking entirely.
.PARAMETER FeatureId
The feature identifier to test.
#>
function Test-FeatureApplied {
param (
[Parameter(Mandatory)]
[string]$FeatureId
)
if (-not $script:Features.ContainsKey($FeatureId)) { return $false }
$feature = $script:Features[$FeatureId]
switch ($FeatureId) {
@@ -147,8 +163,18 @@ function Test-FeatureApplied {
return $true
}
# Returns the 1-based index of the UiGroup option whose features all match current system state,
# or 0 if no option fully matches (meaning the current state is unknown / "No Change").
<#
.SYNOPSIS
Returns the 1-based index of the UiGroup option whose features all match
current system state.
.DESCRIPTION
Returns 0 if no option fully matches, meaning the current state is unknown
or represents "No Change".
.PARAMETER Group
A UiGroup object whose Values array contains options with FeatureIds.
#>
function Get-CurrentGroupActiveIndex {
param (
[Parameter(Mandatory)]

View File

@@ -16,15 +16,11 @@ function Invoke-FeatureApply {
)
# Resolve feature metadata from Features.json
$feature = $null
if ($script:Features.ContainsKey($FeatureId)) {
$feature = $script:Features[$FeatureId]
}
$applyText = if ($feature -and $feature.ApplyText) { $feature.ApplyText } else { $FeatureId }
$feature = $script:Features[$FeatureId]
$applyText = $feature.ApplyText
# ---- Registry-backed features: import .reg file, then handle side effects ----
if ($feature -and $feature.RegistryKey) {
if ($feature.RegistryKey) {
ImportRegistryFile "> $applyText..." $feature.RegistryKey
# Post-import side effects for specific features
@@ -177,15 +173,13 @@ function Invoke-FeatureUndo {
return
}
'EnableWindowsSandbox' {
$undoText = if ($feature) { $feature.ApplyUndoText } else { 'Disabling Windows Sandbox' }
Write-Host "> $undoText..."
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Containers-DisposableClientVM'
Write-Host ""
return
}
'EnableWindowsSubsystemForLinux' {
$undoText = if ($feature) { $feature.ApplyUndoText } else { 'Disabling Windows Subsystem for Linux' }
Write-Host "> $undoText..."
Write-Host "> $($feature.ApplyUndoText)..."
DisableWindowsFeature 'Microsoft-Windows-Subsystem-Linux'
DisableWindowsFeature 'VirtualMachinePlatform'
Write-Host ""
@@ -245,15 +239,8 @@ function Invoke-ApplyFeatures {
if ($script:CancelRequested) { return }
# Resolve display name for the progress indicator
$displayName = $featureId
if ($script:Features.ContainsKey($featureId)) {
$f = $script:Features[$featureId]
if ($f.ApplyText) {
$displayName = $f.ApplyText
} elseif ($f.Label) {
$displayName = $f.Label
}
}
$f = $script:Features[$featureId]
$displayName = $f.ApplyText
if ($script:ApplyProgressCallback) {
& $script:ApplyProgressCallback $step $TotalSteps $displayName
@@ -345,7 +332,6 @@ function Invoke-AllChanges {
# ---- Determine if registry backup is needed ----
$needsBackup = $false
foreach ($id in $applyIds) {
if (-not $script:Features.ContainsKey($id)) { continue }
$f = $script:Features[$id]
if ($f -and -not [string]::IsNullOrWhiteSpace([string]$f.RegistryKey)) {
$needsBackup = $true