174 lines
6.0 KiB
C#
174 lines
6.0 KiB
C#
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Concelier.Core.Linksets;
|
|
using StellaOps.Scanner.WebService.Options;
|
|
using System.Collections.Immutable;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Scanner.WebService.Services;
|
|
|
|
internal sealed class ConcelierHttpLinksetQueryService : IAdvisoryLinksetQueryService
|
|
{
|
|
private readonly HttpClient _client;
|
|
private readonly ConcelierLinksetOptions _options;
|
|
|
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
public ConcelierHttpLinksetQueryService(HttpClient client, IOptions<ConcelierLinksetOptions> options)
|
|
{
|
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
public async Task<AdvisoryLinksetQueryResult> QueryAsync(AdvisoryLinksetQueryOptions options, CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(options);
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
if (options.AdvisoryIds is null)
|
|
{
|
|
return new AdvisoryLinksetQueryResult(ImmutableArray<AdvisoryLinkset>.Empty, null, false);
|
|
}
|
|
|
|
var results = ImmutableArray.CreateBuilder<AdvisoryLinkset>();
|
|
foreach (var advisoryId in options.AdvisoryIds)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(advisoryId))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var path = $"/v1/lnm/linksets/{Uri.EscapeDataString(advisoryId)}?tenant={Uri.EscapeDataString(options.Tenant)}&includeConflicts=true&includeObservations=false&includeTimeline=false";
|
|
try
|
|
{
|
|
using var response = await _client.GetAsync(path, cancellationToken).ConfigureAwait(false);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var payload = await response.Content.ReadFromJsonAsync<LinksetDetailResponse>(SerializerOptions, cancellationToken).ConfigureAwait(false);
|
|
if (payload?.Linksets is null || payload.Linksets.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var linkset in payload.Linksets)
|
|
{
|
|
results.Add(Map(linkset));
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch
|
|
{
|
|
// swallow and continue; caller will see partial results
|
|
}
|
|
}
|
|
|
|
var linksets = results.ToImmutable();
|
|
return new AdvisoryLinksetQueryResult(linksets, null, false);
|
|
}
|
|
|
|
private static AdvisoryLinkset Map(LinksetDto dto)
|
|
{
|
|
var normalized = dto.Normalized is null
|
|
? null
|
|
: new AdvisoryLinksetNormalized(
|
|
dto.Normalized.Purls,
|
|
dto.Normalized.Cpes,
|
|
dto.Normalized.Versions,
|
|
dto.Normalized.Ranges,
|
|
dto.Normalized.Severities);
|
|
|
|
var conflicts = dto.Conflicts is null
|
|
? null
|
|
: dto.Conflicts.Select(c => new AdvisoryLinksetConflict(c.Field, c.Reason ?? string.Empty, c.Values, c.SourceIds)).ToList();
|
|
|
|
return new AdvisoryLinkset(
|
|
TenantId: dto.Tenant ?? string.Empty,
|
|
Source: dto.Source ?? string.Empty,
|
|
AdvisoryId: dto.AdvisoryId ?? string.Empty,
|
|
ObservationIds: dto.ObservationIds?.ToImmutableArray() ?? ImmutableArray<string>.Empty,
|
|
Normalized: normalized,
|
|
Provenance: null,
|
|
Confidence: dto.Confidence,
|
|
Conflicts: conflicts,
|
|
CreatedAt: dto.CreatedAt ?? DateTimeOffset.MinValue,
|
|
BuiltByJobId: dto.BuiltByJobId);
|
|
}
|
|
|
|
private sealed record LinksetDetailResponse([property: JsonPropertyName("linksets")] LinksetDto[] Linksets);
|
|
|
|
private sealed record LinksetDto
|
|
{
|
|
[JsonPropertyName("advisoryId")]
|
|
public string? AdvisoryId { get; init; }
|
|
|
|
[JsonPropertyName("source")]
|
|
public string? Source { get; init; }
|
|
|
|
[JsonPropertyName("tenant")]
|
|
public string? Tenant { get; init; }
|
|
|
|
[JsonPropertyName("confidence")]
|
|
public double? Confidence { get; init; }
|
|
|
|
[JsonPropertyName("createdAt")]
|
|
public DateTimeOffset? CreatedAt { get; init; }
|
|
|
|
[JsonPropertyName("builtByJobId")]
|
|
public string? BuiltByJobId { get; init; }
|
|
|
|
[JsonPropertyName("observationIds")]
|
|
public string[]? ObservationIds { get; init; }
|
|
|
|
[JsonPropertyName("normalized")]
|
|
public LinksetNormalizedDto? Normalized { get; init; }
|
|
|
|
[JsonPropertyName("conflicts")]
|
|
public LinksetConflictDto[]? Conflicts { get; init; }
|
|
}
|
|
|
|
private sealed record LinksetNormalizedDto
|
|
{
|
|
[JsonPropertyName("purls")]
|
|
public IReadOnlyList<string>? Purls { get; init; }
|
|
|
|
[JsonPropertyName("cpes")]
|
|
public IReadOnlyList<string>? Cpes { get; init; }
|
|
|
|
[JsonPropertyName("versions")]
|
|
public IReadOnlyList<string>? Versions { get; init; }
|
|
|
|
[JsonPropertyName("ranges")]
|
|
public IReadOnlyList<Dictionary<string, object?>>? Ranges { get; init; }
|
|
|
|
[JsonPropertyName("severities")]
|
|
public IReadOnlyList<Dictionary<string, object?>>? Severities { get; init; }
|
|
}
|
|
|
|
private sealed record LinksetConflictDto
|
|
{
|
|
[JsonPropertyName("field")]
|
|
public string Field { get; init; } = string.Empty;
|
|
|
|
[JsonPropertyName("reason")]
|
|
public string? Reason { get; init; }
|
|
|
|
[JsonPropertyName("values")]
|
|
public IReadOnlyList<string>? Values { get; init; }
|
|
|
|
[JsonPropertyName("sourceIds")]
|
|
public IReadOnlyList<string>? SourceIds { get; init; }
|
|
}
|
|
}
|