using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.Abstractions; using StellaOps.Scheduler.WebService.Auth; namespace StellaOps.Scheduler.WebService.VulnerabilityResolverJobs; public static class ResolverJobEndpointExtensions { private const string ScopeWrite = StellaOpsScopes.EffectiveWrite; private const string ScopeRead = StellaOpsScopes.FindingsRead; public static void MapResolverJobEndpoints(this IEndpointRouteBuilder builder) { var group = builder.MapGroup("/api/v1/scheduler/vuln/resolver"); group.MapPost("/jobs", CreateJobAsync); group.MapGet("/jobs/{jobId}", GetJobAsync); group.MapGet("/metrics", GetLagMetricsAsync); } internal static async Task CreateJobAsync( [FromBody] ResolverJobRequest request, HttpContext httpContext, [FromServices] ITenantContextAccessor tenantAccessor, [FromServices] IScopeAuthorizer authorizer, [FromServices] IResolverJobService jobService, CancellationToken cancellationToken) { try { authorizer.EnsureScope(httpContext, ScopeWrite); var tenant = tenantAccessor.GetTenant(httpContext); var job = await jobService.CreateAsync(tenant.TenantId, request, cancellationToken).ConfigureAwait(false); return Results.Created($"/api/v1/scheduler/vuln/resolver/jobs/{job.Id}", job); } catch (UnauthorizedAccessException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status401Unauthorized); } catch (InvalidOperationException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status403Forbidden); } catch (ValidationException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status400BadRequest); } } internal static async Task GetJobAsync( string jobId, HttpContext httpContext, [FromServices] ITenantContextAccessor tenantAccessor, [FromServices] IScopeAuthorizer authorizer, [FromServices] IResolverJobService jobService, CancellationToken cancellationToken) { try { authorizer.EnsureScope(httpContext, ScopeRead); var tenant = tenantAccessor.GetTenant(httpContext); var job = await jobService.GetAsync(tenant.TenantId, jobId, cancellationToken).ConfigureAwait(false); return job is null ? Results.NotFound() : Results.Ok(job); } catch (UnauthorizedAccessException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status401Unauthorized); } catch (InvalidOperationException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status403Forbidden); } } internal static IResult GetLagMetricsAsync( HttpContext httpContext, [FromServices] ITenantContextAccessor tenantAccessor, [FromServices] IScopeAuthorizer authorizer, [FromServices] IResolverJobService jobService, [FromServices] IResolverBacklogService backlogService, [FromServices] IResolverBacklogNotifier backlogNotifier) { try { authorizer.EnsureScope(httpContext, ScopeRead); var tenant = tenantAccessor.GetTenant(httpContext); var metrics = jobService.ComputeMetrics(tenant.TenantId); var backlog = backlogService.GetSummary(); backlogNotifier.NotifyIfBreached(metrics with { Pending = (int)backlog.TotalDepth }); return Results.Ok(new { jobs = metrics, backlog }); } catch (UnauthorizedAccessException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status401Unauthorized); } catch (InvalidOperationException ex) { return Results.Json(new { error = ex.Message }, statusCode: StatusCodes.Status403Forbidden); } } }