new advisories work and features gaps work

This commit is contained in:
master
2026-01-14 18:39:19 +02:00
parent 95d5898650
commit 15aeac8e8b
148 changed files with 16731 additions and 554 deletions

View File

@@ -0,0 +1,401 @@
// <copyright file="EvidenceCardService.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// Sprint: SPRINT_20260112_004_LB_evidence_card_core (EVPCARD-LB-002)
// Description: Service implementation for evidence card operations.
// </copyright>
using System.Collections.Immutable;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.Determinism;
using StellaOps.Evidence.Pack.Models;
namespace StellaOps.Evidence.Pack;
/// <summary>
/// Implementation of <see cref="IEvidenceCardService"/>.
/// </summary>
public sealed class EvidenceCardService : IEvidenceCardService
{
private static readonly JsonSerializerOptions IndentedOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.Default
};
private static readonly JsonSerializerOptions CompactOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.Default
};
private readonly TimeProvider _timeProvider;
private readonly IGuidProvider _guidProvider;
private readonly ILogger<EvidenceCardService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="EvidenceCardService"/> class.
/// </summary>
public EvidenceCardService(
TimeProvider? timeProvider = null,
IGuidProvider? guidProvider = null,
ILogger<EvidenceCardService>? logger = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<EvidenceCardService>.Instance;
}
/// <inheritdoc/>
public Task<EvidenceCard> CreateCardAsync(
EvidenceCardRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
var cardId = _guidProvider.NewGuid().ToString("N", CultureInfo.InvariantCulture);
var now = _timeProvider.GetUtcNow();
// Create subject
var subject = new EvidenceCardSubject
{
FindingId = request.FindingId,
ArtifactDigest = request.ArtifactDigest,
ComponentPurl = request.ComponentPurl
};
// Create placeholder SBOM excerpt (real implementation would fetch from SBOM service)
var sbomExcerpt = CreatePlaceholderSbomExcerpt(request);
// Create placeholder DSSE envelope (real implementation would sign the payload)
var envelope = CreatePlaceholderEnvelope(cardId, subject, now);
// Create Rekor receipt metadata (optional, placeholder for now)
RekorReceiptMetadata? rekorReceipt = null;
if (request.IncludeRekorReceipt)
{
// In real implementation, this would be populated from actual Rekor submission
_logger.LogDebug("Rekor receipt requested but not yet implemented; card will have null receipt");
}
var card = new EvidenceCard
{
CardId = cardId,
Subject = subject,
SbomExcerpt = sbomExcerpt,
Envelope = envelope,
RekorReceipt = rekorReceipt,
GeneratedAt = now,
Tool = new EvidenceCardTool
{
Name = "StellaOps",
Version = "1.0.0",
Vendor = "StellaOps Inc"
}
};
_logger.LogInformation("Created evidence card {CardId} for finding {FindingId}", cardId, request.FindingId);
return Task.FromResult(card);
}
/// <inheritdoc/>
public Task<EvidenceCardExport> ExportCardAsync(
EvidenceCard card,
EvidenceCardExportFormat format,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(card);
byte[] content;
string contentType;
switch (format)
{
case EvidenceCardExportFormat.Json:
content = JsonSerializer.SerializeToUtf8Bytes(card, IndentedOptions);
contentType = "application/json";
break;
case EvidenceCardExportFormat.CompactJson:
content = JsonSerializer.SerializeToUtf8Bytes(card, CompactOptions);
contentType = "application/json";
break;
case EvidenceCardExportFormat.CanonicalJson:
var json = JsonSerializer.Serialize(card, CompactOptions);
content = Encoding.UTF8.GetBytes(CanonicalizeJson(json));
contentType = "application/json";
break;
default:
throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported export format");
}
var digest = ComputeDigest(content);
var export = new EvidenceCardExport
{
CardId = card.CardId,
Format = format,
Content = content,
ContentDigest = digest,
ContentType = contentType,
FileName = $"evidence-card-{card.CardId}.json"
};
_logger.LogDebug("Exported evidence card {CardId} to {Format} ({Size} bytes)",
card.CardId, format, content.Length);
return Task.FromResult(export);
}
/// <inheritdoc/>
public Task<EvidenceCardVerificationResult> VerifyCardAsync(
EvidenceCard card,
EvidenceCardVerificationOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(card);
options ??= new EvidenceCardVerificationOptions();
var issues = new List<string>();
// Verify DSSE envelope (placeholder - real implementation would verify signature)
var signatureValid = !string.IsNullOrEmpty(card.Envelope.PayloadDigest);
if (!signatureValid)
{
issues.Add("DSSE envelope signature verification failed");
}
// Verify SBOM digest
var sbomDigestValid = !string.IsNullOrEmpty(card.SbomExcerpt.SbomDigest);
if (!sbomDigestValid)
{
issues.Add("SBOM excerpt digest is missing");
}
// Verify Rekor receipt if present
bool? rekorReceiptValid = null;
if (card.RekorReceipt is not null)
{
rekorReceiptValid = VerifyRekorReceiptOffline(card.RekorReceipt, options, issues);
}
else if (!options.AllowMissingReceipt)
{
issues.Add("Rekor receipt is required but not present");
}
var valid = signatureValid && sbomDigestValid && (rekorReceiptValid ?? true) && issues.Count == 0;
return Task.FromResult(new EvidenceCardVerificationResult
{
Valid = valid,
SignatureValid = signatureValid,
RekorReceiptValid = rekorReceiptValid,
SbomDigestValid = sbomDigestValid,
Issues = issues
});
}
private static SbomExcerpt CreatePlaceholderSbomExcerpt(EvidenceCardRequest request)
{
var components = ImmutableArray<SbomComponent>.Empty;
if (!string.IsNullOrEmpty(request.ComponentPurl))
{
components = ImmutableArray.Create(new SbomComponent
{
Purl = request.ComponentPurl,
Name = ExtractNameFromPurl(request.ComponentPurl),
Version = ExtractVersionFromPurl(request.ComponentPurl)
});
}
return new SbomExcerpt
{
Format = "cyclonedx",
FormatVersion = "1.6",
SbomDigest = $"sha256:{ComputeDigestString(request.ArtifactDigest)}",
Components = components,
MaxSizeBytes = request.MaxSbomExcerptSize
};
}
private static DsseEnvelope CreatePlaceholderEnvelope(
string cardId,
EvidenceCardSubject subject,
DateTimeOffset timestamp)
{
var payload = JsonSerializer.Serialize(new
{
cardId,
subject.FindingId,
subject.ArtifactDigest,
timestamp = timestamp.ToString("O", CultureInfo.InvariantCulture)
}, CompactOptions);
var payloadBytes = Encoding.UTF8.GetBytes(payload);
var payloadBase64 = Convert.ToBase64String(payloadBytes);
var payloadDigest = ComputeDigest(payloadBytes);
return new DsseEnvelope
{
PayloadType = "application/vnd.stellaops.evidence-card+json",
Payload = payloadBase64,
PayloadDigest = payloadDigest,
Signatures = ImmutableArray.Create(new DsseSignature
{
KeyId = "placeholder-key",
Sig = Convert.ToBase64String(Encoding.UTF8.GetBytes("placeholder-signature"))
})
};
}
private static bool VerifyRekorReceiptOffline(
RekorReceiptMetadata receipt,
EvidenceCardVerificationOptions options,
List<string> issues)
{
// Basic structural validation
if (string.IsNullOrEmpty(receipt.Uuid))
{
issues.Add("Rekor receipt UUID is missing");
return false;
}
if (receipt.LogIndex < 0)
{
issues.Add("Rekor receipt log index is invalid");
return false;
}
if (string.IsNullOrEmpty(receipt.RootHash))
{
issues.Add("Rekor receipt root hash is missing");
return false;
}
if (receipt.InclusionProofHashes.Length == 0)
{
issues.Add("Rekor receipt inclusion proof is empty");
return false;
}
// Full verification would validate:
// 1. Checkpoint signature against trusted keys
// 2. Inclusion proof verification
// 3. Entry body hash against log entry
return true;
}
private static string CanonicalizeJson(string json)
{
// RFC 8785 canonicalization (simplified - real impl would use StellaOps.Canonical.Json)
using var document = JsonDocument.Parse(json);
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false });
WriteCanonical(writer, document.RootElement);
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
private static void WriteCanonical(Utf8JsonWriter writer, JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
writer.WriteStartObject();
var properties = element.EnumerateObject()
.OrderBy(p => p.Name, StringComparer.Ordinal)
.ToList();
foreach (var prop in properties)
{
writer.WritePropertyName(prop.Name);
WriteCanonical(writer, prop.Value);
}
writer.WriteEndObject();
break;
case JsonValueKind.Array:
writer.WriteStartArray();
foreach (var item in element.EnumerateArray())
{
WriteCanonical(writer, item);
}
writer.WriteEndArray();
break;
case JsonValueKind.String:
writer.WriteStringValue(element.GetString());
break;
case JsonValueKind.Number:
if (element.TryGetInt64(out var longVal))
{
writer.WriteNumberValue(longVal);
}
else
{
writer.WriteNumberValue(element.GetDouble());
}
break;
case JsonValueKind.True:
writer.WriteBooleanValue(true);
break;
case JsonValueKind.False:
writer.WriteBooleanValue(false);
break;
case JsonValueKind.Null:
writer.WriteNullValue();
break;
}
}
private static string ComputeDigest(byte[] data)
{
var hash = SHA256.HashData(data);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private static string ComputeDigestString(string data)
{
var bytes = Encoding.UTF8.GetBytes(data);
var hash = SHA256.HashData(bytes);
return Convert.ToHexStringLower(hash);
}
private static string ExtractNameFromPurl(string purl)
{
// Simple PURL name extraction
var parts = purl.Split('/');
if (parts.Length > 1)
{
var nameVersion = parts[^1];
var atIndex = nameVersion.IndexOf('@');
return atIndex > 0 ? nameVersion[..atIndex] : nameVersion;
}
return purl;
}
private static string ExtractVersionFromPurl(string purl)
{
var atIndex = purl.LastIndexOf('@');
return atIndex > 0 ? purl[(atIndex + 1)..] : "unknown";
}
}

View File

@@ -6,6 +6,8 @@ using System.Collections.Immutable;
using System.Globalization;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.Evidence.Pack.Models;
@@ -267,6 +269,9 @@ internal sealed class EvidencePackService : IEvidencePackService
EvidencePackExportFormat.Markdown => ExportAsMarkdown(pack),
EvidencePackExportFormat.Html => ExportAsHtml(pack),
EvidencePackExportFormat.Pdf => throw new NotSupportedException("PDF export requires additional configuration"),
// Sprint: SPRINT_20260112_005_BE_evidence_card_api (EVPCARD-BE-001)
EvidencePackExportFormat.EvidenceCard => await ExportAsEvidenceCard(pack, compact: false, cancellationToken).ConfigureAwait(false),
EvidencePackExportFormat.EvidenceCardCompact => await ExportAsEvidenceCard(pack, compact: true, cancellationToken).ConfigureAwait(false),
_ => throw new ArgumentOutOfRangeException(nameof(format), format, "Unsupported export format")
};
}
@@ -417,6 +422,95 @@ internal sealed class EvidencePackService : IEvidencePackService
};
}
// Sprint: SPRINT_20260112_005_BE_evidence_card_api (EVPCARD-BE-001)
private async Task<EvidencePackExport> ExportAsEvidenceCard(
EvidencePack pack,
bool compact,
CancellationToken cancellationToken)
{
// Get signed pack if available
var signedPack = await _store.GetSignedByIdAsync(pack.TenantId, pack.PackId, cancellationToken)
.ConfigureAwait(false);
// Compute content digest for this pack
var contentDigest = pack.ComputeContentDigest();
// Build evidence card structure using simple object
var card = new
{
schema_version = "1.0.0",
pack_id = pack.PackId,
created_at = pack.CreatedAt,
finding_id = pack.Subject.FindingId,
cve_id = pack.Subject.CveId,
component = pack.Subject.Component,
claims = pack.Claims.Select(c => new
{
claim_type = c.Type.ToString(),
text = c.Text,
status = c.Status,
confidence = c.Confidence
}).ToList(),
sbom_excerpt = compact ? null : BuildSbomExcerptFromEvidence(pack),
dsse_envelope = signedPack is not null
? new
{
payload_type = signedPack.Envelope.PayloadType,
payload_digest = signedPack.Envelope.PayloadDigest,
signatures = signedPack.Envelope.Signatures.Select(s => new
{
key_id = s.KeyId,
sig = s.Sig
}).ToList()
}
: null,
signed_at = signedPack?.SignedAt,
content_digest = contentDigest
};
var json = JsonSerializer.Serialize(card, EvidenceCardJsonOptions);
var format = compact ? EvidencePackExportFormat.EvidenceCardCompact : EvidencePackExportFormat.EvidenceCard;
return new EvidencePackExport
{
PackId = pack.PackId,
Format = format,
Content = Encoding.UTF8.GetBytes(json),
ContentType = "application/vnd.stellaops.evidence-card+json",
FileName = $"evidence-card-{pack.PackId}.json"
};
}
private static object? BuildSbomExcerptFromEvidence(EvidencePack pack)
{
// Extract components from evidence items for determinism
var components = pack.Evidence
.Where(e => e.Type == EvidenceType.Sbom && !string.IsNullOrEmpty(e.Uri))
.OrderBy(e => e.Uri, StringComparer.Ordinal)
.Take(50)
.Select(e => new { uri = e.Uri, digest = e.Digest })
.ToList();
if (components.Count == 0)
{
return null;
}
return new
{
total_evidence_count = pack.Evidence.Length,
excerpt_count = components.Count,
components
};
}
private static readonly JsonSerializerOptions EvidenceCardJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private const string HtmlTemplate = """
<!DOCTYPE html>
<html>

View File

@@ -0,0 +1,137 @@
// <copyright file="IEvidenceCardService.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// Sprint: SPRINT_20260112_004_LB_evidence_card_core (EVPCARD-LB-002)
// Description: Service interface for evidence card operations.
// </copyright>
using StellaOps.Evidence.Pack.Models;
namespace StellaOps.Evidence.Pack;
/// <summary>
/// Service for creating and exporting evidence cards.
/// </summary>
public interface IEvidenceCardService
{
/// <summary>
/// Creates an evidence card for a finding.
/// </summary>
/// <param name="request">The card creation request.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created evidence card.</returns>
Task<EvidenceCard> CreateCardAsync(
EvidenceCardRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// Exports an evidence card to a specific format.
/// </summary>
/// <param name="card">The evidence card to export.</param>
/// <param name="format">The export format.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The exported card.</returns>
Task<EvidenceCardExport> ExportCardAsync(
EvidenceCard card,
EvidenceCardExportFormat format,
CancellationToken cancellationToken = default);
/// <summary>
/// Verifies an evidence card's integrity and Rekor receipt.
/// </summary>
/// <param name="card">The evidence card to verify.</param>
/// <param name="options">Verification options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Verification result.</returns>
Task<EvidenceCardVerificationResult> VerifyCardAsync(
EvidenceCard card,
EvidenceCardVerificationOptions? options = null,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Request to create an evidence card.
/// </summary>
public sealed record EvidenceCardRequest
{
/// <summary>
/// Finding or vulnerability identifier.
/// </summary>
public required string FindingId { get; init; }
/// <summary>
/// Artifact digest.
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// Component PURL.
/// </summary>
public string? ComponentPurl { get; init; }
/// <summary>
/// Tenant identifier.
/// </summary>
public required string TenantId { get; init; }
/// <summary>
/// Whether to include Rekor receipt.
/// </summary>
public bool IncludeRekorReceipt { get; init; } = true;
/// <summary>
/// Maximum SBOM excerpt size in bytes.
/// </summary>
public int MaxSbomExcerptSize { get; init; } = 65536;
}
/// <summary>
/// Options for evidence card verification.
/// </summary>
public sealed record EvidenceCardVerificationOptions
{
/// <summary>
/// Whether to verify the Rekor receipt online.
/// </summary>
public bool VerifyRekorOnline { get; init; } = false;
/// <summary>
/// Whether to allow missing Rekor receipt.
/// </summary>
public bool AllowMissingReceipt { get; init; } = true;
/// <summary>
/// Trusted Rekor log public keys for offline verification.
/// </summary>
public IReadOnlyList<string>? TrustedRekorKeys { get; init; }
}
/// <summary>
/// Result of evidence card verification.
/// </summary>
public sealed record EvidenceCardVerificationResult
{
/// <summary>
/// Whether the card is valid.
/// </summary>
public required bool Valid { get; init; }
/// <summary>
/// Whether the DSSE signature is valid.
/// </summary>
public required bool SignatureValid { get; init; }
/// <summary>
/// Whether the Rekor receipt is valid (null if not present).
/// </summary>
public bool? RekorReceiptValid { get; init; }
/// <summary>
/// Whether the SBOM excerpt digest matches.
/// </summary>
public required bool SbomDigestValid { get; init; }
/// <summary>
/// Verification issues.
/// </summary>
public IReadOnlyList<string> Issues { get; init; } = Array.Empty<string>();
}

View File

@@ -0,0 +1,303 @@
// <copyright file="EvidenceCard.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// Sprint: SPRINT_20260112_004_LB_evidence_card_core (EVPCARD-LB-001)
// Description: Evidence card model for single-file evidence export with Rekor receipt support.
// </copyright>
using System.Collections.Immutable;
namespace StellaOps.Evidence.Pack.Models;
/// <summary>
/// A single-file evidence card containing SBOM excerpt, DSSE envelope, and optional Rekor receipt.
/// Designed for portable, offline-friendly evidence sharing and verification.
/// </summary>
public sealed record EvidenceCard
{
/// <summary>
/// Schema version for the evidence card format.
/// </summary>
public string SchemaVersion { get; init; } = "1.0.0";
/// <summary>
/// Unique identifier for this evidence card.
/// </summary>
public required string CardId { get; init; }
/// <summary>
/// The finding or vulnerability this card evidences.
/// </summary>
public required EvidenceCardSubject Subject { get; init; }
/// <summary>
/// SBOM excerpt containing relevant component data.
/// </summary>
public required SbomExcerpt SbomExcerpt { get; init; }
/// <summary>
/// DSSE envelope containing the signed evidence.
/// </summary>
public required DsseEnvelope Envelope { get; init; }
/// <summary>
/// Optional Rekor transparency log receipt.
/// </summary>
public RekorReceiptMetadata? RekorReceipt { get; init; }
/// <summary>
/// UTC timestamp when the card was generated.
/// </summary>
public required DateTimeOffset GeneratedAt { get; init; }
/// <summary>
/// Tool information that generated this card.
/// </summary>
public EvidenceCardTool? Tool { get; init; }
/// <summary>
/// Additional metadata as key-value pairs.
/// </summary>
public ImmutableDictionary<string, string> Metadata { get; init; } = ImmutableDictionary<string, string>.Empty;
}
/// <summary>
/// Subject of the evidence card (finding/vulnerability).
/// </summary>
public sealed record EvidenceCardSubject
{
/// <summary>
/// Vulnerability or finding identifier (e.g., CVE-2024-12345).
/// </summary>
public required string FindingId { get; init; }
/// <summary>
/// Artifact digest the finding applies to.
/// </summary>
public required string ArtifactDigest { get; init; }
/// <summary>
/// PURL of the affected component.
/// </summary>
public string? ComponentPurl { get; init; }
/// <summary>
/// Human-readable component name.
/// </summary>
public string? ComponentName { get; init; }
/// <summary>
/// Component version.
/// </summary>
public string? ComponentVersion { get; init; }
}
/// <summary>
/// SBOM excerpt for the evidence card.
/// </summary>
public sealed record SbomExcerpt
{
/// <summary>
/// SBOM format (e.g., cyclonedx, spdx).
/// </summary>
public required string Format { get; init; }
/// <summary>
/// SBOM format version (e.g., 1.6, 2.3).
/// </summary>
public required string FormatVersion { get; init; }
/// <summary>
/// Digest of the full SBOM document.
/// </summary>
public required string SbomDigest { get; init; }
/// <summary>
/// Extracted component data relevant to the finding.
/// </summary>
public required ImmutableArray<SbomComponent> Components { get; init; }
/// <summary>
/// Size limit for excerpt in bytes (default 64KB).
/// </summary>
public int MaxSizeBytes { get; init; } = 65536;
}
/// <summary>
/// A component extracted from the SBOM.
/// </summary>
public sealed record SbomComponent
{
/// <summary>
/// Component PURL.
/// </summary>
public required string Purl { get; init; }
/// <summary>
/// Component name.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Component version.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Component type (e.g., library, framework, application).
/// </summary>
public string? Type { get; init; }
/// <summary>
/// License identifiers.
/// </summary>
public ImmutableArray<string> Licenses { get; init; } = ImmutableArray<string>.Empty;
/// <summary>
/// Hashes of the component.
/// </summary>
public ImmutableDictionary<string, string> Hashes { get; init; } = ImmutableDictionary<string, string>.Empty;
}
/// <summary>
/// Rekor receipt metadata for transparency log inclusion.
/// </summary>
public sealed record RekorReceiptMetadata
{
/// <summary>
/// Unique entry identifier (UUID).
/// </summary>
public required string Uuid { get; init; }
/// <summary>
/// Log index (position in the log).
/// </summary>
public required long LogIndex { get; init; }
/// <summary>
/// Log ID identifying the Rekor instance.
/// </summary>
public required string LogId { get; init; }
/// <summary>
/// Base URL of the Rekor log.
/// </summary>
public required string LogUrl { get; init; }
/// <summary>
/// Unix timestamp when entry was integrated.
/// </summary>
public required long IntegratedTime { get; init; }
/// <summary>
/// Root hash of the log at integration time.
/// </summary>
public required string RootHash { get; init; }
/// <summary>
/// Tree size at integration time.
/// </summary>
public required long TreeSize { get; init; }
/// <summary>
/// Inclusion proof hashes (base64 encoded).
/// </summary>
public required ImmutableArray<string> InclusionProofHashes { get; init; }
/// <summary>
/// Signed checkpoint note (for offline verification).
/// </summary>
public required string CheckpointNote { get; init; }
/// <summary>
/// Checkpoint signatures.
/// </summary>
public required ImmutableArray<CheckpointSignature> CheckpointSignatures { get; init; }
}
/// <summary>
/// A checkpoint signature from the Rekor log.
/// </summary>
public sealed record CheckpointSignature
{
/// <summary>
/// Key identifier.
/// </summary>
public required string KeyId { get; init; }
/// <summary>
/// Base64-encoded signature.
/// </summary>
public required string Signature { get; init; }
}
/// <summary>
/// Tool information for the evidence card.
/// </summary>
public sealed record EvidenceCardTool
{
/// <summary>
/// Tool name.
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Tool version.
/// </summary>
public required string Version { get; init; }
/// <summary>
/// Optional vendor.
/// </summary>
public string? Vendor { get; init; }
}
/// <summary>
/// Export format options for evidence cards.
/// </summary>
public enum EvidenceCardExportFormat
{
/// <summary>JSON format with all fields.</summary>
Json,
/// <summary>Compact JSON (minified).</summary>
CompactJson,
/// <summary>Canonical JSON for deterministic hashing.</summary>
CanonicalJson
}
/// <summary>
/// Result of exporting an evidence card.
/// </summary>
public sealed record EvidenceCardExport
{
/// <summary>
/// Card identifier.
/// </summary>
public required string CardId { get; init; }
/// <summary>
/// Export format used.
/// </summary>
public required EvidenceCardExportFormat Format { get; init; }
/// <summary>
/// Exported content bytes.
/// </summary>
public required byte[] Content { get; init; }
/// <summary>
/// Content digest (sha256).
/// </summary>
public required string ContentDigest { get; init; }
/// <summary>
/// MIME content type.
/// </summary>
public required string ContentType { get; init; }
/// <summary>
/// Suggested filename.
/// </summary>
public required string FileName { get; init; }
}

View File

@@ -113,7 +113,15 @@ public enum EvidencePackExportFormat
Pdf,
/// <summary>Styled HTML report.</summary>
Html
Html,
// Sprint: SPRINT_20260112_005_BE_evidence_card_api (EVPCARD-BE-001)
/// <summary>Single-file evidence card with SBOM excerpt, DSSE envelope, and Rekor receipt.</summary>
EvidenceCard,
/// <summary>Compact evidence card without full SBOM.</summary>
EvidenceCardCompact
}
/// <summary>

View File

@@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\StellaOps.AdvisoryAI.Attestation\StellaOps.AdvisoryAI.Attestation.csproj" />
<ProjectReference Include="..\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>