release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) StellaOps
|
||||
// Sprint: EVID-001-002 - Reachability Evidence Endpoints
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Scanner.Reachability.Jobs;
|
||||
using StellaOps.Scanner.Reachability.Services;
|
||||
using StellaOps.Scanner.Reachability.Vex;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal API endpoints for reachability evidence operations.
|
||||
/// </summary>
|
||||
public static class ReachabilityEvidenceEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps reachability evidence endpoints.
|
||||
/// </summary>
|
||||
public static IEndpointRouteBuilder MapReachabilityEvidenceEndpoints(
|
||||
this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/reachability")
|
||||
.WithTags("Reachability Evidence");
|
||||
|
||||
// Analyze reachability for a CVE
|
||||
group.MapPost("/analyze", AnalyzeAsync)
|
||||
.WithName("AnalyzeReachability")
|
||||
.WithSummary("Analyze reachability of a CVE in an image")
|
||||
.Produces<ReachabilityAnalyzeResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
|
||||
// Get job result
|
||||
group.MapGet("/result/{jobId}", GetResultAsync)
|
||||
.WithName("GetReachabilityResult")
|
||||
.WithSummary("Get result of a reachability analysis job")
|
||||
.Produces<ReachabilityResultResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
|
||||
// Check if CVE has sink mappings
|
||||
group.MapGet("/mapping/{cveId}", GetCveMappingAsync)
|
||||
.WithName("GetCveMapping")
|
||||
.WithSummary("Get CVE-to-symbol mappings for a vulnerability")
|
||||
.Produces<CveMappingResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
||||
|
||||
// Get VEX statement from reachability
|
||||
group.MapPost("/vex", GenerateVexAsync)
|
||||
.WithName("GenerateVexFromReachability")
|
||||
.WithSummary("Generate VEX statement from reachability analysis")
|
||||
.Produces<VexStatementResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static async Task<IResult> AnalyzeAsync(
|
||||
[FromBody] ReachabilityAnalyzeRequest request,
|
||||
[FromServices] IReachabilityEvidenceJobExecutor executor,
|
||||
[FromServices] ICveSymbolMappingService mappingService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.ImageDigest) ||
|
||||
string.IsNullOrWhiteSpace(request.CveId) ||
|
||||
string.IsNullOrWhiteSpace(request.Purl))
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "imageDigest, cveId, and purl are required",
|
||||
statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
// Check if we have mappings for this CVE
|
||||
var hasMappings = await mappingService.HasMappingAsync(request.CveId, ct);
|
||||
if (!hasMappings)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: $"No sink mappings found for CVE {request.CveId}",
|
||||
statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
// Create and execute job
|
||||
var jobId = ReachabilityEvidenceJob.ComputeJobId(
|
||||
request.ImageDigest, request.CveId, request.Purl);
|
||||
|
||||
var job = new ReachabilityEvidenceJob
|
||||
{
|
||||
JobId = jobId,
|
||||
ImageDigest = request.ImageDigest,
|
||||
CveId = request.CveId,
|
||||
Purl = request.Purl,
|
||||
SourceCommit = request.SourceCommit,
|
||||
Options = new ReachabilityJobOptions
|
||||
{
|
||||
IncludeL2 = request.IncludeBinaryAnalysis,
|
||||
IncludeL3 = request.IncludeRuntimeAnalysis,
|
||||
MaxPathsPerSink = request.MaxPaths ?? 5,
|
||||
MaxDepth = request.MaxDepth ?? 256
|
||||
},
|
||||
QueuedAt = timeProvider.GetUtcNow()
|
||||
};
|
||||
|
||||
var result = await executor.ExecuteAsync(job, ct);
|
||||
|
||||
return Results.Ok(new ReachabilityAnalyzeResponse
|
||||
{
|
||||
JobId = result.JobId,
|
||||
Status = result.Status.ToString(),
|
||||
Verdict = result.Stack?.Verdict.ToString(),
|
||||
EvidenceUri = result.EvidenceUri,
|
||||
DurationMs = result.DurationMs,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetResultAsync(
|
||||
[FromRoute] string jobId,
|
||||
[FromServices] IEvidenceResultStore resultStore,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = await resultStore.GetResultAsync(jobId, ct);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: $"No result found for job {jobId}",
|
||||
statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
return Results.Ok(new ReachabilityResultResponse
|
||||
{
|
||||
JobId = result.JobId,
|
||||
Status = result.Status.ToString(),
|
||||
Verdict = result.Stack?.Verdict.ToString(),
|
||||
VerdictExplanation = result.Stack?.Explanation,
|
||||
IsReachable = result.Stack?.StaticCallGraph.IsReachable,
|
||||
PathCount = result.Stack?.StaticCallGraph.Paths.Length ?? 0,
|
||||
EntrypointCount = result.Stack?.StaticCallGraph.ReachingEntrypoints.Length ?? 0,
|
||||
EvidenceBundleId = result.EvidenceBundleId,
|
||||
EvidenceUri = result.EvidenceUri,
|
||||
CompletedAt = result.CompletedAt,
|
||||
DurationMs = result.DurationMs,
|
||||
Error = result.Error
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetCveMappingAsync(
|
||||
[FromRoute] string cveId,
|
||||
[FromQuery] string? purl,
|
||||
[FromServices] ICveSymbolMappingService mappingService,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var mappings = string.IsNullOrWhiteSpace(purl)
|
||||
? await mappingService.GetAllMappingsForCveAsync(cveId, ct)
|
||||
: await mappingService.GetSinksForCveAsync(cveId, purl, ct);
|
||||
|
||||
if (mappings.Count == 0)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: $"No mappings found for CVE {cveId}",
|
||||
statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
return Results.Ok(new CveMappingResponse
|
||||
{
|
||||
CveId = cveId,
|
||||
MappingCount = mappings.Count,
|
||||
Mappings = mappings.Select(m => new CveMappingItem
|
||||
{
|
||||
SymbolName = m.SymbolName,
|
||||
CanonicalId = m.CanonicalId,
|
||||
Purl = m.Purl,
|
||||
FilePath = m.FilePath,
|
||||
VulnType = m.VulnType.ToString(),
|
||||
Confidence = m.Confidence,
|
||||
Source = m.Source.ToString()
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> GenerateVexAsync(
|
||||
[FromBody] GenerateVexRequest request,
|
||||
[FromServices] IEvidenceResultStore resultStore,
|
||||
[FromServices] IVexStatusDeterminer vexDeterminer,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.JobId) ||
|
||||
string.IsNullOrWhiteSpace(request.ProductId))
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "jobId and productId are required",
|
||||
statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
|
||||
var result = await resultStore.GetResultAsync(request.JobId, ct);
|
||||
|
||||
if (result?.Stack is null)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: $"No reachability result found for job {request.JobId}",
|
||||
statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
|
||||
var evidenceUris = result.EvidenceUri is not null
|
||||
? new[] { result.EvidenceUri }
|
||||
: Array.Empty<string>();
|
||||
|
||||
var statement = vexDeterminer.CreateStatement(
|
||||
result.Stack,
|
||||
request.ProductId,
|
||||
evidenceUris);
|
||||
|
||||
return Results.Ok(new VexStatementResponse
|
||||
{
|
||||
StatementId = statement.StatementId,
|
||||
VulnerabilityId = statement.VulnerabilityId,
|
||||
ProductId = statement.ProductId,
|
||||
Status = statement.Status.ToString(),
|
||||
Justification = statement.Justification is not null ? new VexJustificationResponse
|
||||
{
|
||||
Category = statement.Justification.Category.ToString(),
|
||||
Detail = statement.Justification.Detail,
|
||||
Confidence = statement.Justification.Confidence,
|
||||
EvidenceReferences = statement.Justification.EvidenceReferences.ToList()
|
||||
} : null,
|
||||
ActionStatement = statement.ActionStatement,
|
||||
ImpactStatement = statement.ImpactStatement,
|
||||
Timestamp = statement.Timestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Request/Response DTOs
|
||||
|
||||
public sealed record ReachabilityAnalyzeRequest
|
||||
{
|
||||
public string ImageDigest { get; init; } = string.Empty;
|
||||
public string CveId { get; init; } = string.Empty;
|
||||
public string Purl { get; init; } = string.Empty;
|
||||
public string? SourceCommit { get; init; }
|
||||
public bool IncludeBinaryAnalysis { get; init; } = false;
|
||||
public bool IncludeRuntimeAnalysis { get; init; } = false;
|
||||
public int? MaxPaths { get; init; }
|
||||
public int? MaxDepth { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ReachabilityAnalyzeResponse
|
||||
{
|
||||
public string JobId { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string? Verdict { get; init; }
|
||||
public string? EvidenceUri { get; init; }
|
||||
public long? DurationMs { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ReachabilityResultResponse
|
||||
{
|
||||
public string JobId { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public string? Verdict { get; init; }
|
||||
public string? VerdictExplanation { get; init; }
|
||||
public bool? IsReachable { get; init; }
|
||||
public int PathCount { get; init; }
|
||||
public int EntrypointCount { get; init; }
|
||||
public string? EvidenceBundleId { get; init; }
|
||||
public string? EvidenceUri { get; init; }
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
public long? DurationMs { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
public sealed record CveMappingResponse
|
||||
{
|
||||
public string CveId { get; init; } = string.Empty;
|
||||
public int MappingCount { get; init; }
|
||||
public List<CveMappingItem> Mappings { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed record CveMappingItem
|
||||
{
|
||||
public string SymbolName { get; init; } = string.Empty;
|
||||
public string? CanonicalId { get; init; }
|
||||
public string Purl { get; init; } = string.Empty;
|
||||
public string? FilePath { get; init; }
|
||||
public string VulnType { get; init; } = string.Empty;
|
||||
public decimal Confidence { get; init; }
|
||||
public string Source { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record GenerateVexRequest
|
||||
{
|
||||
public string JobId { get; init; } = string.Empty;
|
||||
public string ProductId { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record VexStatementResponse
|
||||
{
|
||||
public string StatementId { get; init; } = string.Empty;
|
||||
public string VulnerabilityId { get; init; } = string.Empty;
|
||||
public string ProductId { get; init; } = string.Empty;
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public VexJustificationResponse? Justification { get; init; }
|
||||
public string? ActionStatement { get; init; }
|
||||
public string? ImpactStatement { get; init; }
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
}
|
||||
|
||||
public sealed record VexJustificationResponse
|
||||
{
|
||||
public string Category { get; init; } = string.Empty;
|
||||
public string Detail { get; init; } = string.Empty;
|
||||
public decimal Confidence { get; init; }
|
||||
public List<string> EvidenceReferences { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store for evidence job results.
|
||||
/// </summary>
|
||||
public interface IEvidenceResultStore
|
||||
{
|
||||
Task<ReachabilityEvidenceJobResult?> GetResultAsync(string jobId, CancellationToken ct);
|
||||
Task StoreResultAsync(ReachabilityEvidenceJobResult result, CancellationToken ct);
|
||||
}
|
||||
Reference in New Issue
Block a user