new advisories work and features gaps work
This commit is contained in:
401
src/__Libraries/StellaOps.Evidence.Pack/EvidenceCardService.cs
Normal file
401
src/__Libraries/StellaOps.Evidence.Pack/EvidenceCardService.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
137
src/__Libraries/StellaOps.Evidence.Pack/IEvidenceCardService.cs
Normal file
137
src/__Libraries/StellaOps.Evidence.Pack/IEvidenceCardService.cs
Normal 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>();
|
||||
}
|
||||
303
src/__Libraries/StellaOps.Evidence.Pack/Models/EvidenceCard.cs
Normal file
303
src/__Libraries/StellaOps.Evidence.Pack/Models/EvidenceCard.cs
Normal 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; }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user