sln build fix (again), tests fixes, audit work and doctors work
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Concelier Testing Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# StellaOps.Infrastructure.Postgres.Testing Task Board
|
||||
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229_049_BE_csproj_audit_maint_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TestRunAttestationGenerator.cs
|
||||
// Sprint: Testing Enhancement Advisory - Phase 1.3
|
||||
// Description: Generates DSSE-signed attestations linking test outputs to inputs
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Testing.Manifests.Models;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating in-toto attestations for test runs.
|
||||
/// Links test outputs to their inputs (SBOMs, VEX documents, feeds).
|
||||
/// </summary>
|
||||
public sealed class TestRunAttestationGenerator : ITestRunAttestationGenerator
|
||||
{
|
||||
private const string DssePayloadType = "application/vnd.in-toto+json";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly ITestRunAttestationSigner? _signer;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IAttestationIdGenerator _idGenerator;
|
||||
|
||||
public TestRunAttestationGenerator(
|
||||
ITestRunAttestationSigner? signer = null,
|
||||
TimeProvider? timeProvider = null,
|
||||
IAttestationIdGenerator? idGenerator = null)
|
||||
{
|
||||
_signer = signer;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_idGenerator = idGenerator ?? new GuidAttestationIdGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a test run attestation linking outputs to inputs.
|
||||
/// </summary>
|
||||
public async Task<TestRunAttestation> GenerateAsync(
|
||||
RunManifest manifest,
|
||||
ImmutableArray<TestRunOutput> outputs,
|
||||
TestRunEvidence evidence,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifest);
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
|
||||
var statement = CreateInTotoStatement(manifest, outputs, evidence);
|
||||
var statementBytes = SerializeToCanonicalJson(statement);
|
||||
var statementDigest = ComputeSha256Digest(statementBytes);
|
||||
|
||||
var envelope = await CreateDsseEnvelopeAsync(statementBytes, ct);
|
||||
|
||||
return new TestRunAttestation
|
||||
{
|
||||
AttestationId = _idGenerator.NewId(),
|
||||
RunId = manifest.RunId,
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
Statement = statement,
|
||||
StatementDigest = statementDigest,
|
||||
Envelope = envelope,
|
||||
Success = evidence.Success,
|
||||
OutputCount = outputs.Length
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates attestations for multiple test runs.
|
||||
/// </summary>
|
||||
public async Task<ImmutableArray<TestRunAttestation>> GenerateBatchAsync(
|
||||
IEnumerable<(RunManifest Manifest, ImmutableArray<TestRunOutput> Outputs, TestRunEvidence Evidence)> runs,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var attestations = ImmutableArray.CreateBuilder<TestRunAttestation>();
|
||||
|
||||
foreach (var (manifest, outputs, evidence) in runs)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var attestation = await GenerateAsync(manifest, outputs, evidence, ct);
|
||||
attestations.Add(attestation);
|
||||
}
|
||||
|
||||
return attestations.ToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a test run attestation's integrity.
|
||||
/// </summary>
|
||||
public Task<TestRunAttestationVerificationResult> VerifyAsync(
|
||||
TestRunAttestation attestation,
|
||||
ITestRunAttestationVerifier? verifier = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(attestation);
|
||||
|
||||
var errors = ImmutableArray.CreateBuilder<string>();
|
||||
var signatureVerified = false;
|
||||
string? verifiedKeyId = null;
|
||||
|
||||
// Verify statement digest
|
||||
if (attestation.Envelope is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payloadBytes = Convert.FromBase64String(attestation.Envelope.Payload);
|
||||
var payloadDigest = ComputeSha256Digest(payloadBytes);
|
||||
|
||||
if (payloadDigest != attestation.StatementDigest)
|
||||
{
|
||||
errors.Add($"Envelope payload digest mismatch: expected {attestation.StatementDigest}, got {payloadDigest}");
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
errors.Add("Invalid base64 in envelope payload");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var statementBytes = SerializeToCanonicalJson(attestation.Statement);
|
||||
var computedDigest = ComputeSha256Digest(statementBytes);
|
||||
if (computedDigest != attestation.StatementDigest)
|
||||
{
|
||||
errors.Add($"Statement digest mismatch: expected {attestation.StatementDigest}, got {computedDigest}");
|
||||
}
|
||||
}
|
||||
|
||||
// Verify signature if envelope exists and verifier is provided
|
||||
if (attestation.Envelope is not null && attestation.Envelope.Signatures.Length > 0)
|
||||
{
|
||||
if (verifier is null)
|
||||
{
|
||||
errors.Add("Signature verifier not provided for signed attestation");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Signature verification would be done here
|
||||
// For now, mark as not verified without a verifier
|
||||
signatureVerified = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(new TestRunAttestationVerificationResult
|
||||
{
|
||||
IsValid = errors.Count == 0,
|
||||
Errors = errors.ToImmutable(),
|
||||
SignatureVerified = signatureVerified,
|
||||
VerifiedKeyId = verifiedKeyId,
|
||||
VerifiedAt = _timeProvider.GetUtcNow()
|
||||
});
|
||||
}
|
||||
|
||||
private TestRunInTotoStatement CreateInTotoStatement(
|
||||
RunManifest manifest,
|
||||
ImmutableArray<TestRunOutput> outputs,
|
||||
TestRunEvidence evidence)
|
||||
{
|
||||
var subjects = outputs.Select(o => new TestRunSubject
|
||||
{
|
||||
Name = o.Name,
|
||||
Digest = ImmutableDictionary<string, string>.Empty
|
||||
.Add("sha256", o.Digest.Replace("sha256:", ""))
|
||||
}).ToImmutableArray();
|
||||
|
||||
return new TestRunInTotoStatement
|
||||
{
|
||||
Subject = subjects,
|
||||
Predicate = new TestRunPredicate
|
||||
{
|
||||
RunId = manifest.RunId,
|
||||
ManifestSchemaVersion = manifest.SchemaVersion,
|
||||
SbomDigests = manifest.SbomDigests.Select(s => s.Digest).ToImmutableArray(),
|
||||
VexDigests = evidence.VexDigests,
|
||||
FeedSnapshot = new TestRunFeedSnapshotRef
|
||||
{
|
||||
FeedId = manifest.FeedSnapshot.FeedId,
|
||||
Version = manifest.FeedSnapshot.Version,
|
||||
Digest = manifest.FeedSnapshot.Digest,
|
||||
SnapshotAt = manifest.FeedSnapshot.SnapshotAt
|
||||
},
|
||||
PolicyDigest = manifest.PolicySnapshot.LatticeRulesDigest,
|
||||
ToolVersions = new TestRunToolVersionsRef
|
||||
{
|
||||
ScannerVersion = manifest.ToolVersions.ScannerVersion,
|
||||
SbomGeneratorVersion = manifest.ToolVersions.SbomGeneratorVersion,
|
||||
ReachabilityEngineVersion = manifest.ToolVersions.ReachabilityEngineVersion,
|
||||
AttestorVersion = manifest.ToolVersions.AttestorVersion
|
||||
},
|
||||
ExecutedAt = _timeProvider.GetUtcNow(),
|
||||
InitiatedAt = manifest.InitiatedAt,
|
||||
DurationMs = evidence.DurationMs,
|
||||
TestCount = evidence.TestCount,
|
||||
PassedCount = evidence.PassedCount,
|
||||
FailedCount = evidence.FailedCount,
|
||||
SkippedCount = evidence.SkippedCount,
|
||||
Success = evidence.Success,
|
||||
DeterminismVerified = evidence.DeterminismVerified,
|
||||
Metadata = evidence.Metadata
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<TestRunDsseEnvelope?> CreateDsseEnvelopeAsync(
|
||||
byte[] payload,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var signatures = ImmutableArray.CreateBuilder<TestRunDsseSignature>();
|
||||
|
||||
if (_signer is not null)
|
||||
{
|
||||
var signResult = await _signer.SignAsync(payload, ct);
|
||||
signatures.Add(new TestRunDsseSignature
|
||||
{
|
||||
KeyId = signResult.KeyId,
|
||||
Sig = signResult.Signature,
|
||||
Algorithm = signResult.Algorithm
|
||||
});
|
||||
}
|
||||
|
||||
if (signatures.Count == 0)
|
||||
{
|
||||
return null; // Return null if no signatures
|
||||
}
|
||||
|
||||
return new TestRunDsseEnvelope
|
||||
{
|
||||
PayloadType = DssePayloadType,
|
||||
Payload = Convert.ToBase64String(payload),
|
||||
Signatures = signatures.ToImmutable()
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] SerializeToCanonicalJson<T>(T value)
|
||||
{
|
||||
// Use standard JSON serialization with sorted keys
|
||||
// For full RFC 8785 compliance, use a dedicated canonicalizer
|
||||
return JsonSerializer.SerializeToUtf8Bytes(value, JsonOptions);
|
||||
}
|
||||
|
||||
private static string ComputeSha256Digest(byte[] data)
|
||||
{
|
||||
var hash = SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for test run attestation generation.
|
||||
/// </summary>
|
||||
public interface ITestRunAttestationGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a test run attestation.
|
||||
/// </summary>
|
||||
Task<TestRunAttestation> GenerateAsync(
|
||||
RunManifest manifest,
|
||||
ImmutableArray<TestRunOutput> outputs,
|
||||
TestRunEvidence evidence,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Generates attestations for multiple test runs.
|
||||
/// </summary>
|
||||
Task<ImmutableArray<TestRunAttestation>> GenerateBatchAsync(
|
||||
IEnumerable<(RunManifest Manifest, ImmutableArray<TestRunOutput> Outputs, TestRunEvidence Evidence)> runs,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a test run attestation.
|
||||
/// </summary>
|
||||
Task<TestRunAttestationVerificationResult> VerifyAsync(
|
||||
TestRunAttestation attestation,
|
||||
ITestRunAttestationVerifier? verifier = null,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for signing test run attestations.
|
||||
/// </summary>
|
||||
public interface ITestRunAttestationSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// Signs the attestation payload.
|
||||
/// </summary>
|
||||
Task<TestRunSignatureResult> SignAsync(byte[] payload, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for verifying test run attestation signatures.
|
||||
/// </summary>
|
||||
public interface ITestRunAttestationVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies the attestation signature.
|
||||
/// </summary>
|
||||
Task<TestRunSignatureVerification> VerifyAsync(
|
||||
TestRunDsseEnvelope envelope,
|
||||
byte[] payload,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of signing operation.
|
||||
/// </summary>
|
||||
public sealed record TestRunSignatureResult
|
||||
{
|
||||
public required string KeyId { get; init; }
|
||||
public required string Signature { get; init; }
|
||||
public string? Algorithm { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of signature verification.
|
||||
/// </summary>
|
||||
public sealed record TestRunSignatureVerification
|
||||
{
|
||||
public bool Verified { get; init; }
|
||||
public string? Error { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for generating attestation IDs.
|
||||
/// </summary>
|
||||
public interface IAttestationIdGenerator
|
||||
{
|
||||
string NewId();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default GUID-based attestation ID generator.
|
||||
/// </summary>
|
||||
public sealed class GuidAttestationIdGenerator : IAttestationIdGenerator
|
||||
{
|
||||
public string NewId() => Guid.NewGuid().ToString("N");
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TestRunAttestationModels.cs
|
||||
// Sprint: Testing Enhancement Advisory - Phase 1.3
|
||||
// Description: Models for test run attestations linking outputs to inputs (SBOMs, VEX)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Test run attestation linking test outputs to their inputs (SBOMs, VEX documents).
|
||||
/// </summary>
|
||||
public sealed record TestRunAttestation
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this attestation.
|
||||
/// </summary>
|
||||
public required string AttestationId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the run manifest this attestation covers.
|
||||
/// </summary>
|
||||
public required string RunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the attestation was generated (UTC).
|
||||
/// </summary>
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The in-toto statement payload.
|
||||
/// </summary>
|
||||
public required TestRunInTotoStatement Statement { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 digest of the canonical JSON statement.
|
||||
/// </summary>
|
||||
public required string StatementDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope containing signed statement.
|
||||
/// </summary>
|
||||
public TestRunDsseEnvelope? Envelope { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether all test outputs were successfully produced.
|
||||
/// </summary>
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test outputs covered by this attestation.
|
||||
/// </summary>
|
||||
public int OutputCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-toto v1 statement for test run attestation.
|
||||
/// </summary>
|
||||
public sealed record TestRunInTotoStatement
|
||||
{
|
||||
public const string StatementTypeUri = "https://in-toto.io/Statement/v1";
|
||||
public const string PredicateTypeUri = "https://stellaops.io/attestation/test-run/v1";
|
||||
|
||||
[JsonPropertyName("_type")]
|
||||
public string Type { get; init; } = StatementTypeUri;
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public required ImmutableArray<TestRunSubject> Subject { get; init; }
|
||||
|
||||
[JsonPropertyName("predicateType")]
|
||||
public string PredicateType { get; init; } = PredicateTypeUri;
|
||||
|
||||
[JsonPropertyName("predicate")]
|
||||
public required TestRunPredicate Predicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject entry in the in-toto statement (test output artifacts).
|
||||
/// </summary>
|
||||
public sealed record TestRunSubject
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("digest")]
|
||||
public required ImmutableDictionary<string, string> Digest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predicate payload for test run attestation.
|
||||
/// Links test outputs to input artifacts (SBOMs, VEX documents, feeds).
|
||||
/// </summary>
|
||||
public sealed record TestRunPredicate
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique run identifier from the manifest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("runId")]
|
||||
public required string RunId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Schema version of the manifest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("manifestSchemaVersion")]
|
||||
public required string ManifestSchemaVersion { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digests of SBOM inputs used in this test run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sbomDigests")]
|
||||
public required ImmutableArray<string> SbomDigests { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digests of VEX documents used in this test run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("vexDigests")]
|
||||
public ImmutableArray<string> VexDigests { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Feed snapshot information used for vulnerability matching.
|
||||
/// </summary>
|
||||
[JsonPropertyName("feedSnapshot")]
|
||||
public required TestRunFeedSnapshotRef FeedSnapshot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy configuration digest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("policyDigest")]
|
||||
public required string PolicyDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tool versions used in the test run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("toolVersions")]
|
||||
public required TestRunToolVersionsRef ToolVersions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when the test run was executed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("executedAt")]
|
||||
public required DateTimeOffset ExecutedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC timestamp when the test run was initiated.
|
||||
/// </summary>
|
||||
[JsonPropertyName("initiatedAt")]
|
||||
public required DateTimeOffset InitiatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the test run in milliseconds.
|
||||
/// </summary>
|
||||
[JsonPropertyName("durationMs")]
|
||||
public long DurationMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases executed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("testCount")]
|
||||
public int TestCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases that passed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("passedCount")]
|
||||
public int PassedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases that failed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("failedCount")]
|
||||
public int FailedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases that were skipped.
|
||||
/// </summary>
|
||||
[JsonPropertyName("skippedCount")]
|
||||
public int SkippedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the test run completed successfully (all tests passed).
|
||||
/// </summary>
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Determinism verification status.
|
||||
/// </summary>
|
||||
[JsonPropertyName("determinismVerified")]
|
||||
public bool DeterminismVerified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata about the test run.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to feed snapshot used in test run.
|
||||
/// </summary>
|
||||
public sealed record TestRunFeedSnapshotRef
|
||||
{
|
||||
[JsonPropertyName("feedId")]
|
||||
public required string FeedId { get; init; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
[JsonPropertyName("digest")]
|
||||
public required string Digest { get; init; }
|
||||
|
||||
[JsonPropertyName("snapshotAt")]
|
||||
public required DateTimeOffset SnapshotAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to tool versions used in test run.
|
||||
/// </summary>
|
||||
public sealed record TestRunToolVersionsRef
|
||||
{
|
||||
[JsonPropertyName("scannerVersion")]
|
||||
public required string ScannerVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("sbomGeneratorVersion")]
|
||||
public required string SbomGeneratorVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("reachabilityEngineVersion")]
|
||||
public required string ReachabilityEngineVersion { get; init; }
|
||||
|
||||
[JsonPropertyName("attestorVersion")]
|
||||
public required string AttestorVersion { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope for test run attestation.
|
||||
/// </summary>
|
||||
public sealed record TestRunDsseEnvelope
|
||||
{
|
||||
[JsonPropertyName("payloadType")]
|
||||
public required string PayloadType { get; init; }
|
||||
|
||||
[JsonPropertyName("payload")]
|
||||
public required string Payload { get; init; }
|
||||
|
||||
[JsonPropertyName("signatures")]
|
||||
public required ImmutableArray<TestRunDsseSignature> Signatures { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE signature entry for test run attestation.
|
||||
/// </summary>
|
||||
public sealed record TestRunDsseSignature
|
||||
{
|
||||
[JsonPropertyName("keyid")]
|
||||
public required string KeyId { get; init; }
|
||||
|
||||
[JsonPropertyName("sig")]
|
||||
public required string Sig { get; init; }
|
||||
|
||||
[JsonPropertyName("algorithm")]
|
||||
public string? Algorithm { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test output artifact with digest.
|
||||
/// </summary>
|
||||
public sealed record TestRunOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// Name/identifier of the output artifact.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 digest of the output artifact.
|
||||
/// </summary>
|
||||
public required string Digest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Media type of the output artifact.
|
||||
/// </summary>
|
||||
public string? MediaType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes of the output artifact.
|
||||
/// </summary>
|
||||
public long? Size { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of test run attestation verification.
|
||||
/// </summary>
|
||||
public sealed record TestRunAttestationVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the attestation is valid.
|
||||
/// </summary>
|
||||
public bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Validation errors if any.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> Errors { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether the signature was successfully verified.
|
||||
/// </summary>
|
||||
public bool SignatureVerified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID used for verification.
|
||||
/// </summary>
|
||||
public string? VerifiedKeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the verification was performed.
|
||||
/// </summary>
|
||||
public DateTimeOffset VerifiedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// TestRunEvidence.cs
|
||||
// Sprint: Testing Enhancement Advisory - Phase 1.3
|
||||
// Description: Evidence collected during a test run for attestation generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Evidence collected during a test run for attestation generation.
|
||||
/// Captures test execution statistics and inputs used.
|
||||
/// </summary>
|
||||
public sealed record TestRunEvidence
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of test cases executed.
|
||||
/// </summary>
|
||||
public required int TestCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases that passed.
|
||||
/// </summary>
|
||||
public required int PassedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases that failed.
|
||||
/// </summary>
|
||||
public required int FailedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of test cases that were skipped.
|
||||
/// </summary>
|
||||
public int SkippedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the test run completed successfully (all tests passed).
|
||||
/// </summary>
|
||||
public bool Success => FailedCount == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the test run in milliseconds.
|
||||
/// </summary>
|
||||
public long DurationMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether determinism was verified (multiple runs produced identical results).
|
||||
/// </summary>
|
||||
public bool DeterminismVerified { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Digests of VEX documents used in this test run.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> VexDigests { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata about the test run.
|
||||
/// </summary>
|
||||
public ImmutableDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Test framework used (e.g., "xunit", "nunit").
|
||||
/// </summary>
|
||||
public string? TestFramework { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Test result file paths (e.g., TRX files).
|
||||
/// </summary>
|
||||
public ImmutableArray<string> ResultFiles { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Categories/traits of tests that were executed.
|
||||
/// </summary>
|
||||
public ImmutableArray<string> ExecutedCategories { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Git commit SHA at time of test run.
|
||||
/// </summary>
|
||||
public string? GitCommitSha { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Git branch name at time of test run.
|
||||
/// </summary>
|
||||
public string? GitBranch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CI build number or run ID.
|
||||
/// </summary>
|
||||
public string? CiBuildId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CI workflow name.
|
||||
/// </summary>
|
||||
public string? CiWorkflow { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for creating TestRunEvidence instances.
|
||||
/// </summary>
|
||||
public sealed class TestRunEvidenceBuilder
|
||||
{
|
||||
private int _testCount;
|
||||
private int _passedCount;
|
||||
private int _failedCount;
|
||||
private int _skippedCount;
|
||||
private long _durationMs;
|
||||
private bool _determinismVerified;
|
||||
private ImmutableArray<string>.Builder _vexDigests = ImmutableArray.CreateBuilder<string>();
|
||||
private ImmutableDictionary<string, string>.Builder _metadata = ImmutableDictionary.CreateBuilder<string, string>();
|
||||
private string? _testFramework;
|
||||
private ImmutableArray<string>.Builder _resultFiles = ImmutableArray.CreateBuilder<string>();
|
||||
private ImmutableArray<string>.Builder _executedCategories = ImmutableArray.CreateBuilder<string>();
|
||||
private string? _gitCommitSha;
|
||||
private string? _gitBranch;
|
||||
private string? _ciBuildId;
|
||||
private string? _ciWorkflow;
|
||||
|
||||
public TestRunEvidenceBuilder WithTestCount(int total, int passed, int failed, int skipped = 0)
|
||||
{
|
||||
_testCount = total;
|
||||
_passedCount = passed;
|
||||
_failedCount = failed;
|
||||
_skippedCount = skipped;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder WithDuration(long durationMs)
|
||||
{
|
||||
_durationMs = durationMs;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder WithDuration(TimeSpan duration)
|
||||
{
|
||||
_durationMs = (long)duration.TotalMilliseconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder WithDeterminismVerified(bool verified)
|
||||
{
|
||||
_determinismVerified = verified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder AddVexDigest(string digest)
|
||||
{
|
||||
_vexDigests.Add(digest);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder AddVexDigests(IEnumerable<string> digests)
|
||||
{
|
||||
_vexDigests.AddRange(digests);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder AddMetadata(string key, string value)
|
||||
{
|
||||
_metadata[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder WithTestFramework(string framework)
|
||||
{
|
||||
_testFramework = framework;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder AddResultFile(string path)
|
||||
{
|
||||
_resultFiles.Add(path);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder AddExecutedCategory(string category)
|
||||
{
|
||||
_executedCategories.Add(category);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder WithGitInfo(string? commitSha, string? branch)
|
||||
{
|
||||
_gitCommitSha = commitSha;
|
||||
_gitBranch = branch;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidenceBuilder WithCiInfo(string? buildId, string? workflow)
|
||||
{
|
||||
_ciBuildId = buildId;
|
||||
_ciWorkflow = workflow;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestRunEvidence Build()
|
||||
{
|
||||
return new TestRunEvidence
|
||||
{
|
||||
TestCount = _testCount,
|
||||
PassedCount = _passedCount,
|
||||
FailedCount = _failedCount,
|
||||
SkippedCount = _skippedCount,
|
||||
DurationMs = _durationMs,
|
||||
DeterminismVerified = _determinismVerified,
|
||||
VexDigests = _vexDigests.ToImmutable(),
|
||||
Metadata = _metadata.Count > 0 ? _metadata.ToImmutable() : null,
|
||||
TestFramework = _testFramework,
|
||||
ResultFiles = _resultFiles.ToImmutable(),
|
||||
ExecutedCategories = _executedCategories.ToImmutable(),
|
||||
GitCommitSha = _gitCommitSha,
|
||||
GitBranch = _gitBranch,
|
||||
CiBuildId = _ciBuildId,
|
||||
CiWorkflow = _ciWorkflow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new builder instance.
|
||||
/// </summary>
|
||||
public static TestRunEvidenceBuilder Create() => new();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Testing.Manifests.Attestation;
|
||||
|
||||
namespace StellaOps.Testing.Manifests.Models;
|
||||
|
||||
@@ -72,6 +73,18 @@ public sealed record RunManifest
|
||||
/// SHA-256 hash of this manifest (excluding this field).
|
||||
/// </summary>
|
||||
public string? ManifestDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional attestation linking test outputs to this manifest's inputs.
|
||||
/// Generated after test run completion to provide supply-chain linkage.
|
||||
/// </summary>
|
||||
public TestRunAttestation? Attestation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Output artifacts produced by this test run.
|
||||
/// Used as subjects in the attestation.
|
||||
/// </summary>
|
||||
public ImmutableArray<TestRunOutput> Outputs { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user