Refactor SurfaceCacheValidator to simplify oldest entry calculation

Add global using for Xunit in test project

Enhance ImportValidatorTests with async validation and quarantine checks

Implement FileSystemQuarantineServiceTests for quarantine functionality

Add integration tests for ImportValidator to check monotonicity

Create BundleVersionTests to validate version parsing and comparison logic

Implement VersionMonotonicityCheckerTests for monotonicity checks and activation logic
This commit is contained in:
master
2025-12-16 10:44:00 +02:00
parent b1f40945b7
commit 4391f35d8a
107 changed files with 10844 additions and 287 deletions

View File

@@ -0,0 +1,188 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Constants;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Infrastructure;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Endpoints;
internal static class ExportEndpoints
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
WriteIndented = true
};
public static void MapExportEndpoints(this RouteGroupBuilder scansGroup)
{
ArgumentNullException.ThrowIfNull(scansGroup);
// GET /scans/{scanId}/exports/sarif
scansGroup.MapGet("/{scanId}/exports/sarif", HandleExportSarifAsync)
.WithName("scanner.scans.exports.sarif")
.WithTags("Exports")
.Produces(StatusCodes.Status200OK, contentType: "application/sarif+json")
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /scans/{scanId}/exports/cdxr
scansGroup.MapGet("/{scanId}/exports/cdxr", HandleExportCycloneDxRAsync)
.WithName("scanner.scans.exports.cdxr")
.WithTags("Exports")
.Produces(StatusCodes.Status200OK, contentType: "application/vnd.cyclonedx+json")
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /scans/{scanId}/exports/openvex
scansGroup.MapGet("/{scanId}/exports/openvex", HandleExportOpenVexAsync)
.WithName("scanner.scans.exports.openvex")
.WithTags("Exports")
.Produces(StatusCodes.Status200OK, contentType: "application/json")
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
}
private static async Task<IResult> HandleExportSarifAsync(
string scanId,
IScanCoordinator coordinator,
ISarifExportService exportService,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(exportService);
if (!ScanId.TryParse(scanId, out var parsed))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
}
var sarifDocument = await exportService.ExportAsync(parsed, cancellationToken).ConfigureAwait(false);
if (sarifDocument is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No findings available",
StatusCodes.Status404NotFound,
detail: "No findings available for SARIF export.");
}
var json = JsonSerializer.Serialize(sarifDocument, SerializerOptions);
return Results.Content(json, "application/sarif+json", System.Text.Encoding.UTF8, StatusCodes.Status200OK);
}
private static async Task<IResult> HandleExportCycloneDxRAsync(
string scanId,
IScanCoordinator coordinator,
ICycloneDxExportService exportService,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(exportService);
if (!ScanId.TryParse(scanId, out var parsed))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
}
var cdxDocument = await exportService.ExportWithReachabilityAsync(parsed, cancellationToken).ConfigureAwait(false);
if (cdxDocument is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No findings available",
StatusCodes.Status404NotFound,
detail: "No findings available for CycloneDX export.");
}
var json = JsonSerializer.Serialize(cdxDocument, SerializerOptions);
return Results.Content(json, "application/vnd.cyclonedx+json", System.Text.Encoding.UTF8, StatusCodes.Status200OK);
}
private static async Task<IResult> HandleExportOpenVexAsync(
string scanId,
IScanCoordinator coordinator,
IOpenVexExportService exportService,
HttpContext context,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(coordinator);
ArgumentNullException.ThrowIfNull(exportService);
if (!ScanId.TryParse(scanId, out var parsed))
{
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
if (snapshot is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
}
var vexDocument = await exportService.ExportAsync(parsed, cancellationToken).ConfigureAwait(false);
if (vexDocument is null)
{
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No VEX data available",
StatusCodes.Status404NotFound,
detail: "No VEX data available for export.");
}
var json = JsonSerializer.Serialize(vexDocument, SerializerOptions);
return Results.Content(json, "application/json", System.Text.Encoding.UTF8, StatusCodes.Status200OK);
}
}