Add unit tests for PackRunAttestation and SealedInstallEnforcer
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled
- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission. - Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic. - Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TaskRunner.Core.Events;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// Service for generating and verifying pack run attestations.
|
||||
/// Per TASKRUN-OBS-54-001.
|
||||
/// </summary>
|
||||
public interface IPackRunAttestationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates an attestation for a pack run.
|
||||
/// </summary>
|
||||
Task<PackRunAttestationResult> GenerateAsync(
|
||||
PackRunAttestationRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a pack run attestation.
|
||||
/// </summary>
|
||||
Task<PackRunAttestationVerificationResult> VerifyAsync(
|
||||
PackRunAttestationVerificationRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an attestation by ID.
|
||||
/// </summary>
|
||||
Task<PackRunAttestation?> GetAsync(
|
||||
Guid attestationId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists attestations for a run.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PackRunAttestation>> ListByRunAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DSSE envelope for an attestation.
|
||||
/// </summary>
|
||||
Task<PackRunDsseEnvelope?> GetEnvelopeAsync(
|
||||
Guid attestationId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store for pack run attestations.
|
||||
/// </summary>
|
||||
public interface IPackRunAttestationStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores an attestation.
|
||||
/// </summary>
|
||||
Task StoreAsync(
|
||||
PackRunAttestation attestation,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets an attestation by ID.
|
||||
/// </summary>
|
||||
Task<PackRunAttestation?> GetAsync(
|
||||
Guid attestationId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists attestations for a run.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PackRunAttestation>> ListByRunAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates attestation status.
|
||||
/// </summary>
|
||||
Task UpdateStatusAsync(
|
||||
Guid attestationId,
|
||||
PackRunAttestationStatus status,
|
||||
string? error = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signing provider for pack run attestations.
|
||||
/// </summary>
|
||||
public interface IPackRunAttestationSigner
|
||||
{
|
||||
/// <summary>
|
||||
/// Signs an in-toto statement.
|
||||
/// </summary>
|
||||
Task<PackRunDsseEnvelope> SignAsync(
|
||||
byte[] statementBytes,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a DSSE envelope signature.
|
||||
/// </summary>
|
||||
Task<bool> VerifyAsync(
|
||||
PackRunDsseEnvelope envelope,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current signing key ID.
|
||||
/// </summary>
|
||||
string GetKeyId();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of pack run attestation service.
|
||||
/// </summary>
|
||||
public sealed class PackRunAttestationService : IPackRunAttestationService
|
||||
{
|
||||
private readonly IPackRunAttestationStore _store;
|
||||
private readonly IPackRunAttestationSigner? _signer;
|
||||
private readonly IPackRunTimelineEventEmitter? _timelineEmitter;
|
||||
private readonly ILogger<PackRunAttestationService> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public PackRunAttestationService(
|
||||
IPackRunAttestationStore store,
|
||||
ILogger<PackRunAttestationService> logger,
|
||||
IPackRunAttestationSigner? signer = null,
|
||||
IPackRunTimelineEventEmitter? timelineEmitter = null)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_signer = signer;
|
||||
_timelineEmitter = timelineEmitter;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PackRunAttestationResult> GenerateAsync(
|
||||
PackRunAttestationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
try
|
||||
{
|
||||
// Build provenance predicate
|
||||
var buildDefinition = new PackRunBuildDefinition(
|
||||
BuildType: "https://stellaops.io/pack-run/v1",
|
||||
ExternalParameters: request.ExternalParameters,
|
||||
InternalParameters: new Dictionary<string, object>
|
||||
{
|
||||
["planHash"] = request.PlanHash
|
||||
},
|
||||
ResolvedDependencies: request.ResolvedDependencies);
|
||||
|
||||
var runDetails = new PackRunDetails(
|
||||
Builder: new PackRunBuilder(
|
||||
Id: request.BuilderId ?? "https://stellaops.io/task-runner",
|
||||
Version: new Dictionary<string, string>
|
||||
{
|
||||
["stellaops.task-runner"] = GetVersion()
|
||||
},
|
||||
BuilderDependencies: null),
|
||||
Metadata: new PackRunProvMetadata(
|
||||
InvocationId: request.RunId,
|
||||
StartedOn: request.StartedAt,
|
||||
FinishedOn: request.CompletedAt),
|
||||
Byproducts: null);
|
||||
|
||||
var predicate = new PackRunProvenancePredicate(
|
||||
BuildDefinition: buildDefinition,
|
||||
RunDetails: runDetails);
|
||||
|
||||
var predicateJson = JsonSerializer.Serialize(predicate, JsonOptions);
|
||||
|
||||
// Build in-toto statement
|
||||
var statement = new PackRunInTotoStatement(
|
||||
Type: InTotoStatementTypes.V1,
|
||||
Subject: request.Subjects,
|
||||
PredicateType: PredicateTypes.PackRunProvenance,
|
||||
Predicate: predicate);
|
||||
|
||||
var statementJson = JsonSerializer.Serialize(statement, JsonOptions);
|
||||
var statementBytes = Encoding.UTF8.GetBytes(statementJson);
|
||||
|
||||
// Sign if signer is available
|
||||
PackRunDsseEnvelope? envelope = null;
|
||||
PackRunAttestationStatus status = PackRunAttestationStatus.Pending;
|
||||
string? error = null;
|
||||
|
||||
if (_signer is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
envelope = await _signer.SignAsync(statementBytes, cancellationToken);
|
||||
status = PackRunAttestationStatus.Signed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to sign attestation for run {RunId}", request.RunId);
|
||||
error = ex.Message;
|
||||
status = PackRunAttestationStatus.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
// Create attestation record
|
||||
var attestation = new PackRunAttestation(
|
||||
AttestationId: Guid.NewGuid(),
|
||||
TenantId: request.TenantId,
|
||||
RunId: request.RunId,
|
||||
PlanHash: request.PlanHash,
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
Subjects: request.Subjects,
|
||||
PredicateType: PredicateTypes.PackRunProvenance,
|
||||
PredicateJson: predicateJson,
|
||||
Envelope: envelope,
|
||||
Status: status,
|
||||
Error: error,
|
||||
EvidenceSnapshotId: request.EvidenceSnapshotId,
|
||||
Metadata: request.Metadata);
|
||||
|
||||
// Store attestation
|
||||
await _store.StoreAsync(attestation, cancellationToken);
|
||||
|
||||
// Emit timeline event
|
||||
if (_timelineEmitter is not null)
|
||||
{
|
||||
var eventType = status == PackRunAttestationStatus.Signed
|
||||
? PackRunAttestationEventTypes.AttestationCreated
|
||||
: PackRunAttestationEventTypes.AttestationFailed;
|
||||
|
||||
await _timelineEmitter.EmitAsync(
|
||||
PackRunTimelineEvent.Create(
|
||||
tenantId: request.TenantId,
|
||||
eventType: eventType,
|
||||
source: "taskrunner-attestation",
|
||||
occurredAt: DateTimeOffset.UtcNow,
|
||||
runId: request.RunId,
|
||||
planHash: request.PlanHash,
|
||||
attributes: new Dictionary<string, string>
|
||||
{
|
||||
["attestationId"] = attestation.AttestationId.ToString(),
|
||||
["predicateType"] = attestation.PredicateType,
|
||||
["subjectCount"] = request.Subjects.Count.ToString(),
|
||||
["status"] = status.ToString()
|
||||
},
|
||||
evidencePointer: envelope is not null
|
||||
? PackRunEvidencePointer.Attestation(
|
||||
request.RunId,
|
||||
envelope.ComputeDigest())
|
||||
: null),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Generated attestation {AttestationId} for run {RunId} with {SubjectCount} subjects, status {Status}",
|
||||
attestation.AttestationId,
|
||||
request.RunId,
|
||||
request.Subjects.Count,
|
||||
status);
|
||||
|
||||
return new PackRunAttestationResult(
|
||||
Success: status != PackRunAttestationStatus.Failed,
|
||||
Attestation: attestation,
|
||||
Error: error);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate attestation for run {RunId}", request.RunId);
|
||||
|
||||
return new PackRunAttestationResult(
|
||||
Success: false,
|
||||
Attestation: null,
|
||||
Error: ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PackRunAttestationVerificationResult> VerifyAsync(
|
||||
PackRunAttestationVerificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var signatureStatus = PackRunSignatureVerificationStatus.NotVerified;
|
||||
var subjectStatus = PackRunSubjectVerificationStatus.NotVerified;
|
||||
var revocationStatus = PackRunRevocationStatus.NotChecked;
|
||||
|
||||
var attestation = await _store.GetAsync(request.AttestationId, cancellationToken);
|
||||
if (attestation is null)
|
||||
{
|
||||
return new PackRunAttestationVerificationResult(
|
||||
Valid: false,
|
||||
AttestationId: request.AttestationId,
|
||||
SignatureStatus: PackRunSignatureVerificationStatus.NotVerified,
|
||||
SubjectStatus: PackRunSubjectVerificationStatus.NotVerified,
|
||||
RevocationStatus: PackRunRevocationStatus.NotChecked,
|
||||
Errors: ["Attestation not found"],
|
||||
VerifiedAt: DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if (request.VerifySignature && attestation.Envelope is not null && _signer is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var signatureValid = await _signer.VerifyAsync(attestation.Envelope, cancellationToken);
|
||||
signatureStatus = signatureValid
|
||||
? PackRunSignatureVerificationStatus.Valid
|
||||
: PackRunSignatureVerificationStatus.Invalid;
|
||||
|
||||
if (!signatureValid)
|
||||
{
|
||||
errors.Add("Signature verification failed");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
signatureStatus = PackRunSignatureVerificationStatus.Invalid;
|
||||
errors.Add($"Signature verification error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else if (request.VerifySignature && attestation.Envelope is null)
|
||||
{
|
||||
signatureStatus = PackRunSignatureVerificationStatus.Invalid;
|
||||
errors.Add("No envelope available for signature verification");
|
||||
}
|
||||
|
||||
// Verify subjects
|
||||
if (request.VerifySubjects && request.ExpectedSubjects is not null)
|
||||
{
|
||||
var expectedSet = request.ExpectedSubjects
|
||||
.Select(s => $"{s.Name}:{string.Join(",", s.Digest.OrderBy(d => d.Key).Select(d => $"{d.Key}={d.Value}"))}")
|
||||
.ToHashSet();
|
||||
|
||||
var actualSet = attestation.Subjects
|
||||
.Select(s => $"{s.Name}:{string.Join(",", s.Digest.OrderBy(d => d.Key).Select(d => $"{d.Key}={d.Value}"))}")
|
||||
.ToHashSet();
|
||||
|
||||
if (expectedSet.SetEquals(actualSet))
|
||||
{
|
||||
subjectStatus = PackRunSubjectVerificationStatus.Match;
|
||||
}
|
||||
else if (expectedSet.IsSubsetOf(actualSet))
|
||||
{
|
||||
subjectStatus = PackRunSubjectVerificationStatus.Match;
|
||||
}
|
||||
else
|
||||
{
|
||||
var missing = expectedSet.Except(actualSet).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
subjectStatus = PackRunSubjectVerificationStatus.Missing;
|
||||
errors.Add($"Missing subjects: {string.Join(", ", missing)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
subjectStatus = PackRunSubjectVerificationStatus.Mismatch;
|
||||
errors.Add("Subject digest mismatch");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check revocation
|
||||
if (request.CheckRevocation)
|
||||
{
|
||||
revocationStatus = attestation.Status == PackRunAttestationStatus.Revoked
|
||||
? PackRunRevocationStatus.Revoked
|
||||
: PackRunRevocationStatus.NotRevoked;
|
||||
|
||||
if (attestation.Status == PackRunAttestationStatus.Revoked)
|
||||
{
|
||||
errors.Add("Attestation has been revoked");
|
||||
}
|
||||
}
|
||||
|
||||
var valid = errors.Count == 0 &&
|
||||
(signatureStatus is PackRunSignatureVerificationStatus.Valid or PackRunSignatureVerificationStatus.NotVerified) &&
|
||||
(subjectStatus is PackRunSubjectVerificationStatus.Match or PackRunSubjectVerificationStatus.NotVerified) &&
|
||||
(revocationStatus is PackRunRevocationStatus.NotRevoked or PackRunRevocationStatus.NotChecked);
|
||||
|
||||
return new PackRunAttestationVerificationResult(
|
||||
Valid: valid,
|
||||
AttestationId: request.AttestationId,
|
||||
SignatureStatus: signatureStatus,
|
||||
SubjectStatus: subjectStatus,
|
||||
RevocationStatus: revocationStatus,
|
||||
Errors: errors.Count > 0 ? errors : null,
|
||||
VerifiedAt: DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PackRunAttestation?> GetAsync(
|
||||
Guid attestationId,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _store.GetAsync(attestationId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PackRunAttestation>> ListByRunAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _store.ListByRunAsync(tenantId, runId, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PackRunDsseEnvelope?> GetEnvelopeAsync(
|
||||
Guid attestationId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var attestation = await _store.GetAsync(attestationId, cancellationToken);
|
||||
return attestation?.Envelope;
|
||||
}
|
||||
|
||||
private static string GetVersion()
|
||||
{
|
||||
var assembly = typeof(PackRunAttestationService).Assembly;
|
||||
var version = assembly.GetName().Version;
|
||||
return version?.ToString() ?? "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attestation event types for timeline.
|
||||
/// </summary>
|
||||
public static class PackRunAttestationEventTypes
|
||||
{
|
||||
/// <summary>Attestation created successfully.</summary>
|
||||
public const string AttestationCreated = "pack.attestation.created";
|
||||
|
||||
/// <summary>Attestation creation failed.</summary>
|
||||
public const string AttestationFailed = "pack.attestation.failed";
|
||||
|
||||
/// <summary>Attestation verified.</summary>
|
||||
public const string AttestationVerified = "pack.attestation.verified";
|
||||
|
||||
/// <summary>Attestation verification failed.</summary>
|
||||
public const string AttestationVerificationFailed = "pack.attestation.verification_failed";
|
||||
|
||||
/// <summary>Attestation revoked.</summary>
|
||||
public const string AttestationRevoked = "pack.attestation.revoked";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory attestation store for testing.
|
||||
/// </summary>
|
||||
public sealed class InMemoryPackRunAttestationStore : IPackRunAttestationStore
|
||||
{
|
||||
private readonly Dictionary<Guid, PackRunAttestation> _attestations = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StoreAsync(
|
||||
PackRunAttestation attestation,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_attestations[attestation.AttestationId] = attestation;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PackRunAttestation?> GetAsync(
|
||||
Guid attestationId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_attestations.TryGetValue(attestationId, out var attestation);
|
||||
return Task.FromResult(attestation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<PackRunAttestation>> ListByRunAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var results = _attestations.Values
|
||||
.Where(a => a.TenantId == tenantId && a.RunId == runId)
|
||||
.OrderBy(a => a.CreatedAt)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<PackRunAttestation>>(results);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateStatusAsync(
|
||||
Guid attestationId,
|
||||
PackRunAttestationStatus status,
|
||||
string? error = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_attestations.TryGetValue(attestationId, out var attestation))
|
||||
{
|
||||
_attestations[attestationId] = attestation with
|
||||
{
|
||||
Status = status,
|
||||
Error = error
|
||||
};
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Gets all attestations (for testing).</summary>
|
||||
public IReadOnlyList<PackRunAttestation> GetAll()
|
||||
{
|
||||
lock (_lock) { return _attestations.Values.ToList(); }
|
||||
}
|
||||
|
||||
/// <summary>Clears all attestations (for testing).</summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock) { _attestations.Clear(); }
|
||||
}
|
||||
|
||||
/// <summary>Gets attestation count.</summary>
|
||||
public int Count
|
||||
{
|
||||
get { lock (_lock) { return _attestations.Count; } }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub signer for testing (does not perform real cryptographic signing).
|
||||
/// </summary>
|
||||
public sealed class StubPackRunAttestationSigner : IPackRunAttestationSigner
|
||||
{
|
||||
private readonly string _keyId;
|
||||
|
||||
public StubPackRunAttestationSigner(string keyId = "test-key-001")
|
||||
{
|
||||
_keyId = keyId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<PackRunDsseEnvelope> SignAsync(
|
||||
byte[] statementBytes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var payload = Convert.ToBase64String(statementBytes);
|
||||
|
||||
// Create stub signature (not cryptographically valid)
|
||||
var sigBytes = System.Security.Cryptography.SHA256.HashData(statementBytes);
|
||||
var sig = Convert.ToBase64String(sigBytes);
|
||||
|
||||
var envelope = new PackRunDsseEnvelope(
|
||||
PayloadType: PackRunDsseEnvelope.InTotoPayloadType,
|
||||
Payload: payload,
|
||||
Signatures: [new PackRunDsseSignature(_keyId, sig)]);
|
||||
|
||||
return Task.FromResult(envelope);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> VerifyAsync(
|
||||
PackRunDsseEnvelope envelope,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Stub always returns true for testing
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetKeyId() => _keyId;
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.TaskRunner.Core.Evidence;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.Attestation;
|
||||
|
||||
/// <summary>
|
||||
/// DSSE attestation for pack run execution.
|
||||
/// Per TASKRUN-OBS-54-001.
|
||||
/// </summary>
|
||||
public sealed record PackRunAttestation(
|
||||
/// <summary>Unique attestation identifier.</summary>
|
||||
Guid AttestationId,
|
||||
|
||||
/// <summary>Tenant scope.</summary>
|
||||
string TenantId,
|
||||
|
||||
/// <summary>Run ID this attestation covers.</summary>
|
||||
string RunId,
|
||||
|
||||
/// <summary>Plan hash that was executed.</summary>
|
||||
string PlanHash,
|
||||
|
||||
/// <summary>When the attestation was created.</summary>
|
||||
DateTimeOffset CreatedAt,
|
||||
|
||||
/// <summary>Subjects covered by this attestation (produced artifacts).</summary>
|
||||
IReadOnlyList<PackRunAttestationSubject> Subjects,
|
||||
|
||||
/// <summary>Predicate type URI.</summary>
|
||||
string PredicateType,
|
||||
|
||||
/// <summary>Predicate content as JSON.</summary>
|
||||
string PredicateJson,
|
||||
|
||||
/// <summary>DSSE envelope containing signature.</summary>
|
||||
PackRunDsseEnvelope? Envelope,
|
||||
|
||||
/// <summary>Attestation status.</summary>
|
||||
PackRunAttestationStatus Status,
|
||||
|
||||
/// <summary>Error message if signing failed.</summary>
|
||||
string? Error,
|
||||
|
||||
/// <summary>Reference to evidence snapshot.</summary>
|
||||
Guid? EvidenceSnapshotId,
|
||||
|
||||
/// <summary>Attestation metadata.</summary>
|
||||
IReadOnlyDictionary<string, string>? Metadata)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Computes the canonical statement digest.
|
||||
/// </summary>
|
||||
public string ComputeStatementDigest()
|
||||
{
|
||||
var statement = new PackRunInTotoStatement(
|
||||
Type: InTotoStatementTypes.V01,
|
||||
Subject: Subjects,
|
||||
PredicateType: PredicateType,
|
||||
Predicate: JsonSerializer.Deserialize<JsonElement>(PredicateJson, JsonOptions));
|
||||
|
||||
var json = JsonSerializer.Serialize(statement, JsonOptions);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes to JSON.
|
||||
/// </summary>
|
||||
public string ToJson() => JsonSerializer.Serialize(this, JsonOptions);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes from JSON.
|
||||
/// </summary>
|
||||
public static PackRunAttestation? FromJson(string json)
|
||||
=> JsonSerializer.Deserialize<PackRunAttestation>(json, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attestation status.
|
||||
/// </summary>
|
||||
public enum PackRunAttestationStatus
|
||||
{
|
||||
/// <summary>Attestation is pending signing.</summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>Attestation is signed and valid.</summary>
|
||||
Signed,
|
||||
|
||||
/// <summary>Attestation signing failed.</summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>Attestation signature was revoked.</summary>
|
||||
Revoked
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject covered by attestation (an artifact).
|
||||
/// </summary>
|
||||
public sealed record PackRunAttestationSubject(
|
||||
/// <summary>Subject name (artifact path or identifier).</summary>
|
||||
[property: JsonPropertyName("name")]
|
||||
string Name,
|
||||
|
||||
/// <summary>Subject digest (sha256 -> hash).</summary>
|
||||
[property: JsonPropertyName("digest")]
|
||||
IReadOnlyDictionary<string, string> Digest)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a subject from an artifact reference.
|
||||
/// </summary>
|
||||
public static PackRunAttestationSubject FromArtifact(PackRunArtifactReference artifact)
|
||||
{
|
||||
var digest = new Dictionary<string, string>();
|
||||
|
||||
// Parse sha256:abcdef format and extract just the hash
|
||||
if (artifact.Sha256.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
digest["sha256"] = artifact.Sha256[7..];
|
||||
}
|
||||
else
|
||||
{
|
||||
digest["sha256"] = artifact.Sha256;
|
||||
}
|
||||
|
||||
return new PackRunAttestationSubject(artifact.Name, digest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a subject from a material.
|
||||
/// </summary>
|
||||
public static PackRunAttestationSubject FromMaterial(PackRunEvidenceMaterial material)
|
||||
{
|
||||
var digest = new Dictionary<string, string>();
|
||||
|
||||
if (material.Sha256.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
digest["sha256"] = material.Sha256[7..];
|
||||
}
|
||||
else
|
||||
{
|
||||
digest["sha256"] = material.Sha256;
|
||||
}
|
||||
|
||||
return new PackRunAttestationSubject(material.CanonicalPath, digest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-toto statement wrapper for pack runs.
|
||||
/// </summary>
|
||||
public sealed record PackRunInTotoStatement(
|
||||
/// <summary>Statement type (always _type).</summary>
|
||||
[property: JsonPropertyName("_type")]
|
||||
string Type,
|
||||
|
||||
/// <summary>Subjects covered.</summary>
|
||||
[property: JsonPropertyName("subject")]
|
||||
IReadOnlyList<PackRunAttestationSubject> Subject,
|
||||
|
||||
/// <summary>Predicate type URI.</summary>
|
||||
[property: JsonPropertyName("predicateType")]
|
||||
string PredicateType,
|
||||
|
||||
/// <summary>Predicate content.</summary>
|
||||
[property: JsonPropertyName("predicate")]
|
||||
object Predicate);
|
||||
|
||||
/// <summary>
|
||||
/// Standard in-toto statement type URIs.
|
||||
/// </summary>
|
||||
public static class InTotoStatementTypes
|
||||
{
|
||||
/// <summary>In-toto statement v0.1.</summary>
|
||||
public const string V01 = "https://in-toto.io/Statement/v0.1";
|
||||
|
||||
/// <summary>In-toto statement v1.0.</summary>
|
||||
public const string V1 = "https://in-toto.io/Statement/v1";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard predicate type URIs.
|
||||
/// </summary>
|
||||
public static class PredicateTypes
|
||||
{
|
||||
/// <summary>SLSA Provenance v0.2.</summary>
|
||||
public const string SlsaProvenanceV02 = "https://slsa.dev/provenance/v0.2";
|
||||
|
||||
/// <summary>SLSA Provenance v1.0.</summary>
|
||||
public const string SlsaProvenanceV1 = "https://slsa.dev/provenance/v1";
|
||||
|
||||
/// <summary>StellaOps Pack Run provenance.</summary>
|
||||
public const string PackRunProvenance = "https://stellaops.io/attestation/pack-run/v1";
|
||||
|
||||
/// <summary>StellaOps Pack Run completion.</summary>
|
||||
public const string PackRunCompletion = "https://stellaops.io/attestation/pack-run-completion/v1";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope for pack run attestation.
|
||||
/// </summary>
|
||||
public sealed record PackRunDsseEnvelope(
|
||||
/// <summary>Payload type (usually application/vnd.in-toto+json).</summary>
|
||||
[property: JsonPropertyName("payloadType")]
|
||||
string PayloadType,
|
||||
|
||||
/// <summary>Base64-encoded payload.</summary>
|
||||
[property: JsonPropertyName("payload")]
|
||||
string Payload,
|
||||
|
||||
/// <summary>Signatures on the envelope.</summary>
|
||||
[property: JsonPropertyName("signatures")]
|
||||
IReadOnlyList<PackRunDsseSignature> Signatures)
|
||||
{
|
||||
/// <summary>Standard payload type for in-toto attestations.</summary>
|
||||
public const string InTotoPayloadType = "application/vnd.in-toto+json";
|
||||
|
||||
/// <summary>
|
||||
/// Computes the envelope digest.
|
||||
/// </summary>
|
||||
public string ComputeDigest()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
});
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signature in a DSSE envelope.
|
||||
/// </summary>
|
||||
public sealed record PackRunDsseSignature(
|
||||
/// <summary>Key identifier.</summary>
|
||||
[property: JsonPropertyName("keyid")]
|
||||
string? KeyId,
|
||||
|
||||
/// <summary>Base64-encoded signature.</summary>
|
||||
[property: JsonPropertyName("sig")]
|
||||
string Sig);
|
||||
|
||||
/// <summary>
|
||||
/// Pack run provenance predicate per SLSA Provenance v1.
|
||||
/// </summary>
|
||||
public sealed record PackRunProvenancePredicate(
|
||||
/// <summary>Build definition describing what was run.</summary>
|
||||
[property: JsonPropertyName("buildDefinition")]
|
||||
PackRunBuildDefinition BuildDefinition,
|
||||
|
||||
/// <summary>Run details describing the actual execution.</summary>
|
||||
[property: JsonPropertyName("runDetails")]
|
||||
PackRunDetails RunDetails);
|
||||
|
||||
/// <summary>
|
||||
/// Build definition for pack run provenance.
|
||||
/// </summary>
|
||||
public sealed record PackRunBuildDefinition(
|
||||
/// <summary>Build type identifier.</summary>
|
||||
[property: JsonPropertyName("buildType")]
|
||||
string BuildType,
|
||||
|
||||
/// <summary>External parameters (e.g., pack manifest URL).</summary>
|
||||
[property: JsonPropertyName("externalParameters")]
|
||||
IReadOnlyDictionary<string, object>? ExternalParameters,
|
||||
|
||||
/// <summary>Internal parameters resolved during build.</summary>
|
||||
[property: JsonPropertyName("internalParameters")]
|
||||
IReadOnlyDictionary<string, object>? InternalParameters,
|
||||
|
||||
/// <summary>Dependencies resolved during build.</summary>
|
||||
[property: JsonPropertyName("resolvedDependencies")]
|
||||
IReadOnlyList<PackRunDependency>? ResolvedDependencies);
|
||||
|
||||
/// <summary>
|
||||
/// Resolved dependency in provenance.
|
||||
/// </summary>
|
||||
public sealed record PackRunDependency(
|
||||
/// <summary>Dependency URI.</summary>
|
||||
[property: JsonPropertyName("uri")]
|
||||
string Uri,
|
||||
|
||||
/// <summary>Dependency digest.</summary>
|
||||
[property: JsonPropertyName("digest")]
|
||||
IReadOnlyDictionary<string, string>? Digest,
|
||||
|
||||
/// <summary>Dependency name.</summary>
|
||||
[property: JsonPropertyName("name")]
|
||||
string? Name,
|
||||
|
||||
/// <summary>Media type.</summary>
|
||||
[property: JsonPropertyName("mediaType")]
|
||||
string? MediaType);
|
||||
|
||||
/// <summary>
|
||||
/// Run details for pack run provenance.
|
||||
/// </summary>
|
||||
public sealed record PackRunDetails(
|
||||
/// <summary>Builder information.</summary>
|
||||
[property: JsonPropertyName("builder")]
|
||||
PackRunBuilder Builder,
|
||||
|
||||
/// <summary>Run metadata.</summary>
|
||||
[property: JsonPropertyName("metadata")]
|
||||
PackRunProvMetadata Metadata,
|
||||
|
||||
/// <summary>By-products of the run.</summary>
|
||||
[property: JsonPropertyName("byproducts")]
|
||||
IReadOnlyList<PackRunByproduct>? Byproducts);
|
||||
|
||||
/// <summary>
|
||||
/// Builder information.
|
||||
/// </summary>
|
||||
public sealed record PackRunBuilder(
|
||||
/// <summary>Builder ID (URI).</summary>
|
||||
[property: JsonPropertyName("id")]
|
||||
string Id,
|
||||
|
||||
/// <summary>Builder version.</summary>
|
||||
[property: JsonPropertyName("version")]
|
||||
IReadOnlyDictionary<string, string>? Version,
|
||||
|
||||
/// <summary>Builder dependencies.</summary>
|
||||
[property: JsonPropertyName("builderDependencies")]
|
||||
IReadOnlyList<PackRunDependency>? BuilderDependencies);
|
||||
|
||||
/// <summary>
|
||||
/// Provenance metadata.
|
||||
/// </summary>
|
||||
public sealed record PackRunProvMetadata(
|
||||
/// <summary>Invocation ID.</summary>
|
||||
[property: JsonPropertyName("invocationId")]
|
||||
string? InvocationId,
|
||||
|
||||
/// <summary>When the build started.</summary>
|
||||
[property: JsonPropertyName("startedOn")]
|
||||
DateTimeOffset? StartedOn,
|
||||
|
||||
/// <summary>When the build finished.</summary>
|
||||
[property: JsonPropertyName("finishedOn")]
|
||||
DateTimeOffset? FinishedOn);
|
||||
|
||||
/// <summary>
|
||||
/// By-product of the build.
|
||||
/// </summary>
|
||||
public sealed record PackRunByproduct(
|
||||
/// <summary>By-product URI.</summary>
|
||||
[property: JsonPropertyName("uri")]
|
||||
string? Uri,
|
||||
|
||||
/// <summary>By-product digest.</summary>
|
||||
[property: JsonPropertyName("digest")]
|
||||
IReadOnlyDictionary<string, string>? Digest,
|
||||
|
||||
/// <summary>By-product name.</summary>
|
||||
[property: JsonPropertyName("name")]
|
||||
string? Name,
|
||||
|
||||
/// <summary>By-product media type.</summary>
|
||||
[property: JsonPropertyName("mediaType")]
|
||||
string? MediaType);
|
||||
|
||||
/// <summary>
|
||||
/// Request to generate an attestation for a pack run.
|
||||
/// </summary>
|
||||
public sealed record PackRunAttestationRequest(
|
||||
/// <summary>Run ID to attest.</summary>
|
||||
string RunId,
|
||||
|
||||
/// <summary>Tenant ID.</summary>
|
||||
string TenantId,
|
||||
|
||||
/// <summary>Plan hash.</summary>
|
||||
string PlanHash,
|
||||
|
||||
/// <summary>Subjects (artifacts) to attest.</summary>
|
||||
IReadOnlyList<PackRunAttestationSubject> Subjects,
|
||||
|
||||
/// <summary>Evidence snapshot ID to link.</summary>
|
||||
Guid? EvidenceSnapshotId,
|
||||
|
||||
/// <summary>Run started at.</summary>
|
||||
DateTimeOffset StartedAt,
|
||||
|
||||
/// <summary>Run completed at.</summary>
|
||||
DateTimeOffset? CompletedAt,
|
||||
|
||||
/// <summary>Builder ID.</summary>
|
||||
string? BuilderId,
|
||||
|
||||
/// <summary>External parameters.</summary>
|
||||
IReadOnlyDictionary<string, object>? ExternalParameters,
|
||||
|
||||
/// <summary>Resolved dependencies.</summary>
|
||||
IReadOnlyList<PackRunDependency>? ResolvedDependencies,
|
||||
|
||||
/// <summary>Additional metadata.</summary>
|
||||
IReadOnlyDictionary<string, string>? Metadata);
|
||||
|
||||
/// <summary>
|
||||
/// Result of attestation generation.
|
||||
/// </summary>
|
||||
public sealed record PackRunAttestationResult(
|
||||
/// <summary>Whether attestation generation succeeded.</summary>
|
||||
bool Success,
|
||||
|
||||
/// <summary>Generated attestation.</summary>
|
||||
PackRunAttestation? Attestation,
|
||||
|
||||
/// <summary>Error message if failed.</summary>
|
||||
string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// Request to verify a pack run attestation.
|
||||
/// </summary>
|
||||
public sealed record PackRunAttestationVerificationRequest(
|
||||
/// <summary>Attestation ID to verify.</summary>
|
||||
Guid AttestationId,
|
||||
|
||||
/// <summary>Expected subjects to verify against.</summary>
|
||||
IReadOnlyList<PackRunAttestationSubject>? ExpectedSubjects,
|
||||
|
||||
/// <summary>Whether to verify signature.</summary>
|
||||
bool VerifySignature,
|
||||
|
||||
/// <summary>Whether to verify subjects match.</summary>
|
||||
bool VerifySubjects,
|
||||
|
||||
/// <summary>Whether to check revocation status.</summary>
|
||||
bool CheckRevocation);
|
||||
|
||||
/// <summary>
|
||||
/// Result of attestation verification.
|
||||
/// </summary>
|
||||
public sealed record PackRunAttestationVerificationResult(
|
||||
/// <summary>Whether verification passed.</summary>
|
||||
bool Valid,
|
||||
|
||||
/// <summary>Attestation that was verified.</summary>
|
||||
Guid AttestationId,
|
||||
|
||||
/// <summary>Signature verification status.</summary>
|
||||
PackRunSignatureVerificationStatus SignatureStatus,
|
||||
|
||||
/// <summary>Subject verification status.</summary>
|
||||
PackRunSubjectVerificationStatus SubjectStatus,
|
||||
|
||||
/// <summary>Revocation status.</summary>
|
||||
PackRunRevocationStatus RevocationStatus,
|
||||
|
||||
/// <summary>Verification errors.</summary>
|
||||
IReadOnlyList<string>? Errors,
|
||||
|
||||
/// <summary>When verification was performed.</summary>
|
||||
DateTimeOffset VerifiedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification status.
|
||||
/// </summary>
|
||||
public enum PackRunSignatureVerificationStatus
|
||||
{
|
||||
/// <summary>Not verified.</summary>
|
||||
NotVerified,
|
||||
|
||||
/// <summary>Signature is valid.</summary>
|
||||
Valid,
|
||||
|
||||
/// <summary>Signature is invalid.</summary>
|
||||
Invalid,
|
||||
|
||||
/// <summary>Key not found.</summary>
|
||||
KeyNotFound,
|
||||
|
||||
/// <summary>Key expired.</summary>
|
||||
KeyExpired
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subject verification status.
|
||||
/// </summary>
|
||||
public enum PackRunSubjectVerificationStatus
|
||||
{
|
||||
/// <summary>Not verified.</summary>
|
||||
NotVerified,
|
||||
|
||||
/// <summary>All subjects match.</summary>
|
||||
Match,
|
||||
|
||||
/// <summary>Subjects do not match.</summary>
|
||||
Mismatch,
|
||||
|
||||
/// <summary>Missing expected subjects.</summary>
|
||||
Missing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revocation status.
|
||||
/// </summary>
|
||||
public enum PackRunRevocationStatus
|
||||
{
|
||||
/// <summary>Not checked.</summary>
|
||||
NotChecked,
|
||||
|
||||
/// <summary>Not revoked.</summary>
|
||||
NotRevoked,
|
||||
|
||||
/// <summary>Revoked.</summary>
|
||||
Revoked,
|
||||
|
||||
/// <summary>Revocation check failed.</summary>
|
||||
CheckFailed
|
||||
}
|
||||
@@ -313,6 +313,21 @@ public static class PackRunEventTypes
|
||||
/// <summary>Sealed install requirements warning.</summary>
|
||||
public const string SealedInstallWarning = "pack.sealed_install.warning";
|
||||
|
||||
/// <summary>Attestation created successfully (per TASKRUN-OBS-54-001).</summary>
|
||||
public const string AttestationCreated = "pack.attestation.created";
|
||||
|
||||
/// <summary>Attestation creation failed.</summary>
|
||||
public const string AttestationFailed = "pack.attestation.failed";
|
||||
|
||||
/// <summary>Attestation verified successfully.</summary>
|
||||
public const string AttestationVerified = "pack.attestation.verified";
|
||||
|
||||
/// <summary>Attestation verification failed.</summary>
|
||||
public const string AttestationVerificationFailed = "pack.attestation.verification_failed";
|
||||
|
||||
/// <summary>Attestation was revoked.</summary>
|
||||
public const string AttestationRevoked = "pack.attestation.revoked";
|
||||
|
||||
/// <summary>Checks if the event type is a pack run event.</summary>
|
||||
public static bool IsPackRunEvent(string eventType) =>
|
||||
eventType.StartsWith(Prefix, StringComparison.Ordinal);
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Evidence for bundle import operations.
|
||||
/// Per TASKRUN-AIRGAP-58-001.
|
||||
/// </summary>
|
||||
public sealed record BundleImportEvidence(
|
||||
/// <summary>Unique import job identifier.</summary>
|
||||
string JobId,
|
||||
|
||||
/// <summary>Tenant that initiated the import.</summary>
|
||||
string TenantId,
|
||||
|
||||
/// <summary>Bundle source path or URL.</summary>
|
||||
string SourcePath,
|
||||
|
||||
/// <summary>When the import started.</summary>
|
||||
DateTimeOffset StartedAt,
|
||||
|
||||
/// <summary>When the import completed.</summary>
|
||||
DateTimeOffset? CompletedAt,
|
||||
|
||||
/// <summary>Final status of the import.</summary>
|
||||
BundleImportStatus Status,
|
||||
|
||||
/// <summary>Error message if failed.</summary>
|
||||
string? ErrorMessage,
|
||||
|
||||
/// <summary>Actor who initiated the import.</summary>
|
||||
string? InitiatedBy,
|
||||
|
||||
/// <summary>Input bundle manifest.</summary>
|
||||
BundleImportInputManifest? InputManifest,
|
||||
|
||||
/// <summary>Output files with hashes.</summary>
|
||||
IReadOnlyList<BundleImportOutputFile> OutputFiles,
|
||||
|
||||
/// <summary>Import transcript log entries.</summary>
|
||||
IReadOnlyList<BundleImportTranscriptEntry> Transcript,
|
||||
|
||||
/// <summary>Validation results.</summary>
|
||||
BundleImportValidationResult? ValidationResult,
|
||||
|
||||
/// <summary>Computed hashes for evidence chain.</summary>
|
||||
BundleImportHashChain HashChain);
|
||||
|
||||
/// <summary>
|
||||
/// Bundle import status.
|
||||
/// </summary>
|
||||
public enum BundleImportStatus
|
||||
{
|
||||
/// <summary>Import is pending.</summary>
|
||||
Pending,
|
||||
|
||||
/// <summary>Import is in progress.</summary>
|
||||
InProgress,
|
||||
|
||||
/// <summary>Import completed successfully.</summary>
|
||||
Completed,
|
||||
|
||||
/// <summary>Import failed.</summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>Import was cancelled.</summary>
|
||||
Cancelled,
|
||||
|
||||
/// <summary>Import is partially complete.</summary>
|
||||
PartiallyComplete
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input bundle manifest from the import source.
|
||||
/// </summary>
|
||||
public sealed record BundleImportInputManifest(
|
||||
/// <summary>Bundle format version.</summary>
|
||||
string FormatVersion,
|
||||
|
||||
/// <summary>Bundle identifier.</summary>
|
||||
string BundleId,
|
||||
|
||||
/// <summary>Bundle version.</summary>
|
||||
string BundleVersion,
|
||||
|
||||
/// <summary>When the bundle was created.</summary>
|
||||
DateTimeOffset CreatedAt,
|
||||
|
||||
/// <summary>Who created the bundle.</summary>
|
||||
string? CreatedBy,
|
||||
|
||||
/// <summary>Total size in bytes.</summary>
|
||||
long TotalSizeBytes,
|
||||
|
||||
/// <summary>Number of items in the bundle.</summary>
|
||||
int ItemCount,
|
||||
|
||||
/// <summary>SHA-256 of the manifest.</summary>
|
||||
string ManifestSha256,
|
||||
|
||||
/// <summary>Bundle signature if present.</summary>
|
||||
string? Signature,
|
||||
|
||||
/// <summary>Signature verification status.</summary>
|
||||
bool? SignatureValid);
|
||||
|
||||
/// <summary>
|
||||
/// Output file from bundle import.
|
||||
/// </summary>
|
||||
public sealed record BundleImportOutputFile(
|
||||
/// <summary>Relative path within staging directory.</summary>
|
||||
string RelativePath,
|
||||
|
||||
/// <summary>SHA-256 hash of the file.</summary>
|
||||
string Sha256,
|
||||
|
||||
/// <summary>Size in bytes.</summary>
|
||||
long SizeBytes,
|
||||
|
||||
/// <summary>Media type.</summary>
|
||||
string MediaType,
|
||||
|
||||
/// <summary>When the file was staged.</summary>
|
||||
DateTimeOffset StagedAt,
|
||||
|
||||
/// <summary>Source item identifier in the bundle.</summary>
|
||||
string? SourceItemId);
|
||||
|
||||
/// <summary>
|
||||
/// Transcript entry for bundle import.
|
||||
/// </summary>
|
||||
public sealed record BundleImportTranscriptEntry(
|
||||
/// <summary>When the entry was recorded.</summary>
|
||||
DateTimeOffset Timestamp,
|
||||
|
||||
/// <summary>Log level.</summary>
|
||||
string Level,
|
||||
|
||||
/// <summary>Event type.</summary>
|
||||
string EventType,
|
||||
|
||||
/// <summary>Message.</summary>
|
||||
string Message,
|
||||
|
||||
/// <summary>Additional data.</summary>
|
||||
IReadOnlyDictionary<string, string>? Data);
|
||||
|
||||
/// <summary>
|
||||
/// Bundle import validation result.
|
||||
/// </summary>
|
||||
public sealed record BundleImportValidationResult(
|
||||
/// <summary>Whether validation passed.</summary>
|
||||
bool Valid,
|
||||
|
||||
/// <summary>Checksum verification passed.</summary>
|
||||
bool ChecksumValid,
|
||||
|
||||
/// <summary>Signature verification passed.</summary>
|
||||
bool? SignatureValid,
|
||||
|
||||
/// <summary>Format validation passed.</summary>
|
||||
bool FormatValid,
|
||||
|
||||
/// <summary>Validation errors.</summary>
|
||||
IReadOnlyList<string>? Errors,
|
||||
|
||||
/// <summary>Validation warnings.</summary>
|
||||
IReadOnlyList<string>? Warnings);
|
||||
|
||||
/// <summary>
|
||||
/// Hash chain for bundle import evidence.
|
||||
/// </summary>
|
||||
public sealed record BundleImportHashChain(
|
||||
/// <summary>Hash of all input files.</summary>
|
||||
string InputsHash,
|
||||
|
||||
/// <summary>Hash of all output files.</summary>
|
||||
string OutputsHash,
|
||||
|
||||
/// <summary>Hash of the transcript.</summary>
|
||||
string TranscriptHash,
|
||||
|
||||
/// <summary>Combined root hash.</summary>
|
||||
string RootHash,
|
||||
|
||||
/// <summary>Algorithm used.</summary>
|
||||
string Algorithm)
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes hash chain from import evidence data.
|
||||
/// </summary>
|
||||
public static BundleImportHashChain Compute(
|
||||
BundleImportInputManifest? input,
|
||||
IReadOnlyList<BundleImportOutputFile> outputs,
|
||||
IReadOnlyList<BundleImportTranscriptEntry> transcript)
|
||||
{
|
||||
// Compute input hash
|
||||
var inputJson = input is not null
|
||||
? JsonSerializer.Serialize(input, JsonOptions)
|
||||
: "null";
|
||||
var inputsHash = ComputeSha256(inputJson);
|
||||
|
||||
// Compute outputs hash (sorted for determinism)
|
||||
var sortedOutputs = outputs
|
||||
.OrderBy(o => o.RelativePath, StringComparer.Ordinal)
|
||||
.Select(o => o.Sha256)
|
||||
.ToList();
|
||||
var outputsJson = JsonSerializer.Serialize(sortedOutputs, JsonOptions);
|
||||
var outputsHash = ComputeSha256(outputsJson);
|
||||
|
||||
// Compute transcript hash
|
||||
var transcriptJson = JsonSerializer.Serialize(transcript, JsonOptions);
|
||||
var transcriptHash = ComputeSha256(transcriptJson);
|
||||
|
||||
// Compute root hash
|
||||
var combined = $"{inputsHash}|{outputsHash}|{transcriptHash}";
|
||||
var rootHash = ComputeSha256(combined);
|
||||
|
||||
return new BundleImportHashChain(
|
||||
InputsHash: inputsHash,
|
||||
OutputsHash: outputsHash,
|
||||
TranscriptHash: transcriptHash,
|
||||
RootHash: rootHash,
|
||||
Algorithm: "sha256");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
private static string ComputeSha256(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.TaskRunner.Core.Events;
|
||||
|
||||
namespace StellaOps.TaskRunner.Core.Evidence;
|
||||
|
||||
/// <summary>
|
||||
/// Service for capturing bundle import evidence.
|
||||
/// Per TASKRUN-AIRGAP-58-001.
|
||||
/// </summary>
|
||||
public interface IBundleImportEvidenceService
|
||||
{
|
||||
/// <summary>
|
||||
/// Captures evidence for a bundle import operation.
|
||||
/// </summary>
|
||||
Task<BundleImportEvidenceResult> CaptureAsync(
|
||||
BundleImportEvidence evidence,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Exports evidence to a portable bundle format.
|
||||
/// </summary>
|
||||
Task<PortableEvidenceBundleResult> ExportToPortableBundleAsync(
|
||||
string jobId,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets evidence for a bundle import job.
|
||||
/// </summary>
|
||||
Task<BundleImportEvidence?> GetAsync(
|
||||
string jobId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of capturing bundle import evidence.
|
||||
/// </summary>
|
||||
public sealed record BundleImportEvidenceResult(
|
||||
/// <summary>Whether capture was successful.</summary>
|
||||
bool Success,
|
||||
|
||||
/// <summary>The captured snapshot.</summary>
|
||||
PackRunEvidenceSnapshot? Snapshot,
|
||||
|
||||
/// <summary>Evidence pointer for linking.</summary>
|
||||
PackRunEvidencePointer? EvidencePointer,
|
||||
|
||||
/// <summary>Error message if capture failed.</summary>
|
||||
string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// Result of exporting to portable bundle.
|
||||
/// </summary>
|
||||
public sealed record PortableEvidenceBundleResult(
|
||||
/// <summary>Whether export was successful.</summary>
|
||||
bool Success,
|
||||
|
||||
/// <summary>Path to the exported bundle.</summary>
|
||||
string? OutputPath,
|
||||
|
||||
/// <summary>SHA-256 of the bundle.</summary>
|
||||
string? BundleSha256,
|
||||
|
||||
/// <summary>Size in bytes.</summary>
|
||||
long SizeBytes,
|
||||
|
||||
/// <summary>Error message if export failed.</summary>
|
||||
string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of bundle import evidence service.
|
||||
/// </summary>
|
||||
public sealed class BundleImportEvidenceService : IBundleImportEvidenceService
|
||||
{
|
||||
private readonly IPackRunEvidenceStore _store;
|
||||
private readonly IPackRunTimelineEventEmitter? _timelineEmitter;
|
||||
private readonly ILogger<BundleImportEvidenceService> _logger;
|
||||
|
||||
public BundleImportEvidenceService(
|
||||
IPackRunEvidenceStore store,
|
||||
ILogger<BundleImportEvidenceService> logger,
|
||||
IPackRunTimelineEventEmitter? timelineEmitter = null)
|
||||
{
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_timelineEmitter = timelineEmitter;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BundleImportEvidenceResult> CaptureAsync(
|
||||
BundleImportEvidence evidence,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(evidence);
|
||||
|
||||
try
|
||||
{
|
||||
var materials = new List<PackRunEvidenceMaterial>();
|
||||
|
||||
// Add input manifest
|
||||
if (evidence.InputManifest is not null)
|
||||
{
|
||||
materials.Add(PackRunEvidenceMaterial.FromJson(
|
||||
"input",
|
||||
"manifest.json",
|
||||
evidence.InputManifest,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["bundleId"] = evidence.InputManifest.BundleId,
|
||||
["bundleVersion"] = evidence.InputManifest.BundleVersion
|
||||
}));
|
||||
}
|
||||
|
||||
// Add output files as materials
|
||||
foreach (var output in evidence.OutputFiles)
|
||||
{
|
||||
materials.Add(new PackRunEvidenceMaterial(
|
||||
Section: "output",
|
||||
Path: output.RelativePath,
|
||||
Sha256: output.Sha256,
|
||||
SizeBytes: output.SizeBytes,
|
||||
MediaType: output.MediaType,
|
||||
Attributes: new Dictionary<string, string>
|
||||
{
|
||||
["stagedAt"] = output.StagedAt.ToString("O")
|
||||
}));
|
||||
}
|
||||
|
||||
// Add transcript
|
||||
materials.Add(PackRunEvidenceMaterial.FromJson(
|
||||
"transcript",
|
||||
"import-log.json",
|
||||
evidence.Transcript));
|
||||
|
||||
// Add validation result
|
||||
if (evidence.ValidationResult is not null)
|
||||
{
|
||||
materials.Add(PackRunEvidenceMaterial.FromJson(
|
||||
"validation",
|
||||
"result.json",
|
||||
evidence.ValidationResult));
|
||||
}
|
||||
|
||||
// Add hash chain
|
||||
materials.Add(PackRunEvidenceMaterial.FromJson(
|
||||
"hashchain",
|
||||
"chain.json",
|
||||
evidence.HashChain));
|
||||
|
||||
// Create metadata
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["jobId"] = evidence.JobId,
|
||||
["status"] = evidence.Status.ToString(),
|
||||
["sourcePath"] = evidence.SourcePath,
|
||||
["startedAt"] = evidence.StartedAt.ToString("O"),
|
||||
["outputCount"] = evidence.OutputFiles.Count.ToString(),
|
||||
["rootHash"] = evidence.HashChain.RootHash
|
||||
};
|
||||
|
||||
if (evidence.CompletedAt.HasValue)
|
||||
{
|
||||
metadata["completedAt"] = evidence.CompletedAt.Value.ToString("O");
|
||||
metadata["durationMs"] = ((evidence.CompletedAt.Value - evidence.StartedAt).TotalMilliseconds).ToString("F0");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(evidence.InitiatedBy))
|
||||
{
|
||||
metadata["initiatedBy"] = evidence.InitiatedBy;
|
||||
}
|
||||
|
||||
// Create snapshot
|
||||
var snapshot = PackRunEvidenceSnapshot.Create(
|
||||
tenantId: evidence.TenantId,
|
||||
runId: evidence.JobId,
|
||||
planHash: evidence.HashChain.RootHash,
|
||||
kind: PackRunEvidenceSnapshotKind.BundleImport,
|
||||
materials: materials,
|
||||
metadata: metadata);
|
||||
|
||||
// Store snapshot
|
||||
await _store.StoreAsync(snapshot, cancellationToken);
|
||||
|
||||
var evidencePointer = PackRunEvidencePointer.Bundle(
|
||||
snapshot.SnapshotId,
|
||||
snapshot.RootHash);
|
||||
|
||||
// Emit timeline event
|
||||
if (_timelineEmitter is not null)
|
||||
{
|
||||
await _timelineEmitter.EmitAsync(
|
||||
PackRunTimelineEvent.Create(
|
||||
tenantId: evidence.TenantId,
|
||||
eventType: "bundle.import.evidence_captured",
|
||||
source: "taskrunner-bundle-import",
|
||||
occurredAt: DateTimeOffset.UtcNow,
|
||||
runId: evidence.JobId,
|
||||
planHash: evidence.HashChain.RootHash,
|
||||
attributes: new Dictionary<string, string>
|
||||
{
|
||||
["snapshotId"] = snapshot.SnapshotId.ToString(),
|
||||
["rootHash"] = snapshot.RootHash,
|
||||
["status"] = evidence.Status.ToString(),
|
||||
["outputCount"] = evidence.OutputFiles.Count.ToString()
|
||||
},
|
||||
evidencePointer: evidencePointer),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Captured bundle import evidence for job {JobId} with {OutputCount} outputs, root hash {RootHash}",
|
||||
evidence.JobId,
|
||||
evidence.OutputFiles.Count,
|
||||
evidence.HashChain.RootHash);
|
||||
|
||||
return new BundleImportEvidenceResult(
|
||||
Success: true,
|
||||
Snapshot: snapshot,
|
||||
EvidencePointer: evidencePointer,
|
||||
Error: null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to capture bundle import evidence for job {JobId}", evidence.JobId);
|
||||
|
||||
return new BundleImportEvidenceResult(
|
||||
Success: false,
|
||||
Snapshot: null,
|
||||
EvidencePointer: null,
|
||||
Error: ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PortableEvidenceBundleResult> ExportToPortableBundleAsync(
|
||||
string jobId,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get all snapshots for this job
|
||||
var snapshots = await _store.GetByRunIdAsync(jobId, cancellationToken);
|
||||
if (snapshots.Count == 0)
|
||||
{
|
||||
return new PortableEvidenceBundleResult(
|
||||
Success: false,
|
||||
OutputPath: null,
|
||||
BundleSha256: null,
|
||||
SizeBytes: 0,
|
||||
Error: $"No evidence found for job {jobId}");
|
||||
}
|
||||
|
||||
// Create portable bundle structure
|
||||
var bundleManifest = new PortableEvidenceBundleManifest
|
||||
{
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
JobId = jobId,
|
||||
SnapshotCount = snapshots.Count,
|
||||
Snapshots = snapshots.Select(s => new PortableSnapshotReference
|
||||
{
|
||||
SnapshotId = s.SnapshotId,
|
||||
Kind = s.Kind.ToString(),
|
||||
RootHash = s.RootHash,
|
||||
CreatedAt = s.CreatedAt,
|
||||
MaterialCount = s.Materials.Count
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
// Serialize bundle
|
||||
var bundleJson = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
manifest = bundleManifest,
|
||||
snapshots = snapshots
|
||||
}, new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
// Write to file
|
||||
await File.WriteAllTextAsync(outputPath, bundleJson, cancellationToken);
|
||||
var fileInfo = new FileInfo(outputPath);
|
||||
|
||||
// Compute bundle hash
|
||||
var bundleBytes = await File.ReadAllBytesAsync(outputPath, cancellationToken);
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(bundleBytes);
|
||||
var bundleSha256 = $"sha256:{Convert.ToHexString(hash).ToLowerInvariant()}";
|
||||
|
||||
_logger.LogInformation(
|
||||
"Exported portable evidence bundle for job {JobId} to {OutputPath}, size {SizeBytes} bytes",
|
||||
jobId,
|
||||
outputPath,
|
||||
fileInfo.Length);
|
||||
|
||||
return new PortableEvidenceBundleResult(
|
||||
Success: true,
|
||||
OutputPath: outputPath,
|
||||
BundleSha256: bundleSha256,
|
||||
SizeBytes: fileInfo.Length,
|
||||
Error: null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to export portable evidence bundle for job {JobId}", jobId);
|
||||
|
||||
return new PortableEvidenceBundleResult(
|
||||
Success: false,
|
||||
OutputPath: null,
|
||||
BundleSha256: null,
|
||||
SizeBytes: 0,
|
||||
Error: ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BundleImportEvidence?> GetAsync(
|
||||
string jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var snapshots = await _store.GetByRunIdAsync(jobId, cancellationToken);
|
||||
var importSnapshot = snapshots.FirstOrDefault(s => s.Kind == PackRunEvidenceSnapshotKind.BundleImport);
|
||||
|
||||
if (importSnapshot is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reconstruct evidence from snapshot
|
||||
return ReconstructEvidence(importSnapshot);
|
||||
}
|
||||
|
||||
private static BundleImportEvidence? ReconstructEvidence(PackRunEvidenceSnapshot snapshot)
|
||||
{
|
||||
// This would deserialize the stored materials back into the evidence structure
|
||||
// For now, return a minimal reconstruction from metadata
|
||||
var metadata = snapshot.Metadata ?? new Dictionary<string, string>();
|
||||
|
||||
return new BundleImportEvidence(
|
||||
JobId: metadata.GetValueOrDefault("jobId", snapshot.RunId),
|
||||
TenantId: snapshot.TenantId,
|
||||
SourcePath: metadata.GetValueOrDefault("sourcePath", "unknown"),
|
||||
StartedAt: DateTimeOffset.TryParse(metadata.GetValueOrDefault("startedAt"), out var started)
|
||||
? started : snapshot.CreatedAt,
|
||||
CompletedAt: DateTimeOffset.TryParse(metadata.GetValueOrDefault("completedAt"), out var completed)
|
||||
? completed : null,
|
||||
Status: Enum.TryParse<BundleImportStatus>(metadata.GetValueOrDefault("status"), out var status)
|
||||
? status : BundleImportStatus.Completed,
|
||||
ErrorMessage: null,
|
||||
InitiatedBy: metadata.GetValueOrDefault("initiatedBy"),
|
||||
InputManifest: null,
|
||||
OutputFiles: [],
|
||||
Transcript: [],
|
||||
ValidationResult: null,
|
||||
HashChain: new BundleImportHashChain(
|
||||
InputsHash: "sha256:reconstructed",
|
||||
OutputsHash: "sha256:reconstructed",
|
||||
TranscriptHash: "sha256:reconstructed",
|
||||
RootHash: metadata.GetValueOrDefault("rootHash", snapshot.RootHash),
|
||||
Algorithm: "sha256"));
|
||||
}
|
||||
|
||||
private sealed class PortableEvidenceBundleManifest
|
||||
{
|
||||
public required string Version { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required string JobId { get; init; }
|
||||
public required int SnapshotCount { get; init; }
|
||||
public required IReadOnlyList<PortableSnapshotReference> Snapshots { get; init; }
|
||||
}
|
||||
|
||||
private sealed class PortableSnapshotReference
|
||||
{
|
||||
public required Guid SnapshotId { get; init; }
|
||||
public required string Kind { get; init; }
|
||||
public required string RootHash { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required int MaterialCount { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ public interface IPackRunEvidenceStore
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets evidence snapshots by run ID only (across all tenants).
|
||||
/// For bundle import evidence lookups.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PackRunEvidenceSnapshot>> GetByRunIdAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists evidence snapshots by kind for a run.
|
||||
/// </summary>
|
||||
@@ -109,6 +117,20 @@ public sealed class InMemoryPackRunEvidenceStore : IPackRunEvidenceStore
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<PackRunEvidenceSnapshot>> GetByRunIdAsync(
|
||||
string runId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var results = _snapshots.Values
|
||||
.Where(s => s.RunId == runId)
|
||||
.OrderBy(s => s.CreatedAt)
|
||||
.ToList();
|
||||
return Task.FromResult<IReadOnlyList<PackRunEvidenceSnapshot>>(results);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<PackRunEvidenceSnapshot>> ListByKindAsync(
|
||||
string tenantId,
|
||||
string runId,
|
||||
|
||||
@@ -151,7 +151,10 @@ public enum PackRunEvidenceSnapshotKind
|
||||
ArtifactManifest,
|
||||
|
||||
/// <summary>Environment digest snapshot.</summary>
|
||||
EnvironmentDigest
|
||||
EnvironmentDigest,
|
||||
|
||||
/// <summary>Bundle import snapshot (TASKRUN-AIRGAP-58-001).</summary>
|
||||
BundleImport
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user