Public/Get-VBUserFolderRedirections.ps1
|
# ============================================================ # FUNCTION : Get-VBUserFolderRedirections # MODULE : WorkstationReport # VERSION : 1.3.0 # CHANGED : 14-04-2026 -- Standards compliance fixes # AUTHOR : Vibhu Bhatnagar # PURPOSE : Audits folder redirections for all user profiles on local or remote computers # ENCODING : UTF-8 with BOM # ============================================================ function Get-VBUserFolderRedirections { <# .SYNOPSIS Audits folder redirections for all user profiles on local or remote computers. .DESCRIPTION Get-VBUserFolderRedirections scans Windows systems to identify folder redirections for all user profiles. It detects OneDrive, network-based, and manual redirections by examining Shell Folders and User Shell Folders registry keys. System accounts (SYSTEM, LOCAL SERVICE, NETWORK SERVICE) are excluded -- only domain/local user accounts are audited (SIDs matching S-1-5-21-*). Registry hives for inactive profiles are loaded using reg.exe and safely unloaded after inspection. The scriptblock pattern is used so the same logic runs locally or remotely without duplication. Helper functions (Test-IsRedirected, Get-RedirectionType, Get-ProfileLastUpdate) are defined inside the scriptblock so they are serialised correctly when sent via Invoke-Command to remote targets. .PARAMETER ComputerName Computer names to audit. Accepts pipeline input. Defaults to the local computer. .PARAMETER Credential Credentials for remote computer access. Not required for local execution. .PARAMETER TableOutput When specified, suppresses Write-Host console output and returns only structured objects. .EXAMPLE Get-VBUserFolderRedirections Audits folder redirections for all users on the local computer with console output. .EXAMPLE Get-VBUserFolderRedirections -ComputerName 'WS001','WS002' -Credential (Get-Credential) Audits folder redirections on two remote workstations. .EXAMPLE Get-VBUserFolderRedirections -TableOutput | Where-Object { $_.RedirectionCount -gt 0 } | Format-Table Returns only users with active redirections in table format. .EXAMPLE 'WS001','WS002' | Get-VBUserFolderRedirections -TableOutput | Export-Csv 'FolderRedirections.csv' -NoTypeInformation Processes multiple computers via pipeline and exports to CSV. .EXAMPLE Get-VBUserFolderRedirections -TableOutput | Where-Object { $_.OneDriveRedirections -ne 'None' } | Select-Object ComputerName, Username, OneDriveRedirections Identifies all users with OneDrive folder redirections. .OUTPUTS PSCustomObject Returns objects with: - ComputerName : Target computer - Username : User profile name - RedirectedFolders : Semicolon-separated folder=path summary for all redirected folders - RedirectionCount : Number of redirected folders - RedirectionTypes : Detected type(s) -- 'OneDrive', 'Network', 'Manual', or 'Mixed (...)' - Desktop, Documents, Pictures, Downloads, Music, Videos, Favorites, AppDataRoaming, SavedGames, Searches, StartMenu, Contacts, Links : Individual folder path [type] or 'Local' - OneDriveRedirections : Semicolon-separated list of OneDrive-redirected folders - NetworkRedirections : Semicolon-separated list of network-redirected folders - ManualRedirections : Semicolon-separated list of manually-redirected folders - LastProfileUpdate : Last write time of the user profile directory - Status : 'Success', 'Failed', 'Profile Missing', 'Registry Missing', 'SID Not Found', 'No Profiles', or 'Registry Not Accessible' - Error : Error message (only present on failure) .NOTES Version : 1.3.0 Author : Vibhu Bhatnagar Category: User Profile Management Requirements: - PowerShell 5.1 or later - Administrative privileges (required to load user registry hives) - PowerShell Remoting enabled for remote targets - Registry access permissions for HKEY_USERS and HKEY_LOCAL_MACHINE #> [CmdletBinding()] param( [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('Name', 'Server')] [string[]]$ComputerName = $env:COMPUTERNAME, [PSCredential]$Credential, [switch]$TableOutput ) process { # Core logic -- defined once, runs locally or remotely $scriptBlock = { param([bool]$ShowConsole) $computerName = $env:COMPUTERNAME $collectionTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss') $results = [System.Collections.Generic.List[object]]::new() # -- Helper: determine if a path is genuinely redirected ------------------- function Test-IsRedirected { param([string]$Path, [string]$UserProfilePath) if (-not $Path) { return $false } $normalPath = $Path.TrimEnd('\') $normalProfile = $UserProfilePath.TrimEnd('\') if ($normalPath -like "$normalProfile*") { return $false } if ($normalPath -like '\\*') { return $true } if ($normalPath -like '*OneDrive*') { return $true } if ($normalPath -notlike '*\Users\*') { return $true } return $false } # -- Helper: classify redirection type ------------------------------------- function Get-RedirectionType { param([string]$Path) if (-not $Path) { return 'Local' } if ($Path -like 'C:\WINDOWS\system32\config\systemprofile*') { return 'System Profile' } if ($Path -like '*OneDrive*') { return 'OneDrive' } if ($Path -like '\\*') { return 'Network' } if ($Path -like 'C:\Users\*') { return 'Local' } return 'Manual' } # -- Helper: safe last-write timestamp ------------------------------------ function Get-ProfileLastUpdate { param([string]$FilePath) try { return (Get-Item -Path $FilePath -ErrorAction Stop).LastWriteTime.ToString( 'dd-MM-yyyy HH:mm:ss', [System.Globalization.CultureInfo]::InvariantCulture) } catch [System.IO.FileNotFoundException] { return 'File Not Found' } catch [System.UnauthorizedAccessException] { return 'Access Denied' } catch { return 'Error' } } # -- Helper: build a standard 'no data' result object --------------------- function New-ProfileErrorResult { param([string]$Computer, [string]$User, [string]$Message, [string]$StatusValue, [string]$ProfilePath, [string]$CollectionTime) $ts = if ($ProfilePath) { Get-ProfileLastUpdate -FilePath $ProfilePath } else { 'N/A' } [PSCustomObject]@{ ComputerName = $Computer Username = $User RedirectedFolders = $Message RedirectionCount = 0 RedirectionTypes = $Message Desktop = $Message Documents = $Message Pictures = $Message Downloads = $Message Music = $Message Videos = $Message Favorites = $Message AppDataRoaming = $Message SavedGames = $Message Searches = $Message StartMenu = $Message Contacts = $Message Links = $Message OneDriveRedirections = $Message NetworkRedirections = $Message ManualRedirections = $Message LastProfileUpdate = $ts CollectionTime = $CollectionTime Status = $StatusValue } } # -- Folder key-to-display-name mapping ------------------------------------ $folderMappings = [ordered]@{ 'AppData' = 'AppData(Roaming)' 'Desktop' = 'Desktop' 'Personal' = 'Documents' 'My Pictures' = 'Pictures' 'My Music' = 'Music' 'My Video' = 'Videos' 'Favorites' = 'Favorites' '{374de290-123f-4565-9164-39c4925e467b}' = 'Downloads' '{4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4}' = 'Saved Games' '{7d1d3a04-debb-4115-95cf-2f29da2920da}' = 'Searches' 'Start Menu' = 'Start Menu' 'Contacts' = 'Contacts' 'Links' = 'Links' } if ($ShowConsole) { Write-Host "`nStarting folder redirection audit on: $computerName" -ForegroundColor Green Write-Host ('=' * 50) -ForegroundColor Green } # -- Load user profiles (domain/local accounts only) ----------------------- $userProfiles = [System.Collections.Generic.List[string]]::new() try { Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop | ForEach-Object { $sid = $_.PSChildName if ($sid -match '^S-1-5-21-' -and $sid -notin @('S-1-5-18', 'S-1-5-19', 'S-1-5-20')) { $path = $_.GetValue('ProfileImagePath') if ($path -and (Test-Path $path -ErrorAction SilentlyContinue)) { $userProfiles.Add($path) } } } } catch { if ($ShowConsole) { Write-Host "Error reading profile list: $_" -ForegroundColor Red } $results.Add([PSCustomObject]@{ ComputerName = $computerName Username = 'System Error' Error = "Cannot access profile registry: $($_.Exception.Message)" CollectionTime = $collectionTime Status = 'Failed' }) return $results } if ($userProfiles.Count -eq 0) { if ($ShowConsole) { Write-Host 'No user profiles found.' -ForegroundColor Yellow } $results.Add((New-ProfileErrorResult -Computer $computerName -User 'No Profiles' ` -Message 'No user profiles found' -StatusValue 'No Profiles' -ProfilePath $null -CollectionTime $collectionTime)) return $results } # -- Process each profile -------------------------------------------------- foreach ($profilePath in $userProfiles) { $username = Split-Path $profilePath -Leaf $hiveLoaded = $false if ($ShowConsole) { Write-Host "`nChecking: $username" -ForegroundColor Cyan Write-Host ('-' * 46) -ForegroundColor Gray } # Resolve SID $userSID = $null try { Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ErrorAction Stop | ForEach-Object { if ($_.GetValue('ProfileImagePath') -eq $profilePath) { $userSID = $_.PSChildName } } } catch { if ($ShowConsole) { Write-Host " Error resolving SID for $username : $_" -ForegroundColor Red } } if (-not $userSID) { if ($ShowConsole) { Write-Host " SID not found for: $username" -ForegroundColor Yellow } $results.Add((New-ProfileErrorResult -Computer $computerName -User $username ` -Message 'SID Not Found' -StatusValue 'SID Not Found' -ProfilePath $profilePath -CollectionTime $collectionTime)) continue } try { # Profile directory must exist if (-not (Test-Path $profilePath)) { if ($ShowConsole) { Write-Host " Profile directory missing: $profilePath" -ForegroundColor Yellow } $results.Add((New-ProfileErrorResult -Computer $computerName -User $username ` -Message 'Profile Not Found' -StatusValue 'Profile Missing' -ProfilePath $profilePath -CollectionTime $collectionTime)) continue } # Load hive if not already mounted if (-not (Test-Path "Registry::HKEY_USERS\$userSID")) { $ntUserPath = Join-Path $profilePath 'NTUSER.DAT' if (Test-Path $ntUserPath) { $loadResult = Start-Process -FilePath 'reg.exe' ` -ArgumentList 'load', "HKU\$userSID", $ntUserPath ` -Wait -PassThru -WindowStyle Hidden if ($loadResult.ExitCode -eq 0) { $hiveLoaded = $true Start-Sleep -Milliseconds 500 } else { if ($ShowConsole) { Write-Host " Failed to load hive for: $username" -ForegroundColor Yellow } } } else { if ($ShowConsole) { Write-Host " NTUSER.DAT missing for: $username" -ForegroundColor Yellow } $results.Add((New-ProfileErrorResult -Computer $computerName -User $username ` -Message 'NTUSER.DAT Missing' -StatusValue 'Registry Missing' -ProfilePath $profilePath -CollectionTime $collectionTime)) continue } } # Verify the hive is accessible after load attempt if (-not (Test-Path "Registry::HKEY_USERS\$userSID")) { if ($ShowConsole) { Write-Host " Registry hive not accessible for: $username" -ForegroundColor Yellow } $results.Add((New-ProfileErrorResult -Computer $computerName -User $username ` -Message 'Registry Not Accessible' -StatusValue 'Registry Not Accessible' -ProfilePath $profilePath -CollectionTime $collectionTime)) continue } # Read Shell Folders registry keys $sfPath = "Registry::HKEY_USERS\$userSID\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" $usfPath = "Registry::HKEY_USERS\$userSID\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders" $sf = if (Test-Path $sfPath) { Get-ItemProperty $sfPath -ErrorAction SilentlyContinue } else { $null } $usf = if (Test-Path $usfPath) { Get-ItemProperty $usfPath -ErrorAction SilentlyContinue } else { $null } $redirectedFolders = @{} $redirectionTypes = @{} $oneDriveRedirects = [System.Collections.Generic.List[string]]::new() $networkRedirects = [System.Collections.Generic.List[string]]::new() $manualRedirects = [System.Collections.Generic.List[string]]::new() foreach ($regKey in $folderMappings.Keys) { $folderName = $folderMappings[$regKey] $redirectPath = $null if ($sf -and ($sf | Get-Member -Name $regKey -MemberType Properties -ErrorAction SilentlyContinue)) { $redirectPath = $sf.$regKey } elseif ($usf -and ($usf | Get-Member -Name $regKey -MemberType Properties -ErrorAction SilentlyContinue)) { $raw = $usf.$regKey if ($raw -and $raw -like '*%*') { $expanded = $raw -replace '%USERPROFILE%', $profilePath ` -replace '%USERNAME%', $username $redirectPath = [Environment]::ExpandEnvironmentVariables($expanded) } else { $redirectPath = $raw } } if ($redirectPath -and (Test-IsRedirected -Path $redirectPath -UserProfilePath $profilePath)) { $rType = Get-RedirectionType -Path $redirectPath $redirectedFolders[$folderName] = $redirectPath $redirectionTypes[$folderName] = $rType switch ($rType) { 'OneDrive' { $oneDriveRedirects.Add("$folderName=$redirectPath") } 'Network' { $networkRedirects.Add("$folderName=$redirectPath") } 'Manual' { $manualRedirects.Add("$folderName=$redirectPath") } } if ($ShowConsole) { $color = switch ($rType) { 'OneDrive' { 'Cyan' } 'Network' { 'Yellow' } 'Manual' { 'Magenta' } default { 'White' } } Write-Host " $folderName -> $redirectPath [$rType]" -ForegroundColor $color } } } if ($ShowConsole) { if ($redirectedFolders.Count -eq 0) { Write-Host ' No folder redirections found.' -ForegroundColor Gray } else { Write-Host " Total redirected: $($redirectedFolders.Count)" -ForegroundColor Green } } # Build redirection type summary $allTypes = @($redirectionTypes.Values | Sort-Object -Unique) $typeSummary = if ($allTypes.Count -gt 1) { 'Mixed (' + ($allTypes -join ', ') + ')' } elseif ($allTypes.Count -eq 1) { $allTypes[0] } else { 'None' } # Helper to build per-folder field value $fv = { param($name) if ($redirectedFolders[$name]) { "$($redirectedFolders[$name]) [$($redirectionTypes[$name])]" } else { 'Local' } } $results.Add([PSCustomObject]@{ ComputerName = $computerName Username = $username RedirectedFolders = if ($redirectedFolders.Count -gt 0) { ($redirectedFolders.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '; ' } else { 'None' } RedirectionCount = $redirectedFolders.Count RedirectionTypes = $typeSummary Desktop = & $fv 'Desktop' Documents = & $fv 'Documents' Pictures = & $fv 'Pictures' Downloads = & $fv 'Downloads' Music = & $fv 'Music' Videos = & $fv 'Videos' Favorites = & $fv 'Favorites' AppDataRoaming = & $fv 'AppData(Roaming)' SavedGames = & $fv 'Saved Games' Searches = & $fv 'Searches' StartMenu = & $fv 'Start Menu' Contacts = & $fv 'Contacts' Links = & $fv 'Links' OneDriveRedirections = if ($oneDriveRedirects.Count) { $oneDriveRedirects -join '; ' } else { 'None' } NetworkRedirections = if ($networkRedirects.Count) { $networkRedirects -join '; ' } else { 'None' } ManualRedirections = if ($manualRedirects.Count) { $manualRedirects -join '; ' } else { 'None' } LastProfileUpdate = Get-ProfileLastUpdate -FilePath $profilePath CollectionTime = $collectionTime Status = 'Success' }) } catch { if ($ShowConsole) { Write-Host " Error processing $username : $_" -ForegroundColor Red } $results.Add([PSCustomObject]@{ ComputerName = $computerName Username = $username Error = $_.Exception.Message LastProfileUpdate = Get-ProfileLastUpdate -FilePath $profilePath CollectionTime = $collectionTime Status = 'Failed' }) } finally { if ($hiveLoaded) { [System.GC]::Collect() Start-Sleep -Milliseconds 500 try { $unload = Start-Process -FilePath 'reg.exe' ` -ArgumentList 'unload', "HKU\$userSID" -Wait -PassThru -WindowStyle Hidden if ($unload.ExitCode -ne 0 -and $ShowConsole) { Write-Host " Warning: Could not unload registry hive for $username" -ForegroundColor Yellow } } catch { if ($ShowConsole) { Write-Host " Warning: Registry cleanup failed for $username" -ForegroundColor Yellow } } } } } return $results } foreach ($computer in $ComputerName) { try { Write-Verbose "Auditing folder redirections on: $computer" $showConsole = -not $TableOutput.IsPresent if ($computer -eq $env:COMPUTERNAME) { & $scriptBlock $showConsole } else { $invokeParams = @{ ComputerName = $computer ScriptBlock = $scriptBlock ArgumentList = $showConsole ErrorAction = 'Stop' } if ($Credential) { $invokeParams['Credential'] = $Credential } Invoke-Command @invokeParams } } catch { Write-Error -Message "Failed to connect to '$computer': $($_.Exception.Message)" -ErrorAction Continue [PSCustomObject]@{ ComputerName = $computer Username = 'Connection Error' Error = $_.Exception.Message Status = 'Failed' } } } } } |