197 lines
7.0 KiB
C#
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);
|
|
}
|
|
}
|