This commit is contained in:
StellaOps Bot
2025-11-23 23:40:10 +02:00
parent c13355923f
commit 029002ad05
93 changed files with 2160 additions and 285 deletions

View File

@@ -32,7 +32,6 @@ using StellaOps.Excititor.WebService.Extensions;
using StellaOps.Excititor.WebService.Options;
using StellaOps.Excititor.WebService.Services;
using StellaOps.Excititor.Core.Aoc;
using StellaOps.Excititor.WebService.Contracts;
using StellaOps.Excititor.WebService.Telemetry;
using MongoDB.Driver;
using MongoDB.Bson;
@@ -170,14 +169,14 @@ app.MapPost("/airgap/v1/vex/import", async (
if (!trustService.Validate(request, out var trustCode, out var trustMessage))
{
return Results.StatusCode(StatusCodes.Status403Forbidden, new
return Results.Json(new
{
error = new
{
code = trustCode,
message = trustMessage
}
});
}, statusCode: StatusCodes.Status403Forbidden);
}
var record = new AirgapImportRecord
@@ -344,13 +343,26 @@ app.MapGet("/console/vex", async (
}
var query = context.Request.Query;
var purls = query["purl"].Where(static v => !string.IsNullOrWhiteSpace(v)).Select(static v => v.Trim()).ToArray();
var advisories = query["advisoryId"].Where(static v => !string.IsNullOrWhiteSpace(v)).Select(static v => v.Trim()).ToArray();
static string[] NormalizeValues(StringValues values) =>
values.Where(static v => !string.IsNullOrWhiteSpace(v))
.Select(static v => v!.Trim())
.ToArray();
var purls = query.TryGetValue("purl", out var purlValues)
? NormalizeValues(purlValues)
: Array.Empty<string>();
var advisories = query.TryGetValue("advisoryId", out var advisoryValues)
? NormalizeValues(advisoryValues)
: Array.Empty<string>();
var statuses = new List<VexClaimStatus>();
if (query.TryGetValue("status", out var statusValues))
{
foreach (var statusValue in statusValues)
{
if (string.IsNullOrWhiteSpace(statusValue))
{
continue;
}
if (Enum.TryParse<VexClaimStatus>(statusValue, ignoreCase: true, out var parsed))
{
statuses.Add(parsed);
@@ -377,17 +389,17 @@ app.MapGet("/console/vex", async (
}
telemetry.CacheMisses.Add(1);
var options = new VexObservationQueryOptions(
tenant,
observationIds: null,
vulnerabilityIds: advisories,
productKeys: null,
purls: purls,
cpes: null,
providerIds: null,
statuses: statuses,
cursor: cursor,
limit: limit);
var options = new VexObservationQueryOptions(
tenant,
observationIds: null,
vulnerabilityIds: advisories,
productKeys: null,
purls: purls,
cpes: null,
providerIds: null,
statuses: statuses,
limit: limit,
cursor: cursor);
VexObservationQueryResult result;
try
@@ -399,22 +411,24 @@ app.MapGet("/console/vex", async (
return Results.BadRequest(ex.Message);
}
var statements = result.Observations
.SelectMany(obs => obs.Statements.Select(stmt => new VexConsoleStatementDto(
AdvisoryId: stmt.VulnerabilityId,
ProductKey: stmt.ProductKey,
Purl: stmt.Purl ?? obs.Linkset.Purls.FirstOrDefault(),
Status: stmt.Status.ToString().ToLowerInvariant(),
Justification: stmt.Justification?.ToString(),
ProviderId: obs.ProviderId,
ObservationId: obs.ObservationId,
CreatedAtUtc: obs.CreatedAt,
Attributes: obs.Attributes)))
.ToList();
var statements = result.Observations
.SelectMany(obs => obs.Statements.Select(stmt => new VexConsoleStatementDto(
AdvisoryId: stmt.VulnerabilityId,
ProductKey: stmt.ProductKey,
Purl: stmt.Purl
?? (obs.Linkset is { } linkset ? linkset.Purls.FirstOrDefault() : null)
?? string.Empty,
Status: stmt.Status.ToString().ToLowerInvariant(),
Justification: stmt.Justification?.ToString(),
ProviderId: obs.ProviderId,
ObservationId: obs.ObservationId,
CreatedAtUtc: obs.CreatedAt,
Attributes: obs.Attributes ?? ImmutableDictionary<string, string>.Empty)))
.ToList();
var statusCounts = result.Observations
.GroupBy(o => o.Status.ToString().ToLowerInvariant())
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase);
var statusCounts = statements
.GroupBy(o => o.Status)
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase);
var response = new VexConsolePage(
Items: statements,
@@ -455,12 +469,10 @@ app.MapPost("/internal/graph/linkouts", async (
return Results.BadRequest("purls are required (1-500).");
}
var options = new VexObservationQueryOptions(
request.Tenant.Trim(),
purls: normalizedPurls,
includeJustifications: request.IncludeJustifications,
includeProvenance: request.IncludeProvenance,
limit: 200);
var options = new VexObservationQueryOptions(
request.Tenant.Trim(),
purls: normalizedPurls,
limit: 200);
VexObservationQueryResult result;
try
@@ -495,31 +507,18 @@ app.MapPost("/internal/graph/linkouts", async (
Status: stmt.Status.ToString().ToLowerInvariant(),
Justification: request.IncludeJustifications ? stmt.Justification?.ToString() : null,
ModifiedAt: obs.CreatedAt,
EvidenceHash: obs.Linkset.ReferenceHash,
EvidenceHash: string.Empty,
ConnectorId: obs.ProviderId,
DsseEnvelopeHash: request.IncludeProvenance ? obs.Linkset.ReferenceHash : null)))
DsseEnvelopeHash: request.IncludeProvenance ? string.Empty : null)))
.OrderBy(a => a.AdvisoryId, StringComparer.Ordinal)
.ThenBy(a => a.Source, StringComparer.Ordinal)
.Take(200)
.ToList();
var conflicts = obsForPurl
.Where(obs => obs.Statements.Any(s => s.Status == VexClaimStatus.Conflict))
.SelectMany(obs => obs.Statements
.Where(s => s.Status == VexClaimStatus.Conflict)
.Select(stmt => new GraphLinkoutConflict(
Source: obs.ProviderId,
Status: stmt.Status.ToString().ToLowerInvariant(),
Justification: request.IncludeJustifications ? stmt.Justification?.ToString() : null,
ObservedAt: obs.CreatedAt,
EvidenceHash: obs.Linkset.ReferenceHash)))
.OrderBy(c => c.Source, StringComparer.Ordinal)
.ToList();
items.Add(new GraphLinkoutItem(
Purl: inputPurl,
Advisories: advisories,
Conflicts: conflicts,
Conflicts: Array.Empty<GraphLinkoutConflict>(),
Truncated: advisories.Count >= 200,
NextCursor: advisories.Count >= 200 ? $"{advisories[^1].AdvisoryId}:{advisories[^1].Source}" : null));
}