930 lines
32 KiB
C#
930 lines
32 KiB
C#
// <copyright file="EvidencePackEndpoints.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// </copyright>
|
|
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.AdvisoryAI.WebService.Security;
|
|
using StellaOps.Auth.ServerIntegration.Tenancy;
|
|
using StellaOps.Determinism;
|
|
using StellaOps.Evidence.Pack;
|
|
using StellaOps.Evidence.Pack.Models;
|
|
using System.Collections.Immutable;
|
|
using static StellaOps.Localization.T;
|
|
|
|
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")
|
|
.WithSummary("Create an evidence pack")
|
|
.WithDescription("Creates a new evidence pack containing AI-generated claims and supporting evidence items for a vulnerability subject. Claims are linked to evidence items by ID. The pack is assigned a content digest for tamper detection and can subsequently be signed via the sign endpoint.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<EvidencePackResponse>(StatusCodes.Status201Created)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
|
|
// GET /v1/evidence-packs/{packId} - Get Evidence Pack
|
|
app.MapGet("/v1/evidence-packs/{packId}", HandleGetEvidencePack)
|
|
.WithName("evidence-packs.get")
|
|
.WithSummary("Get an evidence pack by ID")
|
|
.WithDescription("Returns the full evidence pack record including all claims, evidence items, subject, context, and related links (sign, verify, export). Access is tenant-scoped; packs from other tenants return 404.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<EvidencePackResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
|
|
// POST /v1/evidence-packs/{packId}/sign - Sign Evidence Pack
|
|
app.MapPost("/v1/evidence-packs/{packId}/sign", HandleSignEvidencePack)
|
|
.WithName("evidence-packs.sign")
|
|
.WithSummary("Sign an evidence pack")
|
|
.WithDescription("Signs the specified evidence pack using DSSE (Dead Simple Signing Envelope), producing a cryptographic attestation over the pack's content digest. The resulting signed pack and DSSE envelope are returned and stored for subsequent verification.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<SignedEvidencePackResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.OperatePolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
|
|
// POST /v1/evidence-packs/{packId}/verify - Verify Evidence Pack
|
|
app.MapPost("/v1/evidence-packs/{packId}/verify", HandleVerifyEvidencePack)
|
|
.WithName("evidence-packs.verify")
|
|
.WithSummary("Verify an evidence pack's signature and integrity")
|
|
.WithDescription("Verifies the cryptographic signature and content digest of a signed evidence pack. Returns per-evidence URI resolution results, digest match status, and signing key ID. Returns 400 if the pack has not been signed or verification fails.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<EvidencePackVerificationResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
|
|
// GET /v1/evidence-packs/{packId}/export - Export Evidence Pack
|
|
app.MapGet("/v1/evidence-packs/{packId}/export", HandleExportEvidencePack)
|
|
.WithName("evidence-packs.export")
|
|
.WithSummary("Export an evidence pack in a specified format")
|
|
.WithDescription("Exports an evidence pack in the requested format. Supported formats: json (default), markdown, html, pdf, signedjson, evidencecard, and evidencecardcompact. The format query parameter controls the output; the appropriate Content-Type and filename are set in the response.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<byte[]>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
|
|
// 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")
|
|
.WithSummary("List evidence packs for a run")
|
|
.WithDescription("Returns all evidence packs associated with a specific AI investigation run, filtered to the current tenant. Includes pack summaries with claim count, evidence count, subject type, and CVE ID.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<EvidencePackListResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
|
|
// GET /v1/evidence-packs - List Evidence Packs
|
|
app.MapGet("/v1/evidence-packs", HandleListEvidencePacks)
|
|
.WithName("evidence-packs.list")
|
|
.WithSummary("List evidence packs")
|
|
.WithDescription("Returns a paginated list of evidence packs for the current tenant, optionally filtered by CVE ID or run ID. Supports limit up to 100. Results include pack summaries with subject type, claim count, and evidence count.")
|
|
.WithTags("EvidencePacks")
|
|
.Produces<EvidencePackListResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status401Unauthorized)
|
|
.RequireAuthorization(AdvisoryAIPolicies.ViewPolicy)
|
|
.RequireRateLimiting("advisory-ai")
|
|
.RequireTenant();
|
|
}
|
|
|
|
private static async Task<IResult> HandleCreateEvidencePack(
|
|
CreateEvidencePackRequest request,
|
|
IEvidencePackService evidencePackService,
|
|
TimeProvider timeProvider,
|
|
IGuidProvider guidProvider,
|
|
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 = _t("advisoryai.validation.claims_required") });
|
|
}
|
|
|
|
if (request.Evidence is null || request.Evidence.Count == 0)
|
|
{
|
|
return Results.BadRequest(new { error = _t("advisoryai.validation.evidence_items_required") });
|
|
}
|
|
|
|
var claims = request.Claims.Select(c => new EvidenceClaim
|
|
{
|
|
ClaimId = c.ClaimId ?? $"claim-{guidProvider.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-{guidProvider.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 ?? timeProvider.GetUtcNow(),
|
|
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 = _t("advisoryai.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 = _t("advisoryai.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 = _t("advisoryai.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 = _t("advisoryai.error.pack_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 = _t("advisoryai.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,
|
|
// Sprint: SPRINT_20260112_005_BE_evidence_card_api (EVPCARD-BE-001)
|
|
"evidencecard" or "evidence-card" or "card" => EvidencePackExportFormat.EvidenceCard,
|
|
"evidencecardcompact" or "card-compact" => EvidencePackExportFormat.EvidenceCardCompact,
|
|
_ => 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-Actor", out var actor)
|
|
&& !string.IsNullOrWhiteSpace(actor.ToString()))
|
|
{
|
|
return actor.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
|