up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
This commit is contained in:
@@ -1,33 +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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,47 +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));
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +1,189 @@
|
||||
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;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
|
||||
|
||||
public sealed class MirrorSignatureVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidSignaturePasses()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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);
|
||||
|
||||
await verifier.VerifyAsync(payload, signature, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_InvalidSignatureThrows()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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');
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(payload, tampered, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_KeyMismatchThrows()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(
|
||||
payload,
|
||||
signature,
|
||||
expectedKeyId: "unexpected-key",
|
||||
expectedProvider: null,
|
||||
fallbackPublicKeyPath: null,
|
||||
cancellationToken: CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ThrowsWhenProviderMissingKey()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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);
|
||||
|
||||
provider.RemoveSigningKey(key.Reference.KeyId);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(
|
||||
payload,
|
||||
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);
|
||||
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
|
||||
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,
|
||||
ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
var signer = provider.GetSigner(SignatureAlgorithms.Es256, new CryptoKeyReference(keyId));
|
||||
var header = new Dictionary<string, object?>
|
||||
{
|
||||
["alg"] = SignatureAlgorithms.Es256,
|
||||
["kid"] = keyId,
|
||||
["provider"] = provider.Name,
|
||||
["typ"] = "application/vnd.stellaops.concelier.mirror-bundle+jws",
|
||||
["b64"] = false,
|
||||
["crit"] = new[] { "b64" }
|
||||
};
|
||||
|
||||
var headerJson = System.Text.Json.JsonSerializer.Serialize(header);
|
||||
var protectedHeader = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(headerJson);
|
||||
|
||||
var signingInput = BuildSigningInput(protectedHeader, payload.Span);
|
||||
var signatureBytes = await signer.SignAsync(signingInput, CancellationToken.None).ConfigureAwait(false);
|
||||
var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signatureBytes);
|
||||
|
||||
return (string.Concat(protectedHeader, "..", encodedSignature), DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> BuildSigningInput(string encodedHeader, ReadOnlySpan<byte> payload)
|
||||
{
|
||||
var headerBytes = System.Text.Encoding.ASCII.GetBytes(encodedHeader);
|
||||
var buffer = new byte[headerBytes.Length + 1 + payload.Length];
|
||||
headerBytes.CopyTo(buffer.AsSpan());
|
||||
buffer[headerBytes.Length] = (byte)'.';
|
||||
payload.CopyTo(buffer.AsSpan(headerBytes.Length + 1));
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
file static class Utf8Extensions
|
||||
{
|
||||
public static ReadOnlyMemory<byte> ToUtf8Bytes(this string value)
|
||||
=> System.Text.Encoding.UTF8.GetBytes(value);
|
||||
}
|
||||
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;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.StellaOpsMirror.Tests;
|
||||
|
||||
public sealed class MirrorSignatureVerifierTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ValidSignaturePasses()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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);
|
||||
|
||||
await verifier.VerifyAsync(payload, signature, CancellationToken.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_InvalidSignatureThrows()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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');
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(payload, tampered, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_KeyMismatchThrows()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(
|
||||
payload,
|
||||
signature,
|
||||
expectedKeyId: "unexpected-key",
|
||||
expectedProvider: null,
|
||||
fallbackPublicKeyPath: null,
|
||||
cancellationToken: CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VerifyAsync_ThrowsWhenProviderMissingKey()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
var key = CreateSigningKey("mirror-key");
|
||||
provider.UpsertSigningKey(key);
|
||||
|
||||
var registry = new CryptoProviderRegistry(new[] { provider });
|
||||
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);
|
||||
|
||||
provider.RemoveSigningKey(key.Reference.KeyId);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => verifier.VerifyAsync(
|
||||
payload,
|
||||
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);
|
||||
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
|
||||
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,
|
||||
ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
var signer = provider.GetSigner(SignatureAlgorithms.Es256, new CryptoKeyReference(keyId));
|
||||
var header = new Dictionary<string, object?>
|
||||
{
|
||||
["alg"] = SignatureAlgorithms.Es256,
|
||||
["kid"] = keyId,
|
||||
["provider"] = provider.Name,
|
||||
["typ"] = "application/vnd.stellaops.concelier.mirror-bundle+jws",
|
||||
["b64"] = false,
|
||||
["crit"] = new[] { "b64" }
|
||||
};
|
||||
|
||||
var headerJson = System.Text.Json.JsonSerializer.Serialize(header);
|
||||
var protectedHeader = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(headerJson);
|
||||
|
||||
var signingInput = BuildSigningInput(protectedHeader, payload.Span);
|
||||
var signatureBytes = await signer.SignAsync(signingInput, CancellationToken.None).ConfigureAwait(false);
|
||||
var encodedSignature = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.Encode(signatureBytes);
|
||||
|
||||
return (string.Concat(protectedHeader, "..", encodedSignature), DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
private static ReadOnlyMemory<byte> BuildSigningInput(string encodedHeader, ReadOnlySpan<byte> payload)
|
||||
{
|
||||
var headerBytes = System.Text.Encoding.ASCII.GetBytes(encodedHeader);
|
||||
var buffer = new byte[headerBytes.Length + 1 + payload.Length];
|
||||
headerBytes.CopyTo(buffer.AsSpan());
|
||||
buffer[headerBytes.Length] = (byte)'.';
|
||||
payload.CopyTo(buffer.AsSpan(headerBytes.Length + 1));
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
file static class Utf8Extensions
|
||||
{
|
||||
public static ReadOnlyMemory<byte> ToUtf8Bytes(this string value)
|
||||
=> System.Text.Encoding.UTF8.GetBytes(value);
|
||||
}
|
||||
|
||||
@@ -1,265 +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)}";
|
||||
}
|
||||
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)}";
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using Microsoft.Extensions.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Bson;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
@@ -88,20 +88,20 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
|
||||
var cursorDocument = state!.Cursor ?? new BsonDocument();
|
||||
var cursorDocument = state!.Cursor ?? new DocumentObject();
|
||||
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
|
||||
var pendingDocumentsArray = cursorDocument.TryGetValue("pendingDocuments", out var pendingDocsBson) && pendingDocsBson is DocumentArray pendingArray
|
||||
? pendingArray
|
||||
: new BsonArray();
|
||||
: new DocumentArray();
|
||||
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
|
||||
var pendingMappingsArray = cursorDocument.TryGetValue("pendingMappings", out var pendingMappingsBson) && pendingMappingsBson is DocumentArray mappingsArray
|
||||
? mappingsArray
|
||||
: new BsonArray();
|
||||
: new DocumentArray();
|
||||
Assert.Empty(pendingMappingsArray);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ public sealed class StellaOpsMirrorConnectorTests : IAsyncLifetime
|
||||
var stateRepository = provider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(StellaOpsMirrorConnector.Source, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var cursor = state!.Cursor ?? new BsonDocument();
|
||||
var cursor = state!.Cursor ?? new DocumentObject();
|
||||
Assert.True(state.FailCount >= 1);
|
||||
Assert.False(cursor.Contains("bundleDigest"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user