mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
140 lines
6.1 KiB
C#
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; }
|
|
}
|
|
}
|
|
}
|