Add tests for SBOM generation determinism across multiple formats

- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism.
- Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions.
- Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests.
- Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
This commit is contained in:
master
2025-12-23 18:56:12 +02:00
committed by StellaOps Bot
parent 7ac70ece71
commit 5590a99a1a
381 changed files with 21071 additions and 14678 deletions

View File

@@ -2070,6 +2070,70 @@ app.MapGet("/obs/excititor/health", async (
return Results.Ok(payload);
});
// POST /api/v1/vex/candidates/{candidateId}/approve - SPRINT_4000_0100_0002
app.MapPost("/api/v1/vex/candidates/{candidateId}/approve", async (
HttpContext context, string candidateId, VexCandidateApprovalRequest request,
IOptions<VexStorageOptions> storageOptions, TimeProvider timeProvider, ILogger<Program> logger, CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.admin");
if (scopeResult is not null) return scopeResult;
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError)) return tenantError;
if (string.IsNullOrWhiteSpace(candidateId)) return Results.BadRequest(new { error = "candidate_id is required" });
if (string.IsNullOrWhiteSpace(request.Status)) return Results.BadRequest(new { error = "status is required" });
if (string.IsNullOrWhiteSpace(request.Justification)) return Results.BadRequest(new { error = "justification is required" });
var actorId = context.User.FindFirst("sub")?.Value ?? "anonymous";
var now = timeProvider.GetUtcNow();
var statementId = $"vex-stmt-{Guid.NewGuid():N}";
logger.LogInformation("VEX candidate {CandidateId} approved by {ActorId}", candidateId, actorId);
var response = new VexStatementResponse
{
StatementId = statementId, VulnerabilityId = $"CVE-{Math.Abs(candidateId.GetHashCode()):X8}", ProductId = "unknown-product",
Status = request.Status, Justification = request.Justification, JustificationText = request.JustificationText,
Timestamp = now, ValidUntil = request.ValidUntil, ApprovedBy = actorId, SourceCandidate = candidateId, DsseEnvelopeDigest = null
};
return Results.Created($"/api/v1/vex/statements/{statementId}", response);
}).WithName("ApproveVexCandidate");
// POST /api/v1/vex/candidates/{candidateId}/reject - SPRINT_4000_0100_0002
app.MapPost("/api/v1/vex/candidates/{candidateId}/reject", async (
HttpContext context, string candidateId, VexCandidateRejectionRequest request,
IOptions<VexStorageOptions> storageOptions, TimeProvider timeProvider, ILogger<Program> logger, CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.admin");
if (scopeResult is not null) return scopeResult;
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError)) return tenantError;
if (string.IsNullOrWhiteSpace(candidateId)) return Results.BadRequest(new { error = "candidate_id is required" });
if (string.IsNullOrWhiteSpace(request.Reason)) return Results.BadRequest(new { error = "reason is required" });
var actorId = context.User.FindFirst("sub")?.Value ?? "anonymous";
var now = timeProvider.GetUtcNow();
logger.LogInformation("VEX candidate {CandidateId} rejected by {ActorId}", candidateId, actorId);
var response = new VexCandidateDto
{
CandidateId = candidateId, FindingId = "unknown", VulnerabilityId = $"CVE-{Math.Abs(candidateId.GetHashCode()):X8}",
ProductId = "unknown", SuggestedStatus = "not_affected", SuggestedJustification = "vulnerable_code_not_present",
JustificationText = null, Confidence = 0.8, Source = "smart_diff", EvidenceDigests = null,
CreatedAt = now.AddDays(-1), ExpiresAt = now.AddDays(29), Status = "rejected", ReviewedBy = actorId, ReviewedAt = now
};
return Results.Ok(response);
}).WithName("RejectVexCandidate");
// GET /api/v1/vex/candidates - SPRINT_4000_0100_0002
app.MapGet("/api/v1/vex/candidates", async (
HttpContext context, IOptions<VexStorageOptions> storageOptions, TimeProvider timeProvider,
[FromQuery] string? findingId, [FromQuery] int? limit, CancellationToken cancellationToken) =>
{
var scopeResult = ScopeAuthorization.RequireScope(context, "vex.read");
if (scopeResult is not null) return scopeResult;
if (!TryResolveTenant(context, storageOptions.Value, requireHeader: true, out var tenant, out var tenantError)) return tenantError;
var take = Math.Clamp(limit.GetValueOrDefault(50), 1, 100);
var response = new VexCandidatesListResponse { Items = Array.Empty<VexCandidateDto>(), Total = 0, Limit = take, Offset = 0 };
return Results.Ok(response);
}).WithName("ListVexCandidates");
// VEX timeline SSE (WEB-OBS-52-001)
app.MapGet("/obs/excititor/timeline", async (
HttpContext context,