up
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy.Engine.BatchEvaluation;
|
||||
using StellaOps.Policy.Engine.Services;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
internal static class BatchEvaluationEndpoint
|
||||
{
|
||||
public static IEndpointRouteBuilder MapBatchEvaluation(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/policy/eval")
|
||||
.RequireAuthorization()
|
||||
.WithTags("Policy Evaluation");
|
||||
|
||||
group.MapPost("/batch", EvaluateBatchAsync)
|
||||
.WithName("PolicyEngine.BatchEvaluate")
|
||||
.WithSummary("Batch-evaluate policy packs against advisory/VEX/SBOM tuples with deterministic ordering and cache-aware responses.")
|
||||
.Produces<BatchEvaluationResponseDto>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static async Task<IResult> EvaluateBatchAsync(
|
||||
HttpContext httpContext,
|
||||
[FromBody] BatchEvaluationRequestDto request,
|
||||
IRuntimeEvaluationExecutor evaluator,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var scopeResult = ScopeAuthorization.RequireScope(httpContext, StellaOpsScopes.PolicyRead);
|
||||
if (scopeResult is not null)
|
||||
{
|
||||
return scopeResult;
|
||||
}
|
||||
|
||||
if (!BatchEvaluationValidator.TryValidate(request, out var error))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid request",
|
||||
Detail = error,
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
if (!TryParseOffset(request.PageToken, out var offset))
|
||||
{
|
||||
return Results.BadRequest(new ProblemDetails
|
||||
{
|
||||
Title = "Invalid pageToken",
|
||||
Detail = "pageToken must be a non-negative integer offset.",
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
});
|
||||
}
|
||||
|
||||
var pageSize = Math.Clamp(request.PageSize ?? 100, 1, 500);
|
||||
var budgetMs = request.BudgetMs;
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var pageItems = request.Items
|
||||
.Skip(offset)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
var runtimeRequests = BatchEvaluationMapper.ToRuntimeRequests(request.TenantId, pageItems);
|
||||
|
||||
var results = new List<BatchEvaluationResultDto>(runtimeRequests.Count);
|
||||
var cacheHits = 0;
|
||||
var cacheMisses = 0;
|
||||
var processed = 0;
|
||||
|
||||
foreach (var runtimeRequest in runtimeRequests)
|
||||
{
|
||||
if (budgetMs is int budget && sw.ElapsedMilliseconds >= budget)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var response = await evaluator.EvaluateAsync(runtimeRequest, cancellationToken).ConfigureAwait(false);
|
||||
processed++;
|
||||
|
||||
if (response.Cached)
|
||||
{
|
||||
cacheHits++;
|
||||
}
|
||||
else
|
||||
{
|
||||
cacheMisses++;
|
||||
}
|
||||
|
||||
results.Add(new BatchEvaluationResultDto(
|
||||
response.PackId,
|
||||
response.Version,
|
||||
response.PolicyDigest,
|
||||
response.Status,
|
||||
response.Severity,
|
||||
response.RuleName,
|
||||
response.Priority,
|
||||
response.Annotations,
|
||||
response.Warnings,
|
||||
response.AppliedException,
|
||||
response.CorrelationId,
|
||||
response.Cached,
|
||||
response.CacheSource,
|
||||
response.EvaluationDurationMs));
|
||||
}
|
||||
|
||||
var nextOffset = offset + processed;
|
||||
string? nextPageToken = null;
|
||||
if (nextOffset < request.Items.Count)
|
||||
{
|
||||
nextPageToken = nextOffset.ToString();
|
||||
}
|
||||
|
||||
var budgetRemaining = budgetMs is int budgetValue
|
||||
? Math.Max(0, budgetValue - sw.ElapsedMilliseconds)
|
||||
: (long?)null;
|
||||
|
||||
var responsePayload = new BatchEvaluationResponseDto(
|
||||
Results: results,
|
||||
NextPageToken: nextPageToken,
|
||||
Total: request.Items.Count,
|
||||
Returned: processed,
|
||||
CacheHits: cacheHits,
|
||||
CacheMisses: cacheMisses,
|
||||
DurationMs: sw.ElapsedMilliseconds,
|
||||
BudgetRemainingMs: budgetRemaining);
|
||||
|
||||
return Results.Ok(responsePayload);
|
||||
}
|
||||
|
||||
private static bool TryParseOffset(string? token, out int offset)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
offset = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return int.TryParse(token, out offset) && offset >= 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user