feat(backend): wire MSRC into correlation + sampling-bypass observability

MsrcApiClient was injected into VulnerabilityCorrelationService for months
but never invoked — every report shipped with source="NVD" only. New
MsrcCorrelationService owns refresh + per-CVE lookup against an in-memory
index built from the last 6 Patch Tuesdays; daily VulnerabilityDataSync
populates it; CorrelateAsync enriches each finding with msrcDocumentId,
msrcReleaseDate, msrcSeverity, msrcBaseScore, msrcVector and flips source
to "NVD+MSRC" when the CVE was published in the indexed window.

Health check also surfaced that all worker-side traces of the daily sync
and CPE auto-resolve were sampled out of App Insights despite running.
Adds Event to host.json excludedTypes and emits TelemetryClient events
for VulnerabilityDataSyncCompleted, VulnerabilityCorrelationCompleted,
CpeAutoResolveHit/Miss, CpeMappingPersisted so customEvents queries can
prove freshness without depending on sampled traces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-04-28 13:16:22 +02:00
parent bba5761d14
commit be63dad534
6 changed files with 383 additions and 17 deletions
@@ -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<CpeMappingEntry> 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;
}
@@ -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<VulnerabilityDataSyncFunction> _logger;
public VulnerabilityDataSyncFunction(
KevDataService kevService,
NvdApiClient nvdApi,
MsrcCorrelationService msrcCorrelation,
IVulnerabilityRepository vulnRepo,
AdminConfigurationService adminConfigService,
TelemetryClient? telemetryClient,
ILogger<VulnerabilityDataSyncFunction> 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<string, string>
{
{ "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;
}
/// <summary>
@@ -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.
/// </summary>
private async Task RefreshStaleCveCacheAsync()
private async Task<CveCacheStats> 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;
}
}
}
@@ -193,6 +193,7 @@ builder.Services.AddHttpClient<AutopilotMonitor.Functions.Services.Vulnerability
.AddPolicyHandler((sp, _) => sp.GetRequiredService<ResiliencePolicies>().ExternalDataApi);
builder.Services.AddHttpClient<AutopilotMonitor.Functions.Services.Vulnerability.MsrcApiClient>()
.AddPolicyHandler((sp, _) => sp.GetRequiredService<ResiliencePolicies>().ExternalDataApi);
builder.Services.AddSingleton<AutopilotMonitor.Functions.Services.Vulnerability.MsrcCorrelationService>();
builder.Services.AddSingleton<AutopilotMonitor.Functions.Services.Vulnerability.VulnerabilityCorrelationService>();
// Register agent Function classes so bootstrap wrappers can inject them for code reuse
@@ -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
{
/// <summary>
/// Builds an in-memory index of CVEs published in recent MSRC monthly CVRF documents
/// (Patch Tuesday releases). Refreshed daily by <c>VulnerabilityDataSyncFunction</c>;
/// 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.
/// </summary>
public class MsrcCorrelationService
{
private readonly MsrcApiClient _msrcApi;
private readonly ILogger<MsrcCorrelationService> _logger;
// Concurrent because correlation may read while a parallel refresh writes.
private ConcurrentDictionary<string, MsrcCveEntry> _index =
new ConcurrentDictionary<string, MsrcCveEntry>(StringComparer.OrdinalIgnoreCase);
private DateTime _lastRefresh = DateTime.MinValue;
private readonly List<string> _coveredDocuments = new List<string>();
private static readonly SemaphoreSlim _refreshLock = new SemaphoreSlim(1, 1);
/// <summary>How many recent CVRF documents to index. Each = ~one Patch Tuesday.</summary>
public const int DefaultMonthsToIndex = 6;
public MsrcCorrelationService(MsrcApiClient msrcApi, ILogger<MsrcCorrelationService> logger)
{
_msrcApi = msrcApi ?? throw new ArgumentNullException(nameof(msrcApi));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>Number of CVEs currently indexed.</summary>
public int IndexSize => _index.Count;
/// <summary>UTC timestamp of the last successful refresh.</summary>
public DateTime LastRefresh => _lastRefresh;
/// <summary>List of CVRF document IDs that contributed to the current index.</summary>
public IReadOnlyList<string> CoveredDocuments => _coveredDocuments.AsReadOnly();
/// <summary>
/// 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 <see cref="DefaultMonthsToIndex"/> Patch Tuesdays.
/// </summary>
public MsrcCveEntry? Lookup(string? cveId)
{
if (string.IsNullOrWhiteSpace(cveId)) return null;
return _index.TryGetValue(cveId, out var entry) ? entry : null;
}
/// <summary>
/// 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.
/// </summary>
public async Task<MsrcRefreshResult> 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<string, MsrcCveEntry>(StringComparer.OrdinalIgnoreCase);
var newCovered = new List<string>();
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;
}
}
@@ -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<VulnerabilityCorrelationService> _logger;
internal List<CpeMappingEntry>? _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<VulnerabilityCorrelationService> 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<string, string?>(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<string, string>
{
{ "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<string, string>
{
{ "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<string, string>
{
{ "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<string, string>
{
{ "rowKey", rowKey },
{ "cpeUri", mapping.CpeUri ?? "" },
{ "normalizedVendor", mapping.NormalizedVendor ?? "" },
{ "normalizedProduct", mapping.NormalizedProduct ?? "" }
});
}
catch (Exception ex)
{
@@ -18,7 +18,7 @@
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
"excludedTypes": "Request;Event"
},
"enableLiveMetricsFilters": true
}