feat: Implement IsolatedReplayContext for deterministic audit replay
- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls. - Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation. - Created supporting interfaces and options for context configuration. feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison - Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison. - Implemented detailed drift detection and error handling during replay execution. - Added interfaces for policy evaluation and replay execution options. feat: Add ScanSnapshotFetcher for fetching scan data and snapshots - Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation. - Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements. - Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
// 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.File,
|
||||
Description: entrypoint.Description);
|
||||
}
|
||||
|
||||
private static CallSiteDto MapCallSiteToDto(CallSite site)
|
||||
{
|
||||
return new CallSiteDto(
|
||||
Method: site.Method,
|
||||
Type: site.ContainingType,
|
||||
File: site.File,
|
||||
Line: site.Line,
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user