Public/Get-VBUserShellFolders.ps1

# ============================================================
# FUNCTION : Get-VBUserShellFolders
# MODULE : VB.WorkstationReport
# VERSION : 1.0.1
# CHANGED : 16-04-2026 -- Initial release
# AUTHOR : Vibhu Bhatnagar
# PURPOSE : Retrieves Shell Folders and User Shell Folders registry values for user profiles
# ENCODING : UTF-8 with BOM
# ============================================================
function Get-VBUserShellFolders {
    <#
    .SYNOPSIS
        Retrieves Shell Folders and User Shell Folders registry values for user profiles.
 
    .DESCRIPTION
        Get-VBUserShellFolders enumerates loaded user profiles and queries the registry
        for Shell Folders and User Shell Folders values. These registry keys define
        the paths to standard Windows folders (Desktop, Documents, Downloads, Favorites,
        Music, Pictures, Videos, etc).
 
        Shell Folders contains expanded paths (all environment variables resolved).
        User Shell Folders contains unexpanded paths (may contain %USERPROFILE%, etc).
 
        Can filter by UserName or SID. If neither is provided, returns data for all
        currently loaded profiles. Only scans profiles whose hives are loaded in
        HKEY_USERS (i.e., logged-in users). Use Mount-VBUserHive to scan offline
        profiles.
 
    .PARAMETER ComputerName
        Computer names to query. Accepts pipeline input. Defaults to the local computer.
 
    .PARAMETER UserName
        Username(s) to query. Optional filter. Accepts pipeline input.
        If not provided, all loaded profiles are returned.
 
    .PARAMETER SID
        SID(s) to query. Optional filter.
        If not provided, all loaded profiles are returned.
 
    .PARAMETER Credential
        Credentials for remote computer access. Not required for local execution.
 
    .EXAMPLE
        Get-VBUserShellFolders
        Returns all shell folder registry values for all loaded user profiles on the
        local computer.
 
    .EXAMPLE
        Get-VBUserShellFolders -UserName 'vibhu.bhatnagar' -ComputerName 'WS001'
        Returns shell folder values for a specific user on a remote workstation.
 
    .EXAMPLE
        Get-VBUserShellFolders -SID 'S-1-5-21-3652201067-2579442110-4045031335-1001' |
            Select-Object UserName, ValueName, ValueData
        Returns Desktop, Documents, Downloads paths and others for a specific user SID.
 
    .EXAMPLE
        Get-VBUserShellFolders -UserName 'admin' | Where-Object ValueName -eq 'Desktop'
        Returns the Desktop folder path for a user named 'admin'.
 
    .OUTPUTS
        PSCustomObject
        Returns one object per registry value with:
          - ComputerName : Target computer
          - UserSID : User SID
          - UserName : Username (profile folder name)
          - FolderType : 'Shell Folders' (expanded) or 'User Shell Folders' (unexpanded)
          - ValueName : Registry value name (e.g. 'Desktop', 'Documents', 'Favorites')
          - ValueData : Registry value data (folder path)
          - CollectionTime: Timestamp of data collection
          - Status : 'Success' or 'Failed'
          - Error : Error message (only present on failure)
 
    .NOTES
        Version : 1.0.1
        Author : Vibhu Bhatnagar
        Category : User Profile Management
        Requirements :
          - PowerShell 5.1 or higher
          - User profile hives must be loaded in HKEY_USERS (users must be logged in)
          - PowerShell Remoting enabled for remote targets
 
        Known Limitations :
          - Only scans currently loaded user profiles (logged-in users)
          - To scan unloaded profiles, use Mount-VBUserHive first
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('Name', 'Server', 'Host')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('User', 'Identity', 'SamAccountName')]
        [string[]]$UserName,

        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('SecurityIdentifier')]
        [string[]]$SID,

        [PSCredential]$Credential
    )

    begin {
        # Shared scriptblock for both local and remote execution.
        # Avoids code duplication and ensures consistent behavior.
        $scriptBlock = {
            param($FilterUserName, $FilterSID)

            $results        = [System.Collections.Generic.List[object]]::new()
            $collectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')

            try {
                # Get all currently loaded user profiles
                $loadedProfiles = Get-CimInstance -ClassName Win32_UserProfile -ErrorAction Stop |
                                  Where-Object { $_.Loaded -eq $true }

                # Filter by UserName and/or SID if provided
                if ($FilterUserName -or $FilterSID) {
                    $loadedProfiles = $loadedProfiles | Where-Object {
                        $profileUserName = Split-Path $_.LocalPath -Leaf
                        $profileSID      = $_.SID

                        ($FilterUserName -and $profileUserName -in $FilterUserName) -or
                        ($FilterSID -and $profileSID -in $FilterSID)
                    }
                }

                # Scan each profile's Shell Folders registry values
                foreach ($profile in $loadedProfiles) {
                    $sid            = $profile.SID
                    $userName       = Split-Path $profile.LocalPath -Leaf
                    $shellPath      = "Registry::HKEY_USERS\$sid\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
                    $userShellPath  = "Registry::HKEY_USERS\$sid\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders"

                    # Get Shell Folders (expanded paths -- env vars resolved)
                    if (Test-Path $shellPath -ErrorAction SilentlyContinue) {
                        $shellFolders = Get-ItemProperty -Path $shellPath -ErrorAction SilentlyContinue
                        if ($shellFolders) {
                            $shellFolders.PSObject.Properties |
                                Where-Object { $_.Name -notlike 'PS*' } |
                                ForEach-Object {
                                    $results.Add([PSCustomObject]@{
                                        UserSID       = $sid
                                        UserName      = $userName
                                        FolderType    = 'Shell Folders'
                                        ValueName     = $_.Name
                                        ValueData     = $_.Value
                                        CollectionTime = $collectionTime
                                        Status        = 'Success'
                                    })
                                }
                        }
                    }

                    # Get User Shell Folders (unexpanded paths -- may contain env variables)
                    if (Test-Path $userShellPath -ErrorAction SilentlyContinue) {
                        $userShellFolders = Get-ItemProperty -Path $userShellPath -ErrorAction SilentlyContinue
                        if ($userShellFolders) {
                            $userShellFolders.PSObject.Properties |
                                Where-Object { $_.Name -notlike 'PS*' } |
                                ForEach-Object {
                                    $results.Add([PSCustomObject]@{
                                        UserSID       = $sid
                                        UserName      = $userName
                                        FolderType    = 'User Shell Folders'
                                        ValueName     = $_.Name
                                        ValueData     = $_.Value
                                        CollectionTime = $collectionTime
                                        Status        = 'Success'
                                    })
                                }
                        }
                    }
                }

                return $results
            }
            catch {
                [PSCustomObject]@{
                    Error          = $_.Exception.Message
                    CollectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                    Status         = 'Failed'
                }
            }
        }
    }

    process {
        foreach ($computer in $ComputerName) {
            try {
                if ($computer -eq $env:COMPUTERNAME) {
                    # Local execution
                    $results = & $scriptBlock $UserName $SID
                    foreach ($result in $results) {
                        $result | Add-Member -NotePropertyName 'ComputerName' -NotePropertyValue $computer -Force
                        $result
                    }
                }
                else {
                    # Remote execution
                    $invokeParams = @{
                        ComputerName = $computer
                        ScriptBlock  = $scriptBlock
                        ArgumentList = $UserName, $SID
                        ErrorAction  = 'Stop'
                    }
                    if ($Credential) { $invokeParams['Credential'] = $Credential }

                    $results = Invoke-Command @invokeParams
                    foreach ($result in $results) {
                        $result | Add-Member -NotePropertyName 'ComputerName' -NotePropertyValue $computer -Force
                        $result
                    }
                }
            }
            catch {
                [PSCustomObject]@{
                    ComputerName   = $computer
                    Error          = $_.Exception.Message
                    CollectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
                    Status         = 'Failed'
                }
            }
        }
    }
}