Protect unreadable registry values in the live backup/restore path - #29
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis PR fixes a data-loss bug in the live Windows backup/restore path: when a registry value was unreadable (e.g. access denied),
Confidence Score: 4/5The fix correctly targets the live backup path and the restore guard is safe for both new and old backups. No registry values are deleted or overwritten by the new code paths. The core logic is sound: the try/catch wraps the right operations, the readError flag propagates cleanly through the snapshot, and the back-compat PSObject.Properties guard means old backups restore without change. Two minor gaps exist: non-registry Get-PolicyValue branches omit ReadError from their return objects (relying on implicit null-to-false coercion in Get-PolicySnapshot), and Assert-BackupPolicyList does not enforce the invariant that readError=true entries must have existed=false, leaving a silent-skip risk for hand-crafted backups. Both changed files are worth a second look: src/PlatformPolicy.ps1 for the implicit null-coercion in the non-registry snapshot path, and src/Backup.ps1 for the missing readError/existed consistency check in the schema validator. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["New-Backup()"] --> B["Get-PolicySnapshot(Target, PolicyNames)"]
B --> C["For each policyName: Get-PolicyValue(Target, Name)"]
C --> D{Target.Kind == Registry?}
D -- Yes --> E{Test-Path Registry Key}
E -- Exists --> F["try: Get-Item + GetValue"]
F -- value != null --> G["Return {Exists=true, ReadError=false}"]
F -- value == null --> H["Return {Exists=false, ReadError=false}"]
F -- catch --> I["Write-Warning\nReturn {Exists=false, ReadError=true}"]
E -- Missing --> H
D -- No: JsonFile/macOS --> J["Return {Exists=..., ReadError=implicit null}"]
G & H & I & J --> K["Snapshot entry: existed, value, kind, readError"]
K --> L["Save to JSON backup file"]
M["Restore-RegistryBackup()"] --> N["Assert-BackupObject"]
N --> O["For each policy in backup"]
O --> P{readError == true?}
P -- Yes --> Q["Write-Warning: Skip, leave untouched"]
P -- No --> R{DoApply?}
R -- No --> S["Write-DryRun"]
R -- Yes --> T{existed?}
T -- Yes --> U["Set-PolicyValue"]
T -- No --> V["Remove-PolicyValue"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["New-Backup()"] --> B["Get-PolicySnapshot(Target, PolicyNames)"]
B --> C["For each policyName: Get-PolicyValue(Target, Name)"]
C --> D{Target.Kind == Registry?}
D -- Yes --> E{Test-Path Registry Key}
E -- Exists --> F["try: Get-Item + GetValue"]
F -- value != null --> G["Return {Exists=true, ReadError=false}"]
F -- value == null --> H["Return {Exists=false, ReadError=false}"]
F -- catch --> I["Write-Warning\nReturn {Exists=false, ReadError=true}"]
E -- Missing --> H
D -- No: JsonFile/macOS --> J["Return {Exists=..., ReadError=implicit null}"]
G & H & I & J --> K["Snapshot entry: existed, value, kind, readError"]
K --> L["Save to JSON backup file"]
M["Restore-RegistryBackup()"] --> N["Assert-BackupObject"]
N --> O["For each policy in backup"]
O --> P{readError == true?}
P -- Yes --> Q["Write-Warning: Skip, leave untouched"]
P -- No --> R{DoApply?}
R -- No --> S["Write-DryRun"]
R -- Yes --> T{existed?}
T -- Yes --> U["Set-PolicyValue"]
T -- No --> V["Remove-PolicyValue"]
|
|
@greptileai can i merge w/o fixing these |
|
Yes, you can merge safely. Both flagged issues are P2 hardening gaps, not bugs:
The actual data-loss bug (the whole point of this PR) is correctly fixed. These two items would be good follow-up issues to track, but they're not blocking. |
Summary
This is the correct version of the destructive-restore fix attempted in #27 (and #28). Both of those patched
Get-RegistrySnapshot, which has zero callers — the live backup path isNew-Backup → Get-PolicySnapshot → Get-PolicyValue, so the guard never ran. Greptile flagged exactly this on #27 ("3/5").What the live path did before: in
Get-PolicyValue's registry branch,$key.GetValue(...)had no error handling. An unreadable value (e.g. access denied) threw and aborted the entire backup; and the dead snapshot function would have recordedexisted=$false, which on restore means delete.Fix, in the path that actually runs:
Get-PolicyValue(registry) wraps the read in try/catch and returns aReadErrormarker instead of throwing:Get-PolicySnapshotpropagates it onto each entry asreadError.Restore-RegistryBackupskipsreadErrorentries (back-compat: missing property →$false), so a value we couldn't read is left untouched rather than deleted:Get-RegistrySnapshotto avoid future confusion.Notably,
Remove-PolicyValueis left as-is (-ErrorAction SilentlyContinue); this avoids the spurious "failed to remove" warnings that #27's-ErrorAction Stoprewrite introduced on idempotent removes (the other issue Greptile flagged).Supersedes #27.
Test-PolicyManifest.ps1andTest-Behavior.ps1pass.Link to Devin session: https://app.devin.ai/sessions/b703b48274874ae6b3df3015a1f741a7
Requested by: @osfv