feat: Add CVSS receipt management endpoints and related functionality
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

- Introduced new API endpoints for creating, retrieving, amending, and listing CVSS receipts.
- Updated IPolicyEngineClient interface to include methods for CVSS receipt operations.
- Implemented PolicyEngineClient to handle CVSS receipt requests.
- Enhanced Program.cs to map new CVSS receipt routes with appropriate authorization.
- Added necessary models and contracts for CVSS receipt requests and responses.
- Integrated Postgres document store for managing CVSS receipts and related data.
- Updated database schema with new migrations for source documents and payload storage.
- Refactored existing components to support new CVSS functionality.
This commit is contained in:
StellaOps Bot
2025-12-07 00:43:14 +02:00
parent 0de92144d2
commit 53889d85e7
67 changed files with 17207 additions and 16293 deletions

View File

@@ -6,36 +6,36 @@ using StellaOps.Concelier.Connector.Common.Html;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Storage.Mongo.Documents;
using Xunit;
namespace StellaOps.Concelier.Connector.Cccs.Tests.Internal;
public sealed class CccsMapperTests
{
[Fact]
public void Map_CreatesCanonicalAdvisory()
{
var raw = CccsHtmlParserTests.LoadFixture<CccsRawAdvisoryDocument>("cccs-raw-advisory.json");
var dto = new CccsHtmlParser(new HtmlContentSanitizer()).Parse(raw);
var document = new DocumentRecord(
Guid.NewGuid(),
CccsConnectorPlugin.SourceName,
dto.CanonicalUrl,
DateTimeOffset.UtcNow,
"sha-test",
DocumentStatuses.PendingMap,
"application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: dto.Modified,
GridFsId: null);
var recordedAt = DateTimeOffset.Parse("2025-08-12T00:00:00Z");
var advisory = CccsMapper.Map(dto, document, recordedAt);
advisory.AdvisoryKey.Should().Be("TEST-001");
advisory.Title.Should().Be(dto.Title);
advisory.Aliases.Should().Contain(new[] { "TEST-001", "CVE-2020-1234", "CVE-2021-9999" });
namespace StellaOps.Concelier.Connector.Cccs.Tests.Internal;
public sealed class CccsMapperTests
{
[Fact]
public void Map_CreatesCanonicalAdvisory()
{
var raw = CccsHtmlParserTests.LoadFixture<CccsRawAdvisoryDocument>("cccs-raw-advisory.json");
var dto = new CccsHtmlParser(new HtmlContentSanitizer()).Parse(raw);
var document = new DocumentRecord(
Guid.NewGuid(),
CccsConnectorPlugin.SourceName,
dto.CanonicalUrl,
DateTimeOffset.UtcNow,
"sha-test",
DocumentStatuses.PendingMap,
"application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: dto.Modified,
PayloadId: null);
var recordedAt = DateTimeOffset.Parse("2025-08-12T00:00:00Z");
var advisory = CccsMapper.Map(dto, document, recordedAt);
advisory.AdvisoryKey.Should().Be("TEST-001");
advisory.Title.Should().Be(dto.Title);
advisory.Aliases.Should().Contain(new[] { "TEST-001", "CVE-2020-1234", "CVE-2021-9999" });
advisory.References.Should().Contain(reference => reference.Url == dto.CanonicalUrl && reference.Kind == "details");
advisory.References.Should().Contain(reference => reference.Url == "https://example.com/details");
advisory.AffectedPackages.Should().HaveCount(2);

View File

@@ -1,118 +1,118 @@
using System;
using System.Globalization;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.CertCc.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using Xunit;
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
public sealed class CertCcMapperTests
{
private static readonly DateTimeOffset PublishedAt = DateTimeOffset.Parse("2025-10-03T11:35:31Z", CultureInfo.InvariantCulture);
[Fact]
public void Map_ProducesCanonicalAdvisoryWithVendorPrimitives()
{
const string vendorStatement =
"The issue is confirmed, and here is the patch list\n\n" +
"V3912/V3910/V2962/V1000B\t4.4.3.6/4.4.5.1\n" +
"V2927/V2865/V2866\t4.5.1\n" +
"V2765/V2766/V2763/V2135\t4.5.1";
var vendor = new CertCcVendorDto(
"DrayTek Corporation",
ContactDate: PublishedAt.AddDays(-10),
StatementDate: PublishedAt.AddDays(-5),
Updated: PublishedAt,
Statement: vendorStatement,
Addendum: null,
References: new[] { "https://www.draytek.com/support/resources?type=version" });
var vendorStatus = new CertCcVendorStatusDto(
Vendor: "DrayTek Corporation",
CveId: "CVE-2025-10547",
Status: "Affected",
Statement: null,
References: Array.Empty<string>(),
DateAdded: PublishedAt,
DateUpdated: PublishedAt);
var vulnerability = new CertCcVulnerabilityDto(
CveId: "CVE-2025-10547",
Description: null,
DateAdded: PublishedAt,
DateUpdated: PublishedAt);
var metadata = new CertCcNoteMetadata(
VuId: "VU#294418",
IdNumber: "294418",
Title: "Vigor routers running DrayOS RCE via EasyVPN",
Overview: "Overview",
Summary: "Summary",
Published: PublishedAt,
Updated: PublishedAt.AddMinutes(5),
Created: PublishedAt,
Revision: 2,
CveIds: new[] { "CVE-2025-10547" },
PublicUrls: new[]
{
"https://www.draytek.com/about/security-advisory/use-of-uninitialized-variable-vulnerabilities/",
"https://www.draytek.com/support/resources?type=version"
},
PrimaryUrl: "https://www.kb.cert.org/vuls/id/294418/");
var dto = new CertCcNoteDto(
metadata,
Vendors: new[] { vendor },
VendorStatuses: new[] { vendorStatus },
Vulnerabilities: new[] { vulnerability });
var document = new DocumentRecord(
Guid.NewGuid(),
"cert-cc",
"https://www.kb.cert.org/vuls/id/294418/",
PublishedAt,
Sha256: new string('0', 64),
Status: "pending-map",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: PublishedAt,
GridFsId: null);
var dtoRecord = new DtoRecord(
Id: Guid.NewGuid(),
DocumentId: document.Id,
SourceName: "cert-cc",
SchemaVersion: "certcc.vince.note.v1",
Payload: new BsonDocument(),
ValidatedAt: PublishedAt.AddMinutes(1));
var advisory = CertCcMapper.Map(dto, document, dtoRecord, "cert-cc");
Assert.Equal("certcc/vu-294418", advisory.AdvisoryKey);
Assert.Contains("VU#294418", advisory.Aliases);
Assert.Contains("CVE-2025-10547", advisory.Aliases);
Assert.Equal("en", advisory.Language);
Assert.Equal(PublishedAt, advisory.Published);
Assert.Contains(advisory.References, reference => reference.Url.Contains("/vuls/id/294418", StringComparison.OrdinalIgnoreCase));
var affected = Assert.Single(advisory.AffectedPackages);
Assert.Equal("vendor", affected.Type);
Assert.Equal("DrayTek Corporation", affected.Identifier);
Assert.Contains(affected.Statuses, status => status.Status == AffectedPackageStatusCatalog.Affected);
var range = Assert.Single(affected.VersionRanges);
Assert.NotNull(range.Primitives);
Assert.NotNull(range.Primitives!.VendorExtensions);
Assert.Contains(range.Primitives.VendorExtensions!, kvp => kvp.Key == "certcc.vendor.patches");
Assert.NotEmpty(affected.NormalizedVersions);
Assert.Contains(affected.NormalizedVersions, rule => rule.Scheme == "certcc.vendor" && rule.Value == "4.5.1");
}
}
using System;
using System.Globalization;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.CertCc.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using Xunit;
namespace StellaOps.Concelier.Connector.CertCc.Tests.Internal;
public sealed class CertCcMapperTests
{
private static readonly DateTimeOffset PublishedAt = DateTimeOffset.Parse("2025-10-03T11:35:31Z", CultureInfo.InvariantCulture);
[Fact]
public void Map_ProducesCanonicalAdvisoryWithVendorPrimitives()
{
const string vendorStatement =
"The issue is confirmed, and here is the patch list\n\n" +
"V3912/V3910/V2962/V1000B\t4.4.3.6/4.4.5.1\n" +
"V2927/V2865/V2866\t4.5.1\n" +
"V2765/V2766/V2763/V2135\t4.5.1";
var vendor = new CertCcVendorDto(
"DrayTek Corporation",
ContactDate: PublishedAt.AddDays(-10),
StatementDate: PublishedAt.AddDays(-5),
Updated: PublishedAt,
Statement: vendorStatement,
Addendum: null,
References: new[] { "https://www.draytek.com/support/resources?type=version" });
var vendorStatus = new CertCcVendorStatusDto(
Vendor: "DrayTek Corporation",
CveId: "CVE-2025-10547",
Status: "Affected",
Statement: null,
References: Array.Empty<string>(),
DateAdded: PublishedAt,
DateUpdated: PublishedAt);
var vulnerability = new CertCcVulnerabilityDto(
CveId: "CVE-2025-10547",
Description: null,
DateAdded: PublishedAt,
DateUpdated: PublishedAt);
var metadata = new CertCcNoteMetadata(
VuId: "VU#294418",
IdNumber: "294418",
Title: "Vigor routers running DrayOS RCE via EasyVPN",
Overview: "Overview",
Summary: "Summary",
Published: PublishedAt,
Updated: PublishedAt.AddMinutes(5),
Created: PublishedAt,
Revision: 2,
CveIds: new[] { "CVE-2025-10547" },
PublicUrls: new[]
{
"https://www.draytek.com/about/security-advisory/use-of-uninitialized-variable-vulnerabilities/",
"https://www.draytek.com/support/resources?type=version"
},
PrimaryUrl: "https://www.kb.cert.org/vuls/id/294418/");
var dto = new CertCcNoteDto(
metadata,
Vendors: new[] { vendor },
VendorStatuses: new[] { vendorStatus },
Vulnerabilities: new[] { vulnerability });
var document = new DocumentRecord(
Guid.NewGuid(),
"cert-cc",
"https://www.kb.cert.org/vuls/id/294418/",
PublishedAt,
Sha256: new string('0', 64),
Status: "pending-map",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: PublishedAt,
PayloadId: null);
var dtoRecord = new DtoRecord(
Id: Guid.NewGuid(),
DocumentId: document.Id,
SourceName: "cert-cc",
SchemaVersion: "certcc.vince.note.v1",
Payload: new BsonDocument(),
ValidatedAt: PublishedAt.AddMinutes(1));
var advisory = CertCcMapper.Map(dto, document, dtoRecord, "cert-cc");
Assert.Equal("certcc/vu-294418", advisory.AdvisoryKey);
Assert.Contains("VU#294418", advisory.Aliases);
Assert.Contains("CVE-2025-10547", advisory.Aliases);
Assert.Equal("en", advisory.Language);
Assert.Equal(PublishedAt, advisory.Published);
Assert.Contains(advisory.References, reference => reference.Url.Contains("/vuls/id/294418", StringComparison.OrdinalIgnoreCase));
var affected = Assert.Single(advisory.AffectedPackages);
Assert.Equal("vendor", affected.Type);
Assert.Equal("DrayTek Corporation", affected.Identifier);
Assert.Contains(affected.Statuses, status => status.Status == AffectedPackageStatusCatalog.Affected);
var range = Assert.Single(affected.VersionRanges);
Assert.NotNull(range.Primitives);
Assert.NotNull(range.Primitives!.VendorExtensions);
Assert.Contains(range.Primitives.VendorExtensions!, kvp => kvp.Key == "certcc.vendor.patches");
Assert.NotEmpty(affected.NormalizedVersions);
Assert.Contains(affected.NormalizedVersions, rule => rule.Scheme == "certcc.vendor" && rule.Value == "4.5.1");
}
}

View File

@@ -93,7 +93,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.Equal(documentId, storedDocument!.Id);
Assert.Equal("application/json", storedDocument.ContentType);
Assert.Equal(DocumentStatuses.PendingParse, storedDocument.Status);
Assert.NotNull(storedDocument.GridFsId);
Assert.NotNull(storedDocument.PayloadId);
Assert.NotNull(storedDocument.Headers);
Assert.Equal("true", storedDocument.Headers!["X-Test"]);
Assert.NotNull(storedDocument.Metadata);
@@ -153,7 +153,7 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
CancellationToken.None);
Assert.NotNull(existingRecord);
var previousGridId = existingRecord!.GridFsId;
var previousGridId = existingRecord!.PayloadId;
Assert.NotNull(previousGridId);
var filesCollection = _database.GetCollection<BsonDocument>("documents.files");
@@ -189,8 +189,8 @@ public sealed class SourceStateSeedProcessorTests : IAsyncLifetime
Assert.NotNull(refreshedRecord);
Assert.Equal(documentId, refreshedRecord!.Id);
Assert.NotNull(refreshedRecord.GridFsId);
Assert.NotEqual(previousGridId, refreshedRecord.GridFsId);
Assert.NotNull(refreshedRecord.PayloadId);
Assert.NotEqual(previousGridId, refreshedRecord.PayloadId);
var files = await filesCollection.Find(FilterDefinition<BsonDocument>.Empty).ToListAsync();
Assert.Single(files);

View File

@@ -1,82 +1,82 @@
using System;
using Xunit;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Distro.Debian;
using StellaOps.Concelier.Connector.Distro.Debian.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
public sealed class DebianMapperTests
{
[Fact]
public void Map_BuildsRangePrimitives_ForResolvedPackage()
{
var dto = new DebianAdvisoryDto(
AdvisoryId: "DSA-2024-123",
SourcePackage: "openssl",
Title: "Openssl security update",
Description: "Fixes multiple issues.",
CveIds: new[] { "CVE-2024-1000", "CVE-2024-1001" },
Packages: new[]
{
new DebianPackageStateDto(
Package: "openssl",
Release: "bullseye",
Status: "resolved",
IntroducedVersion: "1:1.1.1n-0+deb11u2",
FixedVersion: "1:1.1.1n-0+deb11u5",
LastAffectedVersion: null,
Published: new DateTimeOffset(2024, 9, 1, 0, 0, 0, TimeSpan.Zero)),
new DebianPackageStateDto(
Package: "openssl",
Release: "bookworm",
Status: "open",
IntroducedVersion: null,
FixedVersion: null,
LastAffectedVersion: null,
Published: null)
},
References: new[]
{
new DebianReferenceDto(
Url: "https://security-tracker.debian.org/tracker/DSA-2024-123",
Kind: "advisory",
Title: "Debian Security Advisory 2024-123"),
});
var document = new DocumentRecord(
Id: Guid.NewGuid(),
SourceName: DebianConnectorPlugin.SourceName,
Uri: "https://security-tracker.debian.org/tracker/DSA-2024-123",
FetchedAt: new DateTimeOffset(2024, 9, 1, 1, 0, 0, TimeSpan.Zero),
Sha256: "sha",
Status: "Fetched",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: null,
GridFsId: null);
Advisory advisory = DebianMapper.Map(dto, document, new DateTimeOffset(2024, 9, 1, 2, 0, 0, TimeSpan.Zero));
Assert.Equal("DSA-2024-123", advisory.AdvisoryKey);
Assert.Contains("CVE-2024-1000", advisory.Aliases);
Assert.Contains("CVE-2024-1001", advisory.Aliases);
var resolvedPackage = Assert.Single(advisory.AffectedPackages, p => p.Platform == "bullseye");
var range = Assert.Single(resolvedPackage.VersionRanges);
Assert.Equal("evr", range.RangeKind);
Assert.Equal("1:1.1.1n-0+deb11u2", range.IntroducedVersion);
Assert.Equal("1:1.1.1n-0+deb11u5", range.FixedVersion);
Assert.NotNull(range.Primitives);
var evr = range.Primitives!.Evr;
Assert.NotNull(evr);
Assert.NotNull(evr!.Introduced);
Assert.Equal(1, evr.Introduced!.Epoch);
Assert.Equal("1.1.1n", evr.Introduced.UpstreamVersion);
Assert.Equal("0+deb11u2", evr.Introduced.Revision);
using System;
using Xunit;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Distro.Debian;
using StellaOps.Concelier.Connector.Distro.Debian.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Distro.Debian.Tests;
public sealed class DebianMapperTests
{
[Fact]
public void Map_BuildsRangePrimitives_ForResolvedPackage()
{
var dto = new DebianAdvisoryDto(
AdvisoryId: "DSA-2024-123",
SourcePackage: "openssl",
Title: "Openssl security update",
Description: "Fixes multiple issues.",
CveIds: new[] { "CVE-2024-1000", "CVE-2024-1001" },
Packages: new[]
{
new DebianPackageStateDto(
Package: "openssl",
Release: "bullseye",
Status: "resolved",
IntroducedVersion: "1:1.1.1n-0+deb11u2",
FixedVersion: "1:1.1.1n-0+deb11u5",
LastAffectedVersion: null,
Published: new DateTimeOffset(2024, 9, 1, 0, 0, 0, TimeSpan.Zero)),
new DebianPackageStateDto(
Package: "openssl",
Release: "bookworm",
Status: "open",
IntroducedVersion: null,
FixedVersion: null,
LastAffectedVersion: null,
Published: null)
},
References: new[]
{
new DebianReferenceDto(
Url: "https://security-tracker.debian.org/tracker/DSA-2024-123",
Kind: "advisory",
Title: "Debian Security Advisory 2024-123"),
});
var document = new DocumentRecord(
Id: Guid.NewGuid(),
SourceName: DebianConnectorPlugin.SourceName,
Uri: "https://security-tracker.debian.org/tracker/DSA-2024-123",
FetchedAt: new DateTimeOffset(2024, 9, 1, 1, 0, 0, TimeSpan.Zero),
Sha256: "sha",
Status: "Fetched",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: null,
PayloadId: null);
Advisory advisory = DebianMapper.Map(dto, document, new DateTimeOffset(2024, 9, 1, 2, 0, 0, TimeSpan.Zero));
Assert.Equal("DSA-2024-123", advisory.AdvisoryKey);
Assert.Contains("CVE-2024-1000", advisory.Aliases);
Assert.Contains("CVE-2024-1001", advisory.Aliases);
var resolvedPackage = Assert.Single(advisory.AffectedPackages, p => p.Platform == "bullseye");
var range = Assert.Single(resolvedPackage.VersionRanges);
Assert.Equal("evr", range.RangeKind);
Assert.Equal("1:1.1.1n-0+deb11u2", range.IntroducedVersion);
Assert.Equal("1:1.1.1n-0+deb11u5", range.FixedVersion);
Assert.NotNull(range.Primitives);
var evr = range.Primitives!.Evr;
Assert.NotNull(evr);
Assert.NotNull(evr!.Introduced);
Assert.Equal(1, evr.Introduced!.Epoch);
Assert.Equal("1.1.1n", evr.Introduced.UpstreamVersion);
Assert.Equal("0+deb11u2", evr.Introduced.Revision);
Assert.NotNull(evr.Fixed);
Assert.Equal(1, evr.Fixed!.Epoch);
Assert.Equal("1.1.1n", evr.Fixed.UpstreamVersion);
@@ -94,5 +94,5 @@ public sealed class DebianMapperTests
var openPackage = Assert.Single(advisory.AffectedPackages, p => p.Platform == "bookworm");
Assert.Empty(openPackage.VersionRanges);
Assert.Empty(openPackage.NormalizedVersions);
}
}
}
}

View File

@@ -1,47 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Distro.Suse;
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using Xunit;
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
public sealed class SuseMapperTests
{
[Fact]
public void Map_BuildsNevraRangePrimitives()
{
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "Suse", "Fixtures", "suse-su-2025_0001-1.json"));
var dto = SuseCsafParser.Parse(json);
var document = new DocumentRecord(
Guid.NewGuid(),
SuseConnectorPlugin.SourceName,
"https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0001-1.json",
DateTimeOffset.UtcNow,
"sha256",
DocumentStatuses.PendingParse,
"application/json",
Headers: null,
Metadata: new Dictionary<string, string>(StringComparer.Ordinal)
{
["suse.id"] = dto.AdvisoryId
},
Etag: "adv-1",
LastModified: DateTimeOffset.UtcNow,
GridFsId: ObjectId.Empty);
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);
Assert.Equal(dto.AdvisoryId, mapped.AdvisoryKey);
var package = Assert.Single(mapped.AffectedPackages);
Assert.Equal(AffectedPackageTypes.Rpm, package.Type);
var range = Assert.Single(package.VersionRanges);
using System;
using System.Collections.Generic;
using System.IO;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Distro.Suse;
using StellaOps.Concelier.Connector.Distro.Suse.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using Xunit;
namespace StellaOps.Concelier.Connector.Distro.Suse.Tests;
public sealed class SuseMapperTests
{
[Fact]
public void Map_BuildsNevraRangePrimitives()
{
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "Suse", "Fixtures", "suse-su-2025_0001-1.json"));
var dto = SuseCsafParser.Parse(json);
var document = new DocumentRecord(
Guid.NewGuid(),
SuseConnectorPlugin.SourceName,
"https://ftp.suse.com/pub/projects/security/csaf/suse-su-2025_0001-1.json",
DateTimeOffset.UtcNow,
"sha256",
DocumentStatuses.PendingParse,
"application/json",
Headers: null,
Metadata: new Dictionary<string, string>(StringComparer.Ordinal)
{
["suse.id"] = dto.AdvisoryId
},
Etag: "adv-1",
LastModified: DateTimeOffset.UtcNow,
PayloadId: ObjectId.Empty);
var mapped = SuseMapper.Map(dto, document, DateTimeOffset.UtcNow);
Assert.Equal(dto.AdvisoryId, mapped.AdvisoryKey);
var package = Assert.Single(mapped.AffectedPackages);
Assert.Equal(AffectedPackageTypes.Rpm, package.Type);
var range = Assert.Single(package.VersionRanges);
Assert.Equal("nevra", range.RangeKind);
Assert.NotNull(range.Primitives);
Assert.NotNull(range.Primitives!.Nevra);

View File

@@ -1,94 +1,94 @@
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Ghsa.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
public sealed class GhsaConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
var recordedAt = new DateTimeOffset(2025, 3, 4, 8, 30, 0, TimeSpan.Zero);
var document = new DocumentRecord(
Id: Guid.Parse("2f5c4d67-fcac-4ec9-a8d4-8a9c5a6d0fc9"),
SourceName: GhsaConnectorPlugin.SourceName,
Uri: "https://github.com/advisories/GHSA-qqqq-wwww-eeee",
FetchedAt: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
Sha256: "sha256-ghsa-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-ghsa-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
GridFsId: null);
var dto = new GhsaRecordDto
{
GhsaId = "GHSA-qqqq-wwww-eeee",
Summary = "Container escape in conflict-package",
Description = "Container escape vulnerability allowing privilege escalation in conflict-package.",
Severity = "HIGH",
PublishedAt = new DateTimeOffset(2025, 2, 25, 0, 0, 0, TimeSpan.Zero),
UpdatedAt = new DateTimeOffset(2025, 3, 2, 12, 0, 0, TimeSpan.Zero),
Aliases = new[] { "GHSA-qqqq-wwww-eeee", "CVE-2025-4242" },
References = new[]
{
new GhsaReferenceDto
{
Url = "https://github.com/advisories/GHSA-qqqq-wwww-eeee",
Type = "ADVISORY"
},
new GhsaReferenceDto
{
Url = "https://github.com/conflict/package/releases/tag/v1.4.0",
Type = "FIX"
}
},
Affected = new[]
{
new GhsaAffectedDto
{
PackageName = "conflict/package",
Ecosystem = "npm",
VulnerableRange = "< 1.4.0",
PatchedVersion = "1.4.0"
}
},
Credits = new[]
{
new GhsaCreditDto
{
Type = "reporter",
Name = "security-researcher",
Login = "sec-researcher",
ProfileUrl = "https://github.com/sec-researcher"
},
new GhsaCreditDto
{
Type = "remediation_developer",
Name = "maintainer-team",
Login = "conflict-maintainer",
ProfileUrl = "https://github.com/conflict/package"
}
}
};
var advisory = GhsaMapper.Map(dto, document, recordedAt);
Assert.Equal("ghsa:severity/high", advisory.CanonicalMetricId);
Assert.True(advisory.CvssMetrics.IsEmpty);
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-ghsa.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-ghsa.canonical.actual.json");
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Ghsa.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Ghsa.Tests;
public sealed class GhsaConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
var recordedAt = new DateTimeOffset(2025, 3, 4, 8, 30, 0, TimeSpan.Zero);
var document = new DocumentRecord(
Id: Guid.Parse("2f5c4d67-fcac-4ec9-a8d4-8a9c5a6d0fc9"),
SourceName: GhsaConnectorPlugin.SourceName,
Uri: "https://github.com/advisories/GHSA-qqqq-wwww-eeee",
FetchedAt: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
Sha256: "sha256-ghsa-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-ghsa-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 3, 18, 0, 0, TimeSpan.Zero),
PayloadId: null);
var dto = new GhsaRecordDto
{
GhsaId = "GHSA-qqqq-wwww-eeee",
Summary = "Container escape in conflict-package",
Description = "Container escape vulnerability allowing privilege escalation in conflict-package.",
Severity = "HIGH",
PublishedAt = new DateTimeOffset(2025, 2, 25, 0, 0, 0, TimeSpan.Zero),
UpdatedAt = new DateTimeOffset(2025, 3, 2, 12, 0, 0, TimeSpan.Zero),
Aliases = new[] { "GHSA-qqqq-wwww-eeee", "CVE-2025-4242" },
References = new[]
{
new GhsaReferenceDto
{
Url = "https://github.com/advisories/GHSA-qqqq-wwww-eeee",
Type = "ADVISORY"
},
new GhsaReferenceDto
{
Url = "https://github.com/conflict/package/releases/tag/v1.4.0",
Type = "FIX"
}
},
Affected = new[]
{
new GhsaAffectedDto
{
PackageName = "conflict/package",
Ecosystem = "npm",
VulnerableRange = "< 1.4.0",
PatchedVersion = "1.4.0"
}
},
Credits = new[]
{
new GhsaCreditDto
{
Type = "reporter",
Name = "security-researcher",
Login = "sec-researcher",
ProfileUrl = "https://github.com/sec-researcher"
},
new GhsaCreditDto
{
Type = "remediation_developer",
Name = "maintainer-team",
Login = "conflict-maintainer",
ProfileUrl = "https://github.com/conflict/package"
}
}
};
var advisory = GhsaMapper.Map(dto, document, recordedAt);
Assert.Equal("ghsa:severity/high", advisory.CanonicalMetricId);
Assert.True(advisory.CvssMetrics.IsEmpty);
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-ghsa.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-ghsa.canonical.actual.json");
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}

View File

@@ -21,7 +21,7 @@ public sealed class GhsaMapperTests
Metadata: null,
Etag: "\"etag-ghsa-fallback\"",
LastModified: recordedAt.AddHours(-3),
GridFsId: null);
PayloadId: null);
var dto = new GhsaRecordDto
{

View File

@@ -1,103 +1,103 @@
using System.Text.Json;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Nvd.Tests;
public sealed class NvdConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
const string payload = """
{
"vulnerabilities": [
{
"cve": {
"id": "CVE-2025-4242",
"published": "2025-03-01T10:15:00Z",
"lastModified": "2025-03-03T09:45:00Z",
"descriptions": [
{ "lang": "en", "value": "NVD baseline summary for conflict-package allowing container escape." }
],
"references": [
{
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"source": "NVD",
"tags": ["Vendor Advisory"]
}
],
"weaknesses": [
{
"description": [
{ "lang": "en", "value": "CWE-269" }
]
}
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 9.8,
"baseSeverity": "CRITICAL"
},
"exploitabilityScore": 3.9,
"impactScore": 5.9
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"vulnerable": true,
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4"
}
]
}
]
}
}
}
]
}
""";
using var document = JsonDocument.Parse(payload);
var sourceDocument = new DocumentRecord(
Id: Guid.Parse("1a6a0700-2dd0-4f69-bb37-64ca77e51c91"),
SourceName: NvdConnectorPlugin.SourceName,
Uri: "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
FetchedAt: new DateTimeOffset(2025, 3, 3, 10, 0, 0, TimeSpan.Zero),
Sha256: "sha256-nvd-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-nvd-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 3, 9, 45, 0, TimeSpan.Zero),
GridFsId: null);
var advisories = NvdMapper.Map(document, sourceDocument, new DateTimeOffset(2025, 3, 4, 2, 0, 0, TimeSpan.Zero));
var advisory = Assert.Single(advisories);
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", "conflict-nvd.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", "conflict-nvd.canonical.actual.json");
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}
using System.Text.Json;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Nvd.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
namespace StellaOps.Concelier.Connector.Nvd.Tests;
public sealed class NvdConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
const string payload = """
{
"vulnerabilities": [
{
"cve": {
"id": "CVE-2025-4242",
"published": "2025-03-01T10:15:00Z",
"lastModified": "2025-03-03T09:45:00Z",
"descriptions": [
{ "lang": "en", "value": "NVD baseline summary for conflict-package allowing container escape." }
],
"references": [
{
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4242",
"source": "NVD",
"tags": ["Vendor Advisory"]
}
],
"weaknesses": [
{
"description": [
{ "lang": "en", "value": "CWE-269" }
]
}
],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"baseScore": 9.8,
"baseSeverity": "CRITICAL"
},
"exploitabilityScore": 3.9,
"impactScore": 5.9
}
]
},
"configurations": {
"nodes": [
{
"cpeMatch": [
{
"criteria": "cpe:2.3:a:conflict:package:1.0:*:*:*:*:*:*:*",
"vulnerable": true,
"versionStartIncluding": "1.0",
"versionEndExcluding": "1.4"
}
]
}
]
}
}
}
]
}
""";
using var document = JsonDocument.Parse(payload);
var sourceDocument = new DocumentRecord(
Id: Guid.Parse("1a6a0700-2dd0-4f69-bb37-64ca77e51c91"),
SourceName: NvdConnectorPlugin.SourceName,
Uri: "https://services.nvd.nist.gov/rest/json/cve/2.0?cveId=CVE-2025-4242",
FetchedAt: new DateTimeOffset(2025, 3, 3, 10, 0, 0, TimeSpan.Zero),
Sha256: "sha256-nvd-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-nvd-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 3, 9, 45, 0, TimeSpan.Zero),
PayloadId: null);
var advisories = NvdMapper.Map(document, sourceDocument, new DateTimeOffset(2025, 3, 4, 2, 0, 0, TimeSpan.Zero));
var advisory = Assert.Single(advisories);
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", "conflict-nvd.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Nvd", "Fixtures", "conflict-nvd.canonical.actual.json");
Directory.CreateDirectory(Path.GetDirectoryName(actualPath)!);
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}

View File

@@ -1,118 +1,118 @@
using System.Text.Json;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
namespace StellaOps.Concelier.Connector.Osv.Tests;
public sealed class OsvConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
using var databaseSpecificDoc = JsonDocument.Parse("""{"severity":"medium"}""");
var dto = new OsvVulnerabilityDto
{
Id = "OSV-2025-4242",
Summary = "Container escape for conflict-package",
Details = "OSV captures the latest container escape details including patched version metadata.",
Aliases = new[] { "CVE-2025-4242", "GHSA-qqqq-wwww-eeee" },
Published = new DateTimeOffset(2025, 2, 28, 0, 0, 0, TimeSpan.Zero),
Modified = new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
Severity = new[]
{
new OsvSeverityDto
{
Type = "CVSS_V3",
Score = "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L"
}
},
References = new[]
{
new OsvReferenceDto
{
Type = "ADVISORY",
Url = "https://osv.dev/vulnerability/OSV-2025-4242"
},
new OsvReferenceDto
{
Type = "FIX",
Url = "https://github.com/conflict/package/commit/abcdef1234567890"
}
},
Credits = new[]
{
new OsvCreditDto
{
Name = "osv-reporter",
Type = "reporter",
Contact = new[] { "mailto:osv-reporter@example.com" }
}
},
Affected = new[]
{
new OsvAffectedPackageDto
{
Package = new OsvPackageDto
{
Ecosystem = "npm",
Name = "conflict/package"
},
Ranges = new[]
{
new OsvRangeDto
{
Type = "SEMVER",
Events = new[]
{
new OsvEventDto { Introduced = "1.0.0" },
new OsvEventDto { LastAffected = "1.4.2" },
new OsvEventDto { Fixed = "1.5.0" }
}
}
}
}
},
DatabaseSpecific = databaseSpecificDoc.RootElement.Clone()
};
var document = new DocumentRecord(
Id: Guid.Parse("8dd2b0fe-a5f5-4b3b-9f5c-0f3aad6fb6ce"),
SourceName: OsvConnectorPlugin.SourceName,
Uri: "https://api.osv.dev/v1/vulns/OSV-2025-4242",
FetchedAt: new DateTimeOffset(2025, 3, 6, 11, 30, 0, TimeSpan.Zero),
Sha256: "sha256-osv-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-osv-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
GridFsId: null);
var dtoRecord = new DtoRecord(
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),
DocumentId: document.Id,
SourceName: OsvConnectorPlugin.SourceName,
SchemaVersion: "osv.v1",
Payload: new BsonDocument("id", dto.Id),
ValidatedAt: new DateTimeOffset(2025, 3, 6, 12, 5, 0, TimeSpan.Zero));
var advisory = OsvMapper.Map(dto, document, dtoRecord, "npm");
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-osv.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-osv.canonical.actual.json");
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}
using System.Text.Json;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Osv.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
namespace StellaOps.Concelier.Connector.Osv.Tests;
public sealed class OsvConflictFixtureTests
{
[Fact]
public void ConflictFixture_MatchesSnapshot()
{
using var databaseSpecificDoc = JsonDocument.Parse("""{"severity":"medium"}""");
var dto = new OsvVulnerabilityDto
{
Id = "OSV-2025-4242",
Summary = "Container escape for conflict-package",
Details = "OSV captures the latest container escape details including patched version metadata.",
Aliases = new[] { "CVE-2025-4242", "GHSA-qqqq-wwww-eeee" },
Published = new DateTimeOffset(2025, 2, 28, 0, 0, 0, TimeSpan.Zero),
Modified = new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
Severity = new[]
{
new OsvSeverityDto
{
Type = "CVSS_V3",
Score = "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:L"
}
},
References = new[]
{
new OsvReferenceDto
{
Type = "ADVISORY",
Url = "https://osv.dev/vulnerability/OSV-2025-4242"
},
new OsvReferenceDto
{
Type = "FIX",
Url = "https://github.com/conflict/package/commit/abcdef1234567890"
}
},
Credits = new[]
{
new OsvCreditDto
{
Name = "osv-reporter",
Type = "reporter",
Contact = new[] { "mailto:osv-reporter@example.com" }
}
},
Affected = new[]
{
new OsvAffectedPackageDto
{
Package = new OsvPackageDto
{
Ecosystem = "npm",
Name = "conflict/package"
},
Ranges = new[]
{
new OsvRangeDto
{
Type = "SEMVER",
Events = new[]
{
new OsvEventDto { Introduced = "1.0.0" },
new OsvEventDto { LastAffected = "1.4.2" },
new OsvEventDto { Fixed = "1.5.0" }
}
}
}
}
},
DatabaseSpecific = databaseSpecificDoc.RootElement.Clone()
};
var document = new DocumentRecord(
Id: Guid.Parse("8dd2b0fe-a5f5-4b3b-9f5c-0f3aad6fb6ce"),
SourceName: OsvConnectorPlugin.SourceName,
Uri: "https://api.osv.dev/v1/vulns/OSV-2025-4242",
FetchedAt: new DateTimeOffset(2025, 3, 6, 11, 30, 0, TimeSpan.Zero),
Sha256: "sha256-osv-conflict-fixture",
Status: "completed",
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: "\"etag-osv-conflict\"",
LastModified: new DateTimeOffset(2025, 3, 6, 12, 0, 0, TimeSpan.Zero),
PayloadId: null);
var dtoRecord = new DtoRecord(
Id: Guid.Parse("6f7d5ce7-cb47-40a5-8b41-8ad022b5fd5c"),
DocumentId: document.Id,
SourceName: OsvConnectorPlugin.SourceName,
SchemaVersion: "osv.v1",
Payload: new BsonDocument("id", dto.Id),
ValidatedAt: new DateTimeOffset(2025, 3, 6, 12, 5, 0, TimeSpan.Zero));
var advisory = OsvMapper.Map(dto, document, dtoRecord, "npm");
var snapshot = SnapshotSerializer.ToSnapshot(advisory).Replace("\r\n", "\n").TrimEnd();
var expectedPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-osv.canonical.json");
var expected = File.ReadAllText(expectedPath).Replace("\r\n", "\n").TrimEnd();
if (!string.Equals(expected, snapshot, StringComparison.Ordinal))
{
var actualPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "conflict-osv.canonical.actual.json");
File.WriteAllText(actualPath, snapshot);
}
Assert.Equal(expected, snapshot);
}
}

View File

@@ -1,463 +1,463 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Common.Fetch;
using StellaOps.Concelier.Connector.Common.Testing;
using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
using StellaOps.Concelier.Connector.StellaOpsMirror.Settings;
using StellaOps.Concelier.Storage.Mongo;
using StellaOps.Concelier.Storage.Mongo.Advisories;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using StellaOps.Concelier.Testing;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
[Collection("mongo-fixture")]
public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
{
private readonly MongoIntegrationFixture _fixture;
private readonly CannedHttpMessageHandler _handler;
public StellaOpsMirrorConnectorTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
_handler = new CannedHttpMessageHandler();
}
[Fact]
public async Task FetchAsync_PersistsMirrorArtifacts()
{
var manifestContent = "{\"domain\":\"primary\",\"files\":[]}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0001\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestContent), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: false);
await using var provider = await BuildServiceProviderAsync();
SeedResponses(index, manifestContent, bundleContent, signature: null);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var manifestUri = "https://mirror.test/mirror/primary/manifest.json";
var bundleUri = "https://mirror.test/mirror/primary/bundle.json";
var manifestDocument = await documentStore.FindBySourceAndUriAsync(StellaOpsMirrorConnector.Source, manifestUri, CancellationToken.None);
Assert.NotNull(manifestDocument);
Assert.Equal(DocumentStatuses.Mapped, manifestDocument!.Status);
Assert.Equal(NormalizeDigest(manifestDigest), manifestDocument.Sha256);
var bundleDocument = await documentStore.FindBySourceAndUriAsync(StellaOpsMirrorConnector.Source, bundleUri, CancellationToken.None);
Assert.NotNull(bundleDocument);
Assert.Equal(DocumentStatuses.PendingParse, bundleDocument!.Status);
Assert.Equal(NormalizeDigest(bundleDigest), bundleDocument.Sha256);
var rawStorage = provider.GetRequiredService<RawDocumentStorage>();
Assert.NotNull(manifestDocument.GridFsId);
Assert.NotNull(bundleDocument.GridFsId);
var manifestBytes = await rawStorage.DownloadAsync(manifestDocument.GridFsId!.Value, CancellationToken.None);
var bundleBytes = await rawStorage.DownloadAsync(bundleDocument.GridFsId!.Value, CancellationToken.None);
Assert.Equal(manifestContent, Encoding.UTF8.GetString(manifestBytes));
Assert.Equal(bundleContent, Encoding.UTF8.GetString(bundleBytes));
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
var cursorDocument = state!.Cursor ?? new BsonDocument();
var digestValue = cursorDocument.TryGetValue("bundleDigest", out var digestBson) ? digestBson.AsString : string.Empty;
Assert.Equal(NormalizeDigest(bundleDigest), NormalizeDigest(digestValue));
var pendingDocumentsArray = cursorDocument.TryGetValue("pendingDocuments", out var pendingDocsBson) && pendingDocsBson is BsonArray pendingArray
? pendingArray
: new BsonArray();
Assert.Single(pendingDocumentsArray);
var pendingDocumentId = Guid.Parse(pendingDocumentsArray[0].AsString);
Assert.Equal(bundleDocument.Id, pendingDocumentId);
var pendingMappingsArray = cursorDocument.TryGetValue("pendingMappings", out var pendingMappingsBson) && pendingMappingsBson is BsonArray mappingsArray
? mappingsArray
: new BsonArray();
Assert.Empty(pendingMappingsArray);
}
[Fact]
public async Task FetchAsync_TamperedSignatureThrows()
{
var manifestContent = "{\"domain\":\"primary\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0002\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestContent), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: true);
await using var provider = await BuildServiceProviderAsync(options =>
{
options.Signature.Enabled = true;
options.Signature.KeyId = "mirror-key";
options.Signature.Provider = "default";
});
var defaultProvider = provider.GetRequiredService<DefaultCryptoProvider>();
var signingKey = CreateSigningKey("mirror-key");
defaultProvider.UpsertSigningKey(signingKey);
var (signatureValue, _) = CreateDetachedJws(signingKey, bundleContent);
// Tamper with signature so verification fails.
var tamperedSignature = signatureValue.Replace('a', 'b');
SeedResponses(index, manifestContent, bundleContent, tamperedSignature);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.FailCount >= 1);
Assert.False(state.Cursor.TryGetValue("bundleDigest", out _));
}
[Fact]
public async Task FetchAsync_SignatureKeyMismatchThrows()
{
var manifestContent = "{\"domain\":\"primary\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0003\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(
manifestDigest,
Encoding.UTF8.GetByteCount(manifestContent),
bundleDigest,
Encoding.UTF8.GetByteCount(bundleContent),
includeSignature: true,
signatureKeyId: "unexpected-key",
signatureProvider: "default");
var signingKey = CreateSigningKey("unexpected-key");
var (signatureValue, _) = CreateDetachedJws(signingKey, bundleContent);
await using var provider = await BuildServiceProviderAsync(options =>
{
options.Signature.Enabled = true;
options.Signature.KeyId = "mirror-key";
options.Signature.Provider = "default";
});
SeedResponses(index, manifestContent, bundleContent, signatureValue);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
}
[Fact]
public async Task FetchAsync_VerifiesSignatureUsingFallbackPublicKey()
{
var manifestContent = "{\"domain\":\"primary\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0004\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestContent), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: true);
var signingKey = CreateSigningKey("mirror-key");
var (signatureValue, _) = CreateDetachedJws(signingKey, bundleContent);
var publicKeyPath = WritePublicKeyPem(signingKey);
await using var provider = await BuildServiceProviderAsync(options =>
{
options.Signature.Enabled = true;
options.Signature.KeyId = "mirror-key";
options.Signature.Provider = "default";
options.Signature.PublicKeyPath = publicKeyPath;
});
try
{
SeedResponses(index, manifestContent, bundleContent, signatureValue);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal(0, state!.FailCount);
}
finally
{
if (File.Exists(publicKeyPath))
{
File.Delete(publicKeyPath);
}
}
}
[Fact]
public async Task FetchAsync_DigestMismatchMarksFailure()
{
var manifestExpected = "{\"domain\":\"primary\"}";
var manifestTampered = "{\"domain\":\"tampered\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0005\"}]}";
var manifestDigest = ComputeDigest(manifestExpected);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestExpected), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: false);
await using var provider = await BuildServiceProviderAsync();
SeedResponses(index, manifestTampered, bundleContent, signature: null);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor ?? new BsonDocument();
Assert.True(state.FailCount >= 1);
Assert.False(cursor.Contains("bundleDigest"));
}
[Fact]
public void ParseAndMap_PersistAdvisoriesFromBundle()
{
var bundleDocument = SampleData.CreateBundle();
var bundleJson = CanonicalJsonSerializer.SerializeIndented(bundleDocument);
var normalizedFixture = FixtureLoader.Read(SampleData.BundleFixture).TrimEnd();
Assert.Equal(normalizedFixture, FixtureLoader.Normalize(bundleJson).TrimEnd());
var advisories = MirrorAdvisoryMapper.Map(bundleDocument);
Assert.Single(advisories);
var advisory = advisories[0];
var expectedAdvisoryJson = FixtureLoader.Read(SampleData.AdvisoryFixture).TrimEnd();
var mappedJson = CanonicalJsonSerializer.SerializeIndented(advisory);
Assert.Equal(expectedAdvisoryJson, FixtureLoader.Normalize(mappedJson).TrimEnd());
// AdvisoryStore integration validated elsewhere; ensure canonical serialization is stable.
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_handler.Clear();
return Task.CompletedTask;
}
private async Task<ServiceProvider> BuildServiceProviderAsync(Action<StellaOpsMirrorConnectorOptions>? configureOptions = null)
{
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
_handler.Clear();
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton(_handler);
services.AddSingleton(TimeProvider.System);
services.AddMongoStorage(options =>
{
options.ConnectionString = _fixture.Runner.ConnectionString;
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
[Collection("mongo-fixture")]
public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
{
private readonly MongoIntegrationFixture _fixture;
private readonly CannedHttpMessageHandler _handler;
public StellaOpsMirrorConnectorTests(MongoIntegrationFixture fixture)
{
_fixture = fixture;
_handler = new CannedHttpMessageHandler();
}
[Fact]
public async Task FetchAsync_PersistsMirrorArtifacts()
{
var manifestContent = "{\"domain\":\"primary\",\"files\":[]}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0001\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestContent), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: false);
await using var provider = await BuildServiceProviderAsync();
SeedResponses(index, manifestContent, bundleContent, signature: null);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var documentStore = provider.GetRequiredService<IDocumentStore>();
var manifestUri = "https://mirror.test/mirror/primary/manifest.json";
var bundleUri = "https://mirror.test/mirror/primary/bundle.json";
var manifestDocument = await documentStore.FindBySourceAndUriAsync(StellaOpsMirrorConnector.Source, manifestUri, CancellationToken.None);
Assert.NotNull(manifestDocument);
Assert.Equal(DocumentStatuses.Mapped, manifestDocument!.Status);
Assert.Equal(NormalizeDigest(manifestDigest), manifestDocument.Sha256);
var bundleDocument = await documentStore.FindBySourceAndUriAsync(StellaOpsMirrorConnector.Source, bundleUri, CancellationToken.None);
Assert.NotNull(bundleDocument);
Assert.Equal(DocumentStatuses.PendingParse, bundleDocument!.Status);
Assert.Equal(NormalizeDigest(bundleDigest), bundleDocument.Sha256);
var rawStorage = provider.GetRequiredService<RawDocumentStorage>();
Assert.NotNull(manifestDocument.PayloadId);
Assert.NotNull(bundleDocument.PayloadId);
var manifestBytes = await rawStorage.DownloadAsync(manifestDocument.PayloadId!.Value, CancellationToken.None);
var bundleBytes = await rawStorage.DownloadAsync(bundleDocument.PayloadId!.Value, CancellationToken.None);
Assert.Equal(manifestContent, Encoding.UTF8.GetString(manifestBytes));
Assert.Equal(bundleContent, Encoding.UTF8.GetString(bundleBytes));
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
var cursorDocument = state!.Cursor ?? new BsonDocument();
var digestValue = cursorDocument.TryGetValue("bundleDigest", out var digestBson) ? digestBson.AsString : string.Empty;
Assert.Equal(NormalizeDigest(bundleDigest), NormalizeDigest(digestValue));
var pendingDocumentsArray = cursorDocument.TryGetValue("pendingDocuments", out var pendingDocsBson) && pendingDocsBson is BsonArray pendingArray
? pendingArray
: new BsonArray();
Assert.Single(pendingDocumentsArray);
var pendingDocumentId = Guid.Parse(pendingDocumentsArray[0].AsString);
Assert.Equal(bundleDocument.Id, pendingDocumentId);
var pendingMappingsArray = cursorDocument.TryGetValue("pendingMappings", out var pendingMappingsBson) && pendingMappingsBson is BsonArray mappingsArray
? mappingsArray
: new BsonArray();
Assert.Empty(pendingMappingsArray);
}
[Fact]
public async Task FetchAsync_TamperedSignatureThrows()
{
var manifestContent = "{\"domain\":\"primary\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0002\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestContent), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: true);
await using var provider = await BuildServiceProviderAsync(options =>
{
options.Signature.Enabled = true;
options.Signature.KeyId = "mirror-key";
options.Signature.Provider = "default";
});
var defaultProvider = provider.GetRequiredService<DefaultCryptoProvider>();
var signingKey = CreateSigningKey("mirror-key");
defaultProvider.UpsertSigningKey(signingKey);
var (signatureValue, _) = CreateDetachedJws(signingKey, bundleContent);
// Tamper with signature so verification fails.
var tamperedSignature = signatureValue.Replace('a', 'b');
SeedResponses(index, manifestContent, bundleContent, tamperedSignature);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
Assert.True(state!.FailCount >= 1);
Assert.False(state.Cursor.TryGetValue("bundleDigest", out _));
}
[Fact]
public async Task FetchAsync_SignatureKeyMismatchThrows()
{
var manifestContent = "{\"domain\":\"primary\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0003\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(
manifestDigest,
Encoding.UTF8.GetByteCount(manifestContent),
bundleDigest,
Encoding.UTF8.GetByteCount(bundleContent),
includeSignature: true,
signatureKeyId: "unexpected-key",
signatureProvider: "default");
var signingKey = CreateSigningKey("unexpected-key");
var (signatureValue, _) = CreateDetachedJws(signingKey, bundleContent);
await using var provider = await BuildServiceProviderAsync(options =>
{
options.Signature.Enabled = true;
options.Signature.KeyId = "mirror-key";
options.Signature.Provider = "default";
});
SeedResponses(index, manifestContent, bundleContent, signatureValue);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
}
[Fact]
public async Task FetchAsync_VerifiesSignatureUsingFallbackPublicKey()
{
var manifestContent = "{\"domain\":\"primary\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0004\"}]}";
var manifestDigest = ComputeDigest(manifestContent);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestContent), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: true);
var signingKey = CreateSigningKey("mirror-key");
var (signatureValue, _) = CreateDetachedJws(signingKey, bundleContent);
var publicKeyPath = WritePublicKeyPem(signingKey);
await using var provider = await BuildServiceProviderAsync(options =>
{
options.Signature.Enabled = true;
options.Signature.KeyId = "mirror-key";
options.Signature.Provider = "default";
options.Signature.PublicKeyPath = publicKeyPath;
});
try
{
SeedResponses(index, manifestContent, bundleContent, signatureValue);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await connector.FetchAsync(provider, CancellationToken.None);
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
Assert.Equal(0, state!.FailCount);
}
finally
{
if (File.Exists(publicKeyPath))
{
File.Delete(publicKeyPath);
}
}
}
[Fact]
public async Task FetchAsync_DigestMismatchMarksFailure()
{
var manifestExpected = "{\"domain\":\"primary\"}";
var manifestTampered = "{\"domain\":\"tampered\"}";
var bundleContent = "{\"advisories\":[{\"id\":\"CVE-2025-0005\"}]}";
var manifestDigest = ComputeDigest(manifestExpected);
var bundleDigest = ComputeDigest(bundleContent);
var index = BuildIndex(manifestDigest, Encoding.UTF8.GetByteCount(manifestExpected), bundleDigest, Encoding.UTF8.GetByteCount(bundleContent), includeSignature: false);
await using var provider = await BuildServiceProviderAsync();
SeedResponses(index, manifestTampered, bundleContent, signature: null);
var connector = provider.GetRequiredService<StellaOpsMirrorConnector>();
await Assert.ThrowsAsync<InvalidOperationException>(() => connector.FetchAsync(provider, CancellationToken.None));
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
Assert.NotNull(state);
var cursor = state!.Cursor ?? new BsonDocument();
Assert.True(state.FailCount >= 1);
Assert.False(cursor.Contains("bundleDigest"));
}
[Fact]
public void ParseAndMap_PersistAdvisoriesFromBundle()
{
var bundleDocument = SampleData.CreateBundle();
var bundleJson = CanonicalJsonSerializer.SerializeIndented(bundleDocument);
var normalizedFixture = FixtureLoader.Read(SampleData.BundleFixture).TrimEnd();
Assert.Equal(normalizedFixture, FixtureLoader.Normalize(bundleJson).TrimEnd());
var advisories = MirrorAdvisoryMapper.Map(bundleDocument);
Assert.Single(advisories);
var advisory = advisories[0];
var expectedAdvisoryJson = FixtureLoader.Read(SampleData.AdvisoryFixture).TrimEnd();
var mappedJson = CanonicalJsonSerializer.SerializeIndented(advisory);
Assert.Equal(expectedAdvisoryJson, FixtureLoader.Normalize(mappedJson).TrimEnd());
// AdvisoryStore integration validated elsewhere; ensure canonical serialization is stable.
}
public Task InitializeAsync() => Task.CompletedTask;
public Task DisposeAsync()
{
_handler.Clear();
return Task.CompletedTask;
}
private async Task<ServiceProvider> BuildServiceProviderAsync(Action<StellaOpsMirrorConnectorOptions>? configureOptions = null)
{
await _fixture.Client.DropDatabaseAsync(_fixture.Database.DatabaseNamespace.DatabaseName);
_handler.Clear();
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
services.AddSingleton(_handler);
services.AddSingleton(TimeProvider.System);
services.AddMongoStorage(options =>
{
options.ConnectionString = _fixture.Runner.ConnectionString;
options.DatabaseName = _fixture.Database.DatabaseNamespace.DatabaseName;
options.CommandTimeout = TimeSpan.FromSeconds(5);
});
services.AddStellaOpsCrypto();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["concelier:sources:stellaopsMirror:baseAddress"] = "https://mirror.test/",
["concelier:sources:stellaopsMirror:domainId"] = "primary",
["concelier:sources:stellaopsMirror:indexPath"] = "/concelier/exports/index.json",
})
.Build();
var routine = new StellaOpsMirrorDependencyInjectionRoutine();
routine.Register(services, configuration);
if (configureOptions is not null)
{
services.PostConfigure(configureOptions);
}
services.Configure<HttpClientFactoryOptions>("stellaops-mirror", builder =>
{
builder.HttpMessageHandlerBuilderActions.Add(options =>
{
options.PrimaryHandler = _handler;
});
});
var provider = services.BuildServiceProvider();
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
await bootstrapper.InitializeAsync(CancellationToken.None);
return provider;
}
private void SeedResponses(string indexJson, string manifestContent, string bundleContent, string? signature)
{
var baseUri = new Uri("https://mirror.test");
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "/concelier/exports/index.json"), () => CreateJsonResponse(indexJson));
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "mirror/primary/manifest.json"), () => CreateJsonResponse(manifestContent));
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "mirror/primary/bundle.json"), () => CreateJsonResponse(bundleContent));
if (signature is not null)
{
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "mirror/primary/bundle.json.jws"), () => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(signature, Encoding.UTF8, "application/jose+json"),
});
}
}
private static HttpResponseMessage CreateJsonResponse(string content)
=> new(HttpStatusCode.OK)
{
Content = new StringContent(content, Encoding.UTF8, "application/json"),
};
private static string BuildIndex(
string manifestDigest,
int manifestBytes,
string bundleDigest,
int bundleBytes,
bool includeSignature,
string signatureKeyId = "mirror-key",
string signatureProvider = "default")
{
var index = new
{
schemaVersion = 1,
generatedAt = new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
targetRepository = "repo",
domains = new[]
{
new
{
domainId = "primary",
displayName = "Primary",
advisoryCount = 1,
manifest = new
{
path = "mirror/primary/manifest.json",
sizeBytes = manifestBytes,
digest = manifestDigest,
signature = (object?)null,
},
bundle = new
{
path = "mirror/primary/bundle.json",
sizeBytes = bundleBytes,
digest = bundleDigest,
signature = includeSignature
? new
{
path = "mirror/primary/bundle.json.jws",
algorithm = "ES256",
keyId = signatureKeyId,
provider = signatureProvider,
signedAt = new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
}
: null,
},
sources = Array.Empty<object>(),
}
}
};
return JsonSerializer.Serialize(index, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
});
}
private static string ComputeDigest(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}
private static string NormalizeDigest(string digest)
=> digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ? digest[7..] : digest;
private static CryptoSigningKey CreateSigningKey(string keyId)
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
return new CryptoSigningKey(new CryptoKeyReference(keyId), SignatureAlgorithms.Es256, in parameters, DateTimeOffset.UtcNow);
}
private static string WritePublicKeyPem(CryptoSigningKey signingKey)
{
ArgumentNullException.ThrowIfNull(signingKey);
var path = Path.Combine(Path.GetTempPath(), $"stellaops-mirror-{Guid.NewGuid():N}.pem");
using var ecdsa = ECDsa.Create(signingKey.PublicParameters);
var publicKeyInfo = ecdsa.ExportSubjectPublicKeyInfo();
var pem = PemEncoding.Write("PUBLIC KEY", publicKeyInfo);
File.WriteAllText(path, pem);
return path;
}
private static (string Signature, DateTimeOffset SignedAt) CreateDetachedJws(CryptoSigningKey signingKey, string payload)
{
var provider = new DefaultCryptoProvider();
provider.UpsertSigningKey(signingKey);
var signer = provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference);
var header = new Dictionary<string, object?>
{
["alg"] = SignatureAlgorithms.Es256,
["kid"] = signingKey.Reference.KeyId,
["provider"] = provider.Name,
["typ"] = "application/vnd.stellaops.concelier.mirror-bundle+jws",
["b64"] = false,
["crit"] = new[] { "b64" }
};
var headerJson = JsonSerializer.Serialize(header);
var encodedHeader = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(headerJson);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var signingInput = BuildSigningInput(encodedHeader, payloadBytes);
var signatureBytes = signer.SignAsync(signingInput, CancellationToken.None).GetAwaiter().GetResult();
var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signatureBytes);
return (string.Concat(encodedHeader, "..", encodedSignature), DateTimeOffset.UtcNow);
}
private static ReadOnlyMemory<byte> BuildSigningInput(string encodedHeader, ReadOnlySpan<byte> payload)
{
var headerBytes = Encoding.ASCII.GetBytes(encodedHeader);
var buffer = new byte[headerBytes.Length + 1 + payload.Length];
headerBytes.CopyTo(buffer, 0);
buffer[headerBytes.Length] = (byte)'.';
payload.CopyTo(buffer.AsSpan(headerBytes.Length + 1));
return buffer;
}
}
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["concelier:sources:stellaopsMirror:baseAddress"] = "https://mirror.test/",
["concelier:sources:stellaopsMirror:domainId"] = "primary",
["concelier:sources:stellaopsMirror:indexPath"] = "/concelier/exports/index.json",
})
.Build();
var routine = new StellaOpsMirrorDependencyInjectionRoutine();
routine.Register(services, configuration);
if (configureOptions is not null)
{
services.PostConfigure(configureOptions);
}
services.Configure<HttpClientFactoryOptions>("stellaops-mirror", builder =>
{
builder.HttpMessageHandlerBuilderActions.Add(options =>
{
options.PrimaryHandler = _handler;
});
});
var provider = services.BuildServiceProvider();
var bootstrapper = provider.GetRequiredService<MongoBootstrapper>();
await bootstrapper.InitializeAsync(CancellationToken.None);
return provider;
}
private void SeedResponses(string indexJson, string manifestContent, string bundleContent, string? signature)
{
var baseUri = new Uri("https://mirror.test");
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "/concelier/exports/index.json"), () => CreateJsonResponse(indexJson));
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "mirror/primary/manifest.json"), () => CreateJsonResponse(manifestContent));
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "mirror/primary/bundle.json"), () => CreateJsonResponse(bundleContent));
if (signature is not null)
{
_handler.AddResponse(HttpMethod.Get, new Uri(baseUri, "mirror/primary/bundle.json.jws"), () => new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(signature, Encoding.UTF8, "application/jose+json"),
});
}
}
private static HttpResponseMessage CreateJsonResponse(string content)
=> new(HttpStatusCode.OK)
{
Content = new StringContent(content, Encoding.UTF8, "application/json"),
};
private static string BuildIndex(
string manifestDigest,
int manifestBytes,
string bundleDigest,
int bundleBytes,
bool includeSignature,
string signatureKeyId = "mirror-key",
string signatureProvider = "default")
{
var index = new
{
schemaVersion = 1,
generatedAt = new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
targetRepository = "repo",
domains = new[]
{
new
{
domainId = "primary",
displayName = "Primary",
advisoryCount = 1,
manifest = new
{
path = "mirror/primary/manifest.json",
sizeBytes = manifestBytes,
digest = manifestDigest,
signature = (object?)null,
},
bundle = new
{
path = "mirror/primary/bundle.json",
sizeBytes = bundleBytes,
digest = bundleDigest,
signature = includeSignature
? new
{
path = "mirror/primary/bundle.json.jws",
algorithm = "ES256",
keyId = signatureKeyId,
provider = signatureProvider,
signedAt = new DateTimeOffset(2025, 10, 19, 12, 0, 0, TimeSpan.Zero),
}
: null,
},
sources = Array.Empty<object>(),
}
}
};
return JsonSerializer.Serialize(index, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false,
});
}
private static string ComputeDigest(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
var hash = SHA256.HashData(bytes);
return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant();
}
private static string NormalizeDigest(string digest)
=> digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ? digest[7..] : digest;
private static CryptoSigningKey CreateSigningKey(string keyId)
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
return new CryptoSigningKey(new CryptoKeyReference(keyId), SignatureAlgorithms.Es256, in parameters, DateTimeOffset.UtcNow);
}
private static string WritePublicKeyPem(CryptoSigningKey signingKey)
{
ArgumentNullException.ThrowIfNull(signingKey);
var path = Path.Combine(Path.GetTempPath(), $"stellaops-mirror-{Guid.NewGuid():N}.pem");
using var ecdsa = ECDsa.Create(signingKey.PublicParameters);
var publicKeyInfo = ecdsa.ExportSubjectPublicKeyInfo();
var pem = PemEncoding.Write("PUBLIC KEY", publicKeyInfo);
File.WriteAllText(path, pem);
return path;
}
private static (string Signature, DateTimeOffset SignedAt) CreateDetachedJws(CryptoSigningKey signingKey, string payload)
{
var provider = new DefaultCryptoProvider();
provider.UpsertSigningKey(signingKey);
var signer = provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference);
var header = new Dictionary<string, object?>
{
["alg"] = SignatureAlgorithms.Es256,
["kid"] = signingKey.Reference.KeyId,
["provider"] = provider.Name,
["typ"] = "application/vnd.stellaops.concelier.mirror-bundle+jws",
["b64"] = false,
["crit"] = new[] { "b64" }
};
var headerJson = JsonSerializer.Serialize(header);
var encodedHeader = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(headerJson);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var signingInput = BuildSigningInput(encodedHeader, payloadBytes);
var signatureBytes = signer.SignAsync(signingInput, CancellationToken.None).GetAwaiter().GetResult();
var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signatureBytes);
return (string.Concat(encodedHeader, "..", encodedSignature), DateTimeOffset.UtcNow);
}
private static ReadOnlyMemory<byte> BuildSigningInput(string encodedHeader, ReadOnlySpan<byte> payload)
{
var headerBytes = Encoding.ASCII.GetBytes(encodedHeader);
var buffer = new byte[headerBytes.Length + 1 + payload.Length];
headerBytes.CopyTo(buffer, 0);
buffer[headerBytes.Length] = (byte)'.';
payload.CopyTo(buffer.AsSpan(headerBytes.Length + 1));
return buffer;
}
}

View File

@@ -1,36 +1,36 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Vndr.Cisco;
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using Xunit;
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests;
public sealed class CiscoMapperTests
{
[Fact]
public void Map_ProducesCanonicalAdvisory()
{
var published = new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero);
var updated = published.AddDays(1);
using FluentAssertions;
using MongoDB.Bson;
using StellaOps.Concelier.Models;
using StellaOps.Concelier.Connector.Common;
using StellaOps.Concelier.Connector.Vndr.Cisco;
using StellaOps.Concelier.Connector.Vndr.Cisco.Internal;
using StellaOps.Concelier.Storage.Mongo.Documents;
using StellaOps.Concelier.Storage.Mongo.Dtos;
using Xunit;
namespace StellaOps.Concelier.Connector.Vndr.Cisco.Tests;
public sealed class CiscoMapperTests
{
[Fact]
public void Map_ProducesCanonicalAdvisory()
{
var published = new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero);
var updated = published.AddDays(1);
var dto = new CiscoAdvisoryDto(
AdvisoryId: "CISCO-SA-TEST",
Title: "Test Advisory",
Summary: "Sample summary",
Severity: "High",
Published: published,
Updated: updated,
PublicationUrl: "https://example.com/advisory",
CsafUrl: "https://sec.cloudapps.cisco.com/csaf/test.json",
CvrfUrl: "https://example.com/cvrf.xml",
Published: published,
Updated: updated,
PublicationUrl: "https://example.com/advisory",
CsafUrl: "https://sec.cloudapps.cisco.com/csaf/test.json",
CvrfUrl: "https://example.com/cvrf.xml",
CvssBaseScore: 9.8,
Cves: new List<string> { "CVE-2024-0001" },
BugIds: new List<string> { "BUG123" },
@@ -39,31 +39,31 @@ public sealed class CiscoMapperTests
new("Cisco Widget", "PID-1", "1.2.3", new [] { AffectedPackageStatusCatalog.KnownAffected }),
new("Cisco Router", "PID-2", ">=1.0.0 <1.4.0", new [] { AffectedPackageStatusCatalog.KnownAffected })
});
var document = new DocumentRecord(
Id: Guid.NewGuid(),
SourceName: VndrCiscoConnectorPlugin.SourceName,
Uri: "https://api.cisco.com/security/advisories/v2/advisories/CISCO-SA-TEST",
FetchedAt: published,
Sha256: "abc123",
Status: DocumentStatuses.PendingMap,
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: updated,
GridFsId: null);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, VndrCiscoConnectorPlugin.SourceName, "cisco.dto.test", new BsonDocument(), updated);
var advisory = CiscoMapper.Map(dto, document, dtoRecord);
advisory.AdvisoryKey.Should().Be("CISCO-SA-TEST");
advisory.Title.Should().Be("Test Advisory");
advisory.Severity.Should().Be("high");
advisory.Aliases.Should().Contain(new[] { "CISCO-SA-TEST", "CVE-2024-0001", "BUG123" });
advisory.References.Should().Contain(reference => reference.Url == "https://example.com/advisory");
advisory.References.Should().Contain(reference => reference.Url == "https://sec.cloudapps.cisco.com/csaf/test.json");
var document = new DocumentRecord(
Id: Guid.NewGuid(),
SourceName: VndrCiscoConnectorPlugin.SourceName,
Uri: "https://api.cisco.com/security/advisories/v2/advisories/CISCO-SA-TEST",
FetchedAt: published,
Sha256: "abc123",
Status: DocumentStatuses.PendingMap,
ContentType: "application/json",
Headers: null,
Metadata: null,
Etag: null,
LastModified: updated,
PayloadId: null);
var dtoRecord = new DtoRecord(Guid.NewGuid(), document.Id, VndrCiscoConnectorPlugin.SourceName, "cisco.dto.test", new BsonDocument(), updated);
var advisory = CiscoMapper.Map(dto, document, dtoRecord);
advisory.AdvisoryKey.Should().Be("CISCO-SA-TEST");
advisory.Title.Should().Be("Test Advisory");
advisory.Severity.Should().Be("high");
advisory.Aliases.Should().Contain(new[] { "CISCO-SA-TEST", "CVE-2024-0001", "BUG123" });
advisory.References.Should().Contain(reference => reference.Url == "https://example.com/advisory");
advisory.References.Should().Contain(reference => reference.Url == "https://sec.cloudapps.cisco.com/csaf/test.json");
advisory.AffectedPackages.Should().HaveCount(2);
var package = advisory.AffectedPackages.Single(p => p.Identifier == "Cisco Widget");