up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-03 00:10:19 +02:00
parent ea1d58a89b
commit 37cba83708
158 changed files with 147438 additions and 867 deletions

View File

@@ -0,0 +1,171 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Scanner.WebService.Options;
using Microsoft.Extensions.Options;
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; }
}
}

View File

@@ -0,0 +1,181 @@
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<IReadOnlyList<LinksetSummaryDto>> ResolveAsync(IEnumerable<PolicyPreviewFindingDto>? findings, CancellationToken cancellationToken);
Task<IReadOnlyList<LinksetSummaryDto>> ResolveAsync(IEnumerable<StellaOps.Policy.PolicyVerdict> verdicts, CancellationToken cancellationToken);
Task<IReadOnlyList<LinksetSummaryDto>> ResolveByAdvisoryIdsAsync(IEnumerable<string> advisoryIds, CancellationToken cancellationToken);
}
internal sealed class LinksetResolver : ILinksetResolver
{
private readonly IAdvisoryLinksetQueryService _queryService;
private readonly ISurfaceEnvironment _surfaceEnvironment;
private readonly ILogger<LinksetResolver> _logger;
public LinksetResolver(
IAdvisoryLinksetQueryService queryService,
ISurfaceEnvironment surfaceEnvironment,
ILogger<LinksetResolver> logger)
{
_queryService = queryService ?? throw new ArgumentNullException(nameof(queryService));
_surfaceEnvironment = surfaceEnvironment ?? throw new ArgumentNullException(nameof(surfaceEnvironment));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public Task<IReadOnlyList<LinksetSummaryDto>> ResolveAsync(IEnumerable<PolicyPreviewFindingDto>? 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<string>();
return ResolveInternalAsync(advisoryIds, cancellationToken);
}
public Task<IReadOnlyList<LinksetSummaryDto>> ResolveAsync(IEnumerable<StellaOps.Policy.PolicyVerdict> verdicts, CancellationToken cancellationToken)
{
var advisoryIds = verdicts?
.Where(v => !string.IsNullOrWhiteSpace(v.FindingId))
.Select(v => v.FindingId!.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray() ?? Array.Empty<string>();
return ResolveInternalAsync(advisoryIds, cancellationToken);
}
public Task<IReadOnlyList<LinksetSummaryDto>> ResolveByAdvisoryIdsAsync(IEnumerable<string> advisoryIds, CancellationToken cancellationToken)
{
var normalized = advisoryIds?
.Where(id => !string.IsNullOrWhiteSpace(id))
.Select(id => id.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray() ?? Array.Empty<string>();
return ResolveInternalAsync(normalized, cancellationToken);
}
private async Task<IReadOnlyList<LinksetSummaryDto>> ResolveInternalAsync(IReadOnlyList<string> advisoryIds, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (advisoryIds.Count == 0)
{
return Array.Empty<LinksetSummaryDto>();
}
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<LinksetSummaryDto>();
}
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<LinksetSummaryDto>();
}
}
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<string, object?> payload)
{
payload ??= new Dictionary<string, object?>(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<string, object?> 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
};
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
namespace StellaOps.Scanner.WebService.Services;
internal sealed class NullAdvisoryLinksetQueryService : IAdvisoryLinksetQueryService
{
public Task<AdvisoryLinksetQueryResult> QueryAsync(AdvisoryLinksetQueryOptions options, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(options);
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(new AdvisoryLinksetQueryResult(ImmutableArray<AdvisoryLinkset>.Empty, null, false));
}
}

View File

@@ -7,13 +7,14 @@ using System.Linq;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Zastava.Core.Contracts;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Policy;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Options;
using StellaOps.Zastava.Core.Contracts;
using RuntimePolicyVerdict = StellaOps.Zastava.Core.Contracts.PolicyVerdict;
using CanonicalPolicyVerdict = StellaOps.Policy.PolicyVerdict;
using CanonicalPolicyVerdictStatus = StellaOps.Policy.PolicyVerdictStatus;
@@ -35,35 +36,38 @@ internal sealed class RuntimePolicyService : IRuntimePolicyService
private readonly LinkRepository _linkRepository;
private readonly ArtifactRepository _artifactRepository;
private readonly RuntimeEventRepository _runtimeEventRepository;
private readonly PolicySnapshotStore _policySnapshotStore;
private readonly PolicyPreviewService _policyPreviewService;
private readonly IOptionsMonitor<ScannerWebServiceOptions> _optionsMonitor;
private readonly TimeProvider _timeProvider;
private readonly IRuntimeAttestationVerifier _attestationVerifier;
private readonly ILogger<RuntimePolicyService> _logger;
private readonly RuntimeEventRepository _runtimeEventRepository;
private readonly PolicySnapshotStore _policySnapshotStore;
private readonly PolicyPreviewService _policyPreviewService;
private readonly ILinksetResolver _linksetResolver;
private readonly IOptionsMonitor<ScannerWebServiceOptions> _optionsMonitor;
private readonly TimeProvider _timeProvider;
private readonly IRuntimeAttestationVerifier _attestationVerifier;
private readonly ILogger<RuntimePolicyService> _logger;
public RuntimePolicyService(
LinkRepository linkRepository,
ArtifactRepository artifactRepository,
RuntimeEventRepository runtimeEventRepository,
PolicySnapshotStore policySnapshotStore,
PolicyPreviewService policyPreviewService,
IOptionsMonitor<ScannerWebServiceOptions> optionsMonitor,
TimeProvider timeProvider,
IRuntimeAttestationVerifier attestationVerifier,
ILogger<RuntimePolicyService> logger)
{
_linkRepository = linkRepository ?? throw new ArgumentNullException(nameof(linkRepository));
_artifactRepository = artifactRepository ?? throw new ArgumentNullException(nameof(artifactRepository));
_runtimeEventRepository = runtimeEventRepository ?? throw new ArgumentNullException(nameof(runtimeEventRepository));
_policySnapshotStore = policySnapshotStore ?? throw new ArgumentNullException(nameof(policySnapshotStore));
_policyPreviewService = policyPreviewService ?? throw new ArgumentNullException(nameof(policyPreviewService));
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_attestationVerifier = attestationVerifier ?? throw new ArgumentNullException(nameof(attestationVerifier));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
LinkRepository linkRepository,
ArtifactRepository artifactRepository,
RuntimeEventRepository runtimeEventRepository,
PolicySnapshotStore policySnapshotStore,
PolicyPreviewService policyPreviewService,
ILinksetResolver linksetResolver,
IOptionsMonitor<ScannerWebServiceOptions> optionsMonitor,
TimeProvider timeProvider,
IRuntimeAttestationVerifier attestationVerifier,
ILogger<RuntimePolicyService> logger)
{
_linkRepository = linkRepository ?? throw new ArgumentNullException(nameof(linkRepository));
_artifactRepository = artifactRepository ?? throw new ArgumentNullException(nameof(artifactRepository));
_runtimeEventRepository = runtimeEventRepository ?? throw new ArgumentNullException(nameof(runtimeEventRepository));
_policySnapshotStore = policySnapshotStore ?? throw new ArgumentNullException(nameof(policySnapshotStore));
_policyPreviewService = policyPreviewService ?? throw new ArgumentNullException(nameof(policyPreviewService));
_linksetResolver = linksetResolver ?? throw new ArgumentNullException(nameof(linksetResolver));
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
_attestationVerifier = attestationVerifier ?? throw new ArgumentNullException(nameof(attestationVerifier));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<RuntimePolicyEvaluationResult> EvaluateAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken)
{
@@ -118,13 +122,14 @@ internal sealed class RuntimePolicyService : IRuntimePolicyService
heuristicReasons.Add("policy.snapshot.missing");
}
ImmutableArray<CanonicalPolicyVerdict> projectedVerdicts = ImmutableArray<CanonicalPolicyVerdict>.Empty;
ImmutableArray<PolicyIssue> issues = ImmutableArray<PolicyIssue>.Empty;
try
{
if (!findings.IsDefaultOrEmpty && findings.Length > 0)
{
ImmutableArray<CanonicalPolicyVerdict> projectedVerdicts = ImmutableArray<CanonicalPolicyVerdict>.Empty;
ImmutableArray<PolicyIssue> issues = ImmutableArray<PolicyIssue>.Empty;
IReadOnlyList<LinksetSummaryDto> linksets = Array.Empty<LinksetSummaryDto>();
try
{
if (!findings.IsDefaultOrEmpty && findings.Length > 0)
{
var previewRequest = new PolicyPreviewRequest(
image,
findings,
@@ -133,14 +138,15 @@ internal sealed class RuntimePolicyService : IRuntimePolicyService
ProposedPolicy: null);
var preview = await _policyPreviewService.PreviewAsync(previewRequest, cancellationToken).ConfigureAwait(false);
issues = preview.Issues;
if (!preview.Diffs.IsDefaultOrEmpty)
{
projectedVerdicts = preview.Diffs.Select(diff => diff.Projected).ToImmutableArray();
}
}
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
issues = preview.Issues;
if (!preview.Diffs.IsDefaultOrEmpty)
{
projectedVerdicts = preview.Diffs.Select(diff => diff.Projected).ToImmutableArray();
linksets = await _linksetResolver.ResolveAsync(projectedVerdicts, cancellationToken).ConfigureAwait(false);
}
}
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
_logger.LogWarning(ex, "Runtime policy preview failed for image {ImageDigest}; falling back to heuristic evaluation.", image);
}
@@ -151,12 +157,13 @@ internal sealed class RuntimePolicyService : IRuntimePolicyService
var decision = await BuildDecisionAsync(
image,
metadata,
heuristicReasons,
projectedVerdicts,
issues,
policyDigest,
buildIdObservation?.BuildIds,
cancellationToken).ConfigureAwait(false);
heuristicReasons,
projectedVerdicts,
issues,
policyDigest,
linksets,
buildIdObservation?.BuildIds,
cancellationToken).ConfigureAwait(false);
results[image] = decision;
@@ -279,12 +286,13 @@ internal sealed class RuntimePolicyService : IRuntimePolicyService
private async Task<RuntimePolicyImageDecision> BuildDecisionAsync(
string imageDigest,
RuntimeImageMetadata metadata,
List<string> heuristicReasons,
ImmutableArray<CanonicalPolicyVerdict> projectedVerdicts,
ImmutableArray<PolicyIssue> issues,
string? policyDigest,
IReadOnlyList<string>? buildIds,
CancellationToken cancellationToken)
List<string> heuristicReasons,
ImmutableArray<CanonicalPolicyVerdict> projectedVerdicts,
ImmutableArray<PolicyIssue> issues,
string? policyDigest,
IReadOnlyList<LinksetSummaryDto> linksets,
IReadOnlyList<string>? buildIds,
CancellationToken cancellationToken)
{
var reasons = new List<string>(heuristicReasons);
@@ -330,18 +338,19 @@ internal sealed class RuntimePolicyService : IRuntimePolicyService
.Distinct(StringComparer.Ordinal)
.ToArray();
return new RuntimePolicyImageDecision(
overallVerdict,
metadata.Signed,
metadata.HasSbomReferrers,
normalizedReasons,
rekor,
metadataPayload,
confidence,
quieted,
quietedBy,
buildIds);
}
return new RuntimePolicyImageDecision(
overallVerdict,
metadata.Signed,
metadata.HasSbomReferrers,
normalizedReasons,
rekor,
metadataPayload,
confidence,
quieted,
quietedBy,
buildIds,
linksets);
}
private RuntimePolicyVerdict MapVerdict(ImmutableArray<CanonicalPolicyVerdict> projectedVerdicts, IReadOnlyList<string> heuristicReasons)
{
@@ -501,17 +510,18 @@ internal sealed record RuntimePolicyEvaluationResult(
string? PolicyRevision,
IReadOnlyDictionary<string, RuntimePolicyImageDecision> Results);
internal sealed record RuntimePolicyImageDecision(
RuntimePolicyVerdict PolicyVerdict,
bool Signed,
bool HasSbomReferrers,
IReadOnlyList<string> Reasons,
RuntimePolicyRekorReference? Rekor,
IDictionary<string, object?>? Metadata,
double Confidence,
bool Quieted,
string? QuietedBy,
IReadOnlyList<string>? BuildIds);
internal sealed record RuntimePolicyImageDecision(
RuntimePolicyVerdict PolicyVerdict,
bool Signed,
bool HasSbomReferrers,
IReadOnlyList<string> Reasons,
RuntimePolicyRekorReference? Rekor,
IDictionary<string, object?>? Metadata,
double Confidence,
bool Quieted,
string? QuietedBy,
IReadOnlyList<string>? BuildIds,
IReadOnlyList<LinksetSummaryDto> Linksets);
internal sealed record RuntimePolicyRekorReference(string? Uuid, string? Url, bool? Verified);