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:
@@ -139,6 +139,33 @@ public class ReachabilityUnionWriterTests
|
||||
Assert.Contains("\"score\":0.8", edgeLines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WritesSymbolMetadataAndCodeBlockHash()
|
||||
{
|
||||
var writer = new ReachabilityUnionWriter();
|
||||
using var temp = new TempDir();
|
||||
|
||||
var graph = new ReachabilityUnionGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new ReachabilityUnionNode(
|
||||
"sym:binary:foo",
|
||||
"binary",
|
||||
"function",
|
||||
"ssl3_read_bytes",
|
||||
CodeBlockHash: "sha256:deadbeef",
|
||||
Symbol: new ReachabilitySymbol("_Z15ssl3_read_bytes", "ssl3_read_bytes", "DWARF", 0.98))
|
||||
},
|
||||
Edges: Array.Empty<ReachabilityUnionEdge>());
|
||||
|
||||
var result = await writer.WriteAsync(graph, temp.Path, "analysis-symbol");
|
||||
|
||||
var nodeLines = await File.ReadAllLinesAsync(result.Nodes.Path);
|
||||
Assert.Single(nodeLines);
|
||||
Assert.Contains("\"code_block_hash\":\"sha256:deadbeef\"", nodeLines[0]);
|
||||
Assert.Contains("\"symbol\":{\"mangled\":\"_Z15ssl3_read_bytes\",\"demangled\":\"ssl3_read_bytes\",\"source\":\"DWARF\",\"confidence\":0.98}", nodeLines[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OmitsPurlAndSymbolDigestWhenNull()
|
||||
{
|
||||
|
||||
@@ -36,4 +36,31 @@ public class RichGraphWriterTests
|
||||
Assert.Equal(2, result.NodeCount);
|
||||
Assert.Equal(1, result.EdgeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CarriesSymbolMetadataToRichGraph()
|
||||
{
|
||||
var writer = new RichGraphWriter(CryptoHashFactory.CreateDefault());
|
||||
using var temp = new TempDir();
|
||||
|
||||
var union = new ReachabilityUnionGraph(
|
||||
Nodes: new[]
|
||||
{
|
||||
new ReachabilityUnionNode(
|
||||
"sym:binary:target",
|
||||
"binary",
|
||||
"function",
|
||||
"ssl_read",
|
||||
CodeBlockHash: "sha256:blockhash",
|
||||
Symbol: new ReachabilitySymbol("_Zssl_read", "ssl_read", "DWARF", 0.9))
|
||||
},
|
||||
Edges: Array.Empty<ReachabilityUnionEdge>());
|
||||
|
||||
var rich = RichGraphBuilder.FromUnion(union, "test-analyzer", "1.0.0");
|
||||
var result = await writer.WriteAsync(rich, temp.Path, "analysis-symbol-rich");
|
||||
|
||||
var json = await File.ReadAllTextAsync(result.GraphPath);
|
||||
Assert.Contains("\"code_block_hash\":\"sha256:blockhash\"", json);
|
||||
Assert.Contains("\"symbol\":{\"mangled\":\"_Zssl_read\",\"demangled\":\"ssl_read\",\"source\":\"DWARF\",\"confidence\":0.9}", json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Scanner.EntryTrace;
|
||||
using StellaOps.Scanner.EntryTrace.Serialization;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Mongo;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using Xunit;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
public sealed class EntryTraceResultStoreTests : IClassFixture<ScannerMongoFixture>
|
||||
[Collection("scanner-postgres")]
|
||||
public sealed class EntryTraceResultStoreTests
|
||||
{
|
||||
private readonly ScannerMongoFixture _fixture;
|
||||
private readonly ScannerPostgresFixture _fixture;
|
||||
|
||||
public EntryTraceResultStoreTests(ScannerMongoFixture fixture)
|
||||
public EntryTraceResultStoreTests(ScannerPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
@@ -34,7 +35,7 @@ public sealed class EntryTraceResultStoreTests : IClassFixture<ScannerMongoFixtu
|
||||
[Fact]
|
||||
public async Task GetAsync_ReturnsNullWhenMissing()
|
||||
{
|
||||
await ClearCollectionAsync();
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
var store = CreateStore();
|
||||
|
||||
var result = await store.GetAsync("scan-missing", CancellationToken.None);
|
||||
@@ -45,7 +46,7 @@ public sealed class EntryTraceResultStoreTests : IClassFixture<ScannerMongoFixtu
|
||||
[Fact]
|
||||
public async Task StoreAsync_RoundTripsResult()
|
||||
{
|
||||
await ClearCollectionAsync();
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
var store = CreateStore();
|
||||
|
||||
var scanId = $"scan-{Guid.NewGuid():n}";
|
||||
@@ -104,36 +105,21 @@ public sealed class EntryTraceResultStoreTests : IClassFixture<ScannerMongoFixtu
|
||||
Assert.Equal(result.ScanId, stored!.ScanId);
|
||||
Assert.Equal(result.ImageDigest, stored.ImageDigest);
|
||||
Assert.Equal(result.GeneratedAtUtc, stored.GeneratedAtUtc);
|
||||
Assert.Equal(result.Graph, stored.Graph);
|
||||
Assert.Equal(result.Ndjson, stored.Ndjson);
|
||||
}
|
||||
|
||||
private async Task ClearCollectionAsync()
|
||||
{
|
||||
var provider = CreateProvider();
|
||||
await provider.EntryTrace.DeleteManyAsync(_ => true);
|
||||
Assert.Equal(
|
||||
EntryTraceGraphSerializer.Serialize(result.Graph),
|
||||
EntryTraceGraphSerializer.Serialize(stored.Graph));
|
||||
Assert.Equal(result.Ndjson.ToArray(), stored.Ndjson.ToArray());
|
||||
}
|
||||
|
||||
private EntryTraceResultStore CreateStore()
|
||||
{
|
||||
var provider = CreateProvider();
|
||||
var repository = new EntryTraceRepository(provider);
|
||||
var options = new ScannerStorageOptions
|
||||
{
|
||||
Postgres = _fixture.Fixture.CreateOptions()
|
||||
};
|
||||
|
||||
var dataSource = new ScannerDataSource(Options.Create(options), NullLogger<ScannerDataSource>.Instance);
|
||||
var repository = new EntryTraceRepository(dataSource, NullLogger<EntryTraceRepository>.Instance);
|
||||
return new EntryTraceResultStore(repository);
|
||||
}
|
||||
|
||||
private MongoCollectionProvider CreateProvider()
|
||||
{
|
||||
var options = Options.Create(new ScannerStorageOptions
|
||||
{
|
||||
Mongo = new MongoOptions
|
||||
{
|
||||
ConnectionString = _fixture.Runner.ConnectionString,
|
||||
DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName,
|
||||
UseMajorityReadConcern = false,
|
||||
UseMajorityWriteConcern = false
|
||||
}
|
||||
});
|
||||
|
||||
return new MongoCollectionProvider(_fixture.Database, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Scanner.Core.Contracts;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Mongo;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
public sealed class RubyPackageInventoryStoreTests : IClassFixture<ScannerMongoFixture>
|
||||
[Collection("scanner-postgres")]
|
||||
public sealed class RubyPackageInventoryStoreTests
|
||||
{
|
||||
private readonly ScannerMongoFixture _fixture;
|
||||
private readonly ScannerPostgresFixture _fixture;
|
||||
|
||||
public RubyPackageInventoryStoreTests(ScannerMongoFixture fixture)
|
||||
public RubyPackageInventoryStoreTests(ScannerPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public sealed class RubyPackageInventoryStoreTests : IClassFixture<ScannerMongoF
|
||||
[Fact]
|
||||
public async Task GetAsync_ReturnsNullWhenMissing()
|
||||
{
|
||||
await ClearCollectionAsync();
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
var store = CreateStore();
|
||||
|
||||
var inventory = await store.GetAsync("scan-missing", CancellationToken.None);
|
||||
@@ -45,7 +45,7 @@ public sealed class RubyPackageInventoryStoreTests : IClassFixture<ScannerMongoF
|
||||
[Fact]
|
||||
public async Task StoreAsync_RoundTripsInventory()
|
||||
{
|
||||
await ClearCollectionAsync();
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
var store = CreateStore();
|
||||
|
||||
var scanId = $"scan-{Guid.NewGuid():n}";
|
||||
@@ -90,32 +90,15 @@ public sealed class RubyPackageInventoryStoreTests : IClassFixture<ScannerMongoF
|
||||
Assert.Equal("rubygems", stored.Packages[0].Source);
|
||||
}
|
||||
|
||||
private async Task ClearCollectionAsync()
|
||||
{
|
||||
var provider = CreateProvider();
|
||||
await provider.RubyPackages.DeleteManyAsync(Builders<RubyPackageInventoryDocument>.Filter.Empty);
|
||||
}
|
||||
|
||||
private RubyPackageInventoryStore CreateStore()
|
||||
{
|
||||
var provider = CreateProvider();
|
||||
var repository = new RubyPackageInventoryRepository(provider);
|
||||
var options = new ScannerStorageOptions
|
||||
{
|
||||
Postgres = _fixture.Fixture.CreateOptions()
|
||||
};
|
||||
|
||||
var dataSource = new ScannerDataSource(Options.Create(options), NullLogger<ScannerDataSource>.Instance);
|
||||
var repository = new RubyPackageInventoryRepository(dataSource, NullLogger<RubyPackageInventoryRepository>.Instance);
|
||||
return new RubyPackageInventoryStore(repository);
|
||||
}
|
||||
|
||||
private MongoCollectionProvider CreateProvider()
|
||||
{
|
||||
var options = Options.Create(new ScannerStorageOptions
|
||||
{
|
||||
Mongo = new MongoOptions
|
||||
{
|
||||
ConnectionString = _fixture.Runner.ConnectionString,
|
||||
DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName,
|
||||
UseMajorityReadConcern = false,
|
||||
UseMajorityWriteConcern = false
|
||||
}
|
||||
});
|
||||
|
||||
return new MongoCollectionProvider(_fixture.Database, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using Mongo2Go;
|
||||
using MongoDB.Driver;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
public sealed class ScannerMongoFixture : IAsyncLifetime
|
||||
{
|
||||
public MongoDbRunner Runner { get; private set; } = null!;
|
||||
public IMongoClient Client { get; private set; } = null!;
|
||||
public IMongoDatabase Database { get; private set; } = null!;
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
Runner = MongoDbRunner.Start(singleNodeReplSet: true);
|
||||
Client = new MongoClient(Runner.ConnectionString);
|
||||
Database = Client.GetDatabase($"scanner-tests-{Guid.NewGuid():N}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
Runner.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Reflection;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
public sealed class ScannerPostgresFixture : PostgresIntegrationFixture, ICollectionFixture<ScannerPostgresFixture>
|
||||
{
|
||||
protected override Assembly? GetMigrationAssembly() => typeof(ScannerStorageOptions).Assembly;
|
||||
|
||||
protected override string GetModuleName() => "Scanner.Storage";
|
||||
}
|
||||
|
||||
[CollectionDefinition("scanner-postgres")]
|
||||
public sealed class ScannerPostgresCollection : ICollectionFixture<ScannerPostgresFixture>
|
||||
{
|
||||
}
|
||||
@@ -7,5 +7,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Scanner.Storage/StellaOps.Scanner.Storage.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Infrastructure.Postgres.Testing\\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using StellaOps.Scanner.Storage.Migrations;
|
||||
using StellaOps.Scanner.Storage.Mongo;
|
||||
using StellaOps.Scanner.Storage.ObjectStore;
|
||||
using StellaOps.Scanner.Storage.Postgres;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.Storage.Services;
|
||||
using Xunit;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
|
||||
namespace StellaOps.Scanner.Storage.Tests;
|
||||
|
||||
[CollectionDefinition("scanner-mongo-fixture")]
|
||||
public sealed class ScannerMongoCollection : ICollectionFixture<ScannerMongoFixture>
|
||||
{
|
||||
}
|
||||
|
||||
[Collection("scanner-mongo-fixture")]
|
||||
[Collection("scanner-postgres")]
|
||||
public sealed class StorageDualWriteFixture
|
||||
{
|
||||
private readonly ScannerMongoFixture _fixture;
|
||||
private readonly ScannerPostgresFixture _fixture;
|
||||
|
||||
public StorageDualWriteFixture(ScannerMongoFixture fixture)
|
||||
public StorageDualWriteFixture(ScannerPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
@@ -33,14 +25,16 @@ public sealed class StorageDualWriteFixture
|
||||
[Fact]
|
||||
public async Task StoreArtifactAsync_DualWrite_WritesToMirrorAndCatalog()
|
||||
{
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var options = BuildOptions(dualWrite: true, mirrorBucket: "mirror-bucket");
|
||||
var objectStore = new InMemoryArtifactObjectStore();
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero));
|
||||
|
||||
await InitializeMongoAsync(options);
|
||||
var provider = new MongoCollectionProvider(_fixture.Database, Options.Create(options));
|
||||
var artifactRepository = new ArtifactRepository(provider, fakeTime);
|
||||
var lifecycleRepository = new LifecycleRuleRepository(provider, fakeTime);
|
||||
var dataSource = new ScannerDataSource(Options.Create(options), NullLogger<ScannerDataSource>.Instance);
|
||||
await using var _ = dataSource;
|
||||
var artifactRepository = new ArtifactRepository(dataSource, NullLogger<ArtifactRepository>.Instance, fakeTime);
|
||||
var lifecycleRepository = new LifecycleRuleRepository(dataSource, NullLogger<LifecycleRuleRepository>.Instance, fakeTime);
|
||||
var service = new ArtifactStorageService(
|
||||
artifactRepository,
|
||||
lifecycleRepository,
|
||||
@@ -78,44 +72,35 @@ public sealed class StorageDualWriteFixture
|
||||
Assert.Equal(expectedTimestamp, artifact.CreatedAtUtc);
|
||||
Assert.Equal(expectedTimestamp, artifact.UpdatedAtUtc);
|
||||
|
||||
var lifecycleCollection = _fixture.Database.GetCollection<LifecycleRuleDocument>(ScannerStorageDefaults.Collections.LifecycleRules);
|
||||
var lifecycle = await lifecycleCollection.Find(x => x.ArtifactId == document.Id).FirstOrDefaultAsync();
|
||||
Assert.NotNull(lifecycle);
|
||||
Assert.Equal("compliance", lifecycle!.Class);
|
||||
Assert.True(lifecycle.ExpiresAtUtc.HasValue);
|
||||
Assert.True(lifecycle.ExpiresAtUtc.Value <= expiresAt.AddSeconds(5));
|
||||
Assert.Equal(expectedTimestamp, lifecycle.CreatedAtUtc);
|
||||
var lifecycle = await lifecycleRepository.ListExpiredAsync(DateTime.MaxValue, CancellationToken.None);
|
||||
var lifecycleEntry = lifecycle.SingleOrDefault(x => x.ArtifactId == document.Id);
|
||||
Assert.NotNull(lifecycleEntry);
|
||||
Assert.Equal("compliance", lifecycleEntry!.Class);
|
||||
Assert.True(lifecycleEntry.ExpiresAtUtc.HasValue);
|
||||
Assert.True(lifecycleEntry.ExpiresAtUtc.Value <= expiresAt.AddSeconds(5));
|
||||
Assert.Equal(expectedTimestamp, lifecycleEntry.CreatedAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Bootstrapper_CreatesLifecycleTtlIndex()
|
||||
public async Task Migrations_ApplySuccessfully()
|
||||
{
|
||||
var options = BuildOptions(dualWrite: false, mirrorBucket: null);
|
||||
await InitializeMongoAsync(options);
|
||||
await _fixture.TruncateAllTablesAsync();
|
||||
|
||||
var collection = _fixture.Database.GetCollection<BsonDocument>(ScannerStorageDefaults.Collections.LifecycleRules);
|
||||
var cursor = await collection.Indexes.ListAsync();
|
||||
var indexes = await cursor.ToListAsync();
|
||||
var ttlIndex = indexes.SingleOrDefault(x => string.Equals(x["name"].AsString, "lifecycle_expiresAt", StringComparison.Ordinal));
|
||||
|
||||
Assert.NotNull(ttlIndex);
|
||||
Assert.True(ttlIndex!.TryGetValue("expireAfterSeconds", out var expireValue));
|
||||
Assert.Equal(0, expireValue.ToInt64());
|
||||
|
||||
var uniqueIndex = indexes.SingleOrDefault(x => string.Equals(x["name"].AsString, "lifecycle_artifact_class", StringComparison.Ordinal));
|
||||
Assert.NotNull(uniqueIndex);
|
||||
Assert.True(uniqueIndex!["unique"].AsBoolean);
|
||||
await using var connection = new Npgsql.NpgsqlConnection(_fixture.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = new Npgsql.NpgsqlCommand(
|
||||
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = @schema AND table_name = 'artifacts');",
|
||||
connection);
|
||||
command.Parameters.AddWithValue("schema", _fixture.SchemaName);
|
||||
var exists = (bool)(await command.ExecuteScalarAsync() ?? false);
|
||||
Assert.True(exists);
|
||||
}
|
||||
|
||||
private ScannerStorageOptions BuildOptions(bool dualWrite, string? mirrorBucket)
|
||||
{
|
||||
var options = new ScannerStorageOptions
|
||||
{
|
||||
Mongo =
|
||||
{
|
||||
ConnectionString = _fixture.Runner.ConnectionString,
|
||||
DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName,
|
||||
},
|
||||
Postgres = _fixture.Fixture.CreateOptions(),
|
||||
ObjectStore =
|
||||
{
|
||||
BucketName = "primary-bucket",
|
||||
@@ -128,22 +113,4 @@ public sealed class StorageDualWriteFixture
|
||||
options.DualWrite.MirrorBucket = mirrorBucket;
|
||||
return options;
|
||||
}
|
||||
|
||||
private async Task InitializeMongoAsync(ScannerStorageOptions options)
|
||||
{
|
||||
await _fixture.Client.DropDatabaseAsync(options.Mongo.DatabaseName);
|
||||
var migrations = new IMongoMigration[] { new EnsureLifecycleRuleTtlMigration() };
|
||||
var runner = new MongoMigrationRunner(
|
||||
_fixture.Database,
|
||||
migrations,
|
||||
NullLogger<MongoMigrationRunner>.Instance,
|
||||
TimeProvider.System);
|
||||
var bootstrapper = new MongoBootstrapper(
|
||||
_fixture.Database,
|
||||
Options.Create(options),
|
||||
NullLogger<MongoBootstrapper>.Instance,
|
||||
runner);
|
||||
|
||||
await bootstrapper.InitializeAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MongoDB.Driver;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.Storage.Catalog;
|
||||
using StellaOps.Scanner.Storage.Mongo;
|
||||
using StellaOps.Scanner.Storage.Repositories;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
|
||||
@@ -42,8 +41,8 @@ public sealed class RuntimeEndpointsTests
|
||||
Assert.Equal(0, payload.Duplicates);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var collections = scope.ServiceProvider.GetRequiredService<MongoCollectionProvider>();
|
||||
var stored = await collections.RuntimeEvents.Find(FilterDefinition<RuntimeEventDocument>.Empty).ToListAsync();
|
||||
var repository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
|
||||
var stored = await repository.ListAsync(CancellationToken.None);
|
||||
Assert.Equal(2, stored.Count);
|
||||
Assert.Contains(stored, doc => doc.EventId == "evt-001");
|
||||
Assert.All(stored, doc =>
|
||||
@@ -98,8 +97,8 @@ public sealed class RuntimeEndpointsTests
|
||||
Assert.NotNull(response.Headers.RetryAfter);
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var collections = scope.ServiceProvider.GetRequiredService<MongoCollectionProvider>();
|
||||
var count = await collections.RuntimeEvents.CountDocumentsAsync(FilterDefinition<RuntimeEventDocument>.Empty);
|
||||
var repository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
|
||||
var count = await repository.CountAsync(CancellationToken.None);
|
||||
Assert.Equal(0, count);
|
||||
}
|
||||
|
||||
@@ -117,8 +116,11 @@ public sealed class RuntimeEndpointsTests
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var collections = scope.ServiceProvider.GetRequiredService<MongoCollectionProvider>();
|
||||
var artifacts = scope.ServiceProvider.GetRequiredService<ArtifactRepository>();
|
||||
var links = scope.ServiceProvider.GetRequiredService<LinkRepository>();
|
||||
var policyStore = scope.ServiceProvider.GetRequiredService<PolicySnapshotStore>();
|
||||
var runtimeRepository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
|
||||
await runtimeRepository.TruncateAsync(CancellationToken.None);
|
||||
|
||||
const string policyYaml = """
|
||||
version: "1.0"
|
||||
@@ -138,52 +140,44 @@ rules:
|
||||
var sbomArtifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.ImageBom, "sha256:sbomdigest");
|
||||
var attestationArtifactId = CatalogIdFactory.CreateArtifactId(ArtifactDocumentType.Attestation, "sha256:attdigest");
|
||||
|
||||
await collections.Artifacts.InsertManyAsync(new[]
|
||||
await artifacts.UpsertAsync(new ArtifactDocument
|
||||
{
|
||||
new ArtifactDocument
|
||||
{
|
||||
Id = sbomArtifactId,
|
||||
Type = ArtifactDocumentType.ImageBom,
|
||||
Format = ArtifactDocumentFormat.CycloneDxJson,
|
||||
MediaType = "application/json",
|
||||
BytesSha256 = "sha256:sbomdigest",
|
||||
RefCount = 1,
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow
|
||||
},
|
||||
new ArtifactDocument
|
||||
{
|
||||
Id = attestationArtifactId,
|
||||
Type = ArtifactDocumentType.Attestation,
|
||||
Format = ArtifactDocumentFormat.DsseJson,
|
||||
MediaType = "application/vnd.dsse.envelope+json",
|
||||
BytesSha256 = "sha256:attdigest",
|
||||
RefCount = 1,
|
||||
CreatedAtUtc = DateTime.UtcNow,
|
||||
UpdatedAtUtc = DateTime.UtcNow,
|
||||
Rekor = new RekorReference { Uuid = "rekor-uuid", Url = "https://rekor.example/uuid/rekor-uuid", Index = 7 }
|
||||
}
|
||||
});
|
||||
Id = sbomArtifactId,
|
||||
Type = ArtifactDocumentType.ImageBom,
|
||||
Format = ArtifactDocumentFormat.CycloneDxJson,
|
||||
MediaType = "application/json",
|
||||
BytesSha256 = "sha256:sbomdigest",
|
||||
RefCount = 1
|
||||
}, CancellationToken.None);
|
||||
|
||||
await collections.Links.InsertManyAsync(new[]
|
||||
await artifacts.UpsertAsync(new ArtifactDocument
|
||||
{
|
||||
new LinkDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = imageDigest,
|
||||
ArtifactId = sbomArtifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
},
|
||||
new LinkDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = imageDigest,
|
||||
ArtifactId = attestationArtifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
}
|
||||
});
|
||||
Id = attestationArtifactId,
|
||||
Type = ArtifactDocumentType.Attestation,
|
||||
Format = ArtifactDocumentFormat.DsseJson,
|
||||
MediaType = "application/vnd.dsse.envelope+json",
|
||||
BytesSha256 = "sha256:attdigest",
|
||||
RefCount = 1,
|
||||
Rekor = new RekorReference { Uuid = "rekor-uuid", Url = "https://rekor.example/uuid/rekor-uuid", Index = 7 }
|
||||
}, CancellationToken.None);
|
||||
|
||||
await links.UpsertAsync(new LinkDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = imageDigest,
|
||||
ArtifactId = sbomArtifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
}, CancellationToken.None);
|
||||
|
||||
await links.UpsertAsync(new LinkDocument
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
FromType = LinkSourceType.Image,
|
||||
FromDigest = imageDigest,
|
||||
ArtifactId = attestationArtifactId,
|
||||
CreatedAtUtc = DateTime.UtcNow
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
var ingestRequest = new RuntimeEventsIngestRequestDto
|
||||
@@ -247,7 +241,7 @@ rules:
|
||||
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var collections = scope.ServiceProvider.GetRequiredService<MongoCollectionProvider>();
|
||||
var runtimeRepository = scope.ServiceProvider.GetRequiredService<RuntimeEventRepository>();
|
||||
var policyStore = scope.ServiceProvider.GetRequiredService<PolicySnapshotStore>();
|
||||
|
||||
const string policyYaml = """
|
||||
@@ -259,7 +253,7 @@ rules: []
|
||||
CancellationToken.None);
|
||||
|
||||
// Intentionally skip artifacts/links to simulate missing metadata.
|
||||
await collections.RuntimeEvents.DeleteManyAsync(Builders<RuntimeEventDocument>.Filter.Empty);
|
||||
await runtimeRepository.TruncateAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/v1/policy/runtime", new RuntimePolicyRequestDto
|
||||
|
||||
@@ -6,20 +6,22 @@ using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Mongo2Go;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
using StellaOps.Scanner.Storage;
|
||||
using StellaOps.Scanner.Surface.Validation;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly MongoDbRunner mongoRunner;
|
||||
private readonly Dictionary<string, string?> configuration = new()
|
||||
{
|
||||
["scanner:storage:driver"] = "mongo",
|
||||
["scanner:storage:dsn"] = string.Empty,
|
||||
["scanner:queue:driver"] = "redis",
|
||||
["scanner:queue:dsn"] = "redis://localhost:6379",
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Tests;
|
||||
|
||||
internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly ScannerWebServicePostgresFixture postgresFixture;
|
||||
private readonly Dictionary<string, string?> configuration = new()
|
||||
{
|
||||
["scanner:storage:driver"] = "postgres",
|
||||
["scanner:storage:dsn"] = string.Empty,
|
||||
["scanner:storage:database"] = string.Empty,
|
||||
["scanner:queue:driver"] = "redis",
|
||||
["scanner:queue:dsn"] = "redis://localhost:6379",
|
||||
["scanner:artifactStore:driver"] = "rustfs",
|
||||
["scanner:artifactStore:endpoint"] = "https://rustfs.local/api/v1/",
|
||||
["scanner:artifactStore:accessKey"] = "test-access",
|
||||
@@ -28,37 +30,40 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
["scanner:artifactStore:timeoutSeconds"] = "30",
|
||||
["scanner:telemetry:minimumLogLevel"] = "Information",
|
||||
["scanner:telemetry:enableRequestLogging"] = "false",
|
||||
["scanner:events:enabled"] = "false",
|
||||
["scanner:features:enableSignedReports"] = "false"
|
||||
};
|
||||
|
||||
private readonly Action<IDictionary<string, string?>>? configureConfiguration;
|
||||
private readonly Action<IServiceCollection>? configureServices;
|
||||
|
||||
public ScannerApplicationFactory(
|
||||
Action<IDictionary<string, string?>>? configureConfiguration = null,
|
||||
Action<IServiceCollection>? configureServices = null)
|
||||
{
|
||||
EnsureMongo2GoEnvironment();
|
||||
mongoRunner = MongoDbRunner.Start(singleNodeReplSet: true);
|
||||
configuration["scanner:storage:dsn"] = mongoRunner.ConnectionString;
|
||||
this.configureConfiguration = configureConfiguration;
|
||||
this.configureServices = configureServices;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
configureConfiguration?.Invoke(configuration);
|
||||
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ENABLED", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ALLOWANONYMOUSFALLBACK", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ISSUER", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__AUDIENCES__0", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTID", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTSECRET", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__STORAGE__DSN", configuration["scanner:storage:dsn"]);
|
||||
["scanner:events:enabled"] = "false",
|
||||
["scanner:features:enableSignedReports"] = "false"
|
||||
};
|
||||
|
||||
private readonly Action<IDictionary<string, string?>>? configureConfiguration;
|
||||
private readonly Action<IServiceCollection>? configureServices;
|
||||
|
||||
public ScannerApplicationFactory(
|
||||
Action<IDictionary<string, string?>>? configureConfiguration = null,
|
||||
Action<IServiceCollection>? configureServices = null)
|
||||
{
|
||||
postgresFixture = new ScannerWebServicePostgresFixture();
|
||||
postgresFixture.InitializeAsync().GetAwaiter().GetResult();
|
||||
|
||||
configuration["scanner:storage:dsn"] = postgresFixture.ConnectionString;
|
||||
configuration["scanner:storage:database"] = postgresFixture.SchemaName;
|
||||
this.configureConfiguration = configureConfiguration;
|
||||
this.configureServices = configureServices;
|
||||
}
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
configureConfiguration?.Invoke(configuration);
|
||||
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ENABLED", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ALLOWANONYMOUSFALLBACK", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ISSUER", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__AUDIENCES__0", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTID", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTSECRET", null);
|
||||
Environment.SetEnvironmentVariable("SCANNER__STORAGE__DSN", configuration["scanner:storage:dsn"]);
|
||||
Environment.SetEnvironmentVariable("SCANNER__STORAGE__DATABASE", configuration["scanner:storage:database"]);
|
||||
Environment.SetEnvironmentVariable("SCANNER__QUEUE__DSN", configuration["scanner:queue:dsn"]);
|
||||
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ENDPOINT", configuration["scanner:artifactStore:endpoint"]);
|
||||
Environment.SetEnvironmentVariable("SCANNER__ARTIFACTSTORE__ACCESSKEY", configuration["scanner:artifactStore:accessKey"]);
|
||||
@@ -70,42 +75,42 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__EVENTS__ENABLED", eventsEnabled);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:enabled", out var authorityEnabled))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ENABLED", authorityEnabled);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:allowAnonymousFallback", out var allowAnonymous))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ALLOWANONYMOUSFALLBACK", allowAnonymous);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:issuer", out var authorityIssuer))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ISSUER", authorityIssuer);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:audiences:0", out var primaryAudience))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__AUDIENCES__0", primaryAudience);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:clientId", out var clientId))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTID", clientId);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:clientSecret", out var clientSecret))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTSECRET", clientSecret);
|
||||
}
|
||||
|
||||
builder.ConfigureAppConfiguration((_, configBuilder) =>
|
||||
{
|
||||
configBuilder.AddInMemoryCollection(configuration);
|
||||
});
|
||||
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:enabled", out var authorityEnabled))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ENABLED", authorityEnabled);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:allowAnonymousFallback", out var allowAnonymous))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ALLOWANONYMOUSFALLBACK", allowAnonymous);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:issuer", out var authorityIssuer))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__ISSUER", authorityIssuer);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:audiences:0", out var primaryAudience))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__AUDIENCES__0", primaryAudience);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:clientId", out var clientId))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTID", clientId);
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue("scanner:authority:clientSecret", out var clientSecret))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SCANNER__AUTHORITY__CLIENTSECRET", clientSecret);
|
||||
}
|
||||
|
||||
builder.ConfigureAppConfiguration((_, configBuilder) =>
|
||||
{
|
||||
configBuilder.AddInMemoryCollection(configuration);
|
||||
});
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
configureServices?.Invoke(services);
|
||||
@@ -113,65 +118,15 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
services.AddSingleton<ISurfaceValidatorRunner, TestSurfaceValidatorRunner>();
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
mongoRunner.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureMongo2GoEnvironment()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var libraryPath = ResolveOpenSslLibraryPath();
|
||||
if (libraryPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = Environment.GetEnvironmentVariable("LD_LIBRARY_PATH");
|
||||
if (string.IsNullOrEmpty(existing))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", libraryPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var segments = existing.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (Array.IndexOf(segments, libraryPath) < 0)
|
||||
{
|
||||
Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", string.Join(':', new[] { libraryPath }.Concat(segments)));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveOpenSslLibraryPath()
|
||||
{
|
||||
var current = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(current))
|
||||
{
|
||||
var candidate = Path.Combine(current, "tools", "openssl", "linux-x64");
|
||||
if (Directory.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var parent = Directory.GetParent(current);
|
||||
if (parent is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
current = parent.FullName;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
postgresFixture.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSurfaceValidatorRunner : ISurfaceValidatorRunner
|
||||
@@ -186,4 +141,11 @@ internal sealed class ScannerApplicationFactory : WebApplicationFactory<Program>
|
||||
CancellationToken cancellationToken = default)
|
||||
=> ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class ScannerWebServicePostgresFixture : PostgresIntegrationFixture
|
||||
{
|
||||
protected override System.Reflection.Assembly? GetMigrationAssembly() => typeof(ScannerStorageOptions).Assembly;
|
||||
|
||||
protected override string GetModuleName() => "Scanner.Storage.WebService.Tests";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj" />
|
||||
<ProjectReference Include="..\\..\\..\\__Libraries\\StellaOps.Infrastructure.Postgres.Testing\\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\docs\events\samples\scanner.event.report.ready@1.sample.json">
|
||||
@@ -18,4 +19,4 @@
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user