compose and authority fixes. finish sprints.

This commit is contained in:
master
2026-02-17 21:59:47 +02:00
parent fb46a927ad
commit 49cdebe2f1
187 changed files with 23189 additions and 1439 deletions

View File

@@ -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);
}
}
}

View File

@@ -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++;
}
}
}
}

View File

@@ -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"
}
};
}
}

View File

@@ -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>