Comparing registry keys in PowerShell involves evaluating differences in subkey structures, property names, and data values between two registry paths or point-in-time snapshots. The Compare-Object cmdlet provides an automated mechanism to detect configuration drift, audit security compliance against a baseline, and verify registry changes after software deployments. Since Get-ItemProperty returns registry values as object properties, the two work naturally together. However, direct comparisons need careful handling to avoid interference from PowerShell's built-in metadata properties.
Comparing Property Names with Compare-Object
Every registry key object retrieved via Get-Item exposes a .Property collection containing the names of all registry values defined within that key. You can pass these collections directly into Compare-Object to identify which value names exist in one key but not another.
$baselineKey = "HKCU:\Software\CorpApp\Baseline"
$targetKey = "HKCU:\Software\CorpApp\Current"
New-Item -Path $baselineKey, $targetKey -Force | Out-Null
Set-ItemProperty -Path $baselineKey -Name "Timeout" -Value 30
Set-ItemProperty -Path $baselineKey -Name "Retries" -Value 3
Set-ItemProperty -Path $baselineKey -Name "LegacyMode" -Value 0
Set-ItemProperty -Path $targetKey -Name "Timeout" -Value 60
Set-ItemProperty -Path $targetKey -Name "Retries" -Value 3
Set-ItemProperty -Path $targetKey -Name "DebugLevel" -Value 2
Compare-Object -ReferenceObject (Get-Item $baselineKey).Property `
-DifferenceObject (Get-Item $targetKey).Propertywhere:
Compare-Objectcompares two collections of objects and outputs the differences.-ReferenceObjectis the baseline or source collection against which comparison is made.-DifferenceObjectis the current or target collection being evaluated.(Get-Item $baselineKey).Propertyis a string array containing the names of all registry values present in the baseline key.(Get-Item $targetKey).Propertyis a string array containing the names of all registry values present in the target key.
./compare-registry-properties.ps1InputObject SideIndicator
----------- -------------
DebugLevel =>
LegacyMode <=Comparing Specific Properties and Values
Comparing two full Get-ItemProperty results directly often yields no useful output. This happens because Compare-Object compares entire objects by default. However, the built-in metadata properties like PSPath and PSParentPath will always differ between two distinct registry paths. As a result, these differences obscure the actual value variations you're trying to identify.
To get meaningful results, compare only the relevant value names, excluding PowerShell's registry metadata properties.
$current = Get-ItemProperty -Path "HKCU:\Software\MyApp" | Select-Object -Property * -ExcludeProperty PS*
$backup = Get-ItemProperty -Path "HKCU:\Software\MyApp_Backup" | Select-Object -Property * -ExcludeProperty PS*
$currentProps = $current.PSObject.Properties.Name
$backupProps = $backup.PSObject.Properties.Name
foreach ($prop in $currentProps) {
if ($current.$prop -ne $backup.$prop) {
[PSCustomObject]@{
Property = $prop
CurrentValue = $current.$prop
BackupValue = $backup.$prop
}
}
}where:
-ExcludeProperty PS*strips out PowerShell's added metadata properties (PSPath,PSParentPath,PSChildName,PSProvider), leaving only actual registry values..PSObject.Properties.Nameretrieves the list of property names dynamically from the object.- The
foreachloop manually compares each matching property and reports only the ones that differ.
./compare-registry-values.ps1Property CurrentValue BackupValue
-------- ------------ -----------
MaxRetries 5 3Comparing Value Data and Types
Comparing .Property collections checks only whether value names match. In administration workflows, you must also compare the underlying data payloads and data types (e.g., verifying whether a DWORD value changed from 0 to 1).
To compare both names and data simultaneously, construct custom objects containing the value name, data payload, and registry type for each entry.
function Get-RegistryValueDetails {
param([string]$KeyPath)
$key = Get-Item -Path $KeyPath
foreach ($valName in $key.Property) {
[PSCustomObject]@{
Name = $valName
Value = $key.GetValue($valName)
Type = $key.GetValueKind($valName)
}
}
}
$refValues = Get-RegistryValueDetails -KeyPath "HKCU:\Software\CorpApp\Baseline"
$diffValues = Get-RegistryValueDetails -KeyPath "HKCU:\Software\CorpApp\Current"
Compare-Object -ReferenceObject $refValues `
-DifferenceObject $diffValues `
-Property Name, Value `
-PassThru | Select-Object -Property Name, Value, SideIndicatorwhere:
Get-RegistryValueDetailsis a helper function extracting names, data payloads, and types from a key.$key.GetValueKind($valName)is a .NET method that retrieves the specific registry type (such asDWord,String, orBinary).-Property Name, ValuedirectsCompare-Objectto evaluate differences based on both the value name and its contents.-PassThrupasses the original custom objects through to the pipeline with the appendedSideIndicatorproperty.
./compare-registry-values-advanced.ps1Name Value SideIndicator
---- ----- -------------
Timeout 60 =>
DebugLevel 2 =>
Timeout 30 <=
LegacyMode 0 <=Comparing Just the Value Names Present
Sometimes the goal isn't comparing values, but checking whether the same set of value names exists in both keys (e.g., detecting a missing setting after an update).
Compare-Object -ReferenceObject $currentProps -DifferenceObject $backupPropsInputObject SideIndicator
----------- -------------
Debug <=where the SideIndicator <= means the property exists only in the reference (current) object and => means it exists only in the difference (backup) object.
Practical Usage: Auditing Configuration Drift
A common admin scenario is verifying that a registry key on a workstation still matches an approved baseline, flagging any unauthorized changes.
param(
[string]$LiveKeyPath,
[string]$BaselineKeyPath
)
$live = Get-ItemProperty -Path $LiveKeyPath | Select-Object -Property * -ExcludeProperty PS*
$baseline = Get-ItemProperty -Path $BaselineKeyPath | Select-Object -Property * -ExcludeProperty PS*
$allProps = ($live.PSObject.Properties.Name + $baseline.PSObject.Properties.Name) | Select-Object -Unique
$drift = foreach ($prop in $allProps) {
$liveValue = $live.$prop
$baselineValue = $baseline.$prop
if ($liveValue -ne $baselineValue) {
[PSCustomObject]@{
Property = $prop
Live = $liveValue
Baseline = $baselineValue
}
}
}
if ($drift) {
Write-Output "Configuration drift detected:"
$drift | Format-Table -AutoSize
} else {
Write-Output "No drift detected. Configuration matches baseline."
}where:
$allPropsa combined, de-duplicated list of value names from both keys, ensuring properties unique to either side are checked too.$driftcollects each mismatched property into a report; remains$nullif no differences are found.
./audit-registry-drift-simple.ps1 -LiveKeyPath "HKCU:\Software\MyApp" -BaselineKeyPath "HKCU:\Software\MyApp_Backup"Configuration drift detected:
Property Live Baseline
-------- ---- --------
MaxRetries 5 3Advanced Drift Detection with Missing/Extra/Modified Classification
In enterprise compliance, servers must adhere to standardized security baselines. The following script audits a production registry key against a reference baseline and exports an audit report detailing missing, modified, and extraneous settings.
param (
[string]$BaselineKey = "HKCU:\Software\CorpApp\Baseline",
[string]$TargetKey = "HKCU:\Software\CorpApp\Current"
)
if (-not (Test-Path $BaselineKey) -or -not (Test-Path $TargetKey)) {
Write-Error "One or both registry paths do not exist. Audit aborted."
return
}
function Get-KeySnapshot ($Path) {
$item = Get-Item -Path $Path
$hash = @{}
foreach ($prop in $item.Property) {
$hash[$prop] = [string]$item.GetValue($prop)
}
return $hash
}
$baseline = Get-KeySnapshot -Path $BaselineKey
$current = Get-KeySnapshot -Path $TargetKey
$auditResults = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($name in $baseline.Keys) {
if (-not $current.ContainsKey($name)) {
$auditResults.Add([PSCustomObject]@{
Setting = $name
Status = "Missing from Current"
Expected= $baseline[$name]
Actual = "N/A"
})
}
elseif ($baseline[$name] -ne $current[$name]) {
$auditResults.Add([PSCustomObject]@{
Setting = $name
Status = "Value Mismatch / Drift"
Expected= $baseline[$name]
Actual = $current[$name]
})
}
}
foreach ($name in $current.Keys) {
if (-not $baseline.ContainsKey($name)) {
$auditResults.Add([PSCustomObject]@{
Setting = $name
Status = "Unauthorized Extra Setting"
Expected= "N/A"
Actual = $current[$name]
})
}
}
$auditResults | Format-Table -AutoSizewhere:
Get-KeySnapshotconverts the values and data of a registry key into a hash table for fast key-based lookups.[System.Collections.Generic.List[PSCustomObject]]::new()instantiates a generic list for accumulating audit records.$current.ContainsKey($name)validates whether a specific baseline property exists in the active registry key.Format-Table -AutoSizeformats the drift report with dynamic column widths.
./audit-registry-drift-advanced.ps1Setting Status Expected Actual
------- ------ -------- ------
LegacyMode Missing from Current 0 N/A
Timeout Value Mismatch / Drift 30 60
DebugLevel Unauthorized Extra Setting N/A 2