sprints work

This commit is contained in:
master
2026-01-10 20:32:13 +02:00
parent 0d5eda86fc
commit 17d0631b8e
189 changed files with 40667 additions and 497 deletions

View File

@@ -0,0 +1,890 @@
// <copyright file="EvidencePackEndpoints.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Evidence.Pack;
using StellaOps.Evidence.Pack.Models;
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
/// <summary>
/// API endpoints for Evidence Packs.
/// Sprint: SPRINT_20260109_011_005 Task: EVPK-010
/// </summary>
public static class EvidencePackEndpoints
{
/// <summary>
/// Maps all Evidence Pack endpoints.
/// </summary>
public static void MapEvidencePackEndpoints(this WebApplication app)
{
// POST /v1/evidence-packs - Create Evidence Pack
app.MapPost("/v1/evidence-packs", HandleCreateEvidencePack)
.WithName("evidence-packs.create")
.WithTags("EvidencePacks")
.Produces<EvidencePackResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
// GET /v1/evidence-packs/{packId} - Get Evidence Pack
app.MapGet("/v1/evidence-packs/{packId}", HandleGetEvidencePack)
.WithName("evidence-packs.get")
.WithTags("EvidencePacks")
.Produces<EvidencePackResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
// POST /v1/evidence-packs/{packId}/sign - Sign Evidence Pack
app.MapPost("/v1/evidence-packs/{packId}/sign", HandleSignEvidencePack)
.WithName("evidence-packs.sign")
.WithTags("EvidencePacks")
.Produces<SignedEvidencePackResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
// POST /v1/evidence-packs/{packId}/verify - Verify Evidence Pack
app.MapPost("/v1/evidence-packs/{packId}/verify", HandleVerifyEvidencePack)
.WithName("evidence-packs.verify")
.WithTags("EvidencePacks")
.Produces<EvidencePackVerificationResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
// GET /v1/evidence-packs/{packId}/export - Export Evidence Pack
app.MapGet("/v1/evidence-packs/{packId}/export", HandleExportEvidencePack)
.WithName("evidence-packs.export")
.WithTags("EvidencePacks")
.Produces<byte[]>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
// GET /v1/runs/{runId}/evidence-packs - List Evidence Packs for Run
app.MapGet("/v1/runs/{runId}/evidence-packs", HandleListRunEvidencePacks)
.WithName("evidence-packs.list-by-run")
.WithTags("EvidencePacks")
.Produces<EvidencePackListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
// GET /v1/evidence-packs - List Evidence Packs
app.MapGet("/v1/evidence-packs", HandleListEvidencePacks)
.WithName("evidence-packs.list")
.WithTags("EvidencePacks")
.Produces<EvidencePackListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status401Unauthorized)
.RequireRateLimiting("advisory-ai");
}
private static async Task<IResult> HandleCreateEvidencePack(
CreateEvidencePackRequest request,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
if (request.Claims is null || request.Claims.Count == 0)
{
return Results.BadRequest(new { error = "At least one claim is required" });
}
if (request.Evidence is null || request.Evidence.Count == 0)
{
return Results.BadRequest(new { error = "At least one evidence item is required" });
}
var claims = request.Claims.Select(c => new EvidenceClaim
{
ClaimId = c.ClaimId ?? $"claim-{Guid.NewGuid():N}"[..16],
Text = c.Text,
Type = Enum.TryParse<ClaimType>(c.Type, true, out var ct) ? ct : ClaimType.Custom,
Status = c.Status,
Confidence = c.Confidence,
EvidenceIds = c.EvidenceIds?.ToImmutableArray() ?? [],
Source = c.Source
}).ToArray();
var evidence = request.Evidence.Select(e => new EvidenceItem
{
EvidenceId = e.EvidenceId ?? $"ev-{Guid.NewGuid():N}"[..12],
Type = Enum.TryParse<EvidenceType>(e.Type, true, out var et) ? et : EvidenceType.Custom,
Uri = e.Uri,
Digest = e.Digest ?? "sha256:unknown",
CollectedAt = e.CollectedAt ?? DateTimeOffset.UtcNow,
Snapshot = EvidenceSnapshot.Custom(e.SnapshotType ?? "custom", (e.SnapshotData ?? new Dictionary<string, object>()).ToImmutableDictionary(x => x.Key, x => (object?)x.Value))
}).ToArray();
var subject = new EvidenceSubject
{
Type = Enum.TryParse<EvidenceSubjectType>(request.Subject?.Type, true, out var st)
? st
: EvidenceSubjectType.Custom,
FindingId = request.Subject?.FindingId,
CveId = request.Subject?.CveId,
Component = request.Subject?.Component,
ImageDigest = request.Subject?.ImageDigest
};
var context = new EvidencePackContext
{
TenantId = tenantId,
RunId = request.RunId,
ConversationId = request.ConversationId,
UserId = GetUserId(httpContext),
GeneratedBy = "API"
};
var pack = await evidencePackService.CreateAsync(claims, evidence, subject, context, cancellationToken)
.ConfigureAwait(false);
var response = EvidencePackResponse.FromPack(pack);
return Results.Created($"/v1/evidence-packs/{pack.PackId}", response);
}
private static async Task<IResult> HandleGetEvidencePack(
string packId,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
var pack = await evidencePackService.GetAsync(tenantId, packId, cancellationToken)
.ConfigureAwait(false);
if (pack is null)
{
return Results.NotFound(new { error = "Evidence pack not found", packId });
}
return Results.Ok(EvidencePackResponse.FromPack(pack));
}
private static async Task<IResult> HandleSignEvidencePack(
string packId,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
var pack = await evidencePackService.GetAsync(tenantId, packId, cancellationToken)
.ConfigureAwait(false);
if (pack is null)
{
return Results.NotFound(new { error = "Evidence pack not found", packId });
}
var signedPack = await evidencePackService.SignAsync(pack, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(SignedEvidencePackResponse.FromSignedPack(signedPack));
}
private static async Task<IResult> HandleVerifyEvidencePack(
string packId,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
var pack = await evidencePackService.GetAsync(tenantId, packId, cancellationToken)
.ConfigureAwait(false);
if (pack is null)
{
return Results.NotFound(new { error = "Evidence pack not found", packId });
}
// Get signed version from store
var store = httpContext.RequestServices.GetService<IEvidencePackStore>();
var signedPack = store is not null
? await store.GetSignedByIdAsync(tenantId, packId, cancellationToken).ConfigureAwait(false)
: null;
if (signedPack is null)
{
return Results.BadRequest(new { error = "Pack is not signed", packId });
}
var result = await evidencePackService.VerifyAsync(signedPack, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new EvidencePackVerificationResponse
{
PackId = packId,
Valid = result.Valid,
PackDigest = result.PackDigest,
SignatureKeyId = result.SignatureKeyId,
Issues = result.Issues.ToList(),
EvidenceResolutions = result.EvidenceResolutions.Select(r => new EvidenceResolutionApiResponse
{
EvidenceId = r.EvidenceId,
Uri = r.Uri,
Resolved = r.Resolved,
DigestMatches = r.DigestMatches,
Error = r.Error
}).ToList()
});
}
private static async Task<IResult> HandleExportEvidencePack(
string packId,
string? format,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
var pack = await evidencePackService.GetAsync(tenantId, packId, cancellationToken)
.ConfigureAwait(false);
if (pack is null)
{
return Results.NotFound(new { error = "Evidence pack not found", packId });
}
var exportFormat = format?.ToLowerInvariant() switch
{
"markdown" or "md" => EvidencePackExportFormat.Markdown,
"html" => EvidencePackExportFormat.Html,
"pdf" => EvidencePackExportFormat.Pdf,
"signedjson" => EvidencePackExportFormat.SignedJson,
_ => EvidencePackExportFormat.Json
};
var export = await evidencePackService.ExportAsync(packId, exportFormat, cancellationToken)
.ConfigureAwait(false);
return Results.File(export.Content, export.ContentType, export.FileName);
}
private static async Task<IResult> HandleListRunEvidencePacks(
string runId,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
var store = httpContext.RequestServices.GetService<IEvidencePackStore>();
if (store is null)
{
return Results.Ok(new EvidencePackListResponse { Count = 0, Packs = [] });
}
var packs = await store.GetByRunIdAsync(runId, cancellationToken).ConfigureAwait(false);
// Filter by tenant
var filtered = packs.Where(p => p.TenantId == tenantId).ToList();
return Results.Ok(new EvidencePackListResponse
{
Count = filtered.Count,
Packs = filtered.Select(EvidencePackSummary.FromPack).ToList()
});
}
private static async Task<IResult> HandleListEvidencePacks(
string? cveId,
string? runId,
int? limit,
IEvidencePackService evidencePackService,
HttpContext httpContext,
CancellationToken cancellationToken)
{
var tenantId = GetTenantId(httpContext);
if (string.IsNullOrEmpty(tenantId))
{
return Results.Unauthorized();
}
var store = httpContext.RequestServices.GetService<IEvidencePackStore>();
if (store is null)
{
return Results.Ok(new EvidencePackListResponse { Count = 0, Packs = [] });
}
var query = new EvidencePackQuery
{
CveId = cveId,
RunId = runId,
Limit = Math.Min(limit ?? 50, 100)
};
var packs = await store.ListAsync(tenantId, query, cancellationToken).ConfigureAwait(false);
return Results.Ok(new EvidencePackListResponse
{
Count = packs.Count,
Packs = packs.Select(EvidencePackSummary.FromPack).ToList()
});
}
private static string? GetTenantId(HttpContext context)
{
if (context.Request.Headers.TryGetValue("X-StellaOps-Tenant", out var tenant))
{
return tenant.ToString();
}
var tenantClaim = context.User?.FindFirst("tenant_id")?.Value;
return tenantClaim;
}
private static string GetUserId(HttpContext context)
{
if (context.Request.Headers.TryGetValue("X-StellaOps-User", out var user))
{
return user.ToString();
}
return context.User?.FindFirst("sub")?.Value ?? "anonymous";
}
}
#region Request/Response Models
/// <summary>
/// Request to create an Evidence Pack.
/// </summary>
public sealed record CreateEvidencePackRequest
{
/// <summary>Subject of the evidence pack.</summary>
public EvidenceSubjectRequest? Subject { get; init; }
/// <summary>Claims in the pack.</summary>
public IReadOnlyList<EvidenceClaimRequest>? Claims { get; init; }
/// <summary>Evidence items.</summary>
public IReadOnlyList<EvidenceItemRequest>? Evidence { get; init; }
/// <summary>Optional Run ID to link to.</summary>
public string? RunId { get; init; }
/// <summary>Optional conversation ID.</summary>
public string? ConversationId { get; init; }
}
/// <summary>
/// Evidence subject in request.
/// </summary>
public sealed record EvidenceSubjectRequest
{
/// <summary>Subject type.</summary>
public string? Type { get; init; }
/// <summary>Finding ID if applicable.</summary>
public string? FindingId { get; init; }
/// <summary>CVE ID if applicable.</summary>
public string? CveId { get; init; }
/// <summary>Component if applicable.</summary>
public string? Component { get; init; }
/// <summary>Image digest if applicable.</summary>
public string? ImageDigest { get; init; }
}
/// <summary>
/// Evidence claim in request.
/// </summary>
public sealed record EvidenceClaimRequest
{
/// <summary>Optional claim ID (auto-generated if not provided).</summary>
public string? ClaimId { get; init; }
/// <summary>Claim text.</summary>
public required string Text { get; init; }
/// <summary>Claim type.</summary>
public required string Type { get; init; }
/// <summary>Status.</summary>
public required string Status { get; init; }
/// <summary>Confidence score 0-1.</summary>
public double Confidence { get; init; }
/// <summary>Evidence IDs supporting this claim.</summary>
public IReadOnlyList<string>? EvidenceIds { get; init; }
/// <summary>Source of the claim.</summary>
public string? Source { get; init; }
}
/// <summary>
/// Evidence item in request.
/// </summary>
public sealed record EvidenceItemRequest
{
/// <summary>Optional evidence ID (auto-generated if not provided).</summary>
public string? EvidenceId { get; init; }
/// <summary>Evidence type.</summary>
public required string Type { get; init; }
/// <summary>URI to the evidence.</summary>
public required string Uri { get; init; }
/// <summary>Content digest.</summary>
public string? Digest { get; init; }
/// <summary>When the evidence was collected.</summary>
public DateTimeOffset? CollectedAt { get; init; }
/// <summary>Snapshot type.</summary>
public string? SnapshotType { get; init; }
/// <summary>Snapshot data.</summary>
public Dictionary<string, object>? SnapshotData { get; init; }
}
/// <summary>
/// Evidence Pack response.
/// </summary>
public sealed record EvidencePackResponse
{
/// <summary>Pack ID.</summary>
public required string PackId { get; init; }
/// <summary>Version.</summary>
public required string Version { get; init; }
/// <summary>Tenant ID.</summary>
public required string TenantId { get; init; }
/// <summary>Created timestamp.</summary>
public required string CreatedAt { get; init; }
/// <summary>Content digest.</summary>
public required string ContentDigest { get; init; }
/// <summary>Subject.</summary>
public required EvidenceSubjectResponse Subject { get; init; }
/// <summary>Claims.</summary>
public required IReadOnlyList<EvidenceClaimResponse> Claims { get; init; }
/// <summary>Evidence items.</summary>
public required IReadOnlyList<EvidenceItemResponse> Evidence { get; init; }
/// <summary>Context.</summary>
public EvidencePackContextResponse? Context { get; init; }
/// <summary>Related links.</summary>
public EvidencePackLinks? Links { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static EvidencePackResponse FromPack(EvidencePack pack) => new()
{
PackId = pack.PackId,
Version = pack.Version,
TenantId = pack.TenantId,
CreatedAt = pack.CreatedAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
ContentDigest = pack.ComputeContentDigest(),
Subject = EvidenceSubjectResponse.FromSubject(pack.Subject),
Claims = pack.Claims.Select(EvidenceClaimResponse.FromClaim).ToList(),
Evidence = pack.Evidence.Select(EvidenceItemResponse.FromItem).ToList(),
Context = pack.Context is not null ? EvidencePackContextResponse.FromContext(pack.Context) : null,
Links = new EvidencePackLinks
{
Self = $"/v1/evidence-packs/{pack.PackId}",
Sign = $"/v1/evidence-packs/{pack.PackId}/sign",
Verify = $"/v1/evidence-packs/{pack.PackId}/verify",
Export = $"/v1/evidence-packs/{pack.PackId}/export"
}
};
}
/// <summary>
/// Evidence subject response.
/// </summary>
public sealed record EvidenceSubjectResponse
{
/// <summary>Subject type.</summary>
public required string Type { get; init; }
/// <summary>Finding ID.</summary>
public string? FindingId { get; init; }
/// <summary>CVE ID.</summary>
public string? CveId { get; init; }
/// <summary>Component.</summary>
public string? Component { get; init; }
/// <summary>Image digest.</summary>
public string? ImageDigest { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static EvidenceSubjectResponse FromSubject(EvidenceSubject subject) => new()
{
Type = subject.Type.ToString(),
FindingId = subject.FindingId,
CveId = subject.CveId,
Component = subject.Component,
ImageDigest = subject.ImageDigest
};
}
/// <summary>
/// Evidence claim response.
/// </summary>
public sealed record EvidenceClaimResponse
{
/// <summary>Claim ID.</summary>
public required string ClaimId { get; init; }
/// <summary>Claim text.</summary>
public required string Text { get; init; }
/// <summary>Claim type.</summary>
public required string Type { get; init; }
/// <summary>Status.</summary>
public required string Status { get; init; }
/// <summary>Confidence score.</summary>
public double Confidence { get; init; }
/// <summary>Evidence IDs.</summary>
public required IReadOnlyList<string> EvidenceIds { get; init; }
/// <summary>Source.</summary>
public string? Source { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static EvidenceClaimResponse FromClaim(EvidenceClaim claim) => new()
{
ClaimId = claim.ClaimId,
Text = claim.Text,
Type = claim.Type.ToString(),
Status = claim.Status,
Confidence = claim.Confidence,
EvidenceIds = claim.EvidenceIds.ToList(),
Source = claim.Source
};
}
/// <summary>
/// Evidence item response.
/// </summary>
public sealed record EvidenceItemResponse
{
/// <summary>Evidence ID.</summary>
public required string EvidenceId { get; init; }
/// <summary>Evidence type.</summary>
public required string Type { get; init; }
/// <summary>URI.</summary>
public required string Uri { get; init; }
/// <summary>Digest.</summary>
public required string Digest { get; init; }
/// <summary>Collection timestamp.</summary>
public required string CollectedAt { get; init; }
/// <summary>Snapshot.</summary>
public required EvidenceSnapshotResponse Snapshot { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static EvidenceItemResponse FromItem(EvidenceItem item) => new()
{
EvidenceId = item.EvidenceId,
Type = item.Type.ToString(),
Uri = item.Uri,
Digest = item.Digest,
CollectedAt = item.CollectedAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
Snapshot = EvidenceSnapshotResponse.FromSnapshot(item.Snapshot)
};
}
/// <summary>
/// Evidence snapshot response.
/// </summary>
public sealed record EvidenceSnapshotResponse
{
/// <summary>Snapshot type.</summary>
public required string Type { get; init; }
/// <summary>Snapshot data.</summary>
public required IReadOnlyDictionary<string, object?> Data { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static EvidenceSnapshotResponse FromSnapshot(EvidenceSnapshot snapshot) => new()
{
Type = snapshot.Type,
Data = snapshot.Data
};
}
/// <summary>
/// Evidence pack context response.
/// </summary>
public sealed record EvidencePackContextResponse
{
/// <summary>Tenant ID.</summary>
public string? TenantId { get; init; }
/// <summary>Run ID.</summary>
public string? RunId { get; init; }
/// <summary>Conversation ID.</summary>
public string? ConversationId { get; init; }
/// <summary>User ID.</summary>
public string? UserId { get; init; }
/// <summary>Generator.</summary>
public string? GeneratedBy { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static EvidencePackContextResponse FromContext(EvidencePackContext context) => new()
{
TenantId = context.TenantId,
RunId = context.RunId,
ConversationId = context.ConversationId,
UserId = context.UserId,
GeneratedBy = context.GeneratedBy
};
}
/// <summary>
/// Evidence pack links.
/// </summary>
public sealed record EvidencePackLinks
{
/// <summary>Self link.</summary>
public string? Self { get; init; }
/// <summary>Sign link.</summary>
public string? Sign { get; init; }
/// <summary>Verify link.</summary>
public string? Verify { get; init; }
/// <summary>Export link.</summary>
public string? Export { get; init; }
}
/// <summary>
/// Signed evidence pack response.
/// </summary>
public sealed record SignedEvidencePackResponse
{
/// <summary>Pack ID.</summary>
public required string PackId { get; init; }
/// <summary>Signed timestamp.</summary>
public required string SignedAt { get; init; }
/// <summary>Pack content.</summary>
public required EvidencePackResponse Pack { get; init; }
/// <summary>DSSE envelope.</summary>
public required DsseEnvelopeResponse Envelope { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static SignedEvidencePackResponse FromSignedPack(SignedEvidencePack signedPack) => new()
{
PackId = signedPack.Pack.PackId,
SignedAt = signedPack.SignedAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
Pack = EvidencePackResponse.FromPack(signedPack.Pack),
Envelope = DsseEnvelopeResponse.FromEnvelope(signedPack.Envelope)
};
}
/// <summary>
/// DSSE envelope response.
/// </summary>
public sealed record DsseEnvelopeResponse
{
/// <summary>Payload type.</summary>
public required string PayloadType { get; init; }
/// <summary>Payload digest.</summary>
public required string PayloadDigest { get; init; }
/// <summary>Signatures.</summary>
public required IReadOnlyList<DsseSignatureResponse> Signatures { get; init; }
/// <summary>
/// Creates response from domain model.
/// </summary>
public static DsseEnvelopeResponse FromEnvelope(DsseEnvelope envelope) => new()
{
PayloadType = envelope.PayloadType,
PayloadDigest = envelope.PayloadDigest,
Signatures = envelope.Signatures.Select(s => new DsseSignatureResponse
{
KeyId = s.KeyId,
Sig = s.Sig
}).ToList()
};
}
/// <summary>
/// DSSE signature response.
/// </summary>
public sealed record DsseSignatureResponse
{
/// <summary>Key ID.</summary>
public required string KeyId { get; init; }
/// <summary>Signature.</summary>
public required string Sig { get; init; }
}
/// <summary>
/// Evidence pack verification response.
/// </summary>
public sealed record EvidencePackVerificationResponse
{
/// <summary>Pack ID.</summary>
public required string PackId { get; init; }
/// <summary>Whether verification passed.</summary>
public bool Valid { get; init; }
/// <summary>Pack digest.</summary>
public string? PackDigest { get; init; }
/// <summary>Signing key ID.</summary>
public string? SignatureKeyId { get; init; }
/// <summary>Issues found.</summary>
public IReadOnlyList<string>? Issues { get; init; }
/// <summary>Evidence resolution results.</summary>
public IReadOnlyList<EvidenceResolutionApiResponse>? EvidenceResolutions { get; init; }
}
/// <summary>
/// Evidence resolution result in API response.
/// </summary>
public sealed record EvidenceResolutionApiResponse
{
/// <summary>Evidence ID.</summary>
public required string EvidenceId { get; init; }
/// <summary>URI.</summary>
public required string Uri { get; init; }
/// <summary>Whether resolved.</summary>
public bool Resolved { get; init; }
/// <summary>Whether digest matches.</summary>
public bool DigestMatches { get; init; }
/// <summary>Error message.</summary>
public string? Error { get; init; }
}
/// <summary>
/// Evidence pack list response.
/// </summary>
public sealed record EvidencePackListResponse
{
/// <summary>Total count.</summary>
public int Count { get; init; }
/// <summary>Pack summaries.</summary>
public required IReadOnlyList<EvidencePackSummary> Packs { get; init; }
}
/// <summary>
/// Evidence pack summary.
/// </summary>
public sealed record EvidencePackSummary
{
/// <summary>Pack ID.</summary>
public required string PackId { get; init; }
/// <summary>Tenant ID.</summary>
public required string TenantId { get; init; }
/// <summary>Created timestamp.</summary>
public required string CreatedAt { get; init; }
/// <summary>Subject type.</summary>
public required string SubjectType { get; init; }
/// <summary>CVE ID if applicable.</summary>
public string? CveId { get; init; }
/// <summary>Number of claims.</summary>
public int ClaimCount { get; init; }
/// <summary>Number of evidence items.</summary>
public int EvidenceCount { get; init; }
/// <summary>
/// Creates summary from domain model.
/// </summary>
public static EvidencePackSummary FromPack(EvidencePack pack) => new()
{
PackId = pack.PackId,
TenantId = pack.TenantId,
CreatedAt = pack.CreatedAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
SubjectType = pack.Subject.Type.ToString(),
CveId = pack.Subject.CveId,
ClaimCount = pack.Claims.Length,
EvidenceCount = pack.Evidence.Length
};
}
#endregion