462 lines
14 KiB
C#
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)));
|
|
}
|