// Copyright (c) StellaOps. Licensed under AGPL-3.0-or-later. using System.Security.Cryptography; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Attestor; using StellaOps.Scanner.Core.Configuration; using StellaOps.Scanner.Reachability; using StellaOps.Scanner.Reachability.Models; using StellaOps.Scanner.Worker.Orchestration; using StellaOps.Signals.Storage; using Xunit; using StellaOps.TestKit; namespace StellaOps.Scanner.Integration.Tests; /// /// Integration tests for end-to-end PoE generation pipeline. /// Tests the full workflow from scan → subgraph extraction → PoE generation → storage. /// public class PoEPipelineTests : IDisposable { private readonly string _tempCasRoot; private readonly Mock _resolverMock; private readonly Mock _emitterMock; private readonly PoECasStore _casStore; private readonly PoEOrchestrator _orchestrator; public PoEPipelineTests() { _tempCasRoot = Path.Combine(Path.GetTempPath(), $"poe-test-{Guid.NewGuid()}"); Directory.CreateDirectory(_tempCasRoot); _resolverMock = new Mock(); _emitterMock = new Mock(); _casStore = new PoECasStore(_tempCasRoot, NullLogger.Instance); _orchestrator = new PoEOrchestrator( _resolverMock.Object, _emitterMock.Object, _casStore, NullLogger.Instance ); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ScanWithVulnerability_GeneratesPoE_StoresInCas() { // Arrange var context = CreateScanContext(); var vulnerabilities = new List { new VulnerabilityMatch( VulnId: "CVE-2021-44228", ComponentRef: "pkg:maven/log4j@2.14.1", IsReachable: true, Severity: "Critical") }; var subgraph = CreateTestSubgraph("CVE-2021-44228", "pkg:maven/log4j@2.14.1"); var poeBytes = System.Text.Encoding.UTF8.GetBytes("{\"test\":\"poe\"}"); var dsseBytes = System.Text.Encoding.UTF8.GetBytes("{\"test\":\"dsse\"}"); var poeHash = "blake3:abc123"; _resolverMock .Setup(x => x.ResolveBatchAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(new Dictionary { ["CVE-2021-44228"] = subgraph }); _emitterMock .Setup(x => x.EmitPoEAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(poeBytes); _emitterMock .Setup(x => x.ComputePoEHash(poeBytes)) .Returns(poeHash); _emitterMock .Setup(x => x.SignPoEAsync(poeBytes, It.IsAny(), It.IsAny())) .ReturnsAsync(dsseBytes); var configuration = PoEConfiguration.Enabled; // Act var results = await _orchestrator.GeneratePoEArtifactsAsync( context, vulnerabilities, configuration); // Assert Assert.Single(results); var result = results[0]; Assert.Equal("CVE-2021-44228", result.VulnId); Assert.Equal(poeHash, result.PoeHash); // Verify stored in CAS var artifact = await _casStore.FetchAsync(poeHash); Assert.NotNull(artifact); Assert.Equal(poeBytes, artifact.PoeBytes); Assert.Equal(dsseBytes, artifact.DsseBytes); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task ScanWithUnreachableVuln_DoesNotGeneratePoE() { // Arrange var context = CreateScanContext(); var vulnerabilities = new List { new VulnerabilityMatch( VulnId: "CVE-9999-99999", ComponentRef: "pkg:maven/safe-lib@1.0.0", IsReachable: false, Severity: "High") }; var configuration = new PoEConfiguration { Enabled = true, EmitOnlyReachable = true }; // Act var results = await _orchestrator.GeneratePoEArtifactsAsync( context, vulnerabilities, configuration); // Assert Assert.Empty(results); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task PoEGeneration_ProducesDeterministicHash() { // Arrange var poeJson = await File.ReadAllTextAsync( "../../../../tests/Reachability/PoE/Fixtures/log4j-cve-2021-44228.poe.golden.json"); var poeBytes = System.Text.Encoding.UTF8.GetBytes(poeJson); // Act - Compute hash twice var hash1 = ComputeBlake3Hash(poeBytes); var hash2 = ComputeBlake3Hash(poeBytes); // Assert Assert.Equal(hash1, hash2); Assert.StartsWith("blake3:", hash1); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task PoEStorage_PersistsToCas_RetrievesCorrectly() { // Arrange var poeBytes = System.Text.Encoding.UTF8.GetBytes("{\"test\":\"poe\"}"); var dsseBytes = System.Text.Encoding.UTF8.GetBytes("{\"test\":\"dsse\"}"); // Act - Store var poeHash = await _casStore.StoreAsync(poeBytes, dsseBytes); // Act - Retrieve var artifact = await _casStore.FetchAsync(poeHash); // Assert Assert.NotNull(artifact); Assert.Equal(poeHash, artifact.PoeHash); Assert.Equal(poeBytes, artifact.PoeBytes); Assert.Equal(dsseBytes, artifact.DsseBytes); } private ScanContext CreateScanContext() { return new ScanContext( ScanId: "scan-test-123", GraphHash: "blake3:graph123", BuildId: "gnu-build-id:build123", ImageDigest: "sha256:image123", PolicyId: "test-policy-v1", PolicyDigest: "sha256:policy123", ScannerVersion: "1.0.0-test", ConfigPath: "etc/scanner.yaml" ); } private Subgraph CreateTestSubgraph(string vulnId, string componentRef) { return new Subgraph( BuildId: "gnu-build-id:test", ComponentRef: componentRef, VulnId: vulnId, Nodes: new List { new FunctionId("sha256:mod1", "main", "0x401000", null, null), new FunctionId("sha256:mod2", "vulnerable", "0x402000", null, null) }, Edges: new List { new Edge("main", "vulnerable", Array.Empty(), 0.95) }, EntryRefs: new[] { "main" }, SinkRefs: new[] { "vulnerable" }, PolicyDigest: "sha256:policy123", ToolchainDigest: "sha256:tool123" ); } private string ComputeBlake3Hash(byte[] data) { // Using SHA256 as BLAKE3 placeholder using var sha = SHA256.Create(); var hashBytes = sha.ComputeHash(data); var hashHex = Convert.ToHexString(hashBytes).ToLowerInvariant(); return $"blake3:{hashHex}"; } public void Dispose() { if (Directory.Exists(_tempCasRoot)) { Directory.Delete(_tempCasRoot, recursive: true); } } }