Private/Resolve-WUDuplicateProfiles.ps1
|
function Resolve-WUDuplicateProfiles { <# .SYNOPSIS Detects and resolves duplicate user profile registry entries that block Windows upgrades. .DESCRIPTION This function identifies orphaned SID entries in the ProfileList registry that point to the same user profile path, which causes Windows Setup to fail with error 0x8007001F (ERROR_GEN_FAILURE) and 0x000004E5 (ERROR_BAD_PROFILE). This commonly occurs after domain/workgroup changes where old SIDs remain registered while new SIDs are created for the same profile path. .PARAMETER LogPath Path to the log file for logging operations. .PARAMETER WhatIf Shows what would happen without making changes. .EXAMPLE Resolve-WUDuplicateProfiles -LogPath "C:\Windows\Temp\repair.log" Detects and removes orphaned profile SIDs that conflict with active accounts. .OUTPUTS PSCustomObject with detection and remediation results. .NOTES Name: Resolve-WUDuplicateProfiles Author: Anthony Balloi - CSOLVE Version: 1.0.0 Common error codes resolved: - 0x8007001F: ERROR_GEN_FAILURE (device not functioning) - 0x000004E5: ERROR_BAD_PROFILE (user profile invalid) Requires Administrator privileges to modify HKLM registry. #> [CmdletBinding(SupportsShouldProcess)] param( [string]$LogPath ) $result = [PSCustomObject]@{ DuplicatesDetected = 0 DuplicateProfiles = @() OrphanedSIDsRemoved = 0 RemediationSuccessful = $false Errors = @() } try { Write-WULog -Message "Checking for duplicate user profile registry entries..." -LogPath $LogPath # Query all Administrator profile SIDs (ending in -500) $profileListPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $adminProfiles = Get-ChildItem $profileListPath | Where-Object { $_.PSChildName -match '^S-1-5-21-.+-500$' } if ($adminProfiles.Count -eq 0) { Write-WULog -Message "No Administrator profile entries found in registry" -LogPath $LogPath return $result } Write-WULog -Message "Found $($adminProfiles.Count) Administrator profile SID(s) in registry" -LogPath $LogPath # Group by ProfileImagePath to find duplicates $profilePaths = @{} foreach ($profile in $adminProfiles) { $sid = $profile.PSChildName $imagePath = (Get-ItemProperty $profile.PSPath -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath if ($imagePath) { # Normalize path for comparison (case-insensitive) $normalizedPath = $imagePath.ToLower() if ($profilePaths.ContainsKey($normalizedPath)) { $profilePaths[$normalizedPath] += @($sid) } else { $profilePaths[$normalizedPath] = @($sid) } Write-WULog -Message " SID: $sid -> Path: $imagePath" -LogPath $LogPath } } # Identify duplicates $duplicates = $profilePaths.GetEnumerator() | Where-Object { $_.Value.Count -gt 1 } if ($duplicates.Count -eq 0) { Write-WULog -Message "No duplicate profile paths detected" -LogPath $LogPath $result.RemediationSuccessful = $true return $result } $result.DuplicatesDetected = $duplicates.Count Write-WULog -Message "DETECTED: $($duplicates.Count) duplicate profile path(s)" -Level Warning -LogPath $LogPath # Get active local users to identify which SID is valid $localUsers = Get-LocalUser -ErrorAction SilentlyContinue foreach ($duplicate in $duplicates) { $profilePath = $duplicate.Key $sids = $duplicate.Value Write-WULog -Message "Duplicate profile path: $profilePath" -Level Warning -LogPath $LogPath Write-WULog -Message " Multiple SIDs detected: $($sids -join ', ')" -LogPath $LogPath $duplicateInfo = [PSCustomObject]@{ ProfilePath = $profilePath SIDs = $sids ActiveSID = $null OrphanedSIDs = @() } # Identify the active SID by matching against local users $activeSID = $null foreach ($sid in $sids) { $matchingUser = $localUsers | Where-Object { $_.SID.Value -eq $sid } if ($matchingUser) { $activeSID = $sid $duplicateInfo.ActiveSID = $sid Write-WULog -Message " Active SID (matches local user '$($matchingUser.Name)'): $sid" -LogPath $LogPath break } } # If no active SID found, use the most recent (last in array) as a heuristic if (-not $activeSID) { $activeSID = $sids[-1] $duplicateInfo.ActiveSID = $activeSID Write-WULog -Message " No matching local user - assuming newest SID is active: $activeSID" -Level Warning -LogPath $LogPath } # Mark other SIDs as orphaned $orphanedSIDs = $sids | Where-Object { $_ -ne $activeSID } $duplicateInfo.OrphanedSIDs = $orphanedSIDs $result.DuplicateProfiles += $duplicateInfo # Remove orphaned SIDs foreach ($orphanedSID in $orphanedSIDs) { $orphanedKeyPath = Join-Path $profileListPath $orphanedSID Write-WULog -Message " Removing orphaned SID registry key: $orphanedSID" -LogPath $LogPath if ($PSCmdlet.ShouldProcess($orphanedKeyPath, "Remove orphaned profile registry key")) { try { Remove-Item $orphanedKeyPath -Recurse -Force -ErrorAction Stop $result.OrphanedSIDsRemoved++ Write-WULog -Message " Successfully removed orphaned SID: $orphanedSID" -LogPath $LogPath } catch { $errorMsg = "Failed to remove orphaned SID $orphanedSID : $($_.Exception.Message)" Write-WULog -Message " $errorMsg" -Level Error -LogPath $LogPath $result.Errors += $errorMsg } } } } # Verify cleanup if ($result.OrphanedSIDsRemoved -gt 0) { Write-WULog -Message "Successfully removed $($result.OrphanedSIDsRemoved) orphaned profile SID(s)" -LogPath $LogPath $result.RemediationSuccessful = $true # Verify duplicates are gone $adminProfilesAfter = Get-ChildItem $profileListPath | Where-Object { $_.PSChildName -match '^S-1-5-21-.+-500$' } Write-WULog -Message "Verification: $($adminProfilesAfter.Count) Administrator profile SID(s) remain" -LogPath $LogPath } elseif ($result.Errors.Count -eq 0) { $result.RemediationSuccessful = $true } } catch { $errorMsg = "Failed to resolve duplicate profiles: $($_.Exception.Message)" Write-WULog -Message $errorMsg -Level Error -LogPath $LogPath $result.Errors += $errorMsg } return $result } |