compose and authority fixes. finish sprints.
This commit is contained in:
@@ -5,10 +5,12 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Explainability.Assumptions;
|
||||
using StellaOps.Scanner.Reachability.Runtime;
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using StellaOps.Scanner.Reachability.Stack;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
using StellaOps.TestKit;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests.Evidence;
|
||||
@@ -88,6 +90,8 @@ public sealed class RuntimeReachabilityCollectorTests
|
||||
Assert.Equal(ObservationSource.Historical, result.Source);
|
||||
Assert.Single(result.Observations);
|
||||
Assert.True(result.Observations[0].WasObserved);
|
||||
Assert.NotNull(result.BtfSelection);
|
||||
Assert.Equal("kernel", result.BtfSelection!.SourceKind);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
@@ -217,11 +221,88 @@ public sealed class RuntimeReachabilityCollectorTests
|
||||
Assert.NotNull(result.Error);
|
||||
Assert.Equal(GatingOutcome.Unknown, result.Layer3.Outcome);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ObserveAsync_WithWitnessEmission_InvokesRuntimeWitnessGenerator()
|
||||
{
|
||||
var observations = new List<SymbolObservation>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Symbol = "target_sink",
|
||||
WasObserved = true,
|
||||
ObservationCount = 2,
|
||||
FirstObservedAt = DateTimeOffset.UtcNow.AddMinutes(-3),
|
||||
LastObservedAt = DateTimeOffset.UtcNow.AddMinutes(-1)
|
||||
}
|
||||
};
|
||||
|
||||
_observationStore.SetObservations("container-wit", observations);
|
||||
var witnessGenerator = new MockRuntimeWitnessGenerator();
|
||||
var collector = new EbpfRuntimeReachabilityCollector(
|
||||
_signalCollector,
|
||||
_observationStore,
|
||||
NullLogger<EbpfRuntimeReachabilityCollector>.Instance,
|
||||
_timeProvider,
|
||||
witnessGenerator);
|
||||
|
||||
var request = new RuntimeObservationRequest
|
||||
{
|
||||
ContainerId = "container-wit",
|
||||
ImageDigest = "sha256:img123",
|
||||
TargetSymbols = ["target_sink"],
|
||||
UseHistoricalData = true,
|
||||
WitnessEmission = new RuntimeWitnessEmissionRequest
|
||||
{
|
||||
Enabled = true,
|
||||
ComponentPurl = "pkg:oci/demo@sha256:img123",
|
||||
VulnerabilityId = "CVE-2026-0001",
|
||||
Symbolization = new WitnessSymbolization
|
||||
{
|
||||
BuildId = "gnu-build-id:test",
|
||||
DebugArtifactUri = "cas://symbols/test.debug",
|
||||
Symbolizer = new WitnessSymbolizer
|
||||
{
|
||||
Name = "llvm-symbolizer",
|
||||
Version = "18.1.7",
|
||||
Digest = "sha256:symbolizer"
|
||||
},
|
||||
LibcVariant = "glibc",
|
||||
SysrootDigest = "sha256:sysroot"
|
||||
},
|
||||
SigningOptions = new RuntimeWitnessSigningOptions
|
||||
{
|
||||
KeyId = "runtime-signing-key",
|
||||
UseKeyless = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = await collector.ObserveAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Witness);
|
||||
Assert.True(result.Witness!.Success);
|
||||
Assert.NotNull(witnessGenerator.LastRequest);
|
||||
Assert.Equal(request.ImageDigest, witnessGenerator.LastRequest!.ArtifactDigest);
|
||||
Assert.Equal(request.WitnessEmission!.ComponentPurl, witnessGenerator.LastRequest.ComponentPurl);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MockSignalCollector : IRuntimeSignalCollector
|
||||
{
|
||||
private bool _isSupported = true;
|
||||
private readonly RuntimeBtfSelection _btfSelection = new()
|
||||
{
|
||||
SourceKind = "kernel",
|
||||
SourcePath = "/sys/kernel/btf/vmlinux",
|
||||
SourceDigest = "sha256:test",
|
||||
SelectionReason = "kernel_btf_present",
|
||||
KernelRelease = "6.8.0-test",
|
||||
KernelArch = "x86_64",
|
||||
};
|
||||
|
||||
private readonly RuntimeSignalOptions _defaultOptions = new()
|
||||
{
|
||||
TargetSymbols = [],
|
||||
@@ -232,6 +313,8 @@ internal sealed class MockSignalCollector : IRuntimeSignalCollector
|
||||
|
||||
public bool IsSupported() => _isSupported;
|
||||
|
||||
public RuntimeBtfSelection GetBtfSelection() => _btfSelection;
|
||||
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes() => [ProbeType.Uprobe, ProbeType.Uretprobe];
|
||||
|
||||
public Task<SignalCollectionHandle> StartCollectionAsync(
|
||||
@@ -309,3 +392,96 @@ internal sealed class MockObservationStore : IRuntimeObservationStore
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MockRuntimeWitnessGenerator : IRuntimeWitnessGenerator
|
||||
{
|
||||
public RuntimeWitnessRequest? LastRequest { get; private set; }
|
||||
|
||||
public Task<RuntimeWitnessResult> GenerateAsync(RuntimeWitnessRequest request, CancellationToken ct = default)
|
||||
{
|
||||
LastRequest = request;
|
||||
var witness = new PathWitness
|
||||
{
|
||||
WitnessId = "wit:runtime:test",
|
||||
Artifact = new WitnessArtifact
|
||||
{
|
||||
SbomDigest = request.ArtifactDigest,
|
||||
ComponentPurl = request.ComponentPurl
|
||||
},
|
||||
Vuln = new WitnessVuln
|
||||
{
|
||||
Id = request.VulnerabilityId ?? "runtime",
|
||||
Source = "runtime",
|
||||
AffectedRange = "unknown"
|
||||
},
|
||||
Entrypoint = new WitnessEntrypoint
|
||||
{
|
||||
Kind = "runtime",
|
||||
Name = "runtime-entry",
|
||||
SymbolId = "runtime:entry"
|
||||
},
|
||||
Path =
|
||||
[
|
||||
new PathStep
|
||||
{
|
||||
Symbol = "runtime-entry",
|
||||
SymbolId = "runtime:entry"
|
||||
}
|
||||
],
|
||||
Sink = new WitnessSink
|
||||
{
|
||||
Symbol = "runtime-sink",
|
||||
SymbolId = "runtime:sink",
|
||||
SinkType = "runtime-observed"
|
||||
},
|
||||
Evidence = new WitnessEvidence
|
||||
{
|
||||
CallgraphDigest = "sha256:test",
|
||||
BuildId = request.Symbolization?.BuildId
|
||||
},
|
||||
ObservedAt = request.Observations[0].ObservedAt,
|
||||
ObservationType = ObservationType.Runtime,
|
||||
PredicateType = RuntimeWitnessPredicateTypes.RuntimeWitnessCanonical,
|
||||
ClaimId = request.ClaimId,
|
||||
Observations = request.Observations,
|
||||
Symbolization = request.Symbolization
|
||||
};
|
||||
|
||||
return Task.FromResult(RuntimeWitnessResult.Successful(
|
||||
witness,
|
||||
Encoding.UTF8.GetBytes("{}"),
|
||||
casUri: "cas://runtime-witness/dsse/mock"));
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<RuntimeWitnessResult> GenerateBatchAsync(
|
||||
BatchRuntimeWitnessRequest request,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
foreach (var item in request.Requests)
|
||||
{
|
||||
yield return await GenerateAsync(item, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<RuntimeWitnessResult> GenerateFromStreamAsync(
|
||||
IAsyncEnumerable<RuntimeObservation> observations,
|
||||
IRuntimeWitnessContextProvider contextProvider,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct = default)
|
||||
{
|
||||
await foreach (var observation in observations.WithCancellation(ct))
|
||||
{
|
||||
var request = new RuntimeWitnessRequest
|
||||
{
|
||||
ClaimId = contextProvider.GetClaimId(observation),
|
||||
ArtifactDigest = contextProvider.GetArtifactDigest(),
|
||||
ComponentPurl = contextProvider.GetComponentPurl(observation),
|
||||
VulnerabilityId = contextProvider.GetVulnerabilityId(observation),
|
||||
Observations = [observation],
|
||||
Symbolization = contextProvider.GetSymbolization(observation) ?? throw new InvalidOperationException(),
|
||||
SigningOptions = contextProvider.GetSigningOptions()
|
||||
};
|
||||
|
||||
yield return await GenerateAsync(request, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for deterministic runtime witness generation.
|
||||
/// </summary>
|
||||
public sealed class RuntimeWitnessGeneratorTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GenerateAsync_WithValidRequest_ReturnsSignedWitnessAndStoresArtifacts()
|
||||
{
|
||||
var signingKey = CreateTestKey();
|
||||
var signer = new WitnessDsseSigner();
|
||||
var keyProvider = new StaticSigningKeyProvider(signingKey);
|
||||
var storage = new RecordingStorage("cas://runtime-witness/dsse/test-envelope");
|
||||
var sut = new RuntimeWitnessGenerator(signer, keyProvider, storage);
|
||||
|
||||
var request = CreateRequest(
|
||||
claimId: "claim:artifact123:pathabcdef123456",
|
||||
observations:
|
||||
[
|
||||
CreateObservation("obs-b", "sha256:bbb", DateTimeOffset.Parse("2026-02-16T10:00:02Z")),
|
||||
CreateObservation("obs-a", "sha256:aaa", DateTimeOffset.Parse("2026-02-16T10:00:01Z"))
|
||||
]);
|
||||
|
||||
var result = await sut.GenerateAsync(request, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.NotNull(result.Witness);
|
||||
Assert.NotNull(result.EnvelopeBytes);
|
||||
Assert.Equal("cas://runtime-witness/dsse/test-envelope", result.CasUri);
|
||||
Assert.Equal("pathabcdef123456", result.Witness!.PathHash);
|
||||
Assert.NotNull(storage.LastRequest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GenerateAsync_WithEquivalentObservationSets_ProducesStableEnvelopeBytes()
|
||||
{
|
||||
var signingKey = CreateTestKey();
|
||||
var signer = new WitnessDsseSigner();
|
||||
var keyProvider = new StaticSigningKeyProvider(signingKey);
|
||||
var sut = new RuntimeWitnessGenerator(signer, keyProvider, new NullRuntimeWitnessStorage());
|
||||
|
||||
var ordered = new[]
|
||||
{
|
||||
CreateObservation("obs-a", "sha256:aaa", DateTimeOffset.Parse("2026-02-16T10:00:01Z")),
|
||||
CreateObservation("obs-b", "sha256:bbb", DateTimeOffset.Parse("2026-02-16T10:00:02Z"))
|
||||
};
|
||||
|
||||
var reversed = new[] { ordered[1], ordered[0] };
|
||||
|
||||
var requestA = CreateRequest("claim:artifact123:pathabcdef123456", ordered);
|
||||
var requestB = CreateRequest("claim:artifact123:pathabcdef123456", reversed);
|
||||
|
||||
var resultA = await sut.GenerateAsync(requestA, TestContext.Current.CancellationToken);
|
||||
var resultB = await sut.GenerateAsync(requestB, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.True(resultA.Success);
|
||||
Assert.True(resultB.Success);
|
||||
Assert.NotNull(resultA.EnvelopeBytes);
|
||||
Assert.NotNull(resultB.EnvelopeBytes);
|
||||
Assert.Equal(resultA.Witness!.WitnessId, resultB.Witness!.WitnessId);
|
||||
Assert.Equal(resultA.EnvelopeBytes, resultB.EnvelopeBytes);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GenerateFromStreamAsync_GroupsByClaimIdAndEmitsDeterministicOrder()
|
||||
{
|
||||
var signingKey = CreateTestKey();
|
||||
var signer = new WitnessDsseSigner();
|
||||
var keyProvider = new StaticSigningKeyProvider(signingKey);
|
||||
var sut = new RuntimeWitnessGenerator(signer, keyProvider, new NullRuntimeWitnessStorage());
|
||||
|
||||
var observations = StreamObservations(
|
||||
CreateObservation("obs-1", "sha256:111", DateTimeOffset.Parse("2026-02-16T10:01:00Z"), containerId: "c2"),
|
||||
CreateObservation("obs-2", "sha256:222", DateTimeOffset.Parse("2026-02-16T10:02:00Z"), containerId: "c1"));
|
||||
|
||||
var provider = new TestContextProvider();
|
||||
|
||||
var results = new List<RuntimeWitnessResult>();
|
||||
await foreach (var result in sut.GenerateFromStreamAsync(observations, provider, TestContext.Current.CancellationToken))
|
||||
{
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
Assert.Equal(2, results.Count);
|
||||
Assert.All(results, static result => Assert.True(result.Success));
|
||||
Assert.Equal("claim:artifact123:c1", results[0].ClaimId);
|
||||
Assert.Equal("claim:artifact123:c2", results[1].ClaimId);
|
||||
}
|
||||
|
||||
private static RuntimeWitnessRequest CreateRequest(
|
||||
string claimId,
|
||||
IReadOnlyList<RuntimeObservation> observations)
|
||||
{
|
||||
return new RuntimeWitnessRequest
|
||||
{
|
||||
ClaimId = claimId,
|
||||
ArtifactDigest = "sha256:artifact123",
|
||||
ComponentPurl = "pkg:oci/demo@sha256:artifact123",
|
||||
VulnerabilityId = "CVE-2026-0001",
|
||||
Observations = observations,
|
||||
Symbolization = CreateSymbolization(),
|
||||
SigningOptions = new RuntimeWitnessSigningOptions
|
||||
{
|
||||
KeyId = "runtime-signing-key",
|
||||
UseKeyless = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static RuntimeObservation CreateObservation(
|
||||
string observationId,
|
||||
string stackHash,
|
||||
DateTimeOffset observedAt,
|
||||
string containerId = "container-a")
|
||||
{
|
||||
return new RuntimeObservation
|
||||
{
|
||||
ObservedAt = observedAt,
|
||||
ObservationCount = 1,
|
||||
StackSampleHash = stackHash,
|
||||
ProcessId = 100,
|
||||
ContainerId = containerId,
|
||||
PodName = "pod-a",
|
||||
Namespace = "default",
|
||||
SourceType = RuntimeObservationSourceType.Tetragon,
|
||||
ObservationId = observationId
|
||||
};
|
||||
}
|
||||
|
||||
private static WitnessSymbolization CreateSymbolization()
|
||||
{
|
||||
return new WitnessSymbolization
|
||||
{
|
||||
BuildId = "gnu-build-id:runtime-test",
|
||||
DebugArtifactUri = "cas://symbols/runtime-test.debug",
|
||||
Symbolizer = new WitnessSymbolizer
|
||||
{
|
||||
Name = "llvm-symbolizer",
|
||||
Version = "18.1.7",
|
||||
Digest = "sha256:symbolizer"
|
||||
},
|
||||
LibcVariant = "glibc",
|
||||
SysrootDigest = "sha256:sysroot"
|
||||
};
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<RuntimeObservation> StreamObservations(params RuntimeObservation[] items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
yield return item;
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
private static EnvelopeKey CreateTestKey()
|
||||
{
|
||||
var generator = new Ed25519KeyPairGenerator();
|
||||
generator.Init(new Ed25519KeyGenerationParameters(new SecureRandom(new FixedRandomGenerator())));
|
||||
var keyPair = generator.GenerateKeyPair();
|
||||
|
||||
var privateParams = (Ed25519PrivateKeyParameters)keyPair.Private;
|
||||
var publicParams = (Ed25519PublicKeyParameters)keyPair.Public;
|
||||
|
||||
var privateKey = new byte[64];
|
||||
privateParams.Encode(privateKey, 0);
|
||||
var publicKey = publicParams.GetEncoded();
|
||||
Array.Copy(publicKey, 0, privateKey, 32, 32);
|
||||
|
||||
return EnvelopeKey.CreateEd25519Signer(privateKey, publicKey, "runtime-signing-key");
|
||||
}
|
||||
|
||||
private sealed class StaticSigningKeyProvider : IRuntimeWitnessSigningKeyProvider
|
||||
{
|
||||
private readonly EnvelopeKey _key;
|
||||
|
||||
public StaticSigningKeyProvider(EnvelopeKey key)
|
||||
{
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public bool TryResolveSigningKey(
|
||||
RuntimeWitnessSigningOptions options,
|
||||
out EnvelopeKey? signingKey,
|
||||
out string? errorMessage)
|
||||
{
|
||||
signingKey = _key;
|
||||
errorMessage = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingStorage : IRuntimeWitnessStorage
|
||||
{
|
||||
private readonly string _uri;
|
||||
|
||||
public RecordingStorage(string uri)
|
||||
{
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public RuntimeWitnessStorageRequest? LastRequest { get; private set; }
|
||||
|
||||
public Task<string?> StoreAsync(RuntimeWitnessStorageRequest request, CancellationToken ct = default)
|
||||
{
|
||||
LastRequest = request;
|
||||
return Task.FromResult<string?>(_uri);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestContextProvider : IRuntimeWitnessContextProvider
|
||||
{
|
||||
public string GetClaimId(RuntimeObservation observation)
|
||||
{
|
||||
return $"claim:artifact123:{observation.ContainerId}";
|
||||
}
|
||||
|
||||
public string GetArtifactDigest()
|
||||
{
|
||||
return "sha256:artifact123";
|
||||
}
|
||||
|
||||
public string GetComponentPurl(RuntimeObservation observation)
|
||||
{
|
||||
return "pkg:oci/demo@sha256:artifact123";
|
||||
}
|
||||
|
||||
public string? GetVulnerabilityId(RuntimeObservation observation)
|
||||
{
|
||||
return "CVE-2026-0001";
|
||||
}
|
||||
|
||||
public WitnessSymbolization? GetSymbolization(RuntimeObservation observation)
|
||||
{
|
||||
return CreateSymbolization();
|
||||
}
|
||||
|
||||
public RuntimeWitnessSigningOptions GetSigningOptions()
|
||||
{
|
||||
return new RuntimeWitnessSigningOptions
|
||||
{
|
||||
KeyId = "runtime-signing-key",
|
||||
UseKeyless = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixed random generator for deterministic key generation in tests.
|
||||
/// </summary>
|
||||
private sealed class FixedRandomGenerator : Org.BouncyCastle.Crypto.Prng.IRandomGenerator
|
||||
{
|
||||
private byte _value = 0x42;
|
||||
|
||||
public void AddSeedMaterial(byte[] seed) { }
|
||||
public void AddSeedMaterial(ReadOnlySpan<byte> seed) { }
|
||||
public void AddSeedMaterial(long seed) { }
|
||||
public void NextBytes(byte[] bytes) => NextBytes(bytes, 0, bytes.Length);
|
||||
public void NextBytes(byte[] bytes, int start, int len)
|
||||
{
|
||||
for (var i = start; i < start + len; i++)
|
||||
{
|
||||
bytes[i] = _value++;
|
||||
}
|
||||
}
|
||||
|
||||
public void NextBytes(Span<byte> bytes)
|
||||
{
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
bytes[i] = _value++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using StellaOps.Scanner.Reachability.Witnesses;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for runtime witness requests.
|
||||
/// </summary>
|
||||
public sealed class RuntimeWitnessRequestTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WithoutSymbolization_ThrowsArgumentException()
|
||||
{
|
||||
var request = CreateValidRequest() with
|
||||
{
|
||||
Symbolization = null
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => request.Validate());
|
||||
Assert.Contains("Symbolization", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WithoutDebugArtifactAndSymbolTable_ThrowsInvalidOperationException()
|
||||
{
|
||||
var request = CreateValidRequest() with
|
||||
{
|
||||
Symbolization = new WitnessSymbolization
|
||||
{
|
||||
BuildId = "gnu-build-id:abc123",
|
||||
DebugArtifactUri = null,
|
||||
SymbolTableUri = null,
|
||||
Symbolizer = new WitnessSymbolizer
|
||||
{
|
||||
Name = "llvm-symbolizer",
|
||||
Version = "18.1.7",
|
||||
Digest = "sha256:symdigest"
|
||||
},
|
||||
LibcVariant = "glibc",
|
||||
SysrootDigest = "sha256:sysroot"
|
||||
}
|
||||
};
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => request.Validate());
|
||||
Assert.Contains("debug_artifact_uri", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Validate_WithValidSymbolization_DoesNotThrow()
|
||||
{
|
||||
var request = CreateValidRequest();
|
||||
request.Validate();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void BatchValidate_ValidRequest_DoesNotThrow()
|
||||
{
|
||||
var batch = new BatchRuntimeWitnessRequest
|
||||
{
|
||||
Requests = [CreateValidRequest()]
|
||||
};
|
||||
|
||||
batch.Validate();
|
||||
}
|
||||
|
||||
private static RuntimeWitnessRequest CreateValidRequest()
|
||||
{
|
||||
return new RuntimeWitnessRequest
|
||||
{
|
||||
ClaimId = "claim:artifact:path",
|
||||
ArtifactDigest = "sha256:artifact",
|
||||
ComponentPurl = "pkg:docker/example/app@1.0.0",
|
||||
Observations =
|
||||
[
|
||||
new RuntimeObservation
|
||||
{
|
||||
ObservedAt = new DateTimeOffset(2026, 2, 16, 12, 0, 0, TimeSpan.Zero),
|
||||
ObservationCount = 2,
|
||||
SourceType = RuntimeObservationSourceType.Tetragon
|
||||
}
|
||||
],
|
||||
Symbolization = new WitnessSymbolization
|
||||
{
|
||||
BuildId = "gnu-build-id:abc123",
|
||||
DebugArtifactUri = "cas://symbols/by-build-id/gnu-build-id:abc123/artifact.debug",
|
||||
SymbolTableUri = null,
|
||||
Symbolizer = new WitnessSymbolizer
|
||||
{
|
||||
Name = "llvm-symbolizer",
|
||||
Version = "18.1.7",
|
||||
Digest = "sha256:symdigest"
|
||||
},
|
||||
LibcVariant = "glibc",
|
||||
SysrootDigest = "sha256:sysroot"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,42 @@ public class WitnessDsseSignerTests
|
||||
Assert.NotEmpty(result.PayloadBytes!);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignWitness_RuntimeWitnessWithoutSymbolization_ReturnsFails()
|
||||
{
|
||||
// Arrange
|
||||
var witness = CreateRuntimeWitness(includeSymbolization: false);
|
||||
var (privateKey, publicKey) = CreateTestKeyPair();
|
||||
var key = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey);
|
||||
var signer = new WitnessDsseSigner();
|
||||
|
||||
// Act
|
||||
var result = signer.SignWitness(witness, key, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.Contains("symbolization", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void SignWitness_RuntimeWitnessWithSymbolization_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
var witness = CreateRuntimeWitness(includeSymbolization: true);
|
||||
var (privateKey, publicKey) = CreateTestKeyPair();
|
||||
var key = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey);
|
||||
var signer = new WitnessDsseSigner();
|
||||
|
||||
// Act
|
||||
var result = signer.SignWitness(witness, key, TestCancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess, result.Error);
|
||||
Assert.NotNull(result.Envelope);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void VerifyWitness_WithValidSignature_ReturnsSuccess()
|
||||
@@ -304,6 +340,46 @@ public class WitnessDsseSignerTests
|
||||
};
|
||||
}
|
||||
|
||||
private static PathWitness CreateRuntimeWitness(bool includeSymbolization)
|
||||
{
|
||||
var witness = CreateTestWitness() with
|
||||
{
|
||||
ObservationType = ObservationType.Runtime,
|
||||
Observations =
|
||||
[
|
||||
new RuntimeObservation
|
||||
{
|
||||
ObservedAt = new DateTimeOffset(2025, 12, 19, 12, 30, 0, TimeSpan.Zero),
|
||||
ObservationCount = 3,
|
||||
SourceType = RuntimeObservationSourceType.Tetragon
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (!includeSymbolization)
|
||||
{
|
||||
return witness;
|
||||
}
|
||||
|
||||
return witness with
|
||||
{
|
||||
Symbolization = new WitnessSymbolization
|
||||
{
|
||||
BuildId = "gnu-build-id:abcd1234",
|
||||
DebugArtifactUri = "cas://symbols/by-build-id/gnu-build-id:abcd1234/artifact.debug",
|
||||
SymbolTableUri = null,
|
||||
Symbolizer = new WitnessSymbolizer
|
||||
{
|
||||
Name = "llvm-symbolizer",
|
||||
Version = "18.1.7",
|
||||
Digest = "sha256:symbolizer123"
|
||||
},
|
||||
LibcVariant = "glibc",
|
||||
SysrootDigest = "sha256:sysroot123"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fixed random generator for deterministic key generation in tests.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user