Files
git.stella-ops.org/src/StellaOps.Concelier.Connector.Vndr.Cisco/Internal/CiscoOpenVulnClient.cs

197 lines
7.0 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Vndr.Cisco.Configuration;
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
public sealed class CiscoOpenVulnClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
private readonly SourceFetchService _fetchService;
private readonly IOptionsMonitor<CiscoOptions> _options;
private readonly ILogger<CiscoOpenVulnClient> _logger;
private readonly string _sourceName;
public CiscoOpenVulnClient(
SourceFetchService fetchService,
IOptionsMonitor<CiscoOptions> options,
ILogger<CiscoOpenVulnClient> logger,
string sourceName)
{
_fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_sourceName = sourceName ?? throw new ArgumentNullException(nameof(sourceName));
}
internal async Task<CiscoAdvisoryPage?> FetchAsync(DateOnly date, int pageIndex, CancellationToken cancellationToken)
{
var options = _options.CurrentValue;
var requestUri = options.BuildLastModifiedUri(date, pageIndex, options.PageSize);
var request = new SourceFetchRequest(CiscoOptions.HttpClientName, _sourceName, requestUri)
{
AcceptHeaders = new[] { "application/json" },
TimeoutOverride = options.RequestTimeout,
};
var result = await _fetchService.FetchContentAsync(request, cancellationToken).ConfigureAwait(false);
if (!result.IsSuccess || result.Content is null)
{
_logger.LogDebug("Cisco openVuln request returned empty payload for {Uri} (status {Status})", requestUri, result.StatusCode);
return null;
}
return CiscoAdvisoryPage.Parse(result.Content);
}
}
internal sealed record CiscoAdvisoryPage(
IReadOnlyList<CiscoAdvisoryItem> Advisories,
CiscoPagination Pagination)
{
public bool HasMore => Pagination.PageIndex < Pagination.TotalPages;
public static CiscoAdvisoryPage Parse(byte[] content)
{
using var document = JsonDocument.Parse(content);
var root = document.RootElement;
var advisories = new List<CiscoAdvisoryItem>();
if (root.TryGetProperty("advisories", out var advisoriesElement) && advisoriesElement.ValueKind == JsonValueKind.Array)
{
foreach (var advisory in advisoriesElement.EnumerateArray())
{
if (!TryCreateItem(advisory, out var item))
{
continue;
}
advisories.Add(item);
}
}
var pagination = CiscoPagination.FromJson(root.TryGetProperty("pagination", out var paginationElement) ? paginationElement : default);
return new CiscoAdvisoryPage(advisories, pagination);
}
private static bool TryCreateItem(JsonElement advisory, [NotNullWhen(true)] out CiscoAdvisoryItem? item)
{
var rawJson = advisory.GetRawText();
var advisoryId = GetString(advisory, "advisoryId");
if (string.IsNullOrWhiteSpace(advisoryId))
{
item = null;
return false;
}
var lastUpdated = ParseDate(GetString(advisory, "lastUpdated"));
var firstPublished = ParseDate(GetString(advisory, "firstPublished"));
var severity = GetString(advisory, "sir");
var publicationUrl = GetString(advisory, "publicationUrl");
var csafUrl = GetString(advisory, "csafUrl");
var cvrfUrl = GetString(advisory, "cvrfUrl");
var cvss = GetString(advisory, "cvssBaseScore");
var cves = ReadStringArray(advisory, "cves");
var bugIds = ReadStringArray(advisory, "bugIDs");
var productNames = ReadStringArray(advisory, "productNames");
item = new CiscoAdvisoryItem(
advisoryId,
lastUpdated,
firstPublished,
severity,
publicationUrl,
csafUrl,
cvrfUrl,
cvss,
cves,
bugIds,
productNames,
rawJson);
return true;
}
private static string? GetString(JsonElement element, string propertyName)
=> element.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
private static DateTimeOffset? ParseDate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
if (DateTimeOffset.TryParse(value, out var parsed))
{
return parsed.ToUniversalTime();
}
return null;
}
private static IReadOnlyList<string> ReadStringArray(JsonElement element, string property)
{
if (!element.TryGetProperty(property, out var value) || value.ValueKind != JsonValueKind.Array)
{
return Array.Empty<string>();
}
var results = new List<string>();
foreach (var child in value.EnumerateArray())
{
if (child.ValueKind == JsonValueKind.String)
{
var text = child.GetString();
if (!string.IsNullOrWhiteSpace(text))
{
results.Add(text.Trim());
}
}
}
return results;
}
}
internal sealed record CiscoAdvisoryItem(
string AdvisoryId,
DateTimeOffset? LastUpdated,
DateTimeOffset? FirstPublished,
string? Severity,
string? PublicationUrl,
string? CsafUrl,
string? CvrfUrl,
string? CvssBaseScore,
IReadOnlyList<string> Cves,
IReadOnlyList<string> BugIds,
IReadOnlyList<string> ProductNames,
string RawJson)
{
public byte[] GetRawBytes() => Encoding.UTF8.GetBytes(RawJson);
}
internal sealed record CiscoPagination(int PageIndex, int PageSize, int TotalPages, int TotalRecords)
{
public static CiscoPagination FromJson(JsonElement element)
{
var pageIndex = element.TryGetProperty("pageIndex", out var index) && index.TryGetInt32(out var parsedIndex) ? parsedIndex : 1;
var pageSize = element.TryGetProperty("pageSize", out var size) && size.TryGetInt32(out var parsedSize) ? parsedSize : 0;
var totalPages = element.TryGetProperty("totalPages", out var pages) && pages.TryGetInt32(out var parsedPages) ? parsedPages : pageIndex;
var totalRecords = element.TryGetProperty("totalRecords", out var records) && records.TryGetInt32(out var parsedRecords) ? parsedRecords : 0;
return new CiscoPagination(pageIndex, pageSize, totalPages, totalRecords);
}
}