sprints work
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user