Add unit tests for PhpFrameworkSurface and PhpPharScanner
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
- Implement comprehensive tests for PhpFrameworkSurface, covering scenarios such as empty surfaces, presence of routes, controllers, middlewares, CLI commands, cron jobs, and event listeners. - Validate metadata creation for route counts, HTTP methods, protected and public routes, and route patterns. - Introduce tests for PhpPharScanner, including handling of non-existent files, null or empty paths, invalid PHAR files, and minimal PHAR structures. - Ensure correct computation of SHA256 for valid PHAR files and validate the properties of PhpPharArchive, PhpPharEntry, and PhpPharScanResult.
This commit is contained in:
@@ -155,6 +155,14 @@ builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
|
||||
builder.Services.AddHostedService<LedgerProjectionWorker>();
|
||||
builder.Services.AddSingleton<ExportQueryService>();
|
||||
builder.Services.AddSingleton<AttestationQueryService>();
|
||||
builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Attestation.IAttestationPointerRepository,
|
||||
StellaOps.Findings.Ledger.Infrastructure.Postgres.PostgresAttestationPointerRepository>();
|
||||
builder.Services.AddSingleton<AttestationPointerService>();
|
||||
builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Snapshot.ISnapshotRepository,
|
||||
StellaOps.Findings.Ledger.Infrastructure.Postgres.PostgresSnapshotRepository>();
|
||||
builder.Services.AddSingleton<StellaOps.Findings.Ledger.Infrastructure.Snapshot.ITimeTravelRepository,
|
||||
StellaOps.Findings.Ledger.Infrastructure.Postgres.PostgresTimeTravelRepository>();
|
||||
builder.Services.AddSingleton<SnapshotService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -633,6 +641,206 @@ app.MapPost("/internal/ledger/airgap-import", async Task<Results<Accepted<Airgap
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
||||
.ProducesProblem(StatusCodes.Status409Conflict);
|
||||
|
||||
// Attestation Pointer Endpoints (LEDGER-ATTEST-73-001)
|
||||
app.MapPost("/v1/ledger/attestation-pointers", async Task<Results<Created<CreateAttestationPointerResponse>, Ok<CreateAttestationPointerResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
CreateAttestationPointerRequest request,
|
||||
AttestationPointerService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var input = request.ToInput(tenantId);
|
||||
var result = await service.CreatePointerAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new CreateAttestationPointerResponse(
|
||||
result.Success,
|
||||
result.PointerId?.ToString(),
|
||||
result.LedgerEventId?.ToString(),
|
||||
result.Error);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "attestation_pointer_failed",
|
||||
detail: result.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Created($"/v1/ledger/attestation-pointers/{result.PointerId}", response);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_request",
|
||||
detail: ex.Message);
|
||||
}
|
||||
})
|
||||
.WithName("CreateAttestationPointer")
|
||||
.RequireAuthorization(LedgerWritePolicy)
|
||||
.Produces(StatusCodes.Status201Created)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/attestation-pointers/{pointerId}", async Task<Results<JsonHttpResult<AttestationPointerResponse>, NotFound, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string pointerId,
|
||||
AttestationPointerService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(pointerId, out var pointerGuid))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_pointer_id",
|
||||
detail: "Pointer ID must be a valid GUID.");
|
||||
}
|
||||
|
||||
var pointer = await service.GetPointerAsync(tenantId, pointerGuid, cancellationToken).ConfigureAwait(false);
|
||||
if (pointer is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Json(pointer.ToResponse());
|
||||
})
|
||||
.WithName("GetAttestationPointer")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/findings/{findingId}/attestation-pointers", async Task<Results<JsonHttpResult<IReadOnlyList<AttestationPointerResponse>>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string findingId,
|
||||
AttestationPointerService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var pointers = await service.GetPointersAsync(tenantId, findingId, cancellationToken).ConfigureAwait(false);
|
||||
IReadOnlyList<AttestationPointerResponse> responseList = pointers.Select(p => p.ToResponse()).ToList();
|
||||
return TypedResults.Json(responseList);
|
||||
})
|
||||
.WithName("GetFindingAttestationPointers")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/findings/{findingId}/attestation-summary", async Task<Results<JsonHttpResult<AttestationSummaryResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string findingId,
|
||||
AttestationPointerService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var summary = await service.GetSummaryAsync(tenantId, findingId, cancellationToken).ConfigureAwait(false);
|
||||
return TypedResults.Json(summary.ToResponse());
|
||||
})
|
||||
.WithName("GetFindingAttestationSummary")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapPost("/v1/ledger/attestation-pointers/search", async Task<Results<JsonHttpResult<AttestationPointerSearchResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
AttestationPointerSearchRequest request,
|
||||
AttestationPointerService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var query = request.ToQuery(tenantId);
|
||||
var pointers = await service.SearchAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new AttestationPointerSearchResponse(
|
||||
pointers.Select(p => p.ToResponse()).ToList(),
|
||||
pointers.Count);
|
||||
|
||||
return TypedResults.Json(response);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_request",
|
||||
detail: ex.Message);
|
||||
}
|
||||
})
|
||||
.WithName("SearchAttestationPointers")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapPut("/v1/ledger/attestation-pointers/{pointerId}/verification", async Task<Results<NoContent, NotFound, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string pointerId,
|
||||
UpdateVerificationResultRequest request,
|
||||
AttestationPointerService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(pointerId, out var pointerGuid))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_pointer_id",
|
||||
detail: "Pointer ID must be a valid GUID.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var verificationResult = request.VerificationResult.ToModel();
|
||||
var success = await service.UpdateVerificationResultAsync(tenantId, pointerGuid, verificationResult, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.NoContent();
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_request",
|
||||
detail: ex.Message);
|
||||
}
|
||||
})
|
||||
.WithName("UpdateAttestationPointerVerification")
|
||||
.RequireAuthorization(LedgerWritePolicy)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/.well-known/openapi", () =>
|
||||
{
|
||||
var contentRoot = AppContext.BaseDirectory;
|
||||
@@ -649,6 +857,383 @@ app.MapGet("/.well-known/openapi", () =>
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// Snapshot Endpoints (LEDGER-PACKS-42-001-DEV)
|
||||
app.MapPost("/v1/ledger/snapshots", async Task<Results<Created<CreateSnapshotResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
CreateSnapshotRequest request,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var input = request.ToInput(tenantId);
|
||||
var result = await service.CreateSnapshotAsync(input, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new CreateSnapshotResponse(
|
||||
result.Success,
|
||||
result.Snapshot?.ToResponse(),
|
||||
result.Error);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "snapshot_creation_failed",
|
||||
detail: result.Error);
|
||||
}
|
||||
|
||||
return TypedResults.Created($"/v1/ledger/snapshots/{result.Snapshot!.SnapshotId}", response);
|
||||
})
|
||||
.WithName("CreateSnapshot")
|
||||
.RequireAuthorization(LedgerWritePolicy)
|
||||
.Produces(StatusCodes.Status201Created)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/snapshots", async Task<Results<JsonHttpResult<SnapshotListResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var statusStr = httpContext.Request.Query["status"].ToString();
|
||||
Domain.SnapshotStatus? status = null;
|
||||
if (!string.IsNullOrEmpty(statusStr) && Enum.TryParse<Domain.SnapshotStatus>(statusStr, true, out var parsedStatus))
|
||||
{
|
||||
status = parsedStatus;
|
||||
}
|
||||
|
||||
var query = new Domain.SnapshotListQuery(
|
||||
tenantId,
|
||||
status,
|
||||
ParseDate(httpContext.Request.Query["created_after"]),
|
||||
ParseDate(httpContext.Request.Query["created_before"]),
|
||||
ParseInt(httpContext.Request.Query["page_size"]) ?? 100,
|
||||
httpContext.Request.Query["page_token"].ToString());
|
||||
|
||||
var (snapshots, nextPageToken) = await service.ListSnapshotsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new SnapshotListResponse(
|
||||
snapshots.Select(s => s.ToResponse()).ToList(),
|
||||
nextPageToken);
|
||||
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("ListSnapshots")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/snapshots/{snapshotId}", async Task<Results<JsonHttpResult<SnapshotResponse>, NotFound, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string snapshotId,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(snapshotId, out var snapshotGuid))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_snapshot_id",
|
||||
detail: "Snapshot ID must be a valid GUID.");
|
||||
}
|
||||
|
||||
var snapshot = await service.GetSnapshotAsync(tenantId, snapshotGuid, cancellationToken).ConfigureAwait(false);
|
||||
if (snapshot is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Json(snapshot.ToResponse());
|
||||
})
|
||||
.WithName("GetSnapshot")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapDelete("/v1/ledger/snapshots/{snapshotId}", async Task<Results<NoContent, NotFound, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string snapshotId,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(snapshotId, out var snapshotGuid))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_snapshot_id",
|
||||
detail: "Snapshot ID must be a valid GUID.");
|
||||
}
|
||||
|
||||
var deleted = await service.DeleteSnapshotAsync(tenantId, snapshotGuid, cancellationToken).ConfigureAwait(false);
|
||||
if (!deleted)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.NoContent();
|
||||
})
|
||||
.WithName("DeleteSnapshot")
|
||||
.RequireAuthorization(LedgerWritePolicy)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Time-Travel Query Endpoints
|
||||
app.MapGet("/v1/ledger/time-travel/findings", async Task<Results<JsonHttpResult<HistoricalQueryApiResponse<FindingHistoryResponse>>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var request = new HistoricalQueryApiRequest(
|
||||
AtTimestamp: ParseDate(httpContext.Request.Query["at_timestamp"]),
|
||||
AtSequence: ParseLong(httpContext.Request.Query["at_sequence"]),
|
||||
SnapshotId: ParseGuid(httpContext.Request.Query["snapshot_id"]),
|
||||
Status: httpContext.Request.Query["status"].ToString(),
|
||||
SeverityMin: ParseDecimal(httpContext.Request.Query["severity_min"]),
|
||||
SeverityMax: ParseDecimal(httpContext.Request.Query["severity_max"]),
|
||||
PolicyVersion: httpContext.Request.Query["policy_version"].ToString(),
|
||||
ArtifactId: httpContext.Request.Query["artifact_id"].ToString(),
|
||||
VulnId: httpContext.Request.Query["vuln_id"].ToString(),
|
||||
PageSize: ParseInt(httpContext.Request.Query["page_size"]) ?? 500,
|
||||
PageToken: httpContext.Request.Query["page_token"].ToString());
|
||||
|
||||
var domainRequest = request.ToRequest(tenantId, Domain.EntityType.Finding);
|
||||
var result = await service.QueryHistoricalFindingsAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new HistoricalQueryApiResponse<FindingHistoryResponse>(
|
||||
result.QueryPoint.ToResponse(),
|
||||
"Finding",
|
||||
result.Items.Select(i => i.ToResponse()).ToList(),
|
||||
result.NextPageToken,
|
||||
result.TotalCount);
|
||||
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("TimeTravelQueryFindings")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/time-travel/vex", async Task<Results<JsonHttpResult<HistoricalQueryApiResponse<VexHistoryResponse>>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var request = new HistoricalQueryApiRequest(
|
||||
AtTimestamp: ParseDate(httpContext.Request.Query["at_timestamp"]),
|
||||
AtSequence: ParseLong(httpContext.Request.Query["at_sequence"]),
|
||||
SnapshotId: ParseGuid(httpContext.Request.Query["snapshot_id"]),
|
||||
PageSize: ParseInt(httpContext.Request.Query["page_size"]) ?? 500,
|
||||
PageToken: httpContext.Request.Query["page_token"].ToString());
|
||||
|
||||
var domainRequest = request.ToRequest(tenantId, Domain.EntityType.Vex);
|
||||
var result = await service.QueryHistoricalVexAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new HistoricalQueryApiResponse<VexHistoryResponse>(
|
||||
result.QueryPoint.ToResponse(),
|
||||
"Vex",
|
||||
result.Items.Select(i => i.ToResponse()).ToList(),
|
||||
result.NextPageToken,
|
||||
result.TotalCount);
|
||||
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("TimeTravelQueryVex")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.MapGet("/v1/ledger/time-travel/advisories", async Task<Results<JsonHttpResult<HistoricalQueryApiResponse<AdvisoryHistoryResponse>>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var request = new HistoricalQueryApiRequest(
|
||||
AtTimestamp: ParseDate(httpContext.Request.Query["at_timestamp"]),
|
||||
AtSequence: ParseLong(httpContext.Request.Query["at_sequence"]),
|
||||
SnapshotId: ParseGuid(httpContext.Request.Query["snapshot_id"]),
|
||||
PageSize: ParseInt(httpContext.Request.Query["page_size"]) ?? 500,
|
||||
PageToken: httpContext.Request.Query["page_token"].ToString());
|
||||
|
||||
var domainRequest = request.ToRequest(tenantId, Domain.EntityType.Advisory);
|
||||
var result = await service.QueryHistoricalAdvisoriesAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new HistoricalQueryApiResponse<AdvisoryHistoryResponse>(
|
||||
result.QueryPoint.ToResponse(),
|
||||
"Advisory",
|
||||
result.Items.Select(i => i.ToResponse()).ToList(),
|
||||
result.NextPageToken,
|
||||
result.TotalCount);
|
||||
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("TimeTravelQueryAdvisories")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Replay Endpoint
|
||||
app.MapPost("/v1/ledger/replay", async Task<Results<JsonHttpResult<ReplayApiResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
ReplayApiRequest request,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var domainRequest = request.ToRequest(tenantId);
|
||||
var (events, metadata) = await service.ReplayEventsAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new ReplayApiResponse(
|
||||
events.Select(e => e.ToResponse()).ToList(),
|
||||
metadata.ToResponse());
|
||||
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("ReplayEvents")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Diff Endpoint
|
||||
app.MapPost("/v1/ledger/diff", async Task<Results<JsonHttpResult<DiffApiResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
DiffApiRequest request,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var domainRequest = request.ToRequest(tenantId);
|
||||
var result = await service.ComputeDiffAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new DiffApiResponse(
|
||||
result.FromPoint.ToResponse(),
|
||||
result.ToPoint.ToResponse(),
|
||||
result.Summary.ToResponse(),
|
||||
result.Changes?.Select(c => c.ToResponse()).ToList(),
|
||||
result.NextPageToken);
|
||||
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("ComputeDiff")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Changelog Endpoint
|
||||
app.MapGet("/v1/ledger/changelog/{entityType}/{entityId}", async Task<Results<JsonHttpResult<IReadOnlyList<ChangeLogEntryResponse>>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string entityType,
|
||||
string entityId,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<Domain.EntityType>(entityType, true, out var parsedEntityType))
|
||||
{
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "invalid_entity_type",
|
||||
detail: "Entity type must be one of: Finding, Vex, Advisory, Sbom, Evidence.");
|
||||
}
|
||||
|
||||
var limit = ParseInt(httpContext.Request.Query["limit"]) ?? 100;
|
||||
var changelog = await service.GetChangelogAsync(tenantId, parsedEntityType, entityId, limit, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
IReadOnlyList<ChangeLogEntryResponse> response = changelog.Select(e => e.ToResponse()).ToList();
|
||||
return TypedResults.Json(response);
|
||||
})
|
||||
.WithName("GetChangelog")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Staleness Check Endpoint
|
||||
app.MapGet("/v1/ledger/staleness", async Task<Results<JsonHttpResult<StalenessResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var thresholdMinutes = ParseInt(httpContext.Request.Query["threshold_minutes"]) ?? 60;
|
||||
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
|
||||
|
||||
var result = await service.CheckStalenessAsync(tenantId, threshold, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return TypedResults.Json(result.ToResponse());
|
||||
})
|
||||
.WithName("CheckStaleness")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Current Point Endpoint
|
||||
app.MapGet("/v1/ledger/current-point", async Task<Results<JsonHttpResult<QueryPointResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
SnapshotService service,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var point = await service.GetCurrentPointAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
||||
return TypedResults.Json(point.ToResponse());
|
||||
})
|
||||
.WithName("GetCurrentPoint")
|
||||
.RequireAuthorization(LedgerExportPolicy)
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.Run();
|
||||
|
||||
static Created<LedgerEventResponse> CreateCreatedResponse(LedgerEventRecord record)
|
||||
@@ -738,3 +1323,8 @@ static bool? ParseBool(string value)
|
||||
{
|
||||
return bool.TryParse(value, out var result) ? result : null;
|
||||
}
|
||||
|
||||
static Guid? ParseGuid(string value)
|
||||
{
|
||||
return Guid.TryParse(value, out var result) ? result : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user