From a6e8fe320f65869a33928fd229a70ecbee918de5 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 9 Oct 2026 21:32:17 +0200 Subject: [PATCH] Agent: read uninstall-registry strings up to the first NUL (D-338) Some installers pad REG_SZ values with NULs. .NET Framework drops only one terminator, so the rest rode into the normalized software identity and, server-side, into a table key whose point read the HTTP layer rejects ("400 Invalid URL"). DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation, UninstallString and ParentKeyName are now read the way Windows shows them; a DisplayName of NULs only counts as empty. Co-Authored-By: Claude Opus 5.5 --- .../SoftwareInventoryRegistryTextTests.cs | 31 +++++++++++++++++++ .../Analyzers/SoftwareInventoryAnalyzer.cs | 30 +++++++++++++----- 2 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Analyzers/SoftwareInventoryRegistryTextTests.cs diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Analyzers/SoftwareInventoryRegistryTextTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Analyzers/SoftwareInventoryRegistryTextTests.cs new file mode 100644 index 00000000..e9120474 --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Analyzers/SoftwareInventoryRegistryTextTests.cs @@ -0,0 +1,31 @@ +#nullable enable +using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers; +using Xunit; + +namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Analyzers +{ + /// + /// Uninstall-key strings are read the way Windows shows them: up to the first NUL. Installers + /// that pad REG_SZ data with NULs otherwise leak them into the normalized vendor/name, which + /// the backend turns into a table key. + /// + public sealed class SoftwareInventoryRegistryTextTests + { + [Theory] + [InlineData("Contoso Widget", "Contoso Widget")] + [InlineData("Contoso Widget\0\0\0\0\0", "Contoso Widget")] + [InlineData("Contoso\0stale buffer", "Contoso")] + [InlineData("\0\0\0", "")] + [InlineData("", "")] + public void CutAtNul_KeepsTheTextBeforeTheFirstNul(string raw, string expected) + { + Assert.Equal(expected, SoftwareInventoryAnalyzer.CutAtNul(raw)); + } + + [Fact] + public void CutAtNul_Null_StaysNull() + { + Assert.Null(SoftwareInventoryAnalyzer.CutAtNul(null!)); + } + } +} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs index 9fdbadfe..2f5f0fe0 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs @@ -254,9 +254,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers if (subKey == null) continue; - var displayName = subKey.GetValue("DisplayName")?.ToString(); + var displayName = ReadRegistryText(subKey, "DisplayName"); var systemComponent = subKey.GetValue("SystemComponent"); - var parentKeyName = subKey.GetValue("ParentKeyName")?.ToString(); + var parentKeyName = ReadRegistryText(subKey, "ParentKeyName"); if (ShouldExclude(displayName, systemComponent, parentKeyName)) continue; @@ -264,11 +264,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers results.Add(new SoftwareEntry { DisplayName = displayName, - DisplayVersion = subKey.GetValue("DisplayVersion")?.ToString(), - Publisher = subKey.GetValue("Publisher")?.ToString(), - InstallDate = subKey.GetValue("InstallDate")?.ToString(), - InstallLocation = subKey.GetValue("InstallLocation")?.ToString(), - UninstallString = subKey.GetValue("UninstallString")?.ToString(), + DisplayVersion = ReadRegistryText(subKey, "DisplayVersion"), + Publisher = ReadRegistryText(subKey, "Publisher"), + InstallDate = ReadRegistryText(subKey, "InstallDate"), + InstallLocation = ReadRegistryText(subKey, "InstallLocation"), + UninstallString = ReadRegistryText(subKey, "UninstallString"), IsWindowsInstaller = Convert.ToInt32(subKey.GetValue("WindowsInstaller") ?? 0) == 1, RegistrySource = source }); @@ -287,6 +287,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers } } + private static string ReadRegistryText(RegistryKey key, string name) + => CutAtNul(key.GetValue(name)?.ToString()); + + /// + /// A registry string the way Windows shows it: up to the first NUL. Some installers write + /// REG_SZ data padded with NULs, and .NET Framework drops only one terminator — the rest + /// would ride into the normalized identity and, server-side, into a table key. + /// + internal static string CutAtNul(string value) + { + if (value == null) + return null; + var nul = value.IndexOf('\0'); + return nul < 0 ? value : value.Substring(0, nul); + } + // ----------------------------------------------------------------------- // HKU per-user profile enumeration // -----------------------------------------------------------------------