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

@@ -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>