Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Reachability.Tests/ReachabilityWitnessPublisherIntegrationTests.cs
2026-01-08 20:46:43 +02:00

203 lines
7.4 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Attestor.Core.Rekor;
using StellaOps.Attestor.Core.Submission;
using StellaOps.Cryptography;
using StellaOps.Scanner.ProofSpine;
using StellaOps.Scanner.ProofSpine.Options;
using StellaOps.Scanner.Reachability.Attestation;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Scanner.Reachability.Tests;
public sealed class ReachabilityWitnessPublisherIntegrationTests
{
private static CancellationToken TestCancellationToken => TestContext.Current.CancellationToken;
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishAsync_WhenStoreInCasEnabled_StoresGraphAndEnvelopeInCas()
{
var options = Options.Create(new ReachabilityWitnessOptions
{
Enabled = true,
StoreInCas = true,
PublishToRekor = false,
});
var cas = new FakeFileContentAddressableStore();
var cryptoHash = CryptoHashFactory.CreateDefault();
var publisher = new ReachabilityWitnessPublisher(
options,
cryptoHash,
NullLogger<ReachabilityWitnessPublisher>.Instance,
cas: cas);
var graph = CreateTestGraph();
var graphBytes = System.Text.Encoding.UTF8.GetBytes("{\"schema\":\"richgraph-v1\",\"nodes\":[],\"edges\":[]}");
var result = await publisher.PublishAsync(
graph,
graphBytes,
graphHash: "blake3:abc123",
subjectDigest: "sha256:def456",
cancellationToken: TestCancellationToken);
Assert.Equal("cas://reachability/graphs/abc123", result.CasUri);
Assert.Equal(graphBytes, cas.GetBytes("abc123"));
Assert.NotNull(cas.GetBytes("abc123.dsse"));
Assert.NotEmpty(result.DsseEnvelopeBytes);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishAsync_WhenRekorEnabled_SubmitsDsseEnvelope()
{
var rekor = new CapturingRekorClient();
var signer = CreateDeterministicSigner(keyId: "reachability-test-key");
var cryptoProfile = new TestCryptoProfile("reachability-test-key", "hs256");
var options = Options.Create(new ReachabilityWitnessOptions
{
Enabled = true,
StoreInCas = false,
PublishToRekor = true,
RekorUrl = new Uri("https://rekor.test"),
RekorBackendName = "primary",
SigningKeyId = "reachability-test-key",
Tier = AttestationTier.Standard
});
var cryptoHash = CryptoHashFactory.CreateDefault();
var publisher = new ReachabilityWitnessPublisher(
options,
cryptoHash,
NullLogger<ReachabilityWitnessPublisher>.Instance,
dsseSigningService: signer,
cryptoProfile: cryptoProfile,
rekorClient: rekor);
var graph = CreateTestGraph();
var result = await publisher.PublishAsync(
graph,
graphBytes: Array.Empty<byte>(),
graphHash: "blake3:abc123",
subjectDigest: "sha256:def456",
cancellationToken: TestCancellationToken);
Assert.NotNull(rekor.LastRequest);
Assert.NotNull(rekor.LastBackend);
Assert.Equal("primary", rekor.LastBackend!.Name);
Assert.Equal(new Uri("https://rekor.test"), rekor.LastBackend.Url);
var request = rekor.LastRequest!;
Assert.Equal("application/vnd.in-toto+json", request.Bundle.Dsse.PayloadType);
Assert.False(string.IsNullOrWhiteSpace(request.Bundle.Dsse.PayloadBase64));
Assert.NotEmpty(request.Bundle.Dsse.Signatures);
Assert.Equal("reachability-test-key", request.Bundle.Dsse.Signatures[0].KeyId);
Assert.False(string.IsNullOrWhiteSpace(request.Meta.BundleSha256));
Assert.Equal(1234, result.RekorLogIndex);
Assert.Equal("rekor-uuid-1234", result.RekorLogId);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PublishAsync_WhenAirGapped_SkipsRekorSubmission()
{
var rekor = new CapturingRekorClient();
var options = Options.Create(new ReachabilityWitnessOptions
{
Enabled = true,
StoreInCas = false,
PublishToRekor = true,
RekorUrl = new Uri("https://rekor.test"),
Tier = AttestationTier.AirGapped,
});
var cryptoHash = CryptoHashFactory.CreateDefault();
var publisher = new ReachabilityWitnessPublisher(
options,
cryptoHash,
NullLogger<ReachabilityWitnessPublisher>.Instance,
rekorClient: rekor);
var graph = CreateTestGraph();
var result = await publisher.PublishAsync(
graph,
graphBytes: Array.Empty<byte>(),
graphHash: "blake3:abc123",
subjectDigest: "sha256:def456",
cancellationToken: TestCancellationToken);
Assert.Null(rekor.LastRequest);
Assert.Null(result.RekorLogIndex);
Assert.Null(result.RekorLogId);
}
private static RichGraph CreateTestGraph()
{
return new RichGraph(
Schema: "richgraph-v1",
Analyzer: new RichGraphAnalyzer("test-analyzer", "1.0.0", null),
Nodes: new[]
{
new RichGraphNode("n1", "sym:dotnet:A", null, null, "dotnet", "method", "A", null, null, null, null),
new RichGraphNode("n2", "sym:dotnet:B", null, null, "dotnet", "sink", "B", null, null, null, null)
},
Edges: new[]
{
new RichGraphEdge("n1", "n2", "call", null, null, null, 1.0, null)
},
Roots: null!);
}
private static IDsseSigningService CreateDeterministicSigner(string keyId)
{
var options = Options.Create(new ProofSpineDsseSigningOptions
{
Mode = "hash",
KeyId = keyId,
Algorithm = "hs256",
AllowDeterministicFallback = true,
});
return new HmacDsseSigningService(
options,
DefaultCryptoHmac.CreateForTests(),
DefaultCryptoHash.CreateForTests());
}
private sealed record TestCryptoProfile(string KeyId, string Algorithm) : ICryptoProfile;
private sealed class CapturingRekorClient : IRekorClient
{
public AttestorSubmissionRequest? LastRequest { get; private set; }
public RekorBackend? LastBackend { get; private set; }
public Task<RekorSubmissionResponse> SubmitAsync(AttestorSubmissionRequest request, RekorBackend backend, CancellationToken cancellationToken = default)
{
LastRequest = request;
LastBackend = backend;
return Task.FromResult(new RekorSubmissionResponse
{
Uuid = "rekor-uuid-1234",
Index = 1234,
LogUrl = backend.Url.ToString(),
Status = "included",
Proof = null
});
}
public Task<RekorProofResponse?> GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default)
=> Task.FromResult<RekorProofResponse?>(null);
public Task<RekorInclusionVerificationResult> VerifyInclusionAsync(string rekorUuid, byte[] payloadDigest, RekorBackend backend, CancellationToken cancellationToken = default)
=> Task.FromResult(RekorInclusionVerificationResult.Failure("not_implemented"));
}
}