up
Some checks failed
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
namespace StellaOps.SbomService.Options;
|
||||
|
||||
/// <summary>
|
||||
/// MongoDB configuration for SBOM Service storage-backed endpoints.
|
||||
/// </summary>
|
||||
public sealed class SbomMongoOptions
|
||||
{
|
||||
public string? ConnectionString { get; set; }
|
||||
|
||||
public string Database { get; set; } = "sbom_service";
|
||||
|
||||
public string CatalogCollection { get; set; } = "sbom_catalog";
|
||||
|
||||
public string ComponentLookupCollection { get; set; } = "sbom_component_neighbors";
|
||||
}
|
||||
@@ -2,9 +2,7 @@ using System.Globalization;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.SbomService.Models;
|
||||
using StellaOps.SbomService.Options;
|
||||
using StellaOps.SbomService.Services;
|
||||
using StellaOps.SbomService.Observability;
|
||||
using StellaOps.SbomService.Repositories;
|
||||
@@ -20,72 +18,38 @@ builder.Configuration
|
||||
builder.Services.AddOptions();
|
||||
builder.Services.AddLogging();
|
||||
|
||||
var mongoSection = builder.Configuration.GetSection("SbomService:Mongo");
|
||||
builder.Services.Configure<SbomMongoOptions>(mongoSection);
|
||||
var mongoConnectionString = mongoSection.GetValue<string>("ConnectionString");
|
||||
var mongoConfigured = !string.IsNullOrWhiteSpace(mongoConnectionString);
|
||||
|
||||
// Register SBOM query services (Mongo when configured; otherwise file-backed fixtures when present; fallback to in-memory seeds).
|
||||
if (mongoConfigured)
|
||||
// Register SBOM query services using file-backed fixtures when present; fallback to in-memory seeds.
|
||||
builder.Services.AddSingleton<IComponentLookupRepository>(sp =>
|
||||
{
|
||||
builder.Services.AddSingleton<IMongoClient>(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))
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SbomMongoOptions>>().Value;
|
||||
var url = new MongoUrl(options.ConnectionString!);
|
||||
var settings = MongoClientSettings.FromUrl(url);
|
||||
settings.ServerSelectionTimeout = TimeSpan.FromSeconds(5);
|
||||
settings.RetryWrites = false;
|
||||
return new MongoClient(settings);
|
||||
});
|
||||
return new FileComponentLookupRepository(configured!);
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<IMongoDatabase>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SbomMongoOptions>>().Value;
|
||||
var client = sp.GetRequiredService<IMongoClient>();
|
||||
var url = new MongoUrl(options.ConnectionString!);
|
||||
var databaseName = string.IsNullOrWhiteSpace(options.Database)
|
||||
? url.DatabaseName ?? "sbom_service"
|
||||
: options.Database;
|
||||
return client.GetDatabase(databaseName);
|
||||
});
|
||||
var candidate = FindFixture(env, "component_lookup.json");
|
||||
return candidate is not null
|
||||
? new FileComponentLookupRepository(candidate)
|
||||
: new InMemoryComponentLookupRepository();
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<IComponentLookupRepository, MongoComponentLookupRepository>();
|
||||
builder.Services.AddSingleton<ICatalogRepository, MongoCatalogRepository>();
|
||||
}
|
||||
else
|
||||
builder.Services.AddSingleton<ICatalogRepository>(sp =>
|
||||
{
|
||||
builder.Services.AddSingleton<IComponentLookupRepository>(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))
|
||||
{
|
||||
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!);
|
||||
}
|
||||
return new FileCatalogRepository(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();
|
||||
});
|
||||
}
|
||||
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>());
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.SbomService.Models;
|
||||
using StellaOps.SbomService.Options;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public sealed class MongoCatalogRepository : ICatalogRepository
|
||||
{
|
||||
private readonly IMongoCollection<CatalogDocument> _collection;
|
||||
private readonly Collation _caseInsensitive = new("en", strength: CollationStrength.Secondary);
|
||||
|
||||
public MongoCatalogRepository(IMongoDatabase database, IOptions<SbomMongoOptions> options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var opts = options.Value;
|
||||
var collectionName = string.IsNullOrWhiteSpace(opts.CatalogCollection)
|
||||
? "sbom_catalog"
|
||||
: opts.CatalogCollection;
|
||||
|
||||
_collection = database.GetCollection<CatalogDocument>(collectionName);
|
||||
EnsureIndexes();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CatalogRecord>> ListAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var items = await _collection
|
||||
.Find(FilterDefinition<CatalogDocument>.Empty)
|
||||
.SortByDescending(c => c.CreatedAt)
|
||||
.ThenBy(c => c.Artifact)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return items.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<CatalogRecord> Items, int Total)> QueryAsync(SbomCatalogQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = BuildFilter(query);
|
||||
|
||||
var total = (int)await _collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken);
|
||||
|
||||
var items = await _collection
|
||||
.Find(filter, new FindOptions { Collation = _caseInsensitive })
|
||||
.SortByDescending(c => c.CreatedAt)
|
||||
.ThenBy(c => c.Artifact)
|
||||
.Skip(query.Offset)
|
||||
.Limit(query.Limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (items.Select(Map).ToList(), total);
|
||||
}
|
||||
|
||||
private FilterDefinition<CatalogDocument> BuildFilter(SbomCatalogQuery query)
|
||||
{
|
||||
var filter = Builders<CatalogDocument>.Filter.Empty;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Artifact))
|
||||
{
|
||||
filter &= Builders<CatalogDocument>.Filter.Regex(c => c.Artifact, new BsonRegularExpression(query.Artifact, "i"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.License))
|
||||
{
|
||||
var escaped = Regex.Escape(query.License);
|
||||
filter &= Builders<CatalogDocument>.Filter.Regex(c => c.License, new BsonRegularExpression($"^{escaped}$", "i"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Scope))
|
||||
{
|
||||
filter &= Builders<CatalogDocument>.Filter.Eq(c => c.Scope, query.Scope);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.AssetTag))
|
||||
{
|
||||
filter &= Builders<CatalogDocument>.Filter.Exists($"AssetTags.{query.AssetTag}");
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private void EnsureIndexes()
|
||||
{
|
||||
var timestampIdx = Builders<CatalogDocument>.IndexKeys
|
||||
.Descending(c => c.CreatedAt)
|
||||
.Ascending(c => c.Artifact);
|
||||
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<CatalogDocument>(timestampIdx, new CreateIndexOptions
|
||||
{
|
||||
Name = "catalog_created_artifact"
|
||||
}));
|
||||
|
||||
var filterIdx = Builders<CatalogDocument>.IndexKeys
|
||||
.Ascending(c => c.Artifact)
|
||||
.Ascending(c => c.Scope)
|
||||
.Ascending(c => c.License);
|
||||
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<CatalogDocument>(filterIdx, new CreateIndexOptions
|
||||
{
|
||||
Name = "catalog_filters"
|
||||
}));
|
||||
|
||||
var assetTagIdx = Builders<CatalogDocument>.IndexKeys
|
||||
.Ascending("AssetTags");
|
||||
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<CatalogDocument>(assetTagIdx, new CreateIndexOptions
|
||||
{
|
||||
Name = "catalog_asset_tags"
|
||||
}));
|
||||
}
|
||||
|
||||
private static CatalogRecord Map(CatalogDocument doc) => new(
|
||||
doc.Artifact,
|
||||
doc.SbomVersion,
|
||||
doc.Digest,
|
||||
doc.License,
|
||||
doc.Scope,
|
||||
doc.AssetTags ?? new Dictionary<string, string>(StringComparer.Ordinal),
|
||||
doc.CreatedAt,
|
||||
doc.ProjectionHash,
|
||||
doc.EvaluationMetadata ?? string.Empty);
|
||||
|
||||
private sealed class CatalogDocument
|
||||
{
|
||||
public string Artifact { get; set; } = string.Empty;
|
||||
public string SbomVersion { get; set; } = string.Empty;
|
||||
public string Digest { get; set; } = string.Empty;
|
||||
public string? License { get; set; }
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
public Dictionary<string, string>? AssetTags { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public string ProjectionHash { get; set; } = string.Empty;
|
||||
public string? EvaluationMetadata { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.SbomService.Models;
|
||||
using StellaOps.SbomService.Options;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public sealed class MongoComponentLookupRepository : IComponentLookupRepository
|
||||
{
|
||||
private readonly IMongoCollection<ComponentDocument> _collection;
|
||||
private readonly Collation _caseInsensitive = new("en", strength: CollationStrength.Secondary);
|
||||
|
||||
public MongoComponentLookupRepository(IMongoDatabase database, IOptions<SbomMongoOptions> options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(database);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var opts = options.Value;
|
||||
var collectionName = string.IsNullOrWhiteSpace(opts.ComponentLookupCollection)
|
||||
? "sbom_component_neighbors"
|
||||
: opts.ComponentLookupCollection;
|
||||
|
||||
_collection = database.GetCollection<ComponentDocument>(collectionName);
|
||||
EnsureIndexes();
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<ComponentLookupRecord> Items, int Total)> QueryAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var filter = Builders<ComponentDocument>.Filter.Eq(c => c.Purl, query.Purl);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.Artifact))
|
||||
{
|
||||
filter &= Builders<ComponentDocument>.Filter.Eq(c => c.Artifact, query.Artifact);
|
||||
}
|
||||
|
||||
var total = (int)await _collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken);
|
||||
|
||||
var items = await _collection
|
||||
.Find(filter, new FindOptions { Collation = _caseInsensitive })
|
||||
.SortBy(c => c.Artifact)
|
||||
.ThenBy(c => c.NeighborPurl)
|
||||
.Skip(query.Offset)
|
||||
.Limit(query.Limit)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (items.Select(Map).ToList(), total);
|
||||
}
|
||||
|
||||
private void EnsureIndexes()
|
||||
{
|
||||
var purlArtifact = Builders<ComponentDocument>.IndexKeys
|
||||
.Ascending(c => c.Purl)
|
||||
.Ascending(c => c.Artifact);
|
||||
|
||||
_collection.Indexes.CreateOne(new CreateIndexModel<ComponentDocument>(purlArtifact, new CreateIndexOptions
|
||||
{
|
||||
Name = "component_lookup_purl_artifact"
|
||||
}));
|
||||
}
|
||||
|
||||
private static ComponentLookupRecord Map(ComponentDocument doc) => new(
|
||||
doc.Artifact,
|
||||
doc.Purl,
|
||||
doc.NeighborPurl,
|
||||
doc.Relationship,
|
||||
doc.License,
|
||||
doc.Scope,
|
||||
doc.RuntimeFlag);
|
||||
|
||||
private sealed class ComponentDocument
|
||||
{
|
||||
public string Artifact { get; set; } = string.Empty;
|
||||
public string Purl { get; set; } = string.Empty;
|
||||
public string NeighborPurl { get; set; } = string.Empty;
|
||||
public string Relationship { get; set; } = string.Empty;
|
||||
public string? License { get; set; }
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
public bool RuntimeFlag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,5 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user