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

This commit is contained in:
StellaOps Bot
2025-12-12 09:35:37 +02:00
parent ce5ec9c158
commit efaf3cb789
238 changed files with 146274 additions and 5767 deletions

View File

@@ -1,156 +0,0 @@
using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Mongo2Go;
using MongoDB.Bson;
using MongoDB.Driver;
using StellaOps.SbomService.Models;
using Xunit;
namespace StellaOps.SbomService.Tests;
public class SbomMongoStorageTests : IAsyncLifetime
{
private readonly WebApplicationFactory<Program> _factory;
private MongoDbRunner? _runner;
public SbomMongoStorageTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Console_catalog_reads_from_mongo_storage()
{
using var client = CreateClient();
var response = await client.GetAsync("/console/sboms?artifact=mongo-api&limit=1");
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<SbomCatalogResult>();
payload.Should().NotBeNull();
payload!.Items.Should().ContainSingle();
payload.Items[0].Artifact.Should().Be("ghcr.io/stellaops/mongo-api");
payload.Items[0].ProjectionHash.Should().Be("sha256:proj-mongo-2");
payload.NextCursor.Should().Be("1");
}
[Fact]
public async Task Component_lookup_returns_storage_results_and_cursor()
{
using var client = CreateClient();
var response = await client.GetAsync("/components/lookup?purl=pkg:npm/mongo-lib@1.0.0&limit=1");
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<ComponentLookupResult>();
payload.Should().NotBeNull();
payload!.CacheHint.Should().Be("storage");
payload.Neighbors.Should().ContainSingle();
payload.Neighbors[0].Purl.Should().Be("pkg:npm/express@4.18.2");
payload.NextCursor.Should().Be("1");
}
public Task InitializeAsync()
{
_runner = MongoDbRunner.Start(singleNodeReplSet: false, additionalMongodArguments: "--quiet");
return SeedMongoAsync();
}
public Task DisposeAsync()
{
_runner?.Dispose();
return Task.CompletedTask;
}
private HttpClient CreateClient()
{
if (_runner is null)
{
throw new InvalidOperationException("Mongo runner not started");
}
var factory = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, config) =>
{
var settings = new Dictionary<string, string?>
{
["SbomService:Mongo:ConnectionString"] = _runner.ConnectionString,
["SbomService:Mongo:Database"] = "sbom_console_tests"
};
config.AddInMemoryCollection(settings);
});
});
return factory.CreateClient();
}
private async Task SeedMongoAsync()
{
if (_runner is null)
{
return;
}
var client = new MongoClient(_runner.ConnectionString);
var database = client.GetDatabase("sbom_console_tests");
var catalog = database.GetCollection<BsonDocument>("sbom_catalog");
await catalog.DeleteManyAsync(FilterDefinition<BsonDocument>.Empty);
await catalog.InsertManyAsync(new[]
{
new BsonDocument
{
{ "artifact", "ghcr.io/stellaops/mongo-api" },
{ "sbomVersion", "2025.12.04.2" },
{ "digest", "sha256:bbb" },
{ "license", "Apache-2.0" },
{ "scope", "runtime" },
{ "assetTags", new BsonDocument { { "owner", "storage" }, { "env", "prod" } } },
{ "createdAt", new BsonDateTime(DateTime.SpecifyKind(new DateTime(2025, 12, 4, 12, 0, 0), DateTimeKind.Utc)) },
{ "projectionHash", "sha256:proj-mongo-2" },
{ "evaluationMetadata", "eval:storage" }
},
new BsonDocument
{
{ "artifact", "ghcr.io/stellaops/mongo-api" },
{ "sbomVersion", "2025.12.04.1" },
{ "digest", "sha256:aaa" },
{ "license", "Apache-2.0" },
{ "scope", "runtime" },
{ "assetTags", new BsonDocument { { "owner", "storage" }, { "env", "prod" } } },
{ "createdAt", new BsonDateTime(DateTime.SpecifyKind(new DateTime(2025, 12, 4, 11, 0, 0), DateTimeKind.Utc)) },
{ "projectionHash", "sha256:proj-mongo-1" },
{ "evaluationMetadata", "eval:storage" }
}
});
var components = database.GetCollection<BsonDocument>("sbom_component_neighbors");
await components.DeleteManyAsync(FilterDefinition<BsonDocument>.Empty);
await components.InsertManyAsync(new[]
{
new BsonDocument
{
{ "artifact", "ghcr.io/stellaops/mongo-api" },
{ "purl", "pkg:npm/mongo-lib@1.0.0" },
{ "neighborPurl", "pkg:npm/express@4.18.2" },
{ "relationship", "DEPENDS_ON" },
{ "license", "MIT" },
{ "scope", "runtime" },
{ "runtimeFlag", true }
},
new BsonDocument
{
{ "artifact", "ghcr.io/stellaops/mongo-api" },
{ "purl", "pkg:npm/mongo-lib@1.0.0" },
{ "neighborPurl", "pkg:npm/body-parser@1.20.2" },
{ "relationship", "DEPENDS_ON" },
{ "license", "MIT" },
{ "scope", "runtime" },
{ "runtimeFlag", true }
}
});
}
}

View File

@@ -31,8 +31,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normali
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{F921862B-2057-4E57-9765-2C34764BC226}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance.Mongo", "..\__Libraries\StellaOps.Provenance.Mongo\StellaOps.Provenance.Mongo.csproj", "{055EDD0B-F513-40C8-BAC0-80815BCE45E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{872BE10D-03C8-4F6A-9D4C-F56FFDCC6B16}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "..\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{DA1297B3-5B0A-4B4F-A213-9D0E633233EE}"
@@ -215,18 +213,6 @@ Global
{F921862B-2057-4E57-9765-2C34764BC226}.Release|x64.Build.0 = Release|Any CPU
{F921862B-2057-4E57-9765-2C34764BC226}.Release|x86.ActiveCfg = Release|Any CPU
{F921862B-2057-4E57-9765-2C34764BC226}.Release|x86.Build.0 = Release|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Debug|x64.ActiveCfg = Debug|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Debug|x64.Build.0 = Debug|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Debug|x86.ActiveCfg = Debug|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Debug|x86.Build.0 = Debug|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Release|Any CPU.Build.0 = Release|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Release|x64.ActiveCfg = Release|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Release|x64.Build.0 = Release|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Release|x86.ActiveCfg = Release|Any CPU
{055EDD0B-F513-40C8-BAC0-80815BCE45E3}.Release|x86.Build.0 = Release|Any CPU
{872BE10D-03C8-4F6A-9D4C-F56FFDCC6B16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{872BE10D-03C8-4F6A-9D4C-F56FFDCC6B16}.Debug|Any CPU.Build.0 = Debug|Any CPU
{872BE10D-03C8-4F6A-9D4C-F56FFDCC6B16}.Debug|x64.ActiveCfg = Debug|Any CPU

View File

@@ -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";
}

View File

@@ -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>());

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -15,6 +15,5 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="3.5.0" />
</ItemGroup>
</Project>