diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityCorrelationServiceTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityCorrelationServiceTests.cs index 53243da0..b43fd30f 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityCorrelationServiceTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityCorrelationServiceTests.cs @@ -102,7 +102,7 @@ public class VulnerabilityCorrelationServiceTests private VulnerabilityCorrelationService CreateServiceForFilterTests() { // FilterByVersion doesn't use any dependencies — we can pass nulls - return new VulnerabilityCorrelationService(null!, null!, null!, null!, null!, null!, null!); + return new VulnerabilityCorrelationService(null!, null!, null!, null!, null!, null!, null, null!); } [Fact] @@ -430,7 +430,7 @@ public class VulnerabilityCorrelationServiceTests private VulnerabilityCorrelationService CreateServiceWithMappings(List mappings) { - var svc = new VulnerabilityCorrelationService(null!, null!, null!, null!, null!, null!, null!); + var svc = new VulnerabilityCorrelationService(null!, null!, null!, null!, null!, null!, null, null!); svc._cpeMappings = mappings; return svc; } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs index 8aa334f2..87d3a77a 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/VulnerabilityDataSyncFunction.cs @@ -7,6 +7,7 @@ using AutopilotMonitor.Functions.DataAccess.TableStorage; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Functions.Services.Vulnerability; using AutopilotMonitor.Shared.DataAccess; +using Microsoft.ApplicationInsights; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; @@ -25,21 +26,27 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability { private readonly KevDataService _kevService; private readonly NvdApiClient _nvdApi; + private readonly MsrcCorrelationService _msrcCorrelation; private readonly IVulnerabilityRepository _vulnRepo; private readonly AdminConfigurationService _adminConfigService; + private readonly TelemetryClient? _telemetryClient; private readonly ILogger _logger; public VulnerabilityDataSyncFunction( KevDataService kevService, NvdApiClient nvdApi, + MsrcCorrelationService msrcCorrelation, IVulnerabilityRepository vulnRepo, AdminConfigurationService adminConfigService, + TelemetryClient? telemetryClient, ILogger logger) { _kevService = kevService; _nvdApi = nvdApi; + _msrcCorrelation = msrcCorrelation; _vulnRepo = vulnRepo; _adminConfigService = adminConfigService; + _telemetryClient = telemetryClient; _logger = logger; } @@ -50,23 +57,56 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability public async Task Run( [TimerTrigger("0 0 3 * * *")] TimerInfo timer) { + var startedAt = DateTime.UtcNow; _logger.LogInformation("[VulnSync] Starting vulnerability data sync..."); + int kevCount = 0; + string? kevError = null; + int cveRefreshed = 0; + int cveFailed = 0; + int cveStale = 0; + int msrcDocs = 0; + int msrcCves = 0; + string? msrcError = null; + // 1. Refresh CISA KEV catalog try { await _kevService.RefreshAsync(); - _logger.LogInformation("[VulnSync] KEV catalog refreshed: {Count} entries", _kevService.CatalogSize); + kevCount = _kevService.CatalogSize; + _logger.LogInformation("[VulnSync] KEV catalog refreshed: {Count} entries", kevCount); } catch (Exception ex) { + kevError = ex.Message; _logger.LogWarning(ex, "[VulnSync] KEV catalog refresh failed"); } - // 2. Refresh stale CVE cache entries - await RefreshStaleCveCacheAsync(); + // 2. Refresh MSRC CVE index (last 6 Patch Tuesdays). + try + { + var msrc = await _msrcCorrelation.RefreshAsync(); + msrcDocs = msrc.DocumentsFetched; + msrcCves = msrc.IndexedCves; + if (!string.IsNullOrEmpty(msrc.Error)) + msrcError = msrc.Error; + _logger.LogInformation( + "[VulnSync] MSRC index refreshed: {Cves} CVEs across {Docs} CVRF documents (failed={Failed}, ms={Ms:F0})", + msrc.IndexedCves, msrc.DocumentsFetched, msrc.DocumentsFailed, msrc.DurationMs); + } + catch (Exception ex) + { + msrcError = ex.Message; + _logger.LogWarning(ex, "[VulnSync] MSRC index refresh failed"); + } - // 3. Update sync timestamp + // 3. Refresh stale CVE cache entries + var cveStats = await RefreshStaleCveCacheAsync(); + cveStale = cveStats.Stale; + cveRefreshed = cveStats.Refreshed; + cveFailed = cveStats.Failed; + + // 4. Update sync timestamp try { var config = await _adminConfigService.GetConfigurationAsync(); @@ -81,7 +121,30 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability _logger.LogWarning(ex, "[VulnSync] Failed to update sync timestamp"); } - _logger.LogInformation("[VulnSync] Vulnerability data sync completed"); + var durationMs = (DateTime.UtcNow - startedAt).TotalMilliseconds; + _logger.LogInformation("[VulnSync] Vulnerability data sync completed in {DurationMs:F0}ms", durationMs); + + // Sampling-bypass observability: host.json adds "Event" to excludedTypes so this + // survives even when default trace sampling is aggressive. + _telemetryClient?.TrackEvent("VulnerabilityDataSyncCompleted", new Dictionary + { + { "kevCount", kevCount.ToString() }, + { "kevError", kevError ?? "" }, + { "msrcDocs", msrcDocs.ToString() }, + { "msrcCves", msrcCves.ToString() }, + { "msrcError", msrcError ?? "" }, + { "cveStale", cveStale.ToString() }, + { "cveRefreshed", cveRefreshed.ToString() }, + { "cveFailed", cveFailed.ToString() }, + { "durationMs", durationMs.ToString("F0", System.Globalization.CultureInfo.InvariantCulture) } + }); + } + + private struct CveCacheStats + { + public int Stale; + public int Refreshed; + public int Failed; } /// @@ -89,8 +152,9 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability /// and refreshes them by re-querying the NVD API. Processes sequentially with /// rate limiting to avoid NVD API throttling. /// - private async Task RefreshStaleCveCacheAsync() + private async Task RefreshStaleCveCacheAsync() { + var stats = new CveCacheStats(); try { var staleCutoff = DateTime.UtcNow.AddDays(-7).ToString("o"); @@ -116,10 +180,12 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability } } + stats.Stale = staleEntries.Count; + if (staleEntries.Count == 0) { _logger.LogInformation("[VulnSync] CVE cache: no stale entries found"); - return; + return stats; } _logger.LogInformation("[VulnSync] CVE cache: {StaleCount} stale entries to refresh", staleEntries.Count); @@ -227,11 +293,16 @@ namespace AutopilotMonitor.Functions.Functions.Vulnerability _logger.LogInformation("[VulnSync] CVE cache refresh complete: {Refreshed} refreshed, {Failed} failed out of {Total} stale entries", refreshed, failed, staleEntries.Count); + + stats.Refreshed = refreshed; + stats.Failed = failed; } catch (Exception ex) { _logger.LogWarning(ex, "[VulnSync] CVE cache refresh scan failed"); } + + return stats; } } } diff --git a/src/Backend/AutopilotMonitor.Functions/Program.cs b/src/Backend/AutopilotMonitor.Functions/Program.cs index 51263f7b..74bf3035 100644 --- a/src/Backend/AutopilotMonitor.Functions/Program.cs +++ b/src/Backend/AutopilotMonitor.Functions/Program.cs @@ -193,6 +193,7 @@ builder.Services.AddHttpClient sp.GetRequiredService().ExternalDataApi); builder.Services.AddHttpClient() .AddPolicyHandler((sp, _) => sp.GetRequiredService().ExternalDataApi); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Register agent Function classes so bootstrap wrappers can inject them for code reuse diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcCorrelationService.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcCorrelationService.cs new file mode 100644 index 00000000..e6e6a1d3 --- /dev/null +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/MsrcCorrelationService.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace AutopilotMonitor.Functions.Services.Vulnerability +{ + /// + /// Builds an in-memory index of CVEs published in recent MSRC monthly CVRF documents + /// (Patch Tuesday releases). Refreshed daily by VulnerabilityDataSyncFunction; + /// queried per-CVE during session correlation to enrich findings with Microsoft-source + /// metadata (release date, MSRC severity, CVRF document title). + /// + /// MSRC has no per-CVE lookup API — the only available shape is whole-month CVRF + /// documents. Building an index up-front keeps correlation O(1) per CVE. + /// + public class MsrcCorrelationService + { + private readonly MsrcApiClient _msrcApi; + private readonly ILogger _logger; + + // Concurrent because correlation may read while a parallel refresh writes. + private ConcurrentDictionary _index = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + private DateTime _lastRefresh = DateTime.MinValue; + private readonly List _coveredDocuments = new List(); + + private static readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1); + + /// How many recent CVRF documents to index. Each = ~one Patch Tuesday. + public const int DefaultMonthsToIndex = 6; + + public MsrcCorrelationService(MsrcApiClient msrcApi, ILogger logger) + { + _msrcApi = msrcApi ?? throw new ArgumentNullException(nameof(msrcApi)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// Number of CVEs currently indexed. + public int IndexSize => _index.Count; + + /// UTC timestamp of the last successful refresh. + public DateTime LastRefresh => _lastRefresh; + + /// List of CVRF document IDs that contributed to the current index. + public IReadOnlyList CoveredDocuments => _coveredDocuments.AsReadOnly(); + + /// + /// Returns the MSRC entry for a CVE, or null if not present in the indexed window. + /// "Not in index" does not mean "not Microsoft" — it just means the CVE was not + /// published in the last Patch Tuesdays. + /// + public MsrcCveEntry? Lookup(string? cveId) + { + if (string.IsNullOrWhiteSpace(cveId)) return null; + return _index.TryGetValue(cveId, out var entry) ? entry : null; + } + + /// + /// Rebuilds the index from the most recent MSRC CVRF documents. Safe to call + /// concurrently — only one refresh runs at a time; others observe the prior index. + /// + public async Task RefreshAsync(int monthsToIndex = DefaultMonthsToIndex, CancellationToken ct = default) + { + var result = new MsrcRefreshResult { StartedAt = DateTime.UtcNow }; + + if (!await _refreshLock.WaitAsync(0, ct)) + { + // Another refresh is in progress — return the current state without waiting. + result.Skipped = true; + result.IndexedCves = _index.Count; + result.CoveredDocuments = _coveredDocuments.Count; + return result; + } + + try + { + var updates = await _msrcApi.GetRecentUpdatesAsync(ct); + if (updates.Count == 0) + { + _logger.LogWarning("MSRC refresh: GetRecentUpdatesAsync returned 0 documents — keeping prior index of {Size} CVEs", + _index.Count); + result.IndexedCves = _index.Count; + return result; + } + + // Pick the most-recent N documents by InitialReleaseDate (descending). + var pickedDocs = updates + .Where(u => !string.IsNullOrWhiteSpace(u.ID)) + .OrderByDescending(u => u.InitialReleaseDate ?? "", StringComparer.Ordinal) + .Take(monthsToIndex) + .ToList(); + + var newIndex = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + var newCovered = new List(); + int docsFetched = 0; + int docsFailed = 0; + int cvesIndexed = 0; + + foreach (var summary in pickedDocs) + { + if (ct.IsCancellationRequested) break; + + var doc = await _msrcApi.GetCvrfAsync(summary.ID!, ct); + if (doc == null) + { + docsFailed++; + continue; + } + + docsFetched++; + newCovered.Add(summary.ID!); + + var docTitle = doc.DocumentTitle?.Value ?? summary.DocumentTitle ?? summary.ID!; + var releaseDate = summary.InitialReleaseDate ?? ""; + + foreach (var v in doc.Vulnerability) + { + var cveId = v.CVE?.Value; + if (string.IsNullOrWhiteSpace(cveId)) continue; + + // Pick the maximum MSRC base score across product-specific score sets. + double? bestScore = null; + string? bestVector = null; + if (v.CVSSScoreSets != null) + { + foreach (var s in v.CVSSScoreSets) + { + if (bestScore == null || s.BaseScore > bestScore.Value) + { + bestScore = s.BaseScore; + bestVector = s.Vector; + } + } + } + + // MSRC severity comes from a Threat with Type="Severity". + var severity = v.Threats? + .FirstOrDefault(t => string.Equals(t.Type, "Severity", StringComparison.OrdinalIgnoreCase)) + ?.Description?.Value; + + var entry = new MsrcCveEntry + { + CveId = cveId!, + DocumentId = summary.ID!, + DocumentTitle = docTitle, + ReleaseDate = releaseDate, + BaseScore = bestScore, + Vector = bestVector, + Severity = severity, + Title = v.Title?.Value + }; + + // Last-write-wins is fine — same CVE in multiple docs is rare and the + // newest doc wins because we iterate in descending date order. + newIndex[cveId!] = entry; + cvesIndexed++; + } + } + + // Atomic swap — readers observe either the old or the new index, never half. + _index = newIndex; + _coveredDocuments.Clear(); + _coveredDocuments.AddRange(newCovered); + _lastRefresh = DateTime.UtcNow; + + result.DocumentsFetched = docsFetched; + result.DocumentsFailed = docsFailed; + result.IndexedCves = cvesIndexed; + result.CoveredDocuments = newCovered.Count; + + _logger.LogInformation( + "MSRC refresh: indexed {Cves} CVEs across {Docs} CVRF documents ({Failed} failed)", + cvesIndexed, docsFetched, docsFailed); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + result.Error = ex.Message; + _logger.LogWarning(ex, "MSRC refresh failed"); + } + finally + { + _refreshLock.Release(); + result.CompletedAt = DateTime.UtcNow; + } + + return result; + } + } + + public class MsrcCveEntry + { + public string CveId { get; set; } = ""; + public string DocumentId { get; set; } = ""; + public string DocumentTitle { get; set; } = ""; + public string ReleaseDate { get; set; } = ""; + public double? BaseScore { get; set; } + public string? Vector { get; set; } + public string? Severity { get; set; } + public string? Title { get; set; } + } + + public class MsrcRefreshResult + { + public DateTime StartedAt { get; set; } + public DateTime CompletedAt { get; set; } + public bool Skipped { get; set; } + public int DocumentsFetched { get; set; } + public int DocumentsFailed { get; set; } + public int IndexedCves { get; set; } + public int CoveredDocuments { get; set; } + public string? Error { get; set; } + + public double DurationMs => (CompletedAt - StartedAt).TotalMilliseconds; + } +} diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs index ce3b73ab..3c43febb 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelationService.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using AutopilotMonitor.Functions.DataAccess.TableStorage; using AutopilotMonitor.Shared.DataAccess; +using Microsoft.ApplicationInsights; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Services.Vulnerability @@ -17,8 +18,9 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability private readonly ISessionRepository _sessionRepo; private readonly NvdApiClient _nvdApi; private readonly KevDataService _kevService; - private readonly MsrcApiClient _msrcApi; + private readonly MsrcCorrelationService _msrcCorrelation; private readonly AdminConfigurationService _adminConfig; + private readonly TelemetryClient? _telemetryClient; private readonly ILogger _logger; internal List? _cpeMappings; @@ -37,16 +39,18 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability ISessionRepository sessionRepo, NvdApiClient nvdApi, KevDataService kevService, - MsrcApiClient msrcApi, + MsrcCorrelationService msrcCorrelation, AdminConfigurationService adminConfig, + TelemetryClient? telemetryClient, ILogger logger) { _vulnRepo = vulnRepo; _sessionRepo = sessionRepo; _nvdApi = nvdApi; _kevService = kevService; - _msrcApi = msrcApi; + _msrcCorrelation = msrcCorrelation; _adminConfig = adminConfig; + _telemetryClient = telemetryClient; _logger = logger; } @@ -78,6 +82,7 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability int mediumCves = 0; int lowCves = 0; int kevMatches = 0; + int msrcMatches = 0; string overallRisk = "none"; var cpeMappingsDict = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -140,7 +145,7 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability continue; } - // Check KEV for each CVE + // Check KEV + MSRC for each CVE in a single pass. foreach (var vuln in vulns) { var cveId = vuln["cveId"]?.ToString(); @@ -157,6 +162,26 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability { vuln["isKev"] = false; } + + // MSRC enrichment — only present if the CVE was published in a recent + // Patch Tuesday document. Augments source label and adds Microsoft-side + // metadata so the UI can show "confirmed by Microsoft" alongside NVD data. + var msrcEntry = _msrcCorrelation?.Lookup(cveId); + if (msrcEntry != null) + { + vuln["source"] = "NVD+MSRC"; + vuln["msrcDocumentId"] = msrcEntry.DocumentId; + vuln["msrcDocumentTitle"] = msrcEntry.DocumentTitle; + vuln["msrcReleaseDate"] = msrcEntry.ReleaseDate; + vuln["msrcSeverity"] = msrcEntry.Severity ?? ""; + if (msrcEntry.BaseScore.HasValue) + vuln["msrcBaseScore"] = msrcEntry.BaseScore.Value; + if (!string.IsNullOrEmpty(msrcEntry.Vector)) + vuln["msrcVector"] = msrcEntry.Vector; + if (!string.IsNullOrEmpty(msrcEntry.Title)) + vuln["msrcTitle"] = msrcEntry.Title; + msrcMatches++; + } } // Count severities @@ -228,9 +253,28 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability // Summary log for production monitoring _logger.LogInformation( "Vulnerability correlation complete: session={SessionId}, scanned={Scanned}, matched={Matched}, unmatched={Unmatched}, " + - "cves={TotalCves} (critical={Critical}, high={High}, medium={Medium}, low={Low}), kev={Kev}, risk={Risk}", + "cves={TotalCves} (critical={Critical}, high={High}, medium={Medium}, low={Low}), kev={Kev}, msrc={Msrc}, risk={Risk}", sessionId, inventoryItems.Count, matchedCount, unmatchedSoftware.Count, - totalCves, criticalCves, highCves, mediumCves, lowCves, kevMatches, overallRisk); + totalCves, criticalCves, highCves, mediumCves, lowCves, kevMatches, msrcMatches, overallRisk); + + // Sampling-bypass observability event (host.json excludedTypes includes "Event"). + // Mirrors the ILogger summary above in a structured, queryable form. + _telemetryClient?.TrackEvent("VulnerabilityCorrelationCompleted", new Dictionary + { + { "tenantId", tenantId }, + { "sessionId", sessionId }, + { "scanned", inventoryItems.Count.ToString() }, + { "matched", matchedCount.ToString() }, + { "unmatched", unmatchedSoftware.Count.ToString() }, + { "totalCves", totalCves.ToString() }, + { "critical", criticalCves.ToString() }, + { "high", highCves.ToString() }, + { "medium", mediumCves.ToString() }, + { "low", lowCves.ToString() }, + { "kev", kevMatches.ToString() }, + { "msrc", msrcMatches.ToString() }, + { "risk", overallRisk } + }); // No findings = no report (same as RuleResults: don't store empty results) if (findings.Count == 0) @@ -257,6 +301,7 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability { "medium_cves", mediumCves }, { "low_cves", lowCves }, { "kev_matches", kevMatches }, + { "msrc_matches", msrcMatches }, { "overall_risk", overallRisk }, { "additional_findings_count", additionalCount } } @@ -268,7 +313,9 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability { { "cpe_cache_entries", _cpeMappings?.Count ?? 0 }, { "kev_catalog_entries", _kevService.CatalogSize }, - { "kev_last_sync", _kevService.LastRefresh.ToString("o") } + { "kev_last_sync", _kevService.LastRefresh.ToString("o") }, + { "msrc_indexed_cves", _msrcCorrelation?.IndexSize ?? 0 }, + { "msrc_last_sync", _msrcCorrelation?.LastRefresh.ToString("o") ?? "" } } } }; @@ -411,6 +458,13 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability _logger.LogInformation( "CPE auto-resolve MISS: '{DisplayName}' — {CandidateCount} NVD candidates, none above threshold 0.6 (cached for {CacheDays}d)", displayName, candidateCount, AutoResolveFailedCacheDays); + _telemetryClient?.TrackEvent("CpeAutoResolveMiss", new Dictionary + { + { "displayName", displayName ?? "" }, + { "normalizedVendor", normalizedVendor ?? "" }, + { "normalizedName", normalizedName ?? "" }, + { "candidateCount", candidateCount.ToString() } + }); return null; } @@ -428,6 +482,15 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability _logger.LogInformation( "CPE auto-resolve HIT: '{DisplayName}' -> {CpeUri} (score={Score:F2}, candidates={CandidateCount}, cpeName={CpeName})", displayName, mapping.CpeUri, score, candidateCount, cpeEntry.CpeName); + _telemetryClient?.TrackEvent("CpeAutoResolveHit", new Dictionary + { + { "displayName", displayName ?? "" }, + { "normalizedVendor", normalizedVendor ?? "" }, + { "normalizedName", normalizedName ?? "" }, + { "cpeUri", mapping.CpeUri ?? "" }, + { "score", score.ToString("F2", System.Globalization.CultureInfo.InvariantCulture) }, + { "candidateCount", candidateCount.ToString() } + }); // Fire-and-forget: persist the mapping for future sessions _ = Task.Run(async () => @@ -457,6 +520,13 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability await _vulnRepo.UpdateSoftwareInventoryCpeAsync(mapping.NormalizedProduct ?? "", mapping.CpeUri ?? ""); _logger.LogInformation("Persisted auto-resolved CPE mapping: {RowKey} -> {CpeUri}", rowKey, mapping.CpeUri); + _telemetryClient?.TrackEvent("CpeMappingPersisted", new Dictionary + { + { "rowKey", rowKey }, + { "cpeUri", mapping.CpeUri ?? "" }, + { "normalizedVendor", mapping.NormalizedVendor ?? "" }, + { "normalizedProduct", mapping.NormalizedProduct ?? "" } + }); } catch (Exception ex) { diff --git a/src/Backend/AutopilotMonitor.Functions/host.json b/src/Backend/AutopilotMonitor.Functions/host.json index 0db4854f..36450f23 100644 --- a/src/Backend/AutopilotMonitor.Functions/host.json +++ b/src/Backend/AutopilotMonitor.Functions/host.json @@ -18,7 +18,7 @@ "applicationInsights": { "samplingSettings": { "isEnabled": true, - "excludedTypes": "Request" + "excludedTypes": "Request;Event" }, "enableLiveMetricsFilters": true }