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
{

View File

@@ -30,8 +30,10 @@ using StellaOps.Scanner.Core.TrustAnchors;
using StellaOps.Scanner.Emit.Composition;
using StellaOps.Scanner.Gate;
using StellaOps.Scanner.ReachabilityDrift.DependencyInjection;
using StellaOps.Scanner.SmartDiff.Detection;
using StellaOps.Scanner.Storage;
using StellaOps.Scanner.Storage.Extensions;
using StellaOps.Scanner.Storage.Postgres;
using StellaOps.Scanner.Surface.Env;
using StellaOps.Scanner.Surface.FS;
using StellaOps.Scanner.Surface.Secrets;
@@ -180,6 +182,9 @@ builder.Services.AddSingleton<ICounterfactualApiService, CounterfactualApiServic
builder.Services.TryAddSingleton<IVexGateResultsStore, InMemoryVexGateResultsStore>();
builder.Services.TryAddSingleton<IVexGateQueryService, VexGateQueryService>();
builder.Services.TryAddSingleton<IVexReachabilityDecisionFilter, VexReachabilityDecisionFilter>();
builder.Services.TryAddSingleton<IMaterialRiskChangeRepository, PostgresMaterialRiskChangeRepository>();
builder.Services.TryAddSingleton<IVexCandidateStore, PostgresVexCandidateStore>();
builder.Services.TryAddSingleton<IScanMetadataRepository, InMemoryScanMetadataRepository>();
// Secret Detection Settings (Sprint: SPRINT_20260104_006_BE)
builder.Services.AddScoped<ISecretDetectionSettingsService, SecretDetectionSettingsService>();
@@ -607,6 +612,7 @@ apiGroup.MapScanEndpoints(resolvedOptions.Api.ScansSegment);
apiGroup.MapSbomUploadEndpoints();
apiGroup.MapReachabilityDriftRootEndpoints();
apiGroup.MapDeltaCompareEndpoints();
apiGroup.MapSmartDiffEndpoints();
apiGroup.MapBaselineEndpoints();
apiGroup.MapActionablesEndpoints();
apiGroup.MapCounterfactualEndpoints();

View File

@@ -0,0 +1,54 @@
using StellaOps.Scanner.Core;
namespace StellaOps.Scanner.WebService.Services;
/// <summary>
/// Resolves scan metadata from stored scan manifests.
/// </summary>
public sealed class InMemoryScanMetadataRepository : IScanMetadataRepository
{
private readonly IScanManifestRepository _manifestRepository;
public InMemoryScanMetadataRepository(IScanManifestRepository manifestRepository)
{
_manifestRepository = manifestRepository ?? throw new ArgumentNullException(nameof(manifestRepository));
}
public async Task<ScanMetadata?> GetScanMetadataAsync(string scanId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(scanId))
{
return null;
}
var manifest = await _manifestRepository
.GetManifestAsync(scanId.Trim(), cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (manifest is null)
{
return null;
}
return new ScanMetadata(
BaseDigest: null,
TargetDigest: NormalizeDigest(manifest.Manifest.ArtifactDigest),
ScanTime: manifest.Manifest.CreatedAtUtc);
}
private static string? NormalizeDigest(string? digest)
{
if (string.IsNullOrWhiteSpace(digest))
{
return null;
}
var normalized = digest.Trim();
if (!normalized.Contains(':', StringComparison.Ordinal))
{
normalized = $"sha256:{normalized}";
}
return normalized.ToLowerInvariant();
}
}

View File

@@ -14,3 +14,4 @@ Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_appl
| SPRINT-20260208-063-TRIAGE-001 | DONE | Implement triage cluster batch action and cluster statistics endpoints for sprint 063 (2026-02-08). |
| HOT-003 | DONE | `SPRINT_20260210_001_DOCS_sbom_attestation_hot_lookup_contract.md`: wired SBOM ingestion projection writes into Scanner WebService pipeline. |
| HOT-004 | DONE | `SPRINT_20260210_001_DOCS_sbom_attestation_hot_lookup_contract.md`: added SBOM hot-lookup read endpoints with bounded pagination. |
| SPRINT-20260212-002-SMARTDIFF-001 | DONE | `SPRINT_20260212_002_Scanner_unchecked_feature_verification_batch1.md`: wired SmartDiff endpoints into Program, added scan-scoped VEX candidate/review API compatibility, and embedded VEX candidates in SARIF output (2026-02-12). |