search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -30,7 +31,7 @@ internal static class ActionablesEndpoints
// GET /v1/actionables/delta/{deltaId} - Get actionables for a delta
group.MapGet("/delta/{deltaId}", HandleGetDeltaActionablesAsync)
.WithName("scanner.actionables.delta")
.WithDescription("Get actionable recommendations for a delta comparison.")
.WithDescription(_t("scanner.actionables.delta_description"))
.Produces<ActionablesResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -38,7 +39,7 @@ internal static class ActionablesEndpoints
// GET /v1/actionables/delta/{deltaId}/by-priority/{priority} - Filter by priority
group.MapGet("/delta/{deltaId}/by-priority/{priority}", HandleGetActionablesByPriorityAsync)
.WithName("scanner.actionables.by-priority")
.WithDescription("Get actionables filtered by priority level.")
.WithDescription(_t("scanner.actionables.by_priority_description"))
.Produces<ActionablesResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -46,7 +47,7 @@ internal static class ActionablesEndpoints
// GET /v1/actionables/delta/{deltaId}/by-type/{type} - Filter by type
group.MapGet("/delta/{deltaId}/by-type/{type}", HandleGetActionablesByTypeAsync)
.WithName("scanner.actionables.by-type")
.WithDescription("Get actionables filtered by action type.")
.WithDescription(_t("scanner.actionables.by_type_description"))
.Produces<ActionablesResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -65,8 +66,8 @@ internal static class ActionablesEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid delta ID",
detail = "Delta ID is required."
title = _t("scanner.actionables.invalid_delta_id"),
detail = _t("scanner.actionables.delta_id_required")
});
}
@@ -77,8 +78,8 @@ internal static class ActionablesEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Delta not found",
detail = $"Delta with ID '{deltaId}' was not found."
title = _t("scanner.actionables.delta_not_found"),
detail = _tn("scanner.actionables.delta_not_found_detail", ("deltaId", deltaId))
});
}
@@ -100,8 +101,8 @@ internal static class ActionablesEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid priority",
detail = $"Priority must be one of: {string.Join(", ", validPriorities)}"
title = _t("scanner.actionables.invalid_priority"),
detail = _tn("scanner.actionables.invalid_priority_detail", ("validPriorities", string.Join(", ", validPriorities)))
});
}
@@ -112,8 +113,8 @@ internal static class ActionablesEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Delta not found",
detail = $"Delta with ID '{deltaId}' was not found."
title = _t("scanner.actionables.delta_not_found"),
detail = _tn("scanner.actionables.delta_not_found_detail", ("deltaId", deltaId))
});
}
@@ -144,8 +145,8 @@ internal static class ActionablesEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid type",
detail = $"Type must be one of: {string.Join(", ", validTypes)}"
title = _t("scanner.actionables.invalid_type"),
detail = _tn("scanner.actionables.invalid_type_detail", ("validTypes", string.Join(", ", validTypes)))
});
}
@@ -156,8 +157,8 @@ internal static class ActionablesEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Delta not found",
detail = $"Delta with ID '{deltaId}' was not found."
title = _t("scanner.actionables.delta_not_found"),
detail = _tn("scanner.actionables.delta_not_found_detail", ("deltaId", deltaId))
});
}

View File

@@ -17,6 +17,7 @@ using StellaOps.Scanner.WebService.Services;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -42,7 +43,7 @@ internal static class ApprovalEndpoints
scansGroup.MapPost("/{scanId}/approvals", HandleCreateApprovalAsync)
.WithName("scanner.scans.approvals.create")
.WithTags("Approvals")
.WithDescription("Creates a human approval attestation for a finding.")
.WithDescription(_t("scanner.approval.create_description"))
.Produces<ApprovalResponse>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status401Unauthorized)
@@ -53,7 +54,7 @@ internal static class ApprovalEndpoints
scansGroup.MapGet("/{scanId}/approvals", HandleListApprovalsAsync)
.WithName("scanner.scans.approvals.list")
.WithTags("Approvals")
.WithDescription("Lists all active approvals for a scan.")
.WithDescription(_t("scanner.approval.list_description"))
.Produces<ApprovalListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -63,7 +64,7 @@ internal static class ApprovalEndpoints
scansGroup.MapGet("/{scanId}/approvals/{findingId}", HandleGetApprovalAsync)
.WithName("scanner.scans.approvals.get")
.WithTags("Approvals")
.WithDescription("Gets an approval for a specific finding.")
.WithDescription(_t("scanner.approval.get_description"))
.Produces<ApprovalResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -73,7 +74,7 @@ internal static class ApprovalEndpoints
scansGroup.MapDelete("/{scanId}/approvals/{findingId}", HandleRevokeApprovalAsync)
.WithName("scanner.scans.approvals.revoke")
.WithTags("Approvals")
.WithDescription("Revokes an existing approval.")
.WithDescription(_t("scanner.approval.revoke_description"))
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -97,9 +98,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (request is null)
@@ -107,7 +108,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Request body is required",
_t("scanner.approval.body_required"),
StatusCodes.Status400BadRequest);
}
@@ -116,7 +117,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"FindingId is required",
_t("scanner.approval.finding_id_required"),
StatusCodes.Status400BadRequest);
}
@@ -125,7 +126,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Justification is required",
_t("scanner.approval.justification_required"),
StatusCodes.Status400BadRequest);
}
@@ -136,9 +137,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Unable to identify approver",
_t("scanner.approval.unable_to_identify_approver"),
StatusCodes.Status401Unauthorized,
detail: "User identity could not be determined from the request.");
detail: _t("scanner.approval.approver_identity_required"));
}
// Parse the decision
@@ -147,9 +148,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid decision value",
_t("scanner.approval.invalid_decision"),
StatusCodes.Status400BadRequest,
detail: $"Decision must be one of: AcceptRisk, Defer, Reject, Suppress, Escalate. Got: {request.Decision}");
detail: _tn("scanner.approval.invalid_decision_detail", ("decision", request.Decision ?? "")));
}
// Create the approval
@@ -174,7 +175,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Internal,
"Failed to create approval",
_t("scanner.approval.create_failed"),
StatusCodes.Status500InternalServerError,
detail: result.Error);
}
@@ -215,9 +216,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var approvals = await approvalService.GetApprovalsByScanAsync(parsed, cancellationToken);
@@ -247,9 +248,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (string.IsNullOrWhiteSpace(findingId))
@@ -257,7 +258,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"FindingId is required",
_t("scanner.approval.finding_id_required"),
StatusCodes.Status400BadRequest);
}
@@ -268,9 +269,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Approval not found",
_t("scanner.approval.not_found"),
StatusCodes.Status404NotFound,
detail: $"No approval found for finding '{findingId}' in scan '{scanId}'.");
detail: _tn("scanner.approval.not_found_detail", ("findingId", findingId), ("scanId", scanId)));
}
return Results.Ok(MapToResponse(result, null));
@@ -292,9 +293,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (string.IsNullOrWhiteSpace(findingId))
@@ -302,7 +303,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"FindingId is required",
_t("scanner.approval.finding_id_required"),
StatusCodes.Status400BadRequest);
}
@@ -312,7 +313,7 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Unable to identify revoker",
_t("scanner.approval.unable_to_identify_revoker"),
StatusCodes.Status401Unauthorized);
}
@@ -330,9 +331,9 @@ internal static class ApprovalEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Approval not found",
_t("scanner.approval.not_found"),
StatusCodes.Status404NotFound,
detail: $"No approval found for finding '{findingId}' in scan '{scanId}'.");
detail: _tn("scanner.approval.not_found_detail", ("findingId", findingId), ("scanId", scanId)));
}
return Results.NoContent();

View File

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -30,7 +31,7 @@ internal static class BaselineEndpoints
// GET /v1/baselines/recommendations/{artifactDigest} - Get recommended baselines
group.MapGet("/recommendations/{artifactDigest}", HandleGetRecommendationsAsync)
.WithName("scanner.baselines.recommendations")
.WithDescription("Get recommended baselines for an artifact with rationale.")
.WithDescription(_t("scanner.baseline.recommendations_description"))
.Produces<BaselineRecommendationsResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -38,7 +39,7 @@ internal static class BaselineEndpoints
// GET /v1/baselines/rationale/{baseDigest}/{headDigest} - Get selection rationale
group.MapGet("/rationale/{baseDigest}/{headDigest}", HandleGetRationaleAsync)
.WithName("scanner.baselines.rationale")
.WithDescription("Get detailed rationale for a baseline selection.")
.WithDescription(_t("scanner.baseline.rationale_description"))
.Produces<BaselineRationaleResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -59,8 +60,8 @@ internal static class BaselineEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid artifact digest",
detail = "Artifact digest is required."
title = _t("scanner.baseline.invalid_artifact_digest"),
detail = _t("scanner.baseline.artifact_digest_required")
});
}
@@ -87,8 +88,8 @@ internal static class BaselineEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid base digest",
detail = "Base digest is required."
title = _t("scanner.baseline.invalid_base_digest"),
detail = _t("scanner.baseline.base_digest_required")
});
}
@@ -97,8 +98,8 @@ internal static class BaselineEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid head digest",
detail = "Head digest is required."
title = _t("scanner.baseline.invalid_head_digest"),
detail = _t("scanner.baseline.head_digest_required")
});
}
@@ -109,8 +110,8 @@ internal static class BaselineEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Baseline not found",
detail = $"No baseline found for base '{baseDigest}' and head '{headDigest}'."
title = _t("scanner.baseline.not_found"),
detail = _tn("scanner.baseline.not_found_detail", ("baseDigest", baseDigest), ("headDigest", headDigest))
});
}

View File

@@ -9,6 +9,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -58,9 +59,9 @@ internal static class CallGraphEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
// Validate Content-Digest header for idempotency
@@ -70,9 +71,9 @@ internal static class CallGraphEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Missing Content-Digest header",
_t("scanner.callgraph.missing_content_digest"),
StatusCodes.Status400BadRequest,
detail: "Content-Digest header is required for idempotent call graph submission.");
detail: _t("scanner.callgraph.content_digest_required"));
}
// Verify scan exists
@@ -82,9 +83,9 @@ internal static class CallGraphEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
// Validate call graph schema
@@ -99,9 +100,9 @@ internal static class CallGraphEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid call graph",
_t("scanner.callgraph.invalid"),
StatusCodes.Status400BadRequest,
detail: "Call graph validation failed.",
detail: _t("scanner.callgraph.validation_failed"),
extensions: extensions);
}
@@ -120,9 +121,9 @@ internal static class CallGraphEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Conflict,
"Duplicate call graph",
_t("scanner.callgraph.duplicate"),
StatusCodes.Status409Conflict,
detail: "Call graph with this Content-Digest already submitted.",
detail: _t("scanner.callgraph.duplicate_detail"),
extensions: conflictExtensions);
}

View File

@@ -12,6 +12,7 @@ using StellaOps.Policy.Counterfactuals;
using StellaOps.Scanner.WebService.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -40,7 +41,7 @@ internal static class CounterfactualEndpoints
// POST /v1/counterfactuals/compute - Compute counterfactuals for a finding
group.MapPost("/compute", HandleComputeAsync)
.WithName("scanner.counterfactuals.compute")
.WithDescription("Compute counterfactual paths for a blocked finding.")
.WithDescription(_t("scanner.counterfactual.compute_description"))
.Produces<CounterfactualResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -48,7 +49,7 @@ internal static class CounterfactualEndpoints
// GET /v1/counterfactuals/finding/{findingId} - Get counterfactuals for a finding
group.MapGet("/finding/{findingId}", HandleGetForFindingAsync)
.WithName("scanner.counterfactuals.finding")
.WithDescription("Get computed counterfactuals for a specific finding.")
.WithDescription(_t("scanner.counterfactual.finding_description"))
.Produces<CounterfactualResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -56,7 +57,7 @@ internal static class CounterfactualEndpoints
// GET /v1/counterfactuals/scan/{scanId}/summary - Get counterfactual summary for scan
group.MapGet("/scan/{scanId}/summary", HandleGetScanSummaryAsync)
.WithName("scanner.counterfactuals.scan-summary")
.WithDescription("Get counterfactual summary for all blocked findings in a scan.")
.WithDescription(_t("scanner.counterfactual.scan_summary_description"))
.Produces<CounterfactualScanSummaryDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -74,8 +75,8 @@ internal static class CounterfactualEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid request",
detail = "Request body is required."
title = _t("scanner.counterfactual.invalid_request"),
detail = _t("scanner.counterfactual.body_required")
});
}
@@ -84,8 +85,8 @@ internal static class CounterfactualEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid finding ID",
detail = "Finding ID is required."
title = _t("scanner.counterfactual.invalid_finding_id"),
detail = _t("scanner.counterfactual.finding_id_required")
});
}
@@ -105,8 +106,8 @@ internal static class CounterfactualEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid finding ID",
detail = "Finding ID is required."
title = _t("scanner.counterfactual.invalid_finding_id"),
detail = _t("scanner.counterfactual.finding_id_required")
});
}
@@ -117,8 +118,8 @@ internal static class CounterfactualEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Counterfactuals not found",
detail = $"No counterfactuals found for finding '{findingId}'."
title = _t("scanner.counterfactual.not_found"),
detail = _tn("scanner.counterfactual.not_found_detail", ("findingId", findingId))
});
}
@@ -137,8 +138,8 @@ internal static class CounterfactualEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid scan ID",
detail = "Scan ID is required."
title = _t("scanner.counterfactual.invalid_scan_id"),
detail = _t("scanner.counterfactual.scan_id_required")
});
}
@@ -149,8 +150,8 @@ internal static class CounterfactualEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Scan not found",
detail = $"Scan '{scanId}' was not found."
title = _t("scanner.scan.not_found"),
detail = _tn("scanner.counterfactual.scan_not_found_detail", ("scanId", scanId))
});
}

View File

@@ -13,6 +13,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -41,7 +42,7 @@ internal static class DeltaCompareEndpoints
// POST /v1/delta/compare - Full comparison between two snapshots
group.MapPost("/compare", HandleCompareAsync)
.WithName("scanner.delta.compare")
.WithDescription("Compares two scan snapshots and returns detailed delta.")
.WithDescription(_t("scanner.delta.compare_description"))
.Produces<DeltaCompareResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -49,7 +50,7 @@ internal static class DeltaCompareEndpoints
// GET /v1/delta/quick - Quick summary for header display
group.MapGet("/quick", HandleQuickDiffAsync)
.WithName("scanner.delta.quick")
.WithDescription("Returns quick diff summary for Can I Ship header.")
.WithDescription(_t("scanner.delta.quick_description"))
.Produces<QuickDiffSummaryDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -57,7 +58,7 @@ internal static class DeltaCompareEndpoints
// GET /v1/delta/{comparisonId} - Get cached comparison by ID
group.MapGet("/{comparisonId}", HandleGetComparisonAsync)
.WithName("scanner.delta.get")
.WithDescription("Retrieves a cached comparison result by ID.")
.WithDescription(_t("scanner.delta.get_description"))
.Produces<DeltaCompareResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -77,8 +78,8 @@ internal static class DeltaCompareEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid base digest",
detail = "Base digest is required."
title = _t("scanner.delta.invalid_base_digest"),
detail = _t("scanner.delta.base_digest_required")
});
}
@@ -87,8 +88,8 @@ internal static class DeltaCompareEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid target digest",
detail = "Target digest is required."
title = _t("scanner.delta.invalid_target_digest"),
detail = _t("scanner.delta.target_digest_required")
});
}
@@ -110,8 +111,8 @@ internal static class DeltaCompareEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid base digest",
detail = "Base digest is required."
title = _t("scanner.delta.invalid_base_digest"),
detail = _t("scanner.delta.base_digest_required")
});
}
@@ -120,8 +121,8 @@ internal static class DeltaCompareEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid target digest",
detail = "Target digest is required."
title = _t("scanner.delta.invalid_target_digest"),
detail = _t("scanner.delta.target_digest_required")
});
}
@@ -142,8 +143,8 @@ internal static class DeltaCompareEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid comparison ID",
detail = "Comparison ID is required."
title = _t("scanner.delta.invalid_comparison_id"),
detail = _t("scanner.delta.comparison_id_required")
});
}
@@ -153,8 +154,8 @@ internal static class DeltaCompareEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Comparison not found",
detail = $"Comparison with ID '{comparisonId}' was not found or has expired."
title = _t("scanner.delta.comparison_not_found"),
detail = _tn("scanner.delta.comparison_not_found_detail", ("comparisonId", comparisonId))
});
}

View File

@@ -11,6 +11,7 @@ using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -39,7 +40,7 @@ internal static class DeltaEvidenceEndpoints
// GET /v1/delta/evidence/{comparisonId} - Get evidence bundle for a comparison
group.MapGet("/{comparisonId}", HandleGetComparisonEvidenceAsync)
.WithName("scanner.delta.evidence.comparison")
.WithDescription("Get complete evidence bundle for a delta comparison.")
.WithDescription(_t("scanner.delta_evidence.comparison_description"))
.Produces<DeltaEvidenceBundleDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -47,7 +48,7 @@ internal static class DeltaEvidenceEndpoints
// GET /v1/delta/evidence/{comparisonId}/finding/{findingId} - Get evidence for a specific finding change
group.MapGet("/{comparisonId}/finding/{findingId}", HandleGetFindingChangeEvidenceAsync)
.WithName("scanner.delta.evidence.finding")
.WithDescription("Get evidence for a specific finding's change in a delta.")
.WithDescription(_t("scanner.delta_evidence.finding_description"))
.Produces<DeltaFindingEvidenceDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -55,7 +56,7 @@ internal static class DeltaEvidenceEndpoints
// GET /v1/delta/evidence/{comparisonId}/proof-bundle - Get downloadable proof bundle
group.MapGet("/{comparisonId}/proof-bundle", HandleGetProofBundleAsync)
.WithName("scanner.delta.evidence.proof-bundle")
.WithDescription("Get downloadable proof bundle for audit/compliance.")
.WithDescription(_t("scanner.delta_evidence.proof_bundle_description"))
.Produces(StatusCodes.Status200OK, contentType: "application/zip")
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -63,7 +64,7 @@ internal static class DeltaEvidenceEndpoints
// GET /v1/delta/evidence/{comparisonId}/attestations - Get attestation chain
group.MapGet("/{comparisonId}/attestations", HandleGetAttestationsAsync)
.WithName("scanner.delta.evidence.attestations")
.WithDescription("Get attestation chain for a delta comparison.")
.WithDescription(_t("scanner.delta_evidence.attestations_description"))
.Produces<DeltaAttestationsDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -82,8 +83,8 @@ internal static class DeltaEvidenceEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid comparison ID",
detail = "Comparison ID is required."
title = _t("scanner.delta.invalid_comparison_id"),
detail = _t("scanner.delta.comparison_id_required")
});
}
@@ -94,8 +95,8 @@ internal static class DeltaEvidenceEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Comparison not found",
detail = $"Comparison with ID '{comparisonId}' was not found."
title = _t("scanner.delta.comparison_not_found"),
detail = _tn("scanner.delta_evidence.comparison_not_found_detail", ("comparisonId", comparisonId))
});
}
@@ -116,8 +117,8 @@ internal static class DeltaEvidenceEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid identifiers",
detail = "Both comparison ID and finding ID are required."
title = _t("scanner.delta_evidence.invalid_identifiers"),
detail = _t("scanner.delta_evidence.identifiers_required")
});
}
@@ -128,8 +129,8 @@ internal static class DeltaEvidenceEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Finding not found",
detail = $"Finding '{findingId}' not found in comparison '{comparisonId}'."
title = _t("scanner.delta_evidence.finding_not_found"),
detail = _tn("scanner.delta_evidence.finding_not_found_detail", ("findingId", findingId), ("comparisonId", comparisonId))
});
}
@@ -150,8 +151,8 @@ internal static class DeltaEvidenceEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid comparison ID",
detail = "Comparison ID is required."
title = _t("scanner.delta.invalid_comparison_id"),
detail = _t("scanner.delta.comparison_id_required")
});
}
@@ -162,8 +163,8 @@ internal static class DeltaEvidenceEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Proof bundle not found",
detail = $"Proof bundle for comparison '{comparisonId}' was not found."
title = _t("scanner.delta_evidence.proof_bundle_not_found"),
detail = _tn("scanner.delta_evidence.proof_bundle_not_found_detail", ("comparisonId", comparisonId))
});
}
@@ -186,8 +187,8 @@ internal static class DeltaEvidenceEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid comparison ID",
detail = "Comparison ID is required."
title = _t("scanner.delta.invalid_comparison_id"),
detail = _t("scanner.delta.comparison_id_required")
});
}
@@ -198,8 +199,8 @@ internal static class DeltaEvidenceEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Attestations not found",
detail = $"Attestations for comparison '{comparisonId}' were not found."
title = _t("scanner.delta_evidence.attestations_not_found"),
detail = _tn("scanner.delta_evidence.attestations_not_found_detail", ("comparisonId", comparisonId))
});
}

View File

@@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.Core.Epss;
using StellaOps.Scanner.WebService.Security;
using System.ComponentModel.DataAnnotations;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -79,8 +80,8 @@ public static class EpssEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid request",
Detail = "At least one CVE ID is required.",
Title = _t("scanner.epss.invalid_request"),
Detail = _t("scanner.epss.cve_id_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -89,8 +90,8 @@ public static class EpssEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Batch size exceeded",
Detail = "Maximum batch size is 1000 CVE IDs.",
Title = _t("scanner.epss.batch_size_exceeded"),
Detail = _t("scanner.epss.batch_size_exceeded_detail"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -99,7 +100,7 @@ public static class EpssEndpoints
if (!isAvailable)
{
return Results.Problem(
detail: "EPSS data is not available. Please ensure EPSS data has been ingested.",
detail: _t("scanner.epss.data_unavailable"),
statusCode: StatusCodes.Status503ServiceUnavailable);
}
@@ -127,8 +128,8 @@ public static class EpssEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid CVE ID",
Detail = "CVE ID is required.",
Title = _t("scanner.epss.invalid_cve_id"),
Detail = _t("scanner.epss.cve_id_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -139,8 +140,8 @@ public static class EpssEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "CVE not found",
Detail = $"No EPSS score found for {cveId}.",
Title = _t("scanner.epss.cve_not_found"),
Detail = _tn("scanner.epss.cve_not_found_detail", ("cveId", cveId)),
Status = StatusCodes.Status404NotFound
});
}
@@ -164,8 +165,8 @@ public static class EpssEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid CVE ID",
Detail = "CVE ID is required.",
Title = _t("scanner.epss.invalid_cve_id"),
Detail = _t("scanner.epss.cve_id_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -178,8 +179,8 @@ public static class EpssEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid date format",
Detail = "Dates must be in yyyy-MM-dd format.",
Title = _t("scanner.epss.invalid_date_format"),
Detail = _t("scanner.epss.invalid_date_format_detail"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -197,8 +198,8 @@ public static class EpssEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "No history found",
Detail = $"No EPSS history found for {cveId} in the specified date range.",
Title = _t("scanner.epss.no_history_found"),
Detail = _tn("scanner.epss.no_history_found_detail", ("cveId", cveId)),
Status = StatusCodes.Status404NotFound
});
}

View File

@@ -15,6 +15,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -40,7 +41,7 @@ internal static class EvidenceEndpoints
scansGroup.MapGet("/{scanId}/evidence/{findingId}", HandleGetEvidenceAsync)
.WithName("scanner.scans.evidence.get")
.WithTags("Evidence")
.WithDescription("Retrieves unified evidence for a specific finding within a scan.")
.WithDescription(_t("scanner.evidence.get_description"))
.Produces<FindingEvidenceResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -50,7 +51,7 @@ internal static class EvidenceEndpoints
scansGroup.MapGet("/{scanId}/evidence", HandleListEvidenceAsync)
.WithName("scanner.scans.evidence.list")
.WithTags("Evidence")
.WithDescription("Lists all findings with evidence for a scan.")
.WithDescription(_t("scanner.evidence.list_description"))
.Produces<EvidenceListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -73,9 +74,9 @@ internal static class EvidenceEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (string.IsNullOrWhiteSpace(findingId))
@@ -83,9 +84,9 @@ internal static class EvidenceEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid finding identifier",
_t("scanner.evidence.invalid_finding_identifier"),
StatusCodes.Status400BadRequest,
detail: "Finding identifier is required.");
detail: _t("scanner.evidence.finding_identifier_required"));
}
var evidence = await evidenceService.GetEvidenceAsync(
@@ -98,9 +99,9 @@ internal static class EvidenceEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Finding not found",
_t("scanner.evidence.finding_not_found"),
StatusCodes.Status404NotFound,
detail: "The requested finding could not be located in this scan.");
detail: _t("scanner.evidence.finding_not_found_detail"));
}
// Add warning header if evidence is stale or near expiry
@@ -136,9 +137,9 @@ internal static class EvidenceEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
// Get all findings for the scan

View File

@@ -10,6 +10,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -87,9 +88,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -98,9 +99,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var sarifDocument = await exportService.ExportAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -109,9 +110,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No findings available",
_t("scanner.export.no_findings"),
StatusCodes.Status404NotFound,
detail: "No findings available for SARIF export.");
detail: _t("scanner.export.no_findings_sarif"));
}
var json = JsonSerializer.Serialize(sarifDocument, SerializerOptions);
@@ -133,9 +134,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -144,9 +145,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var cdxDocument = await exportService.ExportWithReachabilityAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -155,9 +156,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No findings available",
_t("scanner.export.no_findings"),
StatusCodes.Status404NotFound,
detail: "No findings available for CycloneDX export.");
detail: _t("scanner.export.no_findings_cyclonedx"));
}
var json = JsonSerializer.Serialize(cdxDocument, SerializerOptions);
@@ -179,9 +180,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -190,9 +191,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var vexDocument = await exportService.ExportAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -201,9 +202,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No VEX data available",
_t("scanner.export.no_vex_data"),
StatusCodes.Status404NotFound,
detail: "No VEX data available for export.");
detail: _t("scanner.export.no_vex_data_detail"));
}
var json = JsonSerializer.Serialize(vexDocument, SerializerOptions);
@@ -238,9 +239,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -249,9 +250,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
// SG-012: Format selection logic with fallback to SPDX 2.3 for backward compatibility
@@ -269,9 +270,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No SBOM data available",
_t("scanner.export.no_sbom_data"),
StatusCodes.Status404NotFound,
detail: "No SBOM data available for export.");
detail: _t("scanner.export.no_sbom_data_detail"));
}
// Set appropriate content-type header based on format
@@ -367,9 +368,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -378,9 +379,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
// Export SBOM
@@ -398,9 +399,9 @@ internal static class ExportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No SBOM data available",
_t("scanner.export.no_sbom_data"),
StatusCodes.Status404NotFound,
detail: "No SBOM data available for archive export.");
detail: _t("scanner.export.no_sbom_data_archive"));
}
// Build signed archive request

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using StellaOps.Scanner.Orchestration.Fidelity;
using StellaOps.Scanner.WebService.Security;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -23,7 +24,7 @@ public static class FidelityEndpoints
return Results.Ok(result);
})
.WithName("AnalyzeWithFidelity")
.WithDescription("Analyze with specified fidelity level")
.WithDescription(_t("scanner.fidelity.analyze_description"))
.Produces<FidelityAnalysisResult>(200);
// POST /api/v1/scan/findings/{findingId}/upgrade
@@ -39,7 +40,7 @@ public static class FidelityEndpoints
: Results.BadRequest(result);
})
.WithName("UpgradeFidelity")
.WithDescription("Upgrade analysis fidelity for a finding")
.WithDescription(_t("scanner.fidelity.upgrade_description"))
.Produces<FidelityUpgradeResult>(200)
.Produces<FidelityUpgradeResult>(400);
}

View File

@@ -9,6 +9,7 @@ using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Infrastructure;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -73,7 +74,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest);
}
@@ -83,7 +84,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound);
}
@@ -92,7 +93,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Owner and repo are required",
_t("scanner.github.owner_repo_required"),
StatusCodes.Status400BadRequest);
}
@@ -103,7 +104,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"No findings to export",
_t("scanner.github.no_findings_to_export"),
StatusCodes.Status404NotFound);
}
@@ -137,7 +138,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest);
}
@@ -149,7 +150,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"SARIF upload not found",
_t("scanner.github.sarif_upload_not_found"),
StatusCodes.Status404NotFound);
}
@@ -179,7 +180,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest);
}
@@ -211,7 +212,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest);
}
@@ -223,7 +224,7 @@ internal static class GitHubCodeScanningEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Alert not found",
_t("scanner.github.alert_not_found"),
StatusCodes.Status404NotFound);
}

View File

@@ -10,6 +10,7 @@ using StellaOps.Scanner.WebService.Services;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -77,9 +78,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -88,9 +89,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var layers = await layerSbomService.GetLayerSummariesAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -128,9 +129,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (string.IsNullOrWhiteSpace(layerDigest))
@@ -138,9 +139,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid layer digest",
_t("scanner.layer_sbom.invalid_layer_digest"),
StatusCodes.Status400BadRequest,
detail: "Layer digest is required.");
detail: _t("scanner.layer_sbom.layer_digest_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -149,9 +150,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
// Normalize layer digest (URL decode if needed)
@@ -173,9 +174,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Layer SBOM not found",
_t("scanner.layer_sbom.not_found"),
StatusCodes.Status404NotFound,
detail: $"SBOM for layer {normalizedDigest} could not be found.");
detail: _tn("scanner.layer_sbom.not_found_detail", ("layerDigest", normalizedDigest)));
}
var contentType = sbomFormat == "spdx"
@@ -207,9 +208,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -218,9 +219,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var recipe = await layerSbomService.GetCompositionRecipeAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -230,9 +231,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Composition recipe not found",
_t("scanner.layer_sbom.recipe_not_found"),
StatusCodes.Status404NotFound,
detail: "Composition recipe for this scan is not available.");
detail: _t("scanner.layer_sbom.recipe_not_found_detail"));
}
var response = new CompositionRecipeResponseDto
@@ -284,9 +285,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -295,9 +296,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var verificationResult = await layerSbomService.VerifyCompositionRecipeAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -307,9 +308,9 @@ internal static class LayerSbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Composition recipe not found",
_t("scanner.layer_sbom.recipe_not_found"),
StatusCodes.Status404NotFound,
detail: "Composition recipe for this scan is not available for verification.");
detail: _t("scanner.layer_sbom.recipe_not_found_for_verification"));
}
var response = new CompositionRecipeVerificationResponseDto

View File

@@ -18,6 +18,7 @@ using StellaOps.Scanner.WebService.Extensions;
using StellaOps.Scanner.WebService.Security;
using System.Security.Cryptography;
using System.Text;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -43,7 +44,7 @@ internal static class ManifestEndpoints
.Produces<SignedScanManifestResponse>(StatusCodes.Status200OK, contentType: DsseContentType)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.Produces<ProblemDetails>(StatusCodes.Status429TooManyRequests)
.WithDescription("Get the scan manifest, optionally with DSSE signature")
.WithDescription(_t("scanner.manifest.get_description"))
.RequireAuthorization(ScannerPolicies.ScansRead)
.RequireRateLimiting(RateLimitingExtensions.ManifestPolicy);
@@ -52,7 +53,7 @@ internal static class ManifestEndpoints
.WithName("scanner.scans.proofs.list")
.Produces<ProofBundleListResponse>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.WithDescription("List all proof bundles for a scan")
.WithDescription(_t("scanner.manifest.list_proofs_description"))
.RequireAuthorization(ScannerPolicies.ScansRead);
// GET /scans/{scanId}/proofs/{rootHash}
@@ -60,7 +61,7 @@ internal static class ManifestEndpoints
.WithName("scanner.scans.proofs.get")
.Produces<ProofBundleResponse>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.WithDescription("Get a specific proof bundle by root hash")
.WithDescription(_t("scanner.manifest.get_proof_description"))
.RequireAuthorization(ScannerPolicies.ScansRead);
}
@@ -80,8 +81,8 @@ internal static class ManifestEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Scan not found",
Detail = "Invalid scan ID format",
Title = _t("scanner.scan.not_found"),
Detail = _t("scanner.scan.invalid_id_format"),
Status = StatusCodes.Status404NotFound
});
}
@@ -91,8 +92,8 @@ internal static class ManifestEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Manifest not found",
Detail = $"No manifest found for scan: {scanId}",
Title = _t("scanner.manifest.not_found"),
Detail = _tn("scanner.manifest.not_found_detail", ("scanId", scanId)),
Status = StatusCodes.Status404NotFound
});
}
@@ -154,8 +155,8 @@ internal static class ManifestEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Scan not found",
Detail = "Invalid scan ID format",
Title = _t("scanner.scan.not_found"),
Detail = _t("scanner.scan.invalid_id_format"),
Status = StatusCodes.Status404NotFound
});
}
@@ -191,8 +192,8 @@ internal static class ManifestEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Scan not found",
Detail = "Invalid scan ID format",
Title = _t("scanner.scan.not_found"),
Detail = _t("scanner.scan.invalid_id_format"),
Status = StatusCodes.Status404NotFound
});
}
@@ -201,8 +202,8 @@ internal static class ManifestEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Invalid root hash",
Detail = "Root hash is required",
Title = _t("scanner.manifest.invalid_root_hash"),
Detail = _t("scanner.manifest.root_hash_required"),
Status = StatusCodes.Status404NotFound
});
}
@@ -213,8 +214,8 @@ internal static class ManifestEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Proof bundle not found",
Detail = $"No proof bundle found with root hash: {rootHash}",
Title = _t("scanner.manifest.proof_not_found"),
Detail = _tn("scanner.manifest.proof_not_found_detail", ("rootHash", rootHash)),
Status = StatusCodes.Status404NotFound
});
}

View File

@@ -18,6 +18,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -129,9 +130,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid policy diagnostics request",
_t("scanner.policy.invalid_diagnostics_request"),
StatusCodes.Status400BadRequest,
detail: "Policy content is required for diagnostics.");
detail: _t("scanner.policy.diagnostics_content_required"));
}
var format = PolicyDtoMapper.ParsePolicyFormat(request.Policy.Format);
@@ -167,9 +168,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid policy preview request",
_t("scanner.policy.invalid_preview_request"),
StatusCodes.Status400BadRequest,
detail: "imageDigest is required.");
detail: _t("scanner.policy.preview_image_digest_required"));
}
if (!request.ImageDigest.Contains(':', StringComparison.Ordinal))
@@ -177,9 +178,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid policy preview request",
_t("scanner.policy.invalid_preview_request"),
StatusCodes.Status400BadRequest,
detail: "imageDigest must include algorithm prefix (e.g. sha256:...).");
detail: _t("scanner.policy.preview_image_digest_prefix_required"));
}
if (request.Findings is not null)
@@ -190,9 +191,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid policy preview request",
_t("scanner.policy.invalid_preview_request"),
StatusCodes.Status400BadRequest,
detail: "All findings must include an id value.");
detail: _t("scanner.policy.preview_findings_id_required"));
}
}
@@ -216,9 +217,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime policy request",
_t("scanner.policy.invalid_runtime_request"),
StatusCodes.Status400BadRequest,
detail: "images collection must include at least one digest.");
detail: _t("scanner.policy.runtime_images_required"));
}
var normalizedImages = new List<string>();
@@ -230,9 +231,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime policy request",
_t("scanner.policy.invalid_runtime_request"),
StatusCodes.Status400BadRequest,
detail: "Image digests must be non-empty.");
detail: _t("scanner.policy.runtime_image_digest_nonempty"));
}
var trimmed = image.Trim();
@@ -241,9 +242,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime policy request",
_t("scanner.policy.invalid_runtime_request"),
StatusCodes.Status400BadRequest,
detail: "Image digests must include an algorithm prefix (e.g. sha256:...).");
detail: _t("scanner.policy.runtime_image_digest_prefix_required"));
}
if (seen.Add(trimmed))
@@ -257,9 +258,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime policy request",
_t("scanner.policy.invalid_runtime_request"),
StatusCodes.Status400BadRequest,
detail: "images collection must include at least one unique digest.");
detail: _t("scanner.policy.runtime_images_unique_required"));
}
var namespaceValue = string.IsNullOrWhiteSpace(request.Namespace) ? null : request.Namespace.Trim();
@@ -306,9 +307,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid linkset request",
_t("scanner.policy.invalid_linkset_request"),
StatusCodes.Status400BadRequest,
detail: "advisoryIds must include at least one value.");
detail: _t("scanner.policy.linkset_advisory_ids_required"));
}
if (request.IncludePolicyOverlay && string.IsNullOrWhiteSpace(request.ImageDigest))
@@ -316,9 +317,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid linkset request",
_t("scanner.policy.invalid_linkset_request"),
StatusCodes.Status400BadRequest,
detail: "imageDigest is required when includePolicyOverlay is true.");
detail: _t("scanner.policy.linkset_image_digest_required_for_overlay"));
}
var linksets = await linksetResolver.ResolveByAdvisoryIdsAsync(request.AdvisoryIds, cancellationToken).ConfigureAwait(false);
@@ -472,9 +473,9 @@ internal static class PolicyEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid policy overlay request",
_t("scanner.policy.invalid_overlay_request"),
StatusCodes.Status400BadRequest,
detail: "nodes collection must include at least one node.");
detail: _t("scanner.policy.overlay_nodes_required"));
}
var tenant = !string.IsNullOrWhiteSpace(request.Tenant)

View File

@@ -11,6 +11,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -89,9 +90,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -100,9 +101,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var jobResult = await computeService.TriggerComputeAsync(
@@ -117,9 +118,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Conflict,
"Computation already in progress",
_t("scanner.reachability.computation_in_progress"),
StatusCodes.Status409Conflict,
detail: $"Reachability computation already running for scan {scanId}.");
detail: _tn("scanner.reachability.computation_in_progress_detail", ("scanId", scanId)));
}
var response = new ComputeReachabilityResponseDto(
@@ -147,9 +148,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -158,9 +159,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var components = await queryService.GetComponentsAsync(
@@ -199,9 +200,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -210,9 +211,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var findings = await queryService.GetFindingsAsync(
@@ -253,9 +254,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (string.IsNullOrWhiteSpace(cve) || string.IsNullOrWhiteSpace(purl))
@@ -263,9 +264,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Missing required parameters",
_t("scanner.reachability.missing_parameters"),
StatusCodes.Status400BadRequest,
detail: "Both 'cve' and 'purl' query parameters are required.");
detail: _t("scanner.reachability.missing_parameters_detail"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -274,9 +275,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var explanation = await explainService.ExplainAsync(
@@ -290,9 +291,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Explanation not found",
_t("scanner.reachability.explanation_not_found"),
StatusCodes.Status404NotFound,
detail: $"No reachability data for CVE {cve} and PURL {purl}.");
detail: _tn("scanner.reachability.explanation_not_found_detail", ("cve", cve), ("purl", purl)));
}
var response = new ReachabilityExplanationDto(
@@ -346,9 +347,9 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, cancellationToken).ConfigureAwait(false);
@@ -357,17 +358,17 @@ internal static class ReachabilityEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
return ProblemResultFactory.Create(
context,
ProblemTypes.NotImplemented,
"Trace export not available",
_t("scanner.reachability.trace_export_unavailable"),
StatusCodes.Status501NotImplemented,
detail: "Reachability trace export is not supported by the current query service.");
detail: _t("scanner.reachability.trace_export_unavailable_detail"));
}
private static IResult Json<T>(T value, int statusCode)

View File

@@ -10,6 +10,7 @@ using StellaOps.Scanner.Reachability.Jobs;
using StellaOps.Scanner.Reachability.Services;
using StellaOps.Scanner.Reachability.Vex;
using StellaOps.Scanner.WebService.Security;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -74,7 +75,7 @@ public static class ReachabilityEvidenceEndpoints
string.IsNullOrWhiteSpace(request.Purl))
{
return Results.Problem(
detail: "imageDigest, cveId, and purl are required",
detail: _t("scanner.reachability_evidence.required_fields"),
statusCode: StatusCodes.Status400BadRequest);
}
@@ -83,7 +84,7 @@ public static class ReachabilityEvidenceEndpoints
if (!hasMappings)
{
return Results.Problem(
detail: $"No sink mappings found for CVE {request.CveId}",
detail: _tn("scanner.reachability_evidence.no_sink_mappings", ("cveId", request.CveId)),
statusCode: StatusCodes.Status404NotFound);
}
@@ -131,7 +132,7 @@ public static class ReachabilityEvidenceEndpoints
if (result is null)
{
return Results.Problem(
detail: $"No result found for job {jobId}",
detail: _tn("scanner.reachability_evidence.job_result_not_found", ("jobId", jobId)),
statusCode: StatusCodes.Status404NotFound);
}
@@ -165,7 +166,7 @@ public static class ReachabilityEvidenceEndpoints
if (mappings.Count == 0)
{
return Results.Problem(
detail: $"No mappings found for CVE {cveId}",
detail: _tn("scanner.reachability_evidence.cve_mappings_not_found", ("cveId", cveId)),
statusCode: StatusCodes.Status404NotFound);
}
@@ -196,7 +197,7 @@ public static class ReachabilityEvidenceEndpoints
string.IsNullOrWhiteSpace(request.ProductId))
{
return Results.Problem(
detail: "jobId and productId are required",
detail: _t("scanner.reachability_evidence.vex_required_fields"),
statusCode: StatusCodes.Status400BadRequest);
}
@@ -205,7 +206,7 @@ public static class ReachabilityEvidenceEndpoints
if (result?.Stack is null)
{
return Results.Problem(
detail: $"No reachability result found for job {request.JobId}",
detail: _tn("scanner.reachability_evidence.vex_result_not_found", ("jobId", request.JobId)),
statusCode: StatusCodes.Status404NotFound);
}

View File

@@ -14,6 +14,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -79,9 +80,9 @@ internal static class ReportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid report request",
_t("scanner.report.invalid_request"),
StatusCodes.Status400BadRequest,
detail: "imageDigest is required.");
detail: _t("scanner.report.image_digest_required"));
}
if (!request.ImageDigest.Contains(':', StringComparison.Ordinal))
@@ -89,9 +90,9 @@ internal static class ReportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid report request",
_t("scanner.report.invalid_request"),
StatusCodes.Status400BadRequest,
detail: "imageDigest must include algorithm prefix (e.g. sha256:...).");
detail: _t("scanner.report.image_digest_prefix_required"));
}
if (request.Findings is not null && request.Findings.Any(f => string.IsNullOrWhiteSpace(f.Id)))
@@ -99,9 +100,9 @@ internal static class ReportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid report request",
_t("scanner.report.invalid_request"),
StatusCodes.Status400BadRequest,
detail: "All findings must include an id value.");
detail: _t("scanner.report.findings_id_required"));
}
var previewDto = new PolicyPreviewRequestDto
@@ -126,9 +127,9 @@ internal static class ReportEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Unable to assemble report",
_t("scanner.report.unable_to_assemble"),
StatusCodes.Status503ServiceUnavailable,
detail: "No policy snapshot is available or validation failed.",
detail: _t("scanner.report.no_policy_snapshot"),
extensions: extensions);
}

View File

@@ -15,6 +15,7 @@ using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -80,9 +81,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Runtime event batch too large",
_t("scanner.runtime.batch_too_large"),
StatusCodes.Status400BadRequest,
detail: "Runtime batch payload exceeds configured budget.",
detail: _t("scanner.runtime.batch_too_large_detail"),
extensions: extensions);
}
@@ -101,9 +102,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.RateLimited,
"Runtime ingestion rate limited",
_t("scanner.runtime.rate_limited"),
StatusCodes.Status429TooManyRequests,
detail: "Runtime ingestion exceeded configured rate limits.",
detail: _t("scanner.runtime.rate_limited_detail"),
extensions: extensions);
}
@@ -128,9 +129,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: "events array must include at least one item.");
detail: _t("scanner.runtime.events_array_empty"));
}
if (envelopes.Count > runtimeOptions.MaxBatchSize)
@@ -144,9 +145,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: "events array exceeds allowed batch size.",
detail: _t("scanner.runtime.events_array_exceeds_batch"),
extensions: extensions);
}
@@ -159,9 +160,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"events[{i}] must not be null.");
detail: _tn("scanner.runtime.event_null", ("index", i.ToString())));
}
if (!envelope.IsSupported())
@@ -174,9 +175,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Unsupported runtime schema version",
_t("scanner.runtime.unsupported_schema"),
StatusCodes.Status400BadRequest,
detail: "Runtime event schemaVersion is not supported.",
detail: _t("scanner.runtime.unsupported_schema_version"),
extensions: extensions);
}
@@ -186,9 +187,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"events[{i}].event must not be null.");
detail: _tn("scanner.runtime.event_body_null", ("index", i.ToString())));
}
if (string.IsNullOrWhiteSpace(runtimeEvent.EventId))
@@ -196,9 +197,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"events[{i}].eventId is required.");
detail: _tn("scanner.runtime.event_id_required", ("index", i.ToString())));
}
if (!seenEventIds.Add(runtimeEvent.EventId))
@@ -206,9 +207,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"Duplicate eventId detected within batch ('{runtimeEvent.EventId}').");
detail: _tn("scanner.runtime.duplicate_event_id", ("eventId", runtimeEvent.EventId)));
}
if (string.IsNullOrWhiteSpace(runtimeEvent.Tenant))
@@ -216,9 +217,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"events[{i}].tenant is required.");
detail: _tn("scanner.runtime.event_tenant_required", ("index", i.ToString())));
}
if (string.IsNullOrWhiteSpace(runtimeEvent.Node))
@@ -226,9 +227,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"events[{i}].node is required.");
detail: _tn("scanner.runtime.event_node_required", ("index", i.ToString())));
}
if (runtimeEvent.Workload is null)
@@ -236,9 +237,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid runtime ingest request",
_t("scanner.runtime.invalid_ingest_request"),
StatusCodes.Status400BadRequest,
detail: $"events[{i}].workload is required.");
detail: _tn("scanner.runtime.event_workload_required", ("index", i.ToString())));
}
}
@@ -259,9 +260,9 @@ internal static class RuntimeEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid reconciliation request",
_t("scanner.runtime.invalid_reconcile_request"),
StatusCodes.Status400BadRequest,
detail: "imageDigest is required.");
detail: _t("scanner.runtime.image_digest_required"));
}
var reconcileRequest = new RuntimeReconciliationRequest

View File

@@ -9,6 +9,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -55,9 +56,9 @@ internal static class SbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
// Verify scan exists
@@ -67,9 +68,9 @@ internal static class SbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
// Parse JSON body
@@ -85,7 +86,7 @@ internal static class SbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid JSON",
_t("scanner.sbom.invalid_json"),
StatusCodes.Status400BadRequest,
detail: $"Failed to parse SBOM JSON: {ex.Message}");
}
@@ -100,9 +101,9 @@ internal static class SbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Unknown SBOM format",
_t("scanner.sbom.unknown_format"),
StatusCodes.Status400BadRequest,
detail: "Could not detect SBOM format. Use Content-Type 'application/vnd.cyclonedx+json; version=1.7' (or 1.6) or 'application/spdx+json'.");
detail: _t("scanner.sbom.unknown_format_detail"));
}
// Validate the SBOM
@@ -118,9 +119,9 @@ internal static class SbomEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid SBOM",
_t("scanner.sbom.invalid"),
StatusCodes.Status400BadRequest,
detail: "SBOM validation failed.",
detail: _t("scanner.sbom.invalid_detail"),
extensions: extensions);
}

View File

@@ -1,6 +1,7 @@
using DomainScanProgressEvent = StellaOps.Scanner.WebService.Domain.ScanProgressEvent;
using Microsoft.AspNetCore.Http;
using static StellaOps.Localization.T;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
using StellaOps.Scanner.Core.Contracts;
@@ -111,9 +112,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan submission",
_t("scanner.scan.invalid_submission"),
StatusCodes.Status400BadRequest,
detail: "Request image descriptor is required.");
detail: _t("scanner.scan.image_descriptor_required"));
}
var reference = request.Image.Reference;
@@ -123,9 +124,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan submission",
_t("scanner.scan.invalid_submission"),
StatusCodes.Status400BadRequest,
detail: "Either image.reference or image.digest must be provided.");
detail: _t("scanner.scan.image_ref_or_digest_required"));
}
if (!string.IsNullOrWhiteSpace(digest) && !digest.Contains(':', StringComparison.Ordinal))
@@ -133,9 +134,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan submission",
_t("scanner.scan.invalid_submission"),
StatusCodes.Status400BadRequest,
detail: "Image digest must include algorithm prefix (e.g. sha256:...).");
detail: _t("scanner.scan.image_digest_prefix_required"));
}
var target = new ScanTarget(reference, digest).Normalize();
@@ -161,7 +162,7 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid tenant context",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest,
detail: tenantError ?? "tenant_conflict");
}
@@ -218,9 +219,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var snapshot = await coordinator.GetAsync(parsed, context.RequestAborted).ConfigureAwait(false);
@@ -289,9 +290,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (request.Layers is null || request.Layers.Count == 0)
@@ -299,7 +300,7 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Entropy layers are required",
_t("scanner.scan.entropy_layers_required"),
StatusCodes.Status400BadRequest);
}
@@ -317,7 +318,7 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Entropy layers are required",
_t("scanner.scan.entropy_layers_required"),
StatusCodes.Status400BadRequest);
}
@@ -348,9 +349,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
if (!progressReader.Exists(parsed))
@@ -358,9 +359,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Scan not found",
_t("scanner.scan.not_found"),
StatusCodes.Status404NotFound,
detail: "Requested scan could not be located.");
detail: _t("scanner.scan.not_found_detail"));
}
var streamFormat = string.Equals(format, "jsonl", StringComparison.OrdinalIgnoreCase)
@@ -434,9 +435,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var targetScanId = parsed.Value;
@@ -454,9 +455,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"EntryTrace not found",
_t("scanner.scan.entrytrace_not_found"),
StatusCodes.Status404NotFound,
detail: "EntryTrace data is not available for the requested scan.");
detail: _t("scanner.scan.entrytrace_not_found_detail"));
}
}
@@ -491,9 +492,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var inventory = await inventoryStore.GetAsync(parsed.Value, cancellationToken).ConfigureAwait(false);
@@ -514,9 +515,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Ruby packages not found",
_t("scanner.scan.ruby_packages_not_found"),
StatusCodes.Status404NotFound,
detail: "Ruby package inventory is not available for the requested scan.");
detail: _t("scanner.scan.ruby_packages_not_found_detail"));
}
inventory = fallback;
@@ -548,9 +549,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid scan identifier",
_t("scanner.scan.invalid_identifier"),
StatusCodes.Status400BadRequest,
detail: "Scan identifier is required.");
detail: _t("scanner.scan.identifier_required"));
}
var inventory = await inventoryStore.GetAsync(parsed.Value, cancellationToken).ConfigureAwait(false);
@@ -571,9 +572,9 @@ internal static class ScanEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Bun packages not found",
_t("scanner.scan.bun_packages_not_found"),
StatusCodes.Status404NotFound,
detail: "Bun package inventory is not available for the requested scan.");
detail: _t("scanner.scan.bun_packages_not_found_detail"));
}
inventory = fallback;

View File

@@ -12,6 +12,7 @@ using StellaOps.Scanner.Core;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -28,21 +29,21 @@ internal static class ScoreReplayEndpoints
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
.Produces<ProblemDetails>(StatusCodes.Status422UnprocessableEntity)
.WithDescription("Replay scoring for a previous scan using frozen inputs")
.WithDescription(_t("scanner.score_replay.replay_description"))
.RequireAuthorization(ScannerPolicies.ScansWrite);
score.MapGet("/{scanId}/bundle", HandleGetBundleAsync)
.WithName("scanner.score.bundle")
.Produces<ScoreBundleResponse>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.WithDescription("Get the proof bundle for a scan");
.WithDescription(_t("scanner.score_replay.bundle_description"));
score.MapPost("/{scanId}/verify", HandleVerifyAsync)
.WithName("scanner.score.verify")
.Produces<ScoreVerifyResponse>(StatusCodes.Status200OK)
.Produces<ProblemDetails>(StatusCodes.Status404NotFound)
.Produces<ProblemDetails>(StatusCodes.Status422UnprocessableEntity)
.WithDescription("Verify a proof bundle against expected root hash")
.WithDescription(_t("scanner.score_replay.verify_description"))
.RequireAuthorization(ScannerPolicies.ScansWrite);
}
@@ -61,8 +62,8 @@ internal static class ScoreReplayEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid scan ID",
Detail = "Scan ID is required",
Title = _t("scanner.scan.invalid_identifier"),
Detail = _t("scanner.scan.identifier_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -79,8 +80,8 @@ internal static class ScoreReplayEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Scan not found",
Detail = $"No scan found with ID: {scanId}",
Title = _t("scanner.scan.not_found"),
Detail = _tn("scanner.score_replay.scan_not_found_detail", ("scanId", scanId)),
Status = StatusCodes.Status404NotFound
});
}
@@ -97,7 +98,7 @@ internal static class ScoreReplayEndpoints
{
return Results.UnprocessableEntity(new ProblemDetails
{
Title = "Replay failed",
Title = _t("scanner.score_replay.replay_failed"),
Detail = ex.Message,
Status = StatusCodes.Status422UnprocessableEntity
});
@@ -120,8 +121,8 @@ internal static class ScoreReplayEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid scan ID",
Detail = "Scan ID is required",
Title = _t("scanner.scan.invalid_identifier"),
Detail = _t("scanner.scan.identifier_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -132,8 +133,8 @@ internal static class ScoreReplayEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Bundle not found",
Detail = $"No proof bundle found for scan: {scanId}",
Title = _t("scanner.score_replay.bundle_not_found"),
Detail = _tn("scanner.score_replay.bundle_not_found_detail", ("scanId", scanId)),
Status = StatusCodes.Status404NotFound
});
}
@@ -149,7 +150,7 @@ internal static class ScoreReplayEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Bundle not found",
Title = _t("scanner.score_replay.bundle_not_found"),
Detail = ex.Message,
Status = StatusCodes.Status404NotFound
});
@@ -177,8 +178,8 @@ internal static class ScoreReplayEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Invalid scan ID",
Detail = "Scan ID is required",
Title = _t("scanner.scan.invalid_identifier"),
Detail = _t("scanner.scan.identifier_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -187,8 +188,8 @@ internal static class ScoreReplayEndpoints
{
return Results.BadRequest(new ProblemDetails
{
Title = "Missing expected root hash",
Detail = "Expected root hash is required for verification",
Title = _t("scanner.score_replay.missing_root_hash"),
Detail = _t("scanner.score_replay.root_hash_required"),
Status = StatusCodes.Status400BadRequest
});
}
@@ -214,7 +215,7 @@ internal static class ScoreReplayEndpoints
{
return Results.NotFound(new ProblemDetails
{
Title = "Bundle not found",
Title = _t("scanner.score_replay.bundle_not_found"),
Detail = ex.Message,
Status = StatusCodes.Status404NotFound
});

View File

@@ -8,6 +8,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -73,18 +74,18 @@ internal static class SliceEndpoints
{
if (request == null)
{
return Results.BadRequest(new { error = "Request body is required" });
return Results.BadRequest(new { error = _t("common.error.body_required") });
}
if (string.IsNullOrWhiteSpace(request.ScanId))
{
return Results.BadRequest(new { error = "scanId is required" });
return Results.BadRequest(new { error = _t("scanner.slice.scan_id_required") });
}
if (string.IsNullOrWhiteSpace(request.CveId) &&
(request.Symbols == null || request.Symbols.Count == 0))
{
return Results.BadRequest(new { error = "Either cveId or symbols must be specified" });
return Results.BadRequest(new { error = _t("scanner.slice.cve_or_symbols_required") });
}
try
@@ -132,7 +133,7 @@ internal static class SliceEndpoints
{
if (string.IsNullOrWhiteSpace(digest))
{
return Results.BadRequest(new { error = "digest is required" });
return Results.BadRequest(new { error = _t("scanner.slice.digest_required") });
}
var wantsDsse = accept?.Contains("dsse", StringComparison.OrdinalIgnoreCase) == true;
@@ -144,7 +145,7 @@ internal static class SliceEndpoints
var dsse = await sliceService.GetSliceDsseAsync(digest, cancellationToken).ConfigureAwait(false);
if (dsse == null)
{
return Results.NotFound(new { error = $"Slice {digest} not found" });
return Results.NotFound(new { error = _tn("scanner.slice.not_found", ("digest", digest)) });
}
return Results.Json(dsse, SerializerOptions, "application/dsse+json");
}
@@ -153,7 +154,7 @@ internal static class SliceEndpoints
var slice = await sliceService.GetSliceAsync(digest, cancellationToken).ConfigureAwait(false);
if (slice == null)
{
return Results.NotFound(new { error = $"Slice {digest} not found" });
return Results.NotFound(new { error = _tn("scanner.slice.not_found", ("digest", digest)) });
}
return Results.Json(slice, SerializerOptions, "application/json");
}
@@ -171,12 +172,12 @@ internal static class SliceEndpoints
{
if (request == null)
{
return Results.BadRequest(new { error = "Request body is required" });
return Results.BadRequest(new { error = _t("common.error.body_required") });
}
if (string.IsNullOrWhiteSpace(request.SliceDigest))
{
return Results.BadRequest(new { error = "sliceDigest is required" });
return Results.BadRequest(new { error = _t("scanner.slice.slice_digest_required") });
}
try

View File

@@ -7,6 +7,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using StellaOps.Scanner.WebService.Tenancy;
using System.Collections.Immutable;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -183,7 +184,7 @@ internal static class SmartDiffEndpoints
var targetDigest = NormalizeDigest(metadata?.TargetDigest);
if (string.IsNullOrWhiteSpace(targetDigest))
{
return Results.NotFound(new { error = "Scan metadata not found", scanId });
return Results.NotFound(new { error = _t("scanner.smartdiff.scan_metadata_not_found"), scanId });
}
return await HandleGetCandidatesAsync(targetDigest, store, minConfidence, pendingOnly, context, ct).ConfigureAwait(false);
@@ -287,7 +288,7 @@ internal static class SmartDiffEndpoints
var normalizedDigest = NormalizeDigest(digest);
if (string.IsNullOrWhiteSpace(normalizedDigest))
{
return Results.BadRequest(new { error = "Invalid image digest" });
return Results.BadRequest(new { error = _t("scanner.smartdiff.invalid_image_digest") });
}
var candidates = await store.GetCandidatesAsync(normalizedDigest, ct, tenantId: tenantId);
@@ -330,7 +331,7 @@ internal static class SmartDiffEndpoints
if (candidate is null)
{
return Results.NotFound(new { error = "Candidate not found", candidateId });
return Results.NotFound(new { error = _t("scanner.smartdiff.candidate_not_found"), candidateId });
}
var response = new VexCandidateResponse
@@ -354,7 +355,7 @@ internal static class SmartDiffEndpoints
{
if (!Enum.TryParse<VexReviewAction>(request.Action, true, out var action))
{
return Results.BadRequest(new { error = "Invalid action", validActions = new[] { "accept", "reject", "defer" } });
return Results.BadRequest(new { error = _t("scanner.smartdiff.invalid_action"), validActions = new[] { "accept", "reject", "defer" } });
}
if (!TryResolveTenant(httpContext, out var tenantId, out var failure))
{
@@ -372,7 +373,7 @@ internal static class SmartDiffEndpoints
if (!success)
{
return Results.NotFound(new { error = "Candidate not found", candidateId });
return Results.NotFound(new { error = _t("scanner.smartdiff.candidate_not_found"), candidateId });
}
return Results.Ok(new ReviewResponse
@@ -398,7 +399,7 @@ internal static class SmartDiffEndpoints
{
if (string.IsNullOrWhiteSpace(request.CandidateId))
{
return Results.BadRequest(new { error = "CandidateId is required" });
return Results.BadRequest(new { error = _t("scanner.smartdiff.candidate_id_required") });
}
if (!TryResolveTenant(httpContext, out var tenantId, out var failure))
{
@@ -415,7 +416,7 @@ internal static class SmartDiffEndpoints
var candidate = await store.GetCandidateAsync(request.CandidateId, ct, tenantId: tenantId).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 Results.NotFound(new { error = _t("scanner.smartdiff.candidate_not_found_for_scan"), scanId, candidateId = request.CandidateId });
}
return await HandleReviewCandidateAsync(
@@ -531,8 +532,8 @@ internal static class SmartDiffEndpoints
failure = Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = "tenant_missing"
title = _t("scanner.smartdiff.invalid_tenant_context"),
detail = _t("scanner.error.tenant_missing")
});
return false;
}
@@ -549,8 +550,8 @@ internal static class SmartDiffEndpoints
failure = Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.smartdiff.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
return false;
}

View File

@@ -11,6 +11,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Tenancy;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -149,7 +150,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -177,7 +178,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -187,9 +188,9 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound,
detail: $"Source {sourceId} not found");
detail: _tn("scanner.sources.not_found_detail", ("sourceId", sourceId.ToString())));
}
return Json(source, StatusCodes.Status200OK);
@@ -206,7 +207,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -216,9 +217,9 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound,
detail: $"Source '{name}' not found");
detail: _tn("scanner.sources.not_found_by_name_detail", ("name", name)));
}
return Json(source, StatusCodes.Status200OK);
@@ -236,7 +237,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -263,7 +264,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Conflict,
"Source already exists",
_t("scanner.sources.already_exists"),
StatusCodes.Status409Conflict,
detail: ex.Message);
}
@@ -272,7 +273,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid request",
_t("scanner.sources.invalid_request"),
StatusCodes.Status400BadRequest,
detail: ex.Message);
}
@@ -290,7 +291,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -306,7 +307,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
catch (InvalidOperationException ex)
@@ -314,7 +315,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Conflict,
"Update conflict",
_t("scanner.sources.update_conflict"),
StatusCodes.Status409Conflict,
detail: ex.Message);
}
@@ -323,7 +324,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid request",
_t("scanner.sources.invalid_request"),
StatusCodes.Status400BadRequest,
detail: ex.Message);
}
@@ -340,7 +341,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -354,7 +355,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}
@@ -370,7 +371,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -384,7 +385,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}
@@ -400,7 +401,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -420,7 +421,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -436,7 +437,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}
@@ -452,7 +453,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -468,7 +469,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}
@@ -484,7 +485,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -500,7 +501,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}
@@ -517,7 +518,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -533,7 +534,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
catch (InvalidOperationException ex)
@@ -541,7 +542,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Cannot trigger scan",
_t("scanner.sources.cannot_trigger_scan"),
StatusCodes.Status400BadRequest,
detail: ex.Message);
}
@@ -559,7 +560,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -581,7 +582,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}
@@ -598,7 +599,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Tenant context required",
_t("scanner.error.invalid_tenant"),
StatusCodes.Status400BadRequest);
}
@@ -610,7 +611,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Run not found",
_t("scanner.sources.run_not_found"),
StatusCodes.Status404NotFound);
}
@@ -621,7 +622,7 @@ internal static class SourcesEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
}

View File

@@ -9,6 +9,7 @@ using StellaOps.Scanner.Triage.Services;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Tenancy;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints.Triage;
@@ -26,14 +27,14 @@ internal static class BatchTriageEndpoints
triageGroup.MapGet("/inbox/clusters/stats", HandleGetClusterStatsAsync)
.WithName("scanner.triage.inbox.cluster-stats")
.WithDescription("Returns per-cluster severity and reachability distributions.")
.WithDescription(_t("scanner.triage.cluster_stats_description"))
.Produces<TriageClusterStatsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.TriageRead);
triageGroup.MapPost("/inbox/clusters/{pathId}/actions", HandleApplyBatchActionAsync)
.WithName("scanner.triage.inbox.cluster-action")
.WithDescription("Applies one triage action to all findings in an exploit-path cluster.")
.WithDescription(_t("scanner.triage.cluster_action_description"))
.Produces<BatchTriageClusterActionResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -55,8 +56,8 @@ internal static class BatchTriageEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid artifact digest",
detail = "Artifact digest is required."
title = _t("scanner.triage.invalid_artifact_digest"),
detail = _t("scanner.triage.artifact_digest_required")
});
}
@@ -97,8 +98,8 @@ internal static class BatchTriageEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid artifact digest",
detail = "Artifact digest is required."
title = _t("scanner.triage.invalid_artifact_digest"),
detail = _t("scanner.triage.artifact_digest_required")
});
}
@@ -107,8 +108,8 @@ internal static class BatchTriageEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid path id",
detail = "Path id is required."
title = _t("scanner.triage.invalid_path_id"),
detail = _t("scanner.triage.path_id_required")
});
}
@@ -123,8 +124,8 @@ internal static class BatchTriageEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Cluster not found",
detail = $"Cluster '{pathId}' was not found for artifact '{request.ArtifactDigest}'."
title = _t("scanner.triage.cluster_not_found"),
detail = _tn("scanner.triage.cluster_not_found_detail", ("pathId", pathId), ("artifactDigest", request.ArtifactDigest))
});
}

View File

@@ -12,6 +12,7 @@ using StellaOps.Scanner.Triage.Models;
using StellaOps.Scanner.WebService.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints.Triage;
@@ -33,7 +34,7 @@ internal static class ProofBundleEndpoints
// POST /v1/triage/proof-bundle
triageGroup.MapPost("/proof-bundle", HandleGenerateProofBundleAsync)
.WithName("scanner.triage.proof-bundle")
.WithDescription("Generates an attested proof bundle for an exploit path.")
.WithDescription(_t("scanner.triage.proof_bundle_description"))
.Produces<ProofBundleResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.TriageWrite);
@@ -52,8 +53,8 @@ internal static class ProofBundleEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid path ID",
detail = "Path ID is required."
title = _t("scanner.triage.invalid_path_id"),
detail = _t("scanner.triage.path_id_required")
});
}

View File

@@ -14,6 +14,7 @@ using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Tenancy;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints.Triage;
@@ -41,7 +42,7 @@ internal static class TriageInboxEndpoints
// GET /v1/triage/inbox?artifactDigest={digest}&filter={filter}
triageGroup.MapGet("/inbox", HandleGetInboxAsync)
.WithName("scanner.triage.inbox")
.WithDescription("Retrieves triage inbox with grouped exploit paths for an artifact.")
.WithDescription(_t("scanner.triage.inbox_description"))
.Produces<TriageInboxResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.TriageRead);
@@ -67,8 +68,8 @@ internal static class TriageInboxEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid artifact digest",
detail = "Artifact digest is required."
title = _t("scanner.triage.invalid_artifact_digest"),
detail = _t("scanner.triage.artifact_digest_required")
});
}

View File

@@ -13,6 +13,7 @@ using StellaOps.Scanner.WebService.Services;
using StellaOps.Scanner.WebService.Tenancy;
using System.Text.Json;
using System.Text.Json.Serialization;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints.Triage;
@@ -41,7 +42,7 @@ internal static class TriageStatusEndpoints
// GET /v1/triage/findings/{findingId} - Get triage status for a finding
triageGroup.MapGet("/findings/{findingId}", HandleGetFindingStatusAsync)
.WithName("scanner.triage.finding.status")
.WithDescription("Retrieves triage status for a specific finding.")
.WithDescription(_t("scanner.triage.status_get_description"))
.Produces<FindingTriageStatusDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.TriageRead);
@@ -49,7 +50,7 @@ internal static class TriageStatusEndpoints
// POST /v1/triage/findings/{findingId}/status - Update triage status
triageGroup.MapPost("/findings/{findingId}/status", HandleUpdateStatusAsync)
.WithName("scanner.triage.finding.status.update")
.WithDescription("Updates triage status for a finding (lane change, decision).")
.WithDescription(_t("scanner.triage.status_update_description"))
.Produces<UpdateTriageStatusResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -58,7 +59,7 @@ internal static class TriageStatusEndpoints
// POST /v1/triage/findings/{findingId}/vex - Submit VEX statement
triageGroup.MapPost("/findings/{findingId}/vex", HandleSubmitVexAsync)
.WithName("scanner.triage.finding.vex.submit")
.WithDescription("Submits a VEX statement for a finding.")
.WithDescription(_t("scanner.triage.vex_submit_description"))
.Produces<SubmitVexStatementResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound)
@@ -67,7 +68,7 @@ internal static class TriageStatusEndpoints
// POST /v1/triage/query - Bulk query findings
triageGroup.MapPost("/query", HandleBulkQueryAsync)
.WithName("scanner.triage.query")
.WithDescription("Queries findings with filtering and pagination.")
.WithDescription(_t("scanner.triage.query_description"))
.Produces<BulkTriageQueryResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.TriageRead);
@@ -75,7 +76,7 @@ internal static class TriageStatusEndpoints
// GET /v1/triage/summary - Get triage summary for an artifact
triageGroup.MapGet("/summary", HandleGetSummaryAsync)
.WithName("scanner.triage.summary")
.WithDescription("Returns triage summary statistics for an artifact.")
.WithDescription(_t("scanner.triage.summary_description"))
.Produces<TriageSummaryDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(ScannerPolicies.TriageRead);
@@ -94,8 +95,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid finding ID",
detail = "Finding ID is required."
title = _t("scanner.triage.invalid_finding_id"),
detail = _t("scanner.triage.finding_id_required")
});
}
@@ -108,8 +109,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.triage.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
}
@@ -119,8 +120,8 @@ internal static class TriageStatusEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Finding not found",
detail = $"Finding with ID '{findingId}' was not found."
title = _t("scanner.triage.finding_not_found"),
detail = _tn("scanner.triage.finding_not_found_detail", ("findingId", findingId))
});
}
@@ -142,8 +143,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid finding ID",
detail = "Finding ID is required."
title = _t("scanner.triage.invalid_finding_id"),
detail = _t("scanner.triage.finding_id_required")
});
}
@@ -159,8 +160,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.triage.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
}
@@ -170,8 +171,8 @@ internal static class TriageStatusEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Finding not found",
detail = $"Finding with ID '{findingId}' was not found."
title = _t("scanner.triage.finding_not_found"),
detail = _tn("scanner.triage.finding_not_found_detail", ("findingId", findingId))
});
}
@@ -193,8 +194,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid finding ID",
detail = "Finding ID is required."
title = _t("scanner.triage.invalid_finding_id"),
detail = _t("scanner.triage.finding_id_required")
});
}
@@ -203,8 +204,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid VEX status",
detail = "VEX status is required."
title = _t("scanner.triage.invalid_vex_status"),
detail = _t("scanner.triage.vex_status_required")
});
}
@@ -215,8 +216,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid VEX status",
detail = $"VEX status must be one of: {string.Join(", ", validStatuses)}"
title = _t("scanner.triage.invalid_vex_status"),
detail = _tn("scanner.triage.invalid_vex_status_detail", ("validStatuses", string.Join(", ", validStatuses)))
});
}
@@ -227,8 +228,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Justification required",
detail = "Justification is required when status is NotAffected."
title = _t("scanner.triage.justification_required"),
detail = _t("scanner.triage.justification_required_detail")
});
}
@@ -242,8 +243,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.triage.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
}
@@ -254,8 +255,8 @@ internal static class TriageStatusEndpoints
return Results.NotFound(new
{
type = "not-found",
title = "Finding not found",
detail = $"Finding with ID '{findingId}' was not found."
title = _t("scanner.triage.finding_not_found"),
detail = _tn("scanner.triage.finding_not_found_detail", ("findingId", findingId))
});
}
@@ -283,8 +284,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.triage.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
}
@@ -305,8 +306,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid artifact digest",
detail = "Artifact digest is required."
title = _t("scanner.triage.invalid_artifact_digest"),
detail = _t("scanner.triage.artifact_digest_required")
});
}
@@ -319,8 +320,8 @@ internal static class TriageStatusEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.triage.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
}

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;
using StellaOps.Scanner.WebService.Tenancy;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -25,40 +26,40 @@ internal static class UnknownsEndpoints
.WithName("scanner.unknowns.list")
.Produces<UnknownsListResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.WithDescription("Lists unknown entries with tenant-scoped filtering.");
.WithDescription(_t("scanner.unknowns.list_description"));
unknowns.MapGet("/stats", HandleGetStatsAsync)
.WithName("scanner.unknowns.stats")
.Produces<UnknownsStatsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.WithDescription("Returns tenant-scoped unknown summary statistics.");
.WithDescription(_t("scanner.unknowns.stats_description"));
unknowns.MapGet("/bands", HandleGetBandsAsync)
.WithName("scanner.unknowns.bands")
.Produces<UnknownsBandsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status400BadRequest)
.WithDescription("Returns tenant-scoped unknown distribution by triage band.");
.WithDescription(_t("scanner.unknowns.bands_description"));
unknowns.MapGet("/{id}/evidence", HandleGetEvidenceAsync)
.WithName("scanner.unknowns.evidence")
.Produces<UnknownEvidenceResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.WithDescription("Returns tenant-scoped unknown evidence metadata.");
.WithDescription(_t("scanner.unknowns.evidence_description"));
unknowns.MapGet("/{id}/history", HandleGetHistoryAsync)
.WithName("scanner.unknowns.history")
.Produces<UnknownHistoryResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.WithDescription("Returns tenant-scoped unknown history.");
.WithDescription(_t("scanner.unknowns.history_description"));
unknowns.MapGet("/{id}", HandleGetByIdAsync)
.WithName("scanner.unknowns.get")
.Produces<UnknownDetailResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status400BadRequest)
.WithDescription("Returns tenant-scoped unknown detail.");
.WithDescription(_t("scanner.unknowns.get_description"));
}
private static async Task<IResult> HandleListAsync(
@@ -83,8 +84,8 @@ internal static class UnknownsEndpoints
return Results.BadRequest(new
{
type = "validation-error",
title = "Invalid band",
detail = "Band must be one of HOT, WARM, or COLD."
title = _t("scanner.unknowns.invalid_band"),
detail = _t("scanner.unknowns.invalid_band_detail")
});
}
@@ -258,8 +259,8 @@ internal static class UnknownsEndpoints
failure = Results.BadRequest(new
{
type = "validation-error",
title = "Invalid tenant context",
detail = tenantError ?? "tenant_conflict"
title = _t("scanner.unknowns.invalid_tenant_context"),
detail = tenantError ?? _t("scanner.error.tenant_conflict")
});
return false;
}

View File

@@ -14,6 +14,7 @@ using StellaOps.Scanner.WebService.Services;
using StellaOps.Scanner.WebService.Tenancy;
using System.Text;
using System.Text.Json;
using static StellaOps.Localization.T;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -120,7 +121,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid tenant context",
_t("scanner.webhook.invalid_tenant_context"),
StatusCodes.Status400BadRequest,
detail: tenantError);
}
@@ -137,7 +138,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
@@ -146,7 +147,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
@@ -157,7 +158,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Source does not support webhooks",
_t("scanner.webhook.source_no_webhooks"),
StatusCodes.Status400BadRequest);
}
@@ -167,7 +168,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Webhook secret is not configured",
_t("scanner.webhook.secret_not_configured"),
StatusCodes.Status401Unauthorized);
}
@@ -180,7 +181,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Missing webhook signature",
_t("scanner.webhook.missing_signature"),
StatusCodes.Status401Unauthorized);
}
@@ -194,7 +195,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.InternalError,
"Failed to resolve webhook secret",
_t("scanner.webhook.secret_resolve_failed"),
StatusCodes.Status500InternalServerError);
}
@@ -204,7 +205,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Invalid webhook signature",
_t("scanner.webhook.invalid_signature"),
StatusCodes.Status401Unauthorized);
}
@@ -223,7 +224,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid JSON payload",
_t("scanner.webhook.invalid_json_payload"),
StatusCodes.Status400BadRequest);
}
@@ -275,7 +276,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.InternalError,
"Webhook processing failed",
_t("scanner.webhook.processing_failed"),
StatusCodes.Status500InternalServerError,
detail: ex.Message);
}
@@ -303,9 +304,9 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid tenant context",
_t("scanner.webhook.invalid_tenant_context"),
StatusCodes.Status400BadRequest,
detail: tenantError ?? "tenant_missing");
detail: tenantError ?? _t("scanner.error.tenant_missing"));
}
// Docker Hub uses callback_url for validation
@@ -317,7 +318,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
@@ -356,9 +357,9 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid tenant context",
_t("scanner.webhook.invalid_tenant_context"),
StatusCodes.Status400BadRequest,
detail: tenantError ?? "tenant_missing");
detail: tenantError ?? _t("scanner.error.tenant_missing"));
}
// GitHub can send ping events for webhook validation
@@ -379,7 +380,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
@@ -420,9 +421,9 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid tenant context",
_t("scanner.webhook.invalid_tenant_context"),
StatusCodes.Status400BadRequest,
detail: tenantError ?? "tenant_missing");
detail: tenantError ?? _t("scanner.error.tenant_missing"));
}
// Only process push and merge request events
@@ -437,7 +438,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
@@ -476,9 +477,9 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid tenant context",
_t("scanner.webhook.invalid_tenant_context"),
StatusCodes.Status400BadRequest,
detail: tenantError ?? "tenant_missing");
detail: tenantError ?? _t("scanner.error.tenant_missing"));
}
var source = await FindSourceByNameAsync(sourceRepository, tenantId, sourceName, SbomSourceType.Zastava, ct);
@@ -487,7 +488,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.NotFound,
"Source not found",
_t("scanner.sources.not_found"),
StatusCodes.Status404NotFound);
}
@@ -539,7 +540,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Source does not support webhooks",
_t("scanner.webhook.source_no_webhooks"),
StatusCodes.Status400BadRequest);
}
@@ -549,7 +550,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Webhook secret is not configured",
_t("scanner.webhook.secret_not_configured"),
StatusCodes.Status401Unauthorized);
}
@@ -569,7 +570,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Missing webhook signature",
_t("scanner.webhook.missing_signature"),
StatusCodes.Status401Unauthorized);
}
@@ -583,7 +584,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.InternalError,
"Failed to resolve webhook secret",
_t("scanner.webhook.secret_resolve_failed"),
StatusCodes.Status500InternalServerError);
}
@@ -593,7 +594,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Authentication,
"Invalid webhook signature",
_t("scanner.webhook.invalid_signature"),
StatusCodes.Status401Unauthorized);
}
@@ -612,7 +613,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.Validation,
"Invalid JSON payload",
_t("scanner.webhook.invalid_json_payload"),
StatusCodes.Status400BadRequest);
}
@@ -703,7 +704,7 @@ internal static class WebhookEndpoints
return ProblemResultFactory.Create(
context,
ProblemTypes.InternalError,
"Webhook processing failed",
_t("scanner.webhook.processing_failed"),
StatusCodes.Status500InternalServerError,
detail: ex.Message);
}

View File

@@ -20,6 +20,7 @@ using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using StellaOps.Determinism;
using StellaOps.Infrastructure.Postgres.Options;
using StellaOps.Localization;
using StellaOps.Plugin.DependencyInjection;
using StellaOps.Policy;
using StellaOps.Policy.Explainability;
@@ -553,6 +554,34 @@ builder.Services.AddSingleton<IAdvisoryLinksetQueryService>(sp =>
builder.Services.AddStellaOpsTenantServices();
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
builder.Services.AddStellaOpsLocalization(builder.Configuration, options =>
{
options.DefaultLocale = string.IsNullOrWhiteSpace(options.DefaultLocale) ? "en-US" : options.DefaultLocale;
if (options.SupportedLocales.Count == 0)
{
options.SupportedLocales.Add("en-US");
}
if (!options.SupportedLocales.Contains("de-DE", StringComparer.OrdinalIgnoreCase))
{
options.SupportedLocales.Add("de-DE");
}
if (string.IsNullOrWhiteSpace(options.RemoteBundleUrl))
{
var platformUrl = builder.Configuration["STELLAOPS_PLATFORM_URL"] ?? builder.Configuration["Platform:BaseUrl"];
if (!string.IsNullOrWhiteSpace(platformUrl))
{
options.RemoteBundleUrl = platformUrl;
}
}
options.EnableRemoteBundles =
options.EnableRemoteBundles || !string.IsNullOrWhiteSpace(options.RemoteBundleUrl);
});
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
builder.Services.AddRemoteTranslationBundles();
builder.TryAddStellaOpsLocalBinding("scanner");
var app = builder.Build();
app.LogStellaOpsLocalHostname("scanner");
@@ -627,6 +656,7 @@ app.UseExceptionHandler(errorApp =>
// Always add authentication and authorization middleware
// Even in anonymous mode, endpoints use RequireAuthorization() which needs the middleware
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
@@ -640,6 +670,8 @@ app.UseIdempotency();
// Rate limiting for replay/manifest endpoints (Sprint: SPRINT_3500_0002_0003)
app.UseRateLimiter();
await app.LoadTranslationsAsync();
app.MapHealthEndpoints();
app.MapObservabilityEndpoints();
app.MapOfflineKitEndpoints();

View File

@@ -59,6 +59,11 @@
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Validation/StellaOps.Scanner.Validation.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Scanner.Gate/StellaOps.Scanner.Gate.csproj" />
<ProjectReference Include="../../Router/__Libraries/StellaOps.Router.AspNet/StellaOps.Router.AspNet.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.Localization/StellaOps.Localization.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Translations\*.json" />
</ItemGroup>
<PropertyGroup Label="StellaOpsReleaseVersion">

View File

@@ -20,3 +20,4 @@ Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_appl
| SPRINT-20260222-057-SCAN-TEN-10 | DONE | `SPRINT_20260222_057_Scanner_tenant_isolation_for_scans_triage_webhooks.md`: activated `/api/v1/unknowns` endpoint map with tenant-aware resolver + query service wiring (2026-02-22). |
| SPRINT-20260222-057-SCAN-TEN-11 | DONE | `SPRINT_20260222_057_Scanner_tenant_isolation_for_scans_triage_webhooks.md`: propagated resolved tenant context through SmartDiff/Reachability endpoints into tenant-partitioned repository queries (2026-02-23). |
| SPRINT-20260222-057-SCAN-TEN-13 | DONE | `SPRINT_20260222_057_Scanner_tenant_isolation_for_scans_triage_webhooks.md`: updated source-run and secret-exception service/endpoints to require tenant-scoped repository lookups for API-backed tenant tables (2026-02-23). |
| SPRINT-20260224-002-LOC-101 | DONE | `SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: adopted StellaOps localization runtime bundle loading in Scanner WebService and replaced selected hardcoded endpoint strings with `_t(...)` keys (en-US/de-DE bundles added). |

View File

@@ -0,0 +1,9 @@
{
"_meta": { "locale": "de-DE", "namespace": "scanner", "version": "1.0" },
"scanner.slice.scan_id_required": "scanId ist erforderlich",
"scanner.slice.cve_or_symbols_required": "Entweder cveId oder symbols muss angegeben werden",
"scanner.slice.digest_required": "digest ist erforderlich",
"scanner.slice.slice_digest_required": "sliceDigest ist erforderlich",
"scanner.slice.not_found": "Slice {digest} wurde nicht gefunden"
}

View File

@@ -0,0 +1,362 @@
{
"_meta": { "locale": "en-US", "namespace": "scanner", "version": "1.0" },
"scanner.error.invalid_tenant": "Invalid tenant context",
"scanner.slice.scan_id_required": "scanId is required",
"scanner.slice.cve_or_symbols_required": "Either cveId or symbols must be specified",
"scanner.slice.digest_required": "digest is required",
"scanner.slice.slice_digest_required": "sliceDigest is required",
"scanner.slice.not_found": "Slice {digest} not found",
"scanner.scan.invalid_submission": "Invalid scan submission",
"scanner.scan.image_descriptor_required": "Request image descriptor is required.",
"scanner.scan.image_ref_or_digest_required": "Either image.reference or image.digest must be provided.",
"scanner.scan.image_digest_prefix_required": "Image digest must include algorithm prefix (e.g. sha256:...).",
"scanner.scan.invalid_identifier": "Invalid scan identifier",
"scanner.scan.identifier_required": "Scan identifier is required.",
"scanner.scan.not_found": "Scan not found",
"scanner.scan.not_found_detail": "Requested scan could not be located.",
"scanner.scan.entropy_layers_required": "Entropy layers are required",
"scanner.scan.entrytrace_not_found": "EntryTrace not found",
"scanner.scan.entrytrace_not_found_detail": "EntryTrace data is not available for the requested scan.",
"scanner.scan.ruby_packages_not_found": "Ruby packages not found",
"scanner.scan.ruby_packages_not_found_detail": "Ruby package inventory is not available for the requested scan.",
"scanner.scan.bun_packages_not_found": "Bun packages not found",
"scanner.scan.bun_packages_not_found_detail": "Bun package inventory is not available for the requested scan.",
"scanner.sbom.invalid_json": "Invalid JSON",
"scanner.sbom.invalid_json_detail": "Request body must be valid JSON.",
"scanner.sbom.unknown_format": "Unknown SBOM format",
"scanner.sbom.unknown_format_detail": "Unable to determine SBOM format. Provide Content-Type or use a supported format.",
"scanner.sbom.invalid": "Invalid SBOM",
"scanner.sbom.invalid_detail": "The SBOM document failed validation.",
"scanner.sbom.invalid_identifier": "Invalid SBOM identifier",
"scanner.sbom.upload_not_found": "SBOM upload not found",
"scanner.sbom.no_data_available": "No SBOM data available",
"scanner.sbom.no_data_available_detail": "No SBOM data is available for the requested scan.",
"scanner.sbom.no_sarif_findings": "No findings available",
"scanner.sbom.no_sarif_findings_detail": "No findings are available to export as SARIF for this scan.",
"scanner.sbom.no_cdx_findings": "No findings available",
"scanner.sbom.no_cdx_findings_detail": "No findings are available to export as CycloneDX for this scan.",
"scanner.sbom.no_vex_data": "No VEX data available",
"scanner.sbom.no_vex_data_detail": "No VEX data is available for the requested scan.",
"scanner.sbom.validate_description": "Validates an SBOM document against CycloneDX or SPDX schemas",
"scanner.sbom.validators_description": "Gets information about available SBOM validators",
"scanner.sbom.empty_body": "Empty request body",
"scanner.sbom.empty_body_detail": "SBOM document is required",
"scanner.sbom.invalid_format": "Invalid format",
"scanner.sbom.validator_unavailable": "Validator unavailable",
"scanner.sbom_hot_lookup.invalid_digest": "Invalid payload digest",
"scanner.sbom_hot_lookup.invalid_digest_detail": "Payload digest is required.",
"scanner.sbom_hot_lookup.no_projection": "No SBOM projection found",
"scanner.sbom_hot_lookup.no_projection_detail": "No SBOM projection found for the given digest.",
"scanner.sbom_hot_lookup.invalid_component_query": "Invalid component query",
"scanner.sbom_hot_lookup.invalid_component_query_detail": "At least one query parameter is required.",
"scanner.sbom_hot_lookup.ambiguous_query": "Ambiguous component query",
"scanner.sbom_hot_lookup.ambiguous_query_detail": "Query matches multiple components. Refine your query.",
"scanner.sbom_hot_lookup.invalid_limit": "Invalid limit",
"scanner.sbom_hot_lookup.invalid_limit_detail": "limit must be between 1 and 1000.",
"scanner.sbom_hot_lookup.invalid_offset": "Invalid offset",
"scanner.sbom_hot_lookup.invalid_offset_detail": "offset must be >= 0.",
"scanner.evidence.finding_detail_description": "Retrieves unified evidence for a specific finding within a scan.",
"scanner.evidence.list_description": "Lists all findings with evidence for a scan.",
"scanner.evidence.invalid_finding_id": "Invalid finding identifier",
"scanner.evidence.invalid_finding_id_detail": "Finding identifier is required.",
"scanner.evidence.finding_not_found": "Finding not found",
"scanner.evidence.finding_not_found_detail": "Requested finding could not be located.",
"scanner.reachability.computation_in_progress": "Computation already in progress",
"scanner.reachability.computation_in_progress_detail": "Reachability computation already running for scan {scanId}.",
"scanner.reachability.missing_parameters": "Missing required parameters",
"scanner.reachability.missing_parameters_detail": "Either purl or cveId must be provided.",
"scanner.reachability.explanation_not_found": "Explanation not found",
"scanner.reachability.explanation_not_found_detail": "No reachability data for CVE {cve} and PURL {purl}.",
"scanner.reachability.trace_not_available": "Trace export not available",
"scanner.reachability.trace_not_available_detail": "Trace export is not supported for this scan.",
"scanner.reachability.stack_not_available": "Reachability stack not available",
"scanner.reachability.stack_not_available_detail": "Reachability stack analysis is not yet implemented.",
"scanner.reachability.stack_not_found": "Reachability stack not found",
"scanner.reachability.stack_not_found_detail": "Reachability stack not found for finding {findingId}.",
"scanner.reachability.invalid_layer": "Invalid layer number",
"scanner.reachability.invalid_layer_detail": "Layer number must be a positive integer.",
"scanner.approval.create_description": "Creates a human approval attestation for a finding.",
"scanner.approval.list_description": "Lists all active approvals for a scan.",
"scanner.approval.get_description": "Gets an approval for a specific finding.",
"scanner.approval.revoke_description": "Revokes an existing approval.",
"scanner.approval.body_required": "Request body is required",
"scanner.approval.body_required_detail": "Request body is required.",
"scanner.approval.finding_id_required": "FindingId is required",
"scanner.approval.finding_id_required_detail": "FindingId must be provided.",
"scanner.approval.justification_required": "Justification is required",
"scanner.approval.justification_required_detail": "Justification must be provided.",
"scanner.approval.approver_unidentified": "Unable to identify approver",
"scanner.approval.approver_unidentified_detail": "Could not determine approver identity.",
"scanner.approval.invalid_decision": "Invalid decision value",
"scanner.approval.invalid_decision_detail": "Decision value '{decision}' is not valid.",
"scanner.approval.create_failed": "Failed to create approval",
"scanner.approval.create_failed_detail": "The approval could not be created.",
"scanner.approval.not_found": "Approval not found",
"scanner.approval.not_found_detail": "No approval found for finding '{findingId}'.",
"scanner.approval.revoker_unidentified": "Unable to identify revoker",
"scanner.approval.revoker_unidentified_detail": "Could not determine revoker identity.",
"scanner.baseline.recommendations_description": "Get recommended baselines for an artifact with rationale.",
"scanner.baseline.rationale_description": "Get detailed rationale for a baseline selection.",
"scanner.baseline.invalid_digest": "Invalid artifact digest",
"scanner.baseline.digest_required": "Artifact digest is required.",
"scanner.baseline.invalid_base_digest": "Invalid base digest",
"scanner.baseline.base_digest_required": "Base digest is required.",
"scanner.baseline.invalid_head_digest": "Invalid head digest",
"scanner.baseline.head_digest_required": "Head digest is required.",
"scanner.baseline.not_found": "Baseline not found",
"scanner.baseline.not_found_detail": "Baseline not found for artifact '{digest}'.",
"scanner.manifest.get_description": "Get the scan manifest, optionally with DSSE signature",
"scanner.manifest.list_proofs_description": "List all proof bundles for a scan",
"scanner.manifest.get_proof_description": "Get a specific proof bundle by root hash",
"scanner.manifest.not_found": "Manifest not found",
"scanner.manifest.not_found_detail": "Manifest not found for scan '{scanId}'.",
"scanner.manifest.proof_not_found": "Proof bundle not found",
"scanner.manifest.proof_not_found_detail": "Proof bundle not found for root hash '{rootHash}'.",
"scanner.manifest.invalid_root_hash": "Invalid root hash",
"scanner.manifest.root_hash_required": "Root hash is required.",
"scanner.policy.invalid_diagnostics_request": "Invalid policy diagnostics request",
"scanner.policy.invalid_preview_request": "Invalid policy preview request",
"scanner.policy.invalid_runtime_request": "Invalid runtime policy request",
"scanner.policy.invalid_linkset_request": "Invalid linkset request",
"scanner.policy.invalid_overlay_request": "Invalid policy overlay request",
"scanner.export.no_findings": "No findings available",
"scanner.export.no_findings_detail": "No findings are available to export for this scan.",
"scanner.export.no_sbom": "No SBOM data available",
"scanner.export.no_sbom_detail": "No SBOM data is available for the requested scan.",
"scanner.export.no_vex": "No VEX data available",
"scanner.export.no_vex_detail": "No VEX data is available for the requested scan.",
"scanner.layer_sbom.invalid_digest": "Invalid layer digest",
"scanner.layer_sbom.invalid_digest_detail": "Layer digest is required.",
"scanner.layer_sbom.not_found": "Layer SBOM not found",
"scanner.layer_sbom.not_found_detail": "Layer SBOM not found for digest '{digest}'.",
"scanner.layer_sbom.recipe_not_found": "Composition recipe not found",
"scanner.layer_sbom.recipe_not_found_detail": "Composition recipe not found for the requested scan.",
"scanner.callgraph.missing_content_digest": "Missing Content-Digest header",
"scanner.callgraph.missing_content_digest_detail": "Content-Digest header is required for idempotent call graph submission.",
"scanner.callgraph.invalid": "Invalid call graph",
"scanner.callgraph.invalid_detail": "Call graph validation failed.",
"scanner.callgraph.duplicate": "Duplicate call graph",
"scanner.callgraph.duplicate_detail": "Call graph with this Content-Digest already submitted.",
"scanner.runtime.invalid_ingest": "Invalid runtime ingest request",
"scanner.runtime.batch_too_large": "Runtime event batch too large",
"scanner.runtime.rate_limited": "Runtime ingestion rate limited",
"scanner.runtime.unsupported_schema": "Unsupported runtime schema version",
"scanner.runtime.invalid_reconciliation": "Invalid reconciliation request",
"scanner.runtime.description": "Compares libraries observed at runtime against the static SBOM to identify discrepancies",
"scanner.report.invalid_request": "Invalid report request",
"scanner.report.assembly_failed": "Unable to assemble report",
"scanner.report.assembly_failed_detail": "The report could not be assembled.",
"scanner.sources.tenant_required": "Tenant context required",
"scanner.sources.tenant_required_detail": "A valid tenant context is required for this operation.",
"scanner.sources.not_found": "Source not found",
"scanner.sources.not_found_by_id_detail": "Source {sourceId} not found",
"scanner.sources.not_found_by_name_detail": "Source '{name}' not found",
"scanner.sources.already_exists": "Source already exists",
"scanner.sources.already_exists_detail": "A source with this name already exists.",
"scanner.sources.invalid_request": "Invalid request",
"scanner.sources.update_conflict": "Update conflict",
"scanner.sources.update_conflict_detail": "The source has been modified since it was last read.",
"scanner.sources.run_not_found": "Run not found",
"scanner.sources.run_not_found_detail": "The requested run could not be located.",
"scanner.sources.cannot_trigger": "Cannot trigger scan",
"scanner.sources.cannot_trigger_detail": "Unable to trigger a scan for the requested source.",
"scanner.actionables.description": "Get actionable recommendations for a delta comparison.",
"scanner.actionables.by_priority_description": "Get actionables filtered by priority level.",
"scanner.actionables.by_type_description": "Get actionables filtered by action type.",
"scanner.actionables.invalid_delta_id": "Invalid delta ID",
"scanner.actionables.delta_id_required": "Delta ID is required.",
"scanner.actionables.delta_not_found": "Delta not found",
"scanner.actionables.delta_not_found_detail": "Delta '{deltaId}' not found.",
"scanner.actionables.invalid_priority": "Invalid priority",
"scanner.actionables.invalid_priority_detail": "Priority '{priority}' is not valid.",
"scanner.actionables.invalid_type": "Invalid type",
"scanner.actionables.invalid_type_detail": "Type '{type}' is not valid.",
"scanner.counterfactual.compute_description": "Compute counterfactual paths for a blocked finding.",
"scanner.counterfactual.get_description": "Get computed counterfactuals for a specific finding.",
"scanner.counterfactual.summary_description": "Get counterfactual summary for all blocked findings in a scan.",
"scanner.counterfactual.invalid_finding_id": "Invalid finding ID",
"scanner.counterfactual.finding_id_required": "Finding ID is required.",
"scanner.counterfactual.not_found": "Counterfactuals not found",
"scanner.counterfactual.not_found_detail": "Counterfactuals not found for finding '{findingId}'.",
"scanner.counterfactual.invalid_scan_id": "Invalid scan ID",
"scanner.counterfactual.scan_id_required": "Scan ID is required.",
"scanner.counterfactual.scan_not_found": "Scan not found",
"scanner.counterfactual.scan_not_found_detail": "Scan '{scanId}' not found.",
"scanner.delta.compare_description": "Compares two scan snapshots and returns detailed delta.",
"scanner.delta.quick_diff_description": "Returns quick diff summary for Can I Ship header.",
"scanner.delta.get_cached_description": "Retrieves a cached comparison result by ID.",
"scanner.delta.invalid_base_digest": "Invalid base digest",
"scanner.delta.base_digest_required": "Base digest is required.",
"scanner.delta.invalid_target_digest": "Invalid target digest",
"scanner.delta.target_digest_required": "Target digest is required.",
"scanner.delta.invalid_comparison_id": "Invalid comparison ID",
"scanner.delta.comparison_id_required": "Comparison ID is required.",
"scanner.delta.comparison_not_found": "Comparison not found",
"scanner.delta.comparison_not_found_detail": "Comparison '{comparisonId}' not found.",
"scanner.delta_evidence.invalid_identifiers": "Invalid identifiers",
"scanner.delta_evidence.identifiers_required": "Both comparison ID and finding ID are required.",
"scanner.delta_evidence.finding_not_found": "Finding not found",
"scanner.delta_evidence.finding_not_found_detail": "Finding not found for comparison '{comparisonId}'.",
"scanner.delta_evidence.proof_not_found": "Proof bundle not found",
"scanner.delta_evidence.proof_not_found_detail": "Proof bundle not found for finding '{findingId}'.",
"scanner.delta_evidence.attestations_not_found": "Attestations not found",
"scanner.delta_evidence.attestations_not_found_detail": "Attestations not found for finding '{findingId}'.",
"scanner.github.owner_repo_required": "Owner and repo are required",
"scanner.github.owner_repo_required_detail": "Both owner and repo must be provided.",
"scanner.github.no_findings": "No findings to export",
"scanner.github.no_findings_detail": "No findings available to export as SARIF.",
"scanner.github.sarif_not_found": "SARIF upload not found",
"scanner.github.sarif_not_found_detail": "The requested SARIF upload could not be found.",
"scanner.github.alert_not_found": "Alert not found",
"scanner.github.alert_not_found_detail": "The requested alert could not be found.",
"scanner.triage.proof_bundle_description": "Generates an attested proof bundle for an exploit path.",
"scanner.triage.inbox_description": "Retrieves triage inbox with grouped exploit paths for an artifact.",
"scanner.triage.finding_status_description": "Retrieves triage status for a specific finding.",
"scanner.triage.update_status_description": "Updates triage status for a finding (lane change, decision).",
"scanner.triage.submit_vex_description": "Submits a VEX statement for a finding.",
"scanner.triage.query_description": "Queries findings with filtering and pagination.",
"scanner.triage.summary_description": "Returns triage summary statistics for an artifact.",
"scanner.triage.cluster_stats_description": "Returns per-cluster severity and reachability distributions.",
"scanner.triage.cluster_action_description": "Applies one triage action to all findings in an exploit-path cluster.",
"scanner.triage.invalid_path_id": "Invalid path ID",
"scanner.triage.path_id_required": "Path ID is required.",
"scanner.triage.invalid_artifact_digest": "Invalid artifact digest",
"scanner.triage.artifact_digest_required": "Artifact digest is required.",
"scanner.triage.invalid_finding_id": "Invalid finding ID",
"scanner.triage.finding_id_required": "Finding ID is required.",
"scanner.triage.finding_not_found": "Finding not found",
"scanner.triage.finding_not_found_detail": "Finding with ID '{findingId}' was not found.",
"scanner.triage.invalid_vex_status": "Invalid VEX status",
"scanner.triage.vex_status_required": "VEX status is required.",
"scanner.triage.vex_status_invalid_detail": "VEX status must be one of: Affected, NotAffected, UnderInvestigation, Unknown",
"scanner.triage.justification_required": "Justification required",
"scanner.triage.justification_required_detail": "Justification is required when status is NotAffected.",
"scanner.triage.invalid_path_id_batch": "Invalid path id",
"scanner.triage.path_id_batch_required": "Path id is required.",
"scanner.triage.cluster_not_found": "Cluster not found",
"scanner.triage.cluster_not_found_detail": "Cluster '{pathId}' was not found for artifact '{artifactDigest}'.",
"scanner.score_replay.invalid_scan_id": "Invalid scan ID",
"scanner.score_replay.scan_id_required": "Scan ID is required",
"scanner.score_replay.scan_not_found": "Scan not found",
"scanner.score_replay.scan_not_found_detail": "No scan found with ID: {scanId}",
"scanner.score_replay.bundle_not_found": "Bundle not found",
"scanner.score_replay.bundle_not_found_detail": "No proof bundle found for scan: {scanId}",
"scanner.score_replay.replay_failed": "Replay failed",
"scanner.score_replay.missing_root_hash": "Missing expected root hash",
"scanner.score_replay.root_hash_required": "Expected root hash is required for verification",
"scanner.score_replay.replay_description": "Replay scoring for a previous scan using frozen inputs",
"scanner.score_replay.bundle_description": "Get the proof bundle for a scan",
"scanner.score_replay.verify_description": "Verify a proof bundle against expected root hash",
"scanner.epss.invalid_request": "Invalid request",
"scanner.epss.cve_ids_required": "At least one CVE ID is required.",
"scanner.epss.batch_size_exceeded": "Batch size exceeded",
"scanner.epss.batch_size_detail": "Maximum batch size is 1000 CVE IDs.",
"scanner.epss.data_unavailable": "EPSS data is not available. Please ensure EPSS data has been ingested.",
"scanner.epss.invalid_cve_id": "Invalid CVE ID",
"scanner.epss.cve_id_required": "CVE ID is required.",
"scanner.epss.cve_not_found": "CVE not found",
"scanner.epss.cve_not_found_detail": "No EPSS score found for {cveId}.",
"scanner.epss.invalid_date_format": "Invalid date format",
"scanner.epss.date_format_detail": "Dates must be in yyyy-MM-dd format.",
"scanner.epss.no_history": "No history found",
"scanner.epss.no_history_detail": "No EPSS history found for {cveId} in the specified date range.",
"scanner.webhook.invalid_tenant": "Invalid tenant context",
"scanner.webhook.source_not_found": "Source not found",
"scanner.webhook.source_no_webhooks": "Source does not support webhooks",
"scanner.webhook.secret_not_configured": "Webhook secret is not configured",
"scanner.webhook.missing_signature": "Missing webhook signature",
"scanner.webhook.resolve_secret_failed": "Failed to resolve webhook secret",
"scanner.webhook.invalid_signature": "Invalid webhook signature",
"scanner.webhook.invalid_json_payload": "Invalid JSON payload",
"scanner.webhook.processing_failed": "Webhook processing failed",
"scanner.unknowns.list_description": "Lists unknown entries with tenant-scoped filtering.",
"scanner.unknowns.stats_description": "Returns tenant-scoped unknown summary statistics.",
"scanner.unknowns.bands_description": "Returns tenant-scoped unknown distribution by triage band.",
"scanner.unknowns.evidence_description": "Returns tenant-scoped unknown evidence metadata.",
"scanner.unknowns.history_description": "Returns tenant-scoped unknown history.",
"scanner.unknowns.get_description": "Returns tenant-scoped unknown detail.",
"scanner.unknowns.invalid_band": "Invalid band",
"scanner.unknowns.band_detail": "Band must be one of HOT, WARM, or COLD.",
"scanner.secret_settings.get_description": "Get secret detection settings for a tenant.",
"scanner.secret_settings.create_description": "Create default secret detection settings for a tenant.",
"scanner.secret_settings.update_description": "Update secret detection settings for a tenant.",
"scanner.secret_settings.list_exceptions_description": "List secret exception patterns for a tenant.",
"scanner.secret_settings.get_exception_description": "Get a specific secret exception pattern.",
"scanner.secret_settings.create_exception_description": "Create a new secret exception pattern.",
"scanner.secret_settings.update_exception_description": "Update a secret exception pattern.",
"scanner.secret_settings.delete_exception_description": "Delete a secret exception pattern.",
"scanner.secret_settings.get_categories_description": "Get available secret detection rule categories.",
"scanner.secret_settings.not_found": "Settings not found",
"scanner.secret_settings.not_found_detail": "No secret detection settings found for tenant '{tenantId}'.",
"scanner.secret_settings.already_exist": "Settings already exist",
"scanner.secret_settings.already_exist_detail": "Secret detection settings already exist for tenant '{tenantId}'.",
"scanner.secret_settings.version_conflict": "Version conflict",
"scanner.secret_settings.validation_failed": "Validation failed",
"scanner.secret_settings.exception_not_found": "Exception pattern not found",
"scanner.secret_settings.exception_not_found_detail": "No exception pattern found with ID '{exceptionId}'.",
"scanner.offline_kit.not_enabled": "Offline kit import is not enabled",
"scanner.offline_kit.status_not_enabled": "Offline kit status is not enabled",
"scanner.offline_kit.manifest_not_enabled": "Offline kit is not enabled",
"scanner.offline_kit.validate_not_enabled": "Offline kit validation is not enabled",
"scanner.offline_kit.invalid_import": "Invalid offline kit import request",
"scanner.offline_kit.multipart_required": "Request must be multipart/form-data.",
"scanner.offline_kit.metadata_field_missing": "Missing 'metadata' form field.",
"scanner.offline_kit.metadata_empty": "Metadata payload is empty.",
"scanner.offline_kit.bundle_field_missing": "Missing 'bundle' file upload.",
"scanner.offline_kit.import_failed": "Offline kit import failed",
"scanner.offline_kit.invalid_validation_request": "Invalid validation request",
"scanner.offline_kit.manifest_required": "Request body with manifestJson is required.",
"scanner.drift.invalid_identifier": "Invalid drift identifier",
"scanner.drift.identifier_detail": "driftId must be a non-empty GUID.",
"scanner.drift.invalid_direction": "Invalid direction",
"scanner.drift.direction_detail": "direction must be 'became_reachable' or 'became_unreachable'.",
"scanner.drift.invalid_offset": "Invalid offset",
"scanner.drift.offset_detail": "offset must be >= 0.",
"scanner.drift.invalid_limit": "Invalid limit",
"scanner.drift.limit_detail": "limit must be between 1 and 500.",
"scanner.drift.result_not_found": "Drift result not found",
"scanner.drift.result_not_found_detail": "Requested drift result could not be located.",
"scanner.drift.no_cached_result_detail": "No reachability drift result recorded for scan {scanId} (language={language}).",
"scanner.drift.invalid_base_scan": "Invalid base scan identifier",
"scanner.drift.base_scan_detail": "Query parameter 'baseScanId' must be a valid scan id.",
"scanner.drift.base_scan_not_found": "Base scan not found",
"scanner.drift.base_scan_not_found_detail": "Base scan could not be located.",
"scanner.drift.base_graph_not_found": "Base call graph not found",
"scanner.drift.base_graph_not_found_detail": "No call graph snapshot found for base scan {scanId} (language={language}).",
"scanner.drift.head_graph_not_found": "Head call graph not found",
"scanner.drift.head_graph_not_found_detail": "No call graph snapshot found for head scan {scanId} (language={language}).",
"scanner.drift.invalid_request": "Invalid drift request",
"scanner.fidelity.analyze_description": "Analyze with specified fidelity level",
"scanner.fidelity.upgrade_description": "Upgrade analysis fidelity for a finding"
}

View File

@@ -0,0 +1,42 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using StellaOps.Auth.Abstractions;
using StellaOps.Scanner.WebService.Endpoints;
using StellaOps.TestKit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class SliceLocalizationEndpointsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task QuerySlice_WithoutScanId_WithGermanLocale_ReturnsLocalizedError()
{
await using var factory = ScannerApplicationFactory.CreateLightweight().WithOverrides(
configureConfiguration: static config =>
{
config["scanner:authority:enabled"] = "false";
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/slices/query")
{
Content = JsonContent.Create(new SliceQueryRequestDto
{
CveId = "CVE-2024-1234"
})
};
request.Headers.TryAddWithoutValidation(StellaOpsHttpHeaderNames.Tenant, "tenant-a");
request.Headers.TryAddWithoutValidation("X-Locale", "de-DE");
var response = await client.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
using var payload = JsonDocument.Parse(content);
Assert.Equal("scanId ist erforderlich", payload.RootElement.GetProperty("error").GetString());
}
}

View File

@@ -15,3 +15,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| SPRINT-20260222-057-SCAN-TEN-10 | DONE | `SPRINT_20260222_057_Scanner_tenant_isolation_for_scans_triage_webhooks.md`: added focused Unknowns endpoint tenant-isolation coverage (`UnknownsTenantIsolationEndpointsTests`) for cross-tenant not-found and tenant-conflict rejection (2026-02-22). |
| SPRINT-20260222-057-SCAN-TEN-11 | DONE | `SPRINT_20260222_057_Scanner_tenant_isolation_for_scans_triage_webhooks.md`: added SmartDiff and Reachability tenant-propagation regression checks (`SmartDiffEndpointsTests`, `ReachabilityDriftEndpointsTests`) and validated focused suites (2026-02-23). |
| SPRINT-20260222-057-SCAN-TEN-13 | DONE | `SPRINT_20260222_057_Scanner_tenant_isolation_for_scans_triage_webhooks.md`: added `SecretExceptionPatternServiceTenantIsolationTests` validating tenant-scoped repository lookups for exception get/update/delete (`3` tests, 2026-02-23). |
| SPRINT-20260224-002-LOC-101-T | DONE | `SPRINT_20260224_002_Platform_translation_rollout_phase3_phase4.md`: added focused Scanner localization endpoint behavior test (`SliceLocalizationEndpointsTests`) and validated targeted German locale response text. |