Add tests and implement timeline ingestion options with NATS and Redis subscribers
- Introduced `BinaryReachabilityLifterTests` to validate binary lifting functionality. - Created `PackRunWorkerOptions` for configuring worker paths and execution persistence. - Added `TimelineIngestionOptions` for configuring NATS and Redis ingestion transports. - Implemented `NatsTimelineEventSubscriber` for subscribing to NATS events. - Developed `RedisTimelineEventSubscriber` for reading from Redis Streams. - Added `TimelineEnvelopeParser` to normalize incoming event envelopes. - Created unit tests for `TimelineEnvelopeParser` to ensure correct field mapping. - Implemented `TimelineAuthorizationAuditSink` for logging authorization outcomes.
This commit is contained in:
@@ -17,8 +17,38 @@ builder.Configuration
|
||||
builder.Services.AddOptions();
|
||||
builder.Services.AddLogging();
|
||||
|
||||
// Register SBOM query services (InMemory seed; replace with Mongo-backed repository later).
|
||||
builder.Services.AddSingleton<IComponentLookupRepository>(_ => new InMemoryComponentLookupRepository());
|
||||
// Register SBOM query services (file-backed fixtures when present; fallback to in-memory seeds).
|
||||
builder.Services.AddSingleton<IComponentLookupRepository>(sp =>
|
||||
{
|
||||
var config = sp.GetRequiredService<IConfiguration>();
|
||||
var env = sp.GetRequiredService<IHostEnvironment>();
|
||||
var configured = config.GetValue<string>("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<ICatalogRepository>(sp =>
|
||||
{
|
||||
var config = sp.GetRequiredService<IConfiguration>();
|
||||
var env = sp.GetRequiredService<IHostEnvironment>();
|
||||
var configured = config.GetValue<string>("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<IClock, SystemClock>();
|
||||
builder.Services.AddSingleton<ISbomEventStore, InMemorySbomEventStore>();
|
||||
builder.Services.AddSingleton<ISbomEventPublisher>(sp => sp.GetRequiredService<ISbomEventStore>());
|
||||
@@ -63,6 +93,28 @@ builder.Services.AddSingleton<IProjectionRepository>(sp =>
|
||||
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;
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public sealed class FileCatalogRepository : ICatalogRepository
|
||||
{
|
||||
private readonly IReadOnlyList<CatalogRecord> _items;
|
||||
|
||||
public FileCatalogRepository(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_items = Array.Empty<CatalogRecord>();
|
||||
return;
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(path);
|
||||
var items = JsonSerializer.Deserialize<List<CatalogRecord>>(stream, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
_items = items ?? Array.Empty<CatalogRecord>();
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<CatalogRecord>> ListAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(_items);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Text.Json;
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public sealed class FileComponentLookupRepository : IComponentLookupRepository
|
||||
{
|
||||
private readonly IReadOnlyList<ComponentLookupRecord> _items;
|
||||
|
||||
public FileComponentLookupRepository(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
_items = Array.Empty<ComponentLookupRecord>();
|
||||
return;
|
||||
}
|
||||
|
||||
using var stream = File.OpenRead(path);
|
||||
var items = JsonSerializer.Deserialize<List<ComponentLookupRecord>>(stream, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
_items = items ?? Array.Empty<ComponentLookupRecord>();
|
||||
}
|
||||
|
||||
public Task<(IReadOnlyList<ComponentLookupRecord> Items, int Total)> QueryAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var filtered = _items
|
||||
.Where(c => c.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(c => query.Artifact is null || c.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(c => c.Artifact)
|
||||
.ThenBy(c => c.Purl)
|
||||
.ToList();
|
||||
|
||||
var page = filtered
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<(IReadOnlyList<ComponentLookupRecord> Items, int Total)>((page, filtered.Count));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public interface ICatalogRepository
|
||||
{
|
||||
Task<IReadOnlyList<CatalogRecord>> ListAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public sealed class InMemoryCatalogRepository : ICatalogRepository
|
||||
{
|
||||
private static readonly IReadOnlyList<CatalogRecord> Seed = new List<CatalogRecord>
|
||||
{
|
||||
new(
|
||||
Artifact: "ghcr.io/stellaops/sample-api",
|
||||
SbomVersion: "2025.11.16.1",
|
||||
Digest: "sha256:112",
|
||||
License: "MIT",
|
||||
Scope: "runtime",
|
||||
AssetTags: new Dictionary<string, string>
|
||||
{
|
||||
["owner"] = "payments",
|
||||
["criticality"] = "high",
|
||||
["env"] = "prod"
|
||||
},
|
||||
CreatedAt: new DateTimeOffset(2025, 11, 16, 12, 0, 0, TimeSpan.Zero),
|
||||
ProjectionHash: "sha256:proj112",
|
||||
EvaluationMetadata: "eval:passed:v1"),
|
||||
new(
|
||||
Artifact: "ghcr.io/stellaops/sample-api",
|
||||
SbomVersion: "2025.11.15.1",
|
||||
Digest: "sha256:111",
|
||||
License: "MIT",
|
||||
Scope: "runtime",
|
||||
AssetTags: new Dictionary<string, string>
|
||||
{
|
||||
["owner"] = "payments",
|
||||
["criticality"] = "high",
|
||||
["env"] = "prod"
|
||||
},
|
||||
CreatedAt: new DateTimeOffset(2025, 11, 15, 12, 0, 0, TimeSpan.Zero),
|
||||
ProjectionHash: "sha256:proj111",
|
||||
EvaluationMetadata: "eval:passed:v1"),
|
||||
new(
|
||||
Artifact: "ghcr.io/stellaops/sample-worker",
|
||||
SbomVersion: "2025.11.12.0",
|
||||
Digest: "sha256:222",
|
||||
License: "Apache-2.0",
|
||||
Scope: "runtime",
|
||||
AssetTags: new Dictionary<string, string>
|
||||
{
|
||||
["owner"] = "platform",
|
||||
["criticality"] = "medium",
|
||||
["env"] = "staging"
|
||||
},
|
||||
CreatedAt: new DateTimeOffset(2025, 11, 12, 8, 0, 0, TimeSpan.Zero),
|
||||
ProjectionHash: "sha256:proj222",
|
||||
EvaluationMetadata: "eval:pending:v1"),
|
||||
};
|
||||
|
||||
public Task<IReadOnlyList<CatalogRecord>> ListAsync(CancellationToken cancellationToken)
|
||||
=> Task.FromResult(Seed);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ internal sealed class InMemorySbomQueryService : ISbomQueryService
|
||||
public InMemorySbomQueryService(
|
||||
IComponentLookupRepository componentLookupRepository,
|
||||
IProjectionRepository projectionRepository,
|
||||
ICatalogRepository catalogRepository,
|
||||
ISbomEventPublisher eventPublisher,
|
||||
IClock clock)
|
||||
{
|
||||
@@ -31,7 +32,11 @@ internal sealed class InMemorySbomQueryService : ISbomQueryService
|
||||
// Deterministic seed data for early contract testing; replace with Mongo-backed implementation later.
|
||||
_paths = SeedPaths();
|
||||
_timelines = SeedTimelines();
|
||||
_catalog = SeedCatalog();
|
||||
_catalog = catalogRepository.ListAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (_catalog.Count == 0)
|
||||
{
|
||||
_catalog = SeedCatalog();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<QueryResult<SbomPathResult>> GetPathsAsync(SbomPathQuery query, CancellationToken cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user