search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user