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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user