148 lines
4.7 KiB
C#
148 lines
4.7 KiB
C#
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;
|
|
}
|
|
}
|