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
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user