save checkpoint: save features
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user