feat(graph-api): Add schema review notes for upcoming Graph API changes
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
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:
@@ -0,0 +1,29 @@
|
||||
namespace StellaOps.Findings.Ledger.WebService.Contracts;
|
||||
|
||||
public sealed record AttestationQueryRequest(
|
||||
string TenantId,
|
||||
string? ArtifactId,
|
||||
string? FindingId,
|
||||
string? AttestationId,
|
||||
string? Status,
|
||||
DateTimeOffset? SinceRecordedAt,
|
||||
DateTimeOffset? UntilRecordedAt,
|
||||
int Limit,
|
||||
string FiltersHash,
|
||||
AttestationPagingKey? PagingKey);
|
||||
|
||||
public sealed record AttestationPagingKey(DateTimeOffset RecordedAt, string AttestationId);
|
||||
|
||||
public sealed record AttestationExportItem(
|
||||
string AttestationId,
|
||||
string ArtifactId,
|
||||
string? FindingId,
|
||||
string VerificationStatus,
|
||||
DateTimeOffset VerificationTime,
|
||||
string DsseDigest,
|
||||
string? RekorEntryId,
|
||||
string? EvidenceBundleRef,
|
||||
string LedgerEventId,
|
||||
DateTimeOffset RecordedAt,
|
||||
string MerkleLeafHash,
|
||||
string RootHash);
|
||||
@@ -17,6 +17,55 @@ public sealed record ExportFindingsRequest(
|
||||
|
||||
public sealed record ExportPagingKey(long SequenceNumber, string PolicyVersion, string CycleHash);
|
||||
|
||||
public sealed record ExportVexRequest(
|
||||
string TenantId,
|
||||
string Shape,
|
||||
long? SinceSequence,
|
||||
long? UntilSequence,
|
||||
DateTimeOffset? SinceObservedAt,
|
||||
DateTimeOffset? UntilObservedAt,
|
||||
string? ProductId,
|
||||
string? AdvisoryId,
|
||||
string? Status,
|
||||
string? StatementType,
|
||||
int PageSize,
|
||||
string FiltersHash,
|
||||
ExportPagingKey? PagingKey);
|
||||
|
||||
public sealed record ExportAdvisoryRequest(
|
||||
string TenantId,
|
||||
string Shape,
|
||||
long? SinceSequence,
|
||||
long? UntilSequence,
|
||||
DateTimeOffset? SinceObservedAt,
|
||||
DateTimeOffset? UntilObservedAt,
|
||||
string? Severity,
|
||||
string? Source,
|
||||
string? CweId,
|
||||
bool? Kev,
|
||||
string? CvssVersion,
|
||||
decimal? CvssScoreMin,
|
||||
decimal? CvssScoreMax,
|
||||
int PageSize,
|
||||
string FiltersHash,
|
||||
ExportPagingKey? PagingKey);
|
||||
|
||||
public sealed record ExportSbomRequest(
|
||||
string TenantId,
|
||||
string Shape,
|
||||
long? SinceSequence,
|
||||
long? UntilSequence,
|
||||
DateTimeOffset? SinceObservedAt,
|
||||
DateTimeOffset? UntilObservedAt,
|
||||
string? SubjectDigest,
|
||||
string? SbomFormat,
|
||||
string? ComponentPurl,
|
||||
bool? ContainsNative,
|
||||
string? SlsaBuildType,
|
||||
int PageSize,
|
||||
string FiltersHash,
|
||||
ExportPagingKey? PagingKey);
|
||||
|
||||
public sealed record FindingExportItem(
|
||||
long EventSequence,
|
||||
DateTimeOffset ObservedAt,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,6 @@ public sealed class ExportQueryService
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public ExportPage<VexExportItem> GetVexEmpty() => new(Array.Empty<VexExportItem>(), null);
|
||||
|
||||
public ExportPage<AdvisoryExportItem> GetAdvisoriesEmpty() => new(Array.Empty<AdvisoryExportItem>(), null);
|
||||
|
||||
public ExportPage<SbomExportItem> GetSbomsEmpty() => new(Array.Empty<SbomExportItem>(), null);
|
||||
|
||||
public int ClampPageSize(int? requested)
|
||||
{
|
||||
if (!requested.HasValue || requested.Value <= 0)
|
||||
@@ -54,6 +48,64 @@ public sealed class ExportQueryService
|
||||
return ExportPaging.ComputeFiltersHash(filters);
|
||||
}
|
||||
|
||||
public string ComputeFiltersHash(ExportVexRequest request)
|
||||
{
|
||||
var filters = new Dictionary<string, string?>
|
||||
{
|
||||
["shape"] = request.Shape,
|
||||
["since_sequence"] = request.SinceSequence?.ToString(),
|
||||
["until_sequence"] = request.UntilSequence?.ToString(),
|
||||
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
|
||||
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
|
||||
["product_id"] = request.ProductId,
|
||||
["advisory_id"] = request.AdvisoryId,
|
||||
["status"] = request.Status,
|
||||
["statement_type"] = request.StatementType
|
||||
};
|
||||
|
||||
return ExportPaging.ComputeFiltersHash(filters);
|
||||
}
|
||||
|
||||
public string ComputeFiltersHash(ExportAdvisoryRequest request)
|
||||
{
|
||||
var filters = new Dictionary<string, string?>
|
||||
{
|
||||
["shape"] = request.Shape,
|
||||
["since_sequence"] = request.SinceSequence?.ToString(),
|
||||
["until_sequence"] = request.UntilSequence?.ToString(),
|
||||
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
|
||||
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
|
||||
["severity"] = request.Severity,
|
||||
["source"] = request.Source,
|
||||
["cwe_id"] = request.CweId,
|
||||
["kev"] = request.Kev?.ToString(),
|
||||
["cvss_version"] = request.CvssVersion,
|
||||
["cvss_score_min"] = request.CvssScoreMin?.ToString(),
|
||||
["cvss_score_max"] = request.CvssScoreMax?.ToString()
|
||||
};
|
||||
|
||||
return ExportPaging.ComputeFiltersHash(filters);
|
||||
}
|
||||
|
||||
public string ComputeFiltersHash(ExportSbomRequest request)
|
||||
{
|
||||
var filters = new Dictionary<string, string?>
|
||||
{
|
||||
["shape"] = request.Shape,
|
||||
["since_sequence"] = request.SinceSequence?.ToString(),
|
||||
["until_sequence"] = request.UntilSequence?.ToString(),
|
||||
["since_observed_at"] = request.SinceObservedAt?.ToString("O"),
|
||||
["until_observed_at"] = request.UntilObservedAt?.ToString("O"),
|
||||
["subject_digest"] = request.SubjectDigest,
|
||||
["sbom_format"] = request.SbomFormat,
|
||||
["component_purl"] = request.ComponentPurl,
|
||||
["contains_native"] = request.ContainsNative?.ToString(),
|
||||
["slsa_build_type"] = request.SlsaBuildType
|
||||
};
|
||||
|
||||
return ExportPaging.ComputeFiltersHash(filters);
|
||||
}
|
||||
|
||||
public async Task<ExportPage<FindingExportItem>> GetFindingsAsync(ExportFindingsRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
@@ -211,4 +263,41 @@ public sealed class ExportQueryService
|
||||
|
||||
return new ExportPage<FindingExportItem>(items, nextPageToken);
|
||||
}
|
||||
|
||||
public Task<ExportPage<VexExportItem>> GetVexAsync(ExportVexRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!string.Equals(request.FiltersHash, ComputeFiltersHash(request), StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("filters_hash_mismatch");
|
||||
}
|
||||
|
||||
// Data source to be implemented; deterministic empty page for now.
|
||||
return Task.FromResult(new ExportPage<VexExportItem>(Array.Empty<VexExportItem>(), null));
|
||||
}
|
||||
|
||||
public Task<ExportPage<AdvisoryExportItem>> GetAdvisoriesAsync(ExportAdvisoryRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!string.Equals(request.FiltersHash, ComputeFiltersHash(request), StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("filters_hash_mismatch");
|
||||
}
|
||||
|
||||
return Task.FromResult(new ExportPage<AdvisoryExportItem>(Array.Empty<AdvisoryExportItem>(), null));
|
||||
}
|
||||
|
||||
public Task<ExportPage<SbomExportItem>> GetSbomsAsync(ExportSbomRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
if (!string.Equals(request.FiltersHash, ComputeFiltersHash(request), StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("filters_hash_mismatch");
|
||||
}
|
||||
|
||||
return Task.FromResult(new ExportPage<SbomExportItem>(Array.Empty<SbomExportItem>(), null));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user