sprints and audit work
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.TestKit;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Verdict\StellaOps.Verdict.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FixtureHarvester.csproj" />
|
||||
<ProjectReference Include="FixtureHarvester.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -12,8 +12,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.2.0" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="YamlDotNet" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -18,148 +18,217 @@ internal static class Program
|
||||
|
||||
// Harvest command
|
||||
var harvestCommand = new Command("harvest", "Harvest and store a fixture with metadata");
|
||||
var harvestTypeOption = new Option<string>(
|
||||
"--type",
|
||||
description: "Fixture type: sbom, feed, vex") { IsRequired = true };
|
||||
var harvestIdOption = new Option<string>(
|
||||
"--id",
|
||||
description: "Unique fixture identifier") { IsRequired = true };
|
||||
var harvestSourceOption = new Option<string>(
|
||||
"--source",
|
||||
description: "Source URL or path");
|
||||
var harvestOutputOption = new Option<string>(
|
||||
"--output",
|
||||
description: "Output directory",
|
||||
getDefaultValue: () => "src/__Tests/fixtures");
|
||||
var harvestTypeOption = new Option<string>("--type")
|
||||
{
|
||||
Description = "Fixture type: sbom, feed, vex",
|
||||
Required = true
|
||||
};
|
||||
var harvestIdOption = new Option<string>("--id")
|
||||
{
|
||||
Description = "Unique fixture identifier",
|
||||
Required = true
|
||||
};
|
||||
var harvestSourceOption = new Option<string>("--source")
|
||||
{
|
||||
Description = "Source URL or path"
|
||||
};
|
||||
var harvestOutputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output directory",
|
||||
DefaultValueFactory = _ => "src/__Tests/fixtures"
|
||||
};
|
||||
|
||||
harvestCommand.AddOption(harvestTypeOption);
|
||||
harvestCommand.AddOption(harvestIdOption);
|
||||
harvestCommand.AddOption(harvestSourceOption);
|
||||
harvestCommand.AddOption(harvestOutputOption);
|
||||
harvestCommand.SetHandler(HarvestCommand.ExecuteAsync, harvestTypeOption, harvestIdOption, harvestSourceOption, harvestOutputOption);
|
||||
harvestCommand.Add(harvestTypeOption);
|
||||
harvestCommand.Add(harvestIdOption);
|
||||
harvestCommand.Add(harvestSourceOption);
|
||||
harvestCommand.Add(harvestOutputOption);
|
||||
harvestCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var type = parseResult.GetValue(harvestTypeOption) ?? string.Empty;
|
||||
var id = parseResult.GetValue(harvestIdOption) ?? string.Empty;
|
||||
var source = parseResult.GetValue(harvestSourceOption);
|
||||
var output = parseResult.GetValue(harvestOutputOption) ?? "src/__Tests/fixtures";
|
||||
return HarvestCommand.ExecuteAsync(type, id, source, output);
|
||||
});
|
||||
|
||||
// Validate command
|
||||
var validateCommand = new Command("validate", "Validate fixtures against manifest");
|
||||
var validatePathOption = new Option<string>(
|
||||
"--path",
|
||||
description: "Fixtures directory path",
|
||||
getDefaultValue: () => "src/__Tests/fixtures");
|
||||
var validatePathOption = new Option<string>("--path")
|
||||
{
|
||||
Description = "Fixtures directory path",
|
||||
DefaultValueFactory = _ => "src/__Tests/fixtures"
|
||||
};
|
||||
|
||||
validateCommand.AddOption(validatePathOption);
|
||||
validateCommand.SetHandler(ValidateCommand.ExecuteAsync, validatePathOption);
|
||||
validateCommand.Add(validatePathOption);
|
||||
validateCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var path = parseResult.GetValue(validatePathOption) ?? "src/__Tests/fixtures";
|
||||
return ValidateCommand.ExecuteAsync(path);
|
||||
});
|
||||
|
||||
// Regen command
|
||||
var regenCommand = new Command("regen", "Regenerate expected outputs (manual, use with caution)");
|
||||
var regenFixtureOption = new Option<string>(
|
||||
"--fixture",
|
||||
description: "Fixture ID to regenerate");
|
||||
var regenAllOption = new Option<bool>(
|
||||
"--all",
|
||||
description: "Regenerate all fixtures",
|
||||
getDefaultValue: () => false);
|
||||
var regenConfirmOption = new Option<bool>(
|
||||
"--confirm",
|
||||
description: "Confirm regeneration",
|
||||
getDefaultValue: () => false);
|
||||
var regenFixtureOption = new Option<string>("--fixture")
|
||||
{
|
||||
Description = "Fixture ID to regenerate"
|
||||
};
|
||||
var regenAllOption = new Option<bool>("--all")
|
||||
{
|
||||
Description = "Regenerate all fixtures",
|
||||
DefaultValueFactory = _ => false
|
||||
};
|
||||
var regenConfirmOption = new Option<bool>("--confirm")
|
||||
{
|
||||
Description = "Confirm regeneration",
|
||||
DefaultValueFactory = _ => false
|
||||
};
|
||||
|
||||
regenCommand.AddOption(regenFixtureOption);
|
||||
regenCommand.AddOption(regenAllOption);
|
||||
regenCommand.AddOption(regenConfirmOption);
|
||||
regenCommand.SetHandler(RegenCommand.ExecuteAsync, regenFixtureOption, regenAllOption, regenConfirmOption);
|
||||
regenCommand.Add(regenFixtureOption);
|
||||
regenCommand.Add(regenAllOption);
|
||||
regenCommand.Add(regenConfirmOption);
|
||||
regenCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var fixture = parseResult.GetValue(regenFixtureOption);
|
||||
var all = parseResult.GetValue(regenAllOption);
|
||||
var confirm = parseResult.GetValue(regenConfirmOption);
|
||||
return RegenCommand.ExecuteAsync(fixture, all, confirm);
|
||||
});
|
||||
|
||||
// OCI Pin command (FH-004)
|
||||
var ociPinCommand = new Command("oci-pin", "Pin OCI image digests for deterministic testing");
|
||||
var ociImageOption = new Option<string>(
|
||||
"--image",
|
||||
description: "Image reference (e.g., alpine:3.19, myregistry.io/app:v1)") { IsRequired = true };
|
||||
var ociOutputOption = new Option<string>(
|
||||
"--output",
|
||||
description: "Output directory",
|
||||
getDefaultValue: () => "src/__Tests/fixtures/oci");
|
||||
var ociVerifyOption = new Option<bool>(
|
||||
"--verify",
|
||||
description: "Verify digest by re-fetching manifest",
|
||||
getDefaultValue: () => true);
|
||||
var ociImageOption = new Option<string>("--image")
|
||||
{
|
||||
Description = "Image reference (e.g., alpine:3.19, myregistry.io/app:v1)",
|
||||
Required = true
|
||||
};
|
||||
var ociOutputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output directory",
|
||||
DefaultValueFactory = _ => "src/__Tests/fixtures/oci"
|
||||
};
|
||||
var ociVerifyOption = new Option<bool>("--verify")
|
||||
{
|
||||
Description = "Verify digest by re-fetching manifest",
|
||||
DefaultValueFactory = _ => true
|
||||
};
|
||||
|
||||
ociPinCommand.AddOption(ociImageOption);
|
||||
ociPinCommand.AddOption(ociOutputOption);
|
||||
ociPinCommand.AddOption(ociVerifyOption);
|
||||
ociPinCommand.SetHandler(OciPinCommand.ExecuteAsync, ociImageOption, ociOutputOption, ociVerifyOption);
|
||||
ociPinCommand.Add(ociImageOption);
|
||||
ociPinCommand.Add(ociOutputOption);
|
||||
ociPinCommand.Add(ociVerifyOption);
|
||||
ociPinCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var image = parseResult.GetValue(ociImageOption) ?? string.Empty;
|
||||
var output = parseResult.GetValue(ociOutputOption) ?? "src/__Tests/fixtures/oci";
|
||||
var verify = parseResult.GetValue(ociVerifyOption);
|
||||
return OciPinCommand.ExecuteAsync(image, output, verify);
|
||||
});
|
||||
|
||||
// Feed Snapshot command (FH-005)
|
||||
var feedSnapshotCommand = new Command("feed-snapshot", "Capture vulnerability feed snapshots");
|
||||
var feedTypeOption = new Option<string>(
|
||||
"--feed",
|
||||
description: "Feed type: osv, ghsa, nvd, epss, kev, oval") { IsRequired = true };
|
||||
var feedUrlOption = new Option<string>(
|
||||
"--url",
|
||||
description: "Concelier base URL",
|
||||
getDefaultValue: () => "http://localhost:5010");
|
||||
var feedCountOption = new Option<int>(
|
||||
"--count",
|
||||
description: "Number of advisories to capture",
|
||||
getDefaultValue: () => 30);
|
||||
var feedOutputOption = new Option<string>(
|
||||
"--output",
|
||||
description: "Output directory",
|
||||
getDefaultValue: () => "src/__Tests/fixtures/feeds");
|
||||
var feedTypeOption = new Option<string>("--feed")
|
||||
{
|
||||
Description = "Feed type: osv, ghsa, nvd, epss, kev, oval",
|
||||
Required = true
|
||||
};
|
||||
var feedUrlOption = new Option<string>("--url")
|
||||
{
|
||||
Description = "Concelier base URL",
|
||||
DefaultValueFactory = _ => "http://localhost:5010"
|
||||
};
|
||||
var feedCountOption = new Option<int>("--count")
|
||||
{
|
||||
Description = "Number of advisories to capture",
|
||||
DefaultValueFactory = _ => 30
|
||||
};
|
||||
var feedOutputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output directory",
|
||||
DefaultValueFactory = _ => "src/__Tests/fixtures/feeds"
|
||||
};
|
||||
|
||||
feedSnapshotCommand.AddOption(feedTypeOption);
|
||||
feedSnapshotCommand.AddOption(feedUrlOption);
|
||||
feedSnapshotCommand.AddOption(feedCountOption);
|
||||
feedSnapshotCommand.AddOption(feedOutputOption);
|
||||
feedSnapshotCommand.SetHandler(FeedSnapshotCommand.ExecuteAsync, feedTypeOption, feedUrlOption, feedCountOption, feedOutputOption);
|
||||
feedSnapshotCommand.Add(feedTypeOption);
|
||||
feedSnapshotCommand.Add(feedUrlOption);
|
||||
feedSnapshotCommand.Add(feedCountOption);
|
||||
feedSnapshotCommand.Add(feedOutputOption);
|
||||
feedSnapshotCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var feed = parseResult.GetValue(feedTypeOption) ?? string.Empty;
|
||||
var url = parseResult.GetValue(feedUrlOption) ?? "http://localhost:5010";
|
||||
var count = parseResult.GetValue(feedCountOption);
|
||||
var output = parseResult.GetValue(feedOutputOption) ?? "src/__Tests/fixtures/feeds";
|
||||
return FeedSnapshotCommand.ExecuteAsync(feed, url, count, output);
|
||||
});
|
||||
|
||||
// VEX Source command (FH-006)
|
||||
var vexSourceCommand = new Command("vex", "Acquire OpenVEX and CSAF samples");
|
||||
var vexSourceArg = new Argument<string>(
|
||||
"source",
|
||||
description: "Source name (list, all, openvex-examples, csaf-redhat, alpine-secdb) or 'list' to see all");
|
||||
var vexCustomUrlOption = new Option<string>(
|
||||
"--url",
|
||||
description: "Custom VEX document URL");
|
||||
var vexOutputOption = new Option<string>(
|
||||
"--output",
|
||||
description: "Output directory",
|
||||
getDefaultValue: () => "src/__Tests/fixtures/vex");
|
||||
var vexSourceArg = new Argument<string>("source")
|
||||
{
|
||||
Description = "Source name (list, all, openvex-examples, csaf-redhat, alpine-secdb) or 'list' to see all"
|
||||
};
|
||||
var vexCustomUrlOption = new Option<string>("--url")
|
||||
{
|
||||
Description = "Custom VEX document URL"
|
||||
};
|
||||
var vexOutputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output directory",
|
||||
DefaultValueFactory = _ => "src/__Tests/fixtures/vex"
|
||||
};
|
||||
|
||||
vexSourceCommand.AddArgument(vexSourceArg);
|
||||
vexSourceCommand.AddOption(vexCustomUrlOption);
|
||||
vexSourceCommand.AddOption(vexOutputOption);
|
||||
vexSourceCommand.SetHandler(VexSourceCommand.ExecuteAsync, vexSourceArg, vexCustomUrlOption, vexOutputOption);
|
||||
vexSourceCommand.Add(vexSourceArg);
|
||||
vexSourceCommand.Add(vexCustomUrlOption);
|
||||
vexSourceCommand.Add(vexOutputOption);
|
||||
vexSourceCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var source = parseResult.GetValue(vexSourceArg) ?? string.Empty;
|
||||
var url = parseResult.GetValue(vexCustomUrlOption);
|
||||
var output = parseResult.GetValue(vexOutputOption) ?? "src/__Tests/fixtures/vex";
|
||||
return VexSourceCommand.ExecuteAsync(source, url, output);
|
||||
});
|
||||
|
||||
// SBOM Golden command (FH-007)
|
||||
var sbomGoldenCommand = new Command("sbom-golden", "Generate SBOM golden fixtures from container images");
|
||||
var sbomImageArg = new Argument<string>(
|
||||
"image",
|
||||
description: "Image key (list, all, alpine-minimal, debian-slim, distroless-static) or custom image ref");
|
||||
var sbomFormatOption = new Option<string>(
|
||||
"--format",
|
||||
description: "SBOM format: cyclonedx, spdx",
|
||||
getDefaultValue: () => "cyclonedx");
|
||||
var sbomScannerOption = new Option<string>(
|
||||
"--scanner",
|
||||
description: "Scanner tool: syft, trivy",
|
||||
getDefaultValue: () => "syft");
|
||||
var sbomOutputOption = new Option<string>(
|
||||
"--output",
|
||||
description: "Output directory",
|
||||
getDefaultValue: () => "src/__Tests/fixtures/sbom");
|
||||
var sbomImageArg = new Argument<string>("image")
|
||||
{
|
||||
Description = "Image key (list, all, alpine-minimal, debian-slim, distroless-static) or custom image ref"
|
||||
};
|
||||
var sbomFormatOption = new Option<string>("--format")
|
||||
{
|
||||
Description = "SBOM format: cyclonedx, spdx",
|
||||
DefaultValueFactory = _ => "cyclonedx"
|
||||
};
|
||||
var sbomScannerOption = new Option<string>("--scanner")
|
||||
{
|
||||
Description = "Scanner tool: syft, trivy",
|
||||
DefaultValueFactory = _ => "syft"
|
||||
};
|
||||
var sbomOutputOption = new Option<string>("--output")
|
||||
{
|
||||
Description = "Output directory",
|
||||
DefaultValueFactory = _ => "src/__Tests/fixtures/sbom"
|
||||
};
|
||||
|
||||
sbomGoldenCommand.AddArgument(sbomImageArg);
|
||||
sbomGoldenCommand.AddOption(sbomFormatOption);
|
||||
sbomGoldenCommand.AddOption(sbomScannerOption);
|
||||
sbomGoldenCommand.AddOption(sbomOutputOption);
|
||||
sbomGoldenCommand.SetHandler(SbomGoldenCommand.ExecuteAsync, sbomImageArg, sbomFormatOption, sbomScannerOption, sbomOutputOption);
|
||||
sbomGoldenCommand.Add(sbomImageArg);
|
||||
sbomGoldenCommand.Add(sbomFormatOption);
|
||||
sbomGoldenCommand.Add(sbomScannerOption);
|
||||
sbomGoldenCommand.Add(sbomOutputOption);
|
||||
sbomGoldenCommand.SetAction((parseResult, _) =>
|
||||
{
|
||||
var image = parseResult.GetValue(sbomImageArg) ?? string.Empty;
|
||||
var format = parseResult.GetValue(sbomFormatOption) ?? "cyclonedx";
|
||||
var scanner = parseResult.GetValue(sbomScannerOption) ?? "syft";
|
||||
var output = parseResult.GetValue(sbomOutputOption) ?? "src/__Tests/fixtures/sbom";
|
||||
return SbomGoldenCommand.ExecuteAsync(image, format, scanner, output);
|
||||
});
|
||||
|
||||
rootCommand.AddCommand(harvestCommand);
|
||||
rootCommand.AddCommand(validateCommand);
|
||||
rootCommand.AddCommand(regenCommand);
|
||||
rootCommand.AddCommand(ociPinCommand);
|
||||
rootCommand.AddCommand(feedSnapshotCommand);
|
||||
rootCommand.AddCommand(vexSourceCommand);
|
||||
rootCommand.AddCommand(sbomGoldenCommand);
|
||||
rootCommand.Add(harvestCommand);
|
||||
rootCommand.Add(validateCommand);
|
||||
rootCommand.Add(regenCommand);
|
||||
rootCommand.Add(ociPinCommand);
|
||||
rootCommand.Add(feedSnapshotCommand);
|
||||
rootCommand.Add(vexSourceCommand);
|
||||
rootCommand.Add(sbomGoldenCommand);
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
return await rootCommand.Parse(args).InvokeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class IntegrationTestFixture : IDisposable
|
||||
private readonly string _fixturesPath;
|
||||
private bool _offlineMode;
|
||||
private Action<string>? _connectionMonitor;
|
||||
private Action<string>? _dnsMonitor;
|
||||
private readonly List<string> _connectionAttempts = [];
|
||||
|
||||
public IntegrationTestFixture()
|
||||
@@ -50,6 +51,16 @@ public class IntegrationTestFixture : IDisposable
|
||||
|
||||
public void SetConnectionMonitor(Action<string>? monitor) => _connectionMonitor = monitor;
|
||||
|
||||
public void SetDnsMonitor(Action<string>? monitor) => _dnsMonitor = monitor;
|
||||
|
||||
public IEnumerable<string> GetFixtureFiles(string category, string pattern)
|
||||
{
|
||||
var categoryPath = Path.Combine(_fixturesPath, category);
|
||||
if (!Directory.Exists(categoryPath))
|
||||
return [];
|
||||
return Directory.GetFiles(categoryPath, pattern);
|
||||
}
|
||||
|
||||
public void RecordConnectionAttempt(string endpoint)
|
||||
{
|
||||
_connectionAttempts.Add(endpoint);
|
||||
|
||||
@@ -11,20 +11,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Scanner\StellaOps.Scanner.WebService\StellaOps.Scanner.WebService.csproj" />
|
||||
<ProjectReference Include="..\..\..\VexLens\StellaOps.VexLens\StellaOps.VexLens.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Verdict\StellaOps.Verdict.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Verdict\StellaOps.Verdict.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user