Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Scanner.WebService.Constants;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Infrastructure;
|
||||
using StellaOps.Scanner.WebService.Security;
|
||||
using StellaOps.Scanner.WebService.Services;
|
||||
using StellaOps.Zastava.Core.Contracts;
|
||||
using RuntimePolicyVerdict = StellaOps.Zastava.Core.Contracts.PolicyVerdict;
|
||||
|
||||
namespace StellaOps.Scanner.WebService.Endpoints;
|
||||
|
||||
internal static class PolicyEndpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
public static void MapPolicyEndpoints(this RouteGroupBuilder apiGroup, string policySegment)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(apiGroup);
|
||||
|
||||
var policyGroup = apiGroup
|
||||
.MapGroup(NormalizeSegment(policySegment))
|
||||
.WithTags("Policy");
|
||||
|
||||
policyGroup.MapGet("/schema", HandleSchemaAsync)
|
||||
.WithName("scanner.policy.schema")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.RequireAuthorization(ScannerPolicies.Reports)
|
||||
.WithOpenApi(operation =>
|
||||
{
|
||||
operation.Summary = "Retrieve the embedded policy JSON schema.";
|
||||
operation.Description = "Returns the policy schema (`policy-schema@1`) used to validate YAML or JSON rulesets.";
|
||||
return operation;
|
||||
});
|
||||
|
||||
policyGroup.MapPost("/diagnostics", HandleDiagnosticsAsync)
|
||||
.WithName("scanner.policy.diagnostics")
|
||||
.Produces<PolicyDiagnosticsResponseDto>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(ScannerPolicies.Reports)
|
||||
.WithOpenApi(operation =>
|
||||
{
|
||||
operation.Summary = "Run policy diagnostics.";
|
||||
operation.Description = "Accepts YAML or JSON policy content and returns normalization issues plus recommendations (ignore rules, VEX include/exclude, vendor precedence).";
|
||||
return operation;
|
||||
});
|
||||
|
||||
policyGroup.MapPost("/preview", HandlePreviewAsync)
|
||||
.WithName("scanner.policy.preview")
|
||||
.Produces<PolicyPreviewResponseDto>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(ScannerPolicies.Reports)
|
||||
.WithOpenApi(operation =>
|
||||
{
|
||||
operation.Summary = "Preview policy impact against findings.";
|
||||
operation.Description = "Evaluates the supplied findings against the active or proposed policy, returning diffs, quieted verdicts, and actionable validation messages.";
|
||||
return operation;
|
||||
});
|
||||
|
||||
policyGroup.MapPost("/runtime", HandleRuntimePolicyAsync)
|
||||
.WithName("scanner.policy.runtime")
|
||||
.Produces<RuntimePolicyResponseDto>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status400BadRequest)
|
||||
.RequireAuthorization(ScannerPolicies.Reports)
|
||||
.WithOpenApi(operation =>
|
||||
{
|
||||
operation.Summary = "Evaluate runtime policy for digests.";
|
||||
operation.Description = "Returns per-image policy verdicts, signature and SBOM metadata, and cache hints for admission controllers.";
|
||||
return operation;
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult HandleSchemaAsync(HttpContext context)
|
||||
{
|
||||
var schema = PolicySchemaResource.ReadSchemaJson();
|
||||
return Results.Text(schema, "application/schema+json", Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static IResult HandleDiagnosticsAsync(
|
||||
PolicyDiagnosticsRequestDto request,
|
||||
TimeProvider timeProvider,
|
||||
HttpContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(timeProvider);
|
||||
|
||||
if (request.Policy is null || string.IsNullOrWhiteSpace(request.Policy.Content))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid policy diagnostics request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Policy content is required for diagnostics.");
|
||||
}
|
||||
|
||||
var format = PolicyDtoMapper.ParsePolicyFormat(request.Policy.Format);
|
||||
var binding = PolicyBinder.Bind(request.Policy.Content, format);
|
||||
var diagnostics = PolicyDiagnostics.Create(binding, timeProvider);
|
||||
|
||||
var response = new PolicyDiagnosticsResponseDto
|
||||
{
|
||||
Success = diagnostics.ErrorCount == 0,
|
||||
Version = diagnostics.Version,
|
||||
RuleCount = diagnostics.RuleCount,
|
||||
ErrorCount = diagnostics.ErrorCount,
|
||||
WarningCount = diagnostics.WarningCount,
|
||||
GeneratedAt = diagnostics.GeneratedAt,
|
||||
Issues = diagnostics.Issues.Select(PolicyDtoMapper.ToIssueDto).ToImmutableArray(),
|
||||
Recommendations = diagnostics.Recommendations
|
||||
};
|
||||
|
||||
return Json(response);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandlePreviewAsync(
|
||||
PolicyPreviewRequestDto request,
|
||||
PolicyPreviewService previewService,
|
||||
HttpContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(previewService);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.ImageDigest))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid policy preview request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "imageDigest is required.");
|
||||
}
|
||||
|
||||
if (!request.ImageDigest.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid policy preview request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "imageDigest must include algorithm prefix (e.g. sha256:...).");
|
||||
}
|
||||
|
||||
if (request.Findings is not null)
|
||||
{
|
||||
var missingIds = request.Findings.Any(f => string.IsNullOrWhiteSpace(f.Id));
|
||||
if (missingIds)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid policy preview request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "All findings must include an id value.");
|
||||
}
|
||||
}
|
||||
|
||||
var domainRequest = PolicyDtoMapper.ToDomain(request);
|
||||
var response = await previewService.PreviewAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
var payload = PolicyDtoMapper.ToDto(response);
|
||||
return Json(payload);
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleRuntimePolicyAsync(
|
||||
RuntimePolicyRequestDto request,
|
||||
IRuntimePolicyService runtimePolicyService,
|
||||
HttpContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(runtimePolicyService);
|
||||
|
||||
if (request.Images is null || request.Images.Count == 0)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime policy request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "images collection must include at least one digest.");
|
||||
}
|
||||
|
||||
var normalizedImages = new List<string>();
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var image in request.Images)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime policy request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Image digests must be non-empty.");
|
||||
}
|
||||
|
||||
var trimmed = image.Trim();
|
||||
if (!trimmed.Contains(':', StringComparison.Ordinal))
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime policy request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "Image digests must include an algorithm prefix (e.g. sha256:...).");
|
||||
}
|
||||
|
||||
if (seen.Add(trimmed))
|
||||
{
|
||||
normalizedImages.Add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedImages.Count == 0)
|
||||
{
|
||||
return ProblemResultFactory.Create(
|
||||
context,
|
||||
ProblemTypes.Validation,
|
||||
"Invalid runtime policy request",
|
||||
StatusCodes.Status400BadRequest,
|
||||
detail: "images collection must include at least one unique digest.");
|
||||
}
|
||||
|
||||
var namespaceValue = string.IsNullOrWhiteSpace(request.Namespace) ? null : request.Namespace.Trim();
|
||||
var normalizedLabels = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (request.Labels is not null)
|
||||
{
|
||||
foreach (var pair in request.Labels)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pair.Key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = pair.Key.Trim();
|
||||
var value = pair.Value?.Trim() ?? string.Empty;
|
||||
normalizedLabels[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
var evaluationRequest = new RuntimePolicyEvaluationRequest(
|
||||
namespaceValue,
|
||||
new ReadOnlyDictionary<string, string>(normalizedLabels),
|
||||
normalizedImages);
|
||||
|
||||
var evaluation = await runtimePolicyService.EvaluateAsync(evaluationRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var resultPayload = MapRuntimePolicyResponse(evaluation);
|
||||
return Json(resultPayload);
|
||||
}
|
||||
|
||||
private static string NormalizeSegment(string segment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(segment))
|
||||
{
|
||||
return "/policy";
|
||||
}
|
||||
|
||||
var trimmed = segment.Trim('/');
|
||||
return "/" + trimmed;
|
||||
}
|
||||
|
||||
private static IResult Json<T>(T value)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(value, SerializerOptions);
|
||||
return Results.Content(payload, "application/json", Encoding.UTF8);
|
||||
}
|
||||
|
||||
private static RuntimePolicyResponseDto MapRuntimePolicyResponse(RuntimePolicyEvaluationResult evaluation)
|
||||
{
|
||||
var results = new Dictionary<string, RuntimePolicyImageResponseDto>(evaluation.Results.Count, StringComparer.Ordinal);
|
||||
foreach (var pair in evaluation.Results)
|
||||
{
|
||||
var decision = pair.Value;
|
||||
RuntimePolicyRekorDto? rekor = null;
|
||||
if (decision.Rekor is not null)
|
||||
{
|
||||
rekor = new RuntimePolicyRekorDto
|
||||
{
|
||||
Uuid = decision.Rekor.Uuid,
|
||||
Url = decision.Rekor.Url,
|
||||
Verified = decision.Rekor.Verified
|
||||
};
|
||||
}
|
||||
|
||||
string? metadata = null;
|
||||
if (decision.Metadata is not null && decision.Metadata.Count > 0)
|
||||
{
|
||||
metadata = JsonSerializer.Serialize(decision.Metadata, SerializerOptions);
|
||||
}
|
||||
|
||||
results[pair.Key] = new RuntimePolicyImageResponseDto
|
||||
{
|
||||
PolicyVerdict = ToCamelCase(decision.PolicyVerdict),
|
||||
Signed = decision.Signed,
|
||||
HasSbomReferrers = decision.HasSbomReferrers,
|
||||
HasSbomLegacy = decision.HasSbomReferrers,
|
||||
Reasons = decision.Reasons.ToArray(),
|
||||
Rekor = rekor,
|
||||
Confidence = Math.Round(decision.Confidence, 6, MidpointRounding.AwayFromZero),
|
||||
Quieted = decision.Quieted,
|
||||
QuietedBy = decision.QuietedBy,
|
||||
Metadata = metadata,
|
||||
BuildIds = decision.BuildIds is { Count: > 0 } ? decision.BuildIds.ToArray() : null
|
||||
};
|
||||
}
|
||||
|
||||
return new RuntimePolicyResponseDto
|
||||
{
|
||||
TtlSeconds = evaluation.TtlSeconds,
|
||||
ExpiresAtUtc = evaluation.ExpiresAtUtc,
|
||||
PolicyRevision = evaluation.PolicyRevision,
|
||||
Results = results
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToCamelCase(RuntimePolicyVerdict verdict)
|
||||
=> verdict switch
|
||||
{
|
||||
RuntimePolicyVerdict.Pass => "pass",
|
||||
RuntimePolicyVerdict.Warn => "warn",
|
||||
RuntimePolicyVerdict.Fail => "fail",
|
||||
RuntimePolicyVerdict.Error => "error",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user