Files
git.stella-ops.org/tests/integration/StellaOps.Integration.E2E/E2EReproducibilityTestFixture.cs
StellaOps Bot 2a06f780cf sprints work
2025-12-25 12:19:12 +02:00

952 lines
30 KiB
C#

// -----------------------------------------------------------------------------
// E2EReproducibilityTestFixture.cs
// Sprint: SPRINT_8200_0001_0004_e2e_reproducibility_test
// Task: E2E-8200-002 - Create E2EReproducibilityTestFixture with full service composition
// Description: Test fixture providing full pipeline composition for E2E reproducibility tests.
// Supports: ingest → normalize → diff → decide → attest → bundle → reverify
// -----------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Attestor.Dsse;
using StellaOps.Canonical.Json;
using StellaOps.Policy.Deltas;
using Testcontainers.PostgreSql;
namespace StellaOps.Integration.E2E;
/// <summary>
/// Test fixture for end-to-end reproducibility tests.
/// Provides a fully configured test environment with:
/// - PostgreSQL database via Testcontainers
/// - Mock advisory feeds
/// - Policy engine with test policies
/// - Attestor for DSSE envelope creation
/// - Full pipeline execution capability
/// </summary>
public sealed class E2EReproducibilityTestFixture : IAsyncLifetime
{
private PostgreSqlContainer? _postgresContainer;
private WebApplicationFactory<Program>? _factory;
private ECDsa? _signingKey;
private bool _initialized;
/// <summary>
/// Gets the frozen timestamp used for deterministic tests.
/// </summary>
public DateTimeOffset FrozenTimestamp { get; } = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
/// <summary>
/// Initializes the test fixture, starting required services.
/// </summary>
public async Task InitializeAsync()
{
if (_initialized)
return;
// Generate deterministic signing key from fixed seed
_signingKey = GenerateDeterministicKey(42);
// Start PostgreSQL container
_postgresContainer = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.WithDatabase("stellaops_e2e_test")
.WithUsername("e2e_test_user")
.WithPassword("e2e_test_password")
.WithPortBinding(5432, true)
.Build();
await _postgresContainer.StartAsync();
// Create the test web application factory
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:ScannerDb"] = _postgresContainer.GetConnectionString(),
["Scanner:Authority:Enabled"] = "false",
["Scanner:AllowAnonymous"] = "true",
["Scanner:ProofChain:Enabled"] = "true",
["Scanner:ProofChain:SigningKeyId"] = "e2e-test-key",
["Scanner:ProofChain:AutoSign"] = "true",
["Scanner:Determinism:FrozenClock"] = "true",
["Scanner:Determinism:FrozenTimestamp"] = FrozenTimestamp.ToString("O"),
["Logging:LogLevel:Default"] = "Warning"
});
});
builder.ConfigureServices(services =>
{
services.AddLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Warning);
});
});
});
_initialized = true;
}
/// <summary>
/// Creates an HTTP client for the test application.
/// </summary>
public async Task<HttpClient> CreateClientAsync()
{
if (!_initialized)
{
await InitializeAsync();
}
return _factory!.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}
/// <summary>
/// Creates a snapshot of all inputs with computed hashes for verification.
/// </summary>
public async Task<InputSnapshot> SnapshotInputsAsync(
string? sbomFixturePath = null,
string? advisoryFeedPath = null,
string? policyPackPath = null,
string? vexDocumentPath = null)
{
// Load default fixtures if not specified
var sbomContent = sbomFixturePath is not null
? await File.ReadAllBytesAsync(sbomFixturePath)
: CreateMinimalSbom();
var advisoryFeed = advisoryFeedPath is not null
? await File.ReadAllBytesAsync(advisoryFeedPath)
: CreateMockAdvisoryFeed();
var policyPack = policyPackPath is not null
? await File.ReadAllBytesAsync(policyPackPath)
: CreateDefaultPolicyPack();
var vexDocument = vexDocumentPath is not null
? await File.ReadAllBytesAsync(vexDocumentPath)
: null;
return new InputSnapshot
{
Sbom = sbomContent,
SbomHash = ComputeHash(sbomContent),
AdvisoryFeed = advisoryFeed,
AdvisoryFeedHash = ComputeHash(advisoryFeed),
PolicyPack = policyPack,
PolicyPackHash = ComputeHash(policyPack),
VexDocument = vexDocument,
VexDocumentHash = vexDocument is not null ? ComputeHash(vexDocument) : null,
SnapshotTimestamp = FrozenTimestamp
};
}
/// <summary>
/// Executes the full pipeline with the given inputs.
/// </summary>
public async Task<PipelineResult> RunFullPipelineAsync(InputSnapshot inputs)
{
// Stage 1: Ingest advisories
var advisories = await IngestAdvisoriesAsync(inputs.AdvisoryFeed);
// Stage 2: Normalize advisories
var normalized = await NormalizeAdvisoriesAsync(advisories);
// Stage 3: Diff SBOM against advisories
var diff = await ComputeDiffAsync(inputs.Sbom, normalized);
// Stage 4: Evaluate policy and compute verdict
var verdict = await EvaluatePolicyAsync(diff, inputs.PolicyPack, inputs.VexDocument);
// Stage 5: Create DSSE attestation
var envelope = await CreateAttestationAsync(verdict);
// Stage 6: Package into bundle
var bundle = await CreateBundleAsync(envelope, inputs);
return new PipelineResult
{
VerdictId = verdict.VerdictId,
VerdictHash = ComputeHash(SerializeVerdict(verdict)),
EnvelopeHash = ComputeHash(SerializeEnvelope(envelope)),
BundleManifest = bundle.Manifest,
BundleManifestHash = ComputeHash(bundle.Manifest),
ExecutionTimestamp = FrozenTimestamp
};
}
#region Stage 1: Ingest
/// <summary>
/// Ingests advisory feed data.
/// </summary>
public Task<IReadOnlyList<AdvisoryRecord>> IngestAdvisoriesAsync(byte[] feedData)
{
// Parse advisory feed (mock implementation for E2E tests)
var advisories = ParseAdvisoryFeed(feedData);
return Task.FromResult(advisories);
}
private static IReadOnlyList<AdvisoryRecord> ParseAdvisoryFeed(byte[] feedData)
{
// For E2E tests, parse the mock feed format
var json = System.Text.Encoding.UTF8.GetString(feedData);
using var doc = JsonDocument.Parse(json);
var advisories = new List<AdvisoryRecord>();
foreach (var element in doc.RootElement.GetProperty("advisories").EnumerateArray())
{
advisories.Add(new AdvisoryRecord
{
Id = element.GetProperty("id").GetString()!,
CveId = element.GetProperty("cveId").GetString(),
Severity = element.GetProperty("severity").GetString()!,
AffectedPackages = element.GetProperty("affected").EnumerateArray()
.Select(a => a.GetString()!)
.ToList(),
FixedVersions = element.TryGetProperty("fixed", out var fixedProp)
? fixedProp.EnumerateArray().Select(f => f.GetString()!).ToList()
: []
});
}
// Sort for determinism
return advisories.OrderBy(a => a.Id, StringComparer.Ordinal).ToList();
}
#endregion
#region Stage 2: Normalize
/// <summary>
/// Normalizes and deduplicates advisories.
/// </summary>
public Task<NormalizedAdvisories> NormalizeAdvisoriesAsync(IReadOnlyList<AdvisoryRecord> advisories)
{
// Deduplicate by CVE ID
var uniqueByCve = advisories
.GroupBy(a => a.CveId ?? a.Id)
.Select(g => g.OrderBy(a => a.Id, StringComparer.Ordinal).First())
.OrderBy(a => a.CveId ?? a.Id, StringComparer.Ordinal)
.ToList();
var normalized = new NormalizedAdvisories
{
Advisories = uniqueByCve,
NormalizationTimestamp = FrozenTimestamp,
ContentHash = ComputeHash(SerializeAdvisories(uniqueByCve))
};
return Task.FromResult(normalized);
}
private static byte[] SerializeAdvisories(IReadOnlyList<AdvisoryRecord> advisories)
{
var serializable = advisories.Select(a => new
{
id = a.Id,
cveId = a.CveId,
severity = a.Severity,
affected = a.AffectedPackages,
fixed_ = a.FixedVersions
}).ToList();
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(serializable));
}
#endregion
#region Stage 3: Diff
/// <summary>
/// Computes diff between SBOM and advisories.
/// </summary>
public Task<DiffResult> ComputeDiffAsync(byte[] sbomData, NormalizedAdvisories advisories)
{
// Parse SBOM and find affected components
var sbom = ParseSbom(sbomData);
var findings = new List<Finding>();
foreach (var component in sbom.Components)
{
foreach (var advisory in advisories.Advisories)
{
if (advisory.AffectedPackages.Any(pkg =>
pkg.Equals(component.Purl, StringComparison.OrdinalIgnoreCase) ||
pkg.Contains(component.Name, StringComparison.OrdinalIgnoreCase)))
{
findings.Add(new Finding
{
Id = $"finding:{advisory.CveId ?? advisory.Id}:{component.Purl}",
CveId = advisory.CveId ?? advisory.Id,
Severity = advisory.Severity,
AffectedComponent = component.Purl,
ComponentVersion = component.Version,
FixedVersions = advisory.FixedVersions
});
}
}
}
// Sort findings for determinism
var sortedFindings = findings
.OrderBy(f => f.CveId, StringComparer.Ordinal)
.ThenBy(f => f.AffectedComponent, StringComparer.Ordinal)
.ToList();
var diff = new DiffResult
{
Findings = sortedFindings,
SbomDigest = ComputeHash(sbomData),
AdvisoryDigest = advisories.ContentHash,
DiffTimestamp = FrozenTimestamp
};
return Task.FromResult(diff);
}
private static SbomData ParseSbom(byte[] sbomData)
{
var json = System.Text.Encoding.UTF8.GetString(sbomData);
using var doc = JsonDocument.Parse(json);
var components = new List<SbomComponent>();
foreach (var element in doc.RootElement.GetProperty("components").EnumerateArray())
{
components.Add(new SbomComponent
{
Name = element.GetProperty("name").GetString()!,
Version = element.GetProperty("version").GetString()!,
Purl = element.GetProperty("purl").GetString()!
});
}
return new SbomData
{
Components = components.OrderBy(c => c.Purl, StringComparer.Ordinal).ToList()
};
}
#endregion
#region Stage 4: Decide
/// <summary>
/// Evaluates policy and computes verdict.
/// </summary>
public Task<DeltaVerdict> EvaluatePolicyAsync(DiffResult diff, byte[] policyPack, byte[]? vexDocument)
{
// Parse VEX document if provided for exception handling
var exceptions = vexDocument is not null
? ParseVexExceptions(vexDocument)
: [];
// Evaluate findings against policy
var blockingDrivers = new List<DeltaDriver>();
var warningDrivers = new List<DeltaDriver>();
var appliedExceptions = new List<string>();
foreach (var finding in diff.Findings)
{
// Check if finding is excepted via VEX
var exception = exceptions.FirstOrDefault(e =>
e.VulnerabilityId.Equals(finding.CveId, StringComparison.OrdinalIgnoreCase));
if (exception is not null)
{
appliedExceptions.Add(exception.Id);
continue;
}
var driver = new DeltaDriver
{
Type = "new-finding",
Severity = MapSeverity(finding.Severity),
Description = $"Vulnerability {finding.CveId} found in {finding.AffectedComponent}",
CveId = finding.CveId,
Purl = finding.AffectedComponent
};
if (IsBlockingSeverity(finding.Severity))
{
blockingDrivers.Add(driver);
}
else
{
warningDrivers.Add(driver);
}
}
// Sort drivers for determinism
blockingDrivers = [.. blockingDrivers.OrderBy(d => d.CveId, StringComparer.Ordinal)
.ThenBy(d => d.Purl, StringComparer.Ordinal)];
warningDrivers = [.. warningDrivers.OrderBy(d => d.CveId, StringComparer.Ordinal)
.ThenBy(d => d.Purl, StringComparer.Ordinal)];
appliedExceptions = [.. appliedExceptions.Order(StringComparer.Ordinal)];
// Compute gate level
var gateLevel = blockingDrivers.Count > 0 ? DeltaGateLevel.G4 : DeltaGateLevel.G1;
// Build verdict with content-addressed ID
var deltaId = $"delta:sha256:{ComputeHashString(System.Text.Encoding.UTF8.GetBytes(
CanonJson.Serialize(new { diff.SbomDigest, diff.AdvisoryDigest })))}";
var builder = new DeltaVerdictBuilder()
.WithGate(gateLevel);
foreach (var driver in blockingDrivers)
{
builder.AddBlockingDriver(driver);
}
foreach (var driver in warningDrivers)
{
builder.AddWarningDriver(driver);
}
foreach (var exception in appliedExceptions)
{
builder.AddException(exception);
}
var verdict = builder.Build(deltaId);
return Task.FromResult(verdict);
}
private static IReadOnlyList<VexException> ParseVexExceptions(byte[] vexData)
{
var json = System.Text.Encoding.UTF8.GetString(vexData);
using var doc = JsonDocument.Parse(json);
var exceptions = new List<VexException>();
if (doc.RootElement.TryGetProperty("statements", out var statements))
{
foreach (var stmt in statements.EnumerateArray())
{
if (stmt.GetProperty("status").GetString() == "not_affected")
{
exceptions.Add(new VexException
{
Id = stmt.GetProperty("id").GetString()!,
VulnerabilityId = stmt.GetProperty("vulnerability").GetString()!,
Status = "not_affected",
Justification = stmt.TryGetProperty("justification", out var j) ? j.GetString() : null
});
}
}
}
return exceptions.OrderBy(e => e.VulnerabilityId, StringComparer.Ordinal).ToList();
}
private static DeltaDriverSeverity MapSeverity(string severity) => severity.ToUpperInvariant() switch
{
"CRITICAL" => DeltaDriverSeverity.Critical,
"HIGH" => DeltaDriverSeverity.High,
"MEDIUM" => DeltaDriverSeverity.Medium,
"LOW" => DeltaDriverSeverity.Low,
_ => DeltaDriverSeverity.Unknown
};
private static bool IsBlockingSeverity(string severity) =>
severity.Equals("CRITICAL", StringComparison.OrdinalIgnoreCase) ||
severity.Equals("HIGH", StringComparison.OrdinalIgnoreCase);
#endregion
#region Stage 5: Attest
/// <summary>
/// Creates DSSE attestation for the verdict.
/// </summary>
public Task<DsseEnvelopeData> CreateAttestationAsync(DeltaVerdict verdict)
{
// Serialize verdict to canonical JSON
var payload = SerializeVerdict(verdict);
// Sign using deterministic key
var signature = SignPayload(payload);
var envelope = new DsseEnvelopeData
{
PayloadType = "application/vnd.stellaops.verdict+json",
Payload = payload,
Signatures =
[
new DsseSignatureData
{
KeyId = "e2e-test-key",
Signature = signature
}
]
};
return Task.FromResult(envelope);
}
private byte[] SignPayload(byte[] payload)
{
// Create PAE (Pre-Authentication Encoding) as per DSSE spec
var payloadType = "application/vnd.stellaops.verdict+json"u8.ToArray();
var pae = CreatePae(payloadType, payload);
// Sign with ECDSA P-256
return _signingKey!.SignData(pae, HashAlgorithmName.SHA256);
}
private static byte[] CreatePae(byte[] payloadType, byte[] payload)
{
// PAE(type, payload) = "DSSEv1" || SP || LEN(type) || SP || type || SP || LEN(payload) || SP || payload
var parts = new List<byte>();
parts.AddRange("DSSEv1 "u8.ToArray());
parts.AddRange(System.Text.Encoding.UTF8.GetBytes(payloadType.Length.ToString()));
parts.Add((byte)' ');
parts.AddRange(payloadType);
parts.Add((byte)' ');
parts.AddRange(System.Text.Encoding.UTF8.GetBytes(payload.Length.ToString()));
parts.Add((byte)' ');
parts.AddRange(payload);
return [.. parts];
}
#endregion
#region Stage 6: Bundle
/// <summary>
/// Creates a bundle containing the attestation and all artifacts.
/// </summary>
public Task<BundleResult> CreateBundleAsync(DsseEnvelopeData envelope, InputSnapshot inputs)
{
// Create manifest with all artifact hashes
var manifest = new BundleManifest
{
Version = "1.0",
CreatedAt = FrozenTimestamp,
Artifacts = new Dictionary<string, string>
{
["sbom"] = inputs.SbomHash,
["advisory-feed"] = inputs.AdvisoryFeedHash,
["policy-pack"] = inputs.PolicyPackHash,
["envelope"] = ComputeHashString(SerializeEnvelope(envelope))
}
};
if (inputs.VexDocumentHash is not null)
{
manifest.Artifacts["vex-document"] = inputs.VexDocumentHash;
}
// Serialize manifest deterministically
var manifestBytes = System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(manifest));
var bundle = new BundleResult
{
Manifest = manifestBytes,
Envelope = envelope,
ManifestHash = ComputeHash(manifestBytes)
};
return Task.FromResult(bundle);
}
#endregion
#region Serialization Helpers
private static byte[] SerializeVerdict(DeltaVerdict verdict)
{
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(verdict));
}
private static byte[] SerializeEnvelope(DsseEnvelopeData envelope)
{
var obj = new
{
payloadType = envelope.PayloadType,
payload = Convert.ToBase64String(envelope.Payload),
signatures = envelope.Signatures.Select(s => new
{
keyid = s.KeyId,
sig = Convert.ToBase64String(s.Signature)
}).ToArray()
};
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(obj));
}
#endregion
#region Hashing Helpers
/// <summary>
/// Computes SHA-256 hash of data and returns as hex string.
/// </summary>
public static string ComputeHash(byte[] data)
{
var hash = SHA256.HashData(data);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
/// <summary>
/// Computes SHA-256 hash of data and returns hex string without prefix.
/// </summary>
public static string ComputeHashString(byte[] data)
{
var hash = SHA256.HashData(data);
return Convert.ToHexStringLower(hash);
}
#endregion
#region Test Data Factories
/// <summary>
/// Creates a minimal SBOM for testing.
/// </summary>
public static byte[] CreateMinimalSbom()
{
var sbom = new
{
bomFormat = "CycloneDX",
specVersion = "1.5",
version = 1,
components = new[]
{
new { name = "lodash", version = "4.17.20", purl = "pkg:npm/lodash@4.17.20" },
new { name = "axios", version = "0.21.0", purl = "pkg:npm/axios@0.21.0" },
new { name = "moment", version = "2.29.0", purl = "pkg:npm/moment@2.29.0" }
}
};
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(sbom));
}
/// <summary>
/// Creates a mock advisory feed for testing.
/// </summary>
public static byte[] CreateMockAdvisoryFeed()
{
var feed = new
{
advisories = new[]
{
new
{
id = "GHSA-2024-0001",
cveId = "CVE-2024-0001",
severity = "CRITICAL",
affected = new[] { "pkg:npm/lodash@4.17.20" },
@fixed = new[] { "pkg:npm/lodash@4.17.21" }
},
new
{
id = "GHSA-2024-0002",
cveId = "CVE-2024-0002",
severity = "HIGH",
affected = new[] { "pkg:npm/axios@0.21.0" },
@fixed = new[] { "pkg:npm/axios@0.21.1" }
},
new
{
id = "GHSA-2024-0003",
cveId = "CVE-2024-0003",
severity = "LOW",
affected = new[] { "pkg:npm/moment@2.29.0" },
@fixed = new[] { "pkg:npm/moment@2.29.4" }
}
}
};
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(feed));
}
/// <summary>
/// Creates a default policy pack for testing.
/// </summary>
public static byte[] CreateDefaultPolicyPack()
{
var policy = new
{
version = "1.0",
rules = new[]
{
new { severity = "CRITICAL", action = "block" },
new { severity = "HIGH", action = "block" },
new { severity = "MEDIUM", action = "warn" },
new { severity = "LOW", action = "warn" }
}
};
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(policy));
}
/// <summary>
/// Creates a VEX document with exceptions.
/// </summary>
public static byte[] CreateVexDocumentWithExceptions(params string[] exceptedCveIds)
{
var statements = exceptedCveIds.Select((cve, i) => new
{
id = $"vex-exception-{i + 1:D3}",
vulnerability = cve,
status = "not_affected",
justification = "vulnerable_code_not_in_execute_path"
}).ToArray();
var vex = new
{
@context = "https://openvex.dev/ns/v0.2.0",
id = "https://stellaops.test/vex/test-001",
statements
};
return System.Text.Encoding.UTF8.GetBytes(CanonJson.Serialize(vex));
}
#endregion
#region Key Generation
/// <summary>
/// Generates a deterministic ECDSA key from a seed.
/// </summary>
private static ECDsa GenerateDeterministicKey(int seed)
{
// Use a deterministic RNG seeded from the input
var rng = new DeterministicRng(seed);
var keyBytes = new byte[32];
rng.GetBytes(keyBytes);
// Create ECDSA key from the deterministic bytes
var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
// Import deterministic private key
var parameters = new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
D = keyBytes,
Q = default // Will be computed from D
};
// Compute public key from private key
var tempKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
tempKey.ImportParameters(new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
D = keyBytes
});
var exported = tempKey.ExportParameters(true);
parameters.Q = exported.Q;
ecdsa.ImportParameters(parameters);
return ecdsa;
}
/// <summary>
/// Gets the public key for verification.
/// </summary>
public byte[] GetPublicKey()
{
return _signingKey!.ExportSubjectPublicKeyInfo();
}
#endregion
/// <summary>
/// Disposes of the test fixture resources.
/// </summary>
public async Task DisposeAsync()
{
_signingKey?.Dispose();
_factory?.Dispose();
if (_postgresContainer is not null)
{
await _postgresContainer.DisposeAsync();
}
}
}
/// <summary>
/// Deterministic random number generator for key generation.
/// </summary>
internal sealed class DeterministicRng(int seed)
{
private readonly Random _random = new(seed);
public void GetBytes(byte[] data)
{
_random.NextBytes(data);
}
}
#region Data Transfer Objects
/// <summary>
/// Snapshot of all pipeline inputs with hashes.
/// </summary>
public sealed class InputSnapshot
{
public required byte[] Sbom { get; init; }
public required string SbomHash { get; init; }
public required byte[] AdvisoryFeed { get; init; }
public required string AdvisoryFeedHash { get; init; }
public required byte[] PolicyPack { get; init; }
public required string PolicyPackHash { get; init; }
public byte[]? VexDocument { get; init; }
public string? VexDocumentHash { get; init; }
public DateTimeOffset SnapshotTimestamp { get; init; }
}
/// <summary>
/// Result of full pipeline execution.
/// </summary>
public sealed class PipelineResult
{
public required string VerdictId { get; init; }
public required string VerdictHash { get; init; }
public required string EnvelopeHash { get; init; }
public required byte[] BundleManifest { get; init; }
public required string BundleManifestHash { get; init; }
public DateTimeOffset ExecutionTimestamp { get; init; }
}
/// <summary>
/// Advisory record from ingestion.
/// </summary>
public sealed class AdvisoryRecord
{
public required string Id { get; init; }
public string? CveId { get; init; }
public required string Severity { get; init; }
public required IReadOnlyList<string> AffectedPackages { get; init; }
public IReadOnlyList<string> FixedVersions { get; init; } = [];
}
/// <summary>
/// Normalized advisories after deduplication.
/// </summary>
public sealed class NormalizedAdvisories
{
public required IReadOnlyList<AdvisoryRecord> Advisories { get; init; }
public DateTimeOffset NormalizationTimestamp { get; init; }
public required string ContentHash { get; init; }
}
/// <summary>
/// SBOM component data.
/// </summary>
public sealed class SbomComponent
{
public required string Name { get; init; }
public required string Version { get; init; }
public required string Purl { get; init; }
}
/// <summary>
/// Parsed SBOM data.
/// </summary>
public sealed class SbomData
{
public required IReadOnlyList<SbomComponent> Components { get; init; }
}
/// <summary>
/// Security finding from diff.
/// </summary>
public sealed class Finding
{
public required string Id { get; init; }
public required string CveId { get; init; }
public required string Severity { get; init; }
public required string AffectedComponent { get; init; }
public required string ComponentVersion { get; init; }
public IReadOnlyList<string> FixedVersions { get; init; } = [];
}
/// <summary>
/// Result of diffing SBOM against advisories.
/// </summary>
public sealed class DiffResult
{
public required IReadOnlyList<Finding> Findings { get; init; }
public required string SbomDigest { get; init; }
public required string AdvisoryDigest { get; init; }
public DateTimeOffset DiffTimestamp { get; init; }
}
/// <summary>
/// VEX exception from VEX document.
/// </summary>
public sealed class VexException
{
public required string Id { get; init; }
public required string VulnerabilityId { get; init; }
public required string Status { get; init; }
public string? Justification { get; init; }
}
/// <summary>
/// DSSE envelope data.
/// </summary>
public sealed class DsseEnvelopeData
{
public required string PayloadType { get; init; }
public required byte[] Payload { get; init; }
public required IReadOnlyList<DsseSignatureData> Signatures { get; init; }
}
/// <summary>
/// DSSE signature data.
/// </summary>
public sealed class DsseSignatureData
{
public required string KeyId { get; init; }
public required byte[] Signature { get; init; }
}
/// <summary>
/// Bundle manifest structure.
/// </summary>
public sealed class BundleManifest
{
public required string Version { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public required Dictionary<string, string> Artifacts { get; init; }
}
/// <summary>
/// Result of bundle creation.
/// </summary>
public sealed class BundleResult
{
public required byte[] Manifest { get; init; }
public required DsseEnvelopeData Envelope { get; init; }
public required string ManifestHash { get; init; }
}
#endregion
/// <summary>
/// Placeholder for Program class detection.
/// The actual Program class is from Scanner.WebService.
/// </summary>
#pragma warning disable CA1050 // Declare types in namespaces
public partial class Program { }
#pragma warning restore CA1050