feat(graph-api): Add schema review notes for upcoming Graph API changes
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

feat(sbomservice): Add placeholder for SHA256SUMS in LNM v1 fixtures

docs(devportal): Create README for SDK archives in public directory

build(devportal): Implement offline bundle build script

test(devportal): Add link checker script for validating links in documentation

test(devportal): Create performance check script for dist folder size

test(devportal): Implement accessibility check script using Playwright and Axe

docs(devportal): Add SDK quickstart guide with examples for Node.js, Python, and cURL

feat(excititor): Implement MongoDB storage for airgap import records

test(findings): Add unit tests for export filters hash determinism

feat(findings): Define attestation contracts for ledger web service

feat(graph): Add MongoDB options and service collection extensions for graph indexing

test(graph): Implement integration tests for MongoDB provider and service collection extensions

feat(zastava): Define configuration options for Zastava surface secrets

build(tests): Create script to run Concelier linkset tests with TRX output
This commit is contained in:
StellaOps Bot
2025-11-22 19:22:30 +02:00
parent ca09400069
commit 48702191be
76 changed files with 3878 additions and 1081 deletions

View File

@@ -151,6 +151,7 @@ builder.Services.AddSingleton<IConsoleCsrfValidator, ConsoleCsrfValidator>();
builder.Services.AddHostedService<LedgerMerkleAnchorWorker>();
builder.Services.AddHostedService<LedgerProjectionWorker>();
builder.Services.AddSingleton<ExportQueryService>();
builder.Services.AddSingleton<AttestationQueryService>();
var app = builder.Build();
@@ -290,20 +291,255 @@ app.MapGet("/ledger/export/findings", async Task<Results<FileStreamHttpResult, J
.ProducesProblem(StatusCodes.Status403Forbidden)
.ProducesProblem(StatusCodes.Status500InternalServerError);
app.MapGet("/ledger/export/vex", () => TypedResults.Json(new ExportPage<VexExportItem>(Array.Empty<VexExportItem>(), null)))
app.MapGet("/v1/ledger/attestations", async Task<Results<FileStreamHttpResult, JsonHttpResult<ExportPage<AttestationExportItem>>, ProblemHttpResult>> (
HttpContext httpContext,
AttestationQueryService attestationQueryService,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var request = new AttestationQueryRequest(
tenantId,
httpContext.Request.Query["artifactId"].ToString(),
httpContext.Request.Query["findingId"].ToString(),
httpContext.Request.Query["attestationId"].ToString(),
httpContext.Request.Query["status"].ToString(),
ParseDate(httpContext.Request.Query["sinceRecordedAt"]),
ParseDate(httpContext.Request.Query["untilRecordedAt"]),
attestationQueryService.ClampLimit(ParseInt(httpContext.Request.Query["limit"])),
FiltersHash: string.Empty,
PagingKey: null);
var filtersHash = attestationQueryService.ComputeFiltersHash(request);
AttestationPagingKey? pagingKey = null;
var pageToken = httpContext.Request.Query["page_token"].ToString();
if (!string.IsNullOrWhiteSpace(pageToken))
{
if (!attestationQueryService.TryParsePageToken(pageToken, filtersHash, out pagingKey, out var error))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: error ?? "invalid_page_token");
}
}
request = request with { FiltersHash = filtersHash, PagingKey = pagingKey };
ExportPage<AttestationExportItem> page;
try
{
page = await attestationQueryService.GetAttestationsAsync(request, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException ex) when (ex.Message == "filters_hash_mismatch")
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "page_token_filters_mismatch");
}
return await WritePagedResponse(httpContext, page, cancellationToken).ConfigureAwait(false);
})
.WithName("LedgerAttestationsList")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/ledger/export/vex", async Task<Results<FileStreamHttpResult, JsonHttpResult<ExportPage<VexExportItem>>, ProblemHttpResult>> (
HttpContext httpContext,
ExportQueryService exportQueryService,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var shape = httpContext.Request.Query["shape"].ToString();
if (string.IsNullOrWhiteSpace(shape))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_shape", detail: "shape is required (canonical|compact).");
}
var request = new ExportVexRequest(
tenantId,
shape,
ParseLong(httpContext.Request.Query["since_sequence"]),
ParseLong(httpContext.Request.Query["until_sequence"]),
ParseDate(httpContext.Request.Query["since_observed_at"]),
ParseDate(httpContext.Request.Query["until_observed_at"]),
httpContext.Request.Query["product_id"].ToString(),
httpContext.Request.Query["advisory_id"].ToString(),
httpContext.Request.Query["status"].ToString(),
httpContext.Request.Query["statement_type"].ToString(),
exportQueryService.ClampPageSize(ParseInt(httpContext.Request.Query["page_size"])),
filtersHash: string.Empty,
PagingKey: null);
var filtersHash = exportQueryService.ComputeFiltersHash(request);
ExportPagingKey? pagingKey = null;
var pageToken = httpContext.Request.Query["page_token"].ToString();
if (!string.IsNullOrWhiteSpace(pageToken))
{
if (!ExportPaging.TryParsePageToken(pageToken, filtersHash, out var parsedKey, out var error))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: error ?? "invalid_page_token");
}
pagingKey = new ExportPagingKey(parsedKey!.SequenceNumber, parsedKey.PolicyVersion, parsedKey.CycleHash);
}
request = request with { FiltersHash = filtersHash, PagingKey = pagingKey };
ExportPage<VexExportItem> page;
try
{
page = await exportQueryService.GetVexAsync(request, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException ex) when (ex.Message == "filters_hash_mismatch")
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "page_token_filters_mismatch");
}
return await WritePagedResponse(httpContext, page, cancellationToken).ConfigureAwait(false);
})
.WithName("LedgerExportVex")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK);
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/ledger/export/advisories", () => TypedResults.Json(new ExportPage<AdvisoryExportItem>(Array.Empty<AdvisoryExportItem>(), null)))
app.MapGet("/ledger/export/advisories", async Task<Results<FileStreamHttpResult, JsonHttpResult<ExportPage<AdvisoryExportItem>>, ProblemHttpResult>> (
HttpContext httpContext,
ExportQueryService exportQueryService,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var shape = httpContext.Request.Query["shape"].ToString();
if (string.IsNullOrWhiteSpace(shape))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_shape", detail: "shape is required (canonical|compact).");
}
var kev = ParseBool(httpContext.Request.Query["kev"]);
var cvssScoreMin = ParseDecimal(httpContext.Request.Query["cvss_score_min"]);
var cvssScoreMax = ParseDecimal(httpContext.Request.Query["cvss_score_max"]);
var request = new ExportAdvisoryRequest(
tenantId,
shape,
ParseLong(httpContext.Request.Query["since_sequence"]),
ParseLong(httpContext.Request.Query["until_sequence"]),
ParseDate(httpContext.Request.Query["since_observed_at"]),
ParseDate(httpContext.Request.Query["until_observed_at"]),
httpContext.Request.Query["severity"].ToString(),
httpContext.Request.Query["source"].ToString(),
httpContext.Request.Query["cwe_id"].ToString(),
kev,
httpContext.Request.Query["cvss_version"].ToString(),
cvssScoreMin,
cvssScoreMax,
exportQueryService.ClampPageSize(ParseInt(httpContext.Request.Query["page_size"])),
filtersHash: string.Empty,
PagingKey: null);
var filtersHash = exportQueryService.ComputeFiltersHash(request);
ExportPagingKey? pagingKey = null;
var pageToken = httpContext.Request.Query["page_token"].ToString();
if (!string.IsNullOrWhiteSpace(pageToken))
{
if (!ExportPaging.TryParsePageToken(pageToken, filtersHash, out var parsedKey, out var error))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: error ?? "invalid_page_token");
}
pagingKey = new ExportPagingKey(parsedKey!.SequenceNumber, parsedKey.PolicyVersion, parsedKey.CycleHash);
}
request = request with { FiltersHash = filtersHash, PagingKey = pagingKey };
ExportPage<AdvisoryExportItem> page;
try
{
page = await exportQueryService.GetAdvisoriesAsync(request, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException ex) when (ex.Message == "filters_hash_mismatch")
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "page_token_filters_mismatch");
}
return await WritePagedResponse(httpContext, page, cancellationToken).ConfigureAwait(false);
})
.WithName("LedgerExportAdvisories")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK);
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapGet("/ledger/export/sboms", () => TypedResults.Json(new ExportPage<SbomExportItem>(Array.Empty<SbomExportItem>(), null)))
app.MapGet("/ledger/export/sboms", async Task<Results<FileStreamHttpResult, JsonHttpResult<ExportPage<SbomExportItem>>, ProblemHttpResult>> (
HttpContext httpContext,
ExportQueryService exportQueryService,
CancellationToken cancellationToken) =>
{
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
{
return tenantProblem!;
}
var shape = httpContext.Request.Query["shape"].ToString();
if (string.IsNullOrWhiteSpace(shape))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_shape", detail: "shape is required (canonical|compact).");
}
var request = new ExportSbomRequest(
tenantId,
shape,
ParseLong(httpContext.Request.Query["since_sequence"]),
ParseLong(httpContext.Request.Query["until_sequence"]),
ParseDate(httpContext.Request.Query["since_observed_at"]),
ParseDate(httpContext.Request.Query["until_observed_at"]),
httpContext.Request.Query["subject_digest"].ToString(),
httpContext.Request.Query["sbom_format"].ToString(),
httpContext.Request.Query["component_purl"].ToString(),
ParseBool(httpContext.Request.Query["contains_native"]),
httpContext.Request.Query["slsa_build_type"].ToString(),
exportQueryService.ClampPageSize(ParseInt(httpContext.Request.Query["page_size"])),
filtersHash: string.Empty,
PagingKey: null);
var filtersHash = exportQueryService.ComputeFiltersHash(request);
ExportPagingKey? pagingKey = null;
var pageToken = httpContext.Request.Query["page_token"].ToString();
if (!string.IsNullOrWhiteSpace(pageToken))
{
if (!ExportPaging.TryParsePageToken(pageToken, filtersHash, out var parsedKey, out var error))
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: error ?? "invalid_page_token");
}
pagingKey = new ExportPagingKey(parsedKey!.SequenceNumber, parsedKey.PolicyVersion, parsedKey.CycleHash);
}
request = request with { FiltersHash = filtersHash, PagingKey = pagingKey };
ExportPage<SbomExportItem> page;
try
{
page = await exportQueryService.GetSbomsAsync(request, cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException ex) when (ex.Message == "filters_hash_mismatch")
{
return TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "page_token_filters_mismatch");
}
return await WritePagedResponse(httpContext, page, cancellationToken).ConfigureAwait(false);
})
.WithName("LedgerExportSboms")
.RequireAuthorization(LedgerExportPolicy)
.Produces(StatusCodes.Status200OK);
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);
app.MapPost("/internal/ledger/orchestrator-export", async Task<Results<Accepted<OrchestratorExportResponse>, ProblemHttpResult>> (
HttpContext httpContext,
@@ -394,6 +630,22 @@ app.MapPost("/internal/ledger/airgap-import", async Task<Results<Accepted<Airgap
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status409Conflict);
app.MapGet("/.well-known/openapi", () =>
{
var contentRoot = AppContext.BaseDirectory;
var candidate = Path.GetFullPath(Path.Combine(contentRoot, "../../docs/modules/findings-ledger/openapi/findings-ledger.v1.yaml"));
if (!File.Exists(candidate))
{
return Results.Problem(statusCode: StatusCodes.Status500InternalServerError, title: "openapi_missing", detail: "OpenAPI document not found on server.");
}
var yaml = File.ReadAllText(candidate);
return Results.Text(yaml, "application/yaml");
})
.WithName("LedgerOpenApiDocument")
.Produces(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError);
app.Run();
static Created<LedgerEventResponse> CreateCreatedResponse(LedgerEventRecord record)
@@ -444,3 +696,42 @@ static async Task<Results<FileStreamHttpResult, JsonHttpResult<ExportPage<T>>, P
return TypedResults.Json(page);
}
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
{
tenantId = string.Empty;
if (!httpContext.Request.Headers.TryGetValue("X-Stella-Tenant", out var tenantValues) || string.IsNullOrWhiteSpace(tenantValues))
{
problem = TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_tenant");
return false;
}
tenantId = tenantValues.ToString();
problem = null;
return true;
}
static int? ParseInt(string value)
{
return int.TryParse(value, out var result) ? result : null;
}
static long? ParseLong(string value)
{
return long.TryParse(value, out var result) ? result : null;
}
static DateTimeOffset? ParseDate(string value)
{
return DateTimeOffset.TryParse(value, out var result) ? result : null;
}
static decimal? ParseDecimal(string value)
{
return decimal.TryParse(value, out var result) ? result : null;
}
static bool? ParseBool(string value)
{
return bool.TryParse(value, out var result) ? result : null;
}