using System.Globalization; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StellaOps.SbomService.Models; using StellaOps.SbomService.Services; using StellaOps.SbomService.Observability; using StellaOps.SbomService.Repositories; using System.Text.Json; using System.Diagnostics; var builder = WebApplication.CreateBuilder(args); builder.Configuration .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables("SBOM_"); builder.Services.AddOptions(); builder.Services.AddLogging(); // Register SBOM query services using file-backed fixtures when present; fallback to in-memory seeds. builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); var env = sp.GetRequiredService(); var configured = config.GetValue("SbomService:ComponentLookupPath"); if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured)) { return new FileComponentLookupRepository(configured!); } var candidate = FindFixture(env, "component_lookup.json"); return candidate is not null ? new FileComponentLookupRepository(candidate) : new InMemoryComponentLookupRepository(); }); builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); var env = sp.GetRequiredService(); var configured = config.GetValue("SbomService:CatalogPath"); if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured)) { return new FileCatalogRepository(configured!); } var candidate = FindFixture(env, "catalog.json"); return candidate is not null ? new FileCatalogRepository(candidate) : new InMemoryCatalogRepository(); }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => new OrchestratorControlService( sp.GetRequiredService(), SbomMetrics.Meter)); builder.Services.AddSingleton(); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("SbomService:Ledger")); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); var env = sp.GetRequiredService(); var configured = config.GetValue("SbomService:ProjectionsPath"); if (!string.IsNullOrWhiteSpace(configured)) { return new FileProjectionRepository(configured!); } var candidateRoots = new[] { env.ContentRootPath, Path.GetFullPath(Path.Combine(env.ContentRootPath, "..")), Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..")), Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..", "..")) }; foreach (var root in candidateRoots) { var candidate = Path.Combine(root, "docs", "modules", "sbomservice", "fixtures", "lnm-v1", "projections.json"); if (File.Exists(candidate)) { return new FileProjectionRepository(candidate); } } return new FileProjectionRepository(string.Empty); }); static string? FindFixture(IHostEnvironment env, string fileName) { var candidateRoots = new[] { env.ContentRootPath, Path.GetFullPath(Path.Combine(env.ContentRootPath, "..")), Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..")), Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "..", "..")) }; foreach (var root in candidateRoots) { var candidate = Path.Combine(root, "docs", "modules", "sbomservice", "fixtures", "lnm-v1", fileName); if (File.Exists(candidate)) { return candidate; } } return null; } static int NormalizeLimit(int? requested, int defaultValue, int ceiling) { if (!requested.HasValue) { return defaultValue; } if (requested.Value <= 0) { return 0; } return Math.Min(requested.Value, ceiling); } var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.Use(async (context, next) => { try { await next(); } catch (Exception ex) { Console.WriteLine($"[dev-exception] {ex}"); throw; } }); app.UseDeveloperExceptionPage(); } app.MapGet("/healthz", () => Results.Ok(new { status = "ok" })); app.MapGet("/readyz", () => Results.Ok(new { status = "warming" })); app.MapGet("/entrypoints", async Task ( [FromServices] IEntrypointRepository repo, [FromQuery] string? tenant, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(tenant)) { return Results.BadRequest(new { error = "tenant is required" }); } var tenantId = tenant.Trim(); using var activity = SbomTracing.Source.StartActivity("entrypoints.list", ActivityKind.Server); activity?.SetTag("tenant", tenantId); var items = await repo.ListAsync(tenantId, cancellationToken); return Results.Ok(new EntrypointListResponse(tenantId, items)); }); app.MapPost("/entrypoints", async Task ( [FromServices] IEntrypointRepository repo, [FromBody] EntrypointUpsertRequest request, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(request.Tenant)) { return Results.BadRequest(new { error = "tenant is required" }); } if (string.IsNullOrWhiteSpace(request.Artifact) || string.IsNullOrWhiteSpace(request.Service) || string.IsNullOrWhiteSpace(request.Path)) { return Results.BadRequest(new { error = "artifact, service, and path are required" }); } var entrypoint = new Entrypoint( request.Artifact.Trim(), request.Service.Trim(), request.Path.Trim(), string.IsNullOrWhiteSpace(request.Scope) ? "runtime" : request.Scope.Trim(), request.RuntimeFlag); var tenantId = request.Tenant.Trim(); using var activity = SbomTracing.Source.StartActivity("entrypoints.upsert", ActivityKind.Server); activity?.SetTag("tenant", tenantId); activity?.SetTag("artifact", entrypoint.Artifact); activity?.SetTag("service", entrypoint.Service); await repo.UpsertAsync(tenantId, entrypoint, cancellationToken); var items = await repo.ListAsync(tenantId, cancellationToken); return Results.Ok(new EntrypointListResponse(tenantId, items)); }); app.MapGet("/console/sboms", async Task ( [FromServices] ISbomQueryService service, [FromQuery] string? artifact, [FromQuery] string? license, [FromQuery] string? scope, [FromQuery(Name = "assetTag")] string? assetTag, [FromQuery] string? cursor, [FromQuery] int? limit, CancellationToken cancellationToken) => { if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200)) { return Results.BadRequest(new { error = "limit must be between 1 and 200" }); } if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { return Results.BadRequest(new { error = "cursor must be an integer offset" }); } var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture); var pageSize = limit ?? 50; using var activity = SbomTracing.Source.StartActivity("console.sboms", ActivityKind.Server); activity?.SetTag("artifact", artifact); var start = Stopwatch.GetTimestamp(); var result = await service.GetConsoleCatalogAsync( new SbomCatalogQuery(artifact?.Trim(), license?.Trim(), scope?.Trim(), assetTag?.Trim(), pageSize, offset), cancellationToken); var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds; SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new[] { new KeyValuePair("scope", scope ?? string.Empty), new KeyValuePair("env", string.Empty) }); SbomMetrics.PathsQueryTotal.Add(1, new[] { new KeyValuePair("cache_hit", result.CacheHit), new KeyValuePair("scope", scope ?? string.Empty) }); return Results.Ok(result.Result); }); app.MapGet("/components/lookup", async Task ( [FromServices] ISbomQueryService service, [FromQuery] string? purl, [FromQuery] string? artifact, [FromQuery] string? cursor, [FromQuery] int? limit, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(purl)) { return Results.BadRequest(new { error = "purl is required" }); } if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200)) { return Results.BadRequest(new { error = "limit must be between 1 and 200" }); } if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { return Results.BadRequest(new { error = "cursor must be an integer offset" }); } var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture); var pageSize = limit ?? 50; using var activity = SbomTracing.Source.StartActivity("components.lookup", ActivityKind.Server); activity?.SetTag("purl", purl); activity?.SetTag("artifact", artifact); var start = Stopwatch.GetTimestamp(); var result = await service.GetComponentLookupAsync( new ComponentLookupQuery(purl.Trim(), artifact?.Trim(), pageSize, offset), cancellationToken); var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds; SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new[] { new KeyValuePair("scope", string.Empty), new KeyValuePair("env", string.Empty) }); SbomMetrics.PathsQueryTotal.Add(1, new[] { new KeyValuePair("cache_hit", result.CacheHit), new KeyValuePair("scope", string.Empty) }); return Results.Ok(result.Result); }); app.MapGet("/sbom/context", async Task ( [FromServices] ISbomQueryService service, [FromServices] IClock clock, [FromQuery(Name = "artifactId")] string? artifactId, [FromQuery] string? purl, [FromQuery] int? maxTimelineEntries, [FromQuery] int? maxDependencyPaths, [FromQuery] bool? includeEnvironmentFlags, [FromQuery] bool? includeBlastRadius, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifactId)) { return Results.BadRequest(new { error = "artifactId is required" }); } var normalizedArtifact = artifactId.Trim(); var normalizedPurl = string.IsNullOrWhiteSpace(purl) ? null : purl.Trim(); var timelineLimit = NormalizeLimit(maxTimelineEntries, 50, 500); var dependencyLimit = NormalizeLimit(maxDependencyPaths, 25, 200); var includeEnvFlags = includeEnvironmentFlags ?? true; var includeBlast = includeBlastRadius ?? true; IReadOnlyList versions = Array.Empty(); if (timelineLimit > 0) { var timeline = await service.GetTimelineAsync( new SbomTimelineQuery(normalizedArtifact, timelineLimit, 0), cancellationToken); versions = timeline.Result.Versions; } IReadOnlyList dependencyPaths = Array.Empty(); if (dependencyLimit > 0 && !string.IsNullOrWhiteSpace(normalizedPurl)) { var artifactFilter = normalizedArtifact.Contains('@', StringComparison.Ordinal) ? normalizedArtifact : null; var pathResult = await service.GetPathsAsync( new SbomPathQuery(normalizedPurl!, artifactFilter, Scope: null, Environment: null, Limit: dependencyLimit, Offset: 0), cancellationToken); dependencyPaths = pathResult.Result.Paths; } if (versions.Count == 0 && dependencyPaths.Count == 0) { return Results.NotFound(new { error = "No SBOM context available for specified artifact/purl." }); } var response = SbomContextAssembler.Build( normalizedArtifact, normalizedPurl, clock.UtcNow, versions, dependencyPaths, includeEnvFlags, includeBlast); return Results.Ok(response); }); app.MapGet("/sbom/paths", async Task ( [FromServices] IServiceProvider services, [FromQuery] string? purl, [FromQuery] string? artifact, [FromQuery] string? scope, [FromQuery(Name = "env")] string? environment, [FromQuery] string? cursor, [FromQuery] int? limit, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(purl)) { return Results.BadRequest(new { error = "purl is required" }); } if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200)) { return Results.BadRequest(new { error = "limit must be between 1 and 200" }); } if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { return Results.BadRequest(new { error = "cursor must be an integer offset" }); } var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture); var pageSize = limit ?? 50; var service = services.GetRequiredService(); var start = Stopwatch.GetTimestamp(); var result = await service.GetPathsAsync( new SbomPathQuery(purl.Trim(), artifact?.Trim(), scope?.Trim(), environment?.Trim(), pageSize, offset), cancellationToken); var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds; SbomMetrics.PathsLatencySeconds.Record(elapsedSeconds, new[] { new KeyValuePair("scope", scope ?? string.Empty), new KeyValuePair("env", environment ?? string.Empty) }); SbomMetrics.PathsQueryTotal.Add(1, new[] { new KeyValuePair("cache_hit", result.CacheHit), new KeyValuePair("scope", scope ?? string.Empty) }); return Results.Ok(result.Result); }); app.MapGet("/sbom/versions", async Task ( [FromServices] ISbomQueryService service, [FromQuery] string? artifact, [FromQuery] string? cursor, [FromQuery] int? limit, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } if (limit is { } requestedLimit && (requestedLimit <= 0 || requestedLimit > 200)) { return Results.BadRequest(new { error = "limit must be between 1 and 200" }); } if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { return Results.BadRequest(new { error = "cursor must be an integer offset" }); } var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture); var pageSize = limit ?? 50; var start = Stopwatch.GetTimestamp(); var result = await service.GetTimelineAsync( new SbomTimelineQuery(artifact.Trim(), pageSize, offset), cancellationToken); var elapsedSeconds = Stopwatch.GetElapsedTime(start).TotalSeconds; SbomMetrics.TimelineLatencySeconds.Record(elapsedSeconds, new[] { new KeyValuePair("artifact", artifact) }); SbomMetrics.TimelineQueryTotal.Add(1, new[] { new KeyValuePair("artifact", artifact), new KeyValuePair("cache_hit", result.CacheHit) }); return Results.Ok(result.Result); }); var sbomUploadHandler = async Task ( [FromBody] SbomUploadRequest request, [FromServices] ISbomUploadService uploadService, CancellationToken cancellationToken) => { var (response, validation) = await uploadService.UploadAsync(request, cancellationToken); if (!validation.Valid) { return Results.BadRequest(new { error = "sbom_upload_validation_failed", validation }); } SbomMetrics.LedgerUploadsTotal.Add(1); return Results.Accepted($"/sbom/ledger/history?artifact={Uri.EscapeDataString(response.ArtifactRef)}", response); }; app.MapPost("/sbom/upload", sbomUploadHandler); app.MapPost("/api/v1/sbom/upload", sbomUploadHandler); app.MapGet("/sbom/ledger/history", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? artifact, [FromQuery] string? cursor, [FromQuery] int? limit, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { return Results.BadRequest(new { error = "cursor must be an integer offset" }); } var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture); var pageSize = NormalizeLimit(limit, 50, 200); var history = await ledgerService.GetHistoryAsync(artifact.Trim(), pageSize, offset, cancellationToken); if (history is null) { return Results.NotFound(new { error = "ledger history not found" }); } return Results.Ok(history); }); app.MapGet("/sbom/ledger/point", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? artifact, [FromQuery] string? at, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } if (string.IsNullOrWhiteSpace(at) || !DateTimeOffset.TryParse(at, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var atUtc)) { return Results.BadRequest(new { error = "at must be an ISO-8601 timestamp" }); } var result = await ledgerService.GetAtTimeAsync(artifact.Trim(), atUtc, cancellationToken); if (result is null) { return Results.NotFound(new { error = "ledger point not found" }); } return Results.Ok(result); }); app.MapGet("/sbom/ledger/range", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? artifact, [FromQuery] string? start, [FromQuery] string? end, [FromQuery] string? cursor, [FromQuery] int? limit, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } if (string.IsNullOrWhiteSpace(start) || !DateTimeOffset.TryParse(start, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var startUtc)) { return Results.BadRequest(new { error = "start must be an ISO-8601 timestamp" }); } if (string.IsNullOrWhiteSpace(end) || !DateTimeOffset.TryParse(end, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var endUtc)) { return Results.BadRequest(new { error = "end must be an ISO-8601 timestamp" }); } if (cursor is { Length: > 0 } && !int.TryParse(cursor, NumberStyles.Integer, CultureInfo.InvariantCulture, out _)) { return Results.BadRequest(new { error = "cursor must be an integer offset" }); } var offset = cursor is null ? 0 : int.Parse(cursor, CultureInfo.InvariantCulture); var pageSize = NormalizeLimit(limit, 50, 200); var history = await ledgerService.GetRangeAsync(artifact.Trim(), startUtc, endUtc, pageSize, offset, cancellationToken); if (history is null) { return Results.NotFound(new { error = "ledger range not found" }); } return Results.Ok(history); }); app.MapGet("/sbom/ledger/diff", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? before, [FromQuery] string? after, CancellationToken cancellationToken) => { if (!Guid.TryParse(before, out var beforeId) || !Guid.TryParse(after, out var afterId)) { return Results.BadRequest(new { error = "before and after must be GUIDs" }); } var diff = await ledgerService.DiffAsync(beforeId, afterId, cancellationToken); if (diff is null) { return Results.NotFound(new { error = "diff not found" }); } SbomMetrics.LedgerDiffsTotal.Add(1); return Results.Ok(diff); }); app.MapGet("/sbom/ledger/lineage", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? artifact, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } var lineage = await ledgerService.GetLineageAsync(artifact.Trim(), cancellationToken); if (lineage is null) { return Results.NotFound(new { error = "lineage not found" }); } return Results.Ok(lineage); }); app.MapGet("/sboms/{snapshotId}/projection", async Task ( [FromServices] ISbomQueryService service, [FromRoute] string? snapshotId, [FromQuery(Name = "tenant")] string? tenantId, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(snapshotId)) { return Results.BadRequest(new { error = "snapshotId is required" }); } if (string.IsNullOrWhiteSpace(tenantId)) { return Results.BadRequest(new { error = "tenant is required" }); } var start = Stopwatch.GetTimestamp(); var projection = await service.GetProjectionAsync(snapshotId.Trim(), tenantId.Trim(), cancellationToken); if (projection is null) { return Results.NotFound(new { error = "projection not found" }); } using var activity = SbomTracing.Source.StartActivity("sbom.projection", ActivityKind.Server); activity?.SetTag("tenant", projection.TenantId); activity?.SetTag("snapshotId", projection.SnapshotId); activity?.SetTag("schema", projection.SchemaVersion); var payload = new { snapshotId = projection.SnapshotId, tenantId = projection.TenantId, schemaVersion = projection.SchemaVersion, hash = projection.ProjectionHash, projection = projection.Projection }; var json = JsonSerializer.Serialize(payload); var sizeBytes = System.Text.Encoding.UTF8.GetByteCount(json); SbomMetrics.ProjectionSizeBytes.Record(sizeBytes, new[] { new KeyValuePair("tenant", projection.TenantId) }); SbomMetrics.ProjectionLatencySeconds.Record(Stopwatch.GetElapsedTime(start).TotalSeconds, new[] { new KeyValuePair("tenant", projection.TenantId) }); SbomMetrics.ProjectionQueryTotal.Add(1, new[] { new KeyValuePair("tenant", projection.TenantId) }); app.Logger.LogInformation("projection_returned tenant={Tenant} snapshot={Snapshot} size={SizeBytes}", projection.TenantId, projection.SnapshotId, sizeBytes); return Results.Ok(payload); }); app.MapGet("/internal/sbom/events", async Task ( [FromServices] ISbomEventStore store, CancellationToken cancellationToken) => { using var activity = SbomTracing.Source.StartActivity("events.list", ActivityKind.Server); var events = await store.ListAsync(cancellationToken); SbomMetrics.EventBacklogSize.Record(events.Count); if (events.Count > 100) { app.Logger.LogWarning("sbom event backlog high: {Count}", events.Count); } return Results.Ok(events); }); app.MapGet("/internal/sbom/asset-events", async Task ( [FromServices] ISbomEventStore store, CancellationToken cancellationToken) => { using var activity = SbomTracing.Source.StartActivity("asset-events.list", ActivityKind.Server); var events = await store.ListAssetsAsync(cancellationToken); SbomMetrics.EventBacklogSize.Record(events.Count); if (events.Count > 100) { app.Logger.LogWarning("sbom asset event backlog high: {Count}", events.Count); } return Results.Ok(events); }); app.MapGet("/internal/sbom/ledger/audit", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? artifact, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } var audit = await ledgerService.GetAuditAsync(artifact.Trim(), cancellationToken); return Results.Ok(audit.OrderBy(a => a.TimestampUtc).ToList()); }); app.MapGet("/internal/sbom/analysis/jobs", async Task ( [FromServices] ISbomLedgerService ledgerService, [FromQuery] string? artifact, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(artifact)) { return Results.BadRequest(new { error = "artifact is required" }); } var jobs = await ledgerService.ListAnalysisJobsAsync(artifact.Trim(), cancellationToken); return Results.Ok(jobs.OrderBy(j => j.CreatedAtUtc).ToList()); }); app.MapPost("/internal/sbom/events/backfill", async Task ( [FromServices] IProjectionRepository repository, [FromServices] ISbomEventPublisher publisher, [FromServices] IClock clock, CancellationToken cancellationToken) => { var projections = await repository.ListAsync(cancellationToken); var published = 0; foreach (var projection in projections) { var evt = new SbomVersionCreatedEvent( projection.SnapshotId, projection.TenantId, projection.ProjectionHash, projection.SchemaVersion, clock.UtcNow); if (await publisher.PublishVersionCreatedAsync(evt, cancellationToken)) { published++; } } SbomMetrics.EventBacklogSize.Record(published); if (published > 0) { app.Logger.LogInformation("sbom events backfilled={Count}", published); } return Results.Ok(new { published }); }); app.MapGet("/internal/sbom/inventory", async Task ( [FromServices] ISbomEventStore store, CancellationToken cancellationToken) => { using var activity = SbomTracing.Source.StartActivity("inventory.list", ActivityKind.Server); var items = await store.ListInventoryAsync(cancellationToken); return Results.Ok(items); }); app.MapPost("/internal/sbom/inventory/backfill", async Task ( [FromServices] ISbomQueryService service, [FromServices] ISbomEventStore store, CancellationToken cancellationToken) => { // clear existing inventory and replay by listing projections await store.ClearInventoryAsync(cancellationToken); var projections = new[] { ("snap-001", "tenant-a") }; var published = 0; foreach (var (snapshot, tenant) in projections) { await service.GetProjectionAsync(snapshot, tenant, cancellationToken); published++; } return Results.Ok(new { published }); }); app.MapGet("/internal/sbom/resolver-feed", async Task ( [FromServices] ISbomEventStore store, CancellationToken cancellationToken) => { var feed = await store.ListResolverAsync(cancellationToken); return Results.Ok(feed); }); app.MapPost("/internal/sbom/resolver-feed/backfill", async Task ( [FromServices] ISbomEventStore store, [FromServices] ISbomQueryService service, CancellationToken cancellationToken) => { await store.ClearResolverAsync(cancellationToken); var projections = new[] { ("snap-001", "tenant-a") }; foreach (var (snapshot, tenant) in projections) { await service.GetProjectionAsync(snapshot, tenant, cancellationToken); } var feed = await store.ListResolverAsync(cancellationToken); return Results.Ok(new { published = feed.Count }); }); app.MapGet("/internal/sbom/resolver-feed/export", async Task ( [FromServices] ISbomEventStore store, CancellationToken cancellationToken) => { var feed = await store.ListResolverAsync(cancellationToken); var lines = feed.Select(candidate => JsonSerializer.Serialize(candidate)); var ndjson = string.Join('\n', lines); return Results.Text(ndjson, "application/x-ndjson"); }); app.MapPost("/internal/sbom/retention/prune", async Task ( [FromServices] ISbomLedgerService ledgerService, CancellationToken cancellationToken) => { var result = await ledgerService.ApplyRetentionAsync(cancellationToken); if (result.VersionsPruned > 0) { SbomMetrics.LedgerRetentionPrunedTotal.Add(result.VersionsPruned); } return Results.Ok(result); }); app.MapGet("/internal/orchestrator/sources", async Task ( [FromQuery] string? tenant, [FromServices] IOrchestratorRepository repository, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(tenant)) { return Results.BadRequest(new { error = "tenant required" }); } var sources = await repository.ListAsync(tenant.Trim(), cancellationToken); return Results.Ok(new { tenant = tenant.Trim(), items = sources }); }); app.MapPost("/internal/orchestrator/sources", async Task ( RegisterOrchestratorSourceRequest request, [FromServices] IOrchestratorRepository repository, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(request.TenantId)) { return Results.BadRequest(new { error = "tenant required" }); } if (string.IsNullOrWhiteSpace(request.ArtifactDigest)) { return Results.BadRequest(new { error = "artifactDigest required" }); } if (string.IsNullOrWhiteSpace(request.SourceType)) { return Results.BadRequest(new { error = "sourceType required" }); } var source = await repository.RegisterAsync(request, cancellationToken); return Results.Ok(source); }); app.MapGet("/internal/orchestrator/control", async Task ( [FromQuery] string? tenant, [FromServices] IOrchestratorControlService service, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(tenant)) { return Results.BadRequest(new { error = "tenant required" }); } var state = await service.GetAsync(tenant.Trim(), cancellationToken); return Results.Ok(state); }); app.MapPost("/internal/orchestrator/control", async Task ( OrchestratorControlRequest request, [FromServices] IOrchestratorControlService service, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(request.TenantId)) { return Results.BadRequest(new { error = "tenant required" }); } var updated = await service.UpdateAsync(request, cancellationToken); return Results.Ok(updated); }); app.MapGet("/internal/orchestrator/watermarks", async Task ( [FromQuery] string? tenant, [FromServices] IWatermarkService service, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(tenant)) { return Results.BadRequest(new { error = "tenant required" }); } var state = await service.GetAsync(tenant.Trim(), cancellationToken); return Results.Ok(state); }); app.MapPost("/internal/orchestrator/watermarks", async Task ( [FromQuery] string? tenant, [FromQuery] string? watermark, [FromServices] IWatermarkService service, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(tenant)) { return Results.BadRequest(new { error = "tenant required" }); } var updated = await service.SetAsync(tenant.Trim(), watermark ?? string.Empty, cancellationToken); return Results.Ok(updated); }); app.Run(); public partial class Program;