release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -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);
}