feat: add security sink detection patterns for JavaScript/TypeScript
- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
This commit is contained in:
253
src/Policy/StellaOps.Policy.Engine/Endpoints/BudgetEndpoints.cs
Normal file
253
src/Policy/StellaOps.Policy.Engine/Endpoints/BudgetEndpoints.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BudgetEndpoints.cs
|
||||
// Sprint: SPRINT_4300_0002_0001 (Unknowns Budget Policy Integration)
|
||||
// Task: BUDGET-014 - Create budget management API endpoints
|
||||
// Description: API endpoints for managing unknown budget configurations.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Unknowns.Configuration;
|
||||
using StellaOps.Policy.Unknowns.Models;
|
||||
using StellaOps.Policy.Unknowns.Services;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for managing unknown budget configurations.
|
||||
/// </summary>
|
||||
internal static class BudgetEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapBudgets(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
var group = endpoints.MapGroup("/api/v1/policy/budgets")
|
||||
.RequireAuthorization()
|
||||
.WithTags("Unknown Budgets");
|
||||
|
||||
group.MapGet(string.Empty, ListBudgets)
|
||||
.WithName("ListBudgets")
|
||||
.WithSummary("List all configured unknown budgets.")
|
||||
.Produces<BudgetsListResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapGet("/{environment}", GetBudget)
|
||||
.WithName("GetBudget")
|
||||
.WithSummary("Get budget for a specific environment.")
|
||||
.Produces<BudgetResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapGet("/{environment}/status", GetBudgetStatus)
|
||||
.WithName("GetBudgetStatus")
|
||||
.WithSummary("Get current budget status for an environment.")
|
||||
.Produces<BudgetStatusResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapPost("/{environment}/check", CheckBudget)
|
||||
.WithName("CheckBudget")
|
||||
.WithSummary("Check unknowns against a budget.")
|
||||
.Produces<BudgetCheckResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapGet("/defaults", GetDefaultBudgets)
|
||||
.WithName("GetDefaultBudgets")
|
||||
.WithSummary("Get the default budget configurations.")
|
||||
.Produces<DefaultBudgetsResponse>(StatusCodes.Status200OK);
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static Ok<BudgetsListResponse> ListBudgets(
|
||||
IOptions<UnknownBudgetOptions> options)
|
||||
{
|
||||
var budgets = options.Value.Budgets
|
||||
.Select(kvp => ToBudgetDto(kvp.Key, kvp.Value))
|
||||
.OrderBy(b => b.Environment)
|
||||
.ToList();
|
||||
|
||||
return TypedResults.Ok(new BudgetsListResponse(
|
||||
budgets,
|
||||
budgets.Count,
|
||||
options.Value.EnforceBudgets));
|
||||
}
|
||||
|
||||
private static Results<Ok<BudgetResponse>, NotFound<ProblemDetails>> GetBudget(
|
||||
string environment,
|
||||
IUnknownBudgetService budgetService)
|
||||
{
|
||||
var budget = budgetService.GetBudgetForEnvironment(environment);
|
||||
|
||||
if (budget is null)
|
||||
{
|
||||
return TypedResults.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Budget not found",
|
||||
Detail = $"No budget configured for environment '{environment}'."
|
||||
});
|
||||
}
|
||||
|
||||
return TypedResults.Ok(new BudgetResponse(ToBudgetDto(environment, budget)));
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<BudgetStatusResponse>, ProblemHttpResult>> GetBudgetStatus(
|
||||
HttpContext httpContext,
|
||||
string environment,
|
||||
IUnknownBudgetService budgetService,
|
||||
Unknowns.Repositories.IUnknownsRepository repository,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tenantId = ResolveTenantId(httpContext);
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
return TypedResults.Problem("Tenant ID is required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
// Get all unknowns for the tenant
|
||||
var unknowns = await repository.GetAllAsync(tenantId, limit: 10000, ct: ct);
|
||||
|
||||
var status = budgetService.GetBudgetStatus(environment, unknowns);
|
||||
|
||||
return TypedResults.Ok(new BudgetStatusResponse(
|
||||
status.Environment,
|
||||
status.TotalUnknowns,
|
||||
status.TotalLimit,
|
||||
status.PercentageUsed,
|
||||
status.IsExceeded,
|
||||
status.ViolationCount,
|
||||
status.ByReasonCode.ToDictionary(
|
||||
kvp => kvp.Key.ToString(),
|
||||
kvp => kvp.Value)));
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<BudgetCheckResponse>, ProblemHttpResult>> CheckBudget(
|
||||
HttpContext httpContext,
|
||||
string environment,
|
||||
[FromBody] BudgetCheckRequest request,
|
||||
IUnknownBudgetService budgetService,
|
||||
Unknowns.Repositories.IUnknownsRepository repository,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var tenantId = ResolveTenantId(httpContext);
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
return TypedResults.Problem("Tenant ID is required.", statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
// Get unknowns (either from request or repository)
|
||||
IReadOnlyList<Unknown> unknowns;
|
||||
if (request.UnknownIds is { Count: > 0 })
|
||||
{
|
||||
var allUnknowns = await repository.GetAllAsync(tenantId, limit: 10000, ct: ct);
|
||||
unknowns = allUnknowns.Where(u => request.UnknownIds.Contains(u.Id)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
unknowns = await repository.GetAllAsync(tenantId, limit: 10000, ct: ct);
|
||||
}
|
||||
|
||||
var result = budgetService.CheckBudget(environment, unknowns);
|
||||
|
||||
return TypedResults.Ok(new BudgetCheckResponse(
|
||||
result.IsWithinBudget,
|
||||
result.RecommendedAction.ToString().ToLowerInvariant(),
|
||||
result.TotalUnknowns,
|
||||
result.TotalLimit,
|
||||
result.Message,
|
||||
result.Violations.Select(kvp => new BudgetViolationDto(
|
||||
kvp.Key.ToString(),
|
||||
kvp.Value.Count,
|
||||
kvp.Value.Limit)).ToList()));
|
||||
}
|
||||
|
||||
private static Ok<DefaultBudgetsResponse> GetDefaultBudgets()
|
||||
{
|
||||
return TypedResults.Ok(new DefaultBudgetsResponse(
|
||||
ToBudgetDto("production", DefaultBudgets.Production),
|
||||
ToBudgetDto("staging", DefaultBudgets.Staging),
|
||||
ToBudgetDto("development", DefaultBudgets.Development),
|
||||
ToBudgetDto("default", DefaultBudgets.Default)));
|
||||
}
|
||||
|
||||
private static Guid ResolveTenantId(HttpContext context)
|
||||
{
|
||||
if (context.Request.Headers.TryGetValue("X-Tenant-Id", out var tenantHeader) &&
|
||||
!string.IsNullOrWhiteSpace(tenantHeader) &&
|
||||
Guid.TryParse(tenantHeader.ToString(), out var headerTenantId))
|
||||
{
|
||||
return headerTenantId;
|
||||
}
|
||||
|
||||
var tenantClaim = context.User?.FindFirst("tenant_id")?.Value;
|
||||
if (!string.IsNullOrEmpty(tenantClaim) && Guid.TryParse(tenantClaim, out var claimTenantId))
|
||||
{
|
||||
return claimTenantId;
|
||||
}
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
private static BudgetDto ToBudgetDto(string environment, UnknownBudget budget)
|
||||
{
|
||||
return new BudgetDto(
|
||||
environment,
|
||||
budget.TotalLimit,
|
||||
budget.ReasonLimits.ToDictionary(
|
||||
kvp => kvp.Key.ToString(),
|
||||
kvp => kvp.Value),
|
||||
budget.Action.ToString().ToLowerInvariant(),
|
||||
budget.ExceededMessage);
|
||||
}
|
||||
}
|
||||
|
||||
#region DTOs
|
||||
|
||||
/// <summary>Budget data transfer object.</summary>
|
||||
public sealed record BudgetDto(
|
||||
string Environment,
|
||||
int? TotalLimit,
|
||||
IReadOnlyDictionary<string, int> ReasonLimits,
|
||||
string Action,
|
||||
string? ExceededMessage);
|
||||
|
||||
/// <summary>Response containing a list of budgets.</summary>
|
||||
public sealed record BudgetsListResponse(
|
||||
IReadOnlyList<BudgetDto> Budgets,
|
||||
int TotalCount,
|
||||
bool EnforcementEnabled);
|
||||
|
||||
/// <summary>Response containing a single budget.</summary>
|
||||
public sealed record BudgetResponse(BudgetDto Budget);
|
||||
|
||||
/// <summary>Response containing budget status.</summary>
|
||||
public sealed record BudgetStatusResponse(
|
||||
string Environment,
|
||||
int TotalUnknowns,
|
||||
int? TotalLimit,
|
||||
decimal PercentageUsed,
|
||||
bool IsExceeded,
|
||||
int ViolationCount,
|
||||
IReadOnlyDictionary<string, int> ByReasonCode);
|
||||
|
||||
/// <summary>Request to check unknowns against a budget.</summary>
|
||||
public sealed record BudgetCheckRequest(IReadOnlyList<Guid>? UnknownIds = null);
|
||||
|
||||
/// <summary>Response from budget check.</summary>
|
||||
public sealed record BudgetCheckResponse(
|
||||
bool IsWithinBudget,
|
||||
string RecommendedAction,
|
||||
int TotalUnknowns,
|
||||
int? TotalLimit,
|
||||
string? Message,
|
||||
IReadOnlyList<BudgetViolationDto> Violations);
|
||||
|
||||
/// <summary>Budget violation details.</summary>
|
||||
public sealed record BudgetViolationDto(
|
||||
string ReasonCode,
|
||||
int Count,
|
||||
int Limit);
|
||||
|
||||
/// <summary>Response containing default budgets.</summary>
|
||||
public sealed record DefaultBudgetsResponse(
|
||||
BudgetDto Production,
|
||||
BudgetDto Staging,
|
||||
BudgetDto Development,
|
||||
BudgetDto Default);
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user