Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
254
tests/interop/StellaOps.Interop.Tests/InteropTestHarness.cs
Normal file
254
tests/interop/StellaOps.Interop.Tests/InteropTestHarness.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
namespace StellaOps.Interop.Tests;
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Test harness for SBOM interoperability testing.
|
||||
/// Coordinates Syft, Grype, Trivy, and cosign tools.
|
||||
/// </summary>
|
||||
public sealed class InteropTestHarness : IAsyncLifetime
|
||||
{
|
||||
private readonly ToolManager _toolManager;
|
||||
private readonly string _workDir;
|
||||
|
||||
public InteropTestHarness()
|
||||
{
|
||||
_workDir = Path.Combine(Path.GetTempPath(), $"interop-{Guid.NewGuid():N}");
|
||||
_toolManager = new ToolManager(_workDir);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Directory.CreateDirectory(_workDir);
|
||||
|
||||
// Verify tools are available
|
||||
await _toolManager.VerifyToolAsync("syft", "--version");
|
||||
await _toolManager.VerifyToolAsync("grype", "--version");
|
||||
await _toolManager.VerifyToolAsync("cosign", "version");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate SBOM using Syft.
|
||||
/// </summary>
|
||||
public async Task<SbomResult> GenerateSbomWithSyft(
|
||||
string imageRef,
|
||||
SbomFormat format,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var formatArg = format switch
|
||||
{
|
||||
SbomFormat.CycloneDx16 => "cyclonedx-json",
|
||||
SbomFormat.Spdx30 => "spdx-json",
|
||||
_ => throw new ArgumentException($"Unsupported format: {format}")
|
||||
};
|
||||
|
||||
var outputPath = Path.Combine(_workDir, $"sbom-syft-{format}.json");
|
||||
var result = await _toolManager.RunAsync(
|
||||
"syft",
|
||||
$"{imageRef} -o {formatArg}={outputPath}",
|
||||
ct);
|
||||
|
||||
if (!result.Success)
|
||||
return SbomResult.Failed(result.Error ?? "Syft execution failed");
|
||||
|
||||
var content = await File.ReadAllTextAsync(outputPath, ct);
|
||||
var digest = ComputeDigest(content);
|
||||
|
||||
return new SbomResult(
|
||||
Success: true,
|
||||
Path: outputPath,
|
||||
Format: format,
|
||||
Content: content,
|
||||
Digest: digest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate SBOM using Stella scanner.
|
||||
/// </summary>
|
||||
public async Task<SbomResult> GenerateSbomWithStella(
|
||||
string imageRef,
|
||||
SbomFormat format,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var formatArg = format switch
|
||||
{
|
||||
SbomFormat.CycloneDx16 => "cyclonedx",
|
||||
SbomFormat.Spdx30 => "spdx",
|
||||
_ => throw new ArgumentException($"Unsupported format: {format}")
|
||||
};
|
||||
|
||||
var outputPath = Path.Combine(_workDir, $"stella-sbom-{format}.json");
|
||||
var result = await _toolManager.RunAsync(
|
||||
"stella",
|
||||
$"scan {imageRef} --sbom-format {formatArg} --sbom-output {outputPath}",
|
||||
ct);
|
||||
|
||||
if (!result.Success)
|
||||
return SbomResult.Failed(result.Error ?? "Stella execution failed");
|
||||
|
||||
var content = await File.ReadAllTextAsync(outputPath, ct);
|
||||
var digest = ComputeDigest(content);
|
||||
|
||||
return new SbomResult(
|
||||
Success: true,
|
||||
Path: outputPath,
|
||||
Format: format,
|
||||
Content: content,
|
||||
Digest: digest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attest SBOM using cosign.
|
||||
/// </summary>
|
||||
public async Task<AttestationResult> AttestWithCosign(
|
||||
string sbomPath,
|
||||
string imageRef,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var result = await _toolManager.RunAsync(
|
||||
"cosign",
|
||||
$"attest --predicate {sbomPath} --type cyclonedx {imageRef} --yes",
|
||||
ct);
|
||||
|
||||
if (!result.Success)
|
||||
return AttestationResult.Failed(result.Error ?? "Cosign attestation failed");
|
||||
|
||||
return new AttestationResult(Success: true, ImageRef: imageRef);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scan using Grype from SBOM (no image pull).
|
||||
/// </summary>
|
||||
public async Task<GrypeScanResult> ScanWithGrypeFromSbom(
|
||||
string sbomPath,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var outputPath = Path.Combine(_workDir, "grype-findings.json");
|
||||
var result = await _toolManager.RunAsync(
|
||||
"grype",
|
||||
$"sbom:{sbomPath} -o json --file {outputPath}",
|
||||
ct);
|
||||
|
||||
if (!result.Success)
|
||||
return GrypeScanResult.Failed(result.Error ?? "Grype scan failed");
|
||||
|
||||
var content = await File.ReadAllTextAsync(outputPath, ct);
|
||||
var findings = ParseGrypeFindings(content);
|
||||
|
||||
return new GrypeScanResult(
|
||||
Success: true,
|
||||
Findings: findings,
|
||||
RawOutput: content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare findings between Stella and Grype.
|
||||
/// </summary>
|
||||
public FindingsComparisonResult CompareFindings(
|
||||
IReadOnlyList<Finding> stellaFindings,
|
||||
IReadOnlyList<GrypeFinding> grypeFindings,
|
||||
decimal tolerancePercent = 5)
|
||||
{
|
||||
var stellaVulns = stellaFindings
|
||||
.Select(f => (f.VulnerabilityId, f.PackagePurl))
|
||||
.ToHashSet();
|
||||
|
||||
var grypeVulns = grypeFindings
|
||||
.Select(f => (f.VulnerabilityId, f.PackagePurl))
|
||||
.ToHashSet();
|
||||
|
||||
var onlyInStella = stellaVulns.Except(grypeVulns).ToList();
|
||||
var onlyInGrype = grypeVulns.Except(stellaVulns).ToList();
|
||||
var inBoth = stellaVulns.Intersect(grypeVulns).ToList();
|
||||
|
||||
var totalUnique = stellaVulns.Union(grypeVulns).Count();
|
||||
var parityPercent = totalUnique > 0
|
||||
? (decimal)inBoth.Count / totalUnique * 100
|
||||
: 100;
|
||||
|
||||
return new FindingsComparisonResult(
|
||||
ParityPercent: parityPercent,
|
||||
IsWithinTolerance: parityPercent >= (100 - tolerancePercent),
|
||||
StellaTotalFindings: stellaFindings.Count,
|
||||
GrypeTotalFindings: grypeFindings.Count,
|
||||
MatchingFindings: inBoth.Count,
|
||||
OnlyInStella: onlyInStella.Count,
|
||||
OnlyInGrype: onlyInGrype.Count,
|
||||
OnlyInStellaDetails: onlyInStella,
|
||||
OnlyInGrypeDetails: onlyInGrype);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
if (Directory.Exists(_workDir))
|
||||
Directory.Delete(_workDir, recursive: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string ComputeDigest(string content) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content))).ToLowerInvariant();
|
||||
|
||||
private static IReadOnlyList<GrypeFinding> ParseGrypeFindings(string json)
|
||||
{
|
||||
// Placeholder: In real implementation, parse Grype JSON output
|
||||
// For now, return empty list
|
||||
return Array.Empty<GrypeFinding>();
|
||||
}
|
||||
}
|
||||
|
||||
public enum SbomFormat
|
||||
{
|
||||
CycloneDx16,
|
||||
Spdx30
|
||||
}
|
||||
|
||||
public sealed record SbomResult(
|
||||
bool Success,
|
||||
string? Path = null,
|
||||
SbomFormat? Format = null,
|
||||
string? Content = null,
|
||||
string? Digest = null,
|
||||
string? Error = null)
|
||||
{
|
||||
public static SbomResult Failed(string error) => new(false, Error: error);
|
||||
}
|
||||
|
||||
public sealed record AttestationResult(
|
||||
bool Success,
|
||||
string? ImageRef = null,
|
||||
string? Error = null)
|
||||
{
|
||||
public static AttestationResult Failed(string error) => new(false, Error: error);
|
||||
}
|
||||
|
||||
public sealed record GrypeScanResult(
|
||||
bool Success,
|
||||
IReadOnlyList<GrypeFinding>? Findings = null,
|
||||
string? RawOutput = null,
|
||||
string? Error = null)
|
||||
{
|
||||
public static GrypeScanResult Failed(string error) => new(false, Error: error);
|
||||
}
|
||||
|
||||
public sealed record FindingsComparisonResult(
|
||||
decimal ParityPercent,
|
||||
bool IsWithinTolerance,
|
||||
int StellaTotalFindings,
|
||||
int GrypeTotalFindings,
|
||||
int MatchingFindings,
|
||||
int OnlyInStella,
|
||||
int OnlyInGrype,
|
||||
IReadOnlyList<(string VulnId, string Purl)> OnlyInStellaDetails,
|
||||
IReadOnlyList<(string VulnId, string Purl)> OnlyInGrypeDetails);
|
||||
|
||||
public sealed record Finding(
|
||||
string VulnerabilityId,
|
||||
string PackagePurl,
|
||||
string Severity);
|
||||
|
||||
public sealed record GrypeFinding(
|
||||
string VulnerabilityId,
|
||||
string PackagePurl,
|
||||
string Severity);
|
||||
Reference in New Issue
Block a user