feat: Add MongoIdempotencyStoreOptions for MongoDB configuration
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

feat: Implement BsonJsonConverter for converting BsonDocument and BsonArray to JSON

fix: Update project file to include MongoDB.Bson package

test: Add GraphOverlayExporterTests to validate NDJSON export functionality

refactor: Refactor Program.cs in Attestation Tool for improved argument parsing and error handling

docs: Update README for stella-forensic-verify with usage instructions and exit codes

feat: Enhance HmacVerifier with clock skew and not-after checks

feat: Add MerkleRootVerifier and ChainOfCustodyVerifier for additional verification methods

fix: Update DenoRuntimeShim to correctly handle file paths

feat: Introduce ComposerAutoloadData and related parsing in ComposerLockReader

test: Add tests for Deno runtime execution and verification

test: Enhance PHP package tests to include autoload data verification

test: Add unit tests for HmacVerifier and verification logic
This commit is contained in:
StellaOps Bot
2025-11-22 16:42:56 +02:00
parent 967ae0ab16
commit dc7c75b496
85 changed files with 2272 additions and 917 deletions

View File

@@ -20,6 +20,7 @@ using StellaOps.Excititor.Attestation.Transparency;
using StellaOps.Excititor.ArtifactStores.S3.Extensions;
using StellaOps.Excititor.Connectors.RedHat.CSAF.DependencyInjection;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Observations;
using StellaOps.Excititor.Export;
using StellaOps.Excititor.Formats.CSAF;
using StellaOps.Excititor.Formats.CycloneDX;
@@ -50,7 +51,6 @@ services.AddOpenVexNormalizer();
services.AddSingleton<IVexSignatureVerifier, NoopVexSignatureVerifier>();
services.AddSingleton<AirgapImportValidator>();
services.AddScoped<IVexIngestOrchestrator, VexIngestOrchestrator>();
services.AddScoped<IVexObservationLookup, MongoVexObservationLookup>();
services.AddOptions<ExcititorObservabilityOptions>()
.Bind(configuration.GetSection("Excititor:Observability"));
services.AddScoped<ExcititorHealthService>();
@@ -63,14 +63,11 @@ services.Configure<VexAttestationVerificationOptions>(configuration.GetSection("
services.AddVexPolicy();
services.AddSingleton<IVexEvidenceChunkService, VexEvidenceChunkService>();
services.AddSingleton<ChunkTelemetry>();
services.AddSingleton<ChunkTelemetry>();
services.AddRedHatCsafConnector();
services.Configure<MirrorDistributionOptions>(configuration.GetSection(MirrorDistributionOptions.SectionName));
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())
@@ -196,7 +193,7 @@ app.MapPost("/v1/attestations/verify", async (
var attestationRequest = new VexAttestationRequest(
request.ExportId.Trim(),
new VexQuerySignature(request.QuerySignature.Trim()),
new VexContentAddress(request.ArtifactDigest.Trim()),
new VexContentAddress("sha256", request.ArtifactDigest.Trim()),
format,
request.CreatedAt,
request.SourceProviders?.ToImmutableArray() ?? ImmutableArray<string>.Empty,
@@ -206,8 +203,8 @@ app.MapPost("/v1/attestations/verify", async (
? null
: new VexRekorReference(
request.Attestation.Rekor.ApiVersion ?? "0.2",
request.Attestation.Rekor.Location,
request.Attestation.Rekor.LogIndex,
request.Attestation.Rekor.Location ?? string.Empty,
request.Attestation.Rekor.LogIndex?.ToString(CultureInfo.InvariantCulture),
request.Attestation.Rekor.InclusionProofUrl);
var attestationMetadata = new VexAttestationMetadata(
@@ -621,218 +618,6 @@ 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,
@@ -853,7 +638,7 @@ app.MapGet("/v1/vex/evidence/chunks", async (
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: false, out var tenant, out var tenantError))
{
chunkTelemetry.RecordIngested(tenant?.TenantId, null, "rejected", "tenant-invalid", 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
chunkTelemetry.RecordIngested(tenant, null, "rejected", "tenant-invalid", 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
return tenantError;
}
@@ -886,13 +671,13 @@ app.MapGet("/v1/vex/evidence/chunks", async (
catch (OperationCanceledException)
{
EvidenceTelemetry.RecordChunkOutcome(tenant, "cancelled");
chunkTelemetry.RecordIngested(tenant?.TenantId, request.ProviderFilter.Count > 0 ? string.Join(',', request.ProviderFilter) : null, "cancelled", null, 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
chunkTelemetry.RecordIngested(tenant, providerFilter.Count > 0 ? string.Join(',', providerFilter) : null, "cancelled", null, 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
return Results.StatusCode(StatusCodes.Status499ClientClosedRequest);
}
catch
{
EvidenceTelemetry.RecordChunkOutcome(tenant, "error");
chunkTelemetry.RecordIngested(tenant?.TenantId, request.ProviderFilter.Count > 0 ? string.Join(',', request.ProviderFilter) : null, "error", null, 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
chunkTelemetry.RecordIngested(tenant, providerFilter.Count > 0 ? string.Join(',', providerFilter) : null, "error", null, 0, 0, Stopwatch.GetElapsedTime(start).TotalMilliseconds);
throw;
}
@@ -928,8 +713,8 @@ app.MapGet("/v1/vex/evidence/chunks", async (
var elapsedMs = Stopwatch.GetElapsedTime(start).TotalMilliseconds;
chunkTelemetry.RecordIngested(
tenant?.TenantId,
request.ProviderFilter.Count > 0 ? string.Join(',', request.ProviderFilter) : null,
tenant,
providerFilter.Count > 0 ? string.Join(',', providerFilter) : null,
"success",
null,
result.TotalCount,
@@ -1085,6 +870,12 @@ IngestEndpoints.MapIngestEndpoints(app);
ResolveEndpoint.MapResolveEndpoint(app);
MirrorEndpoints.MapMirrorEndpoints(app);
app.MapGet("/v1/vex/observations", async (HttpContext _, CancellationToken __) =>
Results.StatusCode(StatusCodes.Status501NotImplemented));
app.MapGet("/v1/vex/linksets", async (HttpContext _, CancellationToken __) =>
Results.StatusCode(StatusCodes.Status501NotImplemented));
app.Run();
public partial class Program;
@@ -1185,90 +976,3 @@ 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);
});