Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 0536a4f7d4
1443 changed files with 109671 additions and 7840 deletions

View File

@@ -0,0 +1,117 @@
namespace StellaOps.Interop.Tests.Analysis;
/// <summary>
/// Analyzes and categorizes differences between tool findings.
/// </summary>
public sealed class FindingsParityAnalyzer
{
/// <summary>
/// Categorizes differences between tools.
/// </summary>
public ParityAnalysisReport Analyze(
IReadOnlyList<Finding> stellaFindings,
IReadOnlyList<GrypeFinding> grypeFindings)
{
var differences = new List<FindingDifference>();
// Category 1: Version matching differences
// (e.g., semver vs non-semver interpretation)
var versionDiffs = AnalyzeVersionMatchingDifferences(stellaFindings, grypeFindings);
differences.AddRange(versionDiffs);
// Category 2: Feed coverage differences
// (e.g., Stella has feed X, Grype doesn't)
var feedDiffs = AnalyzeFeedCoverageDifferences(stellaFindings, grypeFindings);
differences.AddRange(feedDiffs);
// Category 3: Package identification differences
// (e.g., different PURL generation)
var purlDiffs = AnalyzePurlDifferences(stellaFindings, grypeFindings);
differences.AddRange(purlDiffs);
// Category 4: VEX application differences
// (e.g., Stella applies VEX, Grype doesn't)
var vexDiffs = AnalyzeVexDifferences(stellaFindings, grypeFindings);
differences.AddRange(vexDiffs);
return new ParityAnalysisReport
{
TotalDifferences = differences.Count,
VersionMatchingDifferences = versionDiffs,
FeedCoverageDifferences = feedDiffs,
PurlDifferences = purlDiffs,
VexDifferences = vexDiffs,
AcceptableDifferences = differences.Count(d => d.IsAcceptable),
RequiresInvestigation = differences.Count(d => !d.IsAcceptable)
};
}
private static List<FindingDifference> AnalyzeVersionMatchingDifferences(
IReadOnlyList<Finding> stellaFindings,
IReadOnlyList<GrypeFinding> grypeFindings)
{
var differences = new List<FindingDifference>();
// TODO: Implement version matching analysis
// Compare how Stella and Grype interpret version ranges
// e.g., >=1.0.0 vs ^1.0.0
return differences;
}
private static List<FindingDifference> AnalyzeFeedCoverageDifferences(
IReadOnlyList<Finding> stellaFindings,
IReadOnlyList<GrypeFinding> grypeFindings)
{
var differences = new List<FindingDifference>();
// TODO: Implement feed coverage analysis
// Identify which vulnerabilities come from feeds only one tool has
// e.g., Stella has GHSA, Grype doesn't, or vice versa
return differences;
}
private static List<FindingDifference> AnalyzePurlDifferences(
IReadOnlyList<Finding> stellaFindings,
IReadOnlyList<GrypeFinding> grypeFindings)
{
var differences = new List<FindingDifference>();
// TODO: Implement PURL difference analysis
// Compare how packages are identified
// e.g., pkg:npm/package vs pkg:npm/package@version
return differences;
}
private static List<FindingDifference> AnalyzeVexDifferences(
IReadOnlyList<Finding> stellaFindings,
IReadOnlyList<GrypeFinding> grypeFindings)
{
var differences = new List<FindingDifference>();
// TODO: Implement VEX application analysis
// Stella applies VEX documents, Grype may not
// This is an acceptable difference
return differences;
}
}
public sealed class ParityAnalysisReport
{
public int TotalDifferences { get; init; }
public IReadOnlyList<FindingDifference> VersionMatchingDifferences { get; init; } = [];
public IReadOnlyList<FindingDifference> FeedCoverageDifferences { get; init; } = [];
public IReadOnlyList<FindingDifference> PurlDifferences { get; init; } = [];
public IReadOnlyList<FindingDifference> VexDifferences { get; init; } = [];
public int AcceptableDifferences { get; init; }
public int RequiresInvestigation { get; init; }
}
public sealed record FindingDifference(
string Category,
string Description,
bool IsAcceptable,
string? Reason = null);

View File

@@ -0,0 +1,129 @@
namespace StellaOps.Interop.Tests.CycloneDx;
[Trait("Category", "Interop")]
[Trait("Format", "CycloneDX")]
public class CycloneDxRoundTripTests : IClassFixture<InteropTestHarness>
{
private readonly InteropTestHarness _harness;
public CycloneDxRoundTripTests(InteropTestHarness harness)
{
_harness = harness;
}
[Theory]
[MemberData(nameof(TestImages))]
public async Task Syft_GeneratesCycloneDx_GrypeCanConsume(string imageRef)
{
// Generate SBOM with Syft
var sbomResult = await _harness.GenerateSbomWithSyft(
imageRef, SbomFormat.CycloneDx16);
sbomResult.Success.Should().BeTrue("Syft should generate CycloneDX SBOM");
// Scan from SBOM with Grype
var grypeResult = await _harness.ScanWithGrypeFromSbom(sbomResult.Path!);
grypeResult.Success.Should().BeTrue("Grype should consume Syft-generated CycloneDX SBOM");
// Grype should be able to parse and find vulnerabilities
grypeResult.Findings.Should().NotBeNull();
}
[Theory]
[MemberData(nameof(TestImages))]
public async Task Stella_GeneratesCycloneDx_GrypeCanConsume(string imageRef)
{
// Generate SBOM with Stella
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.CycloneDx16);
sbomResult.Success.Should().BeTrue("Stella should generate CycloneDX SBOM");
// Scan from SBOM with Grype
var grypeResult = await _harness.ScanWithGrypeFromSbom(sbomResult.Path!);
grypeResult.Success.Should().BeTrue("Grype should consume Stella-generated CycloneDX SBOM");
}
[Theory]
[MemberData(nameof(TestImages))]
[Trait("Category", "Parity")]
public async Task Stella_And_Grype_FindingsParity_Above95Percent(string imageRef)
{
// Generate SBOM with Stella
var stellaSbom = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.CycloneDx16);
stellaSbom.Success.Should().BeTrue();
// TODO: Get Stella findings from scan result
var stellaFindings = new List<Finding>();
// Scan SBOM with Grype
var grypeResult = await _harness.ScanWithGrypeFromSbom(stellaSbom.Path!);
grypeResult.Success.Should().BeTrue();
// Compare findings
var comparison = _harness.CompareFindings(
stellaFindings,
grypeResult.Findings!,
tolerancePercent: 5);
comparison.ParityPercent.Should().BeGreaterOrEqualTo(95,
$"Findings parity {comparison.ParityPercent:F2}% is below 95% threshold. " +
$"Only in Stella: {comparison.OnlyInStella}, Only in Grype: {comparison.OnlyInGrype}");
}
[Theory]
[MemberData(nameof(TestImages))]
[Trait("Category", "Attestation")]
public async Task CycloneDx_Attestation_RoundTrip(string imageRef)
{
// Skip if not in CI - cosign requires credentials
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")))
{
// Skip in local dev
return;
}
// Generate SBOM
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.CycloneDx16);
sbomResult.Success.Should().BeTrue();
// Attest with cosign
var attestResult = await _harness.AttestWithCosign(
sbomResult.Path!, imageRef);
attestResult.Success.Should().BeTrue("Cosign should attest SBOM");
// TODO: Verify attestation
// var verifyResult = await _harness.VerifyCosignAttestation(imageRef);
// verifyResult.Success.Should().BeTrue();
// Digest should match
// var attestedDigest = verifyResult.PredicateDigest;
// attestedDigest.Should().Be(sbomResult.Digest);
}
[Fact]
[Trait("Category", "Schema")]
public async Task Stella_CycloneDx_ValidatesAgainstSchema()
{
var imageRef = "alpine:3.18";
// Generate SBOM
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.CycloneDx16);
sbomResult.Success.Should().BeTrue();
// TODO: Validate against CycloneDX 1.6 JSON schema
sbomResult.Content.Should().NotBeNullOrEmpty();
sbomResult.Content.Should().Contain("\"bomFormat\": \"CycloneDX\"");
sbomResult.Content.Should().Contain("\"specVersion\": \"1.6\"");
}
public static IEnumerable<object[]> TestImages =>
[
["alpine:3.18"],
["debian:12-slim"],
["node:20-alpine"],
["python:3.12-slim"],
["golang:1.22-alpine"]
];
}

View 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);

View File

@@ -0,0 +1,98 @@
namespace StellaOps.Interop.Tests.Spdx;
[Trait("Category", "Interop")]
[Trait("Format", "SPDX")]
public class SpdxRoundTripTests : IClassFixture<InteropTestHarness>
{
private readonly InteropTestHarness _harness;
public SpdxRoundTripTests(InteropTestHarness harness)
{
_harness = harness;
}
[Theory]
[MemberData(nameof(TestImages))]
public async Task Syft_GeneratesSpdx_CanBeParsed(string imageRef)
{
// Generate SBOM with Syft
var sbomResult = await _harness.GenerateSbomWithSyft(
imageRef, SbomFormat.Spdx30);
sbomResult.Success.Should().BeTrue("Syft should generate SPDX SBOM");
// Validate basic SPDX structure
sbomResult.Content.Should().Contain("spdxVersion");
sbomResult.Content.Should().Contain("SPDX-3.0");
}
[Theory]
[MemberData(nameof(TestImages))]
public async Task Stella_GeneratesSpdx_CanBeParsed(string imageRef)
{
// Generate SBOM with Stella
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.Spdx30);
sbomResult.Success.Should().BeTrue("Stella should generate SPDX SBOM");
// Validate basic SPDX structure
sbomResult.Content.Should().Contain("spdxVersion");
sbomResult.Content.Should().Contain("SPDX-3.0");
}
[Theory]
[MemberData(nameof(TestImages))]
[Trait("Category", "Schema")]
public async Task Stella_Spdx_ValidatesAgainstSchema(string imageRef)
{
// Generate SBOM
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.Spdx30);
sbomResult.Success.Should().BeTrue();
// TODO: Validate against SPDX 3.0.1 JSON schema
sbomResult.Content.Should().NotBeNullOrEmpty();
sbomResult.Content.Should().Contain("\"spdxVersion\"");
sbomResult.Content.Should().Contain("\"creationInfo\"");
}
[Fact]
[Trait("Category", "EvidenceChain")]
public async Task Spdx_IncludesEvidenceChain()
{
var imageRef = "alpine:3.18";
// Generate SBOM with evidence
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.Spdx30);
sbomResult.Success.Should().BeTrue();
// TODO: Verify evidence chain is included in SPDX document
// SPDX 3.0 supports relationships that can express evidence chains
sbomResult.Content.Should().Contain("\"relationships\"");
}
[Fact]
[Trait("Category", "Interop")]
public async Task Spdx_CompatibleWithConsumers()
{
var imageRef = "debian:12-slim";
// Generate SBOM
var sbomResult = await _harness.GenerateSbomWithStella(
imageRef, SbomFormat.Spdx30);
sbomResult.Success.Should().BeTrue();
// TODO: Test with SPDX consumers/validators
// For now, just verify structure
sbomResult.Content.Should().NotBeNullOrEmpty();
}
public static IEnumerable<object[]> TestImages =>
[
["alpine:3.18"],
["debian:12-slim"],
["ubuntu:22.04"],
["python:3.12-slim"],
["node:20-alpine"]
];
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.6.6" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
<Using Include="System.Collections.Immutable" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,124 @@
namespace StellaOps.Interop.Tests;
using System.Diagnostics;
using System.Text;
/// <summary>
/// Manages execution of external tools for interop testing.
/// </summary>
public sealed class ToolManager
{
private readonly string _workDir;
public ToolManager(string workDir)
{
_workDir = workDir;
}
/// <summary>
/// Verify that a tool is available and executable.
/// </summary>
public async Task<bool> VerifyToolAsync(string toolName, string testArgs, CancellationToken ct = default)
{
try
{
var result = await RunAsync(toolName, testArgs, ct);
return result.Success || result.ExitCode == 0; // Some tools return 0 even on --version
}
catch
{
return false;
}
}
/// <summary>
/// Run an external tool with arguments.
/// </summary>
public async Task<ToolResult> RunAsync(
string toolName,
string arguments,
CancellationToken ct = default,
int timeoutMs = 300000) // 5 minute default timeout
{
var startInfo = new ProcessStartInfo
{
FileName = toolName,
Arguments = arguments,
WorkingDirectory = _workDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = startInfo };
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();
process.OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
outputBuilder.AppendLine(e.Data);
};
process.ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
errorBuilder.AppendLine(e.Data);
};
try
{
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeoutMs);
await process.WaitForExitAsync(cts.Token);
var output = outputBuilder.ToString();
var error = errorBuilder.ToString();
var exitCode = process.ExitCode;
return new ToolResult(
Success: exitCode == 0,
ExitCode: exitCode,
Output: output,
Error: string.IsNullOrWhiteSpace(error) ? null : error);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited)
process.Kill();
}
catch
{
// Ignore kill failures
}
return new ToolResult(
Success: false,
ExitCode: -1,
Output: outputBuilder.ToString(),
Error: $"Tool execution timed out after {timeoutMs}ms");
}
catch (Exception ex)
{
return new ToolResult(
Success: false,
ExitCode: -1,
Output: outputBuilder.ToString(),
Error: $"Tool execution failed: {ex.Message}");
}
}
}
public sealed record ToolResult(
bool Success,
int ExitCode,
string Output,
string? Error = null);