Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly. - Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps. - Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges. - Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges. - Set up project file for the test project with necessary dependencies and configurations. - Include JSON fixture files for testing purposes.
This commit is contained in:
@@ -1,15 +1,33 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.EvidenceLocker.Core.Domain;
|
||||
using StellaOps.EvidenceLocker.Core.Storage;
|
||||
using StellaOps.EvidenceLocker.Infrastructure.DependencyInjection;
|
||||
using StellaOps.EvidenceLocker.Infrastructure.Services;
|
||||
using StellaOps.EvidenceLocker.WebService.Audit;
|
||||
using StellaOps.EvidenceLocker.WebService.Contracts;
|
||||
using StellaOps.EvidenceLocker.WebService.Security;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddEvidenceLockerInfrastructure(builder.Configuration);
|
||||
builder.Services.AddScoped<EvidenceSnapshotService>();
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configure: options =>
|
||||
{
|
||||
options.RequiredScopes.Clear();
|
||||
});
|
||||
configure: options => options.RequiredScopes.Clear());
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
@@ -34,13 +52,282 @@ app.UseHttpsRedirection();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapGet("/evidence/{id}", (string id) => Results.Ok(new { id, status = "available" }))
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead);
|
||||
app.MapHealthChecks("/health/ready");
|
||||
|
||||
app.MapPost("/evidence", () => Results.Accepted("/evidence", new { status = "queued" }))
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceCreate);
|
||||
app.MapPost("/evidence/snapshot",
|
||||
async (HttpContext context, ClaimsPrincipal user, EvidenceSnapshotRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
|
||||
|
||||
app.MapPost("/evidence/{id}/hold", (string id) => Results.Accepted($"/evidence/{id}/hold", new { id, status = "on-hold" }))
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceHold);
|
||||
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
|
||||
{
|
||||
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/snapshot");
|
||||
return ForbidTenant();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await service.CreateSnapshotAsync(tenantId, request.ToDomain(), cancellationToken);
|
||||
var materialCount = request.Materials.Count;
|
||||
var totalSize = request.Materials.Sum(material => material.SizeBytes);
|
||||
EvidenceAuditLogger.LogSnapshotCreated(logger, user, tenantId, request.Kind, result.BundleId, materialCount, totalSize);
|
||||
|
||||
var dto = new EvidenceSnapshotResponseDto(
|
||||
result.BundleId,
|
||||
result.RootHash,
|
||||
result.Signature.ToDto());
|
||||
|
||||
return Results.Created($"/evidence/{result.BundleId}", dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
EvidenceAuditLogger.LogSnapshotRejected(logger, user, tenantId, request.Kind, ex.Message);
|
||||
return ValidationProblem(ex.Message);
|
||||
}
|
||||
})
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceHold)
|
||||
.Produces<EvidenceSnapshotResponseDto>(StatusCodes.Status201Created)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
|
||||
.WithName("CreateEvidenceSnapshot")
|
||||
.WithTags("Evidence")
|
||||
.WithSummary("Create a new evidence snapshot for the tenant.");
|
||||
|
||||
app.MapGet("/evidence/{bundleId:guid}",
|
||||
async (HttpContext context, ClaimsPrincipal user, Guid bundleId, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
|
||||
|
||||
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
|
||||
{
|
||||
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/{bundleId}");
|
||||
return ForbidTenant();
|
||||
}
|
||||
|
||||
var details = await service.GetBundleAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
|
||||
if (details is null)
|
||||
{
|
||||
EvidenceAuditLogger.LogBundleNotFound(logger, user, tenantId, bundleId);
|
||||
return Results.NotFound(new ErrorResponse("not_found", "Evidence bundle not found."));
|
||||
}
|
||||
|
||||
EvidenceAuditLogger.LogBundleRetrieved(logger, user, tenantId, details.Bundle);
|
||||
|
||||
var dto = new EvidenceBundleResponseDto(
|
||||
details.Bundle.Id.Value,
|
||||
details.Bundle.Kind,
|
||||
details.Bundle.Status,
|
||||
details.Bundle.RootHash,
|
||||
details.Bundle.StorageKey,
|
||||
details.Bundle.CreatedAt,
|
||||
details.Bundle.UpdatedAt,
|
||||
details.Bundle.Description,
|
||||
details.Bundle.SealedAt,
|
||||
details.Bundle.ExpiresAt,
|
||||
details.Signature.ToDto());
|
||||
|
||||
return Results.Ok(dto);
|
||||
})
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
|
||||
.Produces<EvidenceBundleResponseDto>(StatusCodes.Status200OK)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status404NotFound)
|
||||
.WithName("GetEvidenceBundle")
|
||||
.WithTags("Evidence");
|
||||
|
||||
app.MapGet("/evidence/{bundleId:guid}/download",
|
||||
async (HttpContext context,
|
||||
ClaimsPrincipal user,
|
||||
Guid bundleId,
|
||||
EvidenceSnapshotService snapshotService,
|
||||
EvidenceBundlePackagingService packagingService,
|
||||
IEvidenceObjectStore objectStore,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
|
||||
|
||||
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
|
||||
{
|
||||
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/{bundleId}/download");
|
||||
return ForbidTenant();
|
||||
}
|
||||
|
||||
var bundle = await snapshotService.GetBundleAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
|
||||
if (bundle is null)
|
||||
{
|
||||
EvidenceAuditLogger.LogBundleNotFound(logger, user, tenantId, bundleId);
|
||||
return Results.NotFound(new ErrorResponse("not_found", "Evidence bundle not found."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var package = await packagingService.EnsurePackageAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
|
||||
EvidenceAuditLogger.LogBundleDownload(logger, user, tenantId, bundleId, package.StorageKey, package.Created);
|
||||
|
||||
var packageStream = await objectStore.OpenReadAsync(package.StorageKey, cancellationToken).ConfigureAwait(false);
|
||||
return Results.File(
|
||||
packageStream,
|
||||
contentType: "application/gzip",
|
||||
fileDownloadName: $"evidence-bundle-{bundleId:D}.tgz");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
EvidenceAuditLogger.LogBundleDownloadFailure(logger, user, tenantId, bundleId, ex.Message);
|
||||
return ValidationProblem(ex.Message);
|
||||
}
|
||||
})
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status404NotFound)
|
||||
.WithName("DownloadEvidenceBundle")
|
||||
.WithTags("Evidence");
|
||||
|
||||
app.MapGet("/evidence/{bundleId:guid}/portable",
|
||||
async (HttpContext context,
|
||||
ClaimsPrincipal user,
|
||||
Guid bundleId,
|
||||
EvidenceSnapshotService snapshotService,
|
||||
EvidencePortableBundleService portableService,
|
||||
IEvidenceObjectStore objectStore,
|
||||
ILoggerFactory loggerFactory,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
|
||||
|
||||
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
|
||||
{
|
||||
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/{bundleId}/portable");
|
||||
return ForbidTenant();
|
||||
}
|
||||
|
||||
var bundle = await snapshotService.GetBundleAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
|
||||
if (bundle is null)
|
||||
{
|
||||
EvidenceAuditLogger.LogBundleNotFound(logger, user, tenantId, bundleId);
|
||||
return Results.NotFound(new ErrorResponse("not_found", "Evidence bundle not found."));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var package = await portableService.EnsurePortablePackageAsync(tenantId, EvidenceBundleId.FromGuid(bundleId), cancellationToken);
|
||||
EvidenceAuditLogger.LogPortableDownload(logger, user, tenantId, bundleId, package.StorageKey, package.Created);
|
||||
|
||||
var packageStream = await objectStore.OpenReadAsync(package.StorageKey, cancellationToken).ConfigureAwait(false);
|
||||
return Results.File(
|
||||
packageStream,
|
||||
contentType: "application/gzip",
|
||||
fileDownloadName: $"portable-evidence-bundle-{bundleId:D}.tgz");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
EvidenceAuditLogger.LogPortableDownloadFailure(logger, user, tenantId, bundleId, ex.Message);
|
||||
return ValidationProblem(ex.Message);
|
||||
}
|
||||
})
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status404NotFound)
|
||||
.WithName("DownloadPortableEvidenceBundle")
|
||||
.WithTags("Evidence")
|
||||
.WithSummary("Download a sealed, portable evidence bundle for sealed or air-gapped distribution.");
|
||||
|
||||
app.MapPost("/evidence/verify",
|
||||
async (HttpContext context, ClaimsPrincipal user, EvidenceVerifyRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
|
||||
|
||||
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
|
||||
{
|
||||
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/verify");
|
||||
return ForbidTenant();
|
||||
}
|
||||
|
||||
var trusted = await service.VerifyAsync(tenantId, EvidenceBundleId.FromGuid(request.BundleId), request.RootHash, cancellationToken);
|
||||
EvidenceAuditLogger.LogVerificationResult(logger, user, tenantId, request.BundleId, request.RootHash, trusted);
|
||||
|
||||
return Results.Ok(new EvidenceVerifyResponseDto(trusted));
|
||||
})
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceRead)
|
||||
.Produces<EvidenceVerifyResponseDto>(StatusCodes.Status200OK)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
|
||||
.WithName("VerifyEvidenceBundle")
|
||||
.WithTags("Evidence");
|
||||
|
||||
app.MapPost("/evidence/hold/{caseId}",
|
||||
async (HttpContext context, ClaimsPrincipal user, string caseId, EvidenceHoldRequestDto request, EvidenceSnapshotService service, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger(EvidenceAuditLogger.LoggerName);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(caseId))
|
||||
{
|
||||
return ValidationProblem("Case identifier is required.");
|
||||
}
|
||||
|
||||
if (!TenantResolution.TryResolveTenant(user, out var tenantId))
|
||||
{
|
||||
EvidenceAuditLogger.LogTenantMissing(logger, user, context.Request.Path.Value ?? "/evidence/hold/{caseId}");
|
||||
return ForbidTenant();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var hold = await service.CreateHoldAsync(tenantId, caseId, request.ToDomain(), cancellationToken);
|
||||
EvidenceAuditLogger.LogHoldCreated(logger, user, tenantId, hold);
|
||||
|
||||
var dto = new EvidenceHoldResponseDto(
|
||||
hold.Id.Value,
|
||||
hold.BundleId?.Value,
|
||||
hold.CaseId,
|
||||
hold.Reason,
|
||||
hold.CreatedAt,
|
||||
hold.ExpiresAt,
|
||||
hold.ReleasedAt,
|
||||
hold.Notes);
|
||||
|
||||
return Results.Created($"/evidence/hold/{hold.Id.Value}", dto);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (ex.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase) && request.BundleId is Guid referencedBundle)
|
||||
{
|
||||
EvidenceAuditLogger.LogHoldBundleMissing(logger, user, tenantId, caseId, referencedBundle);
|
||||
}
|
||||
else if (ex.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EvidenceAuditLogger.LogHoldConflict(logger, user, tenantId, caseId);
|
||||
}
|
||||
else
|
||||
{
|
||||
EvidenceAuditLogger.LogHoldValidationFailure(logger, user, tenantId, caseId, ex.Message);
|
||||
}
|
||||
|
||||
return ValidationProblem(ex.Message);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
EvidenceAuditLogger.LogHoldValidationFailure(logger, user, tenantId, caseId, ex.Message);
|
||||
return ValidationProblem(ex.Message);
|
||||
}
|
||||
})
|
||||
.RequireAuthorization(StellaOpsResourceServerPolicies.EvidenceCreate)
|
||||
.Produces<EvidenceHoldResponseDto>(StatusCodes.Status201Created)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status403Forbidden)
|
||||
.WithName("CreateEvidenceHold")
|
||||
.WithTags("Evidence")
|
||||
.WithSummary("Create a legal hold for the specified case identifier.");
|
||||
|
||||
app.Run();
|
||||
|
||||
static IResult ForbidTenant() => Results.Forbid();
|
||||
|
||||
static IResult ValidationProblem(string message)
|
||||
=> Results.ValidationProblem(new Dictionary<string, string[]>
|
||||
{
|
||||
["message"] = new[] { message }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user