save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -0,0 +1,346 @@
// <copyright file="ValidationEndpoints.cs" company="StellaOps">
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
// </copyright>
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Validation;
using StellaOps.Scanner.WebService.Security;
namespace StellaOps.Scanner.WebService.Endpoints;
/// <summary>
/// SBOM validation endpoints.
/// Sprint: SPRINT_20260107_005_003 Task VG-006
/// </summary>
internal static class ValidationEndpoints
{
/// <summary>
/// Maps SBOM validation endpoints.
/// </summary>
public static void MapValidationEndpoints(this IEndpointRouteBuilder app)
{
ArgumentNullException.ThrowIfNull(app);
var group = app.MapGroup("/api/v1/sbom")
.WithTags("Validation")
.RequireAuthorization();
// POST /api/v1/sbom/validate
group.MapPost("/validate", ValidateSbomAsync)
.WithName("scanner.sbom.validate")
.WithDescription("Validates an SBOM document against CycloneDX or SPDX schemas")
.Accepts<byte[]>(
"application/vnd.cyclonedx+json",
"application/vnd.cyclonedx+xml",
"application/spdx+json",
"text/spdx",
"application/json",
"application/octet-stream")
.Produces<SbomValidationResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status503ServiceUnavailable)
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /api/v1/sbom/validators
group.MapGet("/validators", GetValidatorsAsync)
.WithName("scanner.sbom.validators")
.WithDescription("Gets information about available SBOM validators")
.Produces<ValidatorsInfoResponseDto>(StatusCodes.Status200OK)
.RequireAuthorization(ScannerPolicies.ScansRead);
}
private static async Task<IResult> ValidateSbomAsync(
HttpContext context,
[FromServices] CompositeValidator validator,
[FromServices] IOptions<ValidationGateOptions> options,
[FromQuery] string? format = null,
CancellationToken cancellationToken = default)
{
var gateOptions = options.Value;
// Check if validation is enabled
if (!gateOptions.Enabled || gateOptions.Mode == SbomValidationMode.Off)
{
return Results.Ok(new SbomValidationResponseDto
{
IsValid = true,
Format = "unknown",
ValidatorName = "validation-disabled",
ValidatorVersion = "n/a",
Message = "Validation is disabled",
Diagnostics = Array.Empty<ValidationDiagnosticDto>()
});
}
// Read request body
using var memoryStream = new MemoryStream();
await context.Request.Body.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
var sbomBytes = memoryStream.ToArray();
if (sbomBytes.Length == 0)
{
return Results.BadRequest(new ProblemDetails
{
Title = "Empty request body",
Detail = "SBOM document is required",
Status = StatusCodes.Status400BadRequest
});
}
// Determine format
SbomFormat sbomFormat;
if (!string.IsNullOrWhiteSpace(format))
{
if (!Enum.TryParse<SbomFormat>(format, ignoreCase: true, out sbomFormat))
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid format",
Detail = $"Unknown SBOM format: {format}. Supported: CycloneDxJson, CycloneDxXml, Spdx23Json, Spdx23TagValue, Spdx3JsonLd",
Status = StatusCodes.Status400BadRequest
});
}
}
else
{
// Auto-detect format
sbomFormat = CompositeValidator.DetectFormat(sbomBytes);
if (sbomFormat == SbomFormat.Unknown)
{
// Try content-type header
sbomFormat = DetectFormatFromContentType(context.Request.ContentType);
}
}
// Run validation
var validationOptions = gateOptions.ToValidationOptions();
SbomValidationResult result;
if (sbomFormat == SbomFormat.Unknown)
{
result = await validator.ValidateAutoAsync(sbomBytes, validationOptions, cancellationToken)
.ConfigureAwait(false);
}
else
{
result = await validator.ValidateAsync(sbomBytes, sbomFormat, validationOptions, cancellationToken)
.ConfigureAwait(false);
}
// Check if validator is available
if (result.Diagnostics.Any(d => d.Code == "VALIDATOR_UNAVAILABLE"))
{
return Results.Problem(
title: "Validator unavailable",
detail: result.Diagnostics.FirstOrDefault(d => d.Code == "VALIDATOR_UNAVAILABLE")?.Message,
statusCode: StatusCodes.Status503ServiceUnavailable);
}
var response = new SbomValidationResponseDto
{
IsValid = result.IsValid,
Format = result.Format.ToString(),
ValidatorName = result.ValidatorName,
ValidatorVersion = result.ValidatorVersion,
ValidationDurationMs = (int)result.ValidationDuration.TotalMilliseconds,
ErrorCount = result.ErrorCount,
WarningCount = result.WarningCount,
SchemaVersion = result.SchemaVersion,
Diagnostics = result.Diagnostics.Select(d => new ValidationDiagnosticDto
{
Severity = d.Severity.ToString(),
Code = d.Code,
Message = d.Message,
Path = d.Path,
Line = d.Line,
Suggestion = d.Suggestion
}).ToArray()
};
return Results.Ok(response);
}
private static async Task<IResult> GetValidatorsAsync(
[FromServices] CompositeValidator validator,
CancellationToken cancellationToken)
{
var info = await validator.GetInfoAsync(cancellationToken).ConfigureAwait(false);
var response = new ValidatorsInfoResponseDto
{
IsAvailable = info.IsAvailable,
Name = info.Name,
Version = info.Version,
SupportedFormats = info.SupportedFormats.Select(f => f.ToString()).ToArray(),
SupportedSchemaVersions = info.SupportedSchemaVersions.ToArray()
};
return Results.Ok(response);
}
private static SbomFormat DetectFormatFromContentType(string? contentType)
{
if (string.IsNullOrWhiteSpace(contentType))
{
return SbomFormat.Unknown;
}
return contentType.ToLowerInvariant() switch
{
var ct when ct.Contains("cyclonedx+json") => SbomFormat.CycloneDxJson,
var ct when ct.Contains("cyclonedx+xml") || ct.Contains("cyclonedx") && ct.Contains("xml") => SbomFormat.CycloneDxXml,
var ct when ct.Contains("spdx+json") || ct.Contains("spdx") && ct.Contains("json") => SbomFormat.Spdx23Json,
var ct when ct.Contains("text/spdx") => SbomFormat.Spdx23TagValue,
_ => SbomFormat.Unknown
};
}
}
/// <summary>
/// Response DTO for SBOM validation.
/// </summary>
public sealed class SbomValidationResponseDto
{
/// <summary>
/// Gets or sets whether the SBOM is valid.
/// </summary>
[JsonPropertyName("isValid")]
public bool IsValid { get; set; }
/// <summary>
/// Gets or sets the SBOM format.
/// </summary>
[JsonPropertyName("format")]
public required string Format { get; set; }
/// <summary>
/// Gets or sets the validator name.
/// </summary>
[JsonPropertyName("validatorName")]
public required string ValidatorName { get; set; }
/// <summary>
/// Gets or sets the validator version.
/// </summary>
[JsonPropertyName("validatorVersion")]
public required string ValidatorVersion { get; set; }
/// <summary>
/// Gets or sets the validation duration in milliseconds.
/// </summary>
[JsonPropertyName("validationDurationMs")]
public int ValidationDurationMs { get; set; }
/// <summary>
/// Gets or sets the error count.
/// </summary>
[JsonPropertyName("errorCount")]
public int ErrorCount { get; set; }
/// <summary>
/// Gets or sets the warning count.
/// </summary>
[JsonPropertyName("warningCount")]
public int WarningCount { get; set; }
/// <summary>
/// Gets or sets the schema version validated against.
/// </summary>
[JsonPropertyName("schemaVersion")]
public string? SchemaVersion { get; set; }
/// <summary>
/// Gets or sets a message (for disabled validation).
/// </summary>
[JsonPropertyName("message")]
public string? Message { get; set; }
/// <summary>
/// Gets or sets the validation diagnostics.
/// </summary>
[JsonPropertyName("diagnostics")]
public required ValidationDiagnosticDto[] Diagnostics { get; set; }
}
/// <summary>
/// DTO for a validation diagnostic.
/// </summary>
public sealed class ValidationDiagnosticDto
{
/// <summary>
/// Gets or sets the severity.
/// </summary>
[JsonPropertyName("severity")]
public required string Severity { get; set; }
/// <summary>
/// Gets or sets the diagnostic code.
/// </summary>
[JsonPropertyName("code")]
public required string Code { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
[JsonPropertyName("message")]
public required string Message { get; set; }
/// <summary>
/// Gets or sets the JSON path.
/// </summary>
[JsonPropertyName("path")]
public string? Path { get; set; }
/// <summary>
/// Gets or sets the line number.
/// </summary>
[JsonPropertyName("line")]
public int? Line { get; set; }
/// <summary>
/// Gets or sets a suggestion.
/// </summary>
[JsonPropertyName("suggestion")]
public string? Suggestion { get; set; }
}
/// <summary>
/// Response DTO for validators info.
/// </summary>
public sealed class ValidatorsInfoResponseDto
{
/// <summary>
/// Gets or sets whether validators are available.
/// </summary>
[JsonPropertyName("isAvailable")]
public bool IsAvailable { get; set; }
/// <summary>
/// Gets or sets the composite validator name.
/// </summary>
[JsonPropertyName("name")]
public required string Name { get; set; }
/// <summary>
/// Gets or sets the version.
/// </summary>
[JsonPropertyName("version")]
public required string Version { get; set; }
/// <summary>
/// Gets or sets the supported formats.
/// </summary>
[JsonPropertyName("supportedFormats")]
public required string[] SupportedFormats { get; set; }
/// <summary>
/// Gets or sets the supported schema versions.
/// </summary>
[JsonPropertyName("supportedSchemaVersions")]
public required string[] SupportedSchemaVersions { get; set; }
}