Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Archives
February 6, 2026

More Registry Roaming

In this issue:

  • Exploring Other Registry Hives
    • Resolving SIDs
    • Querying User Registry Keys
    • Get-RegistryKeyInfo
  • What About Remoting?
    • Get-PwshRegistryInfo
  • Summary

Let's continue exploring how to use the [Microsoft.Win32.RegistryKey] class with PowerShell to work with the Windows registry. The default PSDrives for the registry are extremely useful, but only provide access to the HKEY_CURRENT_USER and HKEY_LOCAL_MACHINE hives. In this article, we'll look at how to access other registry hives and discuss considerations for remoting scenarios. There are a few other hives you might want to work with such as HKEY_USERS. This is where the .NET classes become particularly useful.

Exploring Other Registry Hives

Using regedit.exe, you can see the HKEY_USERS hive contains subkeys for each user profile on the system. Each subkey is named with the user's Security Identifier (SID).

HKEY_USERS
figure 1

To access these keys programmatically, you can open the hive:

$users = [Microsoft.Win32.RegistryKey]::OpenBaseKey('Users', 'Default')

The subkey names are what you see using regedit.exe.

PS C:\> $users.GetSubKeyNames()
.DEFAULT
S-1-5-19
S-1-5-20
S-1-5-21-3465062479-264850141-705915528-1001
S-1-5-21-3465062479-264850141-705915528-1001_Classes
S-1-5-18

I probably don't need .DEFAULT or the Classes keys so I'll filter those out.

$sids = $users.GetSubKeyNames().Where({ $_ -notMatch 'Default|Classes' })

Resolving SIDs

I know that short SIDs belong to system accounts. I can convert the SID to an account name using the [System.Security.Principal.SecurityIdentifier] class.

$sids | ForEach-Object {
    [PSCustomObject]@{
        RegistryPath = $users.Name
        Name         = $_
        Account      = [System.Security.Principal.SecurityIdentifier]::new($_).Translate('System.Security.Principal.NTAccount').value
    }
}

This gives me more meaningful output.

RegistryPath Name                                         Account
------------ ----                                         -------
HKEY_USERS   S-1-5-19                                     NT AUTHORITY\LOCAL SERVICE
HKEY_USERS   S-1-5-20                                     NT AUTHORITY\NETWORK SERVICE
HKEY_USERS   S-1-5-21-3465062479-264850141-705915528-1001 PROSPERO\Jeff
HKEY_USERS   S-1-5-18                                     NT AUTHORITY\SYSTEM

I could further filter from here. However, it might be better to filter out the SIDs I don't want earlier in the process. I can use a regular expression pattern to only match on a user SID.

$sids = $users.GetSubKeyNames().Where({ $_ -match '^S([\-\d+]){8,}$' })
$sids | ForEach-Object {
    [PSCustomObject]@{
        RegistryPath = $users.Name
        Name         = $_
        Account      = [System.Security.Principal.SecurityIdentifier]::new($_).Translate('System.Security.Principal.NTAccount').value
        ComputerName = $env:COMPUTERNAME
    }
}

This limits the results to only user SIDs.

RegistryPath Name                                         Account       ComputerName
------------ ----                                         -------       ------------
HKEY_USERS   S-1-5-21-3465062479-264850141-705915528-1001 PROSPERO\Jeff PROSPERO

Since I only have one user account, let me save the results to a variable.

$user = $sids | ForEach-Object {
    [PSCustomObject]@{
        RegistryPath = $users.Name
        Name         = $_
        Account      = [System.Security.Principal.SecurityIdentifier]::new($_).Translate('System.Security.Principal.NTAccount').value
        ComputerName = $env:COMPUTERNAME
    }
}

Querying User Registry Keys

I can use this object to build a path to a specific registry key that I can open.

$qPath = Join-Path $user.Name -ChildPath 'Software\Microsoft\Windows\CurrentVersion\Uninstall'
$uninstall = $users.OpenSubKey($qPath)

Using my Get-RegistryKeyData function from last time, I can query uninstall information.

PS C:\> Get-RegistryKeyData -RegistryKey $uninstall -KeyName $uninstall.GetSubKeyNames()[0]

RegistryPath : HKEY_USERS\S-1-5-21-3465062479-264850141-705915528-1001\Software\Microsoft\Windows\CurrentV
               ersion\Uninstall\AgileBits.1Password.CLI_Microsoft.Winget.Source_8wekyb3d8bbwe
Computername : PROSPERO
KeyName      : DisplayName
KeyType      : String
KeyValue     : 1Password CLI

RegistryPath : HKEY_USERS\S-1-5-21-3465062479-264850141-705915528-1001\Software\Microsoft\Windows\CurrentV
               ersion\Uninstall\AgileBits.1Password.CLI_Microsoft.Winget.Source_8wekyb3d8bbwe
Computername : PROSPERO
KeyName      : DisplayVersion
KeyType      : String
KeyValue     : 2.31.1
...

This gives me an object for each key. It might be easier to join objects from the same path into a single object. See if you can visualize what this code is doinbg:

```powershell
$r = $uninstall.GetSubKeyNames() |
Get-RegistryKeyData -RegistryKey $uninstall |
Group-Object RegistryPath -PipelineVariable pv |
ForEach-Object {
    Write-Host "Processing $(Split-Path $pv.Group[0].RegistryPath -Leaf)" -ForegroundColor Yellow
    $_.Group | Sort-Object KeyName |
    ForEach-Object -Begin {
        $hash = [ordered]@{
            Product      = Split-Path $pv.Group[0].RegistryPath -Leaf
            User         = $user.Account
            ComputerName = $env:COMPUTERNAME
        }
    } -Process {
       $hash.Add($_.KeyName, $_.KeyValue)
    } -End {
        #write the hashtable to the pipeline
        $hash
    }
}

I am processing all subkeys and grouping the registry key data by the registry path. For each group, I create a hashtable with some standard properties and then add each key name and value to the hashtable. Finally, I output the hashtable as an object.

PS C:\> $r.count
18
PS C:\> $r[3]

Name                           Value
----                           -----
Product                        {889610CC-4337-4BDB-AC3B-4F21806C0BDE}_is1
User                           PROSPERO\Jeff
ComputerName                   PROSPERO
DisplayIcon                    C:\Users\Jeff\AppData\Local\Programs\UniGetUI\Un…
DisplayName                    UniGetUI
DisplayVersion                 3.3.7
EstimatedSize                  244368
HelpLink                       https://github.com/marticliment/UniGetUI
Inno Setup: App Path           C:\Users\Jeff\AppData\Local\Programs\UniGetUI
Inno Setup: Deselected Tasks   portableinstall
Inno Setup: Icon Group         (Default)
Inno Setup: Language           English
Inno Setup: Selected Tasks     regularinstall,regularinstall\startmenuicon,regu…
Inno Setup: Setup Version      6.7.0
Inno Setup: User               Jeff
InstallDate                    20260204
InstallLocation                C:\Users\Jeff\AppData\Local\Programs\UniGetUI\
MajorVersion                   3
MinorVersion                   3
ModifyPath                     "C:\Users\Jeff\AppData\Local\Programs\UniGetUI\U…
NoRepair                       1
Publisher                      Martí Climent
QuietUninstallString           "C:\Users\Jeff\AppData\Local\Programs\UniGetUI\u…
UninstallString                "C:\Users\Jeff\AppData\Local\Programs\UniGetUI\u…
URLInfoAbout                   https://www.marticliment.com/unigetui/
URLUpdateInfo                  https://github.com/marticliment/UniGetUI
VersionMajor                   3
VersionMinor                   3

If you noticed, I didn't specify a typename for my custom output. That's not really possible since each uninstall entry will have different properties.

Want to read the full issue?
GitHub
Bluesky
LinkedIn
Mastodon
jdhitsolutions.github.io
Powered by Buttondown, the easiest way to start and grow your newsletter.