mirror of
https://github.com/Raphire/Win11Debloat.git
synced 2026-05-18 11:46:18 +00:00
383 lines
12 KiB
PowerShell
383 lines
12 KiB
PowerShell
|
|
function Get-NormalizedSelectedFeatureIdsFromBackup {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
$Backup
|
||
|
|
)
|
||
|
|
|
||
|
|
$selectedFeatures = New-Object System.Collections.Generic.List[string]
|
||
|
|
$selectedFeatureIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||
|
|
$errors = New-Object System.Collections.Generic.List[string]
|
||
|
|
$hasInvalidSelectedFeatureId = $false
|
||
|
|
|
||
|
|
if (-not $Backup.PSObject.Properties['SelectedFeatures']) {
|
||
|
|
$errors.Add('Missing property: SelectedFeatures')
|
||
|
|
return [PSCustomObject]@{
|
||
|
|
SelectedFeatures = $selectedFeatures.ToArray()
|
||
|
|
Errors = $errors.ToArray()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($featureId in @($Backup.SelectedFeatures)) {
|
||
|
|
if ($featureId -isnot [string] -or [string]::IsNullOrWhiteSpace([string]$featureId)) {
|
||
|
|
$hasInvalidSelectedFeatureId = $true
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
$normalizedFeatureId = [string]$featureId
|
||
|
|
if ($selectedFeatureIds.Add($normalizedFeatureId)) {
|
||
|
|
$selectedFeatures.Add($normalizedFeatureId)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($hasInvalidSelectedFeatureId) {
|
||
|
|
$errors.Add('SelectedFeatures must contain non-empty string feature IDs.')
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($selectedFeatures.Count -eq 0) {
|
||
|
|
$errors.Add('SelectedFeatures must contain at least one feature ID.')
|
||
|
|
}
|
||
|
|
|
||
|
|
return [PSCustomObject]@{
|
||
|
|
SelectedFeatures = $selectedFeatures.ToArray()
|
||
|
|
Errors = $errors.ToArray()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Normalize-RegistryKeySnapshot {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
$Snapshot
|
||
|
|
)
|
||
|
|
|
||
|
|
if (-not $Snapshot.PSObject.Properties['Path'] -or [string]::IsNullOrWhiteSpace([string]$Snapshot.Path)) {
|
||
|
|
throw 'Backup validation failed: Registry key snapshot is missing Path.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$exists = $false
|
||
|
|
if ($Snapshot.PSObject.Properties['Exists']) {
|
||
|
|
$exists = [bool]$Snapshot.Exists
|
||
|
|
}
|
||
|
|
|
||
|
|
$values = @()
|
||
|
|
if ($Snapshot.PSObject.Properties['Values']) {
|
||
|
|
foreach ($valueSnapshot in @($Snapshot.Values)) {
|
||
|
|
$valueExists = $true
|
||
|
|
if ($valueSnapshot.PSObject.Properties['Exists']) {
|
||
|
|
$valueExists = [bool]$valueSnapshot.Exists
|
||
|
|
}
|
||
|
|
|
||
|
|
$values += [PSCustomObject]@{
|
||
|
|
Name = [string]$valueSnapshot.Name
|
||
|
|
Exists = $valueExists
|
||
|
|
Kind = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { $null }
|
||
|
|
Data = if ($valueSnapshot.PSObject.Properties['Data']) { $valueSnapshot.Data } else { $null }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$subKeys = @()
|
||
|
|
if ($Snapshot.PSObject.Properties['SubKeys']) {
|
||
|
|
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
|
||
|
|
$subKeys += @(Normalize-RegistryKeySnapshot -Snapshot $subKeySnapshot)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return [PSCustomObject]@{
|
||
|
|
Path = [string]$Snapshot.Path
|
||
|
|
Exists = $exists
|
||
|
|
Values = @($values)
|
||
|
|
SubKeys = @($subKeys)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-RegistryBackupMatchesSelectedFeatures {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
[string[]]$SelectedFeatureIds,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
[object[]]$RegistryKeys
|
||
|
|
)
|
||
|
|
|
||
|
|
$errors = New-Object System.Collections.Generic.List[string]
|
||
|
|
|
||
|
|
if (-not $script:Features -or $script:Features.Count -eq 0) {
|
||
|
|
$errors.Add('Unable to validate registry backup allowlist because feature definitions are not loaded.')
|
||
|
|
return $errors.ToArray()
|
||
|
|
}
|
||
|
|
|
||
|
|
$selectedRegistryFeatures = @(Get-SelectedRegistryFeaturesForBackupValidation -SelectedFeatureIds @($SelectedFeatureIds) -Errors $errors)
|
||
|
|
|
||
|
|
$capturePlans = @()
|
||
|
|
if ($errors.Count -eq 0 -and $selectedRegistryFeatures.Count -gt 0) {
|
||
|
|
$capturePlans = @(Get-RegistryBackupCapturePlans -SelectedRegistryFeatures @($selectedRegistryFeatures))
|
||
|
|
}
|
||
|
|
|
||
|
|
$planMap = New-RegistryBackupAllowListPlanMap -CapturePlans @($capturePlans)
|
||
|
|
|
||
|
|
if ($planMap.Count -eq 0 -and @($RegistryKeys).Count -gt 0) {
|
||
|
|
$errors.Add('Backup contains registry snapshots but no allowed registry paths were derived from SelectedFeatures.')
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($rootSnapshot in @($RegistryKeys)) {
|
||
|
|
Test-RegistrySnapshotAgainstAllowList -Snapshot $rootSnapshot -PlanMap $planMap -Errors $errors
|
||
|
|
}
|
||
|
|
|
||
|
|
return $errors.ToArray()
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-SelectedRegistryFeaturesForBackupValidation {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
[string[]]$SelectedFeatureIds,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
$Errors
|
||
|
|
)
|
||
|
|
|
||
|
|
if ($null -eq $Errors -or -not ($Errors -is [System.Collections.IList])) {
|
||
|
|
throw 'Get-SelectedRegistryFeaturesForBackupValidation requires Errors to be a mutable list collection.'
|
||
|
|
}
|
||
|
|
|
||
|
|
$selectedRegistryFeatures = New-Object System.Collections.Generic.List[object]
|
||
|
|
foreach ($featureId in @($SelectedFeatureIds)) {
|
||
|
|
if (-not $script:Features.ContainsKey($featureId)) {
|
||
|
|
$Errors.Add("Selected feature '$featureId' was not found in the current feature catalog.")
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
$feature = $script:Features[$featureId]
|
||
|
|
if ($feature -and -not [string]::IsNullOrWhiteSpace([string]$feature.RegistryKey)) {
|
||
|
|
$selectedRegistryFeatures.Add($feature)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $selectedRegistryFeatures.ToArray()
|
||
|
|
}
|
||
|
|
|
||
|
|
function New-RegistryBackupAllowListPlanMap {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
[object[]]$CapturePlans
|
||
|
|
)
|
||
|
|
|
||
|
|
$planMap = @{}
|
||
|
|
foreach ($plan in @($CapturePlans)) {
|
||
|
|
$normalizedPath = Get-NormalizedRegistryPathKey -Path $plan.Path
|
||
|
|
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
$planMap[$normalizedPath] = [PSCustomObject]@{
|
||
|
|
Path = $plan.Path
|
||
|
|
NormalizedPath = $normalizedPath
|
||
|
|
IncludeSubKeys = [bool]$plan.IncludeSubKeys
|
||
|
|
CaptureAllValues = [bool]$plan.CaptureAllValues
|
||
|
|
ValueNames = ConvertTo-RegistryValueNameSet -ValueNames @($plan.ValueNames)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $planMap
|
||
|
|
}
|
||
|
|
|
||
|
|
function ConvertTo-RegistryValueNameSet {
|
||
|
|
param(
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
[string[]]$ValueNames
|
||
|
|
)
|
||
|
|
|
||
|
|
$valueNameSet = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
|
||
|
|
foreach ($valueName in @($ValueNames)) {
|
||
|
|
$null = $valueNameSet.Add([string]$valueName)
|
||
|
|
}
|
||
|
|
|
||
|
|
return $valueNameSet
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-RegistrySnapshotAgainstAllowList {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
$Snapshot,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[hashtable]$PlanMap,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowEmptyCollection()]
|
||
|
|
[System.Collections.Generic.List[string]]$Errors
|
||
|
|
)
|
||
|
|
|
||
|
|
$snapshotPath = [string]$Snapshot.Path
|
||
|
|
$normalizedPath = Get-NormalizedRegistryPathKey -Path $snapshotPath
|
||
|
|
if ([string]::IsNullOrWhiteSpace($normalizedPath)) {
|
||
|
|
$Errors.Add("Backup contains unsupported registry path '$snapshotPath'.")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
$planMatch = Find-RegistryAllowListPlanMatch -NormalizedPath $normalizedPath -PlanMap $PlanMap
|
||
|
|
if ($null -eq $planMatch) {
|
||
|
|
$Errors.Add("Backup contains unexpected registry path '$snapshotPath' that is not allowed by SelectedFeatures.")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($valueSnapshot in @($Snapshot.Values)) {
|
||
|
|
$valueName = Get-NormalizedRegistryValueName -ValueName $valueSnapshot.Name
|
||
|
|
$valueExists = [bool]$valueSnapshot.Exists
|
||
|
|
|
||
|
|
if (-not (Test-RegistryValueAllowedByPlan -PlanMatch $planMatch -ValueName $valueName)) {
|
||
|
|
$Errors.Add("Backup contains unexpected value '$valueName' under '$snapshotPath'.")
|
||
|
|
}
|
||
|
|
|
||
|
|
$kindName = if ($valueSnapshot.PSObject.Properties['Kind']) { [string]$valueSnapshot.Kind } else { '' }
|
||
|
|
$valueReference = Get-RegistryValueReferenceForError -SnapshotPath $snapshotPath -ValueName $valueName
|
||
|
|
if ($valueExists) {
|
||
|
|
if (-not (Test-RegistryValueKindNameSupported -KindName $kindName)) {
|
||
|
|
$Errors.Add("Backup contains unsupported registry value kind '$kindName' for '$valueReference'.")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
elseif (-not [string]::IsNullOrWhiteSpace($kindName)) {
|
||
|
|
$Errors.Add("Backup value '$valueReference' must not define Kind when Exists is false.")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($subKeySnapshot in @($Snapshot.SubKeys)) {
|
||
|
|
Test-RegistrySnapshotAgainstAllowList -Snapshot $subKeySnapshot -PlanMap $PlanMap -Errors $Errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-RegistryValueAllowedByPlan {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
$PlanMatch,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowNull()]
|
||
|
|
[AllowEmptyString()]
|
||
|
|
[string]$ValueName
|
||
|
|
)
|
||
|
|
|
||
|
|
$ValueName = Get-NormalizedRegistryValueName -ValueName $ValueName
|
||
|
|
|
||
|
|
if ($PlanMatch.CaptureAllValues -or $PlanMatch.IsDescendant) {
|
||
|
|
return $true
|
||
|
|
}
|
||
|
|
|
||
|
|
return $PlanMatch.ValueNames.Contains($ValueName)
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-RegistryValueReferenceForError {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[string]$SnapshotPath,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[AllowNull()]
|
||
|
|
[AllowEmptyString()]
|
||
|
|
[string]$ValueName
|
||
|
|
)
|
||
|
|
|
||
|
|
$ValueName = Get-NormalizedRegistryValueName -ValueName $ValueName
|
||
|
|
|
||
|
|
if ([string]::IsNullOrWhiteSpace($ValueName)) {
|
||
|
|
return "$SnapshotPath\\(Default)"
|
||
|
|
}
|
||
|
|
|
||
|
|
return "$SnapshotPath\\$ValueName"
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-NormalizedRegistryValueName {
|
||
|
|
param(
|
||
|
|
[AllowNull()]
|
||
|
|
[AllowEmptyString()]
|
||
|
|
[object]$ValueName
|
||
|
|
)
|
||
|
|
|
||
|
|
if ($null -eq $ValueName) {
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
return [string]$ValueName
|
||
|
|
}
|
||
|
|
|
||
|
|
function Find-RegistryAllowListPlanMatch {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[string]$NormalizedPath,
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[hashtable]$PlanMap
|
||
|
|
)
|
||
|
|
|
||
|
|
if ($PlanMap.ContainsKey($NormalizedPath)) {
|
||
|
|
$plan = $PlanMap[$NormalizedPath]
|
||
|
|
return [PSCustomObject]@{
|
||
|
|
IsDescendant = $false
|
||
|
|
CaptureAllValues = [bool]$plan.CaptureAllValues
|
||
|
|
ValueNames = $plan.ValueNames
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($plan in @($PlanMap.Values)) {
|
||
|
|
if (-not [bool]$plan.IncludeSubKeys) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
$subKeyPrefix = "$($plan.NormalizedPath)\\"
|
||
|
|
if ($NormalizedPath.StartsWith($subKeyPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||
|
|
return [PSCustomObject]@{
|
||
|
|
IsDescendant = $true
|
||
|
|
CaptureAllValues = $true
|
||
|
|
ValueNames = $plan.ValueNames
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $null
|
||
|
|
}
|
||
|
|
|
||
|
|
function Get-NormalizedRegistryPathKey {
|
||
|
|
param(
|
||
|
|
[Parameter(Mandatory)]
|
||
|
|
[string]$Path
|
||
|
|
)
|
||
|
|
|
||
|
|
$parts = Split-RegistryPath -path $Path
|
||
|
|
if (-not $parts) {
|
||
|
|
return $null
|
||
|
|
}
|
||
|
|
|
||
|
|
$hiveName = [string]$parts.Hive
|
||
|
|
if ([string]::IsNullOrWhiteSpace($hiveName)) {
|
||
|
|
return $null
|
||
|
|
}
|
||
|
|
|
||
|
|
$normalizedHive = $hiveName.ToUpperInvariant()
|
||
|
|
$subKey = [string]$parts.SubKey
|
||
|
|
if ([string]::IsNullOrWhiteSpace($subKey)) {
|
||
|
|
return $normalizedHive
|
||
|
|
}
|
||
|
|
|
||
|
|
$normalizedSubKey = ($subKey -replace '/', '\\').Trim('\')
|
||
|
|
if ([string]::IsNullOrWhiteSpace($normalizedSubKey)) {
|
||
|
|
return $normalizedHive
|
||
|
|
}
|
||
|
|
|
||
|
|
return "$normalizedHive\\$normalizedSubKey"
|
||
|
|
}
|
||
|
|
|
||
|
|
function Test-RegistryValueKindNameSupported {
|
||
|
|
param(
|
||
|
|
[string]$KindName
|
||
|
|
)
|
||
|
|
|
||
|
|
if ([string]::IsNullOrWhiteSpace($KindName)) {
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$kind = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $KindName, $true)
|
||
|
|
return $kind -ne [Microsoft.Win32.RegistryValueKind]::Unknown
|
||
|
|
}
|
||
|
|
catch {
|
||
|
|
return $false
|
||
|
|
}
|
||
|
|
}
|