293 lines
10 KiB
C#
293 lines
10 KiB
C#
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Copyright (c) StellaOps
|
|
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using StellaOps.Scanner.Reachability.Stack;
|
|
using StellaOps.Scanner.WebService.Constants;
|
|
using StellaOps.Scanner.WebService.Contracts;
|
|
using StellaOps.Scanner.WebService.Infrastructure;
|
|
using StellaOps.Scanner.WebService.Security;
|
|
|
|
namespace StellaOps.Scanner.WebService.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Endpoints for three-layer reachability stack analysis.
|
|
/// </summary>
|
|
internal static class ReachabilityStackEndpoints
|
|
{
|
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
|
{
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
Converters = { new JsonStringEnumConverter() }
|
|
};
|
|
|
|
/// <summary>
|
|
/// Maps reachability stack endpoints under /reachability.
|
|
/// </summary>
|
|
public static void MapReachabilityStackEndpoints(this RouteGroupBuilder apiGroup)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(apiGroup);
|
|
|
|
var reachabilityGroup = apiGroup.MapGroup("/reachability");
|
|
|
|
// GET /reachability/{findingId}/stack - Full 3-layer breakdown
|
|
reachabilityGroup.MapGet("/{findingId}/stack", HandleGetStackAsync)
|
|
.WithName("scanner.reachability.stack")
|
|
.WithTags("ReachabilityStack")
|
|
.Produces<ReachabilityStackDto>(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.RequireAuthorization(ScannerPolicies.ScansRead);
|
|
|
|
// GET /reachability/{findingId}/stack/layer/{layerNumber} - Single layer detail
|
|
reachabilityGroup.MapGet("/{findingId}/stack/layer/{layerNumber:int}", HandleGetLayerAsync)
|
|
.WithName("scanner.reachability.stack.layer")
|
|
.WithTags("ReachabilityStack")
|
|
.Produces(StatusCodes.Status200OK)
|
|
.Produces(StatusCodes.Status400BadRequest)
|
|
.Produces(StatusCodes.Status404NotFound)
|
|
.RequireAuthorization(ScannerPolicies.ScansRead);
|
|
}
|
|
|
|
private static async Task<IResult> HandleGetStackAsync(
|
|
string findingId,
|
|
IReachabilityStackRepository? stackRepository,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(findingId))
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.Validation,
|
|
"Invalid finding identifier",
|
|
StatusCodes.Status400BadRequest,
|
|
detail: "Finding identifier is required.");
|
|
}
|
|
|
|
// If no repository is registered, return a stub implementation for now
|
|
if (stackRepository is null)
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.NotImplemented,
|
|
"Reachability stack not available",
|
|
StatusCodes.Status501NotImplemented,
|
|
detail: "Reachability stack analysis is not yet implemented for this deployment.");
|
|
}
|
|
|
|
var stack = await stackRepository.TryGetByFindingIdAsync(findingId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (stack is null)
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.NotFound,
|
|
"Reachability stack not found",
|
|
StatusCodes.Status404NotFound,
|
|
detail: $"No reachability stack found for finding '{findingId}'.");
|
|
}
|
|
|
|
var dto = MapToDto(stack);
|
|
return Json(dto, StatusCodes.Status200OK);
|
|
}
|
|
|
|
private static async Task<IResult> HandleGetLayerAsync(
|
|
string findingId,
|
|
int layerNumber,
|
|
IReachabilityStackRepository? stackRepository,
|
|
HttpContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(findingId))
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.Validation,
|
|
"Invalid finding identifier",
|
|
StatusCodes.Status400BadRequest,
|
|
detail: "Finding identifier is required.");
|
|
}
|
|
|
|
if (layerNumber < 1 || layerNumber > 3)
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.Validation,
|
|
"Invalid layer number",
|
|
StatusCodes.Status400BadRequest,
|
|
detail: "Layer number must be 1, 2, or 3.");
|
|
}
|
|
|
|
if (stackRepository is null)
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.NotImplemented,
|
|
"Reachability stack not available",
|
|
StatusCodes.Status501NotImplemented,
|
|
detail: "Reachability stack analysis is not yet implemented for this deployment.");
|
|
}
|
|
|
|
var stack = await stackRepository.TryGetByFindingIdAsync(findingId, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (stack is null)
|
|
{
|
|
return ProblemResultFactory.Create(
|
|
context,
|
|
ProblemTypes.NotFound,
|
|
"Reachability stack not found",
|
|
StatusCodes.Status404NotFound,
|
|
detail: $"No reachability stack found for finding '{findingId}'.");
|
|
}
|
|
|
|
object layerDto = layerNumber switch
|
|
{
|
|
1 => MapLayer1ToDto(stack.StaticCallGraph),
|
|
2 => MapLayer2ToDto(stack.BinaryResolution),
|
|
3 => MapLayer3ToDto(stack.RuntimeGating),
|
|
_ => throw new InvalidOperationException("Invalid layer number")
|
|
};
|
|
|
|
return Json(layerDto, StatusCodes.Status200OK);
|
|
}
|
|
|
|
private static ReachabilityStackDto MapToDto(ReachabilityStack stack)
|
|
{
|
|
return new ReachabilityStackDto(
|
|
Id: stack.Id,
|
|
FindingId: stack.FindingId,
|
|
Symbol: MapSymbolToDto(stack.Symbol),
|
|
Layer1: MapLayer1ToDto(stack.StaticCallGraph),
|
|
Layer2: MapLayer2ToDto(stack.BinaryResolution),
|
|
Layer3: MapLayer3ToDto(stack.RuntimeGating),
|
|
Verdict: stack.Verdict.ToString(),
|
|
Explanation: stack.Explanation,
|
|
AnalyzedAt: stack.AnalyzedAt);
|
|
}
|
|
|
|
private static VulnerableSymbolDto MapSymbolToDto(VulnerableSymbol symbol)
|
|
{
|
|
return new VulnerableSymbolDto(
|
|
Name: symbol.Name,
|
|
Library: symbol.Library,
|
|
Version: symbol.Version,
|
|
VulnerabilityId: symbol.VulnerabilityId,
|
|
Type: symbol.Type.ToString());
|
|
}
|
|
|
|
private static ReachabilityLayer1Dto MapLayer1ToDto(ReachabilityLayer1 layer)
|
|
{
|
|
return new ReachabilityLayer1Dto(
|
|
IsReachable: layer.IsReachable,
|
|
Confidence: layer.Confidence.ToString(),
|
|
PathCount: layer.Paths.Length,
|
|
EntrypointCount: layer.ReachingEntrypoints.Length,
|
|
AnalysisMethod: layer.AnalysisMethod,
|
|
Paths: layer.Paths.Select(MapCallPathToDto).ToList());
|
|
}
|
|
|
|
private static ReachabilityLayer2Dto MapLayer2ToDto(ReachabilityLayer2 layer)
|
|
{
|
|
return new ReachabilityLayer2Dto(
|
|
IsResolved: layer.IsResolved,
|
|
Confidence: layer.Confidence.ToString(),
|
|
Reason: layer.Reason,
|
|
Resolution: layer.Resolution is not null ? MapResolutionToDto(layer.Resolution) : null,
|
|
LoaderRule: layer.AppliedRule is not null ? MapLoaderRuleToDto(layer.AppliedRule) : null);
|
|
}
|
|
|
|
private static ReachabilityLayer3Dto MapLayer3ToDto(ReachabilityLayer3 layer)
|
|
{
|
|
return new ReachabilityLayer3Dto(
|
|
IsGated: layer.IsGated,
|
|
Outcome: layer.Outcome.ToString(),
|
|
Confidence: layer.Confidence.ToString(),
|
|
Conditions: layer.Conditions.Select(MapGatingConditionToDto).ToList());
|
|
}
|
|
|
|
private static CallPathDto MapCallPathToDto(CallPath path)
|
|
{
|
|
return new CallPathDto(
|
|
Entrypoint: path.Entrypoint is not null ? MapEntrypointToDto(path.Entrypoint) : null,
|
|
Sites: path.Sites.Select(MapCallSiteToDto).ToList(),
|
|
Confidence: path.Confidence,
|
|
HasConditionals: path.HasConditionals);
|
|
}
|
|
|
|
private static EntrypointDto MapEntrypointToDto(Entrypoint entrypoint)
|
|
{
|
|
return new EntrypointDto(
|
|
Name: entrypoint.Name,
|
|
Type: entrypoint.Type.ToString(),
|
|
File: entrypoint.Location,
|
|
Description: entrypoint.Description);
|
|
}
|
|
|
|
private static CallSiteDto MapCallSiteToDto(CallSite site)
|
|
{
|
|
return new CallSiteDto(
|
|
Method: site.MethodName,
|
|
Type: site.ClassName,
|
|
File: site.FileName,
|
|
Line: site.LineNumber,
|
|
CallType: site.Type.ToString());
|
|
}
|
|
|
|
private static SymbolResolutionDto MapResolutionToDto(SymbolResolution resolution)
|
|
{
|
|
return new SymbolResolutionDto(
|
|
SymbolName: resolution.SymbolName,
|
|
ResolvedLibrary: resolution.ResolvedLibrary,
|
|
ResolvedVersion: resolution.ResolvedVersion,
|
|
SymbolVersion: resolution.SymbolVersion,
|
|
Method: resolution.Method.ToString());
|
|
}
|
|
|
|
private static LoaderRuleDto MapLoaderRuleToDto(LoaderRule rule)
|
|
{
|
|
return new LoaderRuleDto(
|
|
Type: rule.Type.ToString(),
|
|
Value: rule.Value,
|
|
Source: rule.Source);
|
|
}
|
|
|
|
private static GatingConditionDto MapGatingConditionToDto(GatingCondition condition)
|
|
{
|
|
return new GatingConditionDto(
|
|
Type: condition.Type.ToString(),
|
|
Description: condition.Description,
|
|
ConfigKey: condition.ConfigKey,
|
|
EnvVar: condition.EnvVar,
|
|
IsBlocking: condition.IsBlocking,
|
|
Status: condition.Status.ToString());
|
|
}
|
|
|
|
private static IResult Json<T>(T value, int statusCode)
|
|
{
|
|
var payload = JsonSerializer.Serialize(value, SerializerOptions);
|
|
return Results.Content(payload, "application/json", System.Text.Encoding.UTF8, statusCode);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Repository interface for reachability stack data.
|
|
/// </summary>
|
|
public interface IReachabilityStackRepository
|
|
{
|
|
/// <summary>
|
|
/// Gets a reachability stack by finding ID.
|
|
/// </summary>
|
|
Task<ReachabilityStack?> TryGetByFindingIdAsync(string findingId, CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Stores a reachability stack.
|
|
/// </summary>
|
|
Task StoreAsync(ReachabilityStack stack, CancellationToken ct);
|
|
}
|