Files
AutopilotMonitor/src/Backend/AutopilotMonitor.Functions/Functions/Vulnerability/GetUnmatchedSoftwareFunction.cs
T

140 lines
6.1 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
public class GetUnmatchedSoftwareFunction
{
private readonly ILogger<GetUnmatchedSoftwareFunction> _logger;
private readonly TableStorageService _storageService;
private readonly GalacticAdminService _galacticAdminService;
public GetUnmatchedSoftwareFunction(
ILogger<GetUnmatchedSoftwareFunction> logger,
TableStorageService storageService,
GalacticAdminService galacticAdminService)
{
_logger = logger;
_storageService = storageService;
_galacticAdminService = galacticAdminService;
}
[Function("GetUnmatchedSoftware")]
public async Task<HttpResponseData> 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<string, UnmatchedSoftwareAgg>(StringComparer.OrdinalIgnoreCase);
var tableClient = _storageService.GetTableClient(Constants.TableNames.Events);
var recentEvents = tableClient.QueryAsync<TableEntity>(
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<Dictionary<string, object>>(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<List<Dictionary<string, string>>>(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; }
}
}
}