- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism. - Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions. - Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests. - Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
323 lines
8.8 KiB
C#
323 lines
8.8 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Testing.Determinism;
|
|
|
|
/// <summary>
|
|
/// Determinism manifest tracking artifact reproducibility with canonical bytes hash,
|
|
/// version stamps, and toolchain information.
|
|
/// </summary>
|
|
public sealed record DeterminismManifest
|
|
{
|
|
/// <summary>
|
|
/// Version of this manifest schema (currently "1.0").
|
|
/// </summary>
|
|
[JsonPropertyName("schemaVersion")]
|
|
public required string SchemaVersion { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact being tracked for determinism.
|
|
/// </summary>
|
|
[JsonPropertyName("artifact")]
|
|
public required ArtifactInfo Artifact { get; init; }
|
|
|
|
/// <summary>
|
|
/// Hash of the canonical representation of the artifact.
|
|
/// </summary>
|
|
[JsonPropertyName("canonicalHash")]
|
|
public required CanonicalHashInfo CanonicalHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// Version stamps of all inputs used to generate the artifact.
|
|
/// </summary>
|
|
[JsonPropertyName("inputs")]
|
|
public InputStamps? Inputs { get; init; }
|
|
|
|
/// <summary>
|
|
/// Toolchain version information.
|
|
/// </summary>
|
|
[JsonPropertyName("toolchain")]
|
|
public required ToolchainInfo Toolchain { get; init; }
|
|
|
|
/// <summary>
|
|
/// UTC timestamp when artifact was generated (ISO 8601).
|
|
/// </summary>
|
|
[JsonPropertyName("generatedAt")]
|
|
public required DateTimeOffset GeneratedAt { get; init; }
|
|
|
|
/// <summary>
|
|
/// Reproducibility metadata.
|
|
/// </summary>
|
|
[JsonPropertyName("reproducibility")]
|
|
public ReproducibilityMetadata? Reproducibility { get; init; }
|
|
|
|
/// <summary>
|
|
/// Verification instructions for reproducing the artifact.
|
|
/// </summary>
|
|
[JsonPropertyName("verification")]
|
|
public VerificationInfo? Verification { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional cryptographic signatures of this manifest.
|
|
/// </summary>
|
|
[JsonPropertyName("signatures")]
|
|
public IReadOnlyList<SignatureInfo>? Signatures { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Artifact being tracked for determinism.
|
|
/// </summary>
|
|
public sealed record ArtifactInfo
|
|
{
|
|
/// <summary>
|
|
/// Type of artifact.
|
|
/// </summary>
|
|
[JsonPropertyName("type")]
|
|
public required string Type { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact identifier or name.
|
|
/// </summary>
|
|
[JsonPropertyName("name")]
|
|
public required string Name { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact version or timestamp.
|
|
/// </summary>
|
|
[JsonPropertyName("version")]
|
|
public required string Version { get; init; }
|
|
|
|
/// <summary>
|
|
/// Artifact format (e.g., 'SPDX 3.0.1', 'CycloneDX 1.6', 'OpenVEX').
|
|
/// </summary>
|
|
[JsonPropertyName("format")]
|
|
public string? Format { get; init; }
|
|
|
|
/// <summary>
|
|
/// Additional artifact-specific metadata.
|
|
/// </summary>
|
|
[JsonPropertyName("metadata")]
|
|
public IReadOnlyDictionary<string, object?>? Metadata { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hash of the canonical representation of the artifact.
|
|
/// </summary>
|
|
public sealed record CanonicalHashInfo
|
|
{
|
|
/// <summary>
|
|
/// Hash algorithm used (SHA-256, SHA-384, SHA-512).
|
|
/// </summary>
|
|
[JsonPropertyName("algorithm")]
|
|
public required string Algorithm { get; init; }
|
|
|
|
/// <summary>
|
|
/// Hex-encoded hash value.
|
|
/// </summary>
|
|
[JsonPropertyName("value")]
|
|
public required string Value { get; init; }
|
|
|
|
/// <summary>
|
|
/// Encoding of the hash value (hex or base64).
|
|
/// </summary>
|
|
[JsonPropertyName("encoding")]
|
|
public required string Encoding { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Version stamps of all inputs used to generate the artifact.
|
|
/// </summary>
|
|
public sealed record InputStamps
|
|
{
|
|
/// <summary>
|
|
/// SHA-256 hash of the vulnerability feed snapshot used.
|
|
/// </summary>
|
|
[JsonPropertyName("feedSnapshotHash")]
|
|
public string? FeedSnapshotHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// SHA-256 hash of the policy manifest used.
|
|
/// </summary>
|
|
[JsonPropertyName("policyManifestHash")]
|
|
public string? PolicyManifestHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// Git commit SHA or source code hash.
|
|
/// </summary>
|
|
[JsonPropertyName("sourceCodeHash")]
|
|
public string? SourceCodeHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// Hash of dependency lockfile (e.g., package-lock.json, Cargo.lock).
|
|
/// </summary>
|
|
[JsonPropertyName("dependencyLockfileHash")]
|
|
public string? DependencyLockfileHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// Container base image digest (sha256:...).
|
|
/// </summary>
|
|
[JsonPropertyName("baseImageDigest")]
|
|
public string? BaseImageDigest { get; init; }
|
|
|
|
/// <summary>
|
|
/// Hashes of all VEX documents used as input.
|
|
/// </summary>
|
|
[JsonPropertyName("vexDocumentHashes")]
|
|
public IReadOnlyList<string>? VexDocumentHashes { get; init; }
|
|
|
|
/// <summary>
|
|
/// Custom input hashes specific to artifact type.
|
|
/// </summary>
|
|
[JsonPropertyName("custom")]
|
|
public IReadOnlyDictionary<string, string>? Custom { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toolchain version information.
|
|
/// </summary>
|
|
public sealed record ToolchainInfo
|
|
{
|
|
/// <summary>
|
|
/// Runtime platform (e.g., '.NET 10.0', 'Node.js 20.0').
|
|
/// </summary>
|
|
[JsonPropertyName("platform")]
|
|
public required string Platform { get; init; }
|
|
|
|
/// <summary>
|
|
/// Toolchain component versions.
|
|
/// </summary>
|
|
[JsonPropertyName("components")]
|
|
public required IReadOnlyList<ComponentInfo> Components { get; init; }
|
|
|
|
/// <summary>
|
|
/// Compiler information if applicable.
|
|
/// </summary>
|
|
[JsonPropertyName("compiler")]
|
|
public CompilerInfo? Compiler { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toolchain component version.
|
|
/// </summary>
|
|
public sealed record ComponentInfo
|
|
{
|
|
/// <summary>
|
|
/// Component name (e.g., 'StellaOps.Scanner', 'CycloneDX Generator').
|
|
/// </summary>
|
|
[JsonPropertyName("name")]
|
|
public required string Name { get; init; }
|
|
|
|
/// <summary>
|
|
/// Semantic version or git SHA.
|
|
/// </summary>
|
|
[JsonPropertyName("version")]
|
|
public required string Version { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional: SHA-256 hash of the component binary.
|
|
/// </summary>
|
|
[JsonPropertyName("hash")]
|
|
public string? Hash { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compiler information.
|
|
/// </summary>
|
|
public sealed record CompilerInfo
|
|
{
|
|
/// <summary>
|
|
/// Compiler name (e.g., 'Roslyn', 'rustc').
|
|
/// </summary>
|
|
[JsonPropertyName("name")]
|
|
public required string Name { get; init; }
|
|
|
|
/// <summary>
|
|
/// Compiler version.
|
|
/// </summary>
|
|
[JsonPropertyName("version")]
|
|
public required string Version { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reproducibility metadata.
|
|
/// </summary>
|
|
public sealed record ReproducibilityMetadata
|
|
{
|
|
/// <summary>
|
|
/// Deterministic random seed if used.
|
|
/// </summary>
|
|
[JsonPropertyName("deterministicSeed")]
|
|
public int? DeterministicSeed { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether system clock was fixed during generation.
|
|
/// </summary>
|
|
[JsonPropertyName("clockFixed")]
|
|
public bool? ClockFixed { get; init; }
|
|
|
|
/// <summary>
|
|
/// Ordering guarantee for collections in output.
|
|
/// </summary>
|
|
[JsonPropertyName("orderingGuarantee")]
|
|
public string? OrderingGuarantee { get; init; }
|
|
|
|
/// <summary>
|
|
/// Normalization rules applied (e.g., 'UTF-8', 'LF line endings', 'no whitespace').
|
|
/// </summary>
|
|
[JsonPropertyName("normalizationRules")]
|
|
public IReadOnlyList<string>? NormalizationRules { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verification instructions for reproducing the artifact.
|
|
/// </summary>
|
|
public sealed record VerificationInfo
|
|
{
|
|
/// <summary>
|
|
/// Command to regenerate the artifact.
|
|
/// </summary>
|
|
[JsonPropertyName("command")]
|
|
public string? Command { get; init; }
|
|
|
|
/// <summary>
|
|
/// Expected SHA-256 hash after reproduction.
|
|
/// </summary>
|
|
[JsonPropertyName("expectedHash")]
|
|
public string? ExpectedHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// Baseline manifest file path for regression testing.
|
|
/// </summary>
|
|
[JsonPropertyName("baseline")]
|
|
public string? Baseline { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cryptographic signature of the manifest.
|
|
/// </summary>
|
|
public sealed record SignatureInfo
|
|
{
|
|
/// <summary>
|
|
/// Signature algorithm (e.g., 'ES256', 'RS256').
|
|
/// </summary>
|
|
[JsonPropertyName("algorithm")]
|
|
public required string Algorithm { get; init; }
|
|
|
|
/// <summary>
|
|
/// Key identifier used for signing.
|
|
/// </summary>
|
|
[JsonPropertyName("keyId")]
|
|
public required string KeyId { get; init; }
|
|
|
|
/// <summary>
|
|
/// Base64-encoded signature.
|
|
/// </summary>
|
|
[JsonPropertyName("signature")]
|
|
public required string Signature { get; init; }
|
|
|
|
/// <summary>
|
|
/// UTC timestamp when signature was created.
|
|
/// </summary>
|
|
[JsonPropertyName("timestamp")]
|
|
public DateTimeOffset? Timestamp { get; init; }
|
|
}
|