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("processor")] public object? Processor { get; set; }
|
||||||
[JsonPropertyName("osinfo")] public object? OsInfo { get; set; }
|
[JsonPropertyName("osinfo")] public object? OsInfo { get; set; }
|
||||||
[JsonPropertyName("domain")] public string? Domain { 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
|
public sealed class EndpointEventRecord
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public sealed class EndpointCollector : ICollector
|
|||||||
Processor = GetProcessor(),
|
Processor = GetProcessor(),
|
||||||
OsInfo = GetOsInfo(),
|
OsInfo = GetOsInfo(),
|
||||||
Domain = GetDomain(),
|
Domain = GetDomain(),
|
||||||
|
Applications = GetApplications(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (report.LastBootTime is DateTimeOffset boot)
|
if (report.LastBootTime is DateTimeOffset boot)
|
||||||
@@ -466,6 +467,76 @@ public sealed class EndpointCollector : ICollector
|
|||||||
return records.ToArray();
|
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
|
// 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;
|
// running out of space. Win32_LogicalDisk DriveType 3 = local fixed disk;
|
||||||
// network/removable drives are skipped. Bytes stay as longs; the server can
|
// 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."""
|
rather than dropping them silently."""
|
||||||
from plugins.endpoints.plugin import KNOWN_PAYLOAD_KEYS
|
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
|
assert key not in KNOWN_PAYLOAD_KEYS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user