partly or unimplemented features - now implemented
This commit is contained in:
@@ -0,0 +1,628 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PointInTimeQueryEndpoints.cs
|
||||
// Sprint: SPRINT_20260208_056_Replay_point_in_time_vulnerability_query
|
||||
// Task: T1 — Point-in-Time Vulnerability Query API endpoints
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.Replay.Core.FeedSnapshots;
|
||||
|
||||
namespace StellaOps.Replay.WebService;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering point-in-time vulnerability query endpoints.
|
||||
/// </summary>
|
||||
public static class PointInTimeQueryEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps point-in-time vulnerability query endpoints to the application.
|
||||
/// </summary>
|
||||
public static void MapPointInTimeQueryEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/v1/pit/advisory")
|
||||
.WithTags("Point-in-Time Advisory");
|
||||
|
||||
// GET /v1/pit/advisory/{cveId} - Query advisory state at a point in time
|
||||
group.MapGet("/{cveId}", QueryAdvisoryAsync)
|
||||
.WithName("QueryAdvisoryAtPointInTime")
|
||||
.Produces<AdvisoryQueryResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// POST /v1/pit/advisory/cross-provider - Query advisory across multiple providers
|
||||
group.MapPost("/cross-provider", QueryCrossProviderAsync)
|
||||
.WithName("QueryCrossProviderAdvisory")
|
||||
.Produces<CrossProviderQueryResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /v1/pit/advisory/{cveId}/timeline - Get advisory timeline
|
||||
group.MapGet("/{cveId}/timeline", GetAdvisoryTimelineAsync)
|
||||
.WithName("GetAdvisoryTimeline")
|
||||
.Produces<AdvisoryTimelineResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// POST /v1/pit/advisory/diff - Compare advisory at two points in time
|
||||
group.MapPost("/diff", CompareAdvisoryAtTimesAsync)
|
||||
.WithName("CompareAdvisoryAtTimes")
|
||||
.Produces<AdvisoryDiffResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
var snapshotsGroup = app.MapGroup("/v1/pit/snapshots")
|
||||
.WithTags("Feed Snapshots");
|
||||
|
||||
// POST /v1/pit/snapshots - Capture a feed snapshot
|
||||
snapshotsGroup.MapPost("/", CaptureSnapshotAsync)
|
||||
.WithName("CaptureFeedSnapshot")
|
||||
.Produces<SnapshotCaptureResponse>(StatusCodes.Status201Created)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /v1/pit/snapshots/{digest} - Get a snapshot by digest
|
||||
snapshotsGroup.MapGet("/{digest}", GetSnapshotAsync)
|
||||
.WithName("GetFeedSnapshot")
|
||||
.Produces<SnapshotResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// GET /v1/pit/snapshots/{digest}/verify - Verify snapshot integrity
|
||||
snapshotsGroup.MapGet("/{digest}/verify", VerifySnapshotIntegrityAsync)
|
||||
.WithName("VerifySnapshotIntegrity")
|
||||
.Produces<SnapshotVerificationResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// POST /v1/pit/snapshots/bundle - Create a snapshot bundle
|
||||
snapshotsGroup.MapPost("/bundle", CreateSnapshotBundleAsync)
|
||||
.WithName("CreateSnapshotBundle")
|
||||
.Produces<SnapshotBundleResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<AdvisoryQueryResponse>, NotFound, ProblemHttpResult>> QueryAdvisoryAsync(
|
||||
HttpContext httpContext,
|
||||
string cveId,
|
||||
[AsParameters] AdvisoryQueryParameters queryParams,
|
||||
PointInTimeAdvisoryResolver resolver,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(queryParams.ProviderId))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_provider",
|
||||
detail: "Provider ID is required");
|
||||
}
|
||||
|
||||
if (!queryParams.PointInTime.HasValue)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_point_in_time",
|
||||
detail: "Point-in-time timestamp is required");
|
||||
}
|
||||
|
||||
var result = await resolver.ResolveAdvisoryAsync(
|
||||
cveId,
|
||||
queryParams.ProviderId,
|
||||
queryParams.PointInTime.Value,
|
||||
ct);
|
||||
|
||||
if (result.Status == AdvisoryResolutionStatus.NoSnapshot)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(new AdvisoryQueryResponse
|
||||
{
|
||||
CveId = result.CveId,
|
||||
ProviderId = result.ProviderId,
|
||||
PointInTime = result.PointInTime,
|
||||
Status = result.Status.ToString(),
|
||||
Advisory = result.Advisory is not null ? MapAdvisory(result.Advisory) : null,
|
||||
SnapshotDigest = result.SnapshotDigest,
|
||||
SnapshotCapturedAt = result.SnapshotCapturedAt
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<CrossProviderQueryResponse>, ProblemHttpResult>> QueryCrossProviderAsync(
|
||||
HttpContext httpContext,
|
||||
CrossProviderQueryRequest request,
|
||||
PointInTimeAdvisoryResolver resolver,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.CveId))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_cve_id",
|
||||
detail: "CVE ID is required");
|
||||
}
|
||||
|
||||
if (request.ProviderIds is null || request.ProviderIds.Count == 0)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_providers",
|
||||
detail: "At least one provider ID is required");
|
||||
}
|
||||
|
||||
var result = await resolver.ResolveCrossProviderAsync(
|
||||
request.CveId,
|
||||
request.ProviderIds,
|
||||
request.PointInTime,
|
||||
ct);
|
||||
|
||||
return TypedResults.Ok(new CrossProviderQueryResponse
|
||||
{
|
||||
CveId = result.CveId,
|
||||
PointInTime = result.PointInTime,
|
||||
FoundCount = result.FoundCount,
|
||||
MissingSnapshotProviders = result.MissingSnapshotProviders,
|
||||
NotFoundProviders = result.NotFoundProviders,
|
||||
Results = result.Results.Select(r => new ProviderAdvisoryResult
|
||||
{
|
||||
ProviderId = r.ProviderId,
|
||||
Status = r.Status.ToString(),
|
||||
Advisory = r.Advisory is not null ? MapAdvisory(r.Advisory) : null,
|
||||
SnapshotDigest = r.SnapshotDigest
|
||||
}).ToList(),
|
||||
Consensus = result.Consensus is not null ? new ConsensusInfo
|
||||
{
|
||||
ProviderCount = result.Consensus.ProviderCount,
|
||||
SeverityConsensus = result.Consensus.SeverityConsensus,
|
||||
FixStatusConsensus = result.Consensus.FixStatusConsensus,
|
||||
ConsensusSeverity = result.Consensus.ConsensusSeverity,
|
||||
ConsensusFixStatus = result.Consensus.ConsensusFixStatus
|
||||
} : null
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<AdvisoryTimelineResponse>, NotFound>> GetAdvisoryTimelineAsync(
|
||||
HttpContext httpContext,
|
||||
string cveId,
|
||||
[AsParameters] TimelineQueryParameters queryParams,
|
||||
PointInTimeAdvisoryResolver resolver,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(queryParams.ProviderId))
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var timeline = await resolver.GetAdvisoryTimelineAsync(
|
||||
cveId,
|
||||
queryParams.ProviderId,
|
||||
queryParams.From,
|
||||
queryParams.To,
|
||||
ct);
|
||||
|
||||
if (timeline.TotalSnapshots == 0)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(new AdvisoryTimelineResponse
|
||||
{
|
||||
CveId = timeline.CveId,
|
||||
ProviderId = timeline.ProviderId,
|
||||
TotalSnapshots = timeline.TotalSnapshots,
|
||||
ChangesCount = timeline.ChangesCount,
|
||||
FirstAppearance = timeline.FirstAppearance,
|
||||
LastUpdate = timeline.LastUpdate,
|
||||
Entries = timeline.Entries.Select(e => new TimelineEntryDto
|
||||
{
|
||||
SnapshotDigest = e.SnapshotDigest,
|
||||
CapturedAt = e.CapturedAt,
|
||||
EpochTimestamp = e.EpochTimestamp,
|
||||
ChangeType = e.ChangeType.ToString(),
|
||||
HasAdvisory = e.Advisory is not null
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<AdvisoryDiffResponse>, ProblemHttpResult>> CompareAdvisoryAtTimesAsync(
|
||||
HttpContext httpContext,
|
||||
AdvisoryDiffRequest request,
|
||||
PointInTimeAdvisoryResolver resolver,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.CveId) || string.IsNullOrWhiteSpace(request.ProviderId))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_required_fields",
|
||||
detail: "CVE ID and Provider ID are required");
|
||||
}
|
||||
|
||||
var diff = await resolver.CompareAtTimesAsync(
|
||||
request.CveId,
|
||||
request.ProviderId,
|
||||
request.Time1,
|
||||
request.Time2,
|
||||
ct);
|
||||
|
||||
return TypedResults.Ok(new AdvisoryDiffResponse
|
||||
{
|
||||
CveId = diff.CveId,
|
||||
ProviderId = diff.ProviderId,
|
||||
Time1 = diff.Time1,
|
||||
Time2 = diff.Time2,
|
||||
DiffType = diff.DiffType.ToString(),
|
||||
Changes = diff.Changes.Select(c => new FieldChangeDto
|
||||
{
|
||||
Field = c.Field,
|
||||
OldValue = c.OldValue,
|
||||
NewValue = c.NewValue
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Created<SnapshotCaptureResponse>, ProblemHttpResult>> CaptureSnapshotAsync(
|
||||
HttpContext httpContext,
|
||||
SnapshotCaptureRequest request,
|
||||
FeedSnapshotService snapshotService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ProviderId) || request.FeedData is null)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_required_fields",
|
||||
detail: "Provider ID and feed data are required");
|
||||
}
|
||||
|
||||
var result = await snapshotService.CaptureSnapshotAsync(
|
||||
new CaptureSnapshotRequest
|
||||
{
|
||||
ProviderId = request.ProviderId,
|
||||
ProviderName = request.ProviderName,
|
||||
FeedType = request.FeedType,
|
||||
FeedData = request.FeedData,
|
||||
EpochTimestamp = request.EpochTimestamp
|
||||
},
|
||||
ct);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "capture_failed",
|
||||
detail: result.Error ?? "Failed to capture snapshot");
|
||||
}
|
||||
|
||||
return TypedResults.Created(
|
||||
$"/v1/pit/snapshots/{result.Digest}",
|
||||
new SnapshotCaptureResponse
|
||||
{
|
||||
Digest = result.Digest,
|
||||
ProviderId = result.ProviderId,
|
||||
CapturedAt = result.CapturedAt,
|
||||
WasExisting = result.WasExisting,
|
||||
ContentSize = result.ContentSize
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<SnapshotResponse>, NotFound>> GetSnapshotAsync(
|
||||
HttpContext httpContext,
|
||||
string digest,
|
||||
FeedSnapshotService snapshotService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var snapshot = await snapshotService.GetByDigestAsync(digest, ct);
|
||||
if (snapshot is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(new SnapshotResponse
|
||||
{
|
||||
Digest = snapshot.Digest,
|
||||
ProviderId = snapshot.ProviderId,
|
||||
ProviderName = snapshot.ProviderName,
|
||||
FeedType = snapshot.FeedType,
|
||||
CapturedAt = snapshot.CapturedAt,
|
||||
EpochTimestamp = snapshot.EpochTimestamp,
|
||||
Format = snapshot.Format.ToString(),
|
||||
ContentSize = snapshot.Content.Length
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<SnapshotVerificationResponse>, NotFound>> VerifySnapshotIntegrityAsync(
|
||||
HttpContext httpContext,
|
||||
string digest,
|
||||
FeedSnapshotService snapshotService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await snapshotService.VerifyIntegrityAsync(digest, ct);
|
||||
if (result.Error?.Contains("not found") == true)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(new SnapshotVerificationResponse
|
||||
{
|
||||
Success = result.Success,
|
||||
ExpectedDigest = result.ExpectedDigest,
|
||||
ActualDigest = result.ActualDigest,
|
||||
ProviderId = result.ProviderId,
|
||||
CapturedAt = result.CapturedAt,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<SnapshotBundleResponse>, ProblemHttpResult>> CreateSnapshotBundleAsync(
|
||||
HttpContext httpContext,
|
||||
SnapshotBundleRequest request,
|
||||
FeedSnapshotService snapshotService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (request.ProviderIds is null || request.ProviderIds.Count == 0)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_providers",
|
||||
detail: "At least one provider ID is required");
|
||||
}
|
||||
|
||||
var bundle = await snapshotService.CreateBundleAsync(
|
||||
request.ProviderIds,
|
||||
request.PointInTime,
|
||||
ct);
|
||||
|
||||
return TypedResults.Ok(new SnapshotBundleResponse
|
||||
{
|
||||
BundleDigest = bundle.BundleDigest,
|
||||
PointInTime = bundle.PointInTime,
|
||||
CreatedAt = bundle.CreatedAt,
|
||||
IsComplete = bundle.IsComplete,
|
||||
SnapshotCount = bundle.Snapshots.Length,
|
||||
MissingProviders = bundle.MissingProviders
|
||||
});
|
||||
}
|
||||
|
||||
private static AdvisoryDto MapAdvisory(AdvisoryData advisory) => new()
|
||||
{
|
||||
CveId = advisory.CveId,
|
||||
Severity = advisory.Severity,
|
||||
CvssScore = advisory.CvssScore,
|
||||
CvssVector = advisory.CvssVector,
|
||||
Description = advisory.Description,
|
||||
FixStatus = advisory.FixStatus,
|
||||
AffectedProducts = advisory.AffectedProducts,
|
||||
References = advisory.References,
|
||||
PublishedAt = advisory.PublishedAt,
|
||||
LastModifiedAt = advisory.LastModifiedAt
|
||||
};
|
||||
}
|
||||
|
||||
#region Request/Response DTOs
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for advisory lookup.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryQueryParameters
|
||||
{
|
||||
public string? ProviderId { get; init; }
|
||||
public DateTimeOffset? PointInTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for timeline lookup.
|
||||
/// </summary>
|
||||
public sealed class TimelineQueryParameters
|
||||
{
|
||||
public required string ProviderId { get; init; }
|
||||
public DateTimeOffset? From { get; init; }
|
||||
public DateTimeOffset? To { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for advisory query.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryQueryResponse
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required DateTimeOffset PointInTime { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public AdvisoryDto? Advisory { get; init; }
|
||||
public string? SnapshotDigest { get; init; }
|
||||
public DateTimeOffset? SnapshotCapturedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for cross-provider query.
|
||||
/// </summary>
|
||||
public sealed class CrossProviderQueryRequest
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required IReadOnlyList<string> ProviderIds { get; init; }
|
||||
public required DateTimeOffset PointInTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for cross-provider query.
|
||||
/// </summary>
|
||||
public sealed class CrossProviderQueryResponse
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required DateTimeOffset PointInTime { get; init; }
|
||||
public required int FoundCount { get; init; }
|
||||
public required IReadOnlyList<string> MissingSnapshotProviders { get; init; }
|
||||
public required IReadOnlyList<string> NotFoundProviders { get; init; }
|
||||
public required IReadOnlyList<ProviderAdvisoryResult> Results { get; init; }
|
||||
public ConsensusInfo? Consensus { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result for a single provider in cross-provider query.
|
||||
/// </summary>
|
||||
public sealed class ProviderAdvisoryResult
|
||||
{
|
||||
public required string ProviderId { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public AdvisoryDto? Advisory { get; init; }
|
||||
public string? SnapshotDigest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consensus information across providers.
|
||||
/// </summary>
|
||||
public sealed class ConsensusInfo
|
||||
{
|
||||
public required int ProviderCount { get; init; }
|
||||
public required bool SeverityConsensus { get; init; }
|
||||
public required bool FixStatusConsensus { get; init; }
|
||||
public string? ConsensusSeverity { get; init; }
|
||||
public string? ConsensusFixStatus { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request for advisory diff.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryDiffRequest
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required DateTimeOffset Time1 { get; init; }
|
||||
public required DateTimeOffset Time2 { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for advisory diff.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryDiffResponse
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required DateTimeOffset Time1 { get; init; }
|
||||
public required DateTimeOffset Time2 { get; init; }
|
||||
public required string DiffType { get; init; }
|
||||
public required IReadOnlyList<FieldChangeDto> Changes { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A field change in a diff.
|
||||
/// </summary>
|
||||
public sealed class FieldChangeDto
|
||||
{
|
||||
public required string Field { get; init; }
|
||||
public string? OldValue { get; init; }
|
||||
public string? NewValue { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for advisory timeline.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryTimelineResponse
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required int TotalSnapshots { get; init; }
|
||||
public required int ChangesCount { get; init; }
|
||||
public DateTimeOffset? FirstAppearance { get; init; }
|
||||
public DateTimeOffset? LastUpdate { get; init; }
|
||||
public required IReadOnlyList<TimelineEntryDto> Entries { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timeline entry DTO.
|
||||
/// </summary>
|
||||
public sealed class TimelineEntryDto
|
||||
{
|
||||
public required string SnapshotDigest { get; init; }
|
||||
public required DateTimeOffset CapturedAt { get; init; }
|
||||
public required DateTimeOffset EpochTimestamp { get; init; }
|
||||
public required string ChangeType { get; init; }
|
||||
public required bool HasAdvisory { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to capture a feed snapshot.
|
||||
/// </summary>
|
||||
public sealed class SnapshotCaptureRequest
|
||||
{
|
||||
public required string ProviderId { get; init; }
|
||||
public string? ProviderName { get; init; }
|
||||
public string? FeedType { get; init; }
|
||||
public required object FeedData { get; init; }
|
||||
public DateTimeOffset? EpochTimestamp { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for snapshot capture.
|
||||
/// </summary>
|
||||
public sealed class SnapshotCaptureResponse
|
||||
{
|
||||
public required string Digest { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public required DateTimeOffset CapturedAt { get; init; }
|
||||
public required bool WasExisting { get; init; }
|
||||
public required long ContentSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for snapshot retrieval.
|
||||
/// </summary>
|
||||
public sealed class SnapshotResponse
|
||||
{
|
||||
public required string Digest { get; init; }
|
||||
public required string ProviderId { get; init; }
|
||||
public string? ProviderName { get; init; }
|
||||
public string? FeedType { get; init; }
|
||||
public required DateTimeOffset CapturedAt { get; init; }
|
||||
public required DateTimeOffset EpochTimestamp { get; init; }
|
||||
public required string Format { get; init; }
|
||||
public required int ContentSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for snapshot verification.
|
||||
/// </summary>
|
||||
public sealed class SnapshotVerificationResponse
|
||||
{
|
||||
public required bool Success { get; init; }
|
||||
public required string ExpectedDigest { get; init; }
|
||||
public string? ActualDigest { get; init; }
|
||||
public string? ProviderId { get; init; }
|
||||
public DateTimeOffset? CapturedAt { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request to create a snapshot bundle.
|
||||
/// </summary>
|
||||
public sealed class SnapshotBundleRequest
|
||||
{
|
||||
public required IReadOnlyList<string> ProviderIds { get; init; }
|
||||
public required DateTimeOffset PointInTime { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for snapshot bundle.
|
||||
/// </summary>
|
||||
public sealed class SnapshotBundleResponse
|
||||
{
|
||||
public required string BundleDigest { get; init; }
|
||||
public required DateTimeOffset PointInTime { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public required bool IsComplete { get; init; }
|
||||
public required int SnapshotCount { get; init; }
|
||||
public required IReadOnlyList<string> MissingProviders { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for advisory data.
|
||||
/// </summary>
|
||||
public sealed class AdvisoryDto
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public string? Severity { get; init; }
|
||||
public decimal? CvssScore { get; init; }
|
||||
public string? CvssVector { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public string? FixStatus { get; init; }
|
||||
public IReadOnlyList<string>? AffectedProducts { get; init; }
|
||||
public IReadOnlyList<string>? References { get; init; }
|
||||
public DateTimeOffset? PublishedAt { get; init; }
|
||||
public DateTimeOffset? LastModifiedAt { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user