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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user