653 lines
22 KiB
C#
653 lines
22 KiB
C#
// <copyright file="EvidenceThreadEndpoints.cs" company="StellaOps">
|
|
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
|
// </copyright>
|
|
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Platform.WebService.Services;
|
|
using StellaOps.ReleaseOrchestrator.EvidenceThread.Export;
|
|
using StellaOps.ReleaseOrchestrator.EvidenceThread.Models;
|
|
using StellaOps.ReleaseOrchestrator.EvidenceThread.Services;
|
|
using StellaOps.ReleaseOrchestrator.EvidenceThread.Transcript;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace StellaOps.Platform.WebService.Endpoints;
|
|
|
|
/// <summary>
|
|
/// REST endpoints for Evidence Thread operations.
|
|
/// </summary>
|
|
public static class EvidenceThreadEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps Evidence Thread API endpoints.
|
|
/// </summary>
|
|
public static IEndpointRouteBuilder MapEvidenceThreadEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var evidence = app.MapGroup("/api/v1/evidence")
|
|
.WithTags("Evidence Thread");
|
|
|
|
// GET /api/v1/evidence/{artifactDigest} - Get evidence thread for artifact
|
|
evidence.MapGet("/{artifactDigest}", GetEvidenceThread)
|
|
.WithName("GetEvidenceThread")
|
|
.WithSummary("Get evidence thread for an artifact")
|
|
.WithDescription("Retrieves the full evidence thread graph for an artifact by its digest.")
|
|
.Produces<EvidenceThreadResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
// POST /api/v1/evidence/{artifactDigest}/export - Export thread as DSSE bundle
|
|
evidence.MapPost("/{artifactDigest}/export", ExportEvidenceThread)
|
|
.WithName("ExportEvidenceThread")
|
|
.WithSummary("Export evidence thread as DSSE bundle")
|
|
.WithDescription("Exports the evidence thread as a signed DSSE envelope for offline verification.")
|
|
.Produces<EvidenceExportResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
// POST /api/v1/evidence/{artifactDigest}/transcript - Generate transcript
|
|
evidence.MapPost("/{artifactDigest}/transcript", GenerateTranscript)
|
|
.WithName("GenerateEvidenceTranscript")
|
|
.WithSummary("Generate natural language transcript")
|
|
.WithDescription("Generates a natural language transcript explaining the evidence thread.")
|
|
.Produces<EvidenceTranscriptResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
// GET /api/v1/evidence/{artifactDigest}/nodes - Get evidence nodes
|
|
evidence.MapGet("/{artifactDigest}/nodes", GetEvidenceNodes)
|
|
.WithName("GetEvidenceNodes")
|
|
.WithSummary("Get evidence nodes for an artifact")
|
|
.WithDescription("Retrieves all evidence nodes in the thread.")
|
|
.Produces<EvidenceNodeListResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
// GET /api/v1/evidence/{artifactDigest}/links - Get evidence links
|
|
evidence.MapGet("/{artifactDigest}/links", GetEvidenceLinks)
|
|
.WithName("GetEvidenceLinks")
|
|
.WithSummary("Get evidence links for an artifact")
|
|
.WithDescription("Retrieves all evidence links in the thread.")
|
|
.Produces<EvidenceLinkListResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
// POST /api/v1/evidence/{artifactDigest}/collect - Trigger evidence collection
|
|
evidence.MapPost("/{artifactDigest}/collect", CollectEvidence)
|
|
.WithName("CollectEvidence")
|
|
.WithSummary("Collect evidence for an artifact")
|
|
.WithDescription("Triggers collection of all available evidence for an artifact.")
|
|
.Produces<EvidenceCollectionResponse>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest);
|
|
|
|
return app;
|
|
}
|
|
|
|
private static async Task<IResult> GetEvidenceThread(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IEvidenceThreadService service,
|
|
string artifactDigest,
|
|
[FromQuery] bool? includeContent,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(artifactDigest))
|
|
{
|
|
return Results.BadRequest(new { error = "artifact_digest_required" });
|
|
}
|
|
|
|
var options = new EvidenceThreadOptions
|
|
{
|
|
IncludeContent = includeContent ?? true
|
|
};
|
|
|
|
var graph = await service.GetThreadGraphAsync(
|
|
requestContext!.TenantId,
|
|
artifactDigest,
|
|
options,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (graph is null)
|
|
{
|
|
return Results.NotFound(new { error = "thread_not_found", artifactDigest });
|
|
}
|
|
|
|
return Results.Ok(new EvidenceThreadResponse
|
|
{
|
|
ThreadId = graph.Thread.Id,
|
|
TenantId = graph.Thread.TenantId,
|
|
ArtifactDigest = graph.Thread.ArtifactDigest,
|
|
ArtifactName = graph.Thread.ArtifactName,
|
|
Status = graph.Thread.Status.ToString(),
|
|
Verdict = graph.Thread.Verdict?.ToString(),
|
|
RiskScore = graph.Thread.RiskScore,
|
|
ReachabilityMode = graph.Thread.ReachabilityMode?.ToString(),
|
|
NodeCount = graph.Nodes.Length,
|
|
LinkCount = graph.Links.Length,
|
|
CreatedAt = graph.Thread.CreatedAt,
|
|
UpdatedAt = graph.Thread.UpdatedAt
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> ExportEvidenceThread(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IEvidenceThreadService threadService,
|
|
IDsseThreadExporter exporter,
|
|
string artifactDigest,
|
|
[FromBody] EvidenceExportRequest? request,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(artifactDigest))
|
|
{
|
|
return Results.BadRequest(new { error = "artifact_digest_required" });
|
|
}
|
|
|
|
var graph = await threadService.GetThreadGraphAsync(
|
|
requestContext!.TenantId,
|
|
artifactDigest,
|
|
null,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (graph is null)
|
|
{
|
|
return Results.NotFound(new { error = "thread_not_found", artifactDigest });
|
|
}
|
|
|
|
var options = new ThreadExportOptions
|
|
{
|
|
Format = ParseExportFormat(request?.Format),
|
|
IncludeTranscript = request?.IncludeTranscript ?? true,
|
|
Sign = request?.Sign ?? true,
|
|
SigningKeyId = request?.SigningKeyId
|
|
};
|
|
|
|
var result = await exporter.ExportAsync(graph, options, ct).ConfigureAwait(false);
|
|
|
|
if (!result.Success)
|
|
{
|
|
return Results.BadRequest(new
|
|
{
|
|
error = result.ErrorCode,
|
|
message = result.ErrorMessage
|
|
});
|
|
}
|
|
|
|
return Results.Ok(new EvidenceExportResponse
|
|
{
|
|
ThreadId = result.ThreadId,
|
|
Format = result.Format.ToString(),
|
|
ContentDigest = result.ContentDigest,
|
|
SizeBytes = result.SizeBytes,
|
|
SigningKeyId = result.SigningKeyId,
|
|
ExportedAt = result.ExportedAt,
|
|
DurationMs = (long)result.Duration.TotalMilliseconds,
|
|
Envelope = result.Envelope is not null
|
|
? new DsseEnvelopeResponse
|
|
{
|
|
PayloadType = result.Envelope.PayloadType,
|
|
Payload = result.Envelope.Payload,
|
|
PayloadDigest = result.Envelope.PayloadDigest,
|
|
Signatures = result.Envelope.Signatures.Select(s => new DsseSignatureResponse
|
|
{
|
|
KeyId = s.KeyId,
|
|
Sig = s.Sig,
|
|
Algorithm = s.Algorithm
|
|
}).ToList()
|
|
}
|
|
: null
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> GenerateTranscript(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IEvidenceThreadService threadService,
|
|
ITranscriptGenerator transcriptGenerator,
|
|
string artifactDigest,
|
|
[FromBody] EvidenceTranscriptRequest? request,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(artifactDigest))
|
|
{
|
|
return Results.BadRequest(new { error = "artifact_digest_required" });
|
|
}
|
|
|
|
var graph = await threadService.GetThreadGraphAsync(
|
|
requestContext!.TenantId,
|
|
artifactDigest,
|
|
null,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (graph is null)
|
|
{
|
|
return Results.NotFound(new { error = "thread_not_found", artifactDigest });
|
|
}
|
|
|
|
var options = new TranscriptOptions
|
|
{
|
|
Type = ParseTranscriptType(request?.Type),
|
|
IncludeLlmRationale = request?.IncludeLlmRationale ?? true,
|
|
RationalePromptHint = request?.RationalePromptHint,
|
|
MaxLength = request?.MaxLength
|
|
};
|
|
|
|
var transcript = await transcriptGenerator.GenerateTranscriptAsync(graph, options, ct).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new EvidenceTranscriptResponse
|
|
{
|
|
TranscriptId = transcript.Id,
|
|
ThreadId = transcript.ThreadId,
|
|
Type = transcript.TranscriptType.ToString(),
|
|
TemplateVersion = transcript.TemplateVersion,
|
|
LlmModel = transcript.LlmModel,
|
|
Content = transcript.Content,
|
|
AnchorCount = transcript.Anchors.Length,
|
|
GeneratedAt = transcript.GeneratedAt
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> GetEvidenceNodes(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IEvidenceThreadService service,
|
|
string artifactDigest,
|
|
[FromQuery] string? kind,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(artifactDigest))
|
|
{
|
|
return Results.BadRequest(new { error = "artifact_digest_required" });
|
|
}
|
|
|
|
var filterKinds = string.IsNullOrWhiteSpace(kind)
|
|
? null
|
|
: kind.Split(',')
|
|
.Select(k => Enum.TryParse<EvidenceNodeKind>(k.Trim(), true, out var nk) ? nk : (EvidenceNodeKind?)null)
|
|
.Where(k => k.HasValue)
|
|
.Select(k => k!.Value)
|
|
.ToList();
|
|
|
|
var options = new EvidenceThreadOptions
|
|
{
|
|
IncludeContent = true,
|
|
NodeKinds = filterKinds
|
|
};
|
|
|
|
var graph = await service.GetThreadGraphAsync(
|
|
requestContext!.TenantId,
|
|
artifactDigest,
|
|
options,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (graph is null)
|
|
{
|
|
return Results.NotFound(new { error = "thread_not_found", artifactDigest });
|
|
}
|
|
|
|
var nodes = graph.Nodes.Select(n => new EvidenceNodeResponse
|
|
{
|
|
Id = n.Id,
|
|
Kind = n.Kind.ToString(),
|
|
RefId = n.RefId,
|
|
RefDigest = n.RefDigest,
|
|
Title = n.Title,
|
|
Summary = n.Summary,
|
|
Confidence = n.Confidence,
|
|
AnchorCount = n.Anchors.Length,
|
|
CreatedAt = n.CreatedAt
|
|
}).ToList();
|
|
|
|
return Results.Ok(new EvidenceNodeListResponse
|
|
{
|
|
ThreadId = graph.Thread.Id,
|
|
ArtifactDigest = artifactDigest,
|
|
Nodes = nodes,
|
|
TotalCount = nodes.Count
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> GetEvidenceLinks(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IEvidenceThreadService service,
|
|
string artifactDigest,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(artifactDigest))
|
|
{
|
|
return Results.BadRequest(new { error = "artifact_digest_required" });
|
|
}
|
|
|
|
var graph = await service.GetThreadGraphAsync(
|
|
requestContext!.TenantId,
|
|
artifactDigest,
|
|
null,
|
|
ct).ConfigureAwait(false);
|
|
|
|
if (graph is null)
|
|
{
|
|
return Results.NotFound(new { error = "thread_not_found", artifactDigest });
|
|
}
|
|
|
|
var links = graph.Links.Select(l => new EvidenceLinkResponse
|
|
{
|
|
Id = l.Id,
|
|
SrcNodeId = l.SrcNodeId,
|
|
DstNodeId = l.DstNodeId,
|
|
Relation = l.Relation.ToString(),
|
|
Weight = l.Weight,
|
|
CreatedAt = l.CreatedAt
|
|
}).ToList();
|
|
|
|
return Results.Ok(new EvidenceLinkListResponse
|
|
{
|
|
ThreadId = graph.Thread.Id,
|
|
ArtifactDigest = artifactDigest,
|
|
Links = links,
|
|
TotalCount = links.Count
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> CollectEvidence(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IEvidenceThreadService threadService,
|
|
IEvidenceNodeCollector collector,
|
|
string artifactDigest,
|
|
[FromBody] EvidenceCollectionRequest? request,
|
|
CancellationToken ct)
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(artifactDigest))
|
|
{
|
|
return Results.BadRequest(new { error = "artifact_digest_required" });
|
|
}
|
|
|
|
// Get or create the thread
|
|
var thread = await threadService.GetOrCreateThreadAsync(
|
|
requestContext!.TenantId,
|
|
artifactDigest,
|
|
artifactName: null,
|
|
ct).ConfigureAwait(false);
|
|
|
|
var options = new EvidenceCollectionOptions
|
|
{
|
|
BaseArtifactDigest = request?.BaseArtifactDigest,
|
|
CollectSbomDiff = request?.CollectSbomDiff ?? true,
|
|
CollectReachability = request?.CollectReachability ?? true,
|
|
CollectVex = request?.CollectVex ?? true,
|
|
CollectAttestations = request?.CollectAttestations ?? true
|
|
};
|
|
|
|
var result = await collector.CollectAllAsync(thread.Id, artifactDigest, options, ct).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new EvidenceCollectionResponse
|
|
{
|
|
ThreadId = thread.Id,
|
|
ArtifactDigest = artifactDigest,
|
|
NodesCollected = result.Nodes.Count,
|
|
LinksCreated = result.Links.Count,
|
|
ErrorCount = result.Errors.Count,
|
|
Errors = result.Errors.Select(e => new EvidenceCollectionErrorResponse
|
|
{
|
|
Source = e.Source,
|
|
Message = e.Message,
|
|
ExceptionType = e.ExceptionType
|
|
}).ToList(),
|
|
DurationMs = result.DurationMs
|
|
});
|
|
}
|
|
|
|
private static ThreadExportFormat ParseExportFormat(string? format) => format?.ToLowerInvariant() switch
|
|
{
|
|
"dsse" => ThreadExportFormat.Dsse,
|
|
"json" => ThreadExportFormat.Json,
|
|
"markdown" => ThreadExportFormat.Markdown,
|
|
"pdf" => ThreadExportFormat.Pdf,
|
|
_ => ThreadExportFormat.Dsse
|
|
};
|
|
|
|
private static TranscriptType ParseTranscriptType(string? type) => type?.ToLowerInvariant() switch
|
|
{
|
|
"summary" => TranscriptType.Summary,
|
|
"detailed" => TranscriptType.Detailed,
|
|
"audit" => TranscriptType.Audit,
|
|
_ => TranscriptType.Detailed
|
|
};
|
|
|
|
private static bool TryResolveContext(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
out PlatformRequestContext? requestContext,
|
|
out IResult? failure)
|
|
{
|
|
if (resolver.TryResolve(context, out requestContext, out var error))
|
|
{
|
|
failure = null;
|
|
return true;
|
|
}
|
|
|
|
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#region Request/Response DTOs
|
|
|
|
/// <summary>
|
|
/// Response for evidence thread query.
|
|
/// </summary>
|
|
public sealed record EvidenceThreadResponse
|
|
{
|
|
public Guid ThreadId { get; init; }
|
|
public string? TenantId { get; init; }
|
|
public string? ArtifactDigest { get; init; }
|
|
public string? ArtifactName { get; init; }
|
|
public string? Status { get; init; }
|
|
public string? Verdict { get; init; }
|
|
public decimal? RiskScore { get; init; }
|
|
public string? ReachabilityMode { get; init; }
|
|
public int NodeCount { get; init; }
|
|
public int LinkCount { get; init; }
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
public DateTimeOffset UpdatedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for evidence export.
|
|
/// </summary>
|
|
public sealed record EvidenceExportRequest
|
|
{
|
|
public string? Format { get; init; }
|
|
public bool? IncludeTranscript { get; init; }
|
|
public bool? Sign { get; init; }
|
|
public string? SigningKeyId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for evidence export.
|
|
/// </summary>
|
|
public sealed record EvidenceExportResponse
|
|
{
|
|
public Guid ThreadId { get; init; }
|
|
public string? Format { get; init; }
|
|
public string? ContentDigest { get; init; }
|
|
public long SizeBytes { get; init; }
|
|
public string? SigningKeyId { get; init; }
|
|
public DateTimeOffset ExportedAt { get; init; }
|
|
public long DurationMs { get; init; }
|
|
public DsseEnvelopeResponse? Envelope { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DSSE envelope response DTO.
|
|
/// </summary>
|
|
public sealed record DsseEnvelopeResponse
|
|
{
|
|
public string? PayloadType { get; init; }
|
|
public string? Payload { get; init; }
|
|
public string? PayloadDigest { get; init; }
|
|
public List<DsseSignatureResponse>? Signatures { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DSSE signature response DTO.
|
|
/// </summary>
|
|
public sealed record DsseSignatureResponse
|
|
{
|
|
public string? KeyId { get; init; }
|
|
public string? Sig { get; init; }
|
|
public string? Algorithm { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for transcript generation.
|
|
/// </summary>
|
|
public sealed record EvidenceTranscriptRequest
|
|
{
|
|
public string? Type { get; init; }
|
|
public bool? IncludeLlmRationale { get; init; }
|
|
public string? RationalePromptHint { get; init; }
|
|
public int? MaxLength { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for transcript generation.
|
|
/// </summary>
|
|
public sealed record EvidenceTranscriptResponse
|
|
{
|
|
public Guid TranscriptId { get; init; }
|
|
public Guid ThreadId { get; init; }
|
|
public string? Type { get; init; }
|
|
public string? TemplateVersion { get; init; }
|
|
public string? LlmModel { get; init; }
|
|
public string? Content { get; init; }
|
|
public int AnchorCount { get; init; }
|
|
public DateTimeOffset GeneratedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for evidence node query.
|
|
/// </summary>
|
|
public sealed record EvidenceNodeResponse
|
|
{
|
|
public Guid Id { get; init; }
|
|
public string? Kind { get; init; }
|
|
public string? RefId { get; init; }
|
|
public string? RefDigest { get; init; }
|
|
public string? Title { get; init; }
|
|
public string? Summary { get; init; }
|
|
public decimal? Confidence { get; init; }
|
|
public int AnchorCount { get; init; }
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for evidence node list.
|
|
/// </summary>
|
|
public sealed record EvidenceNodeListResponse
|
|
{
|
|
public Guid ThreadId { get; init; }
|
|
public string? ArtifactDigest { get; init; }
|
|
public List<EvidenceNodeResponse>? Nodes { get; init; }
|
|
public int TotalCount { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for evidence link query.
|
|
/// </summary>
|
|
public sealed record EvidenceLinkResponse
|
|
{
|
|
public Guid Id { get; init; }
|
|
public Guid SrcNodeId { get; init; }
|
|
public Guid DstNodeId { get; init; }
|
|
public string? Relation { get; init; }
|
|
public decimal? Weight { get; init; }
|
|
public DateTimeOffset CreatedAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for evidence link list.
|
|
/// </summary>
|
|
public sealed record EvidenceLinkListResponse
|
|
{
|
|
public Guid ThreadId { get; init; }
|
|
public string? ArtifactDigest { get; init; }
|
|
public List<EvidenceLinkResponse>? Links { get; init; }
|
|
public int TotalCount { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for evidence collection.
|
|
/// </summary>
|
|
public sealed record EvidenceCollectionRequest
|
|
{
|
|
public string? BaseArtifactDigest { get; init; }
|
|
public bool? CollectSbomDiff { get; init; }
|
|
public bool? CollectReachability { get; init; }
|
|
public bool? CollectVex { get; init; }
|
|
public bool? CollectAttestations { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response for evidence collection.
|
|
/// </summary>
|
|
public sealed record EvidenceCollectionResponse
|
|
{
|
|
public Guid ThreadId { get; init; }
|
|
public string? ArtifactDigest { get; init; }
|
|
public int NodesCollected { get; init; }
|
|
public int LinksCreated { get; init; }
|
|
public int ErrorCount { get; init; }
|
|
public List<EvidenceCollectionErrorResponse>? Errors { get; init; }
|
|
public long DurationMs { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Error response for evidence collection.
|
|
/// </summary>
|
|
public sealed record EvidenceCollectionErrorResponse
|
|
{
|
|
public string? Source { get; init; }
|
|
public string? Message { get; init; }
|
|
public string? ExceptionType { get; init; }
|
|
}
|
|
|
|
#endregion
|