// -----------------------------------------------------------------------------
// ParityHarness.cs
// Sprint: SPRINT_5100_0008_0001_competitor_parity
// Task: PARITY-5100-003 - Implement parity harness
// Description: Harness for running StellaOps and competitors on same fixtures
// -----------------------------------------------------------------------------
using System.Diagnostics;
using System.Text.Json;
using CliWrap;
using CliWrap.Buffered;
namespace StellaOps.Parity.Tests;
///
/// Parity test harness that runs multiple scanners on the same container image
/// and collects their outputs for comparison.
///
public sealed class ParityHarness : IAsyncDisposable
{
private readonly string _workDir;
private readonly Dictionary _toolVersions = new();
///
/// Pinned tool versions for reproducible testing.
///
public static class PinnedVersions
{
public const string Syft = "1.9.0";
public const string Grype = "0.79.3";
public const string Trivy = "0.54.1";
}
public ParityHarness(string? workDir = null)
{
_workDir = workDir ?? Path.Combine(Path.GetTempPath(), $"parity-{Guid.NewGuid():N}");
Directory.CreateDirectory(_workDir);
}
///
/// Runs all configured scanners on the specified image and returns collected results.
///
public async Task RunAllAsync(
ParityImageFixture fixture,
CancellationToken cancellationToken = default)
{
var result = new ParityRunResult
{
Fixture = fixture,
StartedAtUtc = DateTimeOffset.UtcNow
};
// Run each scanner in parallel
var tasks = new List>
{
RunSyftAsync(fixture.Image, cancellationToken),
RunGrypeAsync(fixture.Image, cancellationToken),
RunTrivyAsync(fixture.Image, cancellationToken)
};
try
{
var outputs = await Task.WhenAll(tasks);
result.SyftOutput = outputs[0];
result.GrypeOutput = outputs[1];
result.TrivyOutput = outputs[2];
}
catch (Exception ex)
{
result.Error = ex.Message;
}
result.CompletedAtUtc = DateTimeOffset.UtcNow;
return result;
}
///
/// Runs Syft SBOM generator on the specified image.
///
public async Task RunSyftAsync(
string image,
CancellationToken cancellationToken = default)
{
var output = new ScannerOutput
{
ToolName = "syft",
ToolVersion = PinnedVersions.Syft,
Image = image,
StartedAtUtc = DateTimeOffset.UtcNow
};
var outputPath = Path.Combine(_workDir, $"syft-{Guid.NewGuid():N}.json");
try
{
var sw = Stopwatch.StartNew();
// syft -o spdx-json=