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
// -----------------------------------------------------------------------