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