// ----------------------------------------------------------------------------- // RuntimeTracesEndpoints.cs // Sprint: SPRINT_20260107_006_002_FE_diff_runtime_tabs // Task: DR-014 — Runtime traces API endpoints // ----------------------------------------------------------------------------- using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.Findings.Ledger.WebService.Contracts; namespace StellaOps.Findings.Ledger.WebService.Endpoints; /// /// API endpoints for runtime traces evidence. /// public static class RuntimeTracesEndpoints { /// /// Maps runtime traces endpoints to the application. /// public static void MapRuntimeTracesEndpoints(this WebApplication app) { var group = app.MapGroup("/api/v1/findings") .WithTags("Runtime Evidence") .RequireAuthorization("scoring.read") .RequireTenant(); // POST /api/v1/findings/{findingId}/runtime/traces group.MapPost("/{findingId:guid}/runtime/traces", IngestRuntimeTrace) .WithName("IngestRuntimeTrace") .WithSummary("Ingest runtime trace observation for a finding") .WithDescription("Accepts a runtime trace observation from an eBPF or APM agent, recording which function frames were observed executing within a vulnerable component at runtime. Requires artifact digest and component PURL for cross-referencing. Returns 202 Accepted; the trace is processed asynchronously.") .Accepts("application/json") .Produces(202) .ProducesValidationProblem() .RequireAuthorization("ledger.events.write"); // GET /api/v1/findings/{findingId}/runtime/traces group.MapGet("/{findingId:guid}/runtime/traces", GetRuntimeTraces) .WithName("GetRuntimeTraces") .WithSummary("Get runtime function traces for a finding") .WithDescription("Returns the aggregated runtime function traces recorded for a finding, sorted by hit count or recency. Each trace entry includes the function frame, hit count, artifact digest, and component PURL for cross-referencing with SBOM data.") .Produces(200) .Produces(404) .RequireAuthorization("scoring.read"); // GET /api/v1/findings/{findingId}/runtime/score group.MapGet("/{findingId:guid}/runtime/score", GetRtsScore) .WithName("GetRtsScore") .WithSummary("Get Runtime Trustworthiness Score for a finding") .WithDescription("Returns the Runtime Trustworthiness Score (RTS) for a finding, derived from observed runtime trace density and recency. A higher RTS indicates that the vulnerable code path has been recently and frequently exercised in production, increasing remediation priority.") .Produces(200) .Produces(404) .RequireAuthorization("scoring.read"); } /// /// Ingests a runtime trace observation. /// private static async Task, ValidationProblem>> IngestRuntimeTrace( Guid findingId, RuntimeTraceIngestRequest request, IRuntimeTracesService service, IStellaOpsTenantAccessor tenantAccessor, CancellationToken ct) { var errors = new Dictionary(); if (request.Frames is null || request.Frames.Count == 0) { errors["frames"] = ["At least one frame is required."]; } if (string.IsNullOrWhiteSpace(request.ArtifactDigest)) { errors["artifactDigest"] = ["Artifact digest is required."]; } if (string.IsNullOrWhiteSpace(request.ComponentPurl)) { errors["componentPurl"] = ["Component purl is required."]; } if (errors.Count > 0) { return TypedResults.ValidationProblem(errors); } var response = await service.IngestTraceAsync(findingId, request, ct); return TypedResults.Accepted($"/api/v1/findings/{findingId}/runtime/traces", response); } /// /// Gets runtime function traces for a finding. /// private static async Task, NotFound>> GetRuntimeTraces( Guid findingId, IRuntimeTracesService service, IStellaOpsTenantAccessor tenantAccessor, CancellationToken ct, [FromQuery] int? limit = null, [FromQuery] string? sortBy = null) { var options = new RuntimeTracesQueryOptions { Limit = limit ?? 50, SortBy = sortBy ?? "hits" }; var traces = await service.GetTracesAsync(findingId, options, ct); return traces is not null ? TypedResults.Ok(traces) : TypedResults.NotFound(); } /// /// Gets the RTS score for a finding. /// private static async Task, NotFound>> GetRtsScore( Guid findingId, IRuntimeTracesService service, IStellaOpsTenantAccessor tenantAccessor, CancellationToken ct) { var score = await service.GetRtsScoreAsync(findingId, ct); return score is not null ? TypedResults.Ok(score) : TypedResults.NotFound(); } } /// /// Query options for runtime traces. /// public sealed record RuntimeTracesQueryOptions { /// /// Maximum number of traces to return. /// public int Limit { get; init; } = 50; /// /// Sort by field (hits, recent). /// public string SortBy { get; init; } = "hits"; } /// /// Service for retrieving runtime traces. /// public interface IRuntimeTracesService { /// /// Ingests a runtime trace observation for a finding. /// Task IngestTraceAsync( Guid findingId, RuntimeTraceIngestRequest request, CancellationToken ct); /// /// Gets runtime traces for a finding. /// /// Finding identifier. /// Query options. /// Cancellation token. /// Runtime traces response or null if not found. Task GetTracesAsync( Guid findingId, RuntimeTracesQueryOptions options, CancellationToken ct); /// /// Gets RTS score for a finding. /// /// Finding identifier. /// Cancellation token. /// RTS score response or null if not found. Task GetRtsScoreAsync(Guid findingId, CancellationToken ct); }