Files
git.stella-ops.org/src/Findings/StellaOps.Findings.Ledger.WebService/Contracts/SnapshotContracts.cs
2026-02-01 21:37:40 +02:00

462 lines
14 KiB
C#

using StellaOps.Findings.Ledger.Domain;
using StellaOps.Findings.Ledger.Infrastructure.Snapshot;
namespace StellaOps.Findings.Ledger.WebService.Contracts;
// === Snapshot Contracts ===
/// <summary>
/// Request to create a snapshot.
/// </summary>
public sealed record CreateSnapshotRequest(
string? Label = null,
string? Description = null,
DateTimeOffset? AtTimestamp = null,
long? AtSequence = null,
int? ExpiresInHours = null,
IReadOnlyList<string>? IncludeEntityTypes = null,
bool Sign = false,
Dictionary<string, object>? Metadata = null)
{
public CreateSnapshotInput ToInput(string tenantId) => new(
TenantId: tenantId,
Label: Label,
Description: Description,
AtTimestamp: AtTimestamp,
AtSequence: AtSequence,
ExpiresIn: ExpiresInHours.HasValue ? TimeSpan.FromHours(ExpiresInHours.Value) : null,
IncludeEntityTypes: IncludeEntityTypes?.Select(ParseEntityType).ToList(),
Sign: Sign,
Metadata: Metadata);
private static EntityType ParseEntityType(string s) =>
Enum.TryParse<EntityType>(s, true, out var et) ? et : EntityType.Finding;
}
/// <summary>
/// Response for a snapshot.
/// </summary>
public sealed record SnapshotResponse(
Guid SnapshotId,
string? Label,
string? Description,
string Status,
DateTimeOffset CreatedAt,
DateTimeOffset? ExpiresAt,
long SequenceNumber,
DateTimeOffset Timestamp,
SnapshotStatisticsResponse Statistics,
string? MerkleRoot,
string? DsseDigest,
Dictionary<string, object>? Metadata);
/// <summary>
/// Response for snapshot statistics.
/// </summary>
public sealed record SnapshotStatisticsResponse(
long FindingsCount,
long VexStatementsCount,
long AdvisoriesCount,
long SbomsCount,
long EventsCount,
long SizeBytes);
/// <summary>
/// Result of creating a snapshot.
/// </summary>
public sealed record CreateSnapshotResponse(
bool Success,
SnapshotResponse? Snapshot,
string? Error);
/// <summary>
/// Response for listing snapshots.
/// </summary>
public sealed record SnapshotListResponse(
IReadOnlyList<SnapshotResponse> Snapshots,
string? NextPageToken);
// === Time-Travel Contracts ===
/// <summary>
/// Request for historical query.
/// </summary>
public sealed record HistoricalQueryApiRequest(
DateTimeOffset? AtTimestamp = null,
long? AtSequence = null,
Guid? SnapshotId = null,
string? Status = null,
decimal? SeverityMin = null,
decimal? SeverityMax = null,
string? PolicyVersion = null,
string? ArtifactId = null,
string? VulnId = null,
int PageSize = 500,
string? PageToken = null)
{
public HistoricalQueryRequest ToRequest(string tenantId, EntityType entityType) => new(
TenantId: tenantId,
AtTimestamp: AtTimestamp,
AtSequence: AtSequence,
SnapshotId: SnapshotId,
EntityType: entityType,
Filters: new TimeQueryFilters(
Status: Status,
SeverityMin: SeverityMin,
SeverityMax: SeverityMax,
PolicyVersion: PolicyVersion,
ArtifactId: ArtifactId,
VulnId: VulnId),
PageSize: PageSize,
PageToken: PageToken);
}
/// <summary>
/// Response for historical query.
/// </summary>
public sealed record HistoricalQueryApiResponse<T>(
QueryPointResponse QueryPoint,
string EntityType,
IReadOnlyList<T> Items,
string? NextPageToken,
long TotalCount);
/// <summary>
/// Query point response.
/// </summary>
public sealed record QueryPointResponse(
DateTimeOffset Timestamp,
long SequenceNumber,
Guid? SnapshotId);
/// <summary>
/// Finding history item response.
/// </summary>
public sealed record FindingHistoryResponse(
string FindingId,
string ArtifactId,
string VulnId,
string Status,
decimal? Severity,
string? PolicyVersion,
DateTimeOffset FirstSeen,
DateTimeOffset LastUpdated,
Dictionary<string, string>? Labels);
/// <summary>
/// VEX history item response.
/// </summary>
public sealed record VexHistoryResponse(
string StatementId,
string VulnId,
string ProductId,
string Status,
string? Justification,
DateTimeOffset IssuedAt,
DateTimeOffset? ExpiresAt);
/// <summary>
/// Advisory history item response.
/// </summary>
public sealed record AdvisoryHistoryResponse(
string AdvisoryId,
string Source,
string Title,
decimal? CvssScore,
DateTimeOffset PublishedAt,
DateTimeOffset? ModifiedAt);
// === Replay Contracts ===
/// <summary>
/// Request for replaying events.
/// </summary>
public sealed record ReplayApiRequest(
long? FromSequence = null,
long? ToSequence = null,
DateTimeOffset? FromTimestamp = null,
DateTimeOffset? ToTimestamp = null,
IReadOnlyList<Guid>? ChainIds = null,
IReadOnlyList<string>? EventTypes = null,
bool IncludePayload = true,
int PageSize = 1000)
{
public ReplayRequest ToRequest(string tenantId) => new(
TenantId: tenantId,
FromSequence: FromSequence,
ToSequence: ToSequence,
FromTimestamp: FromTimestamp,
ToTimestamp: ToTimestamp,
ChainIds: ChainIds,
EventTypes: EventTypes,
IncludePayload: IncludePayload,
PageSize: PageSize);
}
/// <summary>
/// Response for replay.
/// </summary>
public sealed record ReplayApiResponse(
IReadOnlyList<ReplayEventResponse> Events,
ReplayMetadataResponse Metadata);
/// <summary>
/// Replay event response.
/// </summary>
public sealed record ReplayEventResponse(
Guid EventId,
long SequenceNumber,
Guid ChainId,
int ChainSequence,
string EventType,
DateTimeOffset OccurredAt,
DateTimeOffset RecordedAt,
string? ActorId,
string? ActorType,
string? ArtifactId,
string? FindingId,
string? PolicyVersion,
string EventHash,
string PreviousHash,
object? Payload);
/// <summary>
/// Replay metadata response.
/// </summary>
public sealed record ReplayMetadataResponse(
long FromSequence,
long ToSequence,
long EventsCount,
bool HasMore,
long ReplayDurationMs);
// === Diff Contracts ===
/// <summary>
/// Request for computing diff.
/// </summary>
public sealed record DiffApiRequest(
DiffPointRequest From,
DiffPointRequest To,
IReadOnlyList<string>? EntityTypes = null,
bool IncludeUnchanged = false,
string OutputFormat = "Summary")
{
public DiffRequest ToRequest(string tenantId) => new(
TenantId: tenantId,
From: From.ToDiffPoint(),
To: To.ToDiffPoint(),
EntityTypes: EntityTypes?.Select(ParseEntityType).ToList(),
IncludeUnchanged: IncludeUnchanged,
OutputFormat: Enum.TryParse<DiffOutputFormat>(OutputFormat, true, out var fmt)
? fmt : DiffOutputFormat.Summary);
private static EntityType ParseEntityType(string s) =>
Enum.TryParse<EntityType>(s, true, out var et) ? et : EntityType.Finding;
}
/// <summary>
/// Diff point request.
/// </summary>
public sealed record DiffPointRequest(
DateTimeOffset? Timestamp = null,
long? SequenceNumber = null,
Guid? SnapshotId = null)
{
public DiffPoint ToDiffPoint() => new(Timestamp, SequenceNumber, SnapshotId);
}
/// <summary>
/// Response for diff.
/// </summary>
public sealed record DiffApiResponse(
QueryPointResponse FromPoint,
QueryPointResponse ToPoint,
DiffSummaryResponse Summary,
IReadOnlyList<DiffEntryResponse>? Changes,
string? NextPageToken);
/// <summary>
/// Diff summary response.
/// </summary>
public sealed record DiffSummaryResponse(
int Added,
int Modified,
int Removed,
int Unchanged,
Dictionary<string, DiffCountsResponse>? ByEntityType);
/// <summary>
/// Diff counts response.
/// </summary>
public sealed record DiffCountsResponse(int Added, int Modified, int Removed);
/// <summary>
/// Diff entry response.
/// </summary>
public sealed record DiffEntryResponse(
string EntityType,
string EntityId,
string ChangeType,
object? FromState,
object? ToState,
IReadOnlyList<string>? ChangedFields);
// === Changelog Contracts ===
/// <summary>
/// Changelog entry response.
/// </summary>
public sealed record ChangeLogEntryResponse(
long SequenceNumber,
DateTimeOffset Timestamp,
string EntityType,
string EntityId,
string EventType,
string? EventHash,
string? ActorId,
string? Summary);
// === Staleness Contracts ===
/// <summary>
/// Staleness check response.
/// </summary>
public sealed record StalenessResponse(
bool IsStale,
DateTimeOffset CheckedAt,
DateTimeOffset? LastEventAt,
string StalenessThreshold,
string? StalenessDuration,
Dictionary<string, EntityStalenessResponse>? ByEntityType);
/// <summary>
/// Entity staleness response.
/// </summary>
public sealed record EntityStalenessResponse(
bool IsStale,
DateTimeOffset? LastEventAt,
long EventsBehind);
// === Extension Methods ===
public static class SnapshotExtensions
{
public static SnapshotResponse ToResponse(this LedgerSnapshot snapshot) => new(
SnapshotId: snapshot.SnapshotId,
Label: snapshot.Label,
Description: snapshot.Description,
Status: snapshot.Status.ToString(),
CreatedAt: snapshot.CreatedAt,
ExpiresAt: snapshot.ExpiresAt,
SequenceNumber: snapshot.SequenceNumber,
Timestamp: snapshot.Timestamp,
Statistics: snapshot.Statistics.ToResponse(),
MerkleRoot: snapshot.MerkleRoot,
DsseDigest: snapshot.DsseDigest,
Metadata: snapshot.Metadata);
public static SnapshotStatisticsResponse ToResponse(this SnapshotStatistics stats) => new(
FindingsCount: stats.FindingsCount,
VexStatementsCount: stats.VexStatementsCount,
AdvisoriesCount: stats.AdvisoriesCount,
SbomsCount: stats.SbomsCount,
EventsCount: stats.EventsCount,
SizeBytes: stats.SizeBytes);
public static QueryPointResponse ToResponse(this QueryPoint point) => new(
Timestamp: point.Timestamp,
SequenceNumber: point.SequenceNumber,
SnapshotId: point.SnapshotId);
public static FindingHistoryResponse ToResponse(this FindingHistoryItem item) => new(
FindingId: item.FindingId,
ArtifactId: item.ArtifactId,
VulnId: item.VulnId,
Status: item.Status,
Severity: item.Severity,
PolicyVersion: item.PolicyVersion,
FirstSeen: item.FirstSeen,
LastUpdated: item.LastUpdated,
Labels: item.Labels);
public static VexHistoryResponse ToResponse(this VexHistoryItem item) => new(
StatementId: item.StatementId,
VulnId: item.VulnId,
ProductId: item.ProductId,
Status: item.Status,
Justification: item.Justification,
IssuedAt: item.IssuedAt,
ExpiresAt: item.ExpiresAt);
public static AdvisoryHistoryResponse ToResponse(this AdvisoryHistoryItem item) => new(
AdvisoryId: item.AdvisoryId,
Source: item.Source,
Title: item.Title,
CvssScore: item.CvssScore,
PublishedAt: item.PublishedAt,
ModifiedAt: item.ModifiedAt);
public static ReplayEventResponse ToResponse(this ReplayEvent e) => new(
EventId: e.EventId,
SequenceNumber: e.SequenceNumber,
ChainId: e.ChainId,
ChainSequence: e.ChainSequence,
EventType: e.EventType,
OccurredAt: e.OccurredAt,
RecordedAt: e.RecordedAt,
ActorId: e.ActorId,
ActorType: e.ActorType,
ArtifactId: e.ArtifactId,
FindingId: e.FindingId,
PolicyVersion: e.PolicyVersion,
EventHash: e.EventHash,
PreviousHash: e.PreviousHash,
Payload: e.Payload);
public static ReplayMetadataResponse ToResponse(this ReplayMetadata m) => new(
FromSequence: m.FromSequence,
ToSequence: m.ToSequence,
EventsCount: m.EventsCount,
HasMore: m.HasMore,
ReplayDurationMs: m.ReplayDurationMs);
public static DiffSummaryResponse ToResponse(this DiffSummary summary) => new(
Added: summary.Added,
Modified: summary.Modified,
Removed: summary.Removed,
Unchanged: summary.Unchanged,
ByEntityType: summary.ByEntityType?.ToDictionary(
kv => kv.Key.ToString(),
kv => new DiffCountsResponse(kv.Value.Added, kv.Value.Modified, kv.Value.Removed)));
public static DiffEntryResponse ToResponse(this DiffEntry entry) => new(
EntityType: entry.EntityType.ToString(),
EntityId: entry.EntityId,
ChangeType: entry.ChangeType.ToString(),
FromState: entry.FromState,
ToState: entry.ToState,
ChangedFields: entry.ChangedFields);
public static ChangeLogEntryResponse ToResponse(this ChangeLogEntry entry) => new(
SequenceNumber: entry.SequenceNumber,
Timestamp: entry.Timestamp,
EntityType: entry.EntityType.ToString(),
EntityId: entry.EntityId,
EventType: entry.EventType,
EventHash: entry.EventHash,
ActorId: entry.ActorId,
Summary: entry.Summary);
public static StalenessResponse ToResponse(this StalenessResult result) => new(
IsStale: result.IsStale,
CheckedAt: result.CheckedAt,
LastEventAt: result.LastEventAt,
StalenessThreshold: result.StalenessThreshold.ToString(),
StalenessDuration: result.StalenessDuration?.ToString(),
ByEntityType: result.ByEntityType?.ToDictionary(
kv => kv.Key.ToString(),
kv => new EntityStalenessResponse(kv.Value.IsStale, kv.Value.LastEventAt, kv.Value.EventsBehind)));
}