collect installed-software SBOM from endpoint agent

The agent now reports installed applications from both Uninstall registry hives (64-bit and WOW6432Node) as {displayname, displayversion, publisher}, mirroring the Assess-Orchestrator applications collection. Registry-only, no Win32_Product/WMIC self-repair and no AppX. The server logs it as an unpersisted key until the SBOM store is built.
This commit is contained in:
benjamin.baker
2026-08-29 17:26:32 -04:00
committed by cproudlock
parent 57845e1a9a
commit cfb0b9ca4e
3 changed files with 77 additions and 1 deletions

View File

@@ -39,6 +39,10 @@ public sealed class EndpointReport
[JsonPropertyName("processor")] public object? Processor { get; set; }
[JsonPropertyName("osinfo")] public object? OsInfo { get; set; }
[JsonPropertyName("domain")] public string? Domain { get; set; }
// Installed-software SBOM, mirroring the Assess-Orchestrator applications
// collection: both Uninstall registry hives, {displayname, displayversion,
// publisher}. Server logs it as an unpersisted key until the SBOM store lands.
[JsonPropertyName("applications")] public object[]? Applications { get; set; }
}
public sealed class EndpointEventRecord

View File

@@ -56,6 +56,7 @@ public sealed class EndpointCollector : ICollector
Processor = GetProcessor(),
OsInfo = GetOsInfo(),
Domain = GetDomain(),
Applications = GetApplications(),
};
if (report.LastBootTime is DateTimeOffset boot)
@@ -466,6 +467,76 @@ public sealed class EndpointCollector : ICollector
return records.ToArray();
}
// Installed-software SBOM. Mirrors the Assess-Orchestrator applications
// collection exactly: enumerate both Uninstall registry hives (64-bit and
// WOW6432Node), keep only entries with a DisplayName, and emit
// {displayname, displayversion, publisher}. This is the same registry
// surface the machine-number/eDNC probes already read. Wire keys stay
// lowercase/no-underscore; the server logs this as an unpersisted key until
// the SBOM store is built. Never throws out to the caller.
private object[] GetApplications()
{
const string uninstall64 =
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
const string uninstall32 =
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
var result = new List<object>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
CollectUninstallHive(uninstall64, result, seen);
CollectUninstallHive(uninstall32, result, seen);
return result.ToArray();
}
private void CollectUninstallHive(string subKey, List<object> result,
HashSet<string> seen)
{
try
{
using var machine = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine, RegistryView.Default);
using var uninstall = machine.OpenSubKey(subKey);
if (uninstall is null)
return;
foreach (var name in uninstall.GetSubKeyNames())
{
try
{
using var entry = uninstall.OpenSubKey(name);
var displayName = entry?.GetValue("DisplayName")?.ToString()?.Trim();
if (string.IsNullOrWhiteSpace(displayName))
continue;
var displayVersion =
entry?.GetValue("DisplayVersion")?.ToString()?.Trim() ?? "";
var publisher =
entry?.GetValue("Publisher")?.ToString()?.Trim() ?? "";
// De-dupe on name+version so the two hives do not double-list
// the same package.
if (!seen.Add(displayName + "\u0000" + displayVersion))
continue;
result.Add(new
{
displayname = displayName,
displayversion = displayVersion,
publisher,
});
}
catch (Exception error)
{
logger.LogDebug(error, "Uninstall entry read blocked: {Entry}", name);
}
}
}
catch (Exception error)
{
logger.LogDebug(error, "Uninstall hive read blocked: {SubKey}", subKey);
}
}
// Fixed local disks with capacity/free so the server can flag a PC that is
// running out of space. Win32_LogicalDisk DriveType 3 = local fixed disk;
// network/removable drives are skipped. Bytes stay as longs; the server can

View File

@@ -182,7 +182,8 @@ def test_new_hardware_telemetry_keys_are_logged_not_silently_known():
rather than dropping them silently."""
from plugins.endpoints.plugin import KNOWN_PAYLOAD_KEYS
for key in ('disks', 'bios', 'memory', 'processor', 'osinfo', 'domain'):
for key in ('disks', 'bios', 'memory', 'processor', 'osinfo', 'domain',
'applications'):
assert key not in KNOWN_PAYLOAD_KEYS