WinProfileOps.psm1
#Region './Classes/ProfileDeletionResult.ps1' -1 class ProfileDeletionResult { [string]$SID [string]$ProfilePath [bool]$DeletionSuccess [string]$DeletionMessage [string]$ComputerName # Constructor to initialize the properties ProfileDeletionResult([string]$sid, [string]$profilePath, [bool]$deletionSuccess, [string]$deletionMessage, [string]$computerName) { $this.SID = $sid $this.ProfilePath = $profilePath $this.DeletionSuccess = $deletionSuccess $this.DeletionMessage = $deletionMessage $this.ComputerName = $computerName } } #EndRegion './Classes/ProfileDeletionResult.ps1' 17 #Region './Classes/UserProfile.ps1' -1 class UserProfile { [string]$SID [string]$ProfilePath [bool]$IsOrphaned [string]$OrphanReason [string]$ComputerName [bool]$IsSpecial # Constructor to initialize the properties UserProfile([string]$sid, [string]$profilePath, [bool]$isOrphaned, [string]$orphanReason, [string]$computerName, [bool]$isSpecial) { $this.SID = $sid $this.ProfilePath = $profilePath $this.IsOrphaned = $isOrphaned $this.OrphanReason = $orphanReason $this.ComputerName = $computerName $this.IsSpecial = $isSpecial } } #EndRegion './Classes/UserProfile.ps1' 19 #Region './Public/Get-AllUserProfiles.ps1' -1 function Get-AllUserProfiles { [CmdletBinding()] param ( [Parameter(Mandatory = $false, ValueFromPipeline = $true)] [string]$ComputerName = $env:COMPUTERNAME, [string]$ProfileFolderPath = "C:\Users", [switch]$IgnoreSpecial ) # Begin block runs once before processing pipeline input begin { # Initialize an array to hold all UserProfile objects across multiple pipeline inputs $AllProfiles = @() } # Process block runs once for each input object (in case of pipeline) process { # Test if the computer is online before proceeding if (-not (Test-ComputerPing -ComputerName $ComputerName)) { Write-Warning "Computer '$ComputerName' is offline or unreachable." return # Skip to the next input in the pipeline } # Get profiles from folders and registry $UserFolders = Get-UserProfilesFromFolders -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath $RegistryProfiles = Get-UserProfilesFromRegistry -ComputerName $ComputerName # Loop through registry profiles and check for folder existence and ProfileImagePath foreach ($regProfile in $RegistryProfiles) { $profilePath = $regProfile.ProfilePath $folderExists = Test-FolderExists -ProfilePath $profilePath -ComputerName $regProfile.ComputerName $folderName = Split-Path -Path $profilePath -Leaf $isSpecial = Test-SpecialAccount -FolderName $folderName -SID $regProfile.SID -ProfilePath $profilePath # Skip special profiles if IgnoreSpecial is set if ($IgnoreSpecial -and $isSpecial) { continue } # Detect if the profile is orphaned and create the user profile object $userProfile = Test-OrphanedProfile -SID $regProfile.SID -ProfilePath $profilePath ` -FolderExists $folderExists -IgnoreSpecial $IgnoreSpecial ` -IsSpecial $isSpecial -ComputerName $ComputerName $AllProfiles += $userProfile } # Loop through user folders and check if they exist in the registry foreach ($folder in $UserFolders) { $registryProfile = $RegistryProfiles | Where-Object { $_.ProfilePath -eq $folder.ProfilePath } $isSpecial = Test-SpecialAccount -FolderName $folder.FolderName -SID $null -ProfilePath $folder.ProfilePath # Skip special profiles if IgnoreSpecial is set if ($IgnoreSpecial -and $isSpecial) { continue } # Case 4: Folder exists in C:\Users but not in the registry if (-not $registryProfile) { $AllProfiles += New-UserProfileObject $null $folder.ProfilePath $true "MissingRegistryEntry" $ComputerName $isSpecial } } } # End block runs once after all processing is complete end { # Output all collected profiles $AllProfiles } } #EndRegion './Public/Get-AllUserProfiles.ps1' 71 #Region './Public/Get-OrphanedProfiles.ps1' -1 function Get-OrphanedProfiles { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [string]$ProfileFolderPath = "C:\Users", [switch]$IgnoreSpecial ) # Get all user profiles (both registry and filesystem) using the existing function $allProfiles = Get-AllUserProfiles -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial # Filter the profiles to return only orphaned ones $orphanedProfiles = $allProfiles | Where-Object { $_.IsOrphaned -eq $true } # Return the orphaned profiles return $orphanedProfiles } #EndRegion './Public/Get-OrphanedProfiles.ps1' 22 #Region './Public/Get-ProfilePathFromSID.ps1' -1 function Get-ProfilePathFromSID { param ( [Microsoft.Win32.RegistryKey]$SidKey ) try { # Use Get-RegistryValue to retrieve the "ProfileImagePath" $profileImagePath = Get-RegistryValue -Key $SidKey -ValueName "ProfileImagePath" if (-not $profileImagePath) { Write-Verbose "ProfileImagePath not found for SID '$($SidKey.Name)'." } return $profileImagePath } catch { Write-Error "Failed to retrieve ProfileImagePath for SID '$($SidKey.Name)'. Error: $_" return $null } } #EndRegion './Public/Get-ProfilePathFromSID.ps1' 20 #Region './Public/Get-RegistryKeyForSID.ps1' -1 function Get-RegistryKeyForSID { param ( [string]$SID, [Microsoft.Win32.RegistryKey]$ProfileListKey ) try { # Use the general Open-RegistrySubKey function to get the subkey for the SID $sidKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID if ($sidKey -eq $null) { Write-Warning "The SID '$SID' does not exist in the ProfileList registry." return $null } return $sidKey } catch { Write-Error "Error accessing registry key for SID '$SID'. Error: $_" return $null } } #EndRegion './Public/Get-RegistryKeyForSID.ps1' 20 #Region './Public/Get-SIDProfileInfo.ps1' -1 function Get-SIDProfileInfo { [CmdletBinding()] param ( [string]$ComputerName = $env:COMPUTERNAME ) $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName if ($ProfileListKey -eq $null) { Write-Error "Failed to open registry path: $RegistryPath on $ComputerName." return } $ProfileRegistryItems = foreach ($sid in $ProfileListKey.GetSubKeyNames()) { # Use Open-RegistrySubKey to get the subkey for the SID $subKey = Open-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $sid if ($subKey -eq $null) { Write-Warning "Registry key for SID '$sid' could not be opened." continue } # Use Get-ProfilePathFromSID to get the ProfileImagePath for the SID $profilePath = Get-ProfilePathFromSID -SidKey $subKey # Return a PSCustomObject with SID, ProfilePath, and ComputerName [PSCustomObject]@{ SID = $sid ProfilePath = $profilePath ComputerName = $ComputerName ExistsInRegistry = $true } } return $ProfileRegistryItems } #EndRegion './Public/Get-SIDProfileInfo.ps1' 38 #Region './Public/Get-UserFolders.ps1' -1 function Get-UserFolders { [CmdletBinding()] param ( [string]$ComputerName, [string]$ProfileFolderPath = "C:\Users" ) $IsLocal = ($ComputerName -eq $env:COMPUTERNAME) $FolderPath = Get-DirectoryPath -BasePath $ProfileFolderPath -ComputerName $ComputerName -IsLocal $IsLocal # Get list of all folders in the user profile directory $ProfileFolders = Get-ChildItem -Path $FolderPath -Directory | ForEach-Object { [PSCustomObject]@{ FolderName = $_.Name ProfilePath = Get-DirectoryPath -basepath $_.FullName -ComputerName $ComputerName -IsLocal $true ComputerName = $ComputerName } } return $ProfileFolders } #EndRegion './Public/Get-UserFolders.ps1' 22 #Region './Public/Get-UserProfilesFromFolders.ps1' -1 function Get-UserProfilesFromFolders { param ( [string]$ComputerName = $env:COMPUTERNAME, [string]$ProfileFolderPath = "C:\Users" ) # Get user folders and return them $UserFolders = Get-UserFolders -ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath return $UserFolders } #EndRegion './Public/Get-UserProfilesFromFolders.ps1' 12 #Region './Public/Get-UserProfilesFromRegistry.ps1' -1 function Get-UserProfilesFromRegistry { param ( [string] $ComputerName = $env:COMPUTERNAME ) # Get registry profiles and return them $RegistryProfiles = Get-SIDProfileInfo -ComputerName $ComputerName return $RegistryProfiles } #EndRegion './Public/Get-UserProfilesFromRegistry.ps1' 11 #Region './Public/New-UserProfileObject.ps1' -1 function New-UserProfileObject { param ( [string]$SID, [string]$ProfilePath, [bool]$IsOrphaned, [string]$OrphanReason, [string]$ComputerName, [bool]$IsSpecial ) return [UserProfile]::new( $SID, $ProfilePath, $IsOrphaned, $OrphanReason, $ComputerName, $IsSpecial ) } #EndRegion './Public/New-UserProfileObject.ps1' 20 #Region './Public/Remove-OrphanedProfiles.ps1' -1 function Remove-OrphanedProfiles { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] [string]$ComputerName, [Parameter(Mandatory = $false)] [string]$ProfileFolderPath = "C:\Users", [switch]$IgnoreSpecial ) # Step 1: Get the list of orphaned profiles $orphanedProfiles = Get-OrphanedProfiles-ComputerName $ComputerName -ProfileFolderPath $ProfileFolderPath -IgnoreSpecial if (-not $orphanedProfiles) { Write-Verbose "No orphaned profiles found on $ComputerName." return } # Step 2: Extract the SIDs of orphaned profiles that exist in the registry $orphanedSIDs = $orphanedProfiles | Where-Object { $_.SID } | Select-Object -ExpandProperty SID if (-not $orphanedSIDs) { Write-Verbose "No orphaned profiles with valid SIDs found for removal on $ComputerName." return } # Step 3: Remove profiles for the collected SIDs $removalResults = Remove-ProfilesForSIDs -SIDs $orphanedSIDs -ComputerName $ComputerName -Confirm:$false # Step 4: Return the results of the removal process return $removalResults } #EndRegion './Public/Remove-OrphanedProfiles.ps1' 35 #Region './Public/Remove-ProfilesForSIDs.ps1' -1 function Remove-ProfilesForSIDs { #Orchestrates the deletion process for multiple SIDs. [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [Parameter(Mandatory = $true)] [string[]]$SIDs, # Accept multiple SIDs as an array [Parameter(Mandatory = $false)] [string]$ComputerName = $env:COMPUTERNAME # Default to local computer ) # Open the ProfileList registry key $RegistryPath = "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $ProfileListKey = Open-RegistryKey -RegistryPath $RegistryPath -ComputerName $ComputerName if ($ProfileListKey -eq $null) { Write-Error "Failed to open ProfileList registry path on $ComputerName." return } $deletionResults = @() # Loop through each SID and process deletion foreach ($sid in $SIDs) { try { # Get profile information for the SID $sidProfileInfo = Get-SIDProfileInfo -SID $sid -ProfileListKey $ProfileListKey if (-not $sidProfileInfo.ExistsInRegistry) { $deletionResults += [ProfileDeletionResult]::new( $sid, $null, $false, $sidProfileInfo.Message, $ComputerName ) continue } # Process the deletion of the profile for the SID $deletionResult = Remove-SIDProfile -SID $sid ` -ProfileListKey $ProfileListKey ` -ComputerName $ComputerName ` -ProfilePath $sidProfileInfo.ProfilePath $deletionResults += $deletionResult } catch { Write-Error "An error occurred while processing SID '$sid'. $_" # Add a deletion result indicating failure due to error $deletionResults += [ProfileDeletionResult]::new( $sid, $null, $false, "Error occurred while processing SID '$sid'. Error: $_", $ComputerName ) } } # Close the registry key when done $ProfileListKey.Close() # Return the array of deletion results return $deletionResults } #EndRegion './Public/Remove-ProfilesForSIDs.ps1' 67 #Region './Public/Remove-RegistryKeyForSID.ps1' -1 function Remove-RegistryKeyForSID { #Deletes a single registry key for a SID. [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')] param ( [Parameter(Mandatory = $true)] [string]$SID, [Parameter(Mandatory = $true)] [Microsoft.Win32.RegistryKey]$ProfileListKey, [Parameter(Mandatory = $true)] [string]$ComputerName = $env:COMPUTERNAME ) try { # Use the general Remove-RegistrySubKey function to delete the SID's subkey return Remove-RegistrySubKey -ParentKey $ProfileListKey -SubKeyName $SID -ComputerName $ComputerName } catch { Write-Error "Failed to remove the profile registry key for SID '$SID' on $ComputerName. Error: $_" return $false } } #EndRegion './Public/Remove-RegistryKeyForSID.ps1' 23 #Region './Public/Remove-SIDProfile.ps1' -1 function Remove-SIDProfile { #Coordinates the registry key deletion and provides a result for a single SID. [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')] param ( [string]$SID, [Microsoft.Win32.RegistryKey]$ProfileListKey, [string]$ComputerName, [string]$ProfilePath ) # Attempt to remove the registry key $deletionSuccess = Remove-RegistryKeyForSID -SID $SID -ProfileListKey $ProfileListKey -ComputerName $ComputerName if ($deletionSuccess) { return [ProfileDeletionResult]::new( $SID, $ProfilePath, $true, "Profile registry key for SID '$SID' successfully deleted.", $ComputerName ) } else { return [ProfileDeletionResult]::new( $SID, $ProfilePath, $false, "Failed to delete the profile registry key for SID '$SID'.", $ComputerName ) } } #EndRegion './Public/Remove-SIDProfile.ps1' 32 #Region './Public/Test-FolderExists.ps1' -1 function Test-FolderExists { param ( [string]$ProfilePath, [string]$ComputerName ) $IsLocal = $ComputerName -eq $env:COMPUTERNAME $pathToCheck = Get-DirectoryPath -BasePath $ProfilePath -ComputerName $ComputerName -IsLocal $IsLocal return Test-Path $pathToCheck } #EndRegion './Public/Test-FolderExists.ps1' 11 #Region './Public/Test-OrphanedProfile.ps1' -1 function Test-OrphanedProfile { param ( [string]$SID, [string]$ProfilePath, [bool]$FolderExists, [bool]$IgnoreSpecial, [bool]$IsSpecial, [string]$ComputerName ) if (-not $ProfilePath) { return New-UserProfileObject $SID "(null)" $true "MissingProfileImagePath" $ComputerName $IsSpecial } elseif (-not $FolderExists) { return New-UserProfileObject $SID $ProfilePath $true "MissingFolder" $ComputerName $IsSpecial } else { return New-UserProfileObject $SID $ProfilePath $false $null $ComputerName $IsSpecial } } #EndRegion './Public/Test-OrphanedProfile.ps1' 21 #Region './Public/Test-SpecialAccount.ps1' -1 function Test-SpecialAccount { param ( [string]$FolderName, [string]$SID, [string]$ProfilePath ) # List of default or special accounts to ignore $IgnoredAccounts = @( "defaultuser0", "DefaultAppPool", "servcm12", "Public", "PBIEgwService", "Default", "All Users", "win2kpro" ) $IgnoredSIDs = @( "S-1-5-18", # Local System "S-1-5-19", # Local Service "S-1-5-20" # Network Service ) $IgnoredPaths = @( "C:\WINDOWS\system32\config\systemprofile", # System profile "C:\WINDOWS\ServiceProfiles\LocalService", # Local service profile "C:\WINDOWS\ServiceProfiles\NetworkService" # Network service profile ) # Check if the account is special based on the folder name, SID, or profile path return ($IgnoredAccounts -contains $FolderName) -or ($IgnoredSIDs -contains $SID) -or ($IgnoredPaths -contains $ProfilePath) } #EndRegion './Public/Test-SpecialAccount.ps1' 27 |