categories/RegistryOrphans.ps1
|
# Registry-orphans category (009/Kudu parity): DETECTS (never auto-deletes without Octa's own # confirmation) Uninstall registry entries whose UninstallString points at an executable that no # longer exists. Deliberately narrower than Kudu's own registry cleaner - "App Paths" scanning # was researched and dropped (research.md) for lacking a reliably false-positive-free pattern. function Get-OctaRegistryOrphansCategory { [CmdletBinding()] param() return [pscustomobject]@{ Id = 'registry-orphans' DisplayName = 'Registry Orphans' Description = 'Detect broken Uninstall registry entries' RequiresElevation = $true ContainsIrreversibleActions = $false GetActionsFunction = 'Get-OctaRegistryOrphansActions' ApplyActionFunction = 'Set-OctaRegistryOrphansAction' } } function Test-OctaUninstallTargetBroken { <# research.md's original assumption (Test-Path alone is enough, since MsiExec.exe "always exists") was wrong for bare executable names - Test-Path does not search PATH, so "MsiExec.exe" as a relative path would almost always report false/missing. Fixed here: an absolute path is Test-Path'd directly; a bare name is resolved via Get-Command, the same way Windows itself would resolve it when actually invoking the uninstaller. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$UninstallString) if ([string]::IsNullOrWhiteSpace($UninstallString)) { return $false } $token = if ($UninstallString -match '^"([^"]+)"') { $Matches[1] } else { ($UninstallString -split '\s+')[0] } if ([string]::IsNullOrWhiteSpace($token)) { return $false } if ([System.IO.Path]::IsPathRooted($token)) { return -not (Test-Path -LiteralPath $token) } return -not [bool](Get-Command $token -ErrorAction SilentlyContinue) } function Get-OctaRegistryOrphansActions { [CmdletBinding()] param() $actions = @() $uninstallRoots = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' ) foreach ($root in $uninstallRoots) { if (-not (Test-Path $root)) { continue } Get-ChildItem -Path $root -ErrorAction SilentlyContinue | ForEach-Object { $keyPath = Join-Path $root $_.PSChildName $entry = Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue if (-not $entry.DisplayName) { return } if (-not $entry.UninstallString) { return } if (-not (Test-OctaUninstallTargetBroken -UninstallString $entry.UninstallString)) { return } $actions += New-OctaAction -TargetType Registry -TargetIdentifier "$keyPath|(delete)" ` -CurrentValue $entry.UninstallString -PlannedValue '(removed)' -Reversible $true -RiskLevel Risky } } return $actions } function Set-OctaRegistryOrphansAction { [CmdletBinding()] param([Parameter(Mandatory)]$Action) $keyPath, $valueName = $Action.TargetIdentifier -split '\|', 2 if ($valueName -eq '(delete)') { Remove-Item -LiteralPath $keyPath -Recurse -Force -ErrorAction SilentlyContinue } } |