using System.Collections.Immutable; using System.Globalization; using Microsoft.Extensions.Logging; using StellaOps.Concelier.Core.Linksets; using StellaOps.Scanner.Surface.Env; using StellaOps.Scanner.WebService.Contracts; namespace StellaOps.Scanner.WebService.Services; internal interface ILinksetResolver { Task> ResolveAsync(IEnumerable? findings, CancellationToken cancellationToken); Task> ResolveAsync(IEnumerable verdicts, CancellationToken cancellationToken); Task> ResolveByAdvisoryIdsAsync(IEnumerable advisoryIds, CancellationToken cancellationToken); } internal sealed class LinksetResolver : ILinksetResolver { private readonly IAdvisoryLinksetQueryService _queryService; private readonly ISurfaceEnvironment _surfaceEnvironment; private readonly ILogger _logger; public LinksetResolver( IAdvisoryLinksetQueryService queryService, ISurfaceEnvironment surfaceEnvironment, ILogger logger) { _queryService = queryService ?? throw new ArgumentNullException(nameof(queryService)); _surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public Task> ResolveAsync(IEnumerable? findings, CancellationToken cancellationToken) { var advisoryIds = findings? .SelectMany(f => new[] { f?.Id, f?.Cve }) .Where(id => !string.IsNullOrWhiteSpace(id)) .Select(id => id!.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() ?? Array.Empty(); return ResolveInternalAsync(advisoryIds, cancellationToken); } public Task> ResolveAsync(IEnumerable verdicts, CancellationToken cancellationToken) { var advisoryIds = verdicts? .Where(v => !string.IsNullOrWhiteSpace(v.FindingId)) .Select(v => v.FindingId!.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() ?? Array.Empty(); return ResolveInternalAsync(advisoryIds, cancellationToken); } public Task> ResolveByAdvisoryIdsAsync(IEnumerable advisoryIds, CancellationToken cancellationToken) { var normalized = advisoryIds? .Where(id => !string.IsNullOrWhiteSpace(id)) .Select(id => id.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray() ?? Array.Empty(); return ResolveInternalAsync(normalized, cancellationToken); } private async Task> ResolveInternalAsync(IReadOnlyList advisoryIds, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (advisoryIds.Count == 0) { return Array.Empty(); } var tenant = string.IsNullOrWhiteSpace(_surfaceEnvironment.Settings.Tenant) ? "default" : _surfaceEnvironment.Settings.Tenant.Trim(); try { var options = new AdvisoryLinksetQueryOptions(tenant, advisoryIds, Sources: null, Limit: advisoryIds.Count); var result = await _queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false); if (result.Linksets.IsDefaultOrEmpty) { return Array.Empty(); } return result.Linksets .Select(MapSummary) .OrderBy(ls => ls.AdvisoryId, StringComparer.Ordinal) .ToArray(); } catch (Exception ex) when (!cancellationToken.IsCancellationRequested) { _logger.LogWarning(ex, "Failed to resolve linksets for {Count} advisories (tenant={Tenant}).", advisoryIds.Count, tenant); return Array.Empty(); } } private static LinksetSummaryDto MapSummary(AdvisoryLinkset linkset) { var severities = linkset.Normalized?.Severities?.Select(MapSeverity).ToArray(); var conflicts = linkset.Conflicts?.Select(MapConflict).ToArray(); return new LinksetSummaryDto { AdvisoryId = linkset.AdvisoryId, Source = linkset.Source, Confidence = linkset.Confidence, ObservationIds = linkset.ObservationIds.Length > 0 ? linkset.ObservationIds : null, References = null, Severities = severities?.Length > 0 ? severities : null, Conflicts = conflicts?.Length > 0 ? conflicts : null }; } private static LinksetSeverityDto MapSeverity(Dictionary payload) { payload ??= new Dictionary(StringComparer.Ordinal); string? GetString(string key) => payload.TryGetValue(key, out var value) ? value?.ToString() : null; double? GetDouble(string key) { if (!payload.TryGetValue(key, out var value) || value is null) { return null; } if (value is double d) { return d; } if (value is float f) { return Convert.ToDouble(f, CultureInfo.InvariantCulture); } if (double.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)) { return parsed; } return null; } var labels = payload.TryGetValue("labels", out var labelsValue) && labelsValue is Dictionary labelsDict ? labelsDict.ToDictionary(kv => kv.Key, kv => kv.Value?.ToString() ?? string.Empty, StringComparer.Ordinal) : null; var raw = payload.Count == 0 ? null : payload.ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.Ordinal); return new LinksetSeverityDto { Source = GetString("source"), Type = GetString("type"), Score = GetDouble("score"), Vector = GetString("vector"), Origin = GetString("origin"), Labels = labels, Raw = raw }; } private static LinksetConflictDto MapConflict(AdvisoryLinksetConflict conflict) { return new LinksetConflictDto { Field = conflict.Field, Reason = conflict.Reason, Values = conflict.Values, SourceIds = conflict.SourceIds }; } }