feat: Enhance MongoDB storage with event publishing and outbox support

- Added `MongoAdvisoryObservationEventPublisher` and `NatsAdvisoryObservationEventPublisher` for event publishing.
- Registered `IAdvisoryObservationEventPublisher` to choose between NATS and MongoDB based on configuration.
- Introduced `MongoAdvisoryObservationEventOutbox` for outbox pattern implementation.
- Updated service collection to include new event publishers and outbox.
- Added a new hosted service `AdvisoryObservationTransportWorker` for processing events.

feat: Update project dependencies

- Added `NATS.Client.Core` package to the project for NATS integration.

test: Add unit tests for AdvisoryLinkset normalization

- Created `AdvisoryLinksetNormalizationConfidenceTests` to validate confidence score calculations.

fix: Adjust confidence assertion in `AdvisoryObservationAggregationTests`

- Updated confidence assertion to allow a range instead of a fixed value.

test: Implement tests for AdvisoryObservationEventFactory

- Added `AdvisoryObservationEventFactoryTests` to ensure correct mapping and hashing of observation events.

chore: Configure test project for Findings Ledger

- Created `Directory.Build.props` for test project configuration.
- Added `StellaOps.Findings.Ledger.Exports.Unit.csproj` for unit tests related to findings ledger exports.

feat: Implement export contracts for findings ledger

- Defined export request and response contracts in `ExportContracts.cs`.
- Created various export item records for findings, VEX, advisories, and SBOMs.

feat: Add export functionality to Findings Ledger Web Service

- Implemented endpoints for exporting findings, VEX, advisories, and SBOMs.
- Integrated `ExportQueryService` for handling export logic and pagination.

test: Add tests for Node language analyzer phase 22

- Implemented `NodePhase22SampleLoaderTests` to validate loading of NDJSON fixtures.
- Created sample NDJSON file for testing.

chore: Set up isolated test environment for Node tests

- Added `node-isolated.runsettings` for isolated test execution.
- Created `node-tests-isolated.sh` script for running tests in isolation.
This commit is contained in:
master
2025-11-20 23:08:45 +02:00
parent f0e74d2ee8
commit 2e276d6676
49 changed files with 1996 additions and 113 deletions

View File

@@ -0,0 +1,31 @@
using System.Collections.Immutable;
using StellaOps.Concelier.Core.Linksets;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Linksets;
public sealed class AdvisoryLinksetNormalizationConfidenceTests
{
[Fact]
public void FromRawLinksetWithConfidence_ComputesWeightedScoreAndReasons()
{
var linkset = new RawLinkset
{
Aliases = ImmutableArray.Create("CVE-2024-11111", "GHSA-aaaa-bbbb"),
PackageUrls = ImmutableArray.Create("pkg:npm/foo@1.0.0", "pkg:npm/foo@1.1.0"),
Cpes = ImmutableArray.Create("cpe:/a:foo:foo:1.0.0", "cpe:/a:foo:foo:1.1.0"),
Notes = ImmutableDictionary.CreateRange(new[] { new KeyValuePair<string, string>("severity", "mismatch") })
};
var (normalized, confidence, conflicts) = AdvisoryLinksetNormalization.FromRawLinksetWithConfidence(linkset);
Assert.NotNull(normalized);
Assert.NotNull(confidence);
Assert.True(confidence!.Value is > 0.7 and < 0.8); // weighted score with conflict penalty
var conflict = Assert.Single(conflicts);
Assert.Equal("severity-mismatch", conflict.Reason);
Assert.Contains("severity:mismatch", conflict.Values!);
}
}

View File

@@ -53,7 +53,7 @@ public sealed class AdvisoryObservationAggregationTests
var confidence = result.confidence;
var conflicts = result.conflicts;
Assert.Equal(0.5, confidence);
Assert.True(confidence is >= 0.1 and <= 0.6);
Assert.Single(conflicts);
Assert.Null(normalized); // no purls supplied
}

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Immutable;
using System.Text.Json.Nodes;
using StellaOps.Concelier.Core.Observations;
using StellaOps.Concelier.Models.Observations;
using StellaOps.Concelier.RawModels;
using Xunit;
namespace StellaOps.Concelier.Core.Tests.Observations;
public sealed class AdvisoryObservationEventFactoryTests
{
[Fact]
public void FromObservation_MapsFieldsAndHashesDeterministically()
{
var observation = CreateObservation();
var evt = AdvisoryObservationUpdatedEvent.FromObservation(
observation,
supersedesId: "655fabcdedc0ffee0000abcd",
traceId: "trace-123");
Assert.Equal("urn:tenant:tenant-1", evt.TenantId);
Assert.Equal("adv-1", evt.AdvisoryId);
Assert.Equal("655fabcdedc0ffee0000abcd", evt.SupersedesId);
Assert.NotNull(evt.ObservationHash);
Assert.Equal(observation.Upstream.ContentHash, evt.DocumentSha);
Assert.Contains("pkg:npm/foo", evt.LinksetSummary.Purls);
}
private static AdvisoryObservation CreateObservation()
{
var source = new AdvisoryObservationSource("ghsa", "advisories", "https://api");
var upstream = new AdvisoryObservationUpstream(
"adv-1",
"v1",
DateTimeOffset.Parse("2025-11-20T12:00:00Z"),
DateTimeOffset.Parse("2025-11-20T12:00:00Z"),
"2f8f568cc1ed3474f0a4564ddb8c64f4b4d176fbe0a2a98a02b88e822a4f5b6d",
new AdvisoryObservationSignature(false, null, null, null));
var content = new AdvisoryObservationContent("json", null, JsonNode.Parse("{}")!);
var linkset = new AdvisoryObservationLinkset(
aliases: new[] { "CVE-2024-1234", "GHSA-xxxx" },
purls: new[] { "pkg:npm/foo@1.0.0" },
cpes: new[] { "cpe:/a:foo:foo:1.0.0" },
references: new[] { new AdvisoryObservationReference("ref", "https://example.com") });
var rawLinkset = new RawLinkset
{
Aliases = ImmutableArray.Create("CVE-2024-1234", "GHSA-xxxx"),
PackageUrls = ImmutableArray.Create("pkg:npm/foo@1.0.0"),
Cpes = ImmutableArray.Create("cpe:/a:foo:foo:1.0.0"),
Scopes = ImmutableArray.Create("runtime"),
Relationships = ImmutableArray.Create(new RawRelationship("contains", "pkg:npm/foo@1.0.0", "file://dist/foo.js")),
};
return new AdvisoryObservation(
"655fabcdf3c5d6ad3b5a0aaa",
"tenant-1",
source,
upstream,
content,
linkset,
rawLinkset,
DateTimeOffset.Parse("2025-11-20T12:01:00Z"));
}
}