up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Policy Simulation / policy-simulate (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
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
devportal-offline / build-offline (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-25 22:09:44 +02:00
parent 6bee1fdcf5
commit 9f6e6f7fb3
116 changed files with 4495 additions and 730 deletions

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Excititor.WebService.Contracts;
public sealed record GraphStatusResponse(
[property: JsonPropertyName("items")] IReadOnlyList<GraphStatusItem> Items,
[property: JsonPropertyName("cached")] bool Cached,
[property: JsonPropertyName("cacheAgeMs")] long? CacheAgeMs);
public sealed record GraphStatusItem(
[property: JsonPropertyName("purl")] string Purl,
[property: JsonPropertyName("summary")] GraphOverlaySummary Summary,
[property: JsonPropertyName("latestModifiedAt")] DateTimeOffset? LatestModifiedAt,
[property: JsonPropertyName("sources")] IReadOnlyList<string> Sources,
[property: JsonPropertyName("lastEvidenceHash")] string? LastEvidenceHash);

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Excititor.WebService.Contracts;
public sealed record GraphTooltipResponse(
[property: JsonPropertyName("items")] IReadOnlyList<GraphTooltipItem> Items,
[property: JsonPropertyName("nextCursor")] string? NextCursor,
[property: JsonPropertyName("hasMore")] bool HasMore);
public sealed record GraphTooltipItem(
[property: JsonPropertyName("purl")] string Purl,
[property: JsonPropertyName("observations")] IReadOnlyList<GraphTooltipObservation> Observations,
[property: JsonPropertyName("truncated")] bool Truncated);
public sealed record GraphTooltipObservation(
[property: JsonPropertyName("observationId")] string ObservationId,
[property: JsonPropertyName("advisoryId")] string AdvisoryId,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("justification")] string? Justification,
[property: JsonPropertyName("providerId")] string ProviderId,
[property: JsonPropertyName("modifiedAt")] DateTimeOffset ModifiedAt,
[property: JsonPropertyName("evidenceHash")] string EvidenceHash,
[property: JsonPropertyName("dsseEnvelopeHash")] string? DsseEnvelopeHash);

View File

@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StellaOps.Excititor.Core.Observations;
using StellaOps.Excititor.WebService.Contracts;
namespace StellaOps.Excititor.WebService.Graph;
internal static class GraphStatusFactory
{
public static IReadOnlyList<GraphStatusItem> Build(
IReadOnlyList<string> orderedPurls,
IReadOnlyList<VexObservation> observations)
{
if (orderedPurls is null)
{
throw new ArgumentNullException(nameof(orderedPurls));
}
if (observations is null)
{
throw new ArgumentNullException(nameof(observations));
}
var overlays = GraphOverlayFactory.Build(orderedPurls, observations, includeJustifications: false);
return overlays
.Select(overlay => new GraphStatusItem(
overlay.Purl,
overlay.Summary,
overlay.LatestModifiedAt,
overlay.Provenance.Sources,
overlay.Provenance.LastEvidenceHash))
.ToList();
}
}

View File

@@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using StellaOps.Excititor.Core.Observations;
using StellaOps.Excititor.WebService.Contracts;
namespace StellaOps.Excititor.WebService.Graph;
internal static class GraphTooltipFactory
{
public static IReadOnlyList<GraphTooltipItem> Build(
IReadOnlyList<string> orderedPurls,
IReadOnlyList<VexObservation> observations,
bool includeJustifications,
int maxItemsPerPurl)
{
if (orderedPurls is null)
{
throw new ArgumentNullException(nameof(orderedPurls));
}
if (observations is null)
{
throw new ArgumentNullException(nameof(observations));
}
if (maxItemsPerPurl <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxItemsPerPurl));
}
var requested = new HashSet<string>(orderedPurls, StringComparer.OrdinalIgnoreCase);
var byPurl = orderedPurls.ToDictionary(
keySelector: static purl => purl,
elementSelector: static _ => new List<GraphTooltipObservation>(),
comparer: StringComparer.OrdinalIgnoreCase);
foreach (var observation in observations)
{
var linksetPurls = observation.Linkset.Purls;
foreach (var statement in observation.Statements)
{
var targets = CollectTargets(statement, linksetPurls, requested);
if (targets.Count == 0)
{
continue;
}
var payload = new GraphTooltipObservation(
observation.ObservationId,
statement.VulnerabilityId,
statement.Status.ToString().ToLowerInvariant(),
includeJustifications ? statement.Justification?.ToString() : null,
observation.ProviderId,
observation.CreatedAt,
observation.Upstream.ContentHash,
observation.Upstream.Signature.Signature);
foreach (var target in targets)
{
byPurl[target].Add(payload);
}
}
}
var items = new List<GraphTooltipItem>(orderedPurls.Count);
foreach (var purl in orderedPurls)
{
if (!byPurl.TryGetValue(purl, out var observationsForPurl))
{
items.Add(new GraphTooltipItem(purl, Array.Empty<GraphTooltipObservation>(), false));
continue;
}
var ordered = observationsForPurl
.OrderByDescending(static o => o.ModifiedAt)
.ThenBy(static o => o.AdvisoryId, StringComparer.Ordinal)
.ThenBy(static o => o.ProviderId, StringComparer.OrdinalIgnoreCase)
.ThenBy(static o => o.ObservationId, StringComparer.Ordinal)
.ToList();
var truncated = ordered.Count > maxItemsPerPurl;
var limited = truncated ? ordered.Take(maxItemsPerPurl).ToList() : ordered;
items.Add(new GraphTooltipItem(purl, limited, truncated));
}
return items;
}
private static List<string> CollectTargets(
VexObservationStatement statement,
ImmutableArray<string> linksetPurls,
HashSet<string> requested)
{
var targets = new List<string>();
if (!string.IsNullOrWhiteSpace(statement.Purl))
{
var normalized = statement.Purl.ToLowerInvariant();
if (requested.Contains(normalized))
{
targets.Add(normalized);
}
}
if (targets.Count > 0)
{
return targets;
}
if (!linksetPurls.IsDefaultOrEmpty)
{
foreach (var purl in linksetPurls)
{
var normalized = purl?.ToLowerInvariant();
if (normalized is not null && requested.Contains(normalized))
{
targets.Add(normalized);
}
}
}
return targets;
}
}

View File

@@ -8,4 +8,6 @@ public sealed class GraphOptions
public int MaxPurls { get; set; } = 500;
public int MaxAdvisoriesPerPurl { get; set; } = 200;
public int OverlayTtlSeconds { get; set; } = 300;
public int MaxTooltipItemsPerPurl { get; set; } = 50;
public int MaxTooltipTotal { get; set; } = 1000;
}

View File

@@ -262,6 +262,10 @@ public partial class Program
signature.VerifiedAt));
}
private sealed record CachedGraphStatus(
IReadOnlyList<GraphStatusItem> Items,
DateTimeOffset CachedAt);
private sealed record CachedGraphOverlay(
IReadOnlyList<GraphOverlayItem> Items,
DateTimeOffset CachedAt);

View File

@@ -896,6 +896,64 @@ var response = new GraphLinkoutsResponse(items, notFound);
return Results.Ok(response);
}).WithName("PostGraphLinkouts");
app.MapGet("/v1/graph/status", async (
HttpContext context,
[FromQuery(Name = "purl")] string[]? purls,
IOptions<VexMongoStorageOptions> storageOptions,
IOptions<GraphOptions> graphOptions,
IVexObservationQueryService queryService,
IMemoryCache cache,
TimeProvider timeProvider,
CancellationToken cancellationToken) =>
{
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var orderedPurls = NormalizePurls(purls);
if (orderedPurls.Count == 0)
{
return Results.BadRequest("purl query parameter is required");
}
if (orderedPurls.Count > graphOptions.Value.MaxPurls)
{
return Results.BadRequest($"purls limit exceeded (max {graphOptions.Value.MaxPurls})");
}
var cacheKey = $"graph-status:{tenant}:{string.Join('|', orderedPurls)}";
var now = timeProvider.GetUtcNow();
if (cache.TryGetValue<CachedGraphStatus>(cacheKey, out var cached) && cached is not null)
{
var ageMs = (long)Math.Max(0, (now - cached.CachedAt).TotalMilliseconds);
return Results.Ok(new GraphStatusResponse(cached.Items, true, ageMs));
}
var options = new VexObservationQueryOptions(
tenant: tenant,
purls: orderedPurls,
limit: graphOptions.Value.MaxAdvisoriesPerPurl * orderedPurls.Count);
VexObservationQueryResult result;
try
{
result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false);
}
catch (FormatException ex)
{
return Results.BadRequest(ex.Message);
}
var items = GraphStatusFactory.Build(orderedPurls, result.Observations);
var response = new GraphStatusResponse(items, false, null);
cache.Set(cacheKey, new CachedGraphStatus(items, now), TimeSpan.FromSeconds(graphOptions.Value.OverlayTtlSeconds));
return Results.Ok(response);
}).WithName("GetGraphStatus");
// Cartographer overlays
app.MapGet("/v1/graph/overlays", async (
HttpContext context,
@@ -956,6 +1014,66 @@ app.MapGet("/v1/graph/overlays", async (
return Results.Ok(response);
}).WithName("GetGraphOverlays");
app.MapGet("/v1/graph/observations", async (
HttpContext context,
[FromQuery(Name = "purl")] string[]? purls,
[FromQuery] bool includeJustifications,
[FromQuery] int? limitPerPurl,
[FromQuery] string? cursor,
IOptions<VexMongoStorageOptions> storageOptions,
IOptions<GraphOptions> graphOptions,
IVexObservationQueryService queryService,
CancellationToken cancellationToken) =>
{
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError))
{
return tenantError;
}
var orderedPurls = NormalizePurls(purls);
if (orderedPurls.Count == 0)
{
return Results.BadRequest("purl query parameter is required");
}
if (orderedPurls.Count > graphOptions.Value.MaxPurls)
{
return Results.BadRequest($"purls limit exceeded (max {graphOptions.Value.MaxPurls})");
}
var perPurlLimit = limitPerPurl.GetValueOrDefault(graphOptions.Value.MaxTooltipItemsPerPurl);
if (perPurlLimit <= 0)
{
return Results.BadRequest("limitPerPurl must be greater than zero when provided.");
}
var effectivePerPurlLimit = Math.Min(perPurlLimit, graphOptions.Value.MaxAdvisoriesPerPurl);
var totalLimit = Math.Min(
Math.Max(1, effectivePerPurlLimit * orderedPurls.Count),
graphOptions.Value.MaxTooltipTotal);
var options = new VexObservationQueryOptions(
tenant: tenant,
purls: orderedPurls,
limit: totalLimit,
cursor: cursor);
VexObservationQueryResult result;
try
{
result = await queryService.QueryAsync(options, cancellationToken).ConfigureAwait(false);
}
catch (FormatException ex)
{
return Results.BadRequest(ex.Message);
}
var items = GraphTooltipFactory.Build(orderedPurls, result.Observations, includeJustifications, effectivePerPurlLimit);
var response = new GraphTooltipResponse(items, result.NextCursor, result.HasMore);
return Results.Ok(response);
}).WithName("GetGraphObservations");
app.MapPost("/ingest/vex", async (
HttpContext context,
VexIngestRequest request,