save checkpoint: save features

This commit is contained in:
master
2026-02-12 10:27:23 +02:00
parent dca86e1248
commit 5bca406787
8837 changed files with 1796879 additions and 5294 deletions

View File

@@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.SmartDiff.Detection;
using StellaOps.Scanner.SmartDiff.Output;
using StellaOps.Scanner.Storage.Postgres;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Collections.Immutable;
using System.Text;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -41,6 +39,21 @@ internal static class SmartDiffEndpoints
.RequireAuthorization(ScannerPolicies.ScansRead);
// VEX candidate endpoints
group.MapGet("/{scanId}/vex-candidates", HandleGetScanCandidatesAsync)
.WithName("scanner.smartdiff.scan-candidates")
.WithTags("SmartDiff")
.Produces<VexCandidatesResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
group.MapPost("/{scanId}/vex-candidates/review", HandleReviewScanCandidateAsync)
.WithName("scanner.smartdiff.scan-review")
.WithTags("SmartDiff")
.Produces<ReviewResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansWrite);
group.MapGet("/images/{digest}/candidates", HandleGetCandidatesAsync)
.WithName("scanner.smartdiff.candidates")
.WithTags("SmartDiff")
@@ -89,12 +102,19 @@ internal static class SmartDiffEndpoints
var metadata = await metadataRepo.GetScanMetadataAsync(scanId, ct);
if (metadata is not null)
{
baseDigest = metadata.BaseDigest;
targetDigest = metadata.TargetDigest;
baseDigest = NormalizeDigest(metadata.BaseDigest);
targetDigest = NormalizeDigest(metadata.TargetDigest);
scanTime = metadata.ScanTime;
}
}
IReadOnlyList<StellaOps.Scanner.SmartDiff.Output.VexCandidate> vexCandidates = [];
if (!string.IsNullOrWhiteSpace(targetDigest))
{
var candidates = await candidateStore.GetCandidatesAsync(targetDigest, ct).ConfigureAwait(false);
vexCandidates = candidates.Select(ToSarifVexCandidate).ToList();
}
// Convert to SARIF input format
var sarifInput = new SmartDiffSarifInput(
ScannerVersion: GetScannerVersion(),
@@ -112,7 +132,7 @@ internal static class SmartDiffEndpoints
))
.ToList(),
HardeningRegressions: [],
VexCandidates: [],
VexCandidates: vexCandidates,
ReachabilityChanges: []);
// Generate SARIF
@@ -135,6 +155,27 @@ internal static class SmartDiffEndpoints
statusCode: StatusCodes.Status200OK);
}
/// <summary>
/// GET /smart-diff/{scanId}/vex-candidates - Get VEX candidates using scan metadata.
/// </summary>
private static async Task<IResult> HandleGetScanCandidatesAsync(
string scanId,
IScanMetadataRepository metadataRepository,
IVexCandidateStore store,
double? minConfidence = null,
bool? pendingOnly = null,
CancellationToken ct = default)
{
var metadata = await metadataRepository.GetScanMetadataAsync(scanId, ct).ConfigureAwait(false);
var targetDigest = NormalizeDigest(metadata?.TargetDigest);
if (string.IsNullOrWhiteSpace(targetDigest))
{
return Results.NotFound(new { error = "Scan metadata not found", scanId });
}
return await HandleGetCandidatesAsync(targetDigest, store, minConfidence, pendingOnly, ct).ConfigureAwait(false);
}
private static StellaOps.Scanner.SmartDiff.Output.RiskDirection ToSarifRiskDirection(MaterialRiskChangeResult change)
{
if (change.Changes.IsDefaultOrEmpty)
@@ -219,6 +260,11 @@ internal static class SmartDiffEndpoints
CancellationToken ct = default)
{
var normalizedDigest = NormalizeDigest(digest);
if (string.IsNullOrWhiteSpace(normalizedDigest))
{
return Results.BadRequest(new { error = "Invalid image digest" });
}
var candidates = await store.GetCandidatesAsync(normalizedDigest, ct);
if (minConfidence.HasValue)
@@ -303,12 +349,76 @@ internal static class SmartDiffEndpoints
});
}
/// <summary>
/// POST /smart-diff/{scanId}/vex-candidates/review - Review a scan-scoped VEX candidate.
/// </summary>
private static async Task<IResult> HandleReviewScanCandidateAsync(
string scanId,
ScanReviewRequest request,
IScanMetadataRepository metadataRepository,
IVexCandidateStore store,
TimeProvider timeProvider,
HttpContext httpContext,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(request.CandidateId))
{
return Results.BadRequest(new { error = "CandidateId is required" });
}
var metadata = await metadataRepository.GetScanMetadataAsync(scanId, ct).ConfigureAwait(false);
var targetDigest = NormalizeDigest(metadata?.TargetDigest);
if (string.IsNullOrWhiteSpace(targetDigest))
{
return Results.NotFound(new { error = "Scan metadata not found", scanId });
}
var candidate = await store.GetCandidateAsync(request.CandidateId, ct).ConfigureAwait(false);
if (candidate is null || !string.Equals(candidate.ImageDigest, targetDigest, StringComparison.OrdinalIgnoreCase))
{
return Results.NotFound(new { error = "Candidate not found for scan", scanId, candidateId = request.CandidateId });
}
return await HandleReviewCandidateAsync(
request.CandidateId,
new ReviewRequest
{
Action = request.Action,
Comment = request.Comment
},
store,
timeProvider,
httpContext,
ct).ConfigureAwait(false);
}
#region Helper Methods
private static string NormalizeDigest(string digest)
private static string? NormalizeDigest(string? digest)
{
// Handle URL-encoded colons
return digest.Replace("%3A", ":", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(digest))
{
return null;
}
// Handle URL-encoded colons.
var normalized = digest.Trim().Replace("%3A", ":", StringComparison.OrdinalIgnoreCase);
if (!normalized.Contains(':', StringComparison.Ordinal))
{
normalized = $"sha256:{normalized}";
}
return normalized;
}
private static StellaOps.Scanner.SmartDiff.Output.VexCandidate ToSarifVexCandidate(
StellaOps.Scanner.SmartDiff.Detection.VexCandidate candidate)
{
return new StellaOps.Scanner.SmartDiff.Output.VexCandidate(
VulnId: candidate.FindingKey.VulnId,
ComponentPurl: candidate.FindingKey.ComponentPurl,
Justification: MapJustificationToString(candidate.Justification),
ImpactStatement: candidate.Rationale);
}
private static MaterialChangeDto ToChangeDto(MaterialRiskChangeResult change)
@@ -452,6 +562,14 @@ public sealed class ReviewRequest
public string? Comment { get; init; }
}
/// <summary>Request for POST /{scanId}/vex-candidates/review</summary>
public sealed class ScanReviewRequest
{
public required string CandidateId { get; init; }
public required string Action { get; init; }
public string? Comment { get; init; }
}
/// <summary>Response for POST /candidates/{id}/review</summary>
public sealed class ReviewResponse
{