feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled

- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
StellaOps Bot
2025-12-07 00:27:33 +02:00
parent 9bd6a73926
commit 0de92144d2
229 changed files with 32351 additions and 1481 deletions

View File

@@ -0,0 +1,240 @@
using StellaOps.Concelier.Core.Orchestration;
namespace StellaOps.Concelier.Core.Tests.Orchestration;
public sealed class OrchestratorRegistryStoreTests
{
[Fact]
public async Task UpsertAsync_CreatesNewRecord()
{
var store = new InMemoryOrchestratorRegistryStore();
var record = CreateRegistryRecord("tenant-1", "connector-1");
await store.UpsertAsync(record, CancellationToken.None);
var retrieved = await store.GetAsync("tenant-1", "connector-1", CancellationToken.None);
Assert.NotNull(retrieved);
Assert.Equal("tenant-1", retrieved.Tenant);
Assert.Equal("connector-1", retrieved.ConnectorId);
}
[Fact]
public async Task UpsertAsync_UpdatesExistingRecord()
{
var store = new InMemoryOrchestratorRegistryStore();
var record1 = CreateRegistryRecord("tenant-1", "connector-1", source: "nvd");
var record2 = CreateRegistryRecord("tenant-1", "connector-1", source: "osv");
await store.UpsertAsync(record1, CancellationToken.None);
await store.UpsertAsync(record2, CancellationToken.None);
var retrieved = await store.GetAsync("tenant-1", "connector-1", CancellationToken.None);
Assert.NotNull(retrieved);
Assert.Equal("osv", retrieved.Source);
}
[Fact]
public async Task GetAsync_ReturnsNullForNonExistentRecord()
{
var store = new InMemoryOrchestratorRegistryStore();
var retrieved = await store.GetAsync("tenant-1", "nonexistent", CancellationToken.None);
Assert.Null(retrieved);
}
[Fact]
public async Task ListAsync_ReturnsRecordsForTenant()
{
var store = new InMemoryOrchestratorRegistryStore();
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-a"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-b"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-2", "connector-c"), CancellationToken.None);
var records = await store.ListAsync("tenant-1", CancellationToken.None);
Assert.Equal(2, records.Count);
Assert.All(records, r => Assert.Equal("tenant-1", r.Tenant));
}
[Fact]
public async Task ListAsync_ReturnsOrderedByConnectorId()
{
var store = new InMemoryOrchestratorRegistryStore();
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "zzz-connector"), CancellationToken.None);
await store.UpsertAsync(CreateRegistryRecord("tenant-1", "aaa-connector"), CancellationToken.None);
var records = await store.ListAsync("tenant-1", CancellationToken.None);
Assert.Equal("aaa-connector", records[0].ConnectorId);
Assert.Equal("zzz-connector", records[1].ConnectorId);
}
[Fact]
public async Task AppendHeartbeatAsync_StoresHeartbeat()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
var heartbeat = new OrchestratorHeartbeatRecord(
"tenant-1", "connector-1", runId, 1,
OrchestratorHeartbeatStatus.Running, 50, 10,
null, null, null, null, DateTimeOffset.UtcNow);
await store.AppendHeartbeatAsync(heartbeat, CancellationToken.None);
var latest = await store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, CancellationToken.None);
Assert.NotNull(latest);
Assert.Equal(1, latest.Sequence);
Assert.Equal(OrchestratorHeartbeatStatus.Running, latest.Status);
}
[Fact]
public async Task GetLatestHeartbeatAsync_ReturnsHighestSequence()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 1, OrchestratorHeartbeatStatus.Starting, now), CancellationToken.None);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 3, OrchestratorHeartbeatStatus.Succeeded, now.AddMinutes(2)), CancellationToken.None);
await store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 2, OrchestratorHeartbeatStatus.Running, now.AddMinutes(1)), CancellationToken.None);
var latest = await store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, CancellationToken.None);
Assert.NotNull(latest);
Assert.Equal(3, latest.Sequence);
Assert.Equal(OrchestratorHeartbeatStatus.Succeeded, latest.Status);
}
[Fact]
public async Task EnqueueCommandAsync_StoresCommand()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
var command = new OrchestratorCommandRecord(
"tenant-1", "connector-1", runId, 1,
OrchestratorCommandKind.Pause, null, null,
DateTimeOffset.UtcNow, null);
await store.EnqueueCommandAsync(command, CancellationToken.None);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, null, CancellationToken.None);
Assert.Single(commands);
Assert.Equal(OrchestratorCommandKind.Pause, commands[0].Command);
}
[Fact]
public async Task GetPendingCommandsAsync_FiltersAfterSequence()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 1, OrchestratorCommandKind.Pause, now), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 2, OrchestratorCommandKind.Resume, now), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 3, OrchestratorCommandKind.Throttle, now), CancellationToken.None);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, 1, CancellationToken.None);
Assert.Equal(2, commands.Count);
Assert.Equal(2, commands[0].Sequence);
Assert.Equal(3, commands[1].Sequence);
}
[Fact]
public async Task GetPendingCommandsAsync_ExcludesExpiredCommands()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
var expired = now.AddMinutes(-5);
var future = now.AddMinutes(5);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 1, OrchestratorCommandKind.Pause, now, expired), CancellationToken.None);
await store.EnqueueCommandAsync(CreateCommand("tenant-1", "connector-1", runId, 2, OrchestratorCommandKind.Resume, now, future), CancellationToken.None);
var commands = await store.GetPendingCommandsAsync("tenant-1", "connector-1", runId, null, CancellationToken.None);
Assert.Single(commands);
Assert.Equal(2, commands[0].Sequence);
}
[Fact]
public async Task StoreManifestAsync_StoresManifest()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
var manifest = new OrchestratorRunManifest(
runId, "connector-1", "tenant-1",
new OrchestratorBackfillRange("cursor-a", "cursor-z"),
["hash1", "hash2"],
"dsse-hash",
DateTimeOffset.UtcNow);
await store.StoreManifestAsync(manifest, CancellationToken.None);
var retrieved = await store.GetManifestAsync("tenant-1", "connector-1", runId, CancellationToken.None);
Assert.NotNull(retrieved);
Assert.Equal(runId, retrieved.RunId);
Assert.Equal(2, retrieved.ArtifactHashes.Count);
Assert.Equal("dsse-hash", retrieved.DsseEnvelopeHash);
}
[Fact]
public async Task GetManifestAsync_ReturnsNullForNonExistentManifest()
{
var store = new InMemoryOrchestratorRegistryStore();
var manifest = await store.GetManifestAsync("tenant-1", "connector-1", Guid.NewGuid(), CancellationToken.None);
Assert.Null(manifest);
}
[Fact]
public void Clear_RemovesAllData()
{
var store = new InMemoryOrchestratorRegistryStore();
var runId = Guid.NewGuid();
store.UpsertAsync(CreateRegistryRecord("tenant-1", "connector-1"), CancellationToken.None).Wait();
store.AppendHeartbeatAsync(CreateHeartbeat("tenant-1", "connector-1", runId, 1, OrchestratorHeartbeatStatus.Running, DateTimeOffset.UtcNow), CancellationToken.None).Wait();
store.Clear();
Assert.Null(store.GetAsync("tenant-1", "connector-1", CancellationToken.None).Result);
Assert.Null(store.GetLatestHeartbeatAsync("tenant-1", "connector-1", runId, CancellationToken.None).Result);
}
private static OrchestratorRegistryRecord CreateRegistryRecord(string tenant, string connectorId, string source = "nvd")
{
return new OrchestratorRegistryRecord(
tenant, connectorId, source,
["observations"],
"secret:ref",
new OrchestratorSchedule("0 * * * *", "UTC", 1, 60),
new OrchestratorRatePolicy(100, 10, 30),
["raw-advisory"],
$"concelier:{tenant}:{connectorId}",
new OrchestratorEgressGuard(["example.com"], false),
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow);
}
private static OrchestratorHeartbeatRecord CreateHeartbeat(
string tenant, string connectorId, Guid runId, long sequence,
OrchestratorHeartbeatStatus status, DateTimeOffset timestamp)
{
return new OrchestratorHeartbeatRecord(
tenant, connectorId, runId, sequence, status,
null, null, null, null, null, null, timestamp);
}
private static OrchestratorCommandRecord CreateCommand(
string tenant, string connectorId, Guid runId, long sequence,
OrchestratorCommandKind command, DateTimeOffset createdAt, DateTimeOffset? expiresAt = null)
{
return new OrchestratorCommandRecord(
tenant, connectorId, runId, sequence, command,
null, null, createdAt, expiresAt);
}
}

View File

@@ -0,0 +1,369 @@
using System.Collections.Immutable;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Concelier.Core.Signals;
namespace StellaOps.Concelier.Core.Tests.Signals;
public sealed class AffectedSymbolProviderTests
{
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
[Fact]
public async Task GetByAdvisoryAsync_ReturnsEmptySetForUnknownAdvisory()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
Assert.Equal("tenant-1", result.TenantId);
Assert.Equal("CVE-2024-0001", result.AdvisoryId);
Assert.Empty(result.Symbols);
Assert.Empty(result.SourceSummaries);
Assert.Equal(0, result.UniqueSymbolCount);
}
[Fact]
public async Task GetByAdvisoryAsync_ReturnsStoredSymbols()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromOsv(
observationHash: "sha256:abc123",
fetchedAt: _timeProvider.GetUtcNow(),
ingestJobId: "job-001",
osvId: "GHSA-1234-5678-9abc");
var symbol = AffectedSymbol.Function(
tenantId: "tenant-1",
advisoryId: "CVE-2024-0001",
observationId: "obs-001",
symbol: "lodash.template",
provenance: provenance,
extractedAt: _timeProvider.GetUtcNow(),
purl: "pkg:npm/lodash@4.17.21",
module: "lodash",
versionRange: "<4.17.21");
await store.StoreAsync([symbol], CancellationToken.None);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
Assert.Single(result.Symbols);
Assert.Equal("lodash.template", result.Symbols[0].Symbol);
Assert.Equal(AffectedSymbolType.Function, result.Symbols[0].SymbolType);
Assert.Equal("osv", result.Symbols[0].Provenance.Source);
}
[Fact]
public async Task GetByAdvisoryAsync_ComputesSourceSummaries()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var osvProvenance = AffectedSymbolProvenance.FromOsv(
"sha256:abc", _timeProvider.GetUtcNow());
var nvdProvenance = AffectedSymbolProvenance.FromNvd(
"sha256:def", _timeProvider.GetUtcNow(), cveId: "CVE-2024-0001");
var symbols = new List<AffectedSymbol>
{
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-1", "func1", osvProvenance, _timeProvider.GetUtcNow()),
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-2", "func2", osvProvenance, _timeProvider.GetUtcNow()),
AffectedSymbol.Method("tenant-1", "CVE-2024-0001", "obs-3", "method1", "ClassName", nvdProvenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
var result = await provider.GetByAdvisoryAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
Assert.Equal(3, result.Symbols.Length);
Assert.Equal(2, result.SourceSummaries.Length);
var osvSummary = result.SourceSummaries.First(s => s.Source == "osv");
Assert.Equal(2, osvSummary.SymbolCount);
Assert.Equal(2, osvSummary.CountByType[AffectedSymbolType.Function]);
var nvdSummary = result.SourceSummaries.First(s => s.Source == "nvd");
Assert.Equal(1, nvdSummary.SymbolCount);
Assert.Equal(1, nvdSummary.CountByType[AffectedSymbolType.Method]);
}
[Fact]
public async Task GetByPackageAsync_ReturnsSymbolsForPackage()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromGhsa(
"sha256:ghi", _timeProvider.GetUtcNow(), ghsaId: "GHSA-abcd-efgh-ijkl");
var symbol = AffectedSymbol.Function(
tenantId: "tenant-1",
advisoryId: "CVE-2024-0002",
observationId: "obs-001",
symbol: "express.render",
provenance: provenance,
extractedAt: _timeProvider.GetUtcNow(),
purl: "pkg:npm/express@4.18.0");
await store.StoreAsync([symbol], CancellationToken.None);
var result = await provider.GetByPackageAsync("tenant-1", "pkg:npm/express@4.18.0", CancellationToken.None);
Assert.Single(result.Symbols);
Assert.Equal("express.render", result.Symbols[0].Symbol);
}
[Fact]
public async Task QueryAsync_FiltersByAdvisoryId()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", _timeProvider.GetUtcNow());
var symbols = new List<AffectedSymbol>
{
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, _timeProvider.GetUtcNow()),
AffectedSymbol.Function("tenant-1", "CVE-2024-0002", "obs-2", "func2", provenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
var options = AffectedSymbolQueryOptions.ForAdvisory("tenant-1", "CVE-2024-0001");
var result = await provider.QueryAsync(options, CancellationToken.None);
Assert.Equal(1, result.TotalCount);
Assert.Single(result.Symbols);
Assert.Equal("func1", result.Symbols[0].Symbol);
}
[Fact]
public async Task QueryAsync_FiltersBySymbolType()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", _timeProvider.GetUtcNow());
var symbols = new List<AffectedSymbol>
{
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, _timeProvider.GetUtcNow()),
AffectedSymbol.Method("tenant-1", "CVE-2024-0001", "obs-2", "method1", "Class1", provenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
var options = new AffectedSymbolQueryOptions(
TenantId: "tenant-1",
SymbolTypes: [AffectedSymbolType.Method]);
var result = await provider.QueryAsync(options, CancellationToken.None);
Assert.Equal(1, result.TotalCount);
Assert.Equal(AffectedSymbolType.Method, result.Symbols[0].SymbolType);
}
[Fact]
public async Task QueryAsync_SupportsPagination()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", _timeProvider.GetUtcNow());
var symbols = Enumerable.Range(1, 10)
.Select(i => AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", $"obs-{i}", $"func{i}", provenance, _timeProvider.GetUtcNow()))
.ToList();
await store.StoreAsync(symbols, CancellationToken.None);
var options = new AffectedSymbolQueryOptions(
TenantId: "tenant-1",
Limit: 3,
Offset: 2);
var result = await provider.QueryAsync(options, CancellationToken.None);
Assert.Equal(10, result.TotalCount);
Assert.Equal(3, result.Symbols.Length);
Assert.True(result.HasMore);
}
[Fact]
public async Task GetByAdvisoriesBatchAsync_ReturnsBatchResults()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", _timeProvider.GetUtcNow());
var symbols = new List<AffectedSymbol>
{
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, _timeProvider.GetUtcNow()),
AffectedSymbol.Function("tenant-1", "CVE-2024-0002", "obs-2", "func2", provenance, _timeProvider.GetUtcNow())
};
await store.StoreAsync(symbols, CancellationToken.None);
var result = await provider.GetByAdvisoriesBatchAsync(
"tenant-1",
["CVE-2024-0001", "CVE-2024-0002", "CVE-2024-0003"],
CancellationToken.None);
Assert.Equal(3, result.Count);
Assert.Single(result["CVE-2024-0001"].Symbols);
Assert.Single(result["CVE-2024-0002"].Symbols);
Assert.Empty(result["CVE-2024-0003"].Symbols);
}
[Fact]
public async Task HasSymbolsAsync_ReturnsTrueWhenSymbolsExist()
{
var store = new InMemoryAffectedSymbolStore();
var provider = new AffectedSymbolProvider(
store,
_timeProvider,
NullLogger<AffectedSymbolProvider>.Instance);
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", _timeProvider.GetUtcNow());
var symbol = AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, _timeProvider.GetUtcNow());
await store.StoreAsync([symbol], CancellationToken.None);
var exists = await provider.HasSymbolsAsync("tenant-1", "CVE-2024-0001", CancellationToken.None);
var notExists = await provider.HasSymbolsAsync("tenant-1", "CVE-2024-9999", CancellationToken.None);
Assert.True(exists);
Assert.False(notExists);
}
[Fact]
public void AffectedSymbol_CanonicalId_GeneratesCorrectFormat()
{
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", DateTimeOffset.UtcNow);
var function = AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", "obs-1", "myFunc", provenance, DateTimeOffset.UtcNow,
module: "myModule");
Assert.Equal("myModule::myFunc", function.CanonicalId);
var method = AffectedSymbol.Method(
"tenant-1", "CVE-2024-0001", "obs-1", "myMethod", "MyClass", provenance, DateTimeOffset.UtcNow,
module: "myModule");
Assert.Equal("myModule::MyClass.myMethod", method.CanonicalId);
var globalFunc = AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", "obs-1", "globalFunc", provenance, DateTimeOffset.UtcNow);
Assert.Equal("global::globalFunc", globalFunc.CanonicalId);
}
[Fact]
public void AffectedSymbol_HasSourceLocation_ReturnsCorrectValue()
{
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", DateTimeOffset.UtcNow);
var withLocation = AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, DateTimeOffset.UtcNow,
filePath: "/src/lib.js", lineNumber: 42);
Assert.True(withLocation.HasSourceLocation);
var withoutLocation = AffectedSymbol.Function(
"tenant-1", "CVE-2024-0001", "obs-1", "func2", provenance, DateTimeOffset.UtcNow);
Assert.False(withoutLocation.HasSourceLocation);
}
[Fact]
public void AffectedSymbolSet_UniqueSymbolCount_CountsDistinctCanonicalIds()
{
var provenance = AffectedSymbolProvenance.FromOsv("sha256:test", DateTimeOffset.UtcNow);
var symbols = ImmutableArray.Create(
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-1", "func1", provenance, DateTimeOffset.UtcNow, module: "mod1"),
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-2", "func1", provenance, DateTimeOffset.UtcNow, module: "mod1"), // duplicate
AffectedSymbol.Function("tenant-1", "CVE-2024-0001", "obs-3", "func2", provenance, DateTimeOffset.UtcNow, module: "mod1")
);
var set = new AffectedSymbolSet(
"tenant-1", "CVE-2024-0001", symbols,
ImmutableArray<AffectedSymbolSourceSummary>.Empty, DateTimeOffset.UtcNow);
Assert.Equal(2, set.UniqueSymbolCount);
}
[Fact]
public void AffectedSymbolProvenance_FromOsv_CreatesCorrectProvenance()
{
var now = DateTimeOffset.UtcNow;
var provenance = AffectedSymbolProvenance.FromOsv(
observationHash: "sha256:abc123",
fetchedAt: now,
ingestJobId: "job-001",
osvId: "GHSA-1234-5678-9abc");
Assert.Equal("osv", provenance.Source);
Assert.Equal("open-source-vulnerabilities", provenance.Vendor);
Assert.Equal("sha256:abc123", provenance.ObservationHash);
Assert.Equal(now, provenance.FetchedAt);
Assert.Equal("job-001", provenance.IngestJobId);
Assert.Equal("GHSA-1234-5678-9abc", provenance.UpstreamId);
Assert.Equal("https://osv.dev/vulnerability/GHSA-1234-5678-9abc", provenance.UpstreamUrl);
}
[Fact]
public void AffectedSymbolProvenance_FromNvd_CreatesCorrectProvenance()
{
var now = DateTimeOffset.UtcNow;
var provenance = AffectedSymbolProvenance.FromNvd(
observationHash: "sha256:def456",
fetchedAt: now,
cveId: "CVE-2024-0001");
Assert.Equal("nvd", provenance.Source);
Assert.Equal("national-vulnerability-database", provenance.Vendor);
Assert.Equal("CVE-2024-0001", provenance.UpstreamId);
Assert.Equal("https://nvd.nist.gov/vuln/detail/CVE-2024-0001", provenance.UpstreamUrl);
}
[Fact]
public void AffectedSymbolProvenance_FromGhsa_CreatesCorrectProvenance()
{
var now = DateTimeOffset.UtcNow;
var provenance = AffectedSymbolProvenance.FromGhsa(
observationHash: "sha256:ghi789",
fetchedAt: now,
ghsaId: "GHSA-abcd-efgh-ijkl");
Assert.Equal("ghsa", provenance.Source);
Assert.Equal("github-security-advisories", provenance.Vendor);
Assert.Equal("GHSA-abcd-efgh-ijkl", provenance.UpstreamId);
Assert.Equal("https://github.com/advisories/GHSA-abcd-efgh-ijkl", provenance.UpstreamUrl);
}
}

View File

@@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Orchestrator;
using StellaOps.Concelier.Core.Orchestration;
using StellaOps.Concelier.WebService;
using StellaOps.Concelier.WebService.Options;
using Xunit;
@@ -53,7 +53,7 @@ public sealed class OrchestratorTestWebAppFactory : WebApplicationFactory<Progra
builder.ConfigureServices(services =>
{
services.RemoveAll<IOrchestratorRegistryStore>();
services.AddSingleton<IOrchestratorRegistryStore, InMemoryOrchestratorStore>();
services.AddSingleton<IOrchestratorRegistryStore, InMemoryOrchestratorRegistryStore>();
// Pre-bind options to keep Program from trying to rebind/validate during tests.
services.RemoveAll<ConcelierOptions>();
@@ -155,42 +155,3 @@ public sealed class OrchestratorEndpointsTests : IClassFixture<OrchestratorTestW
}
}
internal sealed class InMemoryOrchestratorStore : IOrchestratorRegistryStore
{
private readonly Dictionary<(string Tenant, string ConnectorId), OrchestratorRegistryRecord> _registry = new();
private readonly List<OrchestratorHeartbeatRecord> _heartbeats = new();
private readonly List<OrchestratorCommandRecord> _commands = new();
public Task UpsertAsync(OrchestratorRegistryRecord record, CancellationToken cancellationToken)
{
_registry[(record.Tenant, record.ConnectorId)] = record;
return Task.CompletedTask;
}
public Task<OrchestratorRegistryRecord?> GetAsync(string tenant, string connectorId, CancellationToken cancellationToken)
{
_registry.TryGetValue((tenant, connectorId), out var record);
return Task.FromResult(record);
}
public Task EnqueueCommandAsync(OrchestratorCommandRecord command, CancellationToken cancellationToken)
{
_commands.Add(command);
return Task.CompletedTask;
}
public Task<IReadOnlyList<OrchestratorCommandRecord>> GetPendingCommandsAsync(string tenant, string connectorId, Guid runId, long? afterSequence, CancellationToken cancellationToken)
{
var items = _commands
.Where(c => c.Tenant == tenant && c.ConnectorId == connectorId && c.RunId == runId && (afterSequence is null || c.Sequence > afterSequence))
.ToList()
.AsReadOnly();
return Task.FromResult<IReadOnlyList<OrchestratorCommandRecord>>(items);
}
public Task AppendHeartbeatAsync(OrchestratorHeartbeatRecord heartbeat, CancellationToken cancellationToken)
{
_heartbeats.Add(heartbeat);
return Task.CompletedTask;
}
}