Add tests and implement StubBearer authentication for Signer endpoints

- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints.
- Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication.
- Developed ConcelierExporterClient for managing Trivy DB settings and export operations.
- Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering.
- Implemented styles and HTML structure for Trivy DB settings page.
- Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
This commit is contained in:
master
2025-10-21 09:37:07 +03:00
parent d6cb41dd51
commit 48f3071e2a
298 changed files with 20490 additions and 5751 deletions

View File

@@ -0,0 +1,33 @@
using System;
using System.IO;
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
internal static class FixtureLoader
{
private static readonly string FixturesRoot = Path.Combine(AppContext.BaseDirectory, "Fixtures");
public static string Read(string relativePath)
{
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new ArgumentException("Fixture path must be provided.", nameof(relativePath));
}
var normalized = relativePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
var path = Path.Combine(FixturesRoot, normalized);
if (!File.Exists(path))
{
throw new FileNotFoundException($"Fixture '{relativePath}' not found at '{path}'.", path);
}
var content = File.ReadAllText(path);
return NormalizeLineEndings(content);
}
public static string Normalize(string value) => NormalizeLineEndings(value);
private static string NormalizeLineEndings(string value)
=> value.Replace("\r\n", "\n", StringComparison.Ordinal);
}

View File

@@ -0,0 +1,212 @@
{
"advisoryKey": "CVE-2025-1111",
"affectedPackages": [
{
"type": "semver",
"identifier": "pkg:npm/example@1.0.0",
"platform": null,
"versionRanges": [
{
"fixedVersion": "1.2.0",
"introducedVersion": "1.0.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">=1.0.0,<1.2.0",
"exactValue": null,
"fixed": "1.2.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": true,
"style": "range"
},
"vendorExtensions": null
},
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "range",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">=1.0.0,<1.2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "range",
"min": "1.0.0",
"minInclusive": true,
"max": "1.2.0",
"maxInclusive": false,
"value": null,
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "status",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "package",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
},
{
"source": "stellaops-mirror",
"kind": "map",
"value": "domain=primary;repository=mirror-primary;generated=2025-10-19T12:00:00.0000000+00:00;package=pkg:npm/example@1.0.0",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[]",
"affectedpackages[].normalizedversions[]",
"affectedpackages[].statuses[]",
"affectedpackages[].versionranges[]"
]
}
]
}
],
"aliases": [
"CVE-2025-1111",
"GHSA-xxxx-xxxx-xxxx"
],
"canonicalMetricId": "cvss::ghsa::CVE-2025-1111",
"credits": [
{
"displayName": "Security Researcher",
"role": "reporter",
"contacts": [
"mailto:researcher@example.com"
],
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "credit",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
}
],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "cvss",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-79",
"name": "Cross-site Scripting",
"uri": "https://cwe.mitre.org/data/definitions/79.html",
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "cwe",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "Deterministic test payload distributed via mirror.",
"exploitKnown": false,
"language": "en",
"modified": "2025-10-11T00:00:00+00:00",
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "advisory",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"advisory"
]
},
{
"source": "stellaops-mirror",
"kind": "map",
"value": "domain=primary;repository=mirror-primary;generated=2025-10-19T12:00:00.0000000+00:00",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"advisory",
"credits[]",
"cvssmetrics[]",
"cwes[]",
"references[]"
]
}
],
"published": "2025-10-10T00:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "reference",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "vendor",
"summary": "Vendor bulletin",
"url": "https://example.com/advisory"
}
],
"severity": "high",
"summary": "Upstream advisory replicated through StellaOps mirror.",
"title": "Sample Mirror Advisory"
}

View File

@@ -0,0 +1,202 @@
{
"advisories": [
{
"advisoryKey": "CVE-2025-1111",
"affectedPackages": [
{
"type": "semver",
"identifier": "pkg:npm/example@1.0.0",
"platform": null,
"versionRanges": [
{
"fixedVersion": "1.2.0",
"introducedVersion": "1.0.0",
"lastAffectedVersion": null,
"primitives": {
"evr": null,
"hasVendorExtensions": false,
"nevra": null,
"semVer": {
"constraintExpression": ">=1.0.0,<1.2.0",
"exactValue": null,
"fixed": "1.2.0",
"fixedInclusive": false,
"introduced": "1.0.0",
"introducedInclusive": true,
"lastAffected": null,
"lastAffectedInclusive": true,
"style": "range"
},
"vendorExtensions": null
},
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "range",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[].versionranges[]"
]
},
"rangeExpression": ">=1.0.0,<1.2.0",
"rangeKind": "semver"
}
],
"normalizedVersions": [
{
"scheme": "semver",
"type": "range",
"min": "1.0.0",
"minInclusive": true,
"max": "1.2.0",
"maxInclusive": false,
"value": null,
"notes": null
}
],
"statuses": [
{
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "status",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[].statuses[]"
]
},
"status": "fixed"
}
],
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "package",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"affectedpackages[]"
]
}
]
}
],
"aliases": [
"GHSA-xxxx-xxxx-xxxx"
],
"canonicalMetricId": "cvss::ghsa::CVE-2025-1111",
"credits": [
{
"displayName": "Security Researcher",
"role": "reporter",
"contacts": [
"mailto:researcher@example.com"
],
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "credit",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"credits[]"
]
}
}
],
"cvssMetrics": [
{
"baseScore": 9.8,
"baseSeverity": "critical",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "cvss",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"cvssmetrics[]"
]
},
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
"version": "3.1"
}
],
"cwes": [
{
"taxonomy": "cwe",
"identifier": "CWE-79",
"name": "Cross-site Scripting",
"uri": "https://cwe.mitre.org/data/definitions/79.html",
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "cwe",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"cwes[]"
]
}
]
}
],
"description": "Deterministic test payload distributed via mirror.",
"exploitKnown": false,
"language": "en",
"modified": "2025-10-11T00:00:00+00:00",
"provenance": [
{
"source": "ghsa",
"kind": "map",
"value": "advisory",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"advisory"
]
}
],
"published": "2025-10-10T00:00:00+00:00",
"references": [
{
"kind": "advisory",
"provenance": {
"source": "ghsa",
"kind": "map",
"value": "reference",
"decisionReason": null,
"recordedAt": "2025-10-19T12:00:00+00:00",
"fieldMask": [
"references[]"
]
},
"sourceTag": "vendor",
"summary": "Vendor bulletin",
"url": "https://example.com/advisory"
}
],
"severity": "high",
"summary": "Upstream advisory replicated through StellaOps mirror.",
"title": "Sample Mirror Advisory"
}
],
"advisoryCount": 1,
"displayName": "Primary Mirror",
"domainId": "primary",
"generatedAt": "2025-10-19T12:00:00+00:00",
"schemaVersion": 1,
"sources": [
{
"advisoryCount": 1,
"firstRecordedAt": "2025-10-19T12:00:00+00:00",
"lastRecordedAt": "2025-10-19T12:00:00+00:00",
"source": "ghsa"
}
],
"targetRepository": "mirror-primary"
}

View File

@@ -0,0 +1,47 @@
using System;
using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
using StellaOps.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
public sealed class MirrorAdvisoryMapperTests
{
[Fact]
public void Map_ProducesCanonicalAdvisoryWithMirrorProvenance()
{
var bundle = SampleData.CreateBundle();
var bundleJson = CanonicalJsonSerializer.SerializeIndented(bundle);
Assert.Equal(
FixtureLoader.Read(SampleData.BundleFixture).TrimEnd(),
FixtureLoader.Normalize(bundleJson).TrimEnd());
var advisories = MirrorAdvisoryMapper.Map(bundle);
Assert.Single(advisories);
var advisory = advisories[0];
var expectedAdvisory = SampleData.CreateExpectedMappedAdvisory();
var expectedJson = CanonicalJsonSerializer.SerializeIndented(expectedAdvisory);
Assert.Equal(
FixtureLoader.Read(SampleData.AdvisoryFixture).TrimEnd(),
FixtureLoader.Normalize(expectedJson).TrimEnd());
var actualJson = CanonicalJsonSerializer.SerializeIndented(advisory);
Assert.Equal(
FixtureLoader.Normalize(expectedJson).TrimEnd(),
FixtureLoader.Normalize(actualJson).TrimEnd());
Assert.Contains(advisory.Aliases, alias => string.Equals(alias, advisory.AdvisoryKey, StringComparison.OrdinalIgnoreCase));
Assert.Contains(
advisory.Provenance,
provenance => string.Equals(provenance.Source, StellaOpsMirrorConnector.Source, StringComparison.Ordinal) &&
string.Equals(provenance.Kind, "map", StringComparison.Ordinal));
var package = Assert.Single(advisory.AffectedPackages);
Assert.Contains(
package.Provenance,
provenance => string.Equals(provenance.Source, StellaOpsMirrorConnector.Source, StringComparison.Ordinal) &&
string.Equals(provenance.Kind, "map", StringComparison.Ordinal));
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Concelier.Connector.StellaOpsMirror.Security;
using StellaOps.Cryptography;
@@ -18,7 +20,7 @@ public sealed class MirrorSignatureVerifierTests
provider.UpsertSigningKey(key);
var registry = new CryptoProviderRegistry(new[] { provider });
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance);
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
var payload = payloadText.ToUtf8Bytes();
@@ -35,13 +37,13 @@ public sealed class MirrorSignatureVerifierTests
provider.UpsertSigningKey(key);
var registry = new CryptoProviderRegistry(new[] { provider });
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance);
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
var payload = payloadText.ToUtf8Bytes();
var (signature, _) = await CreateDetachedJwsAsync(provider, key.Reference.KeyId, payload);
var tampered = signature.Replace('a', 'b', StringComparison.Ordinal);
var tampered = signature.Replace('a', 'b');
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(payload, tampered, CancellationToken.None));
}
@@ -54,7 +56,7 @@ public sealed class MirrorSignatureVerifierTests
provider.UpsertSigningKey(key);
var registry = new CryptoProviderRegistry(new[] { provider });
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance);
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
var payload = payloadText.ToUtf8Bytes();
@@ -65,6 +67,7 @@ public sealed class MirrorSignatureVerifierTests
signature,
expectedKeyId: "unexpected-key",
expectedProvider: null,
fallbackPublicKeyPath: null,
cancellationToken: CancellationToken.None));
}
@@ -76,7 +79,7 @@ public sealed class MirrorSignatureVerifierTests
provider.UpsertSigningKey(key);
var registry = new CryptoProviderRegistry(new[] { provider });
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance);
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, new MemoryCache(new MemoryCacheOptions()));
var payloadText = System.Text.Json.JsonSerializer.Serialize(new { advisories = Array.Empty<string>() });
var payload = payloadText.ToUtf8Bytes();
@@ -89,9 +92,42 @@ public sealed class MirrorSignatureVerifierTests
signature,
expectedKeyId: key.Reference.KeyId,
expectedProvider: provider.Name,
fallbackPublicKeyPath: null,
cancellationToken: CancellationToken.None));
}
[Fact]
public async Task VerifyAsync_UsesCachedPublicKeyWhenFileRemoved()
{
var provider = new DefaultCryptoProvider();
var signingKey = CreateSigningKey("mirror-key");
provider.UpsertSigningKey(signingKey);
var registry = new CryptoProviderRegistry(new[] { provider });
var memoryCache = new MemoryCache(new MemoryCacheOptions());
var verifier = new MirrorSignatureVerifier(registry, NullLogger<MirrorSignatureVerifier>.Instance, memoryCache);
var payload = "{\"advisories\":[]}";
var (signature, _) = await CreateDetachedJwsAsync(provider, signingKey.Reference.KeyId, payload.ToUtf8Bytes());
provider.RemoveSigningKey(signingKey.Reference.KeyId);
var pemPath = WritePublicKeyPem(signingKey);
try
{
await verifier.VerifyAsync(payload.ToUtf8Bytes(), signature, expectedKeyId: signingKey.Reference.KeyId, expectedProvider: "default", fallbackPublicKeyPath: pemPath, cancellationToken: CancellationToken.None);
File.Delete(pemPath);
await verifier.VerifyAsync(payload.ToUtf8Bytes(), signature, expectedKeyId: signingKey.Reference.KeyId, expectedProvider: "default", fallbackPublicKeyPath: pemPath, cancellationToken: CancellationToken.None);
}
finally
{
if (File.Exists(pemPath))
{
File.Delete(pemPath);
}
}
}
private static CryptoSigningKey CreateSigningKey(string keyId)
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
@@ -99,6 +135,16 @@ public sealed class MirrorSignatureVerifierTests
return new CryptoSigningKey(new CryptoKeyReference(keyId), SignatureAlgorithms.Es256, in parameters, DateTimeOffset.UtcNow);
}
private static string WritePublicKeyPem(CryptoSigningKey signingKey)
{
using var ecdsa = ECDsa.Create(signingKey.PublicParameters);
var info = ecdsa.ExportSubjectPublicKeyInfo();
var pem = PemEncoding.Write("PUBLIC KEY", info);
var path = Path.Combine(Path.GetTempPath(), $"stellaops-mirror-{Guid.NewGuid():N}.pem");
File.WriteAllText(path, pem);
return path;
}
private static async Task<(string Signature, DateTimeOffset SignedAt)> CreateDetachedJwsAsync(
DefaultCryptoProvider provider,
string keyId,

View File

@@ -0,0 +1,265 @@
using System;
using System.Globalization;
using StellaOps.Concelier.Connector.StellaOpsMirror.Internal;
using StellaOps.Concelier.Models;
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
internal static class SampleData
{
public const string BundleFixture = "mirror-bundle.sample.json";
public const string AdvisoryFixture = "mirror-advisory.expected.json";
public const string TargetRepository = "mirror-primary";
public const string DomainId = "primary";
public const string AdvisoryKey = "CVE-2025-1111";
public const string GhsaAlias = "GHSA-xxxx-xxxx-xxxx";
public static DateTimeOffset GeneratedAt { get; } = new(2025, 10, 19, 12, 0, 0, TimeSpan.Zero);
public static MirrorBundleDocument CreateBundle()
=> new(
SchemaVersion: 1,
GeneratedAt: GeneratedAt,
TargetRepository: TargetRepository,
DomainId: DomainId,
DisplayName: "Primary Mirror",
AdvisoryCount: 1,
Advisories: new[] { CreateSourceAdvisory() },
Sources: new[]
{
new MirrorSourceSummary("ghsa", GeneratedAt, GeneratedAt, 1)
});
public static Advisory CreateExpectedMappedAdvisory()
{
var baseAdvisory = CreateSourceAdvisory();
var recordedAt = GeneratedAt.ToUniversalTime();
var mirrorValue = BuildMirrorValue(recordedAt);
var topProvenance = baseAdvisory.Provenance.Add(new AdvisoryProvenance(
StellaOpsMirrorConnector.Source,
"map",
mirrorValue,
recordedAt,
new[]
{
ProvenanceFieldMasks.Advisory,
ProvenanceFieldMasks.References,
ProvenanceFieldMasks.Credits,
ProvenanceFieldMasks.CvssMetrics,
ProvenanceFieldMasks.Weaknesses,
}));
var package = baseAdvisory.AffectedPackages[0];
var packageProvenance = package.Provenance.Add(new AdvisoryProvenance(
StellaOpsMirrorConnector.Source,
"map",
$"{mirrorValue};package={package.Identifier}",
recordedAt,
new[]
{
ProvenanceFieldMasks.AffectedPackages,
ProvenanceFieldMasks.VersionRanges,
ProvenanceFieldMasks.PackageStatuses,
ProvenanceFieldMasks.NormalizedVersions,
}));
var updatedPackage = new AffectedPackage(
package.Type,
package.Identifier,
package.Platform,
package.VersionRanges,
package.Statuses,
packageProvenance,
package.NormalizedVersions);
return new Advisory(
AdvisoryKey,
baseAdvisory.Title,
baseAdvisory.Summary,
baseAdvisory.Language,
baseAdvisory.Published,
baseAdvisory.Modified,
baseAdvisory.Severity,
baseAdvisory.ExploitKnown,
new[] { AdvisoryKey, GhsaAlias },
baseAdvisory.Credits,
baseAdvisory.References,
new[] { updatedPackage },
baseAdvisory.CvssMetrics,
topProvenance,
baseAdvisory.Description,
baseAdvisory.Cwes,
baseAdvisory.CanonicalMetricId);
}
private static Advisory CreateSourceAdvisory()
{
var recordedAt = GeneratedAt.ToUniversalTime();
var reference = new AdvisoryReference(
"https://example.com/advisory",
"advisory",
"vendor",
"Vendor bulletin",
new AdvisoryProvenance(
"ghsa",
"map",
"reference",
recordedAt,
new[]
{
ProvenanceFieldMasks.References,
}));
var credit = new AdvisoryCredit(
"Security Researcher",
"reporter",
new[] { "mailto:researcher@example.com" },
new AdvisoryProvenance(
"ghsa",
"map",
"credit",
recordedAt,
new[]
{
ProvenanceFieldMasks.Credits,
}));
var semVerPrimitive = new SemVerPrimitive(
Introduced: "1.0.0",
IntroducedInclusive: true,
Fixed: "1.2.0",
FixedInclusive: false,
LastAffected: null,
LastAffectedInclusive: true,
ConstraintExpression: ">=1.0.0,<1.2.0",
ExactValue: null);
var range = new AffectedVersionRange(
rangeKind: "semver",
introducedVersion: "1.0.0",
fixedVersion: "1.2.0",
lastAffectedVersion: null,
rangeExpression: ">=1.0.0,<1.2.0",
provenance: new AdvisoryProvenance(
"ghsa",
"map",
"range",
recordedAt,
new[]
{
ProvenanceFieldMasks.VersionRanges,
}),
primitives: new RangePrimitives(semVerPrimitive, null, null, null));
var status = new AffectedPackageStatus(
"fixed",
new AdvisoryProvenance(
"ghsa",
"map",
"status",
recordedAt,
new[]
{
ProvenanceFieldMasks.PackageStatuses,
}));
var normalizedRule = new NormalizedVersionRule(
scheme: "semver",
type: "range",
min: "1.0.0",
minInclusive: true,
max: "1.2.0",
maxInclusive: false,
value: null,
notes: null);
var package = new AffectedPackage(
AffectedPackageTypes.SemVer,
"pkg:npm/example@1.0.0",
platform: null,
versionRanges: new[] { range },
statuses: new[] { status },
provenance: new[]
{
new AdvisoryProvenance(
"ghsa",
"map",
"package",
recordedAt,
new[]
{
ProvenanceFieldMasks.AffectedPackages,
})
},
normalizedVersions: new[] { normalizedRule });
var cvss = new CvssMetric(
"3.1",
"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
9.8,
"critical",
new AdvisoryProvenance(
"ghsa",
"map",
"cvss",
recordedAt,
new[]
{
ProvenanceFieldMasks.CvssMetrics,
}));
var weakness = new AdvisoryWeakness(
"cwe",
"CWE-79",
"Cross-site Scripting",
"https://cwe.mitre.org/data/definitions/79.html",
new[]
{
new AdvisoryProvenance(
"ghsa",
"map",
"cwe",
recordedAt,
new[]
{
ProvenanceFieldMasks.Weaknesses,
})
});
var advisory = new Advisory(
AdvisoryKey,
"Sample Mirror Advisory",
"Upstream advisory replicated through StellaOps mirror.",
"en",
published: new DateTimeOffset(2025, 10, 10, 0, 0, 0, TimeSpan.Zero),
modified: new DateTimeOffset(2025, 10, 11, 0, 0, 0, TimeSpan.Zero),
severity: "high",
exploitKnown: false,
aliases: new[] { GhsaAlias },
credits: new[] { credit },
references: new[] { reference },
affectedPackages: new[] { package },
cvssMetrics: new[] { cvss },
provenance: new[]
{
new AdvisoryProvenance(
"ghsa",
"map",
"advisory",
recordedAt,
new[]
{
ProvenanceFieldMasks.Advisory,
})
},
description: "Deterministic test payload distributed via mirror.",
cwes: new[] { weakness },
canonicalMetricId: "cvss::ghsa::CVE-2025-1111");
return CanonicalJsonSerializer.Normalize(advisory);
}
private static string BuildMirrorValue(DateTimeOffset recordedAt)
=> $"domain={DomainId};repository={TargetRepository};generated={recordedAt.ToString("O", CultureInfo.InvariantCulture)}";
}

View File

@@ -4,8 +4,11 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../StellaOps.Concelier.Connector.StellaOpsMirror/StellaOps.Concelier.Connector.StellaOpsMirror.csproj" />
<ProjectReference Include="../StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="../StellaOps.Concelier.Connector.StellaOpsMirror/StellaOps.Concelier.Connector.StellaOpsMirror.csproj" />
<ProjectReference Include="../StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Fixtures\**\*.json" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
@@ -15,11 +16,15 @@ 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.Concelier.Models;
using Xunit;
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
@@ -168,6 +173,95 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
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()
@@ -323,6 +417,17 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
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();