feat: Implement MongoDB orchestrator storage with registry, commands, and heartbeats
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added NullAdvisoryObservationEventTransport for handling advisory observation events. - Created IOrchestratorRegistryStore interface for orchestrator registry operations. - Implemented MongoOrchestratorRegistryStore for MongoDB interactions with orchestrator data. - Defined OrchestratorCommandDocument and OrchestratorCommandRecord for command handling. - Added OrchestratorHeartbeatDocument and OrchestratorHeartbeatRecord for heartbeat tracking. - Created OrchestratorRegistryDocument and OrchestratorRegistryRecord for registry management. - Developed tests for orchestrator collections migration and MongoOrchestratorRegistryStore functionality. - Introduced AirgapImportRequest and AirgapImportValidator for air-gapped VEX bundle imports. - Added incident mode rules sample JSON for notifier configuration.
This commit is contained in:
@@ -60,6 +60,44 @@ public sealed class AdvisoryObservationAggregationTests
|
||||
Assert.Null(normalized); // no purls supplied
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAggregateLinkset_ComputesConflictsAndConfidenceFromObservations()
|
||||
{
|
||||
var obsA = CreateObservation(
|
||||
"obs-a",
|
||||
new RawLinkset
|
||||
{
|
||||
Aliases = ImmutableArray.Create("CVE-2025-0001"),
|
||||
PackageUrls = ImmutableArray.Create("pkg:npm/foo@1.0.0"),
|
||||
References = ImmutableArray.Create(new RawReference("advisory", "https://a.example/advisory"))
|
||||
},
|
||||
fetchedAt: DateTimeOffset.UtcNow.AddHours(-1),
|
||||
vendor: "vendor-a");
|
||||
|
||||
var obsB = CreateObservation(
|
||||
"obs-b",
|
||||
new RawLinkset
|
||||
{
|
||||
Aliases = ImmutableArray.Create("GHSA-xxxx-xxxx"),
|
||||
PackageUrls = ImmutableArray.Create("pkg:npm/foo@2.0.0"),
|
||||
References = ImmutableArray.Create(new RawReference("advisory", "https://b.example/advisory"))
|
||||
},
|
||||
fetchedAt: DateTimeOffset.UtcNow,
|
||||
vendor: "vendor-b");
|
||||
|
||||
var method = typeof(AdvisoryObservationQueryService).GetMethod(
|
||||
"BuildAggregateLinkset",
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!;
|
||||
|
||||
var aggregate = (AdvisoryObservationLinksetAggregate)method.Invoke(
|
||||
null,
|
||||
new object?[] { ImmutableArray.Create(obsA, obsB) })!;
|
||||
|
||||
Assert.Contains(aggregate.Conflicts, c => c.Reason == "alias-inconsistency");
|
||||
Assert.Contains(aggregate.Conflicts, c => c.Reason == "affected-range-divergence");
|
||||
Assert.True(aggregate.Confidence is > 0.0 and < 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildAggregateLinkset_EmptyInputReturnsEmptyArrays()
|
||||
{
|
||||
@@ -75,13 +113,17 @@ public sealed class AdvisoryObservationAggregationTests
|
||||
Assert.True(aggregate.Relationships.IsEmpty);
|
||||
}
|
||||
|
||||
private static AdvisoryObservation CreateObservation(string id, RawLinkset rawLinkset)
|
||||
private static AdvisoryObservation CreateObservation(
|
||||
string id,
|
||||
RawLinkset rawLinkset,
|
||||
DateTimeOffset? fetchedAt = null,
|
||||
string vendor = "vendor")
|
||||
{
|
||||
var source = new AdvisoryObservationSource("vendor", "stream", "api");
|
||||
var source = new AdvisoryObservationSource(vendor, "stream", "api");
|
||||
var upstream = new AdvisoryObservationUpstream(
|
||||
"adv-id",
|
||||
null,
|
||||
DateTimeOffset.UtcNow,
|
||||
(fetchedAt ?? DateTimeOffset.UtcNow),
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256:abc",
|
||||
new AdvisoryObservationSignature(false, null, null, null));
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class ConcelierMongoLinksetStoreTests : IClassFixture<MongoIntegra
|
||||
0.82,
|
||||
new List<AdvisoryLinksetConflict>
|
||||
{
|
||||
new("severity", "disagree", new[] { "HIGH", "MEDIUM" })
|
||||
new("severity", "disagree", new[] { "HIGH", "MEDIUM" }, new[] { "source-a", "source-b" })
|
||||
},
|
||||
DateTimeOffset.UtcNow,
|
||||
"job-1");
|
||||
@@ -54,6 +54,7 @@ public sealed class ConcelierMongoLinksetStoreTests : IClassFixture<MongoIntegra
|
||||
Assert.Single(document.Conflicts!);
|
||||
Assert.Equal("severity", document.Conflicts![0].Field);
|
||||
Assert.Equal("disagree", document.Conflicts![0].Reason);
|
||||
Assert.Equal(new[] { "source-a", "source-b" }, document.Conflicts![0].SourceIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -72,7 +73,8 @@ public sealed class ConcelierMongoLinksetStoreTests : IClassFixture<MongoIntegra
|
||||
{
|
||||
Field = "references",
|
||||
Reason = "mismatch",
|
||||
Values = new List<string> { "url1", "url2" }
|
||||
Values = new List<string> { "url1", "url2" },
|
||||
SourceIds = new List<string> { "src-a", "src-b" }
|
||||
}
|
||||
},
|
||||
CreatedAt = DateTime.UtcNow
|
||||
@@ -90,6 +92,7 @@ public sealed class ConcelierMongoLinksetStoreTests : IClassFixture<MongoIntegra
|
||||
Assert.NotNull(model.Conflicts);
|
||||
Assert.Single(model.Conflicts!);
|
||||
Assert.Equal("references", model.Conflicts![0].Field);
|
||||
Assert.Equal(new[] { "src-a", "src-b" }, model.Conflicts![0].SourceIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Core.Observations;
|
||||
using StellaOps.Concelier.Storage.Mongo.Observations;
|
||||
using StellaOps.Concelier.Models.Observations;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Storage.Mongo.Tests.Observations;
|
||||
|
||||
public class AdvisoryObservationTransportWorkerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Worker_publishes_outbox_entries_and_marks_published_once()
|
||||
{
|
||||
var evt = new AdvisoryObservationUpdatedEvent(
|
||||
Guid.NewGuid(),
|
||||
"tenant-1",
|
||||
"obs-1",
|
||||
"adv-1",
|
||||
new Models.Observations.AdvisoryObservationSource("vendor", "stream", "api", "1.0.0"),
|
||||
new AdvisoryObservationLinksetSummary(
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<string>.Empty,
|
||||
ImmutableArray<AdvisoryObservationRelationshipSummary>.Empty),
|
||||
"doc-sha",
|
||||
"hash-1",
|
||||
DateTimeOffset.UtcNow,
|
||||
ReplayCursor: "cursor-1",
|
||||
supersedesId: null,
|
||||
traceId: "trace-1");
|
||||
|
||||
var outbox = new FakeOutbox(evt);
|
||||
var transport = new FakeTransport();
|
||||
var options = Options.Create(new AdvisoryObservationEventPublisherOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Transport = "nats",
|
||||
Subject = "subject",
|
||||
Stream = "stream",
|
||||
NatsUrl = "nats://localhost:4222"
|
||||
});
|
||||
|
||||
var worker = new AdvisoryObservationTransportWorker(outbox, transport, options, NullLogger<AdvisoryObservationTransportWorker>.Instance);
|
||||
|
||||
await worker.StartAsync(CancellationToken.None);
|
||||
await Task.Delay(150, CancellationToken.None);
|
||||
await worker.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, transport.Sent.Count);
|
||||
Assert.Equal(evt.EventId, transport.Sent[0].EventId);
|
||||
Assert.Equal(1, outbox.MarkedCount);
|
||||
}
|
||||
|
||||
private sealed class FakeOutbox : IAdvisoryObservationEventOutbox
|
||||
{
|
||||
private readonly AdvisoryObservationUpdatedEvent _event;
|
||||
private bool _dequeued;
|
||||
public int MarkedCount { get; private set; }
|
||||
|
||||
public FakeOutbox(AdvisoryObservationUpdatedEvent @event)
|
||||
{
|
||||
_event = @event;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyCollection<AdvisoryObservationUpdatedEvent>> DequeueAsync(int take, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_dequeued)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyCollection<AdvisoryObservationUpdatedEvent>>(Array.Empty<AdvisoryObservationUpdatedEvent>());
|
||||
}
|
||||
|
||||
_dequeued = true;
|
||||
return Task.FromResult<IReadOnlyCollection<AdvisoryObservationUpdatedEvent>>(new[] { _event });
|
||||
}
|
||||
|
||||
public Task MarkPublishedAsync(Guid eventId, DateTimeOffset publishedAt, CancellationToken cancellationToken)
|
||||
{
|
||||
MarkedCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeTransport : IAdvisoryObservationEventTransport
|
||||
{
|
||||
public List<AdvisoryObservationUpdatedEvent> Sent { get; } = new();
|
||||
|
||||
public Task SendAsync(AdvisoryObservationUpdatedEvent @event, CancellationToken cancellationToken)
|
||||
{
|
||||
Sent.Add(@event);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,6 +603,33 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
Assert.Equal("GHSA-2025-0001", evidence!.AdvisoryKey);
|
||||
Assert.Equal(2, evidence.Records.Count);
|
||||
Assert.All(evidence.Records, record => Assert.Equal("tenant-a", record.Tenant));
|
||||
Assert.Null(evidence.Attestation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AdvisoryEvidenceEndpoint_AttachesAttestationWhenBundleProvided()
|
||||
{
|
||||
await SeedAdvisoryRawDocumentsAsync(
|
||||
CreateAdvisoryRawDocument("tenant-a", "vendor-x", "GHSA-2025-0003", "sha256:201", new BsonDocument("id", "GHSA-2025-0003:1")));
|
||||
|
||||
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..", ".."));
|
||||
var sampleDir = Path.Combine(repoRoot, "docs", "samples", "evidence-bundle");
|
||||
var tarPath = Path.Combine(sampleDir, "evidence-bundle-m0.tar.gz");
|
||||
var manifestPath = Path.Combine(sampleDir, "manifest.json");
|
||||
var transparencyPath = Path.Combine(sampleDir, "transparency.json");
|
||||
|
||||
using var client = _factory.CreateClient();
|
||||
var requestUri = $"/vuln/evidence/advisories/GHSA-2025-0003?tenant=tenant-a&bundlePath={Uri.EscapeDataString(tarPath)}&manifestPath={Uri.EscapeDataString(manifestPath)}&transparencyPath={Uri.EscapeDataString(transparencyPath)}&pipelineVersion=git:test-sha";
|
||||
var response = await client.GetAsync(requestUri);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var evidence = await response.Content.ReadFromJsonAsync<AdvisoryEvidenceResponse>();
|
||||
|
||||
Assert.NotNull(evidence);
|
||||
Assert.NotNull(evidence!.Attestation);
|
||||
Assert.Equal("evidence-bundle-m0", evidence.Attestation!.SubjectName);
|
||||
Assert.Equal("git:test-sha", evidence.Attestation.PipelineVersion);
|
||||
Assert.Equal(tarPath, evidence.Attestation.EvidenceBundlePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user