search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.Replay.Core.FeedSnapshots;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Replay.WebService;
|
||||
|
||||
@@ -26,7 +27,7 @@ public static class PointInTimeQueryEndpoints
|
||||
// GET /v1/pit/advisory/{cveId} - Query advisory state at a point in time
|
||||
group.MapGet("/{cveId}", QueryAdvisoryAsync)
|
||||
.WithName("QueryAdvisoryAtPointInTime")
|
||||
.WithDescription("Returns the advisory state for a specific CVE at a given point-in-time timestamp from the specified provider. Uses the nearest captured feed snapshot to reconstruct the advisory as it appeared at that moment.")
|
||||
.WithDescription(_t("replay.pit.query_description"))
|
||||
.Produces<AdvisoryQueryResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
@@ -34,21 +35,21 @@ public static class PointInTimeQueryEndpoints
|
||||
// POST /v1/pit/advisory/cross-provider - Query advisory across multiple providers
|
||||
group.MapPost("/cross-provider", QueryCrossProviderAsync)
|
||||
.WithName("QueryCrossProviderAdvisory")
|
||||
.WithDescription("Queries advisory state across multiple feed providers at a single point in time and returns per-provider results along with a consensus summary of severity and fix status.")
|
||||
.WithDescription(_t("replay.pit.cross_provider_description"))
|
||||
.Produces<CrossProviderQueryResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /v1/pit/advisory/{cveId}/timeline - Get advisory timeline
|
||||
group.MapGet("/{cveId}/timeline", GetAdvisoryTimelineAsync)
|
||||
.WithName("GetAdvisoryTimeline")
|
||||
.WithDescription("Returns the change timeline for a specific CVE from a given provider within an optional time range. Each entry identifies the snapshot digest and the type of change observed.")
|
||||
.WithDescription(_t("replay.pit.timeline_description"))
|
||||
.Produces<AdvisoryTimelineResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// POST /v1/pit/advisory/diff - Compare advisory at two points in time
|
||||
group.MapPost("/diff", CompareAdvisoryAtTimesAsync)
|
||||
.WithName("CompareAdvisoryAtTimes")
|
||||
.WithDescription("Produces a field-level diff of a CVE advisory between two distinct points in time from the same provider, identifying severity, fix-status, and metadata changes.")
|
||||
.WithDescription(_t("replay.pit.diff_description"))
|
||||
.Produces<AdvisoryDiffResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
@@ -59,28 +60,28 @@ public static class PointInTimeQueryEndpoints
|
||||
// POST /v1/pit/snapshots - Capture a feed snapshot
|
||||
snapshotsGroup.MapPost("/", CaptureSnapshotAsync)
|
||||
.WithName("CaptureFeedSnapshot")
|
||||
.WithDescription("Captures and stores a feed snapshot for a specific provider, computing its content-addressable digest. Returns 201 Created with the digest and whether an existing snapshot was reused.")
|
||||
.WithDescription(_t("replay.snapshot.capture_description"))
|
||||
.Produces<SnapshotCaptureResponse>(StatusCodes.Status201Created)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /v1/pit/snapshots/{digest} - Get a snapshot by digest
|
||||
snapshotsGroup.MapGet("/{digest}", GetSnapshotAsync)
|
||||
.WithName("GetFeedSnapshot")
|
||||
.WithDescription("Returns snapshot metadata for a specific content-addressable digest including provider ID, feed type, and capture timestamp. Returns 404 if the digest is not stored.")
|
||||
.WithDescription(_t("replay.snapshot.get_description"))
|
||||
.Produces<SnapshotResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// GET /v1/pit/snapshots/{digest}/verify - Verify snapshot integrity
|
||||
snapshotsGroup.MapGet("/{digest}/verify", VerifySnapshotIntegrityAsync)
|
||||
.WithName("VerifySnapshotIntegrity")
|
||||
.WithDescription("Verifies the integrity of a stored snapshot by recomputing its content digest and comparing it against the stored value. Returns a verification result with expected and actual digest values.")
|
||||
.WithDescription(_t("replay.snapshot.verify_description"))
|
||||
.Produces<SnapshotVerificationResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// POST /v1/pit/snapshots/bundle - Create a snapshot bundle
|
||||
snapshotsGroup.MapPost("/bundle", CreateSnapshotBundleAsync)
|
||||
.WithName("CreateSnapshotBundle")
|
||||
.WithDescription("Creates a composite snapshot bundle from multiple providers at a given point in time, returning the bundle digest, completeness flag, and the list of any missing providers.")
|
||||
.WithDescription(_t("replay.snapshot.bundle_description"))
|
||||
.Produces<SnapshotBundleResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
@@ -97,7 +98,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_provider",
|
||||
detail: "Provider ID is required");
|
||||
detail: _t("replay.error.missing_provider"));
|
||||
}
|
||||
|
||||
if (!queryParams.PointInTime.HasValue)
|
||||
@@ -105,7 +106,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_point_in_time",
|
||||
detail: "Point-in-time timestamp is required");
|
||||
detail: _t("replay.error.missing_point_in_time"));
|
||||
}
|
||||
|
||||
var result = await resolver.ResolveAdvisoryAsync(
|
||||
@@ -142,7 +143,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_cve_id",
|
||||
detail: "CVE ID is required");
|
||||
detail: _t("replay.error.missing_cve_id"));
|
||||
}
|
||||
|
||||
if (request.ProviderIds is null || request.ProviderIds.Count == 0)
|
||||
@@ -150,7 +151,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_providers",
|
||||
detail: "At least one provider ID is required");
|
||||
detail: _t("replay.error.missing_providers"));
|
||||
}
|
||||
|
||||
var result = await resolver.ResolveCrossProviderAsync(
|
||||
@@ -238,7 +239,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_required_fields",
|
||||
detail: "CVE ID and Provider ID are required");
|
||||
detail: _t("replay.pit.cve_and_provider_required"));
|
||||
}
|
||||
|
||||
var diff = await resolver.CompareAtTimesAsync(
|
||||
@@ -275,7 +276,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_required_fields",
|
||||
detail: "Provider ID and feed data are required");
|
||||
detail: _t("replay.pit.provider_and_feed_required"));
|
||||
}
|
||||
|
||||
var result = await snapshotService.CaptureSnapshotAsync(
|
||||
@@ -294,7 +295,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "capture_failed",
|
||||
detail: result.Error ?? "Failed to capture snapshot");
|
||||
detail: result.Error ?? _t("replay.error.capture_failed"));
|
||||
}
|
||||
|
||||
return TypedResults.Created(
|
||||
@@ -368,7 +369,7 @@ public static class PointInTimeQueryEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "missing_providers",
|
||||
detail: "At least one provider ID is required");
|
||||
detail: _t("replay.error.missing_providers"));
|
||||
}
|
||||
|
||||
var bundle = await snapshotService.CreateBundleAsync(
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using StellaOps.Audit.ReplayToken;
|
||||
using StellaOps.Localization;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
@@ -114,6 +115,9 @@ builder.Services.AddAuthorization(options =>
|
||||
builder.Services.AddStellaOpsTenantServices();
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
builder.Services.AddStellaOpsLocalization(builder.Configuration);
|
||||
builder.Services.AddTranslationBundle(System.Reflection.Assembly.GetExecutingAssembly());
|
||||
|
||||
// Stella Router integration
|
||||
var routerEnabled = builder.Services.AddRouterMicroservice(
|
||||
builder.Configuration,
|
||||
@@ -144,6 +148,7 @@ app.UseExceptionHandler(exceptionApp =>
|
||||
});
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseStellaOpsLocalization();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseStellaOpsTenantMiddleware();
|
||||
@@ -425,8 +430,10 @@ app.MapGet("/.well-known/openapi", (HttpContext context) =>
|
||||
.WithName("ReplayOpenApiDocument")
|
||||
.Produces(StatusCodes.Status200OK);
|
||||
|
||||
await app.LoadTranslationsAsync();
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerEnabled);
|
||||
app.Run();
|
||||
await app.RunAsync().ConfigureAwait(false);
|
||||
|
||||
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
|
||||
{
|
||||
|
||||
@@ -22,7 +22,13 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\Telemetry\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core\StellaOps.Telemetry.Core.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Replay.Core\StellaOps.Replay.Core.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Translations\*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="StellaOpsReleaseVersion">
|
||||
<Version>1.0.0-alpha1</Version>
|
||||
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"_meta": { "locale": "en-US", "namespace": "replay", "version": "1.0" },
|
||||
|
||||
"replay.verdict.execute_description": "Executes a deterministic verdict replay from an audit bundle, re-evaluating the original policy with the stored inputs. Returns whether the replayed verdict matches the original, drift items, and an optional divergence report.",
|
||||
"replay.verdict.verify_description": "Checks whether an audit bundle is eligible for deterministic replay. Returns a confidence score, eligibility flags, and the expected outcome without executing the replay.",
|
||||
"replay.verdict.status_description": "Returns the stored replay history for a given audit manifest ID including total replay count, success/failure counts, and the timestamp of the last replay.",
|
||||
"replay.verdict.compare_description": "Compares two replay execution results and produces a structured divergence report identifying field-level differences with per-divergence severity ratings.",
|
||||
|
||||
"replay.error.bundle_read_failed": "Failed to read audit bundle",
|
||||
"replay.error.replay_not_eligible": "Replay is not eligible.",
|
||||
"replay.error.context_init_failed": "Failed to initialize replay context",
|
||||
"replay.error.missing_provider": "Provider ID is required",
|
||||
"replay.error.missing_point_in_time": "Point-in-time timestamp is required",
|
||||
"replay.error.missing_cve_id": "CVE ID is required",
|
||||
"replay.error.missing_providers": "At least one provider ID is required",
|
||||
"replay.error.missing_required_fields": "Required fields are missing",
|
||||
"replay.error.capture_failed": "Failed to capture snapshot",
|
||||
|
||||
"replay.pit.query_description": "Returns the advisory state for a specific CVE at a given point-in-time timestamp from the specified provider. Uses the nearest captured feed snapshot to reconstruct the advisory as it appeared at that moment.",
|
||||
"replay.pit.cross_provider_description": "Queries advisory state across multiple feed providers at a single point in time and returns per-provider results along with a consensus summary of severity and fix status.",
|
||||
"replay.pit.timeline_description": "Returns the change timeline for a specific CVE from a given provider within an optional time range. Each entry identifies the snapshot digest and the type of change observed.",
|
||||
"replay.pit.diff_description": "Produces a field-level diff of a CVE advisory between two distinct points in time from the same provider, identifying severity, fix-status, and metadata changes.",
|
||||
|
||||
"replay.snapshot.capture_description": "Captures and stores a feed snapshot for a specific provider, computing its content-addressable digest. Returns 201 Created with the digest and whether an existing snapshot was reused.",
|
||||
"replay.snapshot.get_description": "Returns snapshot metadata for a specific content-addressable digest including provider ID, feed type, and capture timestamp. Returns 404 if the digest is not stored.",
|
||||
"replay.snapshot.verify_description": "Verifies the integrity of a stored snapshot by recomputing its content digest and comparing it against the stored value. Returns a verification result with expected and actual digest values.",
|
||||
"replay.snapshot.bundle_description": "Creates a composite snapshot bundle from multiple providers at a given point in time, returning the bundle digest, completeness flag, and the list of any missing providers.",
|
||||
|
||||
"replay.pit.cve_and_provider_required": "CVE ID and Provider ID are required",
|
||||
"replay.pit.provider_and_feed_required": "Provider ID and feed data are required"
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.AuditPack.Models;
|
||||
using StellaOps.AuditPack.Services;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Replay.WebService;
|
||||
|
||||
@@ -30,7 +31,7 @@ public static class VerdictReplayEndpoints
|
||||
// POST /v1/replay/verdict - Execute verdict replay
|
||||
group.MapPost("/", ExecuteReplayAsync)
|
||||
.WithName("ExecuteVerdictReplay")
|
||||
.WithDescription("Executes a deterministic verdict replay from an audit bundle, re-evaluating the original policy with the stored inputs. Returns whether the replayed verdict matches the original, drift items, and an optional divergence report.")
|
||||
.WithDescription(_t("replay.verdict.execute_description"))
|
||||
.Produces<VerdictReplayResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest)
|
||||
.ProducesProblem(StatusCodes.Status404NotFound);
|
||||
@@ -38,21 +39,21 @@ public static class VerdictReplayEndpoints
|
||||
// POST /v1/replay/verdict/verify - Verify replay eligibility
|
||||
group.MapPost("/verify", VerifyEligibilityAsync)
|
||||
.WithName("VerifyReplayEligibility")
|
||||
.WithDescription("Checks whether an audit bundle is eligible for deterministic replay. Returns a confidence score, eligibility flags, and the expected outcome without executing the replay.")
|
||||
.WithDescription(_t("replay.verdict.verify_description"))
|
||||
.Produces<ReplayEligibilityResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// GET /v1/replay/verdict/{manifestId}/status - Get replay status
|
||||
group.MapGet("/{manifestId}/status", GetReplayStatusAsync)
|
||||
.WithName("GetReplayStatus")
|
||||
.WithDescription("Returns the stored replay history for a given audit manifest ID including total replay count, success/failure counts, and the timestamp of the last replay.")
|
||||
.WithDescription(_t("replay.verdict.status_description"))
|
||||
.Produces<ReplayStatusResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
// POST /v1/replay/verdict/compare - Compare two replay executions
|
||||
group.MapPost("/compare", CompareReplayResultsAsync)
|
||||
.WithName("CompareReplayResults")
|
||||
.WithDescription("Compares two replay execution results and produces a structured divergence report identifying field-level differences with per-divergence severity ratings.")
|
||||
.WithDescription(_t("replay.verdict.compare_description"))
|
||||
.Produces<ReplayComparisonResponse>(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
}
|
||||
@@ -73,7 +74,7 @@ public static class VerdictReplayEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "bundle_read_failed",
|
||||
detail: bundleResult.Error ?? "Failed to read audit bundle");
|
||||
detail: bundleResult.Error ?? _t("replay.error.bundle_read_failed"));
|
||||
}
|
||||
|
||||
// Check eligibility
|
||||
@@ -101,7 +102,7 @@ public static class VerdictReplayEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "context_init_failed",
|
||||
detail: initResult.Error ?? "Failed to initialize replay context");
|
||||
detail: initResult.Error ?? _t("replay.error.context_init_failed"));
|
||||
}
|
||||
|
||||
var execOptions = new ReplayExecutionOptions
|
||||
@@ -165,7 +166,7 @@ public static class VerdictReplayEndpoints
|
||||
return TypedResults.Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "bundle_read_failed",
|
||||
detail: bundleResult.Error ?? "Failed to read audit bundle");
|
||||
detail: bundleResult.Error ?? _t("replay.error.bundle_read_failed"));
|
||||
}
|
||||
|
||||
var eligibility = replayPredicate.Evaluate(bundleResult.Manifest, request.CurrentInputState);
|
||||
|
||||
Reference in New Issue
Block a user