This commit is contained in:
StellaOps Bot
2025-11-23 23:40:10 +02:00
parent c13355923f
commit 029002ad05
93 changed files with 2160 additions and 285 deletions

View File

@@ -0,0 +1,10 @@
using System;
namespace StellaOps.SbomService.Models;
public sealed record SbomVersionCreatedEvent(
string SnapshotId,
string TenantId,
string ProjectionHash,
string SchemaVersion,
DateTimeOffset CreatedAtUtc);

View File

@@ -18,23 +18,10 @@ builder.Services.AddOptions();
builder.Services.AddLogging();
// Register SBOM query services (InMemory seed; replace with Mongo-backed repository later).
builder.Services.AddSingleton<IComponentLookupRepository>(sp =>
{
try
{
var config = sp.GetRequiredService<IConfiguration>();
var mongoConn = config.GetConnectionString("SbomServiceMongo") ?? "mongodb://localhost:27017";
var mongoClient = new MongoDB.Driver.MongoClient(mongoConn);
var databaseName = config.GetSection("SbomService")?["Database"] ?? "sbomservice";
var database = mongoClient.GetDatabase(databaseName);
return new MongoComponentLookupRepository(database);
}
catch
{
// Fallback for test/offline environments when Mongo driver is unavailable.
return new InMemoryComponentLookupRepository();
}
});
builder.Services.AddSingleton<IComponentLookupRepository>(_ => new InMemoryComponentLookupRepository());
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddSingleton<ISbomEventStore, InMemorySbomEventStore>();
builder.Services.AddSingleton<ISbomEventPublisher>(sp => sp.GetRequiredService<ISbomEventStore>());
builder.Services.AddSingleton<ISbomQueryService, InMemorySbomQueryService>();
builder.Services.AddSingleton<IProjectionRepository>(sp =>
@@ -279,6 +266,39 @@ app.MapGet("/sboms/{snapshotId}/projection", async Task<IResult> (
});
});
app.MapGet("/internal/sbom/events", async Task<IResult> (
[FromServices] ISbomEventStore store,
CancellationToken cancellationToken) =>
{
var events = await store.ListAsync(cancellationToken);
return Results.Ok(events);
});
app.MapPost("/internal/sbom/events/backfill", async Task<IResult> (
[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++;
}
}
return Results.Ok(new { published });
});
app.Run();
public partial class Program;

View File

@@ -57,6 +57,12 @@ internal sealed class FileProjectionRepository : IProjectionRepository
return Task.FromResult(result);
}
public Task<IReadOnlyList<SbomProjectionResult>> ListAsync(CancellationToken cancellationToken)
{
var list = _projections.Values.ToList();
return Task.FromResult<IReadOnlyList<SbomProjectionResult>>(list);
}
private static string ComputeHash(JsonElement element)
{
var json = JsonSerializer.Serialize(element, new JsonSerializerOptions { WriteIndented = false });

View File

@@ -5,4 +5,5 @@ namespace StellaOps.SbomService.Repositories;
public interface IProjectionRepository
{
Task<SbomProjectionResult?> GetAsync(string snapshotId, string tenantId, CancellationToken cancellationToken);
Task<IReadOnlyList<SbomProjectionResult>> ListAsync(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,13 @@
using System;
namespace StellaOps.SbomService.Services;
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
public sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}

View File

@@ -1,7 +1,8 @@
using System.Collections.Concurrent;
using System.Globalization;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Repositories;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Repositories;
using StellaOps.SbomService.Services;
namespace StellaOps.SbomService.Services;
@@ -12,12 +13,20 @@ internal sealed class InMemorySbomQueryService : ISbomQueryService
private readonly IReadOnlyList<CatalogRecord> _catalog;
private readonly IComponentLookupRepository _componentLookupRepository;
private readonly IProjectionRepository _projectionRepository;
private readonly ISbomEventPublisher _eventPublisher;
private readonly IClock _clock;
private readonly ConcurrentDictionary<string, object> _cache = new();
public InMemorySbomQueryService(IComponentLookupRepository componentLookupRepository, IProjectionRepository projectionRepository)
public InMemorySbomQueryService(
IComponentLookupRepository componentLookupRepository,
IProjectionRepository projectionRepository,
ISbomEventPublisher eventPublisher,
IClock clock)
{
_componentLookupRepository = componentLookupRepository;
_projectionRepository = projectionRepository;
_eventPublisher = eventPublisher;
_clock = clock;
// Deterministic seed data for early contract testing; replace with Mongo-backed implementation later.
_paths = SeedPaths();
_timelines = SeedTimelines();
@@ -170,6 +179,13 @@ internal sealed class InMemorySbomQueryService : ISbomQueryService
if (projection is not null)
{
_cache[cacheKey] = projection;
var evt = new SbomVersionCreatedEvent(
projection.SnapshotId,
projection.TenantId,
projection.ProjectionHash,
projection.SchemaVersion,
_clock.UtcNow);
await _eventPublisher.PublishVersionCreatedAsync(evt, cancellationToken);
}
return projection;

View File

@@ -0,0 +1,37 @@
using System.Collections.Concurrent;
using StellaOps.SbomService.Models;
namespace StellaOps.SbomService.Services;
public interface ISbomEventPublisher
{
/// <summary>
/// Publishes a version-created event. Returns true when the event was newly recorded; false when it was already present.
/// </summary>
Task<bool> PublishVersionCreatedAsync(SbomVersionCreatedEvent evt, CancellationToken cancellationToken);
}
public interface ISbomEventStore : ISbomEventPublisher
{
Task<IReadOnlyList<SbomVersionCreatedEvent>> ListAsync(CancellationToken cancellationToken);
}
public sealed class InMemorySbomEventStore : ISbomEventStore
{
private readonly ConcurrentDictionary<string, SbomVersionCreatedEvent> _events = new();
public Task<IReadOnlyList<SbomVersionCreatedEvent>> ListAsync(CancellationToken cancellationToken)
{
var list = _events.Values.OrderBy(e => e.SnapshotId, StringComparer.Ordinal)
.ThenBy(e => e.TenantId, StringComparer.Ordinal)
.ToList();
return Task.FromResult<IReadOnlyList<SbomVersionCreatedEvent>>(list);
}
public Task<bool> PublishVersionCreatedAsync(SbomVersionCreatedEvent evt, CancellationToken cancellationToken)
{
var key = $"{evt.SnapshotId}|{evt.TenantId}|{evt.ProjectionHash}";
var added = _events.TryAdd(key, evt);
return Task.FromResult(added);
}
}