using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.Json; using System.Threading.Tasks; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Functions.Services.Vulnerability; using AutopilotMonitor.Shared.DataAccess; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Functions.Vulnerability { /// /// Bulk auto-resolve unmapped software by searching the NVD CPE dictionary. /// Accepts up to 25 items per request. For each item, searches NVD and saves /// the best CPE match as a custom mapping if the score meets the threshold. /// public class AutoResolveCpeMappingFunction { private readonly NvdApiClient _nvdApi; private readonly IVulnerabilityRepository _vulnRepo; private readonly ILogger _logger; private const int MaxItemsPerRequest = 25; public AutoResolveCpeMappingFunction( NvdApiClient nvdApi, IVulnerabilityRepository vulnRepo, ILogger logger) { _nvdApi = nvdApi; _vulnRepo = vulnRepo; _logger = logger; } [Function("AutoResolveCpeMapping")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "vulnerability/cpe-mapping/auto-resolve")] HttpRequestData req) { try { // Global Admin only — enforced by PolicyEnforcementMiddleware var userIdentifier = TenantHelper.GetUserIdentifier(req); var body = await req.ReadAsStringAsync(); var request = JsonSerializer.Deserialize(body ?? "", new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (request?.Items == null || request.Items.Count == 0) { var badReq = req.CreateResponse(HttpStatusCode.BadRequest); await badReq.WriteAsJsonAsync(new { success = false, message = "items array is required" }); return badReq; } if (request.Items.Count > MaxItemsPerRequest) { var badReq = req.CreateResponse(HttpStatusCode.BadRequest); await badReq.WriteAsJsonAsync(new { success = false, message = $"Maximum {MaxItemsPerRequest} items per request" }); return badReq; } // Deduplicate by softwareName var uniqueItems = request.Items .GroupBy(i => (i.SoftwareName ?? "").ToLowerInvariant()) .Select(g => g.First()) .ToList(); var resolved = new List(); var failed = new List(); foreach (var item in uniqueItems) { if (string.IsNullOrWhiteSpace(item.SoftwareName)) { _logger.LogWarning("Bulk auto-resolve: skipping item with empty name"); failed.Add(new AutoResolveFailedItem { SoftwareName = item.SoftwareName ?? "", Reason = "empty_name" }); continue; } try { var keyword = CpeMatchScorer.BuildSearchKeyword(item.SoftwareName, item.NormalizedVendor ?? item.Publisher ?? ""); if (string.IsNullOrWhiteSpace(keyword)) { failed.Add(new AutoResolveFailedItem { SoftwareName = item.SoftwareName, Reason = "empty_keyword" }); continue; } var cpeFetch = await _nvdApi.SearchCpeAsync(keyword); if (!cpeFetch.Succeeded) { failed.Add(new AutoResolveFailedItem { SoftwareName = item.SoftwareName, Reason = cpeFetch.Throttled ? "nvd_throttled" : "nvd_unavailable" }); continue; } var cpeResponse = cpeFetch.Data; var bestMatch = CpeMatchScorer.SelectBestMatch( cpeResponse.Products, item.NormalizedVendor ?? item.Publisher ?? "", item.SoftwareName); if (bestMatch == null) { failed.Add(new AutoResolveFailedItem { SoftwareName = item.SoftwareName, Reason = cpeResponse.Products?.Count > 0 ? "no_match" : "no_results" }); continue; } var (cpeEntry, score) = bestMatch.Value; var mapping = CpeMatchScorer.BuildMappingFromNvdResult( cpeEntry, item.NormalizedVendor ?? item.Publisher ?? "", item.SoftwareName, item.SoftwareName); // Save as custom mapping var rowKey = $"{(mapping.NormalizedVendor ?? "unknown")}:{mapping.NormalizedProduct}" .ToLowerInvariant() .Replace("/", "_").Replace("\\", "_").Replace("#", "_").Replace("?", "_"); var properties = new Dictionary { { "NormalizedVendor", mapping.NormalizedVendor ?? "" }, { "NormalizedProduct", mapping.NormalizedProduct ?? "" }, { "CpeVendor", mapping.CpeVendor ?? "" }, { "CpeProduct", mapping.CpeProduct ?? "" }, { "CpeUri", mapping.CpeUri ?? "" }, { "Category", "auto" }, { "DisplayNamePatternsJson", JsonSerializer.Serialize(mapping.DisplayNamePatterns) }, { "PublisherPatternsJson", JsonSerializer.Serialize(mapping.PublisherPatterns) }, { "ExcludePatternsJson", JsonSerializer.Serialize(new List()) }, { "Source", "auto" }, { "CreatedAt", DateTime.UtcNow.ToString("o") } }; await _vulnRepo.UpsertCpeMappingAsync("cpe_map_custom", rowKey, properties); await _vulnRepo.UpdateSoftwareInventoryCpeAsync(mapping.NormalizedProduct ?? "", mapping.CpeUri ?? ""); resolved.Add(new AutoResolveResultItem { SoftwareName = item.SoftwareName, CpeUri = mapping.CpeUri ?? "", Confidence = Math.Round(score, 2) }); _logger.LogInformation("Bulk auto-resolved: '{SoftwareName}' -> {CpeUri} (score={Score:F2})", item.SoftwareName, mapping.CpeUri, score); } catch (Exception ex) { _logger.LogWarning(ex, "Bulk auto-resolve failed for '{SoftwareName}'", item.SoftwareName); failed.Add(new AutoResolveFailedItem { SoftwareName = item.SoftwareName, Reason = "nvd_api_error" }); } } // Reset mappings cache so future correlations pick up new mappings if (resolved.Count > 0) { VulnerabilityCorrelationService.ResetMappingsCache(); } _logger.LogInformation( "Bulk auto-resolve complete: processed={Processed}, resolved={Resolved}, failed={Failed} " + "(reasons: {FailedReasons})", resolved.Count + failed.Count, resolved.Count, failed.Count, string.Join(", ", failed.GroupBy(f => f.Reason).Select(g => $"{g.Key}={g.Count()}"))); var response = req.CreateResponse(HttpStatusCode.OK); await response.WriteAsJsonAsync(new { resolved, failed, totalProcessed = resolved.Count + failed.Count, totalResolved = resolved.Count, totalFailed = failed.Count }); return response; } catch (Exception ex) { _logger.LogError(ex, "AutoResolveCpeMapping failed"); var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError); await errorResponse.WriteAsJsonAsync(new { success = false, message = "Auto-resolve failed" }); return errorResponse; } } // --- Request/Response models --- private class AutoResolveRequest { public List Items { get; set; } = new(); } private class AutoResolveItem { public string SoftwareName { get; set; } = ""; public string? Publisher { get; set; } public string? NormalizedVendor { get; set; } } private class AutoResolveResultItem { public string SoftwareName { get; set; } = ""; public string CpeUri { get; set; } = ""; public double Confidence { get; set; } } private class AutoResolveFailedItem { public string SoftwareName { get; set; } = ""; public string Reason { get; set; } = ""; } } }