using Microsoft.AspNetCore.Mvc; using System.Linq; using StellaOps.RiskEngine.Core.Contracts; using StellaOps.RiskEngine.Core.Providers; using StellaOps.RiskEngine.Core.Services; using StellaOps.RiskEngine.Infrastructure.Stores; using StellaOps.Router.AspNet; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(_ => new RiskScoreProviderRegistry(new IRiskScoreProvider[] { new DefaultTransformsProvider(), new CvssKevProvider(new NullCvssSource(), new NullKevSource()), new VexGateProvider(), new FixExposureProvider() })); // Stella Router integration var routerOptions = builder.Configuration.GetSection("RiskEngine:Router").Get(); builder.Services.TryAddStellaRouter( serviceName: "riskengine", version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0", routerOptions: routerOptions); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } app.UseHttpsRedirection(); app.TryUseStellaRouter(routerOptions); app.MapGet("/risk-scores/providers", (IRiskScoreProviderRegistry registry) => Results.Ok(new { providers = registry.ProviderNames.OrderBy(n => n, StringComparer.OrdinalIgnoreCase) })); app.MapPost("/risk-scores/jobs", async ( ScoreRequest request, [FromServices] RiskScoreQueue queue, [FromServices] IRiskScoreProviderRegistry registry, [FromServices] IRiskScoreResultStore store, CancellationToken ct) => { var normalized = new ScoreRequest( request.Provider, request.Subject, request.Signals ?? new Dictionary()); var jobId = await queue.EnqueueWithIdAsync(normalized, ct).ConfigureAwait(false); var worker = new RiskScoreWorker(queue, registry, store, TimeProvider.System); var result = await worker.ProcessNextAsync(ct).ConfigureAwait(false); return Results.Accepted($"/risk-scores/jobs/{jobId}", new { jobId, result }); }); app.MapGet("/risk-scores/jobs/{jobId:guid}", ( Guid jobId, [FromServices] IRiskScoreResultStore store) => store.TryGet(jobId, out var result) ? Results.Ok(result) : Results.NotFound()); app.MapPost("/risk-scores/simulations", async ( IReadOnlyCollection requests, [FromServices] IRiskScoreProviderRegistry registry, CancellationToken ct) => { var results = await EvaluateAsync(requests, registry, ct).ConfigureAwait(false); return Results.Ok(new { results }); }); app.MapPost("/risk-scores/simulations/summary", async ( IReadOnlyCollection requests, [FromServices] IRiskScoreProviderRegistry registry, CancellationToken ct) => { var results = await EvaluateAsync(requests, registry, ct).ConfigureAwait(false); var scores = results.Select(r => r.Score).ToArray(); var summary = new { averageScore = scores.Length == 0 ? 0d : scores.Average(), minScore = scores.Length == 0 ? 0d : scores.Min(), maxScore = scores.Length == 0 ? 0d : scores.Max(), topMovers = results .OrderByDescending(r => r.Score) .ThenBy(r => r.Subject, StringComparer.Ordinal) .Take(3) .ToArray() }; return Results.Ok(new { summary, results }); }); // Refresh Router endpoint cache app.TryRefreshStellaRouterEndpoints(routerOptions); app.Run(); static async Task> EvaluateAsync( IReadOnlyCollection requests, IRiskScoreProviderRegistry registry, CancellationToken ct) { var results = new List(requests.Count); foreach (var req in requests) { var normalized = new ScoreRequest( req.Provider, req.Subject, req.Signals ?? new Dictionary()); if (!registry.TryGet(normalized.Provider, out var provider)) { results.Add(new RiskScoreResult(Guid.NewGuid(), normalized.Provider, normalized.Subject, 0d, false, "Provider not registered", normalized.Signals, TimeProvider.System.GetUtcNow())); continue; } try { var score = await provider.ScoreAsync(normalized, ct).ConfigureAwait(false); results.Add(new RiskScoreResult(Guid.NewGuid(), normalized.Provider, normalized.Subject, score, true, null, normalized.Signals, TimeProvider.System.GetUtcNow())); } catch (Exception ex) { results.Add(new RiskScoreResult(Guid.NewGuid(), normalized.Provider, normalized.Subject, 0d, false, ex.Message, normalized.Signals, TimeProvider.System.GetUtcNow())); } } return results; }