partly or unimplemented features - now implemented

This commit is contained in:
master
2026-02-09 08:53:51 +02:00
parent 1bf6bbf395
commit 4bdc298ec1
674 changed files with 90194 additions and 2271 deletions

View File

@@ -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