// SPDX-License-Identifier: AGPL-3.0-or-later // Sprint: SPRINT_4100_0004_0001 - Security State Delta & Verdict // Task: T6 - Add Delta API endpoints using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using StellaOps.Auth.Abstractions; using StellaOps.Auth.ServerIntegration; using StellaOps.Policy.Deltas; using StellaOps.Policy.Gateway.Contracts; namespace StellaOps.Policy.Gateway.Endpoints; /// /// Delta API endpoints for Policy Gateway. /// public static class DeltasEndpoints { private const string DeltaCachePrefix = "delta:"; private static readonly TimeSpan DeltaCacheDuration = TimeSpan.FromMinutes(30); /// /// Maps delta endpoints to the application. /// public static void MapDeltasEndpoints(this WebApplication app) { var deltas = app.MapGroup("/api/policy/deltas") .WithTags("Deltas"); // POST /api/policy/deltas/compute - Compute a security state delta deltas.MapPost("/compute", async Task( ComputeDeltaRequest request, IDeltaComputer deltaComputer, IBaselineSelector baselineSelector, IMemoryCache cache, ILogger logger, CancellationToken cancellationToken) => { if (request is null) { return Results.BadRequest(new ProblemDetails { Title = "Request body required", Status = 400 }); } if (string.IsNullOrWhiteSpace(request.ArtifactDigest)) { return Results.BadRequest(new ProblemDetails { Title = "Artifact digest required", Status = 400 }); } if (string.IsNullOrWhiteSpace(request.TargetSnapshotId)) { return Results.BadRequest(new ProblemDetails { Title = "Target snapshot ID required", Status = 400 }); } try { // Select baseline BaselineSelectionResult baselineResult; if (!string.IsNullOrWhiteSpace(request.BaselineSnapshotId)) { baselineResult = await baselineSelector.SelectExplicitAsync( request.BaselineSnapshotId, cancellationToken); } else { var strategy = ParseStrategy(request.BaselineStrategy); baselineResult = await baselineSelector.SelectBaselineAsync( request.ArtifactDigest, strategy, cancellationToken); } if (!baselineResult.IsFound) { return Results.NotFound(new ProblemDetails { Title = "Baseline not found", Status = 404, Detail = baselineResult.Error }); } // Compute delta var delta = await deltaComputer.ComputeDeltaAsync( baselineResult.Snapshot!.SnapshotId, request.TargetSnapshotId, new ArtifactRef( request.ArtifactDigest, request.ArtifactName, request.ArtifactTag), cancellationToken); // Cache the delta for subsequent retrieval cache.Set( DeltaCachePrefix + delta.DeltaId, delta, DeltaCacheDuration); logger.LogInformation( "Computed delta {DeltaId} between {Baseline} and {Target}", delta.DeltaId, delta.BaselineSnapshotId, delta.TargetSnapshotId); return Results.Ok(new ComputeDeltaResponse { DeltaId = delta.DeltaId, BaselineSnapshotId = delta.BaselineSnapshotId, TargetSnapshotId = delta.TargetSnapshotId, ComputedAt = delta.ComputedAt, Summary = DeltaSummaryDto.FromModel(delta.Summary), DriverCount = delta.Drivers.Count }); } catch (InvalidOperationException ex) when (ex.Message.Contains("not found")) { return Results.NotFound(new ProblemDetails { Title = "Snapshot not found", Status = 404, Detail = ex.Message }); } }) .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun)); // GET /api/policy/deltas/{deltaId} - Get a delta by ID deltas.MapGet("/{deltaId}", async Task( string deltaId, IMemoryCache cache, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(deltaId)) { return Results.BadRequest(new ProblemDetails { Title = "Delta ID required", Status = 400 }); } // Try to retrieve from cache if (!cache.TryGetValue(DeltaCachePrefix + deltaId, out SecurityStateDelta? delta) || delta is null) { return Results.NotFound(new ProblemDetails { Title = "Delta not found", Status = 404, Detail = $"No delta found with ID: {deltaId}. Deltas are cached for {DeltaCacheDuration.TotalMinutes} minutes after computation." }); } return Results.Ok(DeltaResponse.FromModel(delta)); }) .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead)); // POST /api/policy/deltas/{deltaId}/evaluate - Evaluate delta and get verdict deltas.MapPost("/{deltaId}/evaluate", async Task( string deltaId, EvaluateDeltaRequest? request, IMemoryCache cache, ILogger logger, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(deltaId)) { return Results.BadRequest(new ProblemDetails { Title = "Delta ID required", Status = 400 }); } // Try to retrieve delta from cache if (!cache.TryGetValue(DeltaCachePrefix + deltaId, out SecurityStateDelta? delta) || delta is null) { return Results.NotFound(new ProblemDetails { Title = "Delta not found", Status = 404, Detail = $"No delta found with ID: {deltaId}" }); } // Build verdict from delta drivers var builder = new DeltaVerdictBuilder(); // Apply risk points based on summary builder.WithRiskPoints((int)delta.Summary.RiskScore); // Categorize drivers as blocking or warning foreach (var driver in delta.Drivers) { if (IsBlockingDriver(driver)) { builder.AddBlockingDriver(driver); } else if (driver.Severity >= DeltaDriverSeverity.Medium) { builder.AddWarningDriver(driver); } } // Apply exceptions if provided if (request?.Exceptions is not null) { foreach (var exceptionId in request.Exceptions) { builder.AddException(exceptionId); } } // Add recommendations based on drivers AddRecommendations(builder, delta.Drivers); var verdict = builder.Build(deltaId); // Cache the verdict cache.Set( DeltaCachePrefix + deltaId + ":verdict", verdict, DeltaCacheDuration); logger.LogInformation( "Evaluated delta {DeltaId}: status={Status}, gate={Gate}", deltaId, verdict.Status, verdict.RecommendedGate); return Results.Ok(DeltaVerdictResponse.FromModel(verdict)); }) .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun)); // GET /api/policy/deltas/{deltaId}/attestation - Get signed attestation deltas.MapGet("/{deltaId}/attestation", async Task( string deltaId, IMemoryCache cache, IDeltaVerdictAttestor? attestor, ILogger logger, CancellationToken cancellationToken) => { if (string.IsNullOrWhiteSpace(deltaId)) { return Results.BadRequest(new ProblemDetails { Title = "Delta ID required", Status = 400 }); } // Try to retrieve delta from cache if (!cache.TryGetValue(DeltaCachePrefix + deltaId, out SecurityStateDelta? delta) || delta is null) { return Results.NotFound(new ProblemDetails { Title = "Delta not found", Status = 404, Detail = $"No delta found with ID: {deltaId}" }); } // Try to retrieve verdict from cache if (!cache.TryGetValue(DeltaCachePrefix + deltaId + ":verdict", out DeltaVerdict? verdict) || verdict is null) { return Results.NotFound(new ProblemDetails { Title = "Verdict not found", Status = 404, Detail = "Delta must be evaluated before attestation can be generated. Call POST /evaluate first." }); } if (attestor is null) { return Results.Problem(new ProblemDetails { Title = "Attestor not configured", Status = 501, Detail = "Delta verdict attestation requires a signer to be configured" }); } try { var envelope = await attestor.AttestAsync(delta, verdict, cancellationToken); logger.LogInformation( "Created attestation for delta {DeltaId} verdict {VerdictId}", deltaId, verdict.VerdictId); return Results.Ok(envelope); } catch (Exception ex) { logger.LogError(ex, "Failed to create attestation for delta {DeltaId}", deltaId); return Results.Problem(new ProblemDetails { Title = "Attestation failed", Status = 500, Detail = "Failed to create signed attestation" }); } }) .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead)); } private static BaselineSelectionStrategy ParseStrategy(string? strategy) { if (string.IsNullOrWhiteSpace(strategy)) return BaselineSelectionStrategy.LastApproved; return strategy.ToLowerInvariant() switch { "previousbuild" or "previous_build" or "previous-build" => BaselineSelectionStrategy.PreviousBuild, "lastapproved" or "last_approved" or "last-approved" => BaselineSelectionStrategy.LastApproved, "productiondeployed" or "production_deployed" or "production-deployed" or "production" => BaselineSelectionStrategy.ProductionDeployed, "branchbase" or "branch_base" or "branch-base" => BaselineSelectionStrategy.BranchBase, _ => BaselineSelectionStrategy.LastApproved }; } private static bool IsBlockingDriver(DeltaDriver driver) { // Block on critical/high severity negative drivers if (driver.Severity is DeltaDriverSeverity.Critical or DeltaDriverSeverity.High) { // These types indicate risk increase return driver.Type is "new-reachable-cve" or "lost-vex-coverage" or "vex-status-downgrade" or "new-policy-violation"; } return false; } private static void AddRecommendations(DeltaVerdictBuilder builder, IReadOnlyList drivers) { var hasReachableCve = drivers.Any(d => d.Type == "new-reachable-cve"); var hasLostVex = drivers.Any(d => d.Type == "lost-vex-coverage"); var hasNewViolation = drivers.Any(d => d.Type == "new-policy-violation"); var hasNewUnknowns = drivers.Any(d => d.Type == "new-unknowns"); if (hasReachableCve) { builder.AddRecommendation("Review new reachable CVEs and apply VEX statements or patches"); } if (hasLostVex) { builder.AddRecommendation("Investigate lost VEX coverage - statements may have expired or been revoked"); } if (hasNewViolation) { builder.AddRecommendation("Address policy violations or request exceptions"); } if (hasNewUnknowns) { builder.AddRecommendation("Investigate new unknown packages - consider adding SBOM metadata"); } } }