Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -11,11 +11,13 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.AdvisoryAI.Caching;
|
||||
using StellaOps.AdvisoryAI.Diagnostics;
|
||||
using StellaOps.AdvisoryAI.Explanation;
|
||||
using StellaOps.AdvisoryAI.Hosting;
|
||||
using StellaOps.AdvisoryAI.Metrics;
|
||||
using StellaOps.AdvisoryAI.Outputs;
|
||||
using StellaOps.AdvisoryAI.Orchestration;
|
||||
using StellaOps.AdvisoryAI.Queue;
|
||||
using StellaOps.AdvisoryAI.Remediation;
|
||||
using StellaOps.AdvisoryAI.WebService.Contracts;
|
||||
using StellaOps.Router.AspNet;
|
||||
|
||||
@@ -88,6 +90,23 @@ app.MapPost("/v1/advisory-ai/pipeline:batch", HandleBatchPlans)
|
||||
app.MapGet("/v1/advisory-ai/outputs/{cacheKey}", HandleGetOutput)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// Explanation endpoints (SPRINT_20251226_015_AI_zastava_companion)
|
||||
app.MapPost("/v1/advisory-ai/explain", HandleExplain)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.MapGet("/v1/advisory-ai/explain/{explanationId}/replay", HandleExplanationReplay)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// Remediation endpoints (SPRINT_20251226_016_AI_remedy_autopilot)
|
||||
app.MapPost("/v1/advisory-ai/remediation/plan", HandleRemediationPlan)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.MapPost("/v1/advisory-ai/remediation/apply", HandleApplyRemediation)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.MapGet("/v1/advisory-ai/remediation/status/{prId}", HandleRemediationStatus)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// Refresh Router endpoint cache
|
||||
app.TryRefreshStellaRouterEndpoints(routerOptions);
|
||||
|
||||
@@ -250,6 +269,213 @@ static bool EnsureAuthorized(HttpContext context, AdvisoryTaskType taskType)
|
||||
return allowed.Contains($"advisory:{taskType.ToString().ToLowerInvariant()}");
|
||||
}
|
||||
|
||||
static bool EnsureExplainAuthorized(HttpContext context)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var allowed = scopes
|
||||
.SelectMany(value => value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return allowed.Contains("advisory:run") || allowed.Contains("advisory:explain");
|
||||
}
|
||||
|
||||
// ZASTAVA-13: POST /v1/advisory-ai/explain
|
||||
static async Task<IResult> HandleExplain(
|
||||
HttpContext httpContext,
|
||||
ExplainRequest request,
|
||||
IExplanationGenerator explanationGenerator,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.explain", ActivityKind.Server);
|
||||
activity?.SetTag("advisory.finding_id", request.FindingId);
|
||||
activity?.SetTag("advisory.vulnerability_id", request.VulnerabilityId);
|
||||
activity?.SetTag("advisory.explanation_type", request.ExplanationType);
|
||||
|
||||
if (!EnsureExplainAuthorized(httpContext))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var domainRequest = request.ToDomain();
|
||||
var result = await explanationGenerator.GenerateAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("advisory.explanation_id", result.ExplanationId);
|
||||
activity?.SetTag("advisory.authority", result.Authority.ToString());
|
||||
activity?.SetTag("advisory.citation_rate", result.CitationRate);
|
||||
|
||||
return Results.Ok(ExplainResponse.FromDomain(result));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// ZASTAVA-14: GET /v1/advisory-ai/explain/{explanationId}/replay
|
||||
static async Task<IResult> HandleExplanationReplay(
|
||||
HttpContext httpContext,
|
||||
string explanationId,
|
||||
IExplanationGenerator explanationGenerator,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.explain_replay", ActivityKind.Server);
|
||||
activity?.SetTag("advisory.explanation_id", explanationId);
|
||||
|
||||
if (!EnsureExplainAuthorized(httpContext))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await explanationGenerator.ReplayAsync(explanationId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("advisory.replayed_explanation_id", result.ExplanationId);
|
||||
activity?.SetTag("advisory.authority", result.Authority.ToString());
|
||||
|
||||
return Results.Ok(ExplainResponse.FromDomain(result));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
static bool EnsureRemediationAuthorized(HttpContext context)
|
||||
{
|
||||
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var allowed = scopes
|
||||
.SelectMany(value => value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return allowed.Contains("advisory:run") || allowed.Contains("advisory:remediate");
|
||||
}
|
||||
|
||||
// REMEDY-19: POST /v1/advisory-ai/remediation/plan
|
||||
static async Task<IResult> HandleRemediationPlan(
|
||||
HttpContext httpContext,
|
||||
RemediationPlanApiRequest request,
|
||||
IRemediationPlanner remediationPlanner,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.remediation_plan", ActivityKind.Server);
|
||||
activity?.SetTag("advisory.finding_id", request.FindingId);
|
||||
activity?.SetTag("advisory.vulnerability_id", request.VulnerabilityId);
|
||||
activity?.SetTag("advisory.remediation_type", request.RemediationType);
|
||||
|
||||
if (!EnsureRemediationAuthorized(httpContext))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var domainRequest = request.ToDomain();
|
||||
var plan = await remediationPlanner.GeneratePlanAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("advisory.plan_id", plan.PlanId);
|
||||
activity?.SetTag("advisory.risk_assessment", plan.RiskAssessment.ToString());
|
||||
activity?.SetTag("advisory.pr_ready", plan.PrReady);
|
||||
|
||||
return Results.Ok(RemediationPlanApiResponse.FromDomain(plan));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// REMEDY-20: POST /v1/advisory-ai/remediation/apply
|
||||
static async Task<IResult> HandleApplyRemediation(
|
||||
HttpContext httpContext,
|
||||
ApplyRemediationRequest request,
|
||||
IRemediationPlanner remediationPlanner,
|
||||
IEnumerable<IPullRequestGenerator> prGenerators,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.apply_remediation", ActivityKind.Server);
|
||||
activity?.SetTag("advisory.plan_id", request.PlanId);
|
||||
activity?.SetTag("advisory.scm_type", request.ScmType);
|
||||
|
||||
if (!EnsureRemediationAuthorized(httpContext))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var plan = await remediationPlanner.GetPlanAsync(request.PlanId, cancellationToken).ConfigureAwait(false);
|
||||
if (plan is null)
|
||||
{
|
||||
return Results.NotFound(new { error = $"Plan {request.PlanId} not found" });
|
||||
}
|
||||
|
||||
var generator = prGenerators.FirstOrDefault(g => g.ScmType.Equals(request.ScmType, StringComparison.OrdinalIgnoreCase));
|
||||
if (generator is null)
|
||||
{
|
||||
return Results.BadRequest(new { error = $"SCM type '{request.ScmType}' not supported" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var prResult = await generator.CreatePullRequestAsync(plan, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("advisory.pr_id", prResult.PrId);
|
||||
activity?.SetTag("advisory.pr_status", prResult.Status.ToString());
|
||||
|
||||
return Results.Ok(PullRequestApiResponse.FromDomain(prResult));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// REMEDY-21: GET /v1/advisory-ai/remediation/status/{prId}
|
||||
static async Task<IResult> HandleRemediationStatus(
|
||||
HttpContext httpContext,
|
||||
string prId,
|
||||
string? scmType,
|
||||
IEnumerable<IPullRequestGenerator> prGenerators,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.remediation_status", ActivityKind.Server);
|
||||
activity?.SetTag("advisory.pr_id", prId);
|
||||
|
||||
if (!EnsureRemediationAuthorized(httpContext))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var resolvedScmType = scmType ?? "github";
|
||||
var generator = prGenerators.FirstOrDefault(g => g.ScmType.Equals(resolvedScmType, StringComparison.OrdinalIgnoreCase));
|
||||
if (generator is null)
|
||||
{
|
||||
return Results.BadRequest(new { error = $"SCM type '{resolvedScmType}' not supported" });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var prResult = await generator.GetStatusAsync(prId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("advisory.pr_status", prResult.Status.ToString());
|
||||
|
||||
return Results.Ok(PullRequestApiResponse.FromDomain(prResult));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.NotFound(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PipelinePlanRequest(
|
||||
AdvisoryTaskType? TaskType,
|
||||
string AdvisoryKey,
|
||||
|
||||
Reference in New Issue
Block a user