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 _options; private readonly ILogger _logger; private readonly string _sourceName; public CiscoOpenVulnClient( SourceFetchService fetchService, IOptionsMonitor options, ILogger 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 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 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(); 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 ReadStringArray(JsonElement element, string property) { if (!element.TryGetProperty(property, out var value) || value.ValueKind != JsonValueKind.Array) { return Array.Empty(); } var results = new List(); 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 Cves, IReadOnlyList BugIds, IReadOnlyList 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); } }