wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
@@ -132,7 +132,9 @@ public static class DeltasEndpoints
|
||||
});
|
||||
}
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun))
|
||||
.WithName("ComputeDelta")
|
||||
.WithDescription("Compute a security state delta between a baseline snapshot and a target snapshot for a given artifact digest. Selects the baseline automatically using the configured strategy (last-approved, previous-build, production-deployed, or branch-base) unless an explicit baseline snapshot ID is provided. Returns a delta summary and driver count for downstream evaluation.");
|
||||
|
||||
// GET /api/policy/deltas/{deltaId} - Get a delta by ID
|
||||
deltas.MapGet("/{deltaId}", async Task<IResult>(
|
||||
@@ -162,7 +164,9 @@ public static class DeltasEndpoints
|
||||
|
||||
return Results.Ok(DeltaResponse.FromModel(delta));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetDelta")
|
||||
.WithDescription("Retrieve a previously computed security state delta by its ID from the in-memory cache. Deltas are retained for 30 minutes after computation. Returns the full driver list, baseline and target snapshot IDs, and risk summary.");
|
||||
|
||||
// POST /api/policy/deltas/{deltaId}/evaluate - Evaluate delta and get verdict
|
||||
deltas.MapPost("/{deltaId}/evaluate", async Task<IResult>(
|
||||
@@ -237,7 +241,9 @@ public static class DeltasEndpoints
|
||||
|
||||
return Results.Ok(DeltaVerdictResponse.FromModel(verdict));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun))
|
||||
.WithName("EvaluateDelta")
|
||||
.WithDescription("Evaluate a previously computed delta and produce a gate verdict. Classifies each driver as blocking or advisory based on severity and type (new-reachable-cve, lost-vex-coverage, new-policy-violation), applies any supplied exception IDs, and returns a recommended gate action with risk points and remediation recommendations.");
|
||||
|
||||
// GET /api/policy/deltas/{deltaId}/attestation - Get signed attestation
|
||||
deltas.MapGet("/{deltaId}/attestation", async Task<IResult>(
|
||||
@@ -309,7 +315,9 @@ public static class DeltasEndpoints
|
||||
});
|
||||
}
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetDeltaAttestation")
|
||||
.WithDescription("Retrieve a signed attestation envelope for a delta verdict, combining the security state delta with its evaluated verdict and producing a cryptographically signed in-toto statement. Requires that the delta has been evaluated via POST /{deltaId}/evaluate before an attestation can be generated.");
|
||||
}
|
||||
|
||||
private static BaselineSelectionStrategy ParseStrategy(string? strategy)
|
||||
|
||||
@@ -30,55 +30,55 @@ public static class ExceptionApprovalEndpoints
|
||||
exceptions.MapPost("/request", CreateApprovalRequestAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsRequest))
|
||||
.WithName("CreateExceptionApprovalRequest")
|
||||
.WithDescription("Create a new exception approval request");
|
||||
.WithDescription("Create a new exception approval request for a vulnerability, policy rule, PURL pattern, or artifact digest. Validates the requested TTL against the gate-level maximum, enforces approval rules for the gate level, and optionally auto-approves low-risk requests that meet the configured criteria. The request enters the pending state and is routed to configured approvers.");
|
||||
|
||||
// GET /api/v1/policy/exception/request/{requestId} - Get an approval request
|
||||
exceptions.MapGet("/request/{requestId}", GetApprovalRequestAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsRead))
|
||||
.WithName("GetExceptionApprovalRequest")
|
||||
.WithDescription("Get an exception approval request by ID");
|
||||
.WithDescription("Retrieve the full details of a specific exception approval request by its request ID, including status, gate level, approval progress (approved count vs required count), scope, lifecycle timestamps, and any validation warnings from the creation step.");
|
||||
|
||||
// GET /api/v1/policy/exception/requests - List approval requests
|
||||
exceptions.MapGet("/requests", ListApprovalRequestsAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsRead))
|
||||
.WithName("ListExceptionApprovalRequests")
|
||||
.WithDescription("List exception approval requests for the tenant");
|
||||
.WithDescription("List exception approval requests for the tenant with optional status filtering and pagination. Returns summary DTOs with request ID, status, gate level, requester, vulnerability or PURL scope, reason code, and approval progress. Used by governance dashboards and approval queue UIs.");
|
||||
|
||||
// GET /api/v1/policy/exception/pending - List pending approvals for current user
|
||||
exceptions.MapGet("/pending", ListPendingApprovalsAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsApprove))
|
||||
.WithName("ListPendingApprovals")
|
||||
.WithDescription("List pending exception approvals for the current user");
|
||||
.WithDescription("List exception approval requests that are currently pending and require action from the authenticated approver. Used to drive approver inbox views and notification counts, returning only requests where the calling user is listed as a required approver and has not yet recorded an approval.");
|
||||
|
||||
// POST /api/v1/policy/exception/{requestId}/approve - Approve an exception request
|
||||
exceptions.MapPost("/{requestId}/approve", ApproveRequestAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsApprove))
|
||||
.WithName("ApproveExceptionRequest")
|
||||
.WithDescription("Approve an exception request");
|
||||
.WithDescription("Record an approval action for a pending exception request. Validates that the approver is authorized at the request's gate level, records the approver's identity, and optionally captures a comment. When sufficient approvers have acted, the request transitions to the approved state and the approval workflow is considered complete.");
|
||||
|
||||
// POST /api/v1/policy/exception/{requestId}/reject - Reject an exception request
|
||||
exceptions.MapPost("/{requestId}/reject", RejectRequestAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsApprove))
|
||||
.WithName("RejectExceptionRequest")
|
||||
.WithDescription("Reject an exception request with a reason");
|
||||
.WithDescription("Reject a pending or partially-approved exception request, providing a mandatory reason that is recorded in the audit trail. Transitions the request to the rejected terminal state, preventing further approval actions. The rejection reason is surfaced to the requester for remediation guidance.");
|
||||
|
||||
// POST /api/v1/policy/exception/{requestId}/cancel - Cancel an exception request
|
||||
exceptions.MapPost("/{requestId}/cancel", CancelRequestAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsRequest))
|
||||
.WithName("CancelExceptionRequest")
|
||||
.WithDescription("Cancel an exception request (requestor only)");
|
||||
.WithDescription("Cancel an open exception approval request, accessible only to the original requester. Enforces ownership by comparing the authenticated actor against the stored requestor ID. Returns HTTP 403 when called by a non-owner and HTTP 400 when the request is already in a terminal state.");
|
||||
|
||||
// GET /api/v1/policy/exception/{requestId}/audit - Get audit trail for a request
|
||||
exceptions.MapGet("/{requestId}/audit", GetAuditTrailAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsRead))
|
||||
.WithName("GetExceptionApprovalAudit")
|
||||
.WithDescription("Get the audit trail for an exception approval request");
|
||||
.WithDescription("Retrieve the ordered audit trail for an exception approval request, returning all recorded lifecycle events with sequence numbers, actor IDs, status transitions, and descriptive entries. Used for compliance reporting and post-incident review of the approval workflow.");
|
||||
|
||||
// GET /api/v1/policy/exception/rules - Get approval rules for the tenant
|
||||
exceptions.MapGet("/rules", GetApprovalRulesAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.ExceptionsRead))
|
||||
.WithName("GetExceptionApprovalRules")
|
||||
.WithDescription("Get exception approval rules for the tenant");
|
||||
.WithDescription("Retrieve the exception approval rules configured for the tenant, including per-gate-level minimum approver counts, required approver roles, maximum TTL days, self-approval policy, and evidence and compensating-control requirements. Used by policy authoring tools to display approval requirements to requestors before submission.");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
@@ -64,7 +64,9 @@ public static class ExceptionEndpoints
|
||||
Limit = filter.Limit
|
||||
});
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("ListExceptions")
|
||||
.WithDescription("List policy exceptions with optional filtering by status, type, vulnerability ID, PURL pattern, environment, or owner. Returns paginated results including per-item status, scope, and lifecycle timestamps for exception management dashboards and compliance reporting.");
|
||||
|
||||
// GET /api/policy/exceptions/counts - Get exception counts
|
||||
exceptions.MapGet("/counts", async Task<IResult>(
|
||||
@@ -83,7 +85,9 @@ public static class ExceptionEndpoints
|
||||
ExpiringSoon = counts.ExpiringSoon
|
||||
});
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetExceptionCounts")
|
||||
.WithDescription("Return aggregate counts of exceptions by lifecycle status (proposed, approved, active, expired, revoked) plus an expiring-soon indicator. Used by governance dashboards to give operators a quick view of the exception portfolio health without fetching the full list.");
|
||||
|
||||
// GET /api/policy/exceptions/{id} - Get exception by ID
|
||||
exceptions.MapGet("/{id}", async Task<IResult>(
|
||||
@@ -103,7 +107,9 @@ public static class ExceptionEndpoints
|
||||
}
|
||||
return Results.Ok(ToDto(exception));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetException")
|
||||
.WithDescription("Retrieve the full details of a single exception by its identifier, including scope, rationale, evidence references, compensating controls, and lifecycle timestamps.");
|
||||
|
||||
// GET /api/policy/exceptions/{id}/history - Get exception history
|
||||
exceptions.MapGet("/{id}/history", async Task<IResult>(
|
||||
@@ -128,7 +134,9 @@ public static class ExceptionEndpoints
|
||||
}).ToList()
|
||||
});
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetExceptionHistory")
|
||||
.WithDescription("Retrieve the ordered audit history of an exception, including every status transition, the actor who performed each action, and descriptive event entries. Supports compliance reviews and traceability of the full exception lifecycle from proposal through resolution.");
|
||||
|
||||
// POST /api/policy/exceptions - Create exception
|
||||
exceptions.MapPost(string.Empty, async Task<IResult>(
|
||||
@@ -204,7 +212,9 @@ public static class ExceptionEndpoints
|
||||
var created = await repository.CreateAsync(exception, actorId, clientInfo, cancellationToken);
|
||||
return Results.Created($"/api/policy/exceptions/{created.ExceptionId}", ToDto(created));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor))
|
||||
.WithName("CreateException")
|
||||
.WithDescription("Create a new policy exception in the proposed state. Validates that the expiry is in the future and does not exceed one year, captures the requesting actor from the authenticated identity, and records the scope (artifact digest, PURL pattern, vulnerability ID, or policy rule), reason code, rationale, and compensating controls.");
|
||||
|
||||
// PUT /api/policy/exceptions/{id} - Update exception
|
||||
exceptions.MapPut("/{id}", async Task<IResult>(
|
||||
@@ -253,7 +263,9 @@ public static class ExceptionEndpoints
|
||||
updated, ExceptionEventType.Updated, actorId, "Exception updated", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor))
|
||||
.WithName("UpdateException")
|
||||
.WithDescription("Update the mutable fields of an existing exception (rationale, evidence references, compensating controls, ticket reference, and metadata). Cannot update expired or revoked exceptions. Version is incremented and the updated-at timestamp is refreshed on every successful update.");
|
||||
|
||||
// POST /api/policy/exceptions/{id}/approve - Approve exception
|
||||
exceptions.MapPost("/{id}/approve", async Task<IResult>(
|
||||
@@ -307,7 +319,9 @@ public static class ExceptionEndpoints
|
||||
updated, ExceptionEventType.Approved, actorId, request?.Comment ?? "Exception approved", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate))
|
||||
.WithName("ApproveException")
|
||||
.WithDescription("Approve a proposed exception and transition it to the approved state. Enforces separation-of-duty by rejecting self-approval (approver must differ from the requester). Multiple approvers may be recorded before the exception is activated.");
|
||||
|
||||
// POST /api/policy/exceptions/{id}/activate - Activate approved exception
|
||||
exceptions.MapPost("/{id}/activate", async Task<IResult>(
|
||||
@@ -347,7 +361,9 @@ public static class ExceptionEndpoints
|
||||
updated, ExceptionEventType.Activated, actorId, "Exception activated", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate))
|
||||
.WithName("ActivateException")
|
||||
.WithDescription("Transition an approved exception to the active state, making it eligible for use in policy evaluation. Only exceptions in the approved state may be activated.");
|
||||
|
||||
// POST /api/policy/exceptions/{id}/extend - Extend expiry
|
||||
exceptions.MapPost("/{id}/extend", async Task<IResult>(
|
||||
@@ -398,7 +414,9 @@ public static class ExceptionEndpoints
|
||||
updated, ExceptionEventType.Extended, actorId, request.Reason, clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate))
|
||||
.WithName("ExtendException")
|
||||
.WithDescription("Extend the expiry date of an active exception. The new expiry must be later than the current expiry. Used when a scheduled fix or mitigation requires additional time beyond the original exception window.");
|
||||
|
||||
// DELETE /api/policy/exceptions/{id} - Revoke exception
|
||||
exceptions.MapDelete("/{id}", async Task<IResult>(
|
||||
@@ -439,7 +457,9 @@ public static class ExceptionEndpoints
|
||||
updated, ExceptionEventType.Revoked, actorId, request?.Reason ?? "Exception revoked", clientInfo, cancellationToken);
|
||||
return Results.Ok(ToDto(result));
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyOperate))
|
||||
.WithName("RevokeException")
|
||||
.WithDescription("Revoke an exception before its natural expiry, recording an optional revocation reason and transitioning the exception to the revoked terminal state. Cannot revoke exceptions that are already expired or revoked.");
|
||||
|
||||
// GET /api/policy/exceptions/expiring - Get exceptions expiring soon
|
||||
exceptions.MapGet("/expiring", async Task<IResult>(
|
||||
@@ -451,7 +471,9 @@ public static class ExceptionEndpoints
|
||||
var results = await repository.GetExpiringAsync(horizon, cancellationToken);
|
||||
return Results.Ok(results.Select(ToDto).ToList());
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead));
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetExpiringExceptions")
|
||||
.WithDescription("List active exceptions that will expire within the specified number of days (default 7). Used by notification and alerting workflows to proactively alert owners before exceptions lapse and cause unexpected policy failures.");
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
@@ -189,7 +189,7 @@ public static class GateEndpoints
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun))
|
||||
.WithName("EvaluateGate")
|
||||
.WithDescription("Evaluate CI/CD gate for an image digest and baseline reference");
|
||||
.WithDescription("Evaluate the CI/CD release gate for a container image by comparing it against a baseline snapshot. Resolves the baseline using a configurable strategy (last-approved, previous-build, production-deployed, or branch-base), computes the security state delta, runs gate rules against the delta context, and returns a pass/warn/block decision with exit codes. If an override justification is supplied on a non-blocking verdict, a bypass audit record is created. Returns HTTP 403 when the gate blocks the release.");
|
||||
|
||||
// GET /api/v1/policy/gate/decision/{decisionId} - Get a previous decision
|
||||
gates.MapGet("/decision/{decisionId}", async Task<IResult>(
|
||||
@@ -222,13 +222,14 @@ public static class GateEndpoints
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetGateDecision")
|
||||
.WithDescription("Retrieve a previous gate evaluation decision by ID");
|
||||
.WithDescription("Retrieve a previously cached gate evaluation decision by its decision ID. Gate decisions are retained in memory for 30 minutes after evaluation, after which this endpoint returns HTTP 404. Used by CI/CD pipelines to poll for results when the evaluation was triggered asynchronously via a registry webhook.");
|
||||
|
||||
// GET /api/v1/policy/gate/health - Health check for gate service
|
||||
gates.MapGet("/health", ([FromServices] TimeProvider timeProvider) =>
|
||||
Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
.WithName("GateHealth")
|
||||
.WithDescription("Health check for the gate evaluation service");
|
||||
.WithDescription("Health check for the gate evaluation service")
|
||||
.AllowAnonymous();
|
||||
}
|
||||
|
||||
private static async Task<BaselineSelectionResult> ResolveBaselineAsync(
|
||||
|
||||
@@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.Persistence.Postgres.Repositories;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -34,34 +36,40 @@ public static class GatesEndpoints
|
||||
.WithTags("Gates");
|
||||
|
||||
group.MapGet("/{bomRef}", GetGateStatus)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetGateStatus")
|
||||
.WithSummary("Get gate check result for a component")
|
||||
.WithDescription("Returns the current unknowns state and gate decision for a BOM reference.");
|
||||
.WithDescription("Retrieve the current unknowns state and gate decision for a BOM reference. Returns the aggregate state across all unknowns (resolved, pending, under_review, or escalated), per-unknown band and SLA details, and a cached gate decision. Results are cached for 30 seconds to reduce database load under CI/CD polling.");
|
||||
|
||||
group.MapPost("/{bomRef}/check", CheckGate)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun))
|
||||
.WithName("CheckGate")
|
||||
.WithSummary("Perform gate check for a component")
|
||||
.WithDescription("Performs a fresh gate check with optional verdict.");
|
||||
.WithDescription("Perform a fresh gate check for a BOM reference with an optional proposed VEX verdict. Returns a pass, warn, or block decision with the list of blocking unknown IDs and the reason for the decision. Returns HTTP 403 when the gate is blocked.");
|
||||
|
||||
group.MapPost("/{bomRef}/exception", RequestException)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor))
|
||||
.WithName("RequestGateException")
|
||||
.WithSummary("Request an exception to bypass the gate")
|
||||
.WithDescription("Requests approval to bypass blocking unknowns.");
|
||||
.WithDescription("Submit an exception request to bypass blocking unknowns for a BOM reference. Requires a justification and a list of unknown IDs to exempt. Returns an exception record with granted status, expiry, and optional denial reason when auto-approval is not available.");
|
||||
|
||||
group.MapGet("/{gateId}/decisions", GetGateDecisionHistory)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAudit))
|
||||
.WithName("GetGateDecisionHistory")
|
||||
.WithSummary("Get historical gate decisions")
|
||||
.WithDescription("Returns paginated list of historical gate decisions for audit and debugging.");
|
||||
.WithDescription("Retrieve a paginated list of historical gate decisions for a gate identifier, with optional filtering by BOM reference, status, actor, and date range. Returns verdict hashes and policy bundle IDs for replay verification and compliance audit.");
|
||||
|
||||
group.MapGet("/decisions/{decisionId}", GetGateDecisionById)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAudit))
|
||||
.WithName("GetGateDecisionById")
|
||||
.WithSummary("Get a specific gate decision by ID")
|
||||
.WithDescription("Returns full details of a specific gate decision.");
|
||||
.WithDescription("Retrieve full details of a specific gate decision by its UUID, including BOM reference, image digest, gate status, verdict hash, policy bundle ID and hash, CI/CD context, actor, blocking unknowns, and warnings.");
|
||||
|
||||
group.MapGet("/decisions/{decisionId}/export", ExportGateDecision)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAudit))
|
||||
.WithName("ExportGateDecision")
|
||||
.WithSummary("Export gate decision in CI/CD format")
|
||||
.WithDescription("Exports gate decision in JUnit, SARIF, or JSON format for CI/CD integration.");
|
||||
.WithDescription("Export a gate decision in JUnit XML, SARIF 2.1.0, or JSON format for integration with CI/CD pipelines. The JUnit format is compatible with Jenkins, GitHub Actions, and GitLab CI; SARIF is compatible with GitHub Code Scanning and VS Code; JSON provides the full structured decision for custom integrations.");
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
@@ -31,66 +33,81 @@ public static class GovernanceEndpoints
|
||||
|
||||
// Sealed Mode endpoints
|
||||
governance.MapGet("/sealed-mode/status", GetSealedModeStatusAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AirgapStatusRead))
|
||||
.WithName("GetSealedModeStatus")
|
||||
.WithDescription("Get sealed mode status");
|
||||
.WithDescription("Retrieve the current sealed mode status for the tenant, including whether the environment is sealed, when it was sealed, by whom, configured trust roots, allowed sources, and any active override entries. Returns HTTP 400 when no tenant can be resolved from the request context.");
|
||||
|
||||
governance.MapGet("/sealed-mode/overrides", GetSealedModeOverridesAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AirgapStatusRead))
|
||||
.WithName("GetSealedModeOverrides")
|
||||
.WithDescription("List sealed mode overrides");
|
||||
.WithDescription("List all sealed mode overrides for the tenant, including override type, target resource, approver IDs, expiry timestamp, and active status. Used by operators to audit active bypass grants and verify sealed posture integrity.");
|
||||
|
||||
governance.MapPost("/sealed-mode/toggle", ToggleSealedModeAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AirgapSeal))
|
||||
.WithName("ToggleSealedMode")
|
||||
.WithDescription("Toggle sealed mode on/off");
|
||||
.WithDescription("Enable or disable sealed mode for the tenant. When enabling, records the sealing actor, timestamp, reason, trust roots, and allowed sources. When disabling, records the unseal timestamp. Every toggle is recorded as a governance audit event.");
|
||||
|
||||
governance.MapPost("/sealed-mode/overrides", CreateSealedModeOverrideAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AirgapSeal))
|
||||
.WithName("CreateSealedModeOverride")
|
||||
.WithDescription("Create a sealed mode override");
|
||||
.WithDescription("Create a time-limited override to allow a specific operation or target to bypass sealed mode restrictions. The override expires after the configured duration (defaulting to 24 hours) and is recorded in the governance audit log with the approving actor.");
|
||||
|
||||
governance.MapPost("/sealed-mode/overrides/{overrideId}/revoke", RevokeSealedModeOverrideAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.AirgapSeal))
|
||||
.WithName("RevokeSealedModeOverride")
|
||||
.WithDescription("Revoke a sealed mode override");
|
||||
.WithDescription("Revoke an active sealed mode override before its natural expiry, providing an optional reason. The override is marked inactive immediately, preventing further bypass use. The revocation is recorded in the governance audit log.");
|
||||
|
||||
// Risk Profile endpoints
|
||||
governance.MapGet("/risk-profiles", ListRiskProfilesAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("ListRiskProfiles")
|
||||
.WithDescription("List risk profiles");
|
||||
.WithDescription("List risk profiles for the tenant with optional status filtering (draft, active, deprecated). Each profile includes its signal configuration, severity overrides, action overrides, and lifecycle metadata. The default risk profile is always included in the response.");
|
||||
|
||||
governance.MapGet("/risk-profiles/{profileId}", GetRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("GetRiskProfile")
|
||||
.WithDescription("Get a risk profile by ID");
|
||||
.WithDescription("Retrieve the full configuration of a specific risk profile by its identifier, including all signals with weights and enabled state, severity and action overrides, and the profile version and lifecycle metadata.");
|
||||
|
||||
governance.MapPost("/risk-profiles", CreateRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor))
|
||||
.WithName("CreateRiskProfile")
|
||||
.WithDescription("Create a new risk profile");
|
||||
.WithDescription("Create a new risk profile in draft state with the specified signal configuration, severity overrides, and action overrides. The profile can optionally extend an existing base profile. Audit events are recorded for all profile changes.");
|
||||
|
||||
governance.MapPut("/risk-profiles/{profileId}", UpdateRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor))
|
||||
.WithName("UpdateRiskProfile")
|
||||
.WithDescription("Update a risk profile");
|
||||
.WithDescription("Update the name, description, signals, severity overrides, or action overrides of an existing risk profile. Partial updates are supported: only supplied fields are changed. The modified-at timestamp and actor are updated on every successful write.");
|
||||
|
||||
governance.MapDelete("/risk-profiles/{profileId}", DeleteRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAuthor))
|
||||
.WithName("DeleteRiskProfile")
|
||||
.WithDescription("Delete a risk profile");
|
||||
.WithDescription("Permanently delete a risk profile by its identifier, removing it from the tenant's profile registry. Returns HTTP 404 when the profile does not exist. Deletion is recorded as a governance audit event.");
|
||||
|
||||
governance.MapPost("/risk-profiles/{profileId}/activate", ActivateRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyActivate))
|
||||
.WithName("ActivateRiskProfile")
|
||||
.WithDescription("Activate a risk profile");
|
||||
.WithDescription("Transition a risk profile to the active state, making it the candidate for policy evaluation use. Records the activating actor and timestamp. Activation is an audit-logged, irreversible state transition from draft.");
|
||||
|
||||
governance.MapPost("/risk-profiles/{profileId}/deprecate", DeprecateRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyActivate))
|
||||
.WithName("DeprecateRiskProfile")
|
||||
.WithDescription("Deprecate a risk profile");
|
||||
.WithDescription("Transition a risk profile to the deprecated state with an optional deprecation reason. Deprecated profiles remain visible for audit and historical reference but should not be assigned to new policy evaluations.");
|
||||
|
||||
governance.MapPost("/risk-profiles/validate", ValidateRiskProfileAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRead))
|
||||
.WithName("ValidateRiskProfile")
|
||||
.WithDescription("Validate a risk profile");
|
||||
.WithDescription("Validate a candidate risk profile configuration without persisting it. Checks for required fields (name, at least one signal) and emits warnings when signal weights do not sum to 1.0. Used by policy authoring tools to provide inline validation feedback before profile creation.");
|
||||
|
||||
// Audit endpoints
|
||||
governance.MapGet("/audit/events", GetAuditEventsAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAudit))
|
||||
.WithName("GetGovernanceAuditEvents")
|
||||
.WithDescription("Get governance audit events");
|
||||
.WithDescription("Retrieve paginated governance audit events for the tenant, ordered by most recent first. Events cover sealed mode changes, override grants and revocations, and risk profile lifecycle actions. Requires tenant ID via header or query parameter.");
|
||||
|
||||
governance.MapGet("/audit/events/{eventId}", GetAuditEventAsync)
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyAudit))
|
||||
.WithName("GetGovernanceAuditEvent")
|
||||
.WithDescription("Get a specific audit event");
|
||||
.WithDescription("Retrieve a single governance audit event by its identifier, including event type, actor, target resource, timestamp, and human-readable summary. Returns HTTP 404 when the event does not exist or belongs to a different tenant.");
|
||||
|
||||
// Initialize default profiles
|
||||
InitializeDefaultProfiles();
|
||||
|
||||
@@ -22,23 +22,27 @@ internal static class RegistryWebhookEndpoints
|
||||
public static IEndpointRouteBuilder MapRegistryWebhooks(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/v1/webhooks/registry")
|
||||
.WithTags("Registry Webhooks");
|
||||
.WithTags("Registry Webhooks")
|
||||
.AllowAnonymous();
|
||||
|
||||
group.MapPost("/docker", HandleDockerRegistryWebhook)
|
||||
.WithName("DockerRegistryWebhook")
|
||||
.WithSummary("Handle Docker Registry v2 webhook events")
|
||||
.WithDescription("Receive Docker Registry v2 notification events and enqueue a gate evaluation job for each push event that includes a valid image digest. Returns a 202 Accepted response with the list of queued job IDs that can be polled for evaluation status.")
|
||||
.Produces<WebhookAcceptedResponse>(StatusCodes.Status202Accepted)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/harbor", HandleHarborWebhook)
|
||||
.WithName("HarborWebhook")
|
||||
.WithSummary("Handle Harbor registry webhook events")
|
||||
.WithDescription("Receive Harbor registry webhook events and enqueue a gate evaluation job for each PUSH_ARTIFACT or pushImage event that contains a resource with a valid digest. Non-push event types are silently acknowledged without queuing any jobs.")
|
||||
.Produces<WebhookAcceptedResponse>(StatusCodes.Status202Accepted)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
group.MapPost("/generic", HandleGenericWebhook)
|
||||
.WithName("GenericRegistryWebhook")
|
||||
.WithSummary("Handle generic registry webhook events with image digest")
|
||||
.WithDescription("Receive a generic registry webhook payload containing an image digest and enqueue a single gate evaluation job. Supports any registry that can POST a JSON body with imageDigest, repository, tag, and optional baselineRef fields.")
|
||||
.Produces<WebhookAcceptedResponse>(StatusCodes.Status202Accepted)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest);
|
||||
|
||||
|
||||
@@ -150,13 +150,14 @@ public static class ScoreGateEndpoints
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun))
|
||||
.WithName("EvaluateScoreGate")
|
||||
.WithDescription("Evaluate score-based CI/CD gate for a finding");
|
||||
.WithDescription("Evaluate a score-based CI/CD release gate for a single security finding using the Evidence Weighted Score (EWS) formula. Computes a composite risk score from CVSS, EPSS, reachability, exploit maturity, patch proof, and VEX status inputs, applies the gate policy thresholds to produce a pass/warn/block action, signs the verdict bundle, and optionally anchors it to a Rekor transparency log. Returns HTTP 403 when the gate action is block.");
|
||||
|
||||
// GET /api/v1/gate/health - Health check for gate service
|
||||
gates.MapGet("/health", ([FromServices] TimeProvider timeProvider) =>
|
||||
Results.Ok(new { status = "healthy", timestamp = timeProvider.GetUtcNow() }))
|
||||
.WithName("ScoreGateHealth")
|
||||
.WithDescription("Health check for the score-based gate evaluation service");
|
||||
.WithDescription("Health check for the score-based gate evaluation service")
|
||||
.AllowAnonymous();
|
||||
|
||||
// POST /api/v1/gate/evaluate-batch - Batch evaluation for multiple findings
|
||||
gates.MapPost("/evaluate-batch", async Task<IResult>(
|
||||
@@ -261,7 +262,7 @@ public static class ScoreGateEndpoints
|
||||
})
|
||||
.RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.PolicyRun))
|
||||
.WithName("EvaluateScoreGateBatch")
|
||||
.WithDescription("Batch evaluate score-based CI/CD gates for multiple findings");
|
||||
.WithDescription("Batch evaluate score-based CI/CD gates for up to 500 findings in a single request using configurable parallelism. Applies the EWS formula to each finding, produces a per-finding action (pass/warn/block), and returns an aggregate summary with overall action, exit code, and optional per-finding verdict bundles. Supports fail-fast mode to stop processing on the first blocked finding.");
|
||||
}
|
||||
|
||||
private static async Task<List<ScoreGateBatchDecision>> EvaluateBatchAsync(
|
||||
|
||||
@@ -328,7 +328,8 @@ app.TryUseStellaRouter(routerEnabled);
|
||||
app.MapHealthChecks("/healthz");
|
||||
|
||||
app.MapGet("/readyz", () => Results.Ok(new { status = "ready" }))
|
||||
.WithName("Readiness");
|
||||
.WithName("Readiness")
|
||||
.AllowAnonymous();
|
||||
|
||||
app.MapGet("/", () => Results.Redirect("/healthz"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user