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(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest); return routes; } private static async Task 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(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; } }