sprints and audit work

This commit is contained in:
StellaOps Bot
2026-01-07 09:36:16 +02:00
parent 05833e0af2
commit ab364c6032
377 changed files with 64534 additions and 1627 deletions

View File

@@ -68,6 +68,10 @@
<!-- Testing infrastructure -->
<ProjectReference Include="../../__Libraries/StellaOps.Testing.Determinism/StellaOps.Testing.Determinism.csproj" />
<!-- Replay infrastructure (SPRINT_20260105_002_001_REPLAY) -->
<ProjectReference Include="../../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
<ProjectReference Include="../../../__Libraries/StellaOps.Verdict/StellaOps.Verdict.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,466 @@
// <copyright file="VerifyProveE2ETests.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// </copyright>
// -----------------------------------------------------------------------------
// VerifyProveE2ETests.cs
// Sprint: SPRINT_20260105_002_001_REPLAY
// Task: RPL-022 - E2E test: Full verify -> prove workflow
// Description: End-to-end tests for bundle verification and proof generation.
// -----------------------------------------------------------------------------
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using StellaOps.Replay.Core.Models;
using StellaOps.Verdict;
namespace StellaOps.Integration.E2E;
/// <summary>
/// E2E tests for verify -> prove workflow.
/// RPL-022: Tests bundle verification and replay proof generation.
/// </summary>
[Trait("Category", "E2E")]
public sealed class VerifyProveE2ETests : IDisposable
{
private readonly string _testDir;
private readonly VerdictBuilderService _verdictBuilder;
public VerifyProveE2ETests()
{
_testDir = Path.Combine(Path.GetTempPath(), $"e2e-verify-prove-{Guid.NewGuid():N}");
Directory.CreateDirectory(_testDir);
_verdictBuilder = new VerdictBuilderService(
NullLogger<VerdictBuilderService>.Instance,
signer: null);
}
public void Dispose()
{
if (Directory.Exists(_testDir))
{
Directory.Delete(_testDir, recursive: true);
}
}
#region Workflow Tests
[Fact]
public async Task FullWorkflow_CreateBundle_VerifyReplay_GenerateProof()
{
// Arrange: Create a complete test bundle
var bundlePath = CreateCompleteTestBundle("workflow-test-001");
// Act: Execute replay and generate proof
var manifestPath = Path.Combine(bundlePath, "manifest.json");
var manifestJson = await File.ReadAllTextAsync(manifestPath);
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
ImageDigest = manifest.Scan.ImageDigest,
PolicyDigest = manifest.Scan.PolicyDigest,
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
};
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert: Replay succeeded
result.Success.Should().BeTrue("Replay should succeed with valid inputs");
result.VerdictHash.Should().NotBeNullOrEmpty();
result.VerdictHash.Should().StartWith("cgs:sha256:");
result.EngineVersion.Should().Be("1.0.0");
result.DurationMs.Should().BeGreaterThanOrEqualTo(0);
}
[Fact]
public async Task FullWorkflow_DeterministicReplay_SameInputsSameOutput()
{
// Arrange: Create bundle
var bundlePath = CreateCompleteTestBundle("determinism-test-001");
var manifestPath = Path.Combine(bundlePath, "manifest.json");
var manifestJson = await File.ReadAllTextAsync(manifestPath);
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
ImageDigest = manifest.Scan.ImageDigest,
PolicyDigest = manifest.Scan.PolicyDigest,
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
};
// Act: Replay twice
var result1 = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
var result2 = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert: Same verdict hash
result1.Success.Should().BeTrue();
result2.Success.Should().BeTrue();
result1.VerdictHash.Should().Be(result2.VerdictHash, "Same inputs must produce same verdict hash");
}
[Fact]
public async Task FullWorkflow_WithVexDocuments_VexInfluencesVerdict()
{
// Arrange: Create bundle with VEX
var bundlePath = CreateBundleWithVex("vex-test-001");
var manifestPath = Path.Combine(bundlePath, "manifest.json");
var manifestJson = await File.ReadAllTextAsync(manifestPath);
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
VexPath = Path.Combine(bundlePath, "inputs", "vex"),
ImageDigest = manifest.Scan.ImageDigest,
PolicyDigest = manifest.Scan.PolicyDigest,
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeTrue();
result.VerdictHash.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ProofGeneration_ValidBundle_ProducesCompactProof()
{
// Arrange
var bundlePath = CreateCompleteTestBundle("proof-gen-001");
var manifestPath = Path.Combine(bundlePath, "manifest.json");
var manifestJson = await File.ReadAllTextAsync(manifestPath);
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
ImageDigest = manifest.Scan.ImageDigest,
PolicyDigest = manifest.Scan.PolicyDigest,
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
var bundleHash = await ComputeBundleHashAsync(bundlePath);
var proof = ReplayProof.FromExecutionResult(
bundleHash: bundleHash,
policyVersion: manifest.Scan.PolicyDigest,
verdictRoot: result.VerdictHash ?? "unknown",
verdictMatches: true,
durationMs: result.DurationMs,
replayedAt: DateTimeOffset.UtcNow,
engineVersion: result.EngineVersion ?? "1.0.0",
artifactDigest: manifest.Scan.ImageDigest);
// Assert
proof.Should().NotBeNull();
var compactProof = proof.ToCompactString();
compactProof.Should().StartWith("replay-proof:sha256:");
compactProof.Should().HaveLength(78); // "replay-proof:sha256:" + 64 hex chars
var canonicalJson = proof.ToCanonicalJson();
canonicalJson.Should().NotBeNullOrEmpty();
canonicalJson.Should().Contain("verdictRoot");
canonicalJson.Should().Contain("bundleHash");
}
[Fact]
public async Task ProofGeneration_DifferentBundles_DifferentProofHashes()
{
// Arrange
var bundle1Path = CreateCompleteTestBundle("bundle-1");
var bundle2Path = CreateCompleteTestBundle("bundle-2", sbomVersion: 2);
async Task<string> GenerateProofHash(string bundlePath)
{
var manifestPath = Path.Combine(bundlePath, "manifest.json");
var manifestJson = await File.ReadAllTextAsync(manifestPath);
var manifest = JsonSerializer.Deserialize<TestManifest>(manifestJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(bundlePath, manifest.Inputs.Sbom.Path),
ImageDigest = manifest.Scan.ImageDigest,
PolicyDigest = manifest.Scan.PolicyDigest,
FeedSnapshotDigest = manifest.Scan.FeedSnapshotDigest
};
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
var bundleHash = await ComputeBundleHashAsync(bundlePath);
var proof = ReplayProof.FromExecutionResult(
bundleHash: bundleHash,
policyVersion: manifest.Scan.PolicyDigest,
verdictRoot: result.VerdictHash ?? "unknown",
verdictMatches: true,
durationMs: result.DurationMs,
replayedAt: DateTimeOffset.UtcNow,
engineVersion: result.EngineVersion ?? "1.0.0",
artifactDigest: manifest.Scan.ImageDigest);
return proof.ToCompactString();
}
// Act
var proof1 = await GenerateProofHash(bundle1Path);
var proof2 = await GenerateProofHash(bundle2Path);
// Assert
proof1.Should().NotBe(proof2, "Different bundles should produce different proof hashes");
}
#endregion
#region Error Handling Tests
[Fact]
public async Task Workflow_MissingBundle_ReturnsFailure()
{
// Arrange
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(_testDir, "nonexistent", "sbom.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeFalse();
result.Error.Should().Contain("not found");
}
[Fact]
public async Task Workflow_InvalidSbom_ReturnsFailure()
{
// Arrange
var bundlePath = Path.Combine(_testDir, "invalid-sbom");
Directory.CreateDirectory(Path.Combine(bundlePath, "inputs"));
File.WriteAllText(Path.Combine(bundlePath, "inputs", "sbom.json"), "not valid json {{{");
var request = new VerdictReplayRequest
{
SbomPath = Path.Combine(bundlePath, "inputs", "sbom.json"),
ImageDigest = "sha256:abc123",
PolicyDigest = "sha256:policy123",
FeedSnapshotDigest = "sha256:feeds123"
};
// Act
var result = await _verdictBuilder.ReplayFromBundleAsync(request, TestContext.Current.CancellationToken);
// Assert
result.Success.Should().BeFalse();
}
#endregion
#region Helper Methods
private string CreateCompleteTestBundle(string bundleId, int sbomVersion = 1)
{
var bundlePath = Path.Combine(_testDir, bundleId);
Directory.CreateDirectory(Path.Combine(bundlePath, "inputs"));
Directory.CreateDirectory(Path.Combine(bundlePath, "outputs"));
// Create SBOM
var sbomContent = $$"""
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": {{sbomVersion}},
"metadata": {
"timestamp": "2026-01-05T10:00:00Z"
},
"components": [
{
"type": "library",
"name": "test-package",
"version": "1.0.0",
"purl": "pkg:npm/test-package@1.0.0"
}
]
}
""";
var sbomPath = Path.Combine(bundlePath, "inputs", "sbom.json");
File.WriteAllText(sbomPath, sbomContent, Encoding.UTF8);
// Compute SBOM hash
var sbomHash = ComputeHash(sbomContent);
// Create verdict output
var verdictContent = """
{
"decision": "pass",
"score": 0.95,
"findings": []
}
""";
var verdictPath = Path.Combine(bundlePath, "outputs", "verdict.json");
File.WriteAllText(verdictPath, verdictContent, Encoding.UTF8);
var verdictHash = ComputeHash(verdictContent);
// Create manifest
var manifest = new
{
schemaVersion = "2.0.0",
bundleId = bundleId,
createdAt = DateTimeOffset.UtcNow.ToString("O"),
scan = new
{
id = $"scan-{bundleId}",
imageDigest = $"sha256:image{bundleId}",
policyDigest = "sha256:policy123",
scorePolicyDigest = "sha256:scorepolicy123",
feedSnapshotDigest = "sha256:feeds123",
toolchain = "stellaops-1.0.0",
analyzerSetDigest = "sha256:analyzers123"
},
inputs = new
{
sbom = new { path = "inputs/sbom.json", sha256 = sbomHash }
},
expectedOutputs = new
{
verdict = new { path = "outputs/verdict.json", sha256 = verdictHash },
verdictHash = $"cgs:sha256:{verdictHash}"
}
};
var manifestPath = Path.Combine(bundlePath, "manifest.json");
File.WriteAllText(manifestPath, JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
return bundlePath;
}
private string CreateBundleWithVex(string bundleId)
{
var bundlePath = CreateCompleteTestBundle(bundleId);
// Add VEX documents
var vexPath = Path.Combine(bundlePath, "inputs", "vex");
Directory.CreateDirectory(vexPath);
var vexContent = """
{
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://example.com/vex/001",
"author": "security@example.com",
"timestamp": "2026-01-05T10:00:00Z",
"version": 1,
"statements": [
{
"vulnerability": {"name": "CVE-2024-0001"},
"products": [{"@id": "pkg:npm/test-package@1.0.0"}],
"status": "not_affected",
"justification": "vulnerable_code_not_present"
}
]
}
""";
File.WriteAllText(Path.Combine(vexPath, "vex-001.json"), vexContent, Encoding.UTF8);
return bundlePath;
}
private static string ComputeHash(string content)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(content);
return Convert.ToHexString(sha256.ComputeHash(bytes)).ToLowerInvariant();
}
private static async Task<string> ComputeBundleHashAsync(string bundlePath)
{
var files = Directory.GetFiles(bundlePath, "*", SearchOption.AllDirectories)
.OrderBy(f => f, StringComparer.Ordinal)
.ToArray();
if (files.Length == 0)
{
return "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
}
using var hasher = System.Security.Cryptography.SHA256.Create();
foreach (var file in files)
{
var fileBytes = await File.ReadAllBytesAsync(file);
hasher.TransformBlock(fileBytes, 0, fileBytes.Length, null, 0);
}
hasher.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return $"sha256:{Convert.ToHexString(hasher.Hash!).ToLowerInvariant()}";
}
#endregion
#region Test DTOs
private sealed record TestManifest
{
public required string SchemaVersion { get; init; }
public required string BundleId { get; init; }
public required string CreatedAt { get; init; }
public required TestScanInfo Scan { get; init; }
public required TestInputs Inputs { get; init; }
public TestOutputs? ExpectedOutputs { get; init; }
}
private sealed record TestScanInfo
{
public required string Id { get; init; }
public required string ImageDigest { get; init; }
public required string PolicyDigest { get; init; }
public required string FeedSnapshotDigest { get; init; }
public string? Toolchain { get; init; }
}
private sealed record TestInputs
{
public required TestInputFile Sbom { get; init; }
}
private sealed record TestInputFile
{
public required string Path { get; init; }
public required string Sha256 { get; init; }
}
private sealed record TestOutputs
{
public TestInputFile? Verdict { get; init; }
public string? VerdictHash { get; init; }
}
#endregion
}