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.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.Instance, dsseSigningService: signer, cryptoProfile: cryptoProfile, rekorClient: rekor); var graph = CreateTestGraph(); var result = await publisher.PublishAsync( graph, graphBytes: Array.Empty(), 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.Instance, rekorClient: rekor); var graph = CreateTestGraph(); var result = await publisher.PublishAsync( graph, graphBytes: Array.Empty(), 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 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 GetProofAsync(string rekorUuid, RekorBackend backend, CancellationToken cancellationToken = default) => Task.FromResult(null); public Task VerifyInclusionAsync(string rekorUuid, byte[] payloadDigest, RekorBackend backend, CancellationToken cancellationToken = default) => Task.FromResult(RekorInclusionVerificationResult.Failure("not_implemented")); } }