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 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-10-09 21:32:17 +02:00
parent e5b69325a2
commit a6e8fe320f
2 changed files with 54 additions and 7 deletions
@@ -0,0 +1,31 @@
#nullable enable
using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers;
using Xunit;
namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Analyzers
{
/// <summary>
/// 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.
/// </summary>
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!));
}
}
}
@@ -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());
/// <summary>
/// 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.
/// </summary>
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
// -----------------------------------------------------------------------