From 26e5925672b4b8ad2fe72386e25d76e8e8252954 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Fri, 20 Mar 2026 14:43:58 +0100 Subject: [PATCH] feat: Software Inventory Analyzer with Vulnerability Correlation --- .gitignore | 1 + rules/analyze/ANALYZE-ID-003.json | 76 + rules/dist/analyze-rules.json | 81 + .../Analyzers/SoftwareInventoryAnalyzer.cs | 546 +++++ .../MonitoringService.CollectorLifecycle.cs | 15 + .../AutopilotMonitor.Agent/buildcounter.txt | 2 +- .../AutopilotMonitor.Functions.csproj | 2 + .../Data/cpe-mapping-seed.json | 2076 +++++++++++++++++ .../Config/GetAgentConfigFunction.cs | 5 +- .../Ingest/IngestEventsFunction.Processing.cs | 148 +- .../Functions/Ingest/IngestEventsFunction.cs | 12 +- .../GetUnmatchedSoftwareFunction.cs | 139 ++ .../VulnerabilityDataSyncFunction.cs | 70 + .../AutopilotMonitor.Functions/Program.cs | 9 + .../Services/AdminConfigurationService.cs | 10 +- .../Services/SignalRNotificationService.cs | 119 + .../Services/TableStorageService.cs | 9 + .../Services/TenantConfigurationService.cs | 2 + .../Services/Vulnerability/KevDataService.cs | 151 ++ .../Services/Vulnerability/MsrcApiClient.cs | 221 ++ .../Services/Vulnerability/NvdApiClient.cs | 203 ++ .../Services/Vulnerability/VersionComparer.cs | 87 + .../VulnerabilityCorrelationService.cs | 517 ++++ .../AutopilotMonitor.Shared/Constants.cs | 8 +- .../Models/Config/AdminConfiguration.cs | 22 + .../Models/Config/AgentConfigResponse.cs | 8 + .../Models/Config/TenantConfiguration.cs | 6 + .../components/VulnerabilityReportSection.tsx | 806 +++++++ .../app/sessions/[sessionId]/page.tsx | 38 +- .../components/AgentAnalyzersSection.tsx | 27 + .../app/settings/page.tsx | 6 + .../app/settings/types.ts | 1 + .../tsconfig.tsbuildinfo | 2 +- 33 files changed, 5395 insertions(+), 30 deletions(-) create mode 100644 rules/analyze/ANALYZE-ID-003.json create mode 100644 src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Analyzers/SoftwareInventoryAnalyzer.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Data/cpe-mapping-seed.json create mode 100644 src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/GetUnmatchedSoftwareFunction.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Services/SignalRNotificationService.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/KevDataService.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcApiClient.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/NvdApiClient.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VersionComparer.cs create mode 100644 src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs create mode 100644 src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/VulnerabilityReportSection.tsx diff --git a/.gitignore b/.gitignore index 0936b4e4..336426c4 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ tmpclaude-*-cwd .claude/commands/ux-insights.md .claude/commands/diag-agent-log.md .claude/commands/diag-ime-log.md +.claude/commands/update-cpe-seed.md .claude/scripts/query-appinsights.sh .claude/scripts/query-table.sh .claude/scripts/fetch-diag-zip.sh diff --git a/rules/analyze/ANALYZE-ID-003.json b/rules/analyze/ANALYZE-ID-003.json new file mode 100644 index 00000000..36fbf984 --- /dev/null +++ b/rules/analyze/ANALYZE-ID-003.json @@ -0,0 +1,76 @@ +{ + "$schema": "../schema/analyze-rule.schema.json", + "ruleId": "ANALYZE-ID-003", + "title": "Critical Vulnerability Detected", + "description": "Fires when the vulnerability correlation finds critical-severity CVEs or CISA KEV entries in installed software. This indicates the device has software with known, actively exploited vulnerabilities.", + "severity": "critical", + "category": "vulnerability", + "version": "1.0.0", + "author": "Autopilot Monitor", + "enabled": true, + "trigger": "single", + "baseConfidence": 90, + "conditions": [ + { + "signal": "critical_vulnerabilities", + "source": "event_type", + "eventType": "vulnerability_report", + "dataField": "scan_summary.critical_cves", + "operator": "gte", + "value": "1", + "required": false + }, + { + "signal": "kev_matches", + "source": "event_type", + "eventType": "vulnerability_report", + "dataField": "scan_summary.kev_matches", + "operator": "gte", + "value": "1", + "required": false + } + ], + "confidenceFactors": [ + { + "signal": "kev_confirmed", + "source": "event_data", + "eventType": "vulnerability_report", + "dataField": "scan_summary.kev_matches", + "operator": "gte", + "value": "1", + "weight": 10 + } + ], + "confidenceThreshold": 80, + "explanation": "The device has installed software with critical vulnerabilities. Software with CVEs rated CRITICAL (CVSS >= 9.0) or listed in the CISA Known Exploited Vulnerabilities catalog was detected. These vulnerabilities are actively targeted by attackers and should be remediated immediately.", + "remediation": [ + { + "title": "Review vulnerability findings", + "steps": [ + "Open the Vulnerability Report section in the session detail", + "Identify all software with critical or KEV-flagged vulnerabilities", + "Check if newer versions are available that address these CVEs", + "Update the affected software in your Intune app deployment" + ] + }, + { + "title": "Update deployment packages", + "steps": [ + "Check Intune for outdated app package versions", + "Update the deployment package to the latest patched version", + "Re-deploy to affected devices" + ] + } + ], + "relatedDocs": [ + { + "title": "CISA KEV Catalog", + "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog" + }, + { + "title": "NVD - National Vulnerability Database", + "url": "https://nvd.nist.gov/" + } + ], + "tags": ["vulnerability", "cve", "kev", "critical", "security"] +} diff --git a/rules/dist/analyze-rules.json b/rules/dist/analyze-rules.json index 3d657e60..fcec9b98 100644 --- a/rules/dist/analyze-rules.json +++ b/rules/dist/analyze-rules.json @@ -1110,6 +1110,87 @@ "local-admin", "bypass-detection" ] + }, + { + "ruleId": "ANALYZE-ID-003", + "title": "Critical Vulnerability Detected", + "description": "Fires when the vulnerability correlation finds critical-severity CVEs or CISA KEV entries in installed software.", + "severity": "critical", + "category": "vulnerability", + "version": "1.0.0", + "author": "Autopilot Monitor", + "enabled": true, + "trigger": "single", + "baseConfidence": 90, + "conditions": [ + { + "signal": "critical_vulnerabilities", + "source": "event_type", + "eventType": "vulnerability_report", + "dataField": "scan_summary.critical_cves", + "operator": "gte", + "value": "1", + "required": false + }, + { + "signal": "kev_matches", + "source": "event_type", + "eventType": "vulnerability_report", + "dataField": "scan_summary.kev_matches", + "operator": "gte", + "value": "1", + "required": false + } + ], + "confidenceFactors": [ + { + "signal": "kev_confirmed", + "source": "event_data", + "eventType": "vulnerability_report", + "dataField": "scan_summary.kev_matches", + "operator": "gte", + "value": "1", + "weight": 10 + } + ], + "confidenceThreshold": 80, + "explanation": "The device has installed software with critical vulnerabilities. Software with CVEs rated CRITICAL (CVSS >= 9.0) or listed in the CISA Known Exploited Vulnerabilities catalog was detected. These vulnerabilities are actively targeted by attackers and should be remediated immediately.", + "remediation": [ + { + "title": "Review vulnerability findings", + "steps": [ + "Open the Vulnerability Report section in the session detail", + "Identify all software with critical or KEV-flagged vulnerabilities", + "Check if newer versions are available that address these CVEs", + "Update the affected software in your Intune app deployment" + ] + }, + { + "title": "Update deployment packages", + "steps": [ + "Check Intune for outdated app package versions", + "Update the deployment package to the latest patched version", + "Re-deploy to affected devices" + ] + } + ], + "relatedDocs": [ + { + "title": "CISA KEV Catalog", + "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog" + }, + { + "title": "NVD - National Vulnerability Database", + "url": "https://nvd.nist.gov/" + } + ], + "tags": [ + "vulnerability", + "cve", + "kev", + "critical", + "security" + ] } ] } diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Analyzers/SoftwareInventoryAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Analyzers/SoftwareInventoryAnalyzer.cs new file mode 100644 index 00000000..92ba9c45 --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Analyzers/SoftwareInventoryAnalyzer.cs @@ -0,0 +1,546 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using AutopilotMonitor.Agent.Core.Logging; +using AutopilotMonitor.Shared.Models; +using Microsoft.Win32; + +namespace AutopilotMonitor.Agent.Core.Monitoring.Analyzers +{ + /// + /// Collects installed software inventory from the Windows registry, normalizes + /// vendor/product/version strings, and emits structured events for vulnerability correlation. + /// + /// Lifecycle: + /// AnalyzeAtStartup() — baseline inventory snapshot (before enrollment installs) + /// AnalyzeAtShutdown() — final inventory + delta (what was installed during enrollment) + /// + /// The actual CVE/KEV correlation happens server-side after the events are ingested. + /// + public class SoftwareInventoryAnalyzer : IAgentAnalyzer + { + private readonly string _sessionId; + private readonly string _tenantId; + private readonly Action _emitEvent; + private readonly AgentLogger _logger; + + // Captured at startup for delta detection at shutdown + private List _startupInventory; + + public string Name => "SoftwareInventoryAnalyzer"; + + // Maximum items per event chunk to stay within Table Storage property size limits (~64 KB) + private const int ChunkSize = 75; + + // ----------------------------------------------------------------------- + // Registry paths for installed software + // ----------------------------------------------------------------------- + + private static readonly string[] UninstallRegistryPaths = new[] + { + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + }; + + private const string UninstallRegistryPathHkcu = + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; + + // ----------------------------------------------------------------------- + // Publisher normalization map + // ----------------------------------------------------------------------- + + private static readonly Dictionary PublisherMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "microsoft corporation", "microsoft" }, + { "microsoft", "microsoft" }, + { "google llc", "google" }, + { "google inc", "google" }, + { "google inc.", "google" }, + { "adobe inc.", "adobe" }, + { "adobe systems incorporated", "adobe" }, + { "adobe systems, incorporated", "adobe" }, + { "mozilla corporation", "mozilla" }, + { "mozilla", "mozilla" }, + { "oracle corporation", "oracle" }, + { "oracle", "oracle" }, + { "apple inc.", "apple" }, + { "apple inc", "apple" }, + { "dell inc.", "dell" }, + { "dell inc", "dell" }, + { "dell technologies", "dell" }, + { "hewlett-packard", "hp" }, + { "hewlett packard enterprise", "hp" }, + { "hp inc.", "hp" }, + { "hp inc", "hp" }, + { "lenovo", "lenovo" }, + { "lenovo group limited", "lenovo" }, + { "cisco systems, inc.", "cisco" }, + { "cisco systems", "cisco" }, + { "vmware, inc.", "vmware" }, + { "vmware", "vmware" }, + { "citrix systems, inc.", "citrix" }, + { "citrix", "citrix" }, + { "zoom video communications, inc.", "zoom" }, + { "slack technologies, inc.", "slack" }, + { "slack technologies, llc", "slack" }, + { "7-zip", "7-zip" }, + { "igor pavlov", "7-zip" }, + { "the 7-zip developers", "7-zip" }, + { "notepad++ team", "notepad++" }, + { "don ho", "notepad++" }, + { "putty", "putty" }, + { "simon tatham", "putty" }, + { "winscp", "winscp" }, + { "martin prikryl", "winscp" }, + { "teamviewer", "teamviewer" }, + { "teamviewer germany gmbh", "teamviewer" }, + { "fortinet technologies (canada) inc.", "fortinet" }, + { "fortinet inc", "fortinet" }, + { "palo alto networks", "paloaltonetworks" }, + { "crowdstrike, inc.", "crowdstrike" }, + { "crowdstrike inc.", "crowdstrike" }, + { "git", "git" }, + { "the git development community", "git" }, + { "python software foundation", "python" }, + { "node.js", "nodejs" }, + { "node.js foundation", "nodejs" }, + { "openjs foundation", "nodejs" }, + }; + + // ----------------------------------------------------------------------- + // Publisher suffix stripping + // ----------------------------------------------------------------------- + + private static readonly string[] PublisherSuffixes = new[] + { + ", inc.", " inc.", " inc", ", llc", " llc", ", ltd", " ltd", " ltd.", + " corporation", " corp.", " corp", " gmbh", " ag", " se", + " co.", " co", " group", " limited" + }; + + // ----------------------------------------------------------------------- + // Exclude patterns for filtering noise + // ----------------------------------------------------------------------- + + private static readonly Regex KbUpdatePattern = new Regex(@"^KB\d{6,}", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly string[] ExcludeContains = new[] + { + "Language Pack", + "MUI Pack", + "Spell Checking", + "Proofing Tools", + "Microsoft .NET Host", + "Microsoft .NET Runtime", + "Microsoft .NET AppHost", + "Microsoft ASP.NET", + "Microsoft Windows Desktop Runtime", + "Additional Runtime", + "Minimum Runtime", + "Microsoft Update Health Tools", + "Update for Windows", + "Security Update for", + "Hotfix for", + }; + + private static readonly string[] ExcludeStartsWith = new[] + { + "vs_", // Visual Studio installer bootstrapper components + "MSVC", // MSVC redistributable sub-components (the parent is kept) + }; + + // ----------------------------------------------------------------------- + // Regex patterns for normalization + // ----------------------------------------------------------------------- + + private static readonly Regex ArchitecturePattern = new Regex( + @"\s*[\(\-]\s*(?:x64|x86|64-bit|32-bit|amd64|arm64)\s*[\)]?", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex TrailingVersionPattern = new Regex( + @"\s+v?\d+(?:\.\d+)*\s*$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + private static readonly Regex VersionExtractPattern = new Regex( + @"(\d+(?:\.\d+){0,3})", + RegexOptions.Compiled); + + public SoftwareInventoryAnalyzer( + string sessionId, + string tenantId, + Action emitEvent, + AgentLogger logger) + { + _sessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId)); + _tenantId = tenantId ?? throw new ArgumentNullException(nameof(tenantId)); + _emitEvent = emitEvent ?? throw new ArgumentNullException(nameof(emitEvent)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public void AnalyzeAtStartup() + { + _logger.Info($"{Name}: Running startup analysis (baseline inventory)"); + try + { + _startupInventory = CollectAndNormalize(); + EmitInventoryEvents("startup", EnrollmentPhase.Unknown, _startupInventory, null); + } + catch (Exception ex) + { + _logger.Error($"{Name}: Startup analysis failed", ex); + } + } + + public void AnalyzeAtShutdown() + { + _logger.Info($"{Name}: Running shutdown analysis (delta detection)"); + try + { + var currentInventory = CollectAndNormalize(); + var newInstalls = ComputeDelta(_startupInventory ?? new List(), currentInventory); + EmitInventoryEvents("shutdown", EnrollmentPhase.Complete, currentInventory, newInstalls); + } + catch (Exception ex) + { + _logger.Error($"{Name}: Shutdown analysis failed", ex); + } + } + + // ----------------------------------------------------------------------- + // Collection + // ----------------------------------------------------------------------- + + private List CollectAndNormalize() + { + var entries = new List(); + + // HKLM 64-bit and WOW6432Node (32-bit) + foreach (var path in UninstallRegistryPaths) + { + var source = path.Contains("WOW6432Node") ? "HKLM_32" : "HKLM_64"; + CollectFromKey(Registry.LocalMachine, path, source, entries); + } + + // HKCU (may be empty during OOBE when running as SYSTEM) + try + { + CollectFromKey(Registry.CurrentUser, UninstallRegistryPathHkcu, "HKCU", entries); + } + catch (Exception ex) + { + _logger.Debug($"{Name}: HKCU read skipped (expected during OOBE): {ex.Message}"); + } + + // Normalize all entries + foreach (var entry in entries) + { + NormalizeEntry(entry); + } + + _logger.Info($"{Name}: Collected {entries.Count} software entries"); + return entries; + } + + private void CollectFromKey(RegistryKey rootKey, string subKeyPath, string source, List results) + { + try + { + using (var key = rootKey.OpenSubKey(subKeyPath, writable: false)) + { + if (key == null) + { + _logger.Debug($"{Name}: Registry key not found: {source}\\{subKeyPath}"); + return; + } + + var subKeyNames = key.GetSubKeyNames(); + foreach (var subKeyName in subKeyNames) + { + try + { + using (var subKey = key.OpenSubKey(subKeyName, writable: false)) + { + if (subKey == null) + continue; + + var displayName = subKey.GetValue("DisplayName")?.ToString(); + var systemComponent = subKey.GetValue("SystemComponent"); + var parentKeyName = subKey.GetValue("ParentKeyName")?.ToString(); + + if (ShouldExclude(displayName, systemComponent, parentKeyName)) + continue; + + 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(), + IsWindowsInstaller = Convert.ToInt32(subKey.GetValue("WindowsInstaller") ?? 0) == 1, + RegistrySource = source + }); + } + } + catch (Exception ex) + { + _logger.Debug($"{Name}: Error reading subkey {subKeyName}: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + _logger.Warning($"{Name}: Failed to read {source}\\{subKeyPath}: {ex.Message}"); + } + } + + // ----------------------------------------------------------------------- + // Filtering + // ----------------------------------------------------------------------- + + private static bool ShouldExclude(string displayName, object systemComponent, string parentKeyName) + { + // No display name = not a user-visible application + if (string.IsNullOrWhiteSpace(displayName)) + return true; + + // System component flag + if (systemComponent != null) + { + try + { + if (Convert.ToInt32(systemComponent) == 1) + return true; + } + catch { /* non-integer value, ignore */ } + } + + // Child component / update (has a parent) + if (!string.IsNullOrEmpty(parentKeyName)) + return true; + + // KB updates (Windows Updates) + if (KbUpdatePattern.IsMatch(displayName)) + return true; + + // Known noise patterns (contains) + foreach (var pattern in ExcludeContains) + { + if (displayName.IndexOf(pattern, StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } + + // Known noise patterns (starts with) + foreach (var pattern in ExcludeStartsWith) + { + if (displayName.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + + // ----------------------------------------------------------------------- + // Normalization + // ----------------------------------------------------------------------- + + private void NormalizeEntry(SoftwareEntry entry) + { + entry.NormalizedPublisher = NormalizePublisher(entry.Publisher); + entry.NormalizedName = NormalizeName(entry.DisplayName); + entry.NormalizedVersion = NormalizeVersion(entry.DisplayVersion); + AssessConfidence(entry); + } + + private static string NormalizePublisher(string publisher) + { + if (string.IsNullOrWhiteSpace(publisher)) + return string.Empty; + + var trimmed = publisher.Trim(); + + // Check known publisher map + if (PublisherMap.TryGetValue(trimmed, out var mapped)) + return mapped; + + // Fallback: lowercase + strip common suffixes + var lower = trimmed.ToLowerInvariant(); + foreach (var suffix in PublisherSuffixes) + { + if (lower.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) + { + lower = lower.Substring(0, lower.Length - suffix.Length).TrimEnd(); + break; // strip only one suffix + } + } + + return lower; + } + + private static string NormalizeName(string displayName) + { + if (string.IsNullOrWhiteSpace(displayName)) + return string.Empty; + + var name = displayName.Trim().ToLowerInvariant(); + + // Strip architecture markers: (x64), (x86), (64-bit), - x64, etc. + name = ArchitecturePattern.Replace(name, ""); + + // Strip trailing version-like suffix: "chrome 134.0.6998.89" → "chrome" + name = TrailingVersionPattern.Replace(name, ""); + + return name.Trim(); + } + + private static string NormalizeVersion(string displayVersion) + { + if (string.IsNullOrWhiteSpace(displayVersion)) + return string.Empty; + + var match = VersionExtractPattern.Match(displayVersion.Trim()); + return match.Success ? match.Groups[1].Value : displayVersion.Trim(); + } + + private void AssessConfidence(SoftwareEntry entry) + { + bool publisherKnown = !string.IsNullOrEmpty(entry.Publisher) && + PublisherMap.ContainsKey(entry.Publisher.Trim()); + bool versionClean = !string.IsNullOrEmpty(entry.DisplayVersion) && + VersionExtractPattern.IsMatch(entry.DisplayVersion); + + if (publisherKnown && versionClean) + entry.NormalizationConfidence = "high"; + else if (publisherKnown || versionClean) + entry.NormalizationConfidence = "medium"; + else + entry.NormalizationConfidence = "low"; + } + + // ----------------------------------------------------------------------- + // Delta computation + // ----------------------------------------------------------------------- + + private static List ComputeDelta(List baseline, List current) + { + var baselineKeys = new HashSet( + baseline.Select(e => MakeKey(e)), + StringComparer.OrdinalIgnoreCase); + + return current.Where(e => !baselineKeys.Contains(MakeKey(e))).ToList(); + } + + private static string MakeKey(SoftwareEntry e) + { + return $"{e.RegistrySource}|{e.DisplayName}|{e.DisplayVersion ?? ""}"; + } + + // ----------------------------------------------------------------------- + // Event emission (with chunking for large inventories) + // ----------------------------------------------------------------------- + + private void EmitInventoryEvents( + string trigger, + EnrollmentPhase phase, + List inventory, + List newInstalls) + { + int totalChunks = Math.Max(1, (int)Math.Ceiling(inventory.Count / (double)ChunkSize)); + + var highCount = inventory.Count(e => e.NormalizationConfidence == "high"); + var medCount = inventory.Count(e => e.NormalizationConfidence == "medium"); + var lowCount = inventory.Count(e => e.NormalizationConfidence == "low"); + + for (int i = 0; i < totalChunks; i++) + { + var chunk = inventory.Skip(i * ChunkSize).Take(ChunkSize).ToList(); + + var data = new Dictionary + { + { "triggered_at", trigger }, + { "total_count", inventory.Count }, + { "chunk_index", i }, + { "chunk_count", totalChunks }, + { "inventory", chunk.Select(SerializeEntry).ToList() } + }; + + // Confidence summary only in first chunk + if (i == 0) + { + data["confidence_summary"] = new Dictionary + { + { "high", highCount }, + { "medium", medCount }, + { "low", lowCount } + }; + } + + // New installs only in last chunk + if (i == totalChunks - 1 && newInstalls != null) + { + data["new_installs_during_enrollment"] = newInstalls.Select(SerializeEntry).ToList(); + data["new_installs_count"] = newInstalls.Count; + } + + var message = i == 0 + ? (trigger == "startup" + ? $"{Name}: Baseline inventory ({inventory.Count} items, {highCount} high-confidence)" + : $"{Name}: Shutdown inventory ({inventory.Count} items, {newInstalls?.Count ?? 0} new during enrollment)") + : $"{Name}: Inventory chunk {i + 1}/{totalChunks}"; + + _emitEvent(new EnrollmentEvent + { + SessionId = _sessionId, + TenantId = _tenantId, + EventType = "software_inventory_analysis", + Severity = EventSeverity.Info, + Source = Name, + Phase = phase, + Message = message, + Data = data + }); + } + } + + private static Dictionary SerializeEntry(SoftwareEntry e) + { + return new Dictionary + { + { "displayName", e.DisplayName ?? "" }, + { "displayVersion", e.DisplayVersion ?? "" }, + { "publisher", e.Publisher ?? "" }, + { "installDate", e.InstallDate ?? "" }, + { "registrySource", e.RegistrySource ?? "" }, + { "normalizedPublisher", e.NormalizedPublisher ?? "" }, + { "normalizedName", e.NormalizedName ?? "" }, + { "normalizedVersion", e.NormalizedVersion ?? "" }, + { "normalizationConfidence", e.NormalizationConfidence ?? "low" } + }; + } + + // ----------------------------------------------------------------------- + // Private model + // ----------------------------------------------------------------------- + + private class SoftwareEntry + { + public string DisplayName { get; set; } + public string DisplayVersion { get; set; } + public string Publisher { get; set; } + public string InstallDate { get; set; } + public string InstallLocation { get; set; } + public string UninstallString { get; set; } + public bool IsWindowsInstaller { get; set; } + public string RegistrySource { get; set; } + + // Normalized fields + public string NormalizedPublisher { get; set; } + public string NormalizedName { get; set; } + public string NormalizedVersion { get; set; } + public string NormalizationConfidence { get; set; } + } + } +} diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs index d62eb2a2..5e7f2d76 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs @@ -440,6 +440,21 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core _logger.Info("LocalAdminAnalyzer disabled by remote config"); } + if (analyzerConfig.EnableSoftwareInventoryAnalyzer) + { + _analyzers.Add(new SoftwareInventoryAnalyzer( + _configuration.SessionId, + _configuration.TenantId, + EmitEvent, + _logger + )); + _logger.Info("SoftwareInventoryAnalyzer registered"); + } + else + { + _logger.Info("SoftwareInventoryAnalyzer disabled by remote config"); + } + _logger.Info($"Analyzers initialized: {_analyzers.Count} active"); } diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index ec0e415d..625c09bc 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -522 +532 diff --git a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj index f634cec2..b474ddd0 100644 --- a/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj +++ b/src/Backend/AutopilotMonitor.Functions/AutopilotMonitor.Functions.csproj @@ -15,6 +15,7 @@ + @@ -35,6 +36,7 @@ + diff --git a/src/Backend/AutopilotMonitor.Functions/Data/cpe-mapping-seed.json b/src/Backend/AutopilotMonitor.Functions/Data/cpe-mapping-seed.json new file mode 100644 index 00000000..762a69f3 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Data/cpe-mapping-seed.json @@ -0,0 +1,2076 @@ +{ + "version": "1.0.0", + "lastUpdated": "2025-03-20", + "mappings": [ + { + "normalizedVendor": "google", + "normalizedProduct": "google chrome", + "displayNamePatterns": ["google chrome", "chrome"], + "publisherPatterns": ["google", "google llc", "google inc"], + "cpeVendor": "google", + "cpeProduct": "chrome", + "cpeUri": "cpe:2.3:a:google:chrome", + "category": "browser" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft edge", + "displayNamePatterns": ["microsoft edge", "edge"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "edge_chromium", + "cpeUri": "cpe:2.3:a:microsoft:edge_chromium", + "category": "browser" + }, + { + "normalizedVendor": "mozilla", + "normalizedProduct": "mozilla firefox", + "displayNamePatterns": ["mozilla firefox", "firefox"], + "publisherPatterns": ["mozilla", "mozilla corporation"], + "cpeVendor": "mozilla", + "cpeProduct": "firefox", + "cpeUri": "cpe:2.3:a:mozilla:firefox", + "category": "browser" + }, + { + "normalizedVendor": "brave", + "normalizedProduct": "brave browser", + "displayNamePatterns": ["brave", "brave browser"], + "publisherPatterns": ["brave software", "brave software inc"], + "cpeVendor": "brave", + "cpeProduct": "brave", + "cpeUri": "cpe:2.3:a:brave:brave", + "category": "browser" + }, + { + "normalizedVendor": "opera", + "normalizedProduct": "opera", + "displayNamePatterns": ["opera stable", "opera browser", "opera"], + "publisherPatterns": ["opera", "opera software"], + "cpeVendor": "opera", + "cpeProduct": "opera_browser", + "cpeUri": "cpe:2.3:a:opera:opera_browser", + "category": "browser" + }, + { + "normalizedVendor": "vivaldi", + "normalizedProduct": "vivaldi", + "displayNamePatterns": ["vivaldi"], + "publisherPatterns": ["vivaldi technologies", "vivaldi"], + "cpeVendor": "vivaldi", + "cpeProduct": "vivaldi", + "cpeUri": "cpe:2.3:a:vivaldi:vivaldi", + "category": "browser" + }, + { + "normalizedVendor": "adobe", + "normalizedProduct": "adobe acrobat reader dc", + "displayNamePatterns": ["adobe acrobat reader", "acrobat reader dc", "acrobat reader"], + "publisherPatterns": ["adobe", "adobe systems", "adobe systems incorporated", "adobe inc."], + "cpeVendor": "adobe", + "cpeProduct": "acrobat_reader_dc", + "cpeUri": "cpe:2.3:a:adobe:acrobat_reader_dc", + "category": "pdf_documents" + }, + { + "normalizedVendor": "adobe", + "normalizedProduct": "adobe acrobat dc", + "displayNamePatterns": ["adobe acrobat dc", "adobe acrobat pro", "acrobat dc"], + "publisherPatterns": ["adobe", "adobe systems", "adobe systems incorporated", "adobe inc."], + "cpeVendor": "adobe", + "cpeProduct": "acrobat_dc", + "cpeUri": "cpe:2.3:a:adobe:acrobat_dc", + "category": "pdf_documents" + }, + { + "normalizedVendor": "foxit", + "normalizedProduct": "foxit reader", + "displayNamePatterns": ["foxit reader", "foxit pdf reader"], + "publisherPatterns": ["foxit", "foxit software", "foxit software inc."], + "cpeVendor": "foxit", + "cpeProduct": "pdf_reader", + "cpeUri": "cpe:2.3:a:foxit:pdf_reader", + "category": "pdf_documents" + }, + { + "normalizedVendor": "foxit", + "normalizedProduct": "foxit pdf editor", + "displayNamePatterns": ["foxit pdf editor", "foxit phantompdf"], + "publisherPatterns": ["foxit", "foxit software", "foxit software inc."], + "cpeVendor": "foxit", + "cpeProduct": "pdf_editor", + "cpeUri": "cpe:2.3:a:foxit:pdf_editor", + "category": "pdf_documents" + }, + { + "normalizedVendor": "the document foundation", + "normalizedProduct": "libreoffice", + "displayNamePatterns": ["libreoffice"], + "publisherPatterns": ["the document foundation"], + "cpeVendor": "libreoffice", + "cpeProduct": "libreoffice", + "cpeUri": "cpe:2.3:a:libreoffice:libreoffice", + "category": "pdf_documents" + }, + { + "normalizedVendor": "oracle", + "normalizedProduct": "java runtime environment", + "displayNamePatterns": ["java 8", "java(tm) se runtime", "java se runtime", "java runtime"], + "publisherPatterns": ["oracle", "oracle corporation"], + "cpeVendor": "oracle", + "cpeProduct": "jre", + "cpeUri": "cpe:2.3:a:oracle:jre", + "category": "runtime" + }, + { + "normalizedVendor": "oracle", + "normalizedProduct": "java development kit", + "displayNamePatterns": ["java se development kit", "jdk", "java(tm) se development kit"], + "publisherPatterns": ["oracle", "oracle corporation"], + "cpeVendor": "oracle", + "cpeProduct": "jdk", + "cpeUri": "cpe:2.3:a:oracle:jdk", + "category": "runtime" + }, + { + "normalizedVendor": "eclipse", + "normalizedProduct": "temurin", + "displayNamePatterns": ["eclipse temurin", "adoptium", "openjdk", "adoptopenjdk"], + "publisherPatterns": ["eclipse adoptium", "eclipse foundation"], + "cpeVendor": "eclipse", + "cpeProduct": "temurin", + "cpeUri": "cpe:2.3:a:eclipse:temurin", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": ".net desktop runtime", + "displayNamePatterns": ["microsoft .net runtime", ".net desktop runtime", "microsoft .net desktop runtime", ".net runtime"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": ".net", + "cpeUri": "cpe:2.3:a:microsoft:.net", + "category": "runtime" + }, + { + "normalizedVendor": "nodejs", + "normalizedProduct": "node.js", + "displayNamePatterns": ["node.js", "nodejs"], + "publisherPatterns": ["node.js foundation", "openjs foundation"], + "cpeVendor": "nodejs", + "cpeProduct": "node.js", + "cpeUri": "cpe:2.3:a:nodejs:node.js", + "category": "runtime" + }, + { + "normalizedVendor": "python", + "normalizedProduct": "python", + "displayNamePatterns": ["python 3", "python 2", "python"], + "publisherPatterns": ["python software foundation"], + "cpeVendor": "python", + "cpeProduct": "python", + "cpeUri": "cpe:2.3:a:python:python", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "visual c++ redistributable", + "displayNamePatterns": ["microsoft visual c++ 2015-2022", "microsoft visual c++ 2015-2019", "visual c++ redistributable", "microsoft visual c++"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "visual_c%2b%2b", + "cpeUri": "cpe:2.3:a:microsoft:visual_c%2b%2b", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "powershell", + "displayNamePatterns": ["powershell 7", "powershell"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "powershell", + "cpeUri": "cpe:2.3:a:microsoft:powershell", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft teams", + "displayNamePatterns": ["microsoft teams", "teams"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "teams", + "cpeUri": "cpe:2.3:a:microsoft:teams", + "category": "communication" + }, + { + "normalizedVendor": "zoom", + "normalizedProduct": "zoom", + "displayNamePatterns": ["zoom workplace", "zoom", "zoom client"], + "publisherPatterns": ["zoom", "zoom video communications", "zoom video communications, inc."], + "cpeVendor": "zoom", + "cpeProduct": "zoom", + "cpeUri": "cpe:2.3:a:zoom:zoom", + "category": "communication" + }, + { + "normalizedVendor": "slack", + "normalizedProduct": "slack", + "displayNamePatterns": ["slack", "slack desktop"], + "publisherPatterns": ["slack technologies", "slack technologies, inc.", "slack technologies, llc"], + "cpeVendor": "slack", + "cpeProduct": "slack", + "cpeUri": "cpe:2.3:a:slack:slack", + "category": "communication" + }, + { + "normalizedVendor": "cisco", + "normalizedProduct": "cisco webex", + "displayNamePatterns": ["webex meetings", "cisco webex meetings", "webex", "cisco webex"], + "publisherPatterns": ["cisco", "cisco systems", "cisco systems, inc."], + "cpeVendor": "cisco", + "cpeProduct": "webex_meetings", + "cpeUri": "cpe:2.3:a:cisco:webex_meetings", + "category": "communication" + }, + { + "normalizedVendor": "discord", + "normalizedProduct": "discord", + "displayNamePatterns": ["discord"], + "publisherPatterns": ["discord inc.", "discord"], + "cpeVendor": "discord", + "cpeProduct": "discord", + "cpeUri": "cpe:2.3:a:discord:discord", + "category": "communication" + }, + { + "normalizedVendor": "signal", + "normalizedProduct": "signal", + "displayNamePatterns": ["signal", "signal desktop"], + "publisherPatterns": ["signal messenger, llc", "signal foundation", "signal"], + "cpeVendor": "signal", + "cpeProduct": "signal-desktop", + "cpeUri": "cpe:2.3:a:signal:signal-desktop", + "category": "communication" + }, + { + "normalizedVendor": "teamviewer", + "normalizedProduct": "teamviewer", + "displayNamePatterns": ["teamviewer"], + "publisherPatterns": ["teamviewer", "teamviewer gmbh", "teamviewer germany gmbh"], + "cpeVendor": "teamviewer", + "cpeProduct": "teamviewer", + "cpeUri": "cpe:2.3:a:teamviewer:teamviewer", + "category": "remote_access" + }, + { + "normalizedVendor": "anydesk", + "normalizedProduct": "anydesk", + "displayNamePatterns": ["anydesk"], + "publisherPatterns": ["anydesk software gmbh", "anydesk"], + "cpeVendor": "anydesk", + "cpeProduct": "anydesk", + "cpeUri": "cpe:2.3:a:anydesk:anydesk", + "category": "remote_access" + }, + { + "normalizedVendor": "devolutions", + "normalizedProduct": "remote desktop manager", + "displayNamePatterns": ["remote desktop manager"], + "publisherPatterns": ["devolutions", "devolutions inc."], + "cpeVendor": "devolutions", + "cpeProduct": "remote_desktop_manager", + "cpeUri": "cpe:2.3:a:devolutions:remote_desktop_manager", + "category": "remote_access" + }, + { + "normalizedVendor": "rustdesk", + "normalizedProduct": "rustdesk", + "displayNamePatterns": ["rustdesk"], + "publisherPatterns": ["rustdesk", "purslane ltd"], + "cpeVendor": "rustdesk", + "cpeProduct": "rustdesk", + "cpeUri": "cpe:2.3:a:rustdesk:rustdesk", + "category": "remote_access" + }, + { + "normalizedVendor": "realvnc", + "normalizedProduct": "realvnc", + "displayNamePatterns": ["vnc server", "vnc viewer", "realvnc", "vnc connect"], + "publisherPatterns": ["realvnc", "realvnc ltd"], + "cpeVendor": "realvnc", + "cpeProduct": "vnc_server", + "cpeUri": "cpe:2.3:a:realvnc:vnc_server", + "category": "remote_access" + }, + { + "normalizedVendor": "crowdstrike", + "normalizedProduct": "crowdstrike falcon sensor", + "displayNamePatterns": ["crowdstrike falcon sensor", "crowdstrike windows sensor", "falcon sensor"], + "publisherPatterns": ["crowdstrike", "crowdstrike, inc."], + "cpeVendor": "crowdstrike", + "cpeProduct": "falcon_sensor", + "cpeUri": "cpe:2.3:a:crowdstrike:falcon_sensor", + "category": "security" + }, + { + "normalizedVendor": "vmware", + "normalizedProduct": "carbon black cloud sensor", + "displayNamePatterns": ["carbon black cloud sensor", "vmware carbon black", "cb defense sensor"], + "publisherPatterns": ["vmware", "vmware, inc.", "carbon black"], + "cpeVendor": "vmware", + "cpeProduct": "carbon_black_cloud", + "cpeUri": "cpe:2.3:a:vmware:carbon_black_cloud", + "category": "security" + }, + { + "normalizedVendor": "sophos", + "normalizedProduct": "sophos endpoint", + "displayNamePatterns": ["sophos endpoint agent", "sophos intercept x", "sophos endpoint"], + "publisherPatterns": ["sophos", "sophos limited", "sophos ltd"], + "cpeVendor": "sophos", + "cpeProduct": "endpoint_protection", + "cpeUri": "cpe:2.3:a:sophos:endpoint_protection", + "category": "security" + }, + { + "normalizedVendor": "eset", + "normalizedProduct": "eset endpoint security", + "displayNamePatterns": ["eset endpoint security", "eset endpoint antivirus", "eset security"], + "publisherPatterns": ["eset", "eset, spol. s r.o."], + "cpeVendor": "eset", + "cpeProduct": "endpoint_security", + "cpeUri": "cpe:2.3:a:eset:endpoint_security", + "category": "security" + }, + { + "normalizedVendor": "malwarebytes", + "normalizedProduct": "malwarebytes", + "displayNamePatterns": ["malwarebytes", "malwarebytes endpoint protection"], + "publisherPatterns": ["malwarebytes", "malwarebytes corporation"], + "cpeVendor": "malwarebytes", + "cpeProduct": "malwarebytes", + "cpeUri": "cpe:2.3:a:malwarebytes:malwarebytes", + "category": "security" + }, + { + "normalizedVendor": "kaspersky", + "normalizedProduct": "kaspersky endpoint security", + "displayNamePatterns": ["kaspersky endpoint security", "kaspersky endpoint agent"], + "publisherPatterns": ["kaspersky", "kaspersky lab", "ao kaspersky lab"], + "cpeVendor": "kaspersky", + "cpeProduct": "endpoint_security", + "cpeUri": "cpe:2.3:a:kaspersky:endpoint_security", + "category": "security" + }, + { + "normalizedVendor": "cisco", + "normalizedProduct": "cisco anyconnect", + "displayNamePatterns": ["cisco anyconnect", "cisco anyconnect secure mobility", "cisco secure client"], + "publisherPatterns": ["cisco", "cisco systems", "cisco systems, inc."], + "cpeVendor": "cisco", + "cpeProduct": "anyconnect_secure_mobility_client", + "cpeUri": "cpe:2.3:a:cisco:anyconnect_secure_mobility_client", + "category": "vpn" + }, + { + "normalizedVendor": "paloaltonetworks", + "normalizedProduct": "globalprotect", + "displayNamePatterns": ["globalprotect", "palo alto globalprotect"], + "publisherPatterns": ["palo alto networks", "paloaltonetworks"], + "cpeVendor": "paloaltonetworks", + "cpeProduct": "globalprotect", + "cpeUri": "cpe:2.3:a:paloaltonetworks:globalprotect", + "category": "vpn" + }, + { + "normalizedVendor": "fortinet", + "normalizedProduct": "forticlient", + "displayNamePatterns": ["forticlient", "forticlient vpn"], + "publisherPatterns": ["fortinet", "fortinet inc", "fortinet technologies"], + "cpeVendor": "fortinet", + "cpeProduct": "forticlient", + "cpeUri": "cpe:2.3:a:fortinet:forticlient", + "category": "vpn" + }, + { + "normalizedVendor": "zscaler", + "normalizedProduct": "zscaler client connector", + "displayNamePatterns": ["zscaler client connector", "zscaler", "zapp"], + "publisherPatterns": ["zscaler", "zscaler, inc."], + "cpeVendor": "zscaler", + "cpeProduct": "client_connector", + "cpeUri": "cpe:2.3:a:zscaler:client_connector", + "category": "vpn" + }, + { + "normalizedVendor": "pulsesecure", + "normalizedProduct": "pulse secure", + "displayNamePatterns": ["pulse secure", "ivanti secure access", "ivanti connect secure"], + "publisherPatterns": ["pulse secure", "pulse secure, llc", "ivanti"], + "cpeVendor": "pulsesecure", + "cpeProduct": "pulse_connect_secure", + "cpeUri": "cpe:2.3:a:pulsesecure:pulse_connect_secure", + "category": "vpn" + }, + { + "normalizedVendor": "openvpn", + "normalizedProduct": "openvpn", + "displayNamePatterns": ["openvpn", "openvpn connect"], + "publisherPatterns": ["openvpn", "openvpn inc.", "openvpn technologies"], + "cpeVendor": "openvpn", + "cpeProduct": "openvpn", + "cpeUri": "cpe:2.3:a:openvpn:openvpn", + "category": "vpn" + }, + { + "normalizedVendor": "citrix", + "normalizedProduct": "citrix workspace", + "displayNamePatterns": ["citrix workspace", "citrix workspace app"], + "publisherPatterns": ["citrix", "citrix systems", "citrix systems, inc."], + "cpeVendor": "citrix", + "cpeProduct": "workspace_app", + "cpeUri": "cpe:2.3:a:citrix:workspace_app", + "category": "vpn" + }, + { + "normalizedVendor": "7-zip", + "normalizedProduct": "7-zip", + "displayNamePatterns": ["7-zip", "7zip"], + "publisherPatterns": ["igor pavlov"], + "cpeVendor": "7-zip", + "cpeProduct": "7-zip", + "cpeUri": "cpe:2.3:a:7-zip:7-zip", + "category": "utility" + }, + { + "normalizedVendor": "rarlab", + "normalizedProduct": "winrar", + "displayNamePatterns": ["winrar"], + "publisherPatterns": ["alexander l. roshal", "win.rar gmbh", "rarlab"], + "cpeVendor": "rarlab", + "cpeProduct": "winrar", + "cpeUri": "cpe:2.3:a:rarlab:winrar", + "category": "utility" + }, + { + "normalizedVendor": "notepad++", + "normalizedProduct": "notepad++", + "displayNamePatterns": ["notepad++"], + "publisherPatterns": ["notepad++ team", "don ho"], + "cpeVendor": "notepad-plus-plus", + "cpeProduct": "notepad\\+\\+", + "cpeUri": "cpe:2.3:a:notepad-plus-plus:notepad\\+\\+", + "category": "utility" + }, + { + "normalizedVendor": "winscp", + "normalizedProduct": "winscp", + "displayNamePatterns": ["winscp"], + "publisherPatterns": ["martin prikryl"], + "cpeVendor": "winscp", + "cpeProduct": "winscp", + "cpeUri": "cpe:2.3:a:winscp:winscp", + "category": "utility" + }, + { + "normalizedVendor": "putty", + "normalizedProduct": "putty", + "displayNamePatterns": ["putty"], + "publisherPatterns": ["simon tatham"], + "cpeVendor": "putty", + "cpeProduct": "putty", + "cpeUri": "cpe:2.3:a:putty:putty", + "category": "utility" + }, + { + "normalizedVendor": "filezilla", + "normalizedProduct": "filezilla", + "displayNamePatterns": ["filezilla client", "filezilla"], + "publisherPatterns": ["tim kosse", "filezilla project"], + "cpeVendor": "filezilla-project", + "cpeProduct": "filezilla_client", + "cpeUri": "cpe:2.3:a:filezilla-project:filezilla_client", + "category": "utility" + }, + { + "normalizedVendor": "keepass", + "normalizedProduct": "keepass", + "displayNamePatterns": ["keepass", "keepass password safe"], + "publisherPatterns": ["dominik reichl"], + "cpeVendor": "keepass", + "cpeProduct": "keepass", + "cpeUri": "cpe:2.3:a:keepass:keepass", + "category": "utility" + }, + { + "normalizedVendor": "voidtools", + "normalizedProduct": "everything", + "displayNamePatterns": ["everything", "everything search"], + "publisherPatterns": ["voidtools"], + "cpeVendor": "voidtools", + "cpeProduct": "everything", + "cpeUri": "cpe:2.3:a:voidtools:everything", + "category": "utility" + }, + { + "normalizedVendor": "greenshot", + "normalizedProduct": "greenshot", + "displayNamePatterns": ["greenshot"], + "publisherPatterns": ["greenshot"], + "cpeVendor": "greenshot", + "cpeProduct": "greenshot", + "cpeUri": "cpe:2.3:a:greenshot:greenshot", + "category": "utility" + }, + { + "normalizedVendor": "dotpdn", + "normalizedProduct": "paint.net", + "displayNamePatterns": ["paint.net"], + "publisherPatterns": ["dotpdn llc", "rick brewster"], + "cpeVendor": "dotpdn", + "cpeProduct": "paint.net", + "cpeUri": "cpe:2.3:a:dotpdn:paint.net", + "category": "utility" + }, + { + "normalizedVendor": "videolan", + "normalizedProduct": "vlc media player", + "displayNamePatterns": ["vlc media player", "vlc"], + "publisherPatterns": ["videolan", "videolan project"], + "cpeVendor": "videolan", + "cpeProduct": "vlc_media_player", + "cpeUri": "cpe:2.3:a:videolan:vlc_media_player", + "category": "utility" + }, + { + "normalizedVendor": "audacity", + "normalizedProduct": "audacity", + "displayNamePatterns": ["audacity"], + "publisherPatterns": ["audacity team", "muse group"], + "cpeVendor": "audacityteam", + "cpeProduct": "audacity", + "cpeUri": "cpe:2.3:a:audacityteam:audacity", + "category": "utility" + }, + { + "normalizedVendor": "git", + "normalizedProduct": "git for windows", + "displayNamePatterns": ["git for windows", "git version"], + "publisherPatterns": ["the git development community", "johannes schindelin"], + "cpeVendor": "git-scm", + "cpeProduct": "git", + "cpeUri": "cpe:2.3:a:git-scm:git", + "category": "development" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "visual studio code", + "displayNamePatterns": ["microsoft visual studio code", "visual studio code", "vs code"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "visual_studio_code", + "cpeUri": "cpe:2.3:a:microsoft:visual_studio_code", + "category": "development" + }, + { + "normalizedVendor": "jetbrains", + "normalizedProduct": "intellij idea", + "displayNamePatterns": ["intellij idea", "jetbrains intellij"], + "publisherPatterns": ["jetbrains", "jetbrains s.r.o."], + "cpeVendor": "jetbrains", + "cpeProduct": "intellij_idea", + "cpeUri": "cpe:2.3:a:jetbrains:intellij_idea", + "category": "development" + }, + { + "normalizedVendor": "jetbrains", + "normalizedProduct": "pycharm", + "displayNamePatterns": ["pycharm", "jetbrains pycharm"], + "publisherPatterns": ["jetbrains", "jetbrains s.r.o."], + "cpeVendor": "jetbrains", + "cpeProduct": "pycharm", + "cpeUri": "cpe:2.3:a:jetbrains:pycharm", + "category": "development" + }, + { + "normalizedVendor": "jetbrains", + "normalizedProduct": "rider", + "displayNamePatterns": ["jetbrains rider", "rider"], + "publisherPatterns": ["jetbrains", "jetbrains s.r.o."], + "cpeVendor": "jetbrains", + "cpeProduct": "rider", + "cpeUri": "cpe:2.3:a:jetbrains:rider", + "category": "development" + }, + { + "normalizedVendor": "postman", + "normalizedProduct": "postman", + "displayNamePatterns": ["postman"], + "publisherPatterns": ["postman", "postman, inc."], + "cpeVendor": "postman", + "cpeProduct": "postman", + "cpeUri": "cpe:2.3:a:postman:postman", + "category": "development" + }, + { + "normalizedVendor": "docker", + "normalizedProduct": "docker desktop", + "displayNamePatterns": ["docker desktop"], + "publisherPatterns": ["docker", "docker inc.", "docker inc"], + "cpeVendor": "docker", + "cpeProduct": "docker_desktop", + "cpeUri": "cpe:2.3:a:docker:docker_desktop", + "category": "development" + }, + { + "normalizedVendor": "github", + "normalizedProduct": "github desktop", + "displayNamePatterns": ["github desktop"], + "publisherPatterns": ["github", "github, inc."], + "cpeVendor": "github", + "cpeProduct": "desktop", + "cpeUri": "cpe:2.3:a:github:desktop", + "category": "development" + }, + { + "normalizedVendor": "oracle", + "normalizedProduct": "mysql workbench", + "displayNamePatterns": ["mysql workbench"], + "publisherPatterns": ["oracle", "oracle corporation", "mysql ab"], + "cpeVendor": "oracle", + "cpeProduct": "mysql_workbench", + "cpeUri": "cpe:2.3:a:oracle:mysql_workbench", + "category": "database" + }, + { + "normalizedVendor": "postgresql", + "normalizedProduct": "pgadmin", + "displayNamePatterns": ["pgadmin 4", "pgadmin"], + "publisherPatterns": ["the pgadmin development team", "postgresql"], + "cpeVendor": "pgadmin", + "cpeProduct": "pgadmin_4", + "cpeUri": "cpe:2.3:a:pgadmin:pgadmin_4", + "category": "database" + }, + { + "normalizedVendor": "dbeaver", + "normalizedProduct": "dbeaver", + "displayNamePatterns": ["dbeaver", "dbeaver community", "dbeaver enterprise"], + "publisherPatterns": ["dbeaver corp", "dbeaver"], + "cpeVendor": "dbeaver", + "cpeProduct": "dbeaver", + "cpeUri": "cpe:2.3:a:dbeaver:dbeaver", + "category": "database" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "azure data studio", + "displayNamePatterns": ["azure data studio"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "azure_data_studio", + "cpeUri": "cpe:2.3:a:microsoft:azure_data_studio", + "category": "database" + }, + { + "normalizedVendor": "wireshark", + "normalizedProduct": "wireshark", + "displayNamePatterns": ["wireshark"], + "publisherPatterns": ["the wireshark developer community", "wireshark foundation"], + "cpeVendor": "wireshark", + "cpeProduct": "wireshark", + "cpeUri": "cpe:2.3:a:wireshark:wireshark", + "category": "networking" + }, + { + "normalizedVendor": "nmap", + "normalizedProduct": "nmap", + "displayNamePatterns": ["nmap", "npcap"], + "publisherPatterns": ["nmap project", "insecure.org"], + "cpeVendor": "nmap", + "cpeProduct": "nmap", + "cpeUri": "cpe:2.3:a:nmap:nmap", + "category": "networking" + }, + { + "normalizedVendor": "famatech", + "normalizedProduct": "advanced ip scanner", + "displayNamePatterns": ["advanced ip scanner"], + "publisherPatterns": ["famatech", "famatech corp."], + "cpeVendor": "famatech", + "cpeProduct": "advanced_ip_scanner", + "cpeUri": "cpe:2.3:a:famatech:advanced_ip_scanner", + "category": "networking" + }, + { + "normalizedVendor": "telerik", + "normalizedProduct": "fiddler", + "displayNamePatterns": ["telerik fiddler", "fiddler everywhere", "fiddler classic", "fiddler"], + "publisherPatterns": ["progress software", "telerik", "progress"], + "cpeVendor": "progress", + "cpeProduct": "telerik_fiddler_everywhere", + "cpeUri": "cpe:2.3:a:progress:telerik_fiddler_everywhere", + "category": "networking" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft 365 apps", + "displayNamePatterns": ["microsoft 365 apps", "microsoft office", "microsoft 365 - en-us", "microsoft office professional"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "365_apps", + "cpeUri": "cpe:2.3:a:microsoft:365_apps", + "category": "office" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft visio", + "displayNamePatterns": ["microsoft visio", "visio"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "visio", + "cpeUri": "cpe:2.3:a:microsoft:visio", + "category": "office" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft project", + "displayNamePatterns": ["microsoft project", "project professional", "project standard"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "project", + "cpeUri": "cpe:2.3:a:microsoft:project", + "category": "office" + }, + { + "normalizedVendor": "citrix", + "normalizedProduct": "citrix receiver", + "displayNamePatterns": ["citrix receiver", "citrix ica client"], + "publisherPatterns": ["citrix", "citrix systems", "citrix systems, inc."], + "cpeVendor": "citrix", + "cpeProduct": "receiver", + "cpeUri": "cpe:2.3:a:citrix:receiver", + "category": "office" + }, + { + "normalizedVendor": "vmware", + "normalizedProduct": "vmware horizon client", + "displayNamePatterns": ["vmware horizon client", "horizon client"], + "publisherPatterns": ["vmware", "vmware, inc."], + "cpeVendor": "vmware", + "cpeProduct": "horizon_client", + "cpeUri": "cpe:2.3:a:vmware:horizon_client", + "category": "office" + }, + { + "normalizedVendor": "veeam", + "normalizedProduct": "veeam agent", + "displayNamePatterns": ["veeam agent for microsoft windows", "veeam agent"], + "publisherPatterns": ["veeam", "veeam software"], + "cpeVendor": "veeam", + "cpeProduct": "agent_for_microsoft_windows", + "cpeUri": "cpe:2.3:a:veeam:agent_for_microsoft_windows", + "category": "archive_backup" + }, + { + "normalizedVendor": "acronis", + "normalizedProduct": "acronis cyber protect", + "displayNamePatterns": ["acronis cyber protect", "acronis true image", "acronis backup"], + "publisherPatterns": ["acronis", "acronis international gmbh"], + "cpeVendor": "acronis", + "cpeProduct": "cyber_protect", + "cpeUri": "cpe:2.3:a:acronis:cyber_protect", + "category": "archive_backup" + }, + { + "normalizedVendor": "corel", + "normalizedProduct": "winzip", + "displayNamePatterns": ["winzip"], + "publisherPatterns": ["corel corporation", "winzip computing"], + "cpeVendor": "winzip", + "cpeProduct": "winzip", + "cpeUri": "cpe:2.3:a:winzip:winzip", + "category": "archive_backup" + }, + { + "normalizedVendor": "sap", + "normalizedProduct": "sap gui", + "displayNamePatterns": ["sap gui for windows", "sap gui", "sapgui"], + "publisherPatterns": ["sap", "sap se", "sap ag"], + "cpeVendor": "sap", + "cpeProduct": "gui_for_windows", + "cpeUri": "cpe:2.3:a:sap:gui_for_windows", + "category": "enterprise" + }, + { + "normalizedVendor": "servicenow", + "normalizedProduct": "servicenow agent", + "displayNamePatterns": ["servicenow agent", "now agent", "servicenow mid server"], + "publisherPatterns": ["servicenow", "servicenow, inc."], + "cpeVendor": "servicenow", + "cpeProduct": "servicenow", + "cpeUri": "cpe:2.3:a:servicenow:servicenow", + "category": "enterprise" + }, + { + "normalizedVendor": "ivanti", + "normalizedProduct": "ivanti endpoint manager", + "displayNamePatterns": ["ivanti endpoint manager", "landesk management suite", "landesk agent", "ivanti epm"], + "publisherPatterns": ["ivanti", "ivanti, inc.", "landesk software"], + "cpeVendor": "ivanti", + "cpeProduct": "endpoint_manager", + "cpeUri": "cpe:2.3:a:ivanti:endpoint_manager", + "category": "enterprise" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "configuration manager client", + "displayNamePatterns": ["configuration manager client", "sccm client", "microsoft endpoint configuration manager"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "endpoint_configuration_manager", + "cpeUri": "cpe:2.3:a:microsoft:endpoint_configuration_manager", + "category": "enterprise" + }, + { + "normalizedVendor": "hcl", + "normalizedProduct": "bigfix", + "displayNamePatterns": ["bigfix client", "bigfix agent", "ibm bigfix"], + "publisherPatterns": ["hcl technologies", "hcl", "ibm"], + "cpeVendor": "hcltech", + "cpeProduct": "bigfix_platform", + "cpeUri": "cpe:2.3:a:hcltech:bigfix_platform", + "category": "enterprise" + }, + { + "normalizedVendor": "beyondtrust", + "normalizedProduct": "beyondtrust privilege management", + "displayNamePatterns": ["beyondtrust privilege management", "beyondtrust endpoint privilege management", "avecto defendpoint"], + "publisherPatterns": ["beyondtrust", "beyondtrust corporation", "avecto"], + "cpeVendor": "beyondtrust", + "cpeProduct": "privilege_management_for_windows", + "cpeUri": "cpe:2.3:a:beyondtrust:privilege_management_for_windows", + "category": "enterprise" + }, + { + "normalizedVendor": "delinea", + "normalizedProduct": "thycotic privilege manager", + "displayNamePatterns": ["thycotic privilege manager", "delinea privilege manager", "thycotic agent"], + "publisherPatterns": ["delinea", "thycotic", "thycotic software"], + "cpeVendor": "delinea", + "cpeProduct": "privilege_manager", + "cpeUri": "cpe:2.3:a:delinea:privilege_manager", + "category": "enterprise" + }, + { + "normalizedVendor": "hashicorp", + "normalizedProduct": "hashicorp vault", + "displayNamePatterns": ["vault", "hashicorp vault"], + "publisherPatterns": ["hashicorp", "hashicorp, inc."], + "cpeVendor": "hashicorp", + "cpeProduct": "vault", + "cpeUri": "cpe:2.3:a:hashicorp:vault", + "category": "enterprise" + }, + { + "normalizedVendor": "hashicorp", + "normalizedProduct": "terraform", + "displayNamePatterns": ["terraform", "hashicorp terraform"], + "publisherPatterns": ["hashicorp", "hashicorp, inc."], + "cpeVendor": "hashicorp", + "cpeProduct": "terraform", + "cpeUri": "cpe:2.3:a:hashicorp:terraform", + "category": "enterprise" + }, + { + "normalizedVendor": "redhat", + "normalizedProduct": "ansible", + "displayNamePatterns": ["ansible", "red hat ansible"], + "publisherPatterns": ["red hat", "red hat, inc.", "ansible"], + "cpeVendor": "redhat", + "cpeProduct": "ansible", + "cpeUri": "cpe:2.3:a:redhat:ansible", + "category": "enterprise" + }, + { + "normalizedVendor": "adobe", + "normalizedProduct": "adobe flash player", + "displayNamePatterns": ["adobe flash player"], + "publisherPatterns": ["adobe", "adobe systems", "adobe inc."], + "cpeVendor": "adobe", + "cpeProduct": "flash_player", + "cpeUri": "cpe:2.3:a:adobe:flash_player", + "category": "runtime" + }, + { + "normalizedVendor": "adobe", + "normalizedProduct": "adobe air", + "displayNamePatterns": ["adobe air"], + "publisherPatterns": ["adobe", "adobe systems", "adobe inc."], + "cpeVendor": "adobe", + "cpeProduct": "air", + "cpeUri": "cpe:2.3:a:adobe:air", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft silverlight", + "displayNamePatterns": ["microsoft silverlight", "silverlight"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "silverlight", + "cpeUri": "cpe:2.3:a:microsoft:silverlight", + "category": "runtime" + }, + { + "normalizedVendor": "apple", + "normalizedProduct": "itunes", + "displayNamePatterns": ["itunes", "apple itunes"], + "publisherPatterns": ["apple", "apple inc.", "apple inc"], + "cpeVendor": "apple", + "cpeProduct": "itunes", + "cpeUri": "cpe:2.3:a:apple:itunes", + "category": "utility" + }, + { + "normalizedVendor": "apple", + "normalizedProduct": "icloud", + "displayNamePatterns": ["icloud", "apple icloud"], + "publisherPatterns": ["apple", "apple inc.", "apple inc"], + "cpeVendor": "apple", + "cpeProduct": "icloud", + "cpeUri": "cpe:2.3:a:apple:icloud", + "category": "utility" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft onedrive", + "displayNamePatterns": ["microsoft onedrive", "onedrive"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "onedrive", + "cpeUri": "cpe:2.3:a:microsoft:onedrive", + "category": "utility" + }, + { + "normalizedVendor": "google", + "normalizedProduct": "google drive", + "displayNamePatterns": ["google drive", "drive for desktop"], + "publisherPatterns": ["google", "google llc", "google inc"], + "cpeVendor": "google", + "cpeProduct": "drive", + "cpeUri": "cpe:2.3:a:google:drive", + "category": "utility" + }, + { + "normalizedVendor": "dropbox", + "normalizedProduct": "dropbox", + "displayNamePatterns": ["dropbox"], + "publisherPatterns": ["dropbox", "dropbox, inc."], + "cpeVendor": "dropbox", + "cpeProduct": "dropbox", + "cpeUri": "cpe:2.3:a:dropbox:dropbox", + "category": "utility" + }, + { + "normalizedVendor": "gpg4win", + "normalizedProduct": "gpg4win", + "displayNamePatterns": ["gpg4win", "gnupg"], + "publisherPatterns": ["the gpg4win project"], + "cpeVendor": "gpg4win", + "cpeProduct": "gpg4win", + "cpeUri": "cpe:2.3:a:gpg4win:gpg4win", + "category": "security" + }, + { + "normalizedVendor": "bitwarden", + "normalizedProduct": "bitwarden", + "displayNamePatterns": ["bitwarden", "bitwarden desktop"], + "publisherPatterns": ["bitwarden inc.", "bitwarden"], + "cpeVendor": "bitwarden", + "cpeProduct": "desktop", + "cpeUri": "cpe:2.3:a:bitwarden:desktop", + "category": "security" + }, + { + "normalizedVendor": "1password", + "normalizedProduct": "1password", + "displayNamePatterns": ["1password"], + "publisherPatterns": ["agilebits", "agilebits inc.", "1password"], + "cpeVendor": "1password", + "cpeProduct": "1password", + "cpeUri": "cpe:2.3:a:1password:1password", + "category": "security" + }, + { + "normalizedVendor": "lastpass", + "normalizedProduct": "lastpass", + "displayNamePatterns": ["lastpass", "lastpass installer"], + "publisherPatterns": ["logmein", "logmein, inc.", "lastpass"], + "cpeVendor": "logmein", + "cpeProduct": "lastpass", + "cpeUri": "cpe:2.3:a:logmein:lastpass", + "category": "security" + }, + { + "normalizedVendor": "splashtop", + "normalizedProduct": "splashtop", + "displayNamePatterns": ["splashtop streamer", "splashtop business", "splashtop"], + "publisherPatterns": ["splashtop", "splashtop inc."], + "cpeVendor": "splashtop", + "cpeProduct": "splashtop", + "cpeUri": "cpe:2.3:a:splashtop:splashtop", + "category": "remote_access" + }, + { + "normalizedVendor": "logmein", + "normalizedProduct": "logmein", + "displayNamePatterns": ["logmein", "logmein client", "goto resolve"], + "publisherPatterns": ["logmein", "logmein, inc.", "goto"], + "cpeVendor": "logmein", + "cpeProduct": "logmein", + "cpeUri": "cpe:2.3:a:logmein:logmein", + "category": "remote_access" + }, + { + "normalizedVendor": "vmware", + "normalizedProduct": "vmware workstation", + "displayNamePatterns": ["vmware workstation"], + "publisherPatterns": ["vmware", "vmware, inc."], + "cpeVendor": "vmware", + "cpeProduct": "workstation", + "cpeUri": "cpe:2.3:a:vmware:workstation", + "category": "utility" + }, + { + "normalizedVendor": "oracle", + "normalizedProduct": "virtualbox", + "displayNamePatterns": ["oracle vm virtualbox", "virtualbox"], + "publisherPatterns": ["oracle", "oracle corporation"], + "cpeVendor": "oracle", + "cpeProduct": "vm_virtualbox", + "cpeUri": "cpe:2.3:a:oracle:vm_virtualbox", + "category": "utility" + }, + { + "normalizedVendor": "gimp", + "normalizedProduct": "gimp", + "displayNamePatterns": ["gimp"], + "publisherPatterns": ["the gimp team"], + "cpeVendor": "gimp", + "cpeProduct": "gimp", + "cpeUri": "cpe:2.3:a:gimp:gimp", + "category": "utility" + }, + { + "normalizedVendor": "inkscape", + "normalizedProduct": "inkscape", + "displayNamePatterns": ["inkscape"], + "publisherPatterns": ["inkscape project", "inkscape"], + "cpeVendor": "inkscape", + "cpeProduct": "inkscape", + "cpeUri": "cpe:2.3:a:inkscape:inkscape", + "category": "utility" + }, + { + "normalizedVendor": "obs", + "normalizedProduct": "obs studio", + "displayNamePatterns": ["obs studio", "obs"], + "publisherPatterns": ["obs project"], + "cpeVendor": "obsproject", + "cpeProduct": "obs_studio", + "cpeUri": "cpe:2.3:a:obsproject:obs_studio", + "category": "utility" + }, + { + "normalizedVendor": "irfanview", + "normalizedProduct": "irfanview", + "displayNamePatterns": ["irfanview"], + "publisherPatterns": ["irfan skiljan"], + "cpeVendor": "irfanview", + "cpeProduct": "irfanview", + "cpeUri": "cpe:2.3:a:irfanview:irfanview", + "category": "utility" + }, + { + "normalizedVendor": "curl", + "normalizedProduct": "curl", + "displayNamePatterns": ["curl"], + "publisherPatterns": ["daniel stenberg", "curl"], + "cpeVendor": "haxx", + "cpeProduct": "curl", + "cpeUri": "cpe:2.3:a:haxx:curl", + "category": "utility" + }, + { + "normalizedVendor": "openssl", + "normalizedProduct": "openssl", + "displayNamePatterns": ["openssl"], + "publisherPatterns": ["openssl project", "openssl software foundation"], + "cpeVendor": "openssl", + "cpeProduct": "openssl", + "cpeUri": "cpe:2.3:a:openssl:openssl", + "category": "runtime" + }, + { + "normalizedVendor": "npcap", + "normalizedProduct": "npcap", + "displayNamePatterns": ["npcap"], + "publisherPatterns": ["nmap project", "insecure.com"], + "cpeVendor": "npcap", + "cpeProduct": "npcap", + "cpeUri": "cpe:2.3:a:npcap:npcap", + "category": "networking" + }, + { + "normalizedVendor": "tortoisesvn", + "normalizedProduct": "tortoisesvn", + "displayNamePatterns": ["tortoisesvn"], + "publisherPatterns": ["the tortoisesvn team"], + "cpeVendor": "tortoisesvn", + "cpeProduct": "tortoisesvn", + "cpeUri": "cpe:2.3:a:tortoisesvn:tortoisesvn", + "category": "development" + }, + { + "normalizedVendor": "tortoisegit", + "normalizedProduct": "tortoisegit", + "displayNamePatterns": ["tortoisegit"], + "publisherPatterns": ["tortoisegit"], + "cpeVendor": "tortoisegit", + "cpeProduct": "tortoisegit", + "cpeUri": "cpe:2.3:a:tortoisegit:tortoisegit", + "category": "development" + }, + { + "normalizedVendor": "sublimehq", + "normalizedProduct": "sublime text", + "displayNamePatterns": ["sublime text", "sublime text 4", "sublime text 3"], + "publisherPatterns": ["sublime hq", "sublimehq"], + "cpeVendor": "sublimetext", + "cpeProduct": "sublime_text", + "cpeUri": "cpe:2.3:a:sublimetext:sublime_text", + "category": "development" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft sql server management studio", + "displayNamePatterns": ["sql server management studio", "ssms"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "sql_server_management_studio", + "cpeUri": "cpe:2.3:a:microsoft:sql_server_management_studio", + "category": "database" + }, + { + "normalizedVendor": "grafana", + "normalizedProduct": "grafana", + "displayNamePatterns": ["grafana"], + "publisherPatterns": ["grafana labs"], + "cpeVendor": "grafana", + "cpeProduct": "grafana", + "cpeUri": "cpe:2.3:a:grafana:grafana", + "category": "enterprise" + }, + { + "normalizedVendor": "elastic", + "normalizedProduct": "kibana", + "displayNamePatterns": ["kibana"], + "publisherPatterns": ["elastic", "elasticsearch"], + "cpeVendor": "elastic", + "cpeProduct": "kibana", + "cpeUri": "cpe:2.3:a:elastic:kibana", + "category": "enterprise" + }, + { + "normalizedVendor": "splunk", + "normalizedProduct": "splunk universal forwarder", + "displayNamePatterns": ["splunk universal forwarder", "splunkforwarder"], + "publisherPatterns": ["splunk", "splunk inc.", "splunk, inc."], + "cpeVendor": "splunk", + "cpeProduct": "universal_forwarder", + "cpeUri": "cpe:2.3:a:splunk:universal_forwarder", + "category": "enterprise" + }, + { + "normalizedVendor": "snagit", + "normalizedProduct": "snagit", + "displayNamePatterns": ["snagit", "techsmith snagit"], + "publisherPatterns": ["techsmith", "techsmith corporation"], + "cpeVendor": "techsmith", + "cpeProduct": "snagit", + "cpeUri": "cpe:2.3:a:techsmith:snagit", + "category": "utility" + }, + { + "normalizedVendor": "techsmith", + "normalizedProduct": "camtasia", + "displayNamePatterns": ["camtasia", "techsmith camtasia"], + "publisherPatterns": ["techsmith", "techsmith corporation"], + "cpeVendor": "techsmith", + "cpeProduct": "camtasia", + "cpeUri": "cpe:2.3:a:techsmith:camtasia", + "category": "utility" + }, + { + "normalizedVendor": "cisco", + "normalizedProduct": "cisco jabber", + "displayNamePatterns": ["cisco jabber", "jabber"], + "publisherPatterns": ["cisco", "cisco systems", "cisco systems, inc."], + "cpeVendor": "cisco", + "cpeProduct": "jabber", + "cpeUri": "cpe:2.3:a:cisco:jabber", + "category": "communication" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "skype for business", + "displayNamePatterns": ["skype for business", "lync"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "skype_for_business", + "cpeUri": "cpe:2.3:a:microsoft:skype_for_business", + "category": "communication" + }, + { + "normalizedVendor": "bluejeans", + "normalizedProduct": "bluejeans", + "displayNamePatterns": ["bluejeans"], + "publisherPatterns": ["bluejeans network", "verizon"], + "cpeVendor": "bluejeans", + "cpeProduct": "bluejeans", + "cpeUri": "cpe:2.3:a:bluejeans:bluejeans", + "category": "communication" + }, + { + "normalizedVendor": "gotomeeting", + "normalizedProduct": "gotomeeting", + "displayNamePatterns": ["gotomeeting", "goto meeting"], + "publisherPatterns": ["logmein", "logmein, inc.", "goto"], + "cpeVendor": "logmein", + "cpeProduct": "gotomeeting", + "cpeUri": "cpe:2.3:a:logmein:gotomeeting", + "category": "communication" + }, + { + "normalizedVendor": "sentinelone", + "normalizedProduct": "sentinelone", + "displayNamePatterns": ["sentinel agent", "sentinelone", "sentinelone agent"], + "publisherPatterns": ["sentinelone", "sentinel labs"], + "cpeVendor": "sentinelone", + "cpeProduct": "sentinelone", + "cpeUri": "cpe:2.3:a:sentinelone:sentinelone", + "category": "security" + }, + { + "normalizedVendor": "trendmicro", + "normalizedProduct": "trend micro apex one", + "displayNamePatterns": ["trend micro apex one", "trend micro officescan", "apex one"], + "publisherPatterns": ["trend micro", "trend micro inc."], + "cpeVendor": "trendmicro", + "cpeProduct": "apex_one", + "cpeUri": "cpe:2.3:a:trendmicro:apex_one", + "category": "security" + }, + { + "normalizedVendor": "symantec", + "normalizedProduct": "symantec endpoint protection", + "displayNamePatterns": ["symantec endpoint protection", "sep client", "broadcom endpoint protection"], + "publisherPatterns": ["symantec", "symantec corporation", "broadcom"], + "cpeVendor": "symantec", + "cpeProduct": "endpoint_protection", + "cpeUri": "cpe:2.3:a:symantec:endpoint_protection", + "category": "security" + }, + { + "normalizedVendor": "mcafee", + "normalizedProduct": "mcafee endpoint security", + "displayNamePatterns": ["mcafee endpoint security", "mcafee agent", "trellix endpoint security"], + "publisherPatterns": ["mcafee", "mcafee, inc.", "mcafee, llc", "trellix"], + "cpeVendor": "mcafee", + "cpeProduct": "endpoint_security", + "cpeUri": "cpe:2.3:a:mcafee:endpoint_security", + "category": "security" + }, + { + "normalizedVendor": "netskope", + "normalizedProduct": "netskope client", + "displayNamePatterns": ["netskope client", "netskope"], + "publisherPatterns": ["netskope", "netskope, inc."], + "cpeVendor": "netskope", + "cpeProduct": "netskope", + "cpeUri": "cpe:2.3:a:netskope:netskope", + "category": "vpn" + }, + { + "normalizedVendor": "wireguard", + "normalizedProduct": "wireguard", + "displayNamePatterns": ["wireguard"], + "publisherPatterns": ["wireguard", "jason a. donenfeld"], + "cpeVendor": "wireguard", + "cpeProduct": "wireguard", + "cpeUri": "cpe:2.3:a:wireguard:wireguard", + "category": "vpn" + }, + { + "normalizedVendor": "proton", + "normalizedProduct": "protonvpn", + "displayNamePatterns": ["protonvpn", "proton vpn"], + "publisherPatterns": ["proton ag", "proton technologies"], + "cpeVendor": "protonvpn", + "cpeProduct": "protonvpn", + "cpeUri": "cpe:2.3:a:protonvpn:protonvpn", + "category": "vpn" + }, + { + "normalizedVendor": "tailscale", + "normalizedProduct": "tailscale", + "displayNamePatterns": ["tailscale"], + "publisherPatterns": ["tailscale", "tailscale inc."], + "cpeVendor": "tailscale", + "cpeProduct": "tailscale", + "cpeUri": "cpe:2.3:a:tailscale:tailscale", + "category": "vpn" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft .net framework", + "displayNamePatterns": ["microsoft .net framework", ".net framework"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": ".net_framework", + "cpeUri": "cpe:2.3:a:microsoft:.net_framework", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "windows terminal", + "displayNamePatterns": ["windows terminal"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "windows_terminal", + "cpeUri": "cpe:2.3:a:microsoft:windows_terminal", + "category": "development" + }, + { + "normalizedVendor": "jetbrains", + "normalizedProduct": "webstorm", + "displayNamePatterns": ["webstorm", "jetbrains webstorm"], + "publisherPatterns": ["jetbrains", "jetbrains s.r.o."], + "cpeVendor": "jetbrains", + "cpeProduct": "webstorm", + "cpeUri": "cpe:2.3:a:jetbrains:webstorm", + "category": "development" + }, + { + "normalizedVendor": "jetbrains", + "normalizedProduct": "goland", + "displayNamePatterns": ["goland", "jetbrains goland"], + "publisherPatterns": ["jetbrains", "jetbrains s.r.o."], + "cpeVendor": "jetbrains", + "cpeProduct": "goland", + "cpeUri": "cpe:2.3:a:jetbrains:goland", + "category": "development" + }, + { + "normalizedVendor": "jetbrains", + "normalizedProduct": "datagrip", + "displayNamePatterns": ["datagrip", "jetbrains datagrip"], + "publisherPatterns": ["jetbrains", "jetbrains s.r.o."], + "cpeVendor": "jetbrains", + "cpeProduct": "datagrip", + "cpeUri": "cpe:2.3:a:jetbrains:datagrip", + "category": "database" + }, + { + "normalizedVendor": "github", + "normalizedProduct": "git lfs", + "displayNamePatterns": ["git lfs", "git large file storage"], + "publisherPatterns": ["github", "github, inc."], + "cpeVendor": "git_large_file_storage_project", + "cpeProduct": "git_large_file_storage", + "cpeUri": "cpe:2.3:a:git_large_file_storage_project:git_large_file_storage", + "category": "development" + }, + { + "normalizedVendor": "sap", + "normalizedProduct": "sap business client", + "displayNamePatterns": ["sap business client"], + "publisherPatterns": ["sap", "sap se", "sap ag"], + "cpeVendor": "sap", + "cpeProduct": "business_client", + "cpeUri": "cpe:2.3:a:sap:business_client", + "category": "enterprise" + }, + { + "normalizedVendor": "bmc", + "normalizedProduct": "bmc client management", + "displayNamePatterns": ["bmc client management", "bmc footprints"], + "publisherPatterns": ["bmc", "bmc software"], + "cpeVendor": "bmc", + "cpeProduct": "client_management", + "cpeUri": "cpe:2.3:a:bmc:client_management", + "category": "enterprise" + }, + { + "normalizedVendor": "qualys", + "normalizedProduct": "qualys cloud agent", + "displayNamePatterns": ["qualys cloud agent", "qualys agent"], + "publisherPatterns": ["qualys", "qualys, inc."], + "cpeVendor": "qualys", + "cpeProduct": "cloud_agent", + "cpeUri": "cpe:2.3:a:qualys:cloud_agent", + "category": "security" + }, + { + "normalizedVendor": "rapid7", + "normalizedProduct": "rapid7 insight agent", + "displayNamePatterns": ["rapid7 insight agent", "insight agent"], + "publisherPatterns": ["rapid7", "rapid7, inc.", "rapid7 llc"], + "cpeVendor": "rapid7", + "cpeProduct": "insight_agent", + "cpeUri": "cpe:2.3:a:rapid7:insight_agent", + "category": "security" + }, + { + "normalizedVendor": "tanium", + "normalizedProduct": "tanium client", + "displayNamePatterns": ["tanium client"], + "publisherPatterns": ["tanium", "tanium inc."], + "cpeVendor": "tanium", + "cpeProduct": "tanium_client", + "cpeUri": "cpe:2.3:a:tanium:tanium_client", + "category": "enterprise" + }, + { + "normalizedVendor": "flexera", + "normalizedProduct": "flexera agent", + "displayNamePatterns": ["flexnet inventory agent", "flexera agent", "managesoft"], + "publisherPatterns": ["flexera", "flexera software"], + "cpeVendor": "flexera", + "cpeProduct": "flexnet_manager", + "cpeUri": "cpe:2.3:a:flexera:flexnet_manager", + "category": "enterprise" + }, + { + "normalizedVendor": "jamf", + "normalizedProduct": "jamf", + "displayNamePatterns": ["jamf", "jamf connect"], + "publisherPatterns": ["jamf"], + "cpeVendor": "jamf", + "cpeProduct": "jamf", + "cpeUri": "cpe:2.3:a:jamf:jamf", + "category": "enterprise" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft intune management extension", + "displayNamePatterns": ["microsoft intune management extension", "intune management extension"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "intune_management_extension", + "cpeUri": "cpe:2.3:a:microsoft:intune_management_extension", + "category": "enterprise" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft sql server", + "displayNamePatterns": ["microsoft sql server", "sql server 2019", "sql server 2022"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "sql_server", + "cpeUri": "cpe:2.3:a:microsoft:sql_server", + "category": "database" + }, + { + "normalizedVendor": "postgresql", + "normalizedProduct": "postgresql", + "displayNamePatterns": ["postgresql"], + "publisherPatterns": ["postgresql global development group"], + "cpeVendor": "postgresql", + "cpeProduct": "postgresql", + "cpeUri": "cpe:2.3:a:postgresql:postgresql", + "category": "database" + }, + { + "normalizedVendor": "mariadb", + "normalizedProduct": "mariadb", + "displayNamePatterns": ["mariadb"], + "publisherPatterns": ["mariadb", "mariadb corporation", "mariadb foundation"], + "cpeVendor": "mariadb", + "cpeProduct": "mariadb", + "cpeUri": "cpe:2.3:a:mariadb:mariadb", + "category": "database" + }, + { + "normalizedVendor": "mongodb", + "normalizedProduct": "mongodb", + "displayNamePatterns": ["mongodb", "mongodb compass"], + "publisherPatterns": ["mongodb", "mongodb, inc."], + "cpeVendor": "mongodb", + "cpeProduct": "mongodb", + "cpeUri": "cpe:2.3:a:mongodb:mongodb", + "category": "database" + }, + { + "normalizedVendor": "redis", + "normalizedProduct": "redis", + "displayNamePatterns": ["redis"], + "publisherPatterns": ["redis", "redis ltd"], + "cpeVendor": "redis", + "cpeProduct": "redis", + "cpeUri": "cpe:2.3:a:redis:redis", + "category": "database" + }, + { + "normalizedVendor": "apache", + "normalizedProduct": "apache tomcat", + "displayNamePatterns": ["apache tomcat"], + "publisherPatterns": ["apache", "the apache software foundation"], + "cpeVendor": "apache", + "cpeProduct": "tomcat", + "cpeUri": "cpe:2.3:a:apache:tomcat", + "category": "runtime" + }, + { + "normalizedVendor": "nginx", + "normalizedProduct": "nginx", + "displayNamePatterns": ["nginx"], + "publisherPatterns": ["nginx", "f5", "f5 networks"], + "cpeVendor": "f5", + "cpeProduct": "nginx", + "cpeUri": "cpe:2.3:a:f5:nginx", + "category": "runtime" + }, + { + "normalizedVendor": "perforce", + "normalizedProduct": "p4v", + "displayNamePatterns": ["helix visual client", "p4v", "perforce helix"], + "publisherPatterns": ["perforce", "perforce software"], + "cpeVendor": "perforce", + "cpeProduct": "helix_core", + "cpeUri": "cpe:2.3:a:perforce:helix_core", + "category": "development" + }, + { + "normalizedVendor": "atlassian", + "normalizedProduct": "sourcetree", + "displayNamePatterns": ["sourcetree"], + "publisherPatterns": ["atlassian"], + "cpeVendor": "atlassian", + "cpeProduct": "sourcetree", + "cpeUri": "cpe:2.3:a:atlassian:sourcetree", + "category": "development" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft visual studio", + "displayNamePatterns": ["microsoft visual studio", "visual studio professional", "visual studio enterprise", "visual studio community"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "visual_studio", + "cpeUri": "cpe:2.3:a:microsoft:visual_studio", + "category": "development" + }, + { + "normalizedVendor": "github", + "normalizedProduct": "github cli", + "displayNamePatterns": ["github cli"], + "publisherPatterns": ["github", "github, inc."], + "cpeVendor": "github", + "cpeProduct": "cli", + "cpeUri": "cpe:2.3:a:github:cli", + "category": "development" + }, + { + "normalizedVendor": "kubernetes", + "normalizedProduct": "kubectl", + "displayNamePatterns": ["kubectl", "kubernetes cli"], + "publisherPatterns": ["kubernetes", "cloud native computing foundation"], + "cpeVendor": "kubernetes", + "cpeProduct": "kubectl", + "cpeUri": "cpe:2.3:a:kubernetes:kubectl", + "category": "development" + }, + { + "normalizedVendor": "helm", + "normalizedProduct": "helm", + "displayNamePatterns": ["helm"], + "publisherPatterns": ["helm", "cloud native computing foundation"], + "cpeVendor": "helm", + "cpeProduct": "helm", + "cpeUri": "cpe:2.3:a:helm:helm", + "category": "development" + }, + { + "normalizedVendor": "datadog", + "normalizedProduct": "datadog agent", + "displayNamePatterns": ["datadog agent"], + "publisherPatterns": ["datadog", "datadog, inc."], + "cpeVendor": "datadoghq", + "cpeProduct": "datadog_agent", + "cpeUri": "cpe:2.3:a:datadoghq:datadog_agent", + "category": "enterprise" + }, + { + "normalizedVendor": "dynatrace", + "normalizedProduct": "dynatrace oneagent", + "displayNamePatterns": ["dynatrace oneagent", "oneagent"], + "publisherPatterns": ["dynatrace", "dynatrace llc"], + "cpeVendor": "dynatrace", + "cpeProduct": "oneagent", + "cpeUri": "cpe:2.3:a:dynatrace:oneagent", + "category": "enterprise" + }, + { + "normalizedVendor": "newrelic", + "normalizedProduct": "new relic infrastructure agent", + "displayNamePatterns": ["new relic infrastructure agent", "newrelic agent"], + "publisherPatterns": ["new relic", "new relic, inc."], + "cpeVendor": "newrelic", + "cpeProduct": "infrastructure_agent", + "cpeUri": "cpe:2.3:a:newrelic:infrastructure_agent", + "category": "enterprise" + }, + { + "normalizedVendor": "puppet", + "normalizedProduct": "puppet agent", + "displayNamePatterns": ["puppet agent"], + "publisherPatterns": ["puppet", "puppet, inc."], + "cpeVendor": "puppet", + "cpeProduct": "puppet_agent", + "cpeUri": "cpe:2.3:a:puppet:puppet_agent", + "category": "enterprise" + }, + { + "normalizedVendor": "chef", + "normalizedProduct": "chef infra client", + "displayNamePatterns": ["chef infra client", "chef client"], + "publisherPatterns": ["chef", "chef software", "progress"], + "cpeVendor": "chef", + "cpeProduct": "chef_infra_client", + "cpeUri": "cpe:2.3:a:chef:chef_infra_client", + "category": "enterprise" + }, + { + "normalizedVendor": "saltstack", + "normalizedProduct": "salt minion", + "displayNamePatterns": ["salt minion", "salt"], + "publisherPatterns": ["saltstack", "vmware"], + "cpeVendor": "saltstack", + "cpeProduct": "salt", + "cpeUri": "cpe:2.3:a:saltstack:salt", + "category": "enterprise" + }, + { + "normalizedVendor": "proofpoint", + "normalizedProduct": "proofpoint endpoint dlp", + "displayNamePatterns": ["proofpoint endpoint dlp", "proofpoint agent"], + "publisherPatterns": ["proofpoint", "proofpoint, inc."], + "cpeVendor": "proofpoint", + "cpeProduct": "enterprise_dlp", + "cpeUri": "cpe:2.3:a:proofpoint:enterprise_dlp", + "category": "security" + }, + { + "normalizedVendor": "zabbix", + "normalizedProduct": "zabbix agent", + "displayNamePatterns": ["zabbix agent"], + "publisherPatterns": ["zabbix", "zabbix sla"], + "cpeVendor": "zabbix", + "cpeProduct": "zabbix_agent", + "cpeUri": "cpe:2.3:a:zabbix:zabbix_agent", + "category": "enterprise" + }, + { + "normalizedVendor": "nagios", + "normalizedProduct": "nagios agent", + "displayNamePatterns": ["nsclient++", "nscp", "nagios agent"], + "publisherPatterns": ["nagios", "nsclient"], + "cpeVendor": "nsclient", + "cpeProduct": "nsclient\\+\\+", + "cpeUri": "cpe:2.3:a:nsclient:nsclient\\+\\+", + "category": "enterprise" + }, + { + "normalizedVendor": "telerik", + "normalizedProduct": "telerik test studio", + "displayNamePatterns": ["telerik test studio", "test studio"], + "publisherPatterns": ["progress software", "telerik", "progress"], + "cpeVendor": "progress", + "cpeProduct": "telerik_test_studio", + "cpeUri": "cpe:2.3:a:progress:telerik_test_studio", + "category": "development" + }, + { + "normalizedVendor": "blackberry", + "normalizedProduct": "cylance protect", + "displayNamePatterns": ["cylanceprotect", "cylance protect", "blackberry protect"], + "publisherPatterns": ["blackberry", "cylance"], + "cpeVendor": "blackberry", + "cpeProduct": "cylanceprotect", + "cpeUri": "cpe:2.3:a:blackberry:cylanceprotect", + "category": "security" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft defender for endpoint", + "displayNamePatterns": ["microsoft defender for endpoint", "windows defender advanced threat protection"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "defender_for_endpoint", + "cpeUri": "cpe:2.3:a:microsoft:defender_for_endpoint", + "category": "security" + }, + { + "normalizedVendor": "paloaltonetworks", + "normalizedProduct": "cortex xdr agent", + "displayNamePatterns": ["cortex xdr", "palo alto cortex", "traps endpoint security"], + "publisherPatterns": ["palo alto networks", "paloaltonetworks"], + "cpeVendor": "paloaltonetworks", + "cpeProduct": "cortex_xdr_agent", + "cpeUri": "cpe:2.3:a:paloaltonetworks:cortex_xdr_agent", + "category": "security" + }, + { + "normalizedVendor": "cisco", + "normalizedProduct": "cisco amp for endpoints", + "displayNamePatterns": ["cisco amp for endpoints", "cisco secure endpoint", "amp for endpoints"], + "publisherPatterns": ["cisco", "cisco systems", "cisco systems, inc."], + "cpeVendor": "cisco", + "cpeProduct": "secure_endpoint", + "cpeUri": "cpe:2.3:a:cisco:secure_endpoint", + "category": "security" + }, + { + "normalizedVendor": "f-secure", + "normalizedProduct": "f-secure elements", + "displayNamePatterns": ["f-secure elements", "f-secure client security", "withsecure elements"], + "publisherPatterns": ["f-secure", "f-secure corporation", "withsecure"], + "cpeVendor": "f-secure", + "cpeProduct": "elements_endpoint_protection", + "cpeUri": "cpe:2.3:a:f-secure:elements_endpoint_protection", + "category": "security" + }, + { + "normalizedVendor": "bitdefender", + "normalizedProduct": "bitdefender endpoint security", + "displayNamePatterns": ["bitdefender endpoint security tools", "bitdefender gravityzone", "bitdefender"], + "publisherPatterns": ["bitdefender", "bitdefender srl"], + "cpeVendor": "bitdefender", + "cpeProduct": "endpoint_security_tools", + "cpeUri": "cpe:2.3:a:bitdefender:endpoint_security_tools", + "category": "security" + }, + { + "normalizedVendor": "avast", + "normalizedProduct": "avast business antivirus", + "displayNamePatterns": ["avast business antivirus", "avast business security"], + "publisherPatterns": ["avast software", "avast", "gen digital"], + "cpeVendor": "avast", + "cpeProduct": "antivirus", + "cpeUri": "cpe:2.3:a:avast:antivirus", + "category": "security" + }, + { + "normalizedVendor": "avg", + "normalizedProduct": "avg business antivirus", + "displayNamePatterns": ["avg business", "avg antivirus business"], + "publisherPatterns": ["avg technologies", "avg", "gen digital"], + "cpeVendor": "avg", + "cpeProduct": "anti-virus", + "cpeUri": "cpe:2.3:a:avg:anti-virus", + "category": "security" + }, + { + "normalizedVendor": "carbonite", + "normalizedProduct": "carbonite", + "displayNamePatterns": ["carbonite backup", "carbonite"], + "publisherPatterns": ["carbonite", "opentext"], + "cpeVendor": "carbonite", + "cpeProduct": "carbonite_server_backup", + "cpeUri": "cpe:2.3:a:carbonite:carbonite_server_backup", + "category": "archive_backup" + }, + { + "normalizedVendor": "veritas", + "normalizedProduct": "veritas backup exec", + "displayNamePatterns": ["veritas backup exec", "backup exec"], + "publisherPatterns": ["veritas", "veritas technologies"], + "cpeVendor": "veritas", + "cpeProduct": "backup_exec", + "cpeUri": "cpe:2.3:a:veritas:backup_exec", + "category": "archive_backup" + }, + { + "normalizedVendor": "commvault", + "normalizedProduct": "commvault", + "displayNamePatterns": ["commvault", "commvault simpana"], + "publisherPatterns": ["commvault", "commvault systems"], + "cpeVendor": "commvault", + "cpeProduct": "commcell", + "cpeUri": "cpe:2.3:a:commvault:commcell", + "category": "archive_backup" + }, + { + "normalizedVendor": "barracuda", + "normalizedProduct": "barracuda backup", + "displayNamePatterns": ["barracuda backup agent", "barracuda backup"], + "publisherPatterns": ["barracuda", "barracuda networks"], + "cpeVendor": "barracuda", + "cpeProduct": "backup", + "cpeUri": "cpe:2.3:a:barracuda:backup", + "category": "archive_backup" + }, + { + "normalizedVendor": "checkmarx", + "normalizedProduct": "checkmarx", + "displayNamePatterns": ["checkmarx"], + "publisherPatterns": ["checkmarx", "checkmarx ltd"], + "cpeVendor": "checkmarx", + "cpeProduct": "checkmarx", + "cpeUri": "cpe:2.3:a:checkmarx:checkmarx", + "category": "development" + }, + { + "normalizedVendor": "sonatype", + "normalizedProduct": "nexus repository manager", + "displayNamePatterns": ["nexus repository manager", "nexus iq"], + "publisherPatterns": ["sonatype", "sonatype, inc."], + "cpeVendor": "sonatype", + "cpeProduct": "nexus_repository_manager", + "cpeUri": "cpe:2.3:a:sonatype:nexus_repository_manager", + "category": "development" + }, + { + "normalizedVendor": "jfrog", + "normalizedProduct": "jfrog artifactory", + "displayNamePatterns": ["jfrog artifactory", "artifactory"], + "publisherPatterns": ["jfrog", "jfrog ltd"], + "cpeVendor": "jfrog", + "cpeProduct": "artifactory", + "cpeUri": "cpe:2.3:a:jfrog:artifactory", + "category": "development" + }, + { + "normalizedVendor": "rustlang", + "normalizedProduct": "rust", + "displayNamePatterns": ["rust", "rustup"], + "publisherPatterns": ["rust", "the rust project"], + "cpeVendor": "rust-lang", + "cpeProduct": "rust", + "cpeUri": "cpe:2.3:a:rust-lang:rust", + "category": "development" + }, + { + "normalizedVendor": "golang", + "normalizedProduct": "go", + "displayNamePatterns": ["go programming language", "golang"], + "publisherPatterns": ["google", "the go authors"], + "cpeVendor": "golang", + "cpeProduct": "go", + "cpeUri": "cpe:2.3:a:golang:go", + "category": "development" + }, + { + "normalizedVendor": "ruby", + "normalizedProduct": "ruby", + "displayNamePatterns": ["ruby", "rubyinstaller"], + "publisherPatterns": ["ruby", "ruby community"], + "cpeVendor": "ruby-lang", + "cpeProduct": "ruby", + "cpeUri": "cpe:2.3:a:ruby-lang:ruby", + "category": "runtime" + }, + { + "normalizedVendor": "php", + "normalizedProduct": "php", + "displayNamePatterns": ["php"], + "publisherPatterns": ["php group", "the php group"], + "cpeVendor": "php", + "cpeProduct": "php", + "cpeUri": "cpe:2.3:a:php:php", + "category": "runtime" + }, + { + "normalizedVendor": "perl", + "normalizedProduct": "perl", + "displayNamePatterns": ["strawberry perl", "activeperl", "perl"], + "publisherPatterns": ["perl", "activestate"], + "cpeVendor": "perl", + "cpeProduct": "perl", + "cpeUri": "cpe:2.3:a:perl:perl", + "category": "runtime" + }, + { + "normalizedVendor": "r-project", + "normalizedProduct": "r", + "displayNamePatterns": ["r for windows", "r-"], + "publisherPatterns": ["r core team", "r foundation"], + "cpeVendor": "r-project", + "cpeProduct": "r", + "cpeUri": "cpe:2.3:a:r-project:r", + "category": "runtime" + }, + { + "normalizedVendor": "miniconda", + "normalizedProduct": "anaconda", + "displayNamePatterns": ["anaconda3", "miniconda3", "anaconda", "miniconda"], + "publisherPatterns": ["anaconda", "anaconda, inc.", "continuum analytics"], + "cpeVendor": "anaconda", + "cpeProduct": "anaconda3", + "cpeUri": "cpe:2.3:a:anaconda:anaconda3", + "category": "runtime" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft power bi desktop", + "displayNamePatterns": ["microsoft power bi desktop", "power bi desktop"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "power_bi_desktop", + "cpeUri": "cpe:2.3:a:microsoft:power_bi_desktop", + "category": "office" + }, + { + "normalizedVendor": "tableau", + "normalizedProduct": "tableau desktop", + "displayNamePatterns": ["tableau desktop", "tableau"], + "publisherPatterns": ["tableau software", "salesforce", "tableau"], + "cpeVendor": "tableau", + "cpeProduct": "tableau_desktop", + "cpeUri": "cpe:2.3:a:tableau:tableau_desktop", + "category": "office" + }, + { + "normalizedVendor": "cisco", + "normalizedProduct": "cisco duo", + "displayNamePatterns": ["duo authentication", "cisco duo", "duo security"], + "publisherPatterns": ["cisco", "duo security", "cisco systems"], + "cpeVendor": "cisco", + "cpeProduct": "duo", + "cpeUri": "cpe:2.3:a:cisco:duo", + "category": "security" + }, + { + "normalizedVendor": "okta", + "normalizedProduct": "okta verify", + "displayNamePatterns": ["okta verify"], + "publisherPatterns": ["okta", "okta, inc."], + "cpeVendor": "okta", + "cpeProduct": "verify", + "cpeUri": "cpe:2.3:a:okta:verify", + "category": "security" + }, + { + "normalizedVendor": "yubico", + "normalizedProduct": "yubikey manager", + "displayNamePatterns": ["yubikey manager", "yubico authenticator"], + "publisherPatterns": ["yubico"], + "cpeVendor": "yubico", + "cpeProduct": "yubikey_manager", + "cpeUri": "cpe:2.3:a:yubico:yubikey_manager", + "category": "security" + }, + { + "normalizedVendor": "citrix", + "normalizedProduct": "citrix virtual apps and desktops", + "displayNamePatterns": ["citrix virtual apps", "xenapp", "xendesktop", "citrix virtual apps and desktops"], + "publisherPatterns": ["citrix", "citrix systems", "citrix systems, inc."], + "cpeVendor": "citrix", + "cpeProduct": "virtual_apps_and_desktops", + "cpeUri": "cpe:2.3:a:citrix:virtual_apps_and_desktops", + "category": "enterprise" + }, + { + "normalizedVendor": "dell", + "normalizedProduct": "dell command update", + "displayNamePatterns": ["dell command | update", "dell command update"], + "publisherPatterns": ["dell", "dell inc.", "dell technologies"], + "cpeVendor": "dell", + "cpeProduct": "command_update", + "cpeUri": "cpe:2.3:a:dell:command_update", + "category": "enterprise" + }, + { + "normalizedVendor": "hp", + "normalizedProduct": "hp client security manager", + "displayNamePatterns": ["hp client security manager", "hp security"], + "publisherPatterns": ["hp", "hp inc.", "hewlett-packard"], + "cpeVendor": "hp", + "cpeProduct": "client_security_manager", + "cpeUri": "cpe:2.3:a:hp:client_security_manager", + "category": "enterprise" + }, + { + "normalizedVendor": "lenovo", + "normalizedProduct": "lenovo system update", + "displayNamePatterns": ["lenovo system update", "system update"], + "publisherPatterns": ["lenovo", "lenovo group"], + "cpeVendor": "lenovo", + "cpeProduct": "system_update", + "cpeUri": "cpe:2.3:a:lenovo:system_update", + "category": "enterprise" + }, + { + "normalizedVendor": "logitech", + "normalizedProduct": "logitech options", + "displayNamePatterns": ["logi options+", "logitech options", "logi options"], + "publisherPatterns": ["logitech", "logitec"], + "cpeVendor": "logitech", + "cpeProduct": "options", + "cpeUri": "cpe:2.3:a:logitech:options", + "category": "utility" + }, + { + "normalizedVendor": "displaylink", + "normalizedProduct": "displaylink", + "displayNamePatterns": ["displaylink graphics driver", "displaylink"], + "publisherPatterns": ["displaylink", "synaptics"], + "cpeVendor": "displaylink", + "cpeProduct": "usb_graphics_software", + "cpeUri": "cpe:2.3:a:displaylink:usb_graphics_software", + "category": "utility" + }, + { + "normalizedVendor": "microsoft", + "normalizedProduct": "microsoft access database engine", + "displayNamePatterns": ["microsoft access database engine", "access database engine"], + "publisherPatterns": ["microsoft", "microsoft corporation"], + "cpeVendor": "microsoft", + "cpeProduct": "access", + "cpeUri": "cpe:2.3:a:microsoft:access", + "category": "database" + }, + { + "normalizedVendor": "xerox", + "normalizedProduct": "xerox print driver", + "displayNamePatterns": ["xerox print driver", "xerox global print"], + "publisherPatterns": ["xerox", "xerox corporation"], + "cpeVendor": "xerox", + "cpeProduct": "global_print_driver", + "cpeUri": "cpe:2.3:a:xerox:global_print_driver", + "category": "utility" + }, + { + "normalizedVendor": "schneider-electric", + "normalizedProduct": "schneider electric ecostruxure", + "displayNamePatterns": ["ecostruxure", "schneider electric"], + "publisherPatterns": ["schneider electric", "schneider-electric"], + "cpeVendor": "schneider-electric", + "cpeProduct": "ecostruxure_operator_terminal_expert", + "cpeUri": "cpe:2.3:a:schneider-electric:ecostruxure_operator_terminal_expert", + "category": "enterprise" + }, + { + "normalizedVendor": "siemens", + "normalizedProduct": "siemens sinec nms", + "displayNamePatterns": ["sinec nms"], + "publisherPatterns": ["siemens", "siemens ag"], + "cpeVendor": "siemens", + "cpeProduct": "sinec_nms", + "cpeUri": "cpe:2.3:a:siemens:sinec_nms", + "category": "enterprise" + } + ] +} diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs index b0ed2f4c..a7c9f652 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs @@ -136,7 +136,7 @@ namespace AutopilotMonitor.Functions.Functions.Config var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(new AgentConfigResponse { - ConfigVersion = 19, // AAD join with user overrides SkipUserStatusPage device-only classification + ConfigVersion = 20, // Software inventory analyzer UploadIntervalSeconds = 10, SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true, KeepLogFile = tenantConfig.KeepLogFile ?? false, @@ -159,7 +159,8 @@ namespace AutopilotMonitor.Functions.Functions.Config Analyzers = new AnalyzerConfiguration { EnableLocalAdminAnalyzer = tenantConfig.EnableLocalAdminAnalyzer ?? true, - LocalAdminAllowedAccounts = tenantConfig.GetLocalAdminAllowedAccounts() + LocalAdminAllowedAccounts = tenantConfig.GetLocalAdminAllowedAccounts(), + EnableSoftwareInventoryAnalyzer = tenantConfig.EnableSoftwareInventoryAnalyzer ?? true }, SendTraceEvents = tenantConfig.SendTraceEvents, UnrestrictedMode = tenantConfig.UnrestrictedModeEnabled && tenantConfig.UnrestrictedMode, diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.Processing.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.Processing.cs index 22c047a5..f9fbfe42 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.Processing.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.Processing.cs @@ -3,6 +3,7 @@ using System.Net; using AutopilotMonitor.Functions.Security; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Functions.Services.Notifications; +using AutopilotMonitor.Functions.Services.Vulnerability; using AutopilotMonitor.Shared.Models; using AutopilotMonitor.Shared.Models.Notifications; using Microsoft.Azure.Functions.Worker.Http; @@ -139,18 +140,22 @@ namespace AutopilotMonitor.Functions.Functions.Ingest await _storageService.StoreAppInstallSummaryAsync(summary.Summary); } - // Extract geo-location data and merge into session row + // Extract geo-location data and merge into session row (fire-and-forget — + // subsequent event batches arrive frequently so the UI sees it almost immediately) if (classification.DeviceLocationEvent?.Data != null) { var geoData = classification.DeviceLocationEvent.Data; - await _storageService.UpdateSessionGeoAsync( - request.TenantId, - request.SessionId, + var geoTenantId = request.TenantId; + var geoSessionId = request.SessionId; + _ = _storageService.UpdateSessionGeoAsync( + geoTenantId, + geoSessionId, geoData.ContainsKey("country") ? geoData["country"]?.ToString() : null, geoData.ContainsKey("region") ? geoData["region"]?.ToString() : null, geoData.ContainsKey("city") ? geoData["city"]?.ToString() : null, geoData.ContainsKey("loc") ? geoData["loc"]?.ToString() : null - ); + ).ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, + "Fire-and-forget UpdateSessionGeoAsync failed"), TaskContinuationOptions.OnlyOnFaulted); } // Update session status based on events @@ -170,37 +175,140 @@ namespace AutopilotMonitor.Functions.Functions.Ingest ); } - // Run full analysis when enrollment ends (cost-efficient: one pass over all events) + // Run full analysis when enrollment ends — fire-and-forget so the agent + // gets its HTTP 200 immediately. Rule results are stored in Table Storage and + // the UI picks them up on the next poll or via the existing "Re-Analyze" button. + // This avoids 500ms-10s+ blocking on the critical ingest path. var newRuleResults = new List(); if (classification.CompletionEvent != null || classification.FailureEvent != null) { - try + var ruleSessionId = request.SessionId; + var ruleTenantId = request.TenantId; + var rulePrefix = sessionPrefix; + _ = Task.Run(async () => { - var ruleEngine = new RuleEngine(_analyzeRuleService, _storageService, _logger); - newRuleResults = await ruleEngine.AnalyzeSessionAsync(request.TenantId, request.SessionId); - - foreach (var result in newRuleResults) + try { - await _storageService.StoreRuleResultAsync(result); + var ruleEngine = new RuleEngine(_analyzeRuleService, _storageService, _logger); + var results = await ruleEngine.AnalyzeSessionAsync(ruleTenantId, ruleSessionId); + + foreach (var result in results) + { + await _storageService.StoreRuleResultAsync(result); + } + + if (results.Count > 0) + { + _logger.LogInformation("{Prefix} Enrollment-end analysis (async): {Count} issue(s) detected", + rulePrefix, results.Count); + + // Push SignalR notification so the UI fetches results immediately + await _signalRNotification.NotifyRuleResultsAvailableAsync( + ruleTenantId, ruleSessionId, results.Count); + } + + // Increment platform stats for detected issues + if (results.Count > 0) + { + _ = _storageService.IncrementPlatformStatAsync("IssuesDetected", results.Count) + .ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, + "Fire-and-forget IncrementPlatformStatAsync failed"), TaskContinuationOptions.OnlyOnFaulted); + } } - - if (newRuleResults.Count > 0) + catch (Exception ruleEx) { - _logger.LogInformation($"{sessionPrefix} Enrollment-end analysis: {newRuleResults.Count} issue(s) detected"); + _logger.LogWarning(ruleEx, "{Prefix} Enrollment-end analysis failed (async, non-fatal)", rulePrefix); + } + }); + } + + // Vulnerability Correlation — fire-and-forget when shutdown inventory arrives. + // Runs asynchronously so it never blocks the HTTP response to the agent. + // The vulnerability_report event is stored in Table Storage; the UI picks it up + // on the next poll or page refresh (no SignalR broadcast for async results). + var shutdownInventoryDetected = storedEvents.Any(e => + e.EventType == AutopilotMonitor.Shared.Constants.EventTypes.SoftwareInventoryAnalysis && + e.Data != null && + e.Data.ContainsKey("triggered_at") && + e.Data["triggered_at"]?.ToString() == "shutdown" && + e.Data.ContainsKey("chunk_index") && + Convert.ToInt32(e.Data["chunk_index"]) == 0); + + if (shutdownInventoryDetected) + { + // Capture everything we need before going async (request object may be disposed) + var capturedSessionId = request.SessionId; + var capturedTenantId = request.TenantId; + var capturedPrefix = sessionPrefix; + + // Merge all inventory chunks from this batch into a self-contained list + var allInventoryItems = new List>(); + var inventoryChunks = storedEvents + .Where(e => e.EventType == AutopilotMonitor.Shared.Constants.EventTypes.SoftwareInventoryAnalysis && + e.Data != null && + e.Data.ContainsKey("triggered_at") && + e.Data["triggered_at"]?.ToString() == "shutdown" && + e.Data.ContainsKey("inventory")) + .OrderBy(e => Convert.ToInt32(e.Data.GetValueOrDefault("chunk_index", 0))) + .ToList(); + + foreach (var chunk in inventoryChunks) + { + if (chunk.Data["inventory"] is System.Collections.IEnumerable items) + { + foreach (var item in items) + { + if (item is Dictionary dict) + allInventoryItems.Add(dict); + } } } - catch (Exception ruleEx) + + if (allInventoryItems.Count > 0) { - _logger.LogWarning(ruleEx, $"{sessionPrefix} Enrollment-end analysis failed (non-fatal)"); + _ = Task.Run(async () => + { + try + { + var adminConfig = await _adminConfigService.GetConfigurationAsync(); + if (adminConfig?.VulnerabilityCorrelationEnabled != true) + return; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var vulnReport = await _vulnerabilityCorrelation.CorrelateAsync( + capturedSessionId, capturedTenantId, allInventoryItems, cts.Token); + + if (vulnReport != null) + { + vulnReport.SessionId = capturedSessionId; + vulnReport.TenantId = capturedTenantId; + vulnReport.ReceivedAt = DateTime.UtcNow; + await _storageService.StoreEventsBatchAsync(new List { vulnReport }); + _logger.LogInformation("{Prefix} Vulnerability correlation complete (async): {Message}", + capturedPrefix, vulnReport.Message); + + // Push SignalR notification so the UI fetches the report immediately + var overallRisk = vulnReport.Data?.ContainsKey("scan_summary") == true + && vulnReport.Data["scan_summary"] is Dictionary summary + && summary.ContainsKey("overall_risk") + ? summary["overall_risk"]?.ToString() ?? "unknown" + : "unknown"; + await _signalRNotification.NotifyVulnerabilityReportAvailableAsync( + capturedTenantId, capturedSessionId, overallRisk); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "{Prefix} Vulnerability correlation failed (async, non-fatal)", capturedPrefix); + } + }); } } // Increment platform stats (fire-and-forget, non-blocking) + // Note: IssuesDetected is now incremented inside the async rule engine task above. _ = _storageService.IncrementPlatformStatAsync("TotalEventsProcessed", processedCount) .ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, "Fire-and-forget IncrementPlatformStatAsync failed"), TaskContinuationOptions.OnlyOnFaulted); - if (newRuleResults.Count > 0) - _ = _storageService.IncrementPlatformStatAsync("IssuesDetected", newRuleResults.Count) - .ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, "Fire-and-forget IncrementPlatformStatAsync failed"), TaskContinuationOptions.OnlyOnFaulted); if (classification.CompletionEvent != null) _ = _storageService.IncrementPlatformStatAsync("SuccessfulEnrollments") .ContinueWith(t => _logger.LogWarning(t.Exception?.InnerException, "Fire-and-forget IncrementPlatformStatAsync failed"), TaskContinuationOptions.OnlyOnFaulted); diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.cs index cc9236ed..c1575685 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/IngestEventsFunction.cs @@ -2,6 +2,7 @@ using System.Net; using AutopilotMonitor.Functions.Security; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Functions.Services.Notifications; +using AutopilotMonitor.Functions.Services.Vulnerability; using AutopilotMonitor.Shared.Models; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; @@ -23,6 +24,9 @@ namespace AutopilotMonitor.Functions.Functions.Ingest private readonly BlockedDeviceService _blockedDeviceService; private readonly BlockedVersionService _blockedVersionService; private readonly BootstrapSessionService _bootstrapSessionService; + private readonly VulnerabilityCorrelationService _vulnerabilityCorrelation; + private readonly AdminConfigurationService _adminConfigService; + private readonly SignalRNotificationService _signalRNotification; public IngestEventsFunction( ILogger logger, @@ -35,7 +39,10 @@ namespace AutopilotMonitor.Functions.Functions.Ingest WebhookNotificationService webhookNotificationService, BlockedDeviceService blockedDeviceService, BlockedVersionService blockedVersionService, - BootstrapSessionService bootstrapSessionService) + BootstrapSessionService bootstrapSessionService, + VulnerabilityCorrelationService vulnerabilityCorrelation, + AdminConfigurationService adminConfigService, + SignalRNotificationService signalRNotification) { _logger = logger; _storageService = storageService; @@ -48,6 +55,9 @@ namespace AutopilotMonitor.Functions.Functions.Ingest _blockedDeviceService = blockedDeviceService; _blockedVersionService = blockedVersionService; _bootstrapSessionService = bootstrapSessionService; + _vulnerabilityCorrelation = vulnerabilityCorrelation; + _adminConfigService = adminConfigService; + _signalRNotification = signalRNotification; } [Function("IngestEvents")] diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/GetUnmatchedSoftwareFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/GetUnmatchedSoftwareFunction.cs new file mode 100644 index 00000000..8b7cef09 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/GetUnmatchedSoftwareFunction.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using AutopilotMonitor.Functions.Security; +using AutopilotMonitor.Functions.Services; +using AutopilotMonitor.Shared; +using AutopilotMonitor.Shared.Models; +using Azure.Data.Tables; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Functions.Vulnerability +{ + /// + /// Admin endpoint that aggregates frequently unmatched software across all sessions. + /// Used by the /update-cpe-seed skill and Admin UI to identify gaps in the CPE mapping. + /// + public class GetUnmatchedSoftwareFunction + { + private readonly ILogger _logger; + private readonly TableStorageService _storageService; + private readonly GalacticAdminService _galacticAdminService; + + public GetUnmatchedSoftwareFunction( + ILogger logger, + TableStorageService storageService, + GalacticAdminService galacticAdminService) + { + _logger = logger; + _storageService = storageService; + _galacticAdminService = galacticAdminService; + } + + [Function("GetUnmatchedSoftware")] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "admin/vulnerability/unmatched-software")] HttpRequestData req) + { + try + { + // Galactic Admin only + var authHeader = req.Headers.Contains("Authorization") + ? req.Headers.GetValues("Authorization").FirstOrDefault() + : null; + if (string.IsNullOrEmpty(authHeader)) + { + var unauthorized = req.CreateResponse(HttpStatusCode.Unauthorized); + return unauthorized; + } + + // Scan recent vulnerability_report events for unmatched_software lists + var unmatchedAgg = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var tableClient = _storageService.GetTableClient(Constants.TableNames.Events); + var recentEvents = tableClient.QueryAsync( + filter: $"EventType eq 'vulnerability_report'", + maxPerPage: 200); + + await foreach (var entity in recentEvents) + { + var dataJson = entity.GetString("Data"); + if (string.IsNullOrEmpty(dataJson)) continue; + + try + { + var data = JsonSerializer.Deserialize>(dataJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (data == null || !data.ContainsKey("unmatched_software")) continue; + + var unmatchedJson = data["unmatched_software"]?.ToString(); + if (string.IsNullOrEmpty(unmatchedJson)) continue; + + var unmatchedList = JsonSerializer.Deserialize>>(unmatchedJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (unmatchedList == null) continue; + + foreach (var item in unmatchedList) + { + var softwareName = item.GetValueOrDefault("softwareName", ""); + if (string.IsNullOrWhiteSpace(softwareName)) continue; + + var key = softwareName.ToLowerInvariant(); + if (!unmatchedAgg.ContainsKey(key)) + { + unmatchedAgg[key] = new UnmatchedSoftwareAgg + { + SoftwareName = softwareName, + Publisher = item.GetValueOrDefault("publisher", ""), + Frequency = 0, + LastSeenAt = entity.GetDateTimeOffset("Timestamp")?.UtcDateTime ?? DateTime.UtcNow, + ExampleSessionId = entity.PartitionKey?.Split('_').LastOrDefault() ?? "" + }; + } + + unmatchedAgg[key].Frequency++; + var ts = entity.GetDateTimeOffset("Timestamp")?.UtcDateTime ?? DateTime.MinValue; + if (ts > unmatchedAgg[key].LastSeenAt) + { + unmatchedAgg[key].LastSeenAt = ts; + unmatchedAgg[key].ExampleSessionId = entity.PartitionKey?.Split('_').LastOrDefault() ?? ""; + } + } + } + catch { /* skip malformed events */ } + } + + var result = unmatchedAgg.Values + .OrderByDescending(x => x.Frequency) + .Take(100) + .ToList(); + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(new { software = result, total = result.Count }); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get unmatched software report"); + var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); + await errorResponse.WriteStringAsync("Internal server error"); + return errorResponse; + } + } + + private class UnmatchedSoftwareAgg + { + public string SoftwareName { get; set; } + public string Publisher { get; set; } + public int Frequency { get; set; } + public DateTime LastSeenAt { get; set; } + public string ExampleSessionId { get; set; } + } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs new file mode 100644 index 00000000..93bc180e --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using AutopilotMonitor.Functions.Services; +using AutopilotMonitor.Functions.Services.Vulnerability; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Functions.Vulnerability +{ + /// + /// Timer-triggered function that refreshes vulnerability data caches daily. + /// - CISA KEV catalog (full refresh) + /// - Stale NVD CVE cache entries (older than 7 days) + /// - MSRC security updates (latest months) + /// + public class VulnerabilityDataSyncFunction + { + private readonly KevDataService _kevService; + private readonly AdminConfigurationService _adminConfigService; + private readonly ILogger _logger; + + public VulnerabilityDataSyncFunction( + KevDataService kevService, + AdminConfigurationService adminConfigService, + ILogger logger) + { + _kevService = kevService; + _adminConfigService = adminConfigService; + _logger = logger; + } + + /// + /// Runs daily at 03:00 UTC to refresh vulnerability data caches. + /// + [Function("VulnerabilityDataSync")] + public async Task Run( + [TimerTrigger("0 0 3 * * *")] TimerInfo timer) + { + _logger.LogInformation("Starting vulnerability data sync..."); + + try + { + // 1. Refresh CISA KEV catalog (small JSON, fast) + await _kevService.RefreshAsync(); + _logger.LogInformation("KEV catalog refreshed: {Count} entries", _kevService.CatalogSize); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "KEV catalog refresh failed"); + } + + // 2. Update sync timestamp in AdminConfiguration + try + { + var config = await _adminConfigService.GetConfigurationAsync(); + if (config != null) + { + config.VulnerabilityDataLastSyncUtc = DateTime.UtcNow.ToString("o"); + await _adminConfigService.SaveConfigurationAsync(config); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update vulnerability sync timestamp"); + } + + _logger.LogInformation("Vulnerability data sync completed"); + } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs index c19c8e4e..42028057 100644 --- a/src/Backend/AutopilotMonitor.Functions/Program.cs +++ b/src/Backend/AutopilotMonitor.Functions/Program.cs @@ -118,6 +118,15 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// Programmatic SignalR push for background tasks (rule engine, vulnerability correlation) +builder.Services.AddSingleton(); + +// Vulnerability correlation services +builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); +builder.Services.AddHttpClient(); +builder.Services.AddSingleton(); + // Register agent Function classes so bootstrap wrappers can inject them for code reuse builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/AdminConfigurationService.cs b/src/Backend/AutopilotMonitor.Functions/Services/AdminConfigurationService.cs index 7ad9df31..8a88bb85 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/AdminConfigurationService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/AdminConfigurationService.cs @@ -185,7 +185,10 @@ namespace AutopilotMonitor.Functions.Services { "CollectorIdleTimeoutMinutes", config.CollectorIdleTimeoutMinutes }, { "MaxSessionWindowHours", config.MaxSessionWindowHours }, { "MaintenanceBlockDurationHours", config.MaintenanceBlockDurationHours }, - { "DiagnosticsGlobalLogPathsJson", config.DiagnosticsGlobalLogPathsJson } + { "DiagnosticsGlobalLogPathsJson", config.DiagnosticsGlobalLogPathsJson }, + { "NvdApiKey", config.NvdApiKey }, + { "VulnerabilityCorrelationEnabled", config.VulnerabilityCorrelationEnabled }, + { "VulnerabilityDataLastSyncUtc", config.VulnerabilityDataLastSyncUtc } }; return entity; @@ -204,7 +207,10 @@ namespace AutopilotMonitor.Functions.Services CollectorIdleTimeoutMinutes = entity.GetInt32("CollectorIdleTimeoutMinutes") ?? 15, MaxSessionWindowHours = entity.GetInt32("MaxSessionWindowHours") ?? 24, MaintenanceBlockDurationHours = entity.GetInt32("MaintenanceBlockDurationHours") ?? 12, - DiagnosticsGlobalLogPathsJson = entity.GetString("DiagnosticsGlobalLogPathsJson") + DiagnosticsGlobalLogPathsJson = entity.GetString("DiagnosticsGlobalLogPathsJson"), + NvdApiKey = entity.GetString("NvdApiKey"), + VulnerabilityCorrelationEnabled = entity.GetBoolean("VulnerabilityCorrelationEnabled") ?? true, + VulnerabilityDataLastSyncUtc = entity.GetString("VulnerabilityDataLastSyncUtc") }; } } diff --git a/src/Backend/AutopilotMonitor.Functions/Services/SignalRNotificationService.cs b/src/Backend/AutopilotMonitor.Functions/Services/SignalRNotificationService.cs new file mode 100644 index 00000000..5485ac0c --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/SignalRNotificationService.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Azure.SignalR.Management; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Services +{ + /// + /// Programmatic SignalR message sender for background tasks that run outside + /// of Azure Function output bindings (e.g., async rule engine, vulnerability correlation). + /// Uses the Azure SignalR Management SDK to send messages to connected clients. + /// + public class SignalRNotificationService : IDisposable + { + private readonly ILogger _logger; + private readonly ServiceManager _serviceManager; + private IServiceHubContext? _hubContext; + private static readonly SemaphoreSlim _initLock = new(1, 1); + + private const string HubName = "autopilotmonitor"; + + public SignalRNotificationService(IConfiguration configuration, ILogger logger) + { + _logger = logger; + var connectionString = configuration["AzureSignalRConnectionString"] + ?? throw new InvalidOperationException("AzureSignalRConnectionString is not configured"); + + _serviceManager = new ServiceManagerBuilder() + .WithOptions(o => + { + o.ConnectionString = connectionString; + o.ServiceTransportType = ServiceTransportType.Persistent; + }) + .BuildServiceManager(); + } + + /// + /// Notify a session's connected clients that new rule analysis results are available. + /// Sends to the session-specific SignalR group so only the relevant session detail page reacts. + /// + public async Task NotifyRuleResultsAvailableAsync(string tenantId, string sessionId, int resultCount) + { + try + { + var hub = await GetHubContextAsync(); + var groupName = $"session-{tenantId}-{sessionId}"; + + await hub.Clients.Group(groupName).SendCoreAsync("ruleResultsReady", new object[] + { + new + { + sessionId, + tenantId, + ruleResultCount = resultCount, + timestamp = DateTime.UtcNow.ToString("o") + } + }); + + _logger.LogDebug("Sent ruleResultsReady to group {Group} ({Count} results)", groupName, resultCount); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send ruleResultsReady notification for session {SessionId}", sessionId); + } + } + + /// + /// Notify a session's connected clients that vulnerability correlation results are available. + /// + public async Task NotifyVulnerabilityReportAvailableAsync(string tenantId, string sessionId, string overallRisk) + { + try + { + var hub = await GetHubContextAsync(); + var groupName = $"session-{tenantId}-{sessionId}"; + + await hub.Clients.Group(groupName).SendCoreAsync("vulnerabilityReportReady", new object[] + { + new + { + sessionId, + tenantId, + overallRisk, + timestamp = DateTime.UtcNow.ToString("o") + } + }); + + _logger.LogDebug("Sent vulnerabilityReportReady to group {Group} (risk={Risk})", groupName, overallRisk); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send vulnerabilityReportReady notification for session {SessionId}", sessionId); + } + } + + private async Task GetHubContextAsync() + { + if (_hubContext != null) return _hubContext; + + await _initLock.WaitAsync(); + try + { + _hubContext ??= await _serviceManager.CreateHubContextAsync(HubName, default); + return _hubContext; + } + finally + { + _initLock.Release(); + } + } + + public void Dispose() + { + (_hubContext as IDisposable)?.Dispose(); + _serviceManager?.Dispose(); + } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs index 250b30da..403dd1cb 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.cs @@ -33,6 +33,15 @@ namespace AutopilotMonitor.Functions.Services _tableServiceClient = new TableServiceClient(connectionString); } + /// + /// Returns a TableClient for the specified table name. + /// Used by services that need direct table access (e.g. VulnerabilityCorrelationService). + /// + public TableClient GetTableClient(string tableName) + { + return _tableServiceClient.GetTableClient(tableName); + } + /// /// Initializes all Azure Table Storage tables. /// This method is idempotent and safe to call multiple times. diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TenantConfigurationService.cs b/src/Backend/AutopilotMonitor.Functions/Services/TenantConfigurationService.cs index 2ea1412d..923cb6d8 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TenantConfigurationService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TenantConfigurationService.cs @@ -239,6 +239,7 @@ namespace AutopilotMonitor.Functions.Services { "AgentMaxLifetimeMinutes", config.AgentMaxLifetimeMinutes }, { "SendTraceEvents", config.SendTraceEvents }, { "EnableLocalAdminAnalyzer", config.EnableLocalAdminAnalyzer }, + { "EnableSoftwareInventoryAnalyzer", config.EnableSoftwareInventoryAnalyzer }, { "LocalAdminAllowedAccountsJson", config.LocalAdminAllowedAccountsJson }, { "BootstrapTokenEnabled", config.BootstrapTokenEnabled }, { "UnrestrictedModeEnabled", config.UnrestrictedModeEnabled }, @@ -301,6 +302,7 @@ namespace AutopilotMonitor.Functions.Services AgentMaxLifetimeMinutes = entity.GetInt32("AgentMaxLifetimeMinutes"), SendTraceEvents = entity.GetBoolean("SendTraceEvents") ?? true, EnableLocalAdminAnalyzer = entity.GetBoolean("EnableLocalAdminAnalyzer"), + EnableSoftwareInventoryAnalyzer = entity.GetBoolean("EnableSoftwareInventoryAnalyzer"), LocalAdminAllowedAccountsJson = entity.GetString("LocalAdminAllowedAccountsJson"), BootstrapTokenEnabled = entity.GetBoolean("BootstrapTokenEnabled") ?? false, UnrestrictedModeEnabled = entity.GetBoolean("UnrestrictedModeEnabled") ?? false, diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/KevDataService.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/KevDataService.cs new file mode 100644 index 00000000..b5413213 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/KevDataService.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Services.Vulnerability +{ + /// + /// Downloads and caches the CISA Known Exploited Vulnerabilities (KEV) catalog. + /// The catalog is ~500 KB JSON and is held entirely in memory for fast lookup. + /// Auto-refreshes when the cache is older than 24 hours. + /// + public class KevDataService + { + private readonly HttpClient _http; + private readonly ILogger _logger; + + private Dictionary _kevByCveId = new Dictionary(StringComparer.OrdinalIgnoreCase); + private DateTime _lastRefresh = DateTime.MinValue; + private static readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1); + + private const string KevUrl = "https://www.cisa.gov/sites/default/files/feeds/known-exploited-vulnerabilities.json"; + private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(24); + + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + public KevDataService(HttpClient http, ILogger logger) + { + _http = http ?? throw new ArgumentNullException(nameof(http)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _http.Timeout = TimeSpan.FromSeconds(60); + } + + /// + /// Check if a CVE is in the KEV catalog. Auto-refreshes if cache is stale. + /// Returns the KevEntry if found, null otherwise. + /// + public async Task CheckKevAsync(string cveId) + { + await EnsureCacheAsync(); + + if (string.IsNullOrWhiteSpace(cveId)) + return null; + + _kevByCveId.TryGetValue(cveId, out var entry); + return entry; + } + + /// + /// Get the total number of entries in the cached KEV catalog. + /// + public int CatalogSize => _kevByCveId.Count; + + /// + /// Last time the KEV catalog was successfully refreshed. + /// + public DateTime LastRefresh => _lastRefresh; + + /// + /// Force refresh the KEV catalog from CISA. + /// + public async Task RefreshAsync() + { + await _refreshLock.WaitAsync(); + try + { + _logger.LogInformation("Refreshing CISA KEV catalog from {Url}", KevUrl); + + using var request = new HttpRequestMessage(HttpMethod.Get, KevUrl); + request.Headers.Add("User-Agent", "AutopilotMonitor/1.0"); + + var response = await _http.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var catalog = JsonSerializer.Deserialize(json, JsonOptions); + + if (catalog?.Vulnerabilities == null || catalog.Vulnerabilities.Count == 0) + { + _logger.LogWarning("KEV catalog returned empty or null vulnerabilities list"); + return; + } + + var newIndex = new Dictionary(catalog.Vulnerabilities.Count, StringComparer.OrdinalIgnoreCase); + foreach (var vuln in catalog.Vulnerabilities) + { + if (!string.IsNullOrWhiteSpace(vuln.CveID)) + { + newIndex[vuln.CveID] = vuln; + } + } + + _kevByCveId = newIndex; + _lastRefresh = DateTime.UtcNow; + + _logger.LogInformation("KEV catalog refreshed: {Count} entries, catalog version {Version}", + newIndex.Count, catalog.CatalogVersion ?? "unknown"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to refresh KEV catalog"); + } + finally + { + _refreshLock.Release(); + } + } + + private async Task EnsureCacheAsync() + { + if (DateTime.UtcNow - _lastRefresh > CacheTtl) + { + await RefreshAsync(); + } + } + } + + // ----------------------------------------------------------------------- + // KEV Catalog Models + // ----------------------------------------------------------------------- + + public class KevCatalog + { + public string Title { get; set; } + public string CatalogVersion { get; set; } + public string DateReleased { get; set; } + public int Count { get; set; } + public List Vulnerabilities { get; set; } = new List(); + } + + public class KevEntry + { + public string CveID { get; set; } + public string VendorProject { get; set; } + public string Product { get; set; } + public string VulnerabilityName { get; set; } + public string DateAdded { get; set; } + public string ShortDescription { get; set; } + public string RequiredAction { get; set; } + public string DueDate { get; set; } + public string KnownRansomwareCampaignUse { get; set; } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcApiClient.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcApiClient.cs new file mode 100644 index 00000000..4d131425 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcApiClient.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Services.Vulnerability +{ + /// + /// Client for the MSRC Security Update Guide API. + /// Provides Microsoft-specific vulnerability data (CVEs for Office, Edge, Windows components, etc.). + /// + public class MsrcApiClient + { + private readonly HttpClient _http; + private readonly ILogger _logger; + + private const string MsrcBaseUrl = "https://api.msrc.microsoft.com"; + + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public MsrcApiClient(HttpClient http, ILogger logger) + { + _http = http ?? throw new ArgumentNullException(nameof(http)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _http.Timeout = TimeSpan.FromSeconds(30); + } + + /// + /// Get the list of recent MSRC security updates (last 6 months). + /// Each update represents a monthly Patch Tuesday release. + /// + public async Task> GetRecentUpdatesAsync(CancellationToken ct = default) + { + try + { + var url = $"{MsrcBaseUrl}/cvrf/v3.0/updates"; + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Accept", "application/json"); + + var response = await _http.SendAsync(request, ct); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("MSRC updates API returned {StatusCode}", (int)response.StatusCode); + return new List(); + } + + var json = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(json, JsonOptions); + return result?.Value ?? new List(); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogWarning(ex, "Failed to fetch MSRC updates list"); + return new List(); + } + } + + /// + /// Get the CVRF document for a specific monthly update (e.g., "2025-Mar"). + /// Contains detailed vulnerability information per Microsoft product. + /// + public async Task GetCvrfAsync(string documentId, CancellationToken ct = default) + { + try + { + var url = $"{MsrcBaseUrl}/cvrf/v3.0/cvrf/{Uri.EscapeDataString(documentId)}"; + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("Accept", "application/json"); + + var response = await _http.SendAsync(request, ct); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("MSRC CVRF API returned {StatusCode} for {DocumentId}", + (int)response.StatusCode, documentId); + return null; + } + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json, JsonOptions); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogWarning(ex, "Failed to fetch MSRC CVRF document {DocumentId}", documentId); + return null; + } + } + } + + // ----------------------------------------------------------------------- + // MSRC API Response Models + // ----------------------------------------------------------------------- + + public class MsrcUpdatesResponse + { + public List Value { get; set; } = new List(); + } + + public class MsrcUpdateSummary + { + public string ID { get; set; } + public string Alias { get; set; } + public string DocumentTitle { get; set; } + public string InitialReleaseDate { get; set; } + public string CurrentReleaseDate { get; set; } + public string CvrfUrl { get; set; } + } + + public class MsrcCvrfDocument + { + public MsrcDocumentTitle DocumentTitle { get; set; } + public MsrcDocumentTracking DocumentTracking { get; set; } + public MsrcProductTree ProductTree { get; set; } + public List Vulnerability { get; set; } = new List(); + } + + public class MsrcDocumentTitle + { + public string Value { get; set; } + } + + public class MsrcDocumentTracking + { + public MsrcRevisionHistory Identification { get; set; } + } + + public class MsrcRevisionHistory + { + public MsrcDocumentId ID { get; set; } + } + + public class MsrcDocumentId + { + public string Value { get; set; } + } + + public class MsrcProductTree + { + public List Branch { get; set; } = new List(); + public List FullProductName { get; set; } = new List(); + } + + public class MsrcBranch + { + public string Type { get; set; } + public string Name { get; set; } + public List Items { get; set; } = new List(); + } + + public class MsrcFullProductName + { + public string ProductID { get; set; } + public string Value { get; set; } + } + + public class MsrcVulnerability + { + public MsrcOrdinal Ordinal { get; set; } + public MsrcCveTitle Title { get; set; } + public List Notes { get; set; } = new List(); + public MsrcCveNumber CVE { get; set; } + public List ProductStatuses { get; set; } = new List(); + public List Threats { get; set; } = new List(); + public List CVSSScoreSets { get; set; } = new List(); + } + + public class MsrcOrdinal + { + public string Value { get; set; } + } + + public class MsrcCveTitle + { + public string Value { get; set; } + } + + public class MsrcCveNumber + { + public string Value { get; set; } + } + + public class MsrcNote + { + public string Title { get; set; } + public string Type { get; set; } + public string Value { get; set; } + } + + public class MsrcProductStatus + { + public string Type { get; set; } + public List ProductID { get; set; } = new List(); + } + + public class MsrcThreat + { + public string Type { get; set; } + public MsrcThreatDescription Description { get; set; } + public List ProductID { get; set; } = new List(); + } + + public class MsrcThreatDescription + { + public string Value { get; set; } + } + + public class MsrcCvssScoreSet + { + public double BaseScore { get; set; } + public string TemporalScore { get; set; } + public string Vector { get; set; } + public List ProductID { get; set; } = new List(); + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/NvdApiClient.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/NvdApiClient.cs new file mode 100644 index 00000000..af934444 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/NvdApiClient.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Services.Vulnerability +{ + /// + /// Typed HttpClient for the NVD API 2.0. + /// Handles rate limiting (5 req/30s without key, 50 req/30s with key). + /// + public class NvdApiClient + { + private readonly HttpClient _http; + private readonly AdminConfigurationService _adminConfig; + private readonly ILogger _logger; + + private static readonly SemaphoreSlim _rateLimiter = new SemaphoreSlim(1, 1); + private static DateTime _lastRequestTime = DateTime.MinValue; + + private const string NvdCveUrl = "https://services.nvd.nist.gov/rest/json/cves/2.0"; + private const string NvdCpeUrl = "https://services.nvd.nist.gov/rest/json/cpes/2.0"; + + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public NvdApiClient(HttpClient http, AdminConfigurationService adminConfig, ILogger logger) + { + _http = http ?? throw new ArgumentNullException(nameof(http)); + _adminConfig = adminConfig ?? throw new ArgumentNullException(nameof(adminConfig)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _http.Timeout = TimeSpan.FromSeconds(30); + } + + /// + /// Search CVEs for a specific CPE name. Returns CVEs with version range configurations. + /// + public async Task SearchCvesByCpeAsync(string cpeName, CancellationToken ct = default) + { + var url = $"{NvdCveUrl}?cpeName={Uri.EscapeDataString(cpeName)}&resultsPerPage=100"; + return await ExecuteWithRateLimitAsync(url, ct); + } + + /// + /// Search CPE dictionary by keyword (vendor + product). + /// Used when seed mapping doesn't cover a product. + /// + public async Task SearchCpeAsync(string keywordSearch, CancellationToken ct = default) + { + var url = $"{NvdCpeUrl}?keywordSearch={Uri.EscapeDataString(keywordSearch)}&resultsPerPage=20"; + return await ExecuteWithRateLimitAsync(url, ct); + } + + private async Task ExecuteWithRateLimitAsync(string url, CancellationToken ct) where T : class, new() + { + await _rateLimiter.WaitAsync(ct); + try + { + // Determine rate limit delay based on API key presence + var adminConfig = await _adminConfig.GetConfigurationAsync(); + bool hasApiKey = !string.IsNullOrWhiteSpace(adminConfig?.NvdApiKey); + int delayMs = hasApiKey ? 600 : 6000; // 50/30s vs 5/30s + + var elapsed = (DateTime.UtcNow - _lastRequestTime).TotalMilliseconds; + if (elapsed < delayMs) + { + await Task.Delay((int)(delayMs - elapsed), ct); + } + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + if (hasApiKey) + { + request.Headers.Add("apiKey", adminConfig.NvdApiKey); + } + request.Headers.Add("User-Agent", "AutopilotMonitor/1.0"); + + _lastRequestTime = DateTime.UtcNow; + var response = await _http.SendAsync(request, ct); + + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("NVD API returned {StatusCode} for {Url}", (int)response.StatusCode, url); + return new T(); + } + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json, JsonOptions) ?? new T(); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + _logger.LogWarning(ex, "NVD API request failed for {Url}", url); + return new T(); + } + finally + { + _rateLimiter.Release(); + } + } + } + + // ----------------------------------------------------------------------- + // NVD API Response Models + // ----------------------------------------------------------------------- + + public class NvdCveResponse + { + public int TotalResults { get; set; } + public List Vulnerabilities { get; set; } = new List(); + } + + public class NvdVulnerabilityItem + { + public NvdCve Cve { get; set; } + } + + public class NvdCve + { + public string Id { get; set; } + public List Descriptions { get; set; } = new List(); + public NvdMetrics Metrics { get; set; } + public List Configurations { get; set; } = new List(); + public string Published { get; set; } + public string LastModified { get; set; } + } + + public class NvdDescription + { + public string Lang { get; set; } + public string Value { get; set; } + } + + public class NvdMetrics + { + public List CvssMetricV31 { get; set; } = new List(); + public List CvssMetricV30 { get; set; } = new List(); + } + + public class NvdCvssMetricV31 + { + public NvdCvssData CvssData { get; set; } + } + + public class NvdCvssData + { + public double BaseScore { get; set; } + public string BaseSeverity { get; set; } + public string VectorString { get; set; } + } + + public class NvdConfiguration + { + public List Nodes { get; set; } = new List(); + } + + public class NvdNode + { + public string Operator { get; set; } + public bool Negate { get; set; } + public List CpeMatch { get; set; } = new List(); + } + + public class NvdCpeMatch + { + public bool Vulnerable { get; set; } + public string Criteria { get; set; } + public string VersionStartIncluding { get; set; } + public string VersionStartExcluding { get; set; } + public string VersionEndIncluding { get; set; } + public string VersionEndExcluding { get; set; } + } + + // CPE search response + public class NvdCpeResponse + { + public int TotalResults { get; set; } + public List Products { get; set; } = new List(); + } + + public class NvdCpeProduct + { + public NvdCpeEntry Cpe { get; set; } + } + + public class NvdCpeEntry + { + public string CpeName { get; set; } + public string CpeNameId { get; set; } + public List Titles { get; set; } = new List(); + } + + public class NvdCpeTitle + { + public string Title { get; set; } + public string Lang { get; set; } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VersionComparer.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VersionComparer.cs new file mode 100644 index 00000000..b6571dfb --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VersionComparer.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; + +namespace AutopilotMonitor.Functions.Services.Vulnerability +{ + /// + /// Semantic version comparison for CVE version range evaluation. + /// Handles partial versions (e.g., "1.2" vs "1.2.3") by padding with zeros. + /// + public static class VersionComparer + { + /// + /// Compare two version strings numerically component by component. + /// Returns: negative if v1 < v2, 0 if equal, positive if v1 > v2. + /// + public static int Compare(string version1, string version2) + { + if (string.IsNullOrWhiteSpace(version1) && string.IsNullOrWhiteSpace(version2)) + return 0; + if (string.IsNullOrWhiteSpace(version1)) + return -1; + if (string.IsNullOrWhiteSpace(version2)) + return 1; + + var parts1 = version1.Split('.').Select(ParsePart).ToArray(); + var parts2 = version2.Split('.').Select(ParsePart).ToArray(); + + int maxLen = Math.Max(parts1.Length, parts2.Length); + for (int i = 0; i < maxLen; i++) + { + long p1 = i < parts1.Length ? parts1[i] : 0; + long p2 = i < parts2.Length ? parts2[i] : 0; + if (p1 < p2) return -1; + if (p1 > p2) return 1; + } + + return 0; + } + + /// + /// Check if installedVersion falls within the affected range defined by NVD CPE match. + /// + public static bool IsVersionAffected( + string installedVersion, + string versionStartIncluding, + string versionStartExcluding, + string versionEndIncluding, + string versionEndExcluding) + { + if (string.IsNullOrWhiteSpace(installedVersion)) + return false; + + // Check start boundary + if (!string.IsNullOrWhiteSpace(versionStartIncluding)) + { + if (Compare(installedVersion, versionStartIncluding) < 0) + return false; + } + if (!string.IsNullOrWhiteSpace(versionStartExcluding)) + { + if (Compare(installedVersion, versionStartExcluding) <= 0) + return false; + } + + // Check end boundary + if (!string.IsNullOrWhiteSpace(versionEndIncluding)) + { + if (Compare(installedVersion, versionEndIncluding) > 0) + return false; + } + if (!string.IsNullOrWhiteSpace(versionEndExcluding)) + { + if (Compare(installedVersion, versionEndExcluding) >= 0) + return false; + } + + return true; + } + + private static long ParsePart(string part) + { + // Strip non-numeric suffixes (e.g., "89beta" → 89) + var numericPart = new string(part.TakeWhile(char.IsDigit).ToArray()); + return long.TryParse(numericPart, out var result) ? result : 0; + } + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs new file mode 100644 index 00000000..dd557be3 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs @@ -0,0 +1,517 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using AutopilotMonitor.Shared.Models; +using Azure.Data.Tables; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Services.Vulnerability +{ + public class VulnerabilityCorrelationService + { + private readonly TableStorageService _storage; + private readonly NvdApiClient _nvdApi; + private readonly KevDataService _kevService; + private readonly MsrcApiClient _msrcApi; + private readonly AdminConfigurationService _adminConfig; + private readonly ILogger _logger; + + private List _cpeMappings; + private static readonly SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); + private static bool _seedLoaded = false; + + private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + public VulnerabilityCorrelationService( + TableStorageService storage, + NvdApiClient nvdApi, + KevDataService kevService, + MsrcApiClient msrcApi, + AdminConfigurationService adminConfig, + ILogger logger) + { + _storage = storage; + _nvdApi = nvdApi; + _kevService = kevService; + _msrcApi = msrcApi; + _adminConfig = adminConfig; + _logger = logger; + } + + public async Task CorrelateAsync( + string sessionId, + string tenantId, + List> inventoryItems, + CancellationToken ct = default) + { + await EnsureMappingsLoadedAsync(); + + var findings = new List>(); + var unmatchedSoftware = new List>(); + int matchedCount = 0; + int totalCves = 0; + int criticalCves = 0; + int highCves = 0; + int mediumCves = 0; + int lowCves = 0; + int kevMatches = 0; + string overallRisk = "none"; + + foreach (var item in inventoryItems) + { + if (ct.IsCancellationRequested) break; + + var normalizedVendor = item.ContainsKey("normalizedPublisher") ? item["normalizedPublisher"]?.ToString() ?? "" : ""; + var normalizedName = item.ContainsKey("normalizedName") ? item["normalizedName"]?.ToString() ?? "" : ""; + var installedVersion = item.ContainsKey("normalizedVersion") ? item["normalizedVersion"]?.ToString() ?? "" : ""; + var displayName = item.ContainsKey("displayName") ? item["displayName"]?.ToString() ?? "" : ""; + + // Find CPE mapping + var cpeMapping = FindCpeMapping(normalizedVendor, normalizedName, displayName); + if (cpeMapping == null) + { + unmatchedSoftware.Add(new Dictionary + { + { "softwareName", displayName }, + { "installedVersion", installedVersion }, + { "publisher", item.ContainsKey("publisher") ? item["publisher"]?.ToString() ?? "" : "" }, + { "reason", "no_cpe_match" } + }); + continue; + } + + matchedCount++; + var cpeUri = cpeMapping.CpeUri; + + // Look up CVEs + List> vulns; + try + { + vulns = await GetVulnerabilitiesForCpeAsync(cpeUri, installedVersion, ct); + } + catch (OperationCanceledException) + { + break; + } + + if (vulns.Count == 0) + continue; + + // Check KEV for each CVE + foreach (var vuln in vulns) + { + var cveId = vuln["cveId"]?.ToString(); + var kevEntry = await _kevService.CheckKevAsync(cveId); + if (kevEntry != null) + { + vuln["isKev"] = true; + vuln["kevDueDate"] = kevEntry.DueDate ?? ""; + vuln["kevRequiredAction"] = kevEntry.RequiredAction ?? ""; + vuln["knownRansomware"] = kevEntry.KnownRansomwareCampaignUse ?? ""; + kevMatches++; + } + else + { + vuln["isKev"] = false; + } + } + + // Count severities + foreach (var vuln in vulns) + { + totalCves++; + var severity = vuln.ContainsKey("cvssSeverity") ? vuln["cvssSeverity"]?.ToString() ?? "" : ""; + switch (severity.ToUpperInvariant()) + { + case "CRITICAL": criticalCves++; break; + case "HIGH": highCves++; break; + case "MEDIUM": mediumCves++; break; + case "LOW": lowCves++; break; + } + } + + // Determine risk level for this software + string riskLevel = "none"; + bool hasKev = vulns.Any(v => v.ContainsKey("isKev") && (bool)v["isKev"]); + double maxCvss = vulns.Max(v => v.ContainsKey("cvssScore") ? Convert.ToDouble(v["cvssScore"]) : 0); + + if (hasKev || maxCvss >= 9.0) riskLevel = "critical"; + else if (maxCvss >= 7.0) riskLevel = "high"; + else if (maxCvss >= 4.0) riskLevel = "medium"; + else if (maxCvss > 0) riskLevel = "low"; + + findings.Add(new Dictionary + { + { "softwareName", displayName }, + { "installedVersion", installedVersion }, + { "cpeUri", $"{cpeUri}:{installedVersion}" }, + { "matchConfidence", cpeMapping.MatchConfidence ?? "high" }, + { "riskLevel", riskLevel }, + { "vulnerabilities", vulns } + }); + } + + // Determine overall risk + if (kevMatches > 0 || criticalCves > 0) overallRisk = "critical"; + else if (highCves > 0) overallRisk = "high"; + else if (mediumCves > 0) overallRisk = "medium"; + else if (lowCves > 0) overallRisk = "low"; + + // Sort findings by risk (critical first) + var riskOrder = new Dictionary + { + { "critical", 0 }, { "high", 1 }, { "medium", 2 }, { "low", 3 }, { "none", 4 } + }; + findings.Sort((a, b) => + { + var ra = riskOrder.GetValueOrDefault(a["riskLevel"]?.ToString() ?? "none", 4); + var rb = riskOrder.GetValueOrDefault(b["riskLevel"]?.ToString() ?? "none", 4); + return ra.CompareTo(rb); + }); + + // Cap findings at top 50 + int additionalCount = 0; + if (findings.Count > 50) + { + additionalCount = findings.Count - 50; + findings = findings.Take(50).ToList(); + } + + // Determine event severity + EventSeverity eventSeverity; + if (overallRisk == "critical" || kevMatches > 0) eventSeverity = EventSeverity.Error; + else if (overallRisk == "high" || criticalCves > 0) eventSeverity = EventSeverity.Warning; + else eventSeverity = EventSeverity.Info; + + var data = new Dictionary + { + { "scan_summary", new Dictionary + { + { "total_software_scanned", inventoryItems.Count }, + { "matched_to_cpe", matchedCount }, + { "unmatched", unmatchedSoftware.Count }, + { "total_cves_found", totalCves }, + { "critical_cves", criticalCves }, + { "high_cves", highCves }, + { "medium_cves", mediumCves }, + { "low_cves", lowCves }, + { "kev_matches", kevMatches }, + { "overall_risk", overallRisk }, + { "additional_findings_count", additionalCount } + } + }, + { "findings", findings }, + { "unmatched_software", unmatchedSoftware.Take(100).ToList() }, + { "data_freshness", new Dictionary + { + { "cpe_cache_entries", _cpeMappings?.Count ?? 0 }, + { "kev_catalog_entries", _kevService.CatalogSize }, + { "kev_last_sync", _kevService.LastRefresh.ToString("o") }, + { "nvd_api_key_configured", !string.IsNullOrWhiteSpace((await _adminConfig.GetConfigurationAsync())?.NvdApiKey) } + } + } + }; + + var message = totalCves > 0 + ? $"Vulnerability scan: {criticalCves} critical, {highCves} high across {matchedCount} scanned products" + : $"Vulnerability scan complete: {matchedCount} products scanned, no known vulnerabilities found"; + + return new EnrollmentEvent + { + SessionId = sessionId, + TenantId = tenantId, + Timestamp = DateTime.UtcNow, + EventType = "vulnerability_report", + Severity = eventSeverity, + Source = "VulnerabilityCorrelation", + Phase = EnrollmentPhase.Complete, + Message = message, + Data = data + }; + } + + // ----------------------------------------------------------------------- + // CPE Mapping + // ----------------------------------------------------------------------- + + private CpeMappingEntry FindCpeMapping(string normalizedVendor, string normalizedName, string displayName) + { + if (_cpeMappings == null || _cpeMappings.Count == 0) + return null; + + // 1. Exact match on normalizedVendor + normalizedProduct + var exact = _cpeMappings.FirstOrDefault(m => + string.Equals(m.NormalizedVendor, normalizedVendor, StringComparison.OrdinalIgnoreCase) && + string.Equals(m.NormalizedProduct, normalizedName, StringComparison.OrdinalIgnoreCase)); + if (exact != null) + { + exact.MatchConfidence = "high"; + return exact; + } + + // 2. Pattern match on displayNamePatterns + var displayNameLower = (displayName ?? "").ToLowerInvariant(); + var patternMatch = _cpeMappings.FirstOrDefault(m => + m.DisplayNamePatterns != null && + m.DisplayNamePatterns.Any(p => displayNameLower.Contains(p.ToLowerInvariant()))); + if (patternMatch != null) + { + patternMatch.MatchConfidence = "medium"; + return patternMatch; + } + + // 3. Publisher + partial product name + if (!string.IsNullOrWhiteSpace(normalizedVendor)) + { + var vendorMatch = _cpeMappings.FirstOrDefault(m => + m.PublisherPatterns != null && + m.PublisherPatterns.Any(p => string.Equals(p, normalizedVendor, StringComparison.OrdinalIgnoreCase)) && + !string.IsNullOrWhiteSpace(normalizedName) && + normalizedName.Contains(m.CpeProduct?.Replace("_", " ") ?? "", StringComparison.OrdinalIgnoreCase)); + if (vendorMatch != null) + { + vendorMatch.MatchConfidence = "medium"; + return vendorMatch; + } + } + + return null; + } + + // ----------------------------------------------------------------------- + // CVE Lookup + // ----------------------------------------------------------------------- + + private async Task>> GetVulnerabilitiesForCpeAsync( + string cpeUri, string installedVersion, CancellationToken ct) + { + var results = new List>(); + + try + { + // Query NVD API for CVEs matching this CPE + var cveResponse = await _nvdApi.SearchCvesByCpeAsync(cpeUri, ct); + + if (cveResponse?.Vulnerabilities == null) + return results; + + foreach (var vulnItem in cveResponse.Vulnerabilities) + { + var cve = vulnItem?.Cve; + if (cve == null) continue; + + // Check if installed version is affected + bool isAffected = false; + if (cve.Configurations != null) + { + foreach (var config in cve.Configurations) + { + if (config.Nodes == null) continue; + foreach (var node in config.Nodes) + { + if (node.CpeMatch == null) continue; + foreach (var match in node.CpeMatch) + { + if (!match.Vulnerable) continue; + + // If no version constraints, consider affected + if (string.IsNullOrWhiteSpace(match.VersionStartIncluding) && + string.IsNullOrWhiteSpace(match.VersionStartExcluding) && + string.IsNullOrWhiteSpace(match.VersionEndIncluding) && + string.IsNullOrWhiteSpace(match.VersionEndExcluding)) + { + // Wildcard match -- only if CPE criteria matches our product + if (match.Criteria != null && match.Criteria.StartsWith(cpeUri, StringComparison.OrdinalIgnoreCase)) + isAffected = true; + } + else if (!string.IsNullOrWhiteSpace(installedVersion)) + { + isAffected = VersionComparer.IsVersionAffected( + installedVersion, + match.VersionStartIncluding, + match.VersionStartExcluding, + match.VersionEndIncluding, + match.VersionEndExcluding); + } + + if (isAffected) break; + } + if (isAffected) break; + } + if (isAffected) break; + } + } + + if (!isAffected) + continue; + + // Extract CVSS score (prefer v3.1, fallback to v3.0) + double cvssScore = 0; + string cvssSeverity = "UNKNOWN"; + var cvssMetric = cve.Metrics?.CvssMetricV31?.FirstOrDefault() + ?? cve.Metrics?.CvssMetricV30?.FirstOrDefault(); + if (cvssMetric?.CvssData != null) + { + cvssScore = cvssMetric.CvssData.BaseScore; + cvssSeverity = cvssMetric.CvssData.BaseSeverity ?? "UNKNOWN"; + } + + // Extract English description + var description = cve.Descriptions?.FirstOrDefault(d => + string.Equals(d.Lang, "en", StringComparison.OrdinalIgnoreCase))?.Value ?? ""; + // Truncate long descriptions + if (description.Length > 500) + description = description.Substring(0, 497) + "..."; + + results.Add(new Dictionary + { + { "cveId", cve.Id ?? "" }, + { "cvssScore", cvssScore }, + { "cvssSeverity", cvssSeverity }, + { "description", description }, + { "publishedDate", cve.Published ?? "" }, + { "source", "NVD" }, + { "isKev", false }, + { "kevDueDate", "" }, + { "kevRequiredAction", "" }, + { "knownRansomware", "" } + }); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to look up CVEs for CPE {CpeUri}", cpeUri); + } + + return results; + } + + // ----------------------------------------------------------------------- + // Seed Loading + // ----------------------------------------------------------------------- + + private async Task EnsureMappingsLoadedAsync() + { + if (_seedLoaded && _cpeMappings != null) + return; + + await _initLock.WaitAsync(); + try + { + if (_seedLoaded && _cpeMappings != null) + return; + + _cpeMappings = new List(); + + // Load from embedded seed resource + try + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = assembly.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith("cpe-mapping-seed.json", StringComparison.OrdinalIgnoreCase)); + + if (resourceName != null) + { + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream != null) + { + using var reader = new StreamReader(stream); + var json = await reader.ReadToEndAsync(); + var seed = JsonSerializer.Deserialize(json, JsonOptions); + if (seed?.Mappings != null) + { + _cpeMappings.AddRange(seed.Mappings); + _logger.LogInformation("Loaded {Count} CPE mappings from seed", seed.Mappings.Count); + } + } + } + else + { + _logger.LogWarning("CPE mapping seed resource not found in assembly"); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to load CPE mapping seed"); + } + + // Load custom mappings from VulnerabilityCache table + try + { + var tableClient = _storage.GetTableClient(AutopilotMonitor.Shared.Constants.TableNames.VulnerabilityCache); + var customMappings = tableClient.QueryAsync( + filter: $"PartitionKey eq 'cpe_map_custom'"); + + await foreach (var entity in customMappings) + { + _cpeMappings.Add(new CpeMappingEntry + { + NormalizedVendor = entity.GetString("NormalizedVendor") ?? "", + NormalizedProduct = entity.GetString("NormalizedProduct") ?? "", + CpeVendor = entity.GetString("CpeVendor") ?? "", + CpeProduct = entity.GetString("CpeProduct") ?? "", + CpeUri = entity.GetString("CpeUri") ?? "", + Category = entity.GetString("Category") ?? "custom", + DisplayNamePatterns = JsonSerializer.Deserialize>( + entity.GetString("DisplayNamePatternsJson") ?? "[]"), + PublisherPatterns = JsonSerializer.Deserialize>( + entity.GetString("PublisherPatternsJson") ?? "[]") + }); + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "No custom CPE mappings loaded (table may not exist yet)"); + } + + _seedLoaded = true; + _logger.LogInformation("CPE mappings ready: {Count} total entries", _cpeMappings.Count); + } + finally + { + _initLock.Release(); + } + } + } + + // ----------------------------------------------------------------------- + // CPE Mapping Models + // ----------------------------------------------------------------------- + + public class CpeMappingSeed + { + public string Version { get; set; } + public string LastUpdated { get; set; } + public List Mappings { get; set; } = new List(); + } + + public class CpeMappingEntry + { + public string NormalizedVendor { get; set; } + public string NormalizedProduct { get; set; } + public List DisplayNamePatterns { get; set; } = new List(); + public List PublisherPatterns { get; set; } = new List(); + public string CpeVendor { get; set; } + public string CpeProduct { get; set; } + public string CpeUri { get; set; } + public string Category { get; set; } + + // Set during matching -- not serialized + [JsonIgnore] + public string MatchConfidence { get; set; } + } +} diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index 1d138379..840787f5 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -151,6 +151,8 @@ namespace AutopilotMonitor.Shared public const string ScriptCompleted = "script_completed"; public const string ScriptFailed = "script_failed"; public const string EspProvisioningStatus = "esp_provisioning_status"; + public const string SoftwareInventoryAnalysis = "software_inventory_analysis"; + public const string VulnerabilityReport = "vulnerability_report"; } // ----------------------------------------------------------------------- @@ -251,6 +253,9 @@ namespace AutopilotMonitor.Shared // Galactic Admin in-app notifications (persistent until dismissed) public const string GalacticNotifications = "GalacticNotifications"; + // Vulnerability data cache (CPE mappings, CVE data, KEV catalog) + public const string VulnerabilityCache = "VulnerabilityCache"; + /// /// Returns all table names for initialization /// @@ -279,7 +284,8 @@ namespace AutopilotMonitor.Shared BlockedVersions, SessionReports, BootstrapSessions, - GalacticNotifications + GalacticNotifications, + VulnerabilityCache }; } } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs index 6124b30d..ee45bd81 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/AdminConfiguration.cs @@ -123,6 +123,28 @@ namespace AutopilotMonitor.Shared.Models } } + // ===== VULNERABILITY CORRELATION SETTINGS ===== + + /// + /// NVD API key for higher rate limits (50 req/30s vs 5 req/30s without key). + /// Free registration at https://nvd.nist.gov/developers/request-an-api-key + /// null = operate without API key (slower, still functional). + /// + public string NvdApiKey { get; set; } + + /// + /// Whether vulnerability correlation is globally enabled. + /// When false, agents still collect inventory but backend skips correlation. + /// Default: true + /// + public bool VulnerabilityCorrelationEnabled { get; set; } = true; + + /// + /// Last successful vulnerability data sync timestamp (UTC ISO 8601). + /// Updated by VulnerabilityDataSyncFunction. + /// + public string VulnerabilityDataLastSyncUtc { get; set; } + /// /// Creates default configuration /// diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs index 2d62edd0..d4879853 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/AgentConfigResponse.cs @@ -241,5 +241,13 @@ namespace AutopilotMonitor.Shared.Models /// Default: empty list (built-in defaults only) /// public List LocalAdminAllowedAccounts { get; set; } = new List(); + + /// + /// Whether to run the SoftwareInventoryAnalyzer at startup and shutdown. + /// Collects installed software registry inventory, normalizes entries, and + /// emits a baseline snapshot (startup) plus delta (shutdown) for vulnerability insight. + /// Default: true + /// + public bool EnableSoftwareInventoryAnalyzer { get; set; } = true; } } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs index ab53f5e0..1bd751f4 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs @@ -272,6 +272,12 @@ namespace AutopilotMonitor.Shared.Models /// public bool? EnableLocalAdminAnalyzer { get; set; } = null; + /// + /// Whether the SoftwareInventoryAnalyzer is enabled for this tenant's devices. + /// null = use agent default (true). + /// + public bool? EnableSoftwareInventoryAnalyzer { get; set; } = null; + /// /// JSON-serialized list of additional local account names that are considered expected /// on a newly enrolled device (merged with built-in defaults on the agent). diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/VulnerabilityReportSection.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/VulnerabilityReportSection.tsx new file mode 100644 index 00000000..2bac5f92 --- /dev/null +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/VulnerabilityReportSection.tsx @@ -0,0 +1,806 @@ +"use client"; + +import { useState, useMemo } from "react"; + +interface VulnerabilityReportSectionProps { + events: any[]; + expanded: boolean; + setExpanded: (v: boolean) => void; +} + +type TabKey = "findings" | "inventory" | "delta"; + +interface CveDetail { + cveId: string; + cvss?: number; + severity?: string; + description?: string; + kev?: boolean; + publishedDate?: string; + references?: string[]; +} + +interface Finding { + softwareName: string; + version: string; + cves: CveDetail[]; + maxCvss: number; + maxSeverity: string; + kevCount: number; +} + +interface InventoryItem { + name: string; + version: string; + publisher?: string; + installDate?: string; + source?: string; + confidence?: string; +} + +const SEVERITY_ORDER: Record = { critical: 0, high: 1, medium: 2, low: 3, info: 4 }; + +function normalizeSeverity(s?: string): string { + if (!s) return "info"; + const lower = s.toLowerCase(); + if (lower === "critical") return "critical"; + if (lower === "high") return "high"; + if (lower === "medium" || lower === "moderate") return "medium"; + if (lower === "low") return "low"; + return "info"; +} + +function severityFromCvss(cvss: number): string { + if (cvss >= 9.0) return "critical"; + if (cvss >= 7.0) return "high"; + if (cvss >= 4.0) return "medium"; + if (cvss > 0) return "low"; + return "info"; +} + +const severityBadgeColors: Record = { + critical: "bg-red-100 text-red-800", + high: "bg-orange-100 text-orange-800", + medium: "bg-yellow-100 text-yellow-800", + low: "bg-blue-100 text-blue-800", + info: "bg-gray-100 text-gray-800", +}; + +const severityHeaderBadgeColors: Record = { + critical: "bg-red-100 text-red-700", + high: "bg-orange-100 text-orange-700", + medium: "bg-yellow-100 text-yellow-700", + low: "bg-blue-100 text-blue-700", +}; + +function confidenceBadgeColor(confidence?: string): string { + if (!confidence) return "bg-gray-100 text-gray-700"; + const c = confidence.toLowerCase(); + if (c === "high") return "bg-green-100 text-green-700"; + if (c === "medium") return "bg-yellow-100 text-yellow-700"; + if (c === "low") return "bg-red-100 text-red-700"; + return "bg-gray-100 text-gray-700"; +} + +export default function VulnerabilityReportSection({ + events, + expanded, + setExpanded, +}: VulnerabilityReportSectionProps) { + const [activeTab, setActiveTab] = useState("findings"); + const [findingsSearch, setFindingsSearch] = useState(""); + const [findingsSortKey, setFindingsSortKey] = useState<"severity" | "software" | "cves" | "cvss">("severity"); + const [findingsSortAsc, setFindingsSortAsc] = useState(false); + const [expandedRows, setExpandedRows] = useState>(new Set()); + const [inventorySearch, setInventorySearch] = useState(""); + const [unmatchedExpanded, setUnmatchedExpanded] = useState(false); + + // Filter relevant events + const vulnEvents = useMemo(() => events.filter(e => e.eventType === "vulnerability_report"), [events]); + const inventoryEvents = useMemo(() => events.filter(e => e.eventType === "software_inventory_analysis"), [events]); + + // Return null if no relevant events + if (vulnEvents.length === 0 && inventoryEvents.length === 0) return null; + + // --- Findings data --- + const { findings, unmatchedSoftware, totalSoftware, kevTotal, dataFreshness } = useMemo(() => { + const findingsMap = new Map(); + let unmatched: string[] = []; + let total = 0; + let freshness = ""; + let kevCount = 0; + + for (const evt of vulnEvents) { + const d = evt.data || {}; + if (d.findings && Array.isArray(d.findings)) { + for (const f of d.findings) { + const key = `${f.softwareName || f.software || "Unknown"}|||${f.version || ""}`; + const cves: CveDetail[] = (f.cves || f.vulnerabilities || []).map((c: any) => ({ + cveId: c.cveId || c.id || c.cve_id || "Unknown", + cvss: c.cvss ?? c.cvssScore ?? c.cvss_score ?? 0, + severity: normalizeSeverity(c.severity), + description: c.description || c.summary || "", + kev: c.kev ?? c.isKev ?? c.is_kev ?? false, + publishedDate: c.publishedDate || c.published_date || "", + references: c.references || [], + })); + + const maxCvss = cves.reduce((max: number, c: CveDetail) => Math.max(max, c.cvss ?? 0), 0); + const maxSev = cves.reduce((worst: string, c: CveDetail) => { + const cSev = c.severity || severityFromCvss(c.cvss ?? 0); + return (SEVERITY_ORDER[cSev] ?? 4) < (SEVERITY_ORDER[worst] ?? 4) ? cSev : worst; + }, "info"); + const cveKevCount = cves.filter(c => c.kev).length; + + if (findingsMap.has(key)) { + const existing = findingsMap.get(key)!; + // Merge CVEs (deduplicate by cveId) + const existingIds = new Set(existing.cves.map(c => c.cveId)); + for (const c of cves) { + if (!existingIds.has(c.cveId)) { + existing.cves.push(c); + } + } + existing.maxCvss = Math.max(existing.maxCvss, maxCvss); + existing.maxSeverity = (SEVERITY_ORDER[maxSev] ?? 4) < (SEVERITY_ORDER[existing.maxSeverity] ?? 4) ? maxSev : existing.maxSeverity; + existing.kevCount += cveKevCount; + } else { + findingsMap.set(key, { + softwareName: f.softwareName || f.software || "Unknown", + version: f.version || "", + cves, + maxCvss, + maxSeverity: maxSev, + kevCount: cveKevCount, + }); + } + kevCount += cveKevCount; + } + } + if (d.unmatchedSoftware && Array.isArray(d.unmatchedSoftware)) { + unmatched = [...unmatched, ...d.unmatchedSoftware]; + } else if (d.unmatched_software && Array.isArray(d.unmatched_software)) { + unmatched = [...unmatched, ...d.unmatched_software]; + } + total = d.totalSoftware ?? d.total_software ?? d.totalCount ?? total; + freshness = d.dataFreshness || d.data_freshness || d.analyzedAt || d.analyzed_at || freshness; + } + + // Deduplicate unmatched + const uniqueUnmatched = [...new Set(unmatched)]; + + return { + findings: Array.from(findingsMap.values()), + unmatchedSoftware: uniqueUnmatched, + totalSoftware: total, + kevTotal: kevCount, + dataFreshness: freshness, + }; + }, [vulnEvents]); + + // Severity counts for header badges + const severityCounts = useMemo(() => { + const counts: Record = { critical: 0, high: 0, medium: 0, low: 0 }; + for (const f of findings) { + const sev = f.maxSeverity; + if (sev in counts) counts[sev] += f.cves.length; + } + return counts; + }, [findings]); + + // --- Inventory data (merge chunks) --- + const inventoryItems = useMemo(() => { + // Group by triggered_at to handle chunks + const chunkGroups = new Map }>(); + + for (const evt of inventoryEvents) { + const d = evt.data || {}; + const triggeredAt = d.triggered_at || d.triggeredAt || evt.timestamp || "default"; + const chunkIndex = d.chunk_index ?? d.chunkIndex ?? 0; + const chunkCount = d.chunk_count ?? d.chunkCount ?? 1; + const items: InventoryItem[] = (d.inventory || d.software || d.items || []).map((item: any) => ({ + name: item.name || item.softwareName || item.software_name || "Unknown", + version: item.version || "", + publisher: item.publisher || item.vendor || "", + installDate: item.installDate || item.install_date || "", + source: item.source || "", + confidence: item.confidence || "", + })); + + if (!chunkGroups.has(triggeredAt)) { + chunkGroups.set(triggeredAt, { items: [], chunkCount, seenChunks: new Set() }); + } + const group = chunkGroups.get(triggeredAt)!; + if (!group.seenChunks.has(chunkIndex)) { + group.seenChunks.add(chunkIndex); + group.items.push(...items); + } + group.chunkCount = Math.max(group.chunkCount, chunkCount); + } + + // Take the latest group (most items or latest triggered_at) + let allItems: InventoryItem[] = []; + for (const group of chunkGroups.values()) { + if (group.items.length > allItems.length) { + allItems = group.items; + } + } + return allItems; + }, [inventoryEvents]); + + // --- Delta data (new installs during enrollment) --- + const deltaItems = useMemo(() => { + const items: InventoryItem[] = []; + // Look in vulnerability_report events for new_installs_during_enrollment in shutdown data + for (const evt of [...vulnEvents, ...inventoryEvents]) { + const d = evt.data || {}; + const newInstalls = d.new_installs_during_enrollment || d.newInstallsDuringEnrollment || []; + if (Array.isArray(newInstalls)) { + for (const item of newInstalls) { + items.push({ + name: item.name || item.softwareName || item.software_name || "Unknown", + version: item.version || "", + publisher: item.publisher || item.vendor || "", + installDate: item.installDate || item.install_date || "", + source: item.source || "", + confidence: item.confidence || "", + }); + } + } + } + // Also check all events for shutdown events + for (const evt of events) { + if (evt.eventType === "enrollment_complete" || evt.eventType === "enrollment_failed" || evt.eventType === "collector_shutdown") { + const d = evt.data || {}; + const newInstalls = d.new_installs_during_enrollment || d.newInstallsDuringEnrollment || []; + if (Array.isArray(newInstalls)) { + for (const item of newInstalls) { + // Deduplicate by name+version + if (!items.some(i => i.name === (item.name || item.softwareName || "Unknown") && i.version === (item.version || ""))) { + items.push({ + name: item.name || item.softwareName || item.software_name || "Unknown", + version: item.version || "", + publisher: item.publisher || item.vendor || "", + installDate: item.installDate || item.install_date || "", + source: item.source || "", + confidence: item.confidence || "", + }); + } + } + } + } + } + return items; + }, [vulnEvents, inventoryEvents, events]); + + // --- Findings: sort & filter --- + const filteredFindings = useMemo(() => { + let result = [...findings]; + if (findingsSearch.trim()) { + const q = findingsSearch.toLowerCase(); + result = result.filter( + f => f.softwareName.toLowerCase().includes(q) || + f.version.toLowerCase().includes(q) || + f.cves.some(c => c.cveId.toLowerCase().includes(q)) + ); + } + result.sort((a, b) => { + let cmp = 0; + switch (findingsSortKey) { + case "severity": + cmp = (SEVERITY_ORDER[a.maxSeverity] ?? 4) - (SEVERITY_ORDER[b.maxSeverity] ?? 4); + break; + case "software": + cmp = a.softwareName.localeCompare(b.softwareName); + break; + case "cves": + cmp = a.cves.length - b.cves.length; + break; + case "cvss": + cmp = a.maxCvss - b.maxCvss; + break; + } + return findingsSortAsc ? cmp : -cmp; + }); + return result; + }, [findings, findingsSearch, findingsSortKey, findingsSortAsc]); + + // Inventory: filter + const filteredInventory = useMemo(() => { + if (!inventorySearch.trim()) return inventoryItems; + const q = inventorySearch.toLowerCase(); + return inventoryItems.filter( + i => i.name.toLowerCase().includes(q) || + i.version.toLowerCase().includes(q) || + (i.publisher || "").toLowerCase().includes(q) || + (i.source || "").toLowerCase().includes(q) + ); + }, [inventoryItems, inventorySearch]); + + const toggleFindingsSort = (key: typeof findingsSortKey) => { + if (findingsSortKey === key) { + setFindingsSortAsc(!findingsSortAsc); + } else { + setFindingsSortKey(key); + setFindingsSortAsc(false); + } + }; + + const toggleRowExpanded = (key: string) => { + setExpandedRows(prev => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + const SortIndicator = ({ column }: { column: typeof findingsSortKey }) => { + if (findingsSortKey !== column) return ↕; + return {findingsSortAsc ? "\u25B2" : "\u25BC"}; + }; + + return ( +
+ {/* Section Header */} +
setExpanded(!expanded)} + className="flex items-center justify-between w-full text-left mb-4 cursor-pointer" + > +
+ + + +

Vulnerability Report

+ {findings.length > 0 && ( + <> + + ({findings.reduce((sum, f) => sum + f.cves.length, 0)} CVEs across {findings.length} {findings.length === 1 ? "package" : "packages"}) + +
+ {severityCounts.critical > 0 && ( + + {severityCounts.critical} Critical + + )} + {severityCounts.high > 0 && ( + + {severityCounts.high} High + + )} + {severityCounts.medium > 0 && ( + + {severityCounts.medium} Medium + + )} + {severityCounts.low > 0 && ( + + {severityCounts.low} Low + + )} + {kevTotal > 0 && ( + + {kevTotal} KEV + + )} +
+ + )} +
+ + + +
+ + {expanded && ( +
+ {/* Tabs */} +
+ {( + [ + { key: "findings" as TabKey, label: "Findings", count: findings.length }, + { key: "inventory" as TabKey, label: "Inventory", count: inventoryItems.length }, + { key: "delta" as TabKey, label: "Delta", count: deltaItems.length }, + ] as const + ).map(tab => ( + + ))} +
+ + {/* Findings Tab */} + {activeTab === "findings" && ( +
+ {findings.length === 0 ? ( +
+ No vulnerability findings reported. +
+ ) : ( + <> + {/* Summary Cards */} +
+ + + + + + + + } + /> + +
+ + {/* Search */} +
+ setFindingsSearch(e.target.value)} + className="w-full sm:w-80 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none" + /> +
+ + {/* Findings Table */} +
+ + + + + + + + + + + + + {filteredFindings.map(f => { + const rowKey = `${f.softwareName}|||${f.version}`; + const isRowExpanded = expandedRows.has(rowKey); + return ( + toggleRowExpanded(rowKey)} + /> + ); + })} + {filteredFindings.length === 0 && ( + + + + )} + +
toggleFindingsSort("severity")}> + Risk + toggleFindingsSort("software")}> + Software + Version toggleFindingsSort("cves")}> + CVEs + KEV toggleFindingsSort("cvss")}> + Max CVSS +
+ No findings match your search. +
+
+ + {/* Unmatched Software */} + {unmatchedSoftware.length > 0 && ( +
+ + {unmatchedExpanded && ( +
+

+ Software detected on the device that could not be matched to any known vulnerability database entry. +

+
+ {unmatchedSoftware.map((name, i) => ( + + {name} + + ))} +
+
+ )} +
+ )} + + {/* Data Freshness */} + {dataFreshness && ( +
+ Data Freshness: {dataFreshness} +
+ )} + + )} +
+ )} + + {/* Inventory Tab */} + {activeTab === "inventory" && ( +
+ {inventoryItems.length === 0 ? ( +
+ No software inventory data available. +
+ ) : ( + <> +
+ setInventorySearch(e.target.value)} + className="w-full sm:w-80 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none" + /> +
+
+ + + + + + + + + + + + + {filteredInventory.map((item, i) => ( + + + + + + + + + ))} + {filteredInventory.length === 0 && ( + + + + )} + +
NameVersionPublisherInstall DateSourceConfidence
{item.name}{item.version || "-"}{item.publisher || "-"}{item.installDate || "-"}{item.source || "-"} + {item.confidence ? ( + + {item.confidence} + + ) : ( + - + )} +
+ No inventory items match your search. +
+
+
+ Showing {filteredInventory.length} of {inventoryItems.length} items +
+ + )} +
+ )} + + {/* Delta Tab */} + {activeTab === "delta" && ( +
+ {deltaItems.length === 0 ? ( +
+ No new software installations detected during enrollment. +
+ ) : ( + <> +
+ + {deltaItems.length} new {deltaItems.length === 1 ? "install" : "installs"} during enrollment + +
+
+ + + + + + + + + + + + + {deltaItems.map((item, i) => ( + + + + + + + + + ))} + +
NameVersionPublisherInstall DateSourceConfidence
{item.name}{item.version || "-"}{item.publisher || "-"}{item.installDate || "-"}{item.source || "-"} + {item.confidence ? ( + + {item.confidence} + + ) : ( + - + )} +
+
+ + )} +
+ )} +
+ )} +
+ ); +} + +// --- Sub-components --- + +function SummaryCard({ label, value, color, bg, icon }: { + label: string; + value: number | string; + color: string; + bg: string; + icon?: React.ReactNode; +}) { + return ( +
+
+ {icon} + {label} +
+
{value}
+
+ ); +} + +function FindingRow({ finding, isExpanded, onToggle }: { + finding: Finding; + isExpanded: boolean; + onToggle: () => void; +}) { + const severityRowColors: Record = { + critical: "bg-red-50/50", + high: "bg-orange-50/50", + medium: "bg-yellow-50/30", + low: "", + }; + + return ( + <> + + + + {finding.maxSeverity.toUpperCase()} + + + {finding.softwareName} + {finding.version || "-"} + {finding.cves.length} + + {finding.kevCount > 0 ? ( + + + + + KEV + + ) : ( + - + )} + + + + + + {isExpanded && ( + + +
+

+ CVE Details ({finding.cves.length}) +

+
+ {finding.cves.map(cve => ( +
+
+ + {(cve.severity || severityFromCvss(cve.cvss ?? 0)).toUpperCase()} + +
+
+
+ + {cve.cveId} + + + {cve.kev && ( + + + + + KEV + + )} + {cve.publishedDate && ( + {cve.publishedDate} + )} +
+ {cve.description && ( +

{cve.description}

+ )} +
+
+ ))} +
+
+ + + )} + + ); +} + +function CvssScore({ value }: { value: number }) { + if (!value || value === 0) return -; + let color = "text-blue-700 bg-blue-50"; + if (value >= 9.0) color = "text-red-700 bg-red-50"; + else if (value >= 7.0) color = "text-orange-700 bg-orange-50"; + else if (value >= 4.0) color = "text-yellow-700 bg-yellow-50"; + return ( + + {value.toFixed(1)} + + ); +} diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx index 50430da4..be8504cc 100644 --- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx +++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/page.tsx @@ -20,11 +20,12 @@ import SessionInfoCard from "./components/SessionInfoCard"; import PhaseTimeline from "./components/PhaseTimeline"; import EventTimeline from "./components/EventTimeline"; import AnalysisResultsSection from "./components/AnalysisResultsSection"; +import VulnerabilityReportSection from "./components/VulnerabilityReportSection"; import AdminOverrideModal from "./components/AdminOverrideModal"; import ReportSessionModal from "./components/ReportSessionModal"; import { usePageSections } from "../../../hooks/usePageSections"; import { PageSectionItem } from "../../../contexts/SidebarContext"; -import { InformationCircleIcon, ComputerDesktopIcon, PlayCircleIcon, SparklesIcon, ChartBarIcon, CodeBracketIcon, ArrowDownTrayIcon, ListBulletIcon, ClockIcon } from "../../../lib/sidebarIcons"; +import { InformationCircleIcon, ComputerDesktopIcon, PlayCircleIcon, SparklesIcon, ChartBarIcon, CodeBracketIcon, ArrowDownTrayIcon, ListBulletIcon, ClockIcon, ShieldCheckIcon } from "../../../lib/sidebarIcons"; import DeviceDetailsCard from "./components/DeviceDetailsCard"; import { generateUiExport, generateCsvExport, generateSessionCsvExport, generateRuleResultsCsvExport, SessionExportEvent } from "@/lib/sessionExportUtils"; import { Session, EnrollmentEvent, RuleResult } from "@/types"; @@ -51,6 +52,7 @@ export default function SessionDetailPage() { const [analysisResults, setAnalysisResults] = useState([]); const [loadingAnalysis, setLoadingAnalysis] = useState(false); const [analysisExpanded, setAnalysisExpanded] = useState(true); + const [vulnerabilityReportExpanded, setVulnerabilityReportExpanded] = useState(false); const [phaseTimelineExpanded, setPhaseTimelineExpanded] = useState(true); const [perfExpanded, setPerfExpanded] = useState(true); const [timelineExpanded, setTimelineExpanded] = useState(true); @@ -280,12 +282,30 @@ export default function SessionDetailPage() { } }; + // Listen for async rule engine results (pushed after background analysis completes) + const handleRuleResultsReady = (data: { sessionId: string; tenantId: string; ruleResultCount: number }) => { + if (data.sessionId !== sessionIdRef.current) return; + console.info(`[SessionDetail] ruleResultsReady signal: ${data.ruleResultCount} results`); + fetchAnalysisResults(); + }; + + // Listen for async vulnerability correlation results + const handleVulnerabilityReportReady = (data: { sessionId: string; tenantId: string; overallRisk: string }) => { + if (data.sessionId !== sessionIdRef.current) return; + console.info(`[SessionDetail] vulnerabilityReportReady signal: risk=${data.overallRisk}`); + scheduleFetchEvents(); // vulnerability_report is stored as an event + }; + on('eventStream', handleEventStream); on('newevents', handleNewEvents); + on('ruleResultsReady', handleRuleResultsReady); + on('vulnerabilityReportReady', handleVulnerabilityReportReady); return () => { off('eventStream', handleEventStream); off('newevents', handleNewEvents); + off('ruleResultsReady', handleRuleResultsReady); + off('vulnerabilityReportReady', handleVulnerabilityReportReady); if (eventRefreshTimeoutRef.current) { clearTimeout(eventRefreshTimeoutRef.current); } @@ -833,13 +853,16 @@ export default function SessionDetailPage() { if (!isGatherRulesSession) s.push({ id: "section-device-details", label: "Device Details", icon: }); if (!isGatherRulesSession && session) s.push({ id: "section-enrollment-progress", label: "Enrollment Progress", icon: }); if (!isGatherRulesSession) s.push({ id: "section-analysis", label: "Analysis", icon: }); + if (!isGatherRulesSession && events.some(e => e.eventType === "vulnerability_report" || e.eventType === "software_inventory_analysis")) { + s.push({ id: "section-vulnerability-report", label: "Vulnerability Report", icon: }); + } if (!isGatherRulesSession) s.push({ id: "section-performance", label: "Performance", icon: }); if (!isGatherRulesSession) s.push({ id: "section-scripts", label: "Script Executions", icon: }); if (!isGatherRulesSession) s.push({ id: "section-downloads", label: "Downloads", icon: }); if (!isGatherRulesSession) s.push({ id: "section-install-progress", label: "Install Progress", icon: }); s.push({ id: "section-event-timeline", label: "Event Timeline", icon: }); return s; - }, [session, isGatherRulesSession]); + }, [session, isGatherRulesSession, events]); usePageSections(sessionSections, "Sections", "scroll-spy"); @@ -995,6 +1018,17 @@ export default function SessionDetailPage() { )} + {/* Vulnerability Report */} + {!isGatherRulesSession && ( +
+ +
+ )} + {/* Performance Metrics (from performance_snapshot events) */} {!isGatherRulesSession && (
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx index 6a38e3b5..588a538f 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/AgentAnalyzersSection.tsx @@ -7,6 +7,8 @@ interface AgentAnalyzersSectionProps { setLocalAdminAllowedAccounts: (value: string[]) => void; newAllowedAccount: string; setNewAllowedAccount: (value: string) => void; + enableSoftwareInventoryAnalyzer: boolean; + setEnableSoftwareInventoryAnalyzer: (value: boolean) => void; onSave: () => void; onReset: () => void; saving: boolean; @@ -33,6 +35,8 @@ export default function AgentAnalyzersSection({ setLocalAdminAllowedAccounts, newAllowedAccount, setNewAllowedAccount, + enableSoftwareInventoryAnalyzer, + setEnableSoftwareInventoryAnalyzer, onSave, onReset, saving, @@ -150,6 +154,29 @@ export default function AgentAnalyzersSection({ )}
+ {/* Software Inventory & Vulnerability Analyzer */} +
+

+ The Software Inventory & Vulnerability Analyzer collects installed software from the + Windows registry at enrollment start and completion. It produces a normalized inventory snapshot, + detects software installed during enrollment (delta detection), and correlates findings against + known vulnerabilities (NVD CVEs, CISA KEV, MSRC). +

+
+ +
+
+

Software Inventory & Vulnerability Analyzer

+

Collect installed software inventory and correlate against known vulnerabilities

+
+ +
+ {/* Save / Reset */}