nuget reorganization
This commit is contained in:
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using StellaOps.Excititor.Attestation.Verification;
|
||||
using StellaOps.Excititor.Attestation.Extensions;
|
||||
@@ -47,6 +48,7 @@ services.AddCycloneDxNormalizer();
|
||||
services.AddOpenVexNormalizer();
|
||||
services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>();
|
||||
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
|
||||
services.AddScoped<IVexObservationLookup, MongoVexObservationLookup>();
|
||||
services.AddOptions<ExcititorObservabilityOptions>()
|
||||
.Bind(configuration.GetSection("Excititor:Observability"));
|
||||
services.AddScoped<ExcititorHealthService>();
|
||||
@@ -63,6 +65,8 @@ services.Configure<MirrorDistributionOptions>(configuration.GetSection(MirrorDis
|
||||
services.AddSingleton<MirrorRateLimiter>();
|
||||
services.TryAddSingleton(TimeProvider.System);
|
||||
services.AddSingleton<IVexObservationProjectionService, VexObservationProjectionService>();
|
||||
services.AddScoped<IVexObservationLookup, MongoVexObservationLookup>();
|
||||
services.AddScoped<IVexObservationLookup, MongoVexObservationLookup>();
|
||||
|
||||
var rekorSection = configuration.GetSection("Excititor:Attestation:Rekor");
|
||||
if (rekorSection.Exists())
|
||||
@@ -522,10 +526,223 @@ app.MapGet("/v1/vex/observations/{vulnerabilityId}/{productKey}", async (
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapGet("/v1/vex/observations", async (
|
||||
HttpContext context,
|
||||
[FromServices] IVexObservationLookup observationLookup,
|
||||
[FromServices] IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var observationIds = BuildStringFilterSet(context.Request.Query["observationId"]);
|
||||
var vulnerabilityIds = BuildStringFilterSet(context.Request.Query["vulnerabilityId"], toLower: true);
|
||||
var productKeys = BuildStringFilterSet(context.Request.Query["productKey"], toLower: true);
|
||||
var purls = BuildStringFilterSet(context.Request.Query["purl"], toLower: true);
|
||||
var cpes = BuildStringFilterSet(context.Request.Query["cpe"], toLower: true);
|
||||
var providerIds = BuildStringFilterSet(context.Request.Query["providerId"], toLower: true);
|
||||
var statuses = BuildStatusFilter(context.Request.Query["status"]);
|
||||
|
||||
var limit = ResolveLimit(context.Request.Query["limit"], defaultValue: 500, min: 1, max: 2000);
|
||||
var cursorRaw = context.Request.Query["cursor"].FirstOrDefault();
|
||||
VexObservationCursor? cursor = null;
|
||||
if (!string.IsNullOrWhiteSpace(cursorRaw))
|
||||
{
|
||||
try
|
||||
{
|
||||
cursor = VexObservationCursor.Parse(cursorRaw!);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Results.BadRequest("Cursor is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<VexObservation> observations;
|
||||
try
|
||||
{
|
||||
observations = await observationLookup.FindByFiltersAsync(
|
||||
tenant,
|
||||
observationIds,
|
||||
vulnerabilityIds,
|
||||
productKeys,
|
||||
purls,
|
||||
cpes,
|
||||
providerIds,
|
||||
statuses,
|
||||
cursor,
|
||||
limit,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status499ClientClosedRequest);
|
||||
}
|
||||
|
||||
var items = observations.Select(obs => new VexObservationListItem(
|
||||
obs.ObservationId,
|
||||
obs.Tenant,
|
||||
obs.ProviderId,
|
||||
obs.Statements.FirstOrDefault()?.VulnerabilityId ?? string.Empty,
|
||||
obs.Statements.FirstOrDefault()?.ProductKey ?? string.Empty,
|
||||
obs.Statements.FirstOrDefault()?.Status.ToString().ToLowerInvariant() ?? string.Empty,
|
||||
obs.CreatedAt,
|
||||
obs.Statements.FirstOrDefault()?.LastObserved,
|
||||
obs.Linkset.Purls)).ToList();
|
||||
|
||||
var nextCursor = observations.Count == limit
|
||||
? VexObservationCursor.FromObservation(observations.Last()).ToString()
|
||||
: null;
|
||||
|
||||
var response = new VexObservationListResponse(items, nextCursor);
|
||||
context.Response.Headers["Excititor-Results-Count"] = items.Count.ToString(CultureInfo.InvariantCulture);
|
||||
if (nextCursor is not null)
|
||||
{
|
||||
context.Response.Headers["Excititor-Results-Cursor"] = nextCursor;
|
||||
}
|
||||
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
app.MapGet("/v1/vex/linksets", async (
|
||||
HttpContext context,
|
||||
[FromServices] IVexObservationLookup observationLookup,
|
||||
[FromServices] IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var vulnerabilityIds = BuildStringFilterSet(context.Request.Query["vulnerabilityId"], toLower: true);
|
||||
var productKeys = BuildStringFilterSet(context.Request.Query["productKey"], toLower: true);
|
||||
var providerIds = BuildStringFilterSet(context.Request.Query["providerId"], toLower: true);
|
||||
var statuses = BuildStatusFilter(context.Request.Query["status"]);
|
||||
var limit = ResolveLimit(context.Request.Query["limit"], defaultValue: 200, min: 1, max: 500);
|
||||
var cursorRaw = context.Request.Query["cursor"].FirstOrDefault();
|
||||
|
||||
VexObservationCursor? cursor = null;
|
||||
if (!string.IsNullOrWhiteSpace(cursorRaw))
|
||||
{
|
||||
try
|
||||
{
|
||||
cursor = VexObservationCursor.Parse(cursorRaw!);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Results.BadRequest("Cursor is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<VexObservation> observations;
|
||||
try
|
||||
{
|
||||
observations = await observationLookup.FindByFiltersAsync(
|
||||
tenant,
|
||||
observationIds: Array.Empty<string>(),
|
||||
vulnerabilityIds,
|
||||
productKeys,
|
||||
purls: Array.Empty<string>(),
|
||||
cpes: Array.Empty<string>(),
|
||||
providerIds,
|
||||
statuses,
|
||||
cursor,
|
||||
limit,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status499ClientClosedRequest);
|
||||
}
|
||||
|
||||
var grouped = observations
|
||||
.GroupBy(obs => (VulnerabilityId: obs.Statements.FirstOrDefault()?.VulnerabilityId ?? string.Empty,
|
||||
ProductKey: obs.Statements.FirstOrDefault()?.ProductKey ?? string.Empty))
|
||||
.Select(group =>
|
||||
{
|
||||
var sample = group.FirstOrDefault();
|
||||
var linkset = sample?.Linkset ?? new VexObservationLinkset(null, null, null, null);
|
||||
var vulnerabilityId = group.Key.VulnerabilityId;
|
||||
var productKey = group.Key.ProductKey;
|
||||
|
||||
var providerSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var statusSet = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var obsRefs = new List<VexLinksetObservationRef>();
|
||||
|
||||
foreach (var obs in group)
|
||||
{
|
||||
var stmt = obs.Statements.FirstOrDefault();
|
||||
if (stmt is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
providerSet.Add(obs.ProviderId);
|
||||
statusSet.Add(stmt.Status.ToString().ToLowerInvariant());
|
||||
obsRefs.Add(new VexLinksetObservationRef(
|
||||
obs.ObservationId,
|
||||
obs.ProviderId,
|
||||
stmt.Status.ToString().ToLowerInvariant(),
|
||||
stmt.Signals?.Severity?.Score));
|
||||
}
|
||||
|
||||
var item = new VexLinksetListItem(
|
||||
linksetId: string.Create(CultureInfo.InvariantCulture, $"{vulnerabilityId}:{productKey}"),
|
||||
tenant,
|
||||
vulnerabilityId,
|
||||
productKey,
|
||||
providerSet.ToList(),
|
||||
statusSet.ToList(),
|
||||
linkset.Aliases,
|
||||
linkset.Purls,
|
||||
linkset.Cpes,
|
||||
linkset.References.Select(r => new VexLinksetReference(r.Type, r.Url)).ToList(),
|
||||
linkset.Disagreements.Select(d => new VexLinksetDisagreement(d.ProviderId, d.Status, d.Justification, d.Confidence)).ToList(),
|
||||
obsRefs,
|
||||
createdAt: group.Min(o => o.CreatedAt));
|
||||
|
||||
return item;
|
||||
})
|
||||
.OrderBy(item => item.VulnerabilityId, StringComparer.Ordinal)
|
||||
.ThenBy(item => item.ProductKey, StringComparer.Ordinal)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
var nextCursor = grouped.Count == limit && observations.Count > 0
|
||||
? VexObservationCursor.FromObservation(observations.Last()).ToString()
|
||||
: null;
|
||||
|
||||
var response = new VexLinksetListResponse(grouped, nextCursor);
|
||||
context.Response.Headers["Excititor-Results-Count"] = grouped.Count.ToString(CultureInfo.InvariantCulture);
|
||||
if (nextCursor is not null)
|
||||
{
|
||||
context.Response.Headers["Excititor-Results-Cursor"] = nextCursor;
|
||||
}
|
||||
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
|
||||
app.MapGet("/v1/vex/evidence/chunks", async (
|
||||
HttpContext context,
|
||||
[FromServices] IVexEvidenceChunkService chunkService,
|
||||
[FromServices] IOptions<VexMongoStorageOptions> storageOptions,
|
||||
[FromServices] ILogger<VexEvidenceChunkRequest> logger,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
@@ -579,6 +796,18 @@ app.MapGet("/v1/vex/evidence/chunks", async (
|
||||
EvidenceTelemetry.RecordChunkOutcome(tenant, "success", result.Chunks.Count, result.Truncated);
|
||||
EvidenceTelemetry.RecordChunkSignatureStatus(tenant, result.Chunks);
|
||||
|
||||
logger.LogInformation(
|
||||
"vex_evidence_chunks_success tenant={Tenant} vulnerabilityId={Vuln} productKey={ProductKey} providers={Providers} statuses={Statuses} limit={Limit} total={Total} truncated={Truncated} returned={Returned}",
|
||||
tenant ?? "(default)",
|
||||
request.VulnerabilityId,
|
||||
request.ProductKey,
|
||||
providerFilter.Count,
|
||||
statusFilter.Count,
|
||||
request.Limit,
|
||||
result.TotalCount,
|
||||
result.Truncated,
|
||||
result.Chunks.Count);
|
||||
|
||||
// Align headers with published contract.
|
||||
context.Response.Headers["Excititor-Results-Total"] = result.TotalCount.ToString(CultureInfo.InvariantCulture);
|
||||
context.Response.Headers["Excititor-Results-Truncated"] = result.Truncated ? "true" : "false";
|
||||
@@ -841,3 +1070,90 @@ internal sealed record VexSeveritySignalRequest(string Scheme, double? Score, st
|
||||
{
|
||||
public VexSeveritySignal ToDomain() => new(Scheme, Score, Label, Vector);
|
||||
}
|
||||
app.MapGet(
|
||||
"/v1/vex/observations",
|
||||
async (
|
||||
HttpContext context,
|
||||
[FromServices] IVexObservationLookup observationLookup,
|
||||
[FromServices] IOptions<VexMongoStorageOptions> storageOptions,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
|
||||
{
|
||||
return tenantError;
|
||||
}
|
||||
|
||||
var observationIds = BuildStringFilterSet(context.Request.Query["observationId"]);
|
||||
var vulnerabilityIds = BuildStringFilterSet(context.Request.Query["vulnerabilityId"], toLower: true);
|
||||
var productKeys = BuildStringFilterSet(context.Request.Query["productKey"], toLower: true);
|
||||
var purls = BuildStringFilterSet(context.Request.Query["purl"], toLower: true);
|
||||
var cpes = BuildStringFilterSet(context.Request.Query["cpe"], toLower: true);
|
||||
var providerIds = BuildStringFilterSet(context.Request.Query["providerId"], toLower: true);
|
||||
var statuses = BuildStatusFilter(context.Request.Query["status"]);
|
||||
|
||||
var limit = ResolveLimit(context.Request.Query["limit"], defaultValue: 200, min: 1, max: 500);
|
||||
var cursorRaw = context.Request.Query["cursor"].FirstOrDefault();
|
||||
VexObservationCursor? cursor = null;
|
||||
if (!string.IsNullOrWhiteSpace(cursorRaw))
|
||||
{
|
||||
try
|
||||
{
|
||||
cursor = VexObservationCursor.Parse(cursorRaw!);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Results.BadRequest("Cursor is malformed.");
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<VexObservation> observations;
|
||||
try
|
||||
{
|
||||
observations = await observationLookup.FindByFiltersAsync(
|
||||
tenant,
|
||||
observationIds,
|
||||
vulnerabilityIds,
|
||||
productKeys,
|
||||
purls,
|
||||
cpes,
|
||||
providerIds,
|
||||
statuses,
|
||||
cursor,
|
||||
limit,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status499ClientClosedRequest);
|
||||
}
|
||||
|
||||
var items = observations.Select(obs => new VexObservationListItem(
|
||||
obs.ObservationId,
|
||||
obs.Tenant,
|
||||
obs.ProviderId,
|
||||
obs.Statements.FirstOrDefault()?.VulnerabilityId ?? string.Empty,
|
||||
obs.Statements.FirstOrDefault()?.ProductKey ?? string.Empty,
|
||||
obs.Statements.FirstOrDefault()?.Status.ToString().ToLowerInvariant() ?? string.Empty,
|
||||
obs.CreatedAt,
|
||||
obs.Statements.FirstOrDefault()?.LastObserved,
|
||||
obs.Linkset.Purls)).ToList();
|
||||
|
||||
var nextCursor = observations.Count == limit
|
||||
? VexObservationCursor.FromObservation(observations.Last()).ToString()
|
||||
: null;
|
||||
|
||||
var response = new VexObservationListResponse(items, nextCursor);
|
||||
context.Response.Headers["X-Count"] = items.Count.ToString(CultureInfo.InvariantCulture);
|
||||
if (nextCursor is not null)
|
||||
{
|
||||
context.Response.Headers["X-Cursor"] = nextCursor;
|
||||
}
|
||||
|
||||
return Results.Json(response);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user