Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -71,6 +71,36 @@ builder.Services.AddSingleton<ISbomLedgerService, SbomLedgerService>();
|
||||
builder.Services.AddSingleton<ISbomAnalysisTrigger, InMemorySbomAnalysisTrigger>();
|
||||
builder.Services.AddSingleton<ISbomUploadService, SbomUploadService>();
|
||||
|
||||
// Lineage graph services (LIN-BE-013)
|
||||
builder.Services.AddSingleton<ISbomLineageEdgeRepository, InMemorySbomLineageEdgeRepository>();
|
||||
|
||||
// LIN-BE-015: Hover card cache for <150ms response times
|
||||
// Use distributed cache if configured, otherwise in-memory
|
||||
var hoverCacheConfig = builder.Configuration.GetSection("SbomService:HoverCache");
|
||||
if (hoverCacheConfig.GetValue<bool>("UseDistributed"))
|
||||
{
|
||||
// Expects IDistributedCache to be registered (e.g., Valkey/Redis)
|
||||
builder.Services.AddSingleton<ILineageHoverCache, DistributedLineageHoverCache>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddSingleton<ILineageHoverCache, InMemoryLineageHoverCache>();
|
||||
}
|
||||
builder.Services.AddSingleton<ISbomLineageGraphService, SbomLineageGraphService>();
|
||||
|
||||
// LIN-BE-028: Lineage compare service
|
||||
builder.Services.AddSingleton<ILineageCompareService, LineageCompareService>();
|
||||
|
||||
// LIN-BE-023: Replay hash service
|
||||
builder.Services.AddSingleton<IReplayHashService, ReplayHashService>();
|
||||
|
||||
// LIN-BE-033: Replay verification service
|
||||
builder.Services.AddSingleton<IReplayVerificationService, ReplayVerificationService>();
|
||||
|
||||
// LIN-BE-034: Compare cache with TTL and VEX invalidation
|
||||
builder.Services.Configure<CompareCacheOptions>(builder.Configuration.GetSection("SbomService:CompareCache"));
|
||||
builder.Services.AddSingleton<ILineageCompareCache, InMemoryLineageCompareCache>();
|
||||
|
||||
builder.Services.AddSingleton<IProjectionRepository>(sp =>
|
||||
{
|
||||
var config = sp.GetRequiredService<IConfiguration>();
|
||||
@@ -618,6 +648,307 @@ app.MapGet("/sbom/ledger/lineage", async Task<IResult> (
|
||||
return Results.Ok(lineage);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Lineage Graph API Endpoints (LIN-BE-013/014)
|
||||
// Sprint: SPRINT_20251228_005_BE_sbom_lineage_graph_i
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
app.MapGet("/api/v1/lineage/{artifactDigest}", async Task<IResult> (
|
||||
[FromServices] ISbomLineageGraphService lineageService,
|
||||
[FromRoute] string artifactDigest,
|
||||
[FromQuery] string? tenant,
|
||||
[FromQuery] int? maxDepth,
|
||||
[FromQuery] bool? includeBadges,
|
||||
[FromQuery] bool? includeReplayHash,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifactDigest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "artifactDigest is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant is required" });
|
||||
}
|
||||
|
||||
var options = new SbomLineageQueryOptions
|
||||
{
|
||||
MaxDepth = maxDepth ?? 10,
|
||||
IncludeBadges = includeBadges ?? true,
|
||||
IncludeReplayHash = includeReplayHash ?? false
|
||||
};
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.graph", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", tenant);
|
||||
activity?.SetTag("artifact_digest", artifactDigest);
|
||||
|
||||
var graph = await lineageService.GetLineageGraphAsync(
|
||||
artifactDigest.Trim(),
|
||||
tenant.Trim(),
|
||||
options,
|
||||
cancellationToken);
|
||||
|
||||
if (graph is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "lineage graph not found" });
|
||||
}
|
||||
|
||||
return Results.Ok(graph);
|
||||
});
|
||||
|
||||
app.MapGet("/api/v1/lineage/diff", async Task<IResult> (
|
||||
[FromServices] ISbomLineageGraphService lineageService,
|
||||
[FromQuery] string? from,
|
||||
[FromQuery] string? to,
|
||||
[FromQuery] string? tenant,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
||||
{
|
||||
return Results.BadRequest(new { error = "from and to digests are required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant is required" });
|
||||
}
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.diff", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", tenant);
|
||||
activity?.SetTag("from_digest", from);
|
||||
activity?.SetTag("to_digest", to);
|
||||
|
||||
var diff = await lineageService.GetLineageDiffAsync(
|
||||
from.Trim(),
|
||||
to.Trim(),
|
||||
tenant.Trim(),
|
||||
cancellationToken);
|
||||
|
||||
if (diff is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "lineage diff not found" });
|
||||
}
|
||||
|
||||
SbomMetrics.LedgerDiffsTotal.Add(1);
|
||||
return Results.Ok(diff);
|
||||
});
|
||||
|
||||
app.MapGet("/api/v1/lineage/hover", async Task<IResult> (
|
||||
[FromServices] ISbomLineageGraphService lineageService,
|
||||
[FromQuery] string? from,
|
||||
[FromQuery] string? to,
|
||||
[FromQuery] string? tenant,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to))
|
||||
{
|
||||
return Results.BadRequest(new { error = "from and to digests are required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant is required" });
|
||||
}
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.hover", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", tenant);
|
||||
|
||||
var hoverCard = await lineageService.GetHoverCardAsync(
|
||||
from.Trim(),
|
||||
to.Trim(),
|
||||
tenant.Trim(),
|
||||
cancellationToken);
|
||||
|
||||
if (hoverCard is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "hover card data not found" });
|
||||
}
|
||||
|
||||
return Results.Ok(hoverCard);
|
||||
});
|
||||
|
||||
app.MapGet("/api/v1/lineage/{artifactDigest}/children", async Task<IResult> (
|
||||
[FromServices] ISbomLineageGraphService lineageService,
|
||||
[FromRoute] string artifactDigest,
|
||||
[FromQuery] string? tenant,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifactDigest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "artifactDigest is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant is required" });
|
||||
}
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.children", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", tenant);
|
||||
activity?.SetTag("artifact_digest", artifactDigest);
|
||||
|
||||
var children = await lineageService.GetChildrenAsync(
|
||||
artifactDigest.Trim(),
|
||||
tenant.Trim(),
|
||||
cancellationToken);
|
||||
|
||||
return Results.Ok(new { parentDigest = artifactDigest.Trim(), children });
|
||||
});
|
||||
|
||||
app.MapGet("/api/v1/lineage/{artifactDigest}/parents", async Task<IResult> (
|
||||
[FromServices] ISbomLineageGraphService lineageService,
|
||||
[FromRoute] string artifactDigest,
|
||||
[FromQuery] string? tenant,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(artifactDigest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "artifactDigest is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant is required" });
|
||||
}
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.parents", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", tenant);
|
||||
activity?.SetTag("artifact_digest", artifactDigest);
|
||||
|
||||
var parents = await lineageService.GetParentsAsync(
|
||||
artifactDigest.Trim(),
|
||||
tenant.Trim(),
|
||||
cancellationToken);
|
||||
|
||||
return Results.Ok(new { childDigest = artifactDigest.Trim(), parents });
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Lineage Compare API (LIN-BE-028)
|
||||
// Sprint: SPRINT_20251228_007_BE_sbom_lineage_graph_ii
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
app.MapGet("/api/v1/lineage/compare", async Task<IResult> (
|
||||
[FromServices] ILineageCompareService compareService,
|
||||
[FromQuery(Name = "a")] string? fromDigest,
|
||||
[FromQuery(Name = "b")] string? toDigest,
|
||||
[FromQuery] string? tenant,
|
||||
[FromQuery] bool? includeSbomDiff,
|
||||
[FromQuery] bool? includeVexDeltas,
|
||||
[FromQuery] bool? includeReachabilityDeltas,
|
||||
[FromQuery] bool? includeAttestations,
|
||||
[FromQuery] bool? includeReplayHashes,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fromDigest) || string.IsNullOrWhiteSpace(toDigest))
|
||||
{
|
||||
return Results.BadRequest(new { error = "a (from digest) and b (to digest) query parameters are required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenant))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenant is required" });
|
||||
}
|
||||
|
||||
var options = new LineageCompareOptions
|
||||
{
|
||||
IncludeSbomDiff = includeSbomDiff ?? true,
|
||||
IncludeVexDeltas = includeVexDeltas ?? true,
|
||||
IncludeReachabilityDeltas = includeReachabilityDeltas ?? true,
|
||||
IncludeAttestations = includeAttestations ?? true,
|
||||
IncludeReplayHashes = includeReplayHashes ?? true
|
||||
};
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.compare", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", tenant);
|
||||
activity?.SetTag("from_digest", fromDigest);
|
||||
activity?.SetTag("to_digest", toDigest);
|
||||
|
||||
var result = await compareService.CompareAsync(
|
||||
fromDigest.Trim(),
|
||||
toDigest.Trim(),
|
||||
tenant.Trim(),
|
||||
options,
|
||||
cancellationToken);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "comparison data not found for the specified artifacts" });
|
||||
}
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Replay Verification API (LIN-BE-033)
|
||||
// Sprint: SPRINT_20251228_007_BE_sbom_lineage_graph_ii
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
app.MapPost("/api/v1/lineage/verify", async Task<IResult> (
|
||||
[FromServices] IReplayVerificationService verificationService,
|
||||
[FromBody] ReplayVerifyRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ReplayHash))
|
||||
{
|
||||
return Results.BadRequest(new { error = "replayHash is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.TenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenantId is required" });
|
||||
}
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.verify", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", request.TenantId);
|
||||
activity?.SetTag("replay_hash", request.ReplayHash.Length > 16 ? request.ReplayHash[..16] + "..." : request.ReplayHash);
|
||||
|
||||
var verifyRequest = new ReplayVerificationRequest
|
||||
{
|
||||
ReplayHash = request.ReplayHash,
|
||||
TenantId = request.TenantId,
|
||||
SbomDigest = request.SbomDigest,
|
||||
FeedsSnapshotDigest = request.FeedsSnapshotDigest,
|
||||
PolicyVersion = request.PolicyVersion,
|
||||
VexVerdictsDigest = request.VexVerdictsDigest,
|
||||
Timestamp = request.Timestamp,
|
||||
FreezeTime = request.FreezeTime ?? true,
|
||||
ReEvaluatePolicy = request.ReEvaluatePolicy ?? false
|
||||
};
|
||||
|
||||
var result = await verificationService.VerifyAsync(verifyRequest, cancellationToken);
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
app.MapPost("/api/v1/lineage/compare-drift", async Task<IResult> (
|
||||
[FromServices] IReplayVerificationService verificationService,
|
||||
[FromBody] CompareDriftRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.HashA) || string.IsNullOrWhiteSpace(request.HashB))
|
||||
{
|
||||
return Results.BadRequest(new { error = "hashA and hashB are required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.TenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "tenantId is required" });
|
||||
}
|
||||
|
||||
using var activity = SbomTracing.Source.StartActivity("lineage.compare-drift", ActivityKind.Server);
|
||||
activity?.SetTag("tenant", request.TenantId);
|
||||
|
||||
var result = await verificationService.CompareDriftAsync(
|
||||
request.HashA,
|
||||
request.HashB,
|
||||
request.TenantId,
|
||||
cancellationToken);
|
||||
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
app.MapGet("/sboms/{snapshotId}/projection", async Task<IResult> (
|
||||
[FromServices] ISbomQueryService service,
|
||||
[FromRoute] string? snapshotId,
|
||||
@@ -932,4 +1263,5 @@ app.MapPost("/internal/orchestrator/watermarks", async Task<IResult> (
|
||||
|
||||
app.Run();
|
||||
|
||||
// Program class public for WebApplicationFactory<Program>
|
||||
public partial class Program;
|
||||
|
||||
Reference in New Issue
Block a user