259 lines
9.2 KiB
C#
259 lines
9.2 KiB
C#
// SPDX-License-Identifier: BUSL-1.1
|
|
// Copyright (c) 2025 StellaOps
|
|
// Sprint: SPRINT_20260122_039_Scanner_runtime_linkage_verification
|
|
// Task: RLV-009 - Platform API: Function Map Endpoints
|
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Auth.ServerIntegration.Tenancy;
|
|
using StellaOps.Platform.WebService.Constants;
|
|
using StellaOps.Platform.WebService.Contracts;
|
|
using StellaOps.Platform.WebService.Services;
|
|
|
|
namespace StellaOps.Platform.WebService.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Function map management API endpoints.
|
|
/// </summary>
|
|
public static class FunctionMapEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps function-map-related endpoints.
|
|
/// </summary>
|
|
public static IEndpointRouteBuilder MapFunctionMapEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var maps = app.MapGroup("/api/v1/function-maps")
|
|
.WithTags("Function Maps")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapRead)
|
|
.RequireTenant();
|
|
|
|
MapCrudEndpoints(maps);
|
|
MapVerifyEndpoints(maps);
|
|
|
|
return app;
|
|
}
|
|
|
|
private static void MapCrudEndpoints(IEndpointRouteBuilder maps)
|
|
{
|
|
// POST /api/v1/function-maps - Create function map
|
|
maps.MapPost("/", async Task<IResult> (
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IFunctionMapService service,
|
|
[FromBody] CreateFunctionMapRequest request,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var result = await service.CreateAsync(
|
|
requestContext!,
|
|
request,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Created(
|
|
$"/api/v1/function-maps/{result.Value.Id}",
|
|
new PlatformItemResponse<FunctionMapDetail>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
result.DataAsOf,
|
|
result.Cached,
|
|
result.CacheTtlSeconds,
|
|
result.Value));
|
|
})
|
|
.WithName("CreateFunctionMap")
|
|
.WithSummary("Create function map")
|
|
.WithDescription("Creates a new function map from an SBOM reference and hot function patterns.")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapWrite);
|
|
|
|
// GET /api/v1/function-maps - List function maps
|
|
maps.MapGet("/", async Task<IResult> (
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IFunctionMapService service,
|
|
[FromQuery] int? limit,
|
|
[FromQuery] int? offset,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var result = await service.ListAsync(
|
|
requestContext!,
|
|
limit ?? 100,
|
|
offset ?? 0,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformListResponse<FunctionMapSummary>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
result.DataAsOf,
|
|
result.Cached,
|
|
result.CacheTtlSeconds,
|
|
result.Value,
|
|
result.Value.Count,
|
|
limit ?? 100,
|
|
offset ?? 0));
|
|
})
|
|
.WithName("ListFunctionMaps")
|
|
.WithSummary("List function maps")
|
|
.WithDescription("Lists all function maps for the current tenant.")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
|
|
|
|
// GET /api/v1/function-maps/{id} - Get function map by ID
|
|
maps.MapGet("/{id}", async Task<IResult> (
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IFunctionMapService service,
|
|
string id,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var result = await service.GetByIdAsync(
|
|
requestContext!,
|
|
id,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
if (result.Value is null)
|
|
{
|
|
return Results.NotFound(new { error = "Function map not found", id });
|
|
}
|
|
|
|
return Results.Ok(new PlatformItemResponse<FunctionMapDetail>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
result.DataAsOf,
|
|
result.Cached,
|
|
result.CacheTtlSeconds,
|
|
result.Value));
|
|
})
|
|
.WithName("GetFunctionMap")
|
|
.WithSummary("Get function map")
|
|
.WithDescription("Retrieves a function map by its unique identifier.")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
|
|
|
|
// DELETE /api/v1/function-maps/{id} - Delete function map
|
|
maps.MapDelete("/{id}", async Task<IResult> (
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IFunctionMapService service,
|
|
string id,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var result = await service.DeleteAsync(
|
|
requestContext!,
|
|
id,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
if (!result.Value)
|
|
{
|
|
return Results.NotFound(new { error = "Function map not found", id });
|
|
}
|
|
|
|
return Results.NoContent();
|
|
})
|
|
.WithName("DeleteFunctionMap")
|
|
.WithSummary("Delete function map")
|
|
.WithDescription("Deletes a function map by its unique identifier.")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapWrite);
|
|
}
|
|
|
|
private static void MapVerifyEndpoints(IEndpointRouteBuilder maps)
|
|
{
|
|
// POST /api/v1/function-maps/{id}/verify - Verify observations against map
|
|
maps.MapPost("/{id}/verify", async Task<IResult> (
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IFunctionMapService service,
|
|
string id,
|
|
[FromBody] VerifyFunctionMapRequest request,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var result = await service.VerifyAsync(
|
|
requestContext!,
|
|
id,
|
|
request,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformItemResponse<FunctionMapVerifyResponse>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
result.DataAsOf,
|
|
result.Cached,
|
|
result.CacheTtlSeconds,
|
|
result.Value));
|
|
})
|
|
.WithName("VerifyFunctionMap")
|
|
.WithSummary("Verify function map")
|
|
.WithDescription("Verifies runtime observations against a declared function map.")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapVerify);
|
|
|
|
// GET /api/v1/function-maps/{id}/coverage - Get coverage statistics
|
|
maps.MapGet("/{id}/coverage", async Task<IResult> (
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
IFunctionMapService service,
|
|
string id,
|
|
CancellationToken cancellationToken) =>
|
|
{
|
|
if (!TryResolveContext(context, resolver, out var requestContext, out var failure))
|
|
{
|
|
return failure!;
|
|
}
|
|
|
|
var result = await service.GetCoverageAsync(
|
|
requestContext!,
|
|
id,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return Results.Ok(new PlatformItemResponse<FunctionMapCoverageResponse>(
|
|
requestContext!.TenantId,
|
|
requestContext.ActorId,
|
|
result.DataAsOf,
|
|
result.Cached,
|
|
result.CacheTtlSeconds,
|
|
result.Value));
|
|
})
|
|
.WithName("GetFunctionMapCoverage")
|
|
.WithSummary("Get function map coverage")
|
|
.WithDescription("Returns current coverage statistics for a function map.")
|
|
.RequireAuthorization(PlatformPolicies.FunctionMapRead);
|
|
}
|
|
|
|
private static bool TryResolveContext(
|
|
HttpContext context,
|
|
PlatformRequestContextResolver resolver,
|
|
out PlatformRequestContext? requestContext,
|
|
out IResult? failure)
|
|
{
|
|
if (resolver.TryResolve(context, out requestContext, out var error))
|
|
{
|
|
failure = null;
|
|
return true;
|
|
}
|
|
|
|
failure = Results.BadRequest(new { error = error ?? "tenant_missing" });
|
|
return false;
|
|
}
|
|
}
|