Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Engine/Endpoints/BatchEvaluationEndpoint.cs
StellaOps Bot 909d9b6220
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
up
2025-12-01 21:16:22 +02:00

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;
}
}