search and ai stabilization work, localization stablized.

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

View File

@@ -10,6 +10,7 @@ using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Core.Repositories;
using StellaOps.Unknowns.WebService.Security;
using StellaOps.Auth.ServerIntegration.Tenancy;
using static StellaOps.Localization.T;
namespace StellaOps.Unknowns.WebService.Endpoints;
@@ -32,112 +33,112 @@ public static class GreyQueueEndpoints
group.MapGet("/", ListEntries)
.WithName("ListGreyQueueEntries")
.WithSummary("List grey queue entries with pagination")
.WithDescription("Returns paginated list of grey queue entries. Supports filtering by status and reason.");
.WithDescription(_t("unknowns.grey_queue.list_description"));
group.MapGet("/{id:guid}", GetEntryById)
.WithName("GetGreyQueueEntry")
.WithSummary("Get grey queue entry by ID")
.WithDescription("Returns a single grey queue entry with full evidence bundle.");
.WithDescription(_t("unknowns.grey_queue.get_by_id_description"));
group.MapGet("/by-unknown/{unknownId:guid}", GetByUnknownId)
.WithName("GetGreyQueueByUnknownId")
.WithSummary("Get grey queue entry by unknown ID")
.WithDescription("Returns the grey queue entry for a specific unknown.");
.WithDescription(_t("unknowns.grey_queue.get_by_unknown_description"));
group.MapGet("/ready", GetReadyForProcessing)
.WithName("GetReadyForProcessing")
.WithSummary("Get entries ready for processing")
.WithDescription("Returns entries that are ready to be processed (pending, not exhausted, past next processing time).");
.WithDescription(_t("unknowns.grey_queue.ready_description"));
// Triggers
group.MapGet("/triggers/feed/{feedId}", GetByFeedTrigger)
.WithName("GetByFeedTrigger")
.WithSummary("Get entries triggered by feed update")
.WithDescription("Returns entries that should be reprocessed due to a feed update.");
.WithDescription(_t("unknowns.grey_queue.get_by_feed_description"));
group.MapGet("/triggers/tool/{toolId}", GetByToolTrigger)
.WithName("GetByToolTrigger")
.WithSummary("Get entries triggered by tool update")
.WithDescription("Returns entries that should be reprocessed due to a tool update.");
.WithDescription(_t("unknowns.grey_queue.get_by_tool_description"));
group.MapGet("/triggers/cve/{cveId}", GetByCveTrigger)
.WithName("GetByCveTrigger")
.WithSummary("Get entries triggered by CVE update")
.WithDescription("Returns entries that should be reprocessed due to a CVE update.");
.WithDescription(_t("unknowns.grey_queue.get_by_cve_description"));
// Actions (require write scope)
group.MapPost("/", EnqueueEntry)
.WithName("EnqueueGreyQueueEntry")
.WithSummary("Enqueue a new grey queue entry")
.WithDescription("Creates a new grey queue entry with evidence bundle and trigger conditions.")
.WithDescription(_t("unknowns.grey_queue.enqueue_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/process", StartProcessing)
.WithName("StartGreyQueueProcessing")
.WithSummary("Mark entry as processing")
.WithDescription("Marks an entry as currently being processed.")
.WithDescription(_t("unknowns.grey_queue.process_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/result", RecordResult)
.WithName("RecordGreyQueueResult")
.WithSummary("Record processing result")
.WithDescription("Records the result of a processing attempt.")
.WithDescription(_t("unknowns.grey_queue.record_result_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/resolve", ResolveEntry)
.WithName("ResolveGreyQueueEntry")
.WithSummary("Resolve a grey queue entry")
.WithDescription("Marks an entry as resolved with resolution type and reference.")
.WithDescription(_t("unknowns.grey_queue.resolve_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/dismiss", DismissEntry)
.WithName("DismissGreyQueueEntry")
.WithSummary("Dismiss a grey queue entry")
.WithDescription("Manually dismisses an entry from the queue.")
.WithDescription(_t("unknowns.grey_queue.dismiss_description"))
.RequireAuthorization(UnknownsPolicies.Write);
// Maintenance (require write scope)
group.MapPost("/expire", ExpireOldEntries)
.WithName("ExpireGreyQueueEntries")
.WithSummary("Expire old entries")
.WithDescription("Expires entries that have exceeded their TTL.")
.WithDescription(_t("unknowns.grey_queue.expire_description"))
.RequireAuthorization(UnknownsPolicies.Write);
// Statistics
group.MapGet("/summary", GetSummary)
.WithName("GetGreyQueueSummary")
.WithSummary("Get grey queue summary statistics")
.WithDescription("Returns summary counts by status, reason, and performance metrics.");
.WithDescription(_t("unknowns.grey_queue.summary_description"));
// Sprint: SPRINT_20260118_018 (UQ-005) - New state transitions (require write scope)
group.MapPost("/{id:guid}/assign", AssignForReview)
.WithName("AssignGreyQueueEntry")
.WithSummary("Assign entry for review")
.WithDescription("Assigns an entry to a reviewer, transitioning to UnderReview state.")
.WithDescription(_t("unknowns.grey_queue.assign_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/escalate", EscalateEntry)
.WithName("EscalateGreyQueueEntry")
.WithSummary("Escalate entry to security team")
.WithDescription("Escalates an entry to the security team, transitioning to Escalated state.")
.WithDescription(_t("unknowns.grey_queue.escalate_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/reject", RejectEntry)
.WithName("RejectGreyQueueEntry")
.WithSummary("Reject a grey queue entry")
.WithDescription("Marks an entry as rejected (invalid or not actionable).")
.WithDescription(_t("unknowns.grey_queue.reject_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapPost("/{id:guid}/reopen", ReopenEntry)
.WithName("ReopenGreyQueueEntry")
.WithSummary("Reopen a closed entry")
.WithDescription("Reopens a rejected, failed, or dismissed entry back to pending.")
.WithDescription(_t("unknowns.grey_queue.reopen_description"))
.RequireAuthorization(UnknownsPolicies.Write);
group.MapGet("/{id:guid}/transitions", GetValidTransitions)
.WithName("GetValidTransitions")
.WithSummary("Get valid state transitions")
.WithDescription("Returns the valid next states for an entry based on current state.");
.WithDescription(_t("unknowns.grey_queue.transitions_description"));
return routes;
}
@@ -147,8 +148,8 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] AssignForReviewRequest request,
IGreyQueueRepository repository = null!,
INotificationPublisher? notificationPublisher = null,
[FromServices] IGreyQueueRepository repository = null!,
[FromServices] INotificationPublisher? notificationPublisher = null,
CancellationToken ct = default)
{
var entry = await repository.GetByIdAsync(tenantId, id, ct);
@@ -177,8 +178,8 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] EscalateRequest request,
IGreyQueueRepository repository = null!,
INotificationPublisher? notificationPublisher = null,
[FromServices] IGreyQueueRepository repository = null!,
[FromServices] INotificationPublisher? notificationPublisher = null,
CancellationToken ct = default)
{
var entry = await repository.GetByIdAsync(tenantId, id, ct);
@@ -219,7 +220,7 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] RejectRequest request,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entry = await repository.GetByIdAsync(tenantId, id, ct);
@@ -248,7 +249,7 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] ReopenRequest request,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entry = await repository.GetByIdAsync(tenantId, id, ct);
@@ -275,7 +276,7 @@ public static class GreyQueueEndpoints
private static async Task<Results<Ok<ValidTransitionsResponse>, NotFound>> GetValidTransitions(
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entry = await repository.GetByIdAsync(tenantId, id, ct);
@@ -302,7 +303,7 @@ public static class GreyQueueEndpoints
[FromQuery] int take = 50,
[FromQuery] GreyQueueStatus? status = null,
[FromQuery] GreyQueueReason? reason = null,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
IReadOnlyList<GreyQueueEntry> entries;
@@ -337,7 +338,7 @@ public static class GreyQueueEndpoints
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound>> GetEntryById(
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entry = await repository.GetByIdAsync(tenantId, id, ct);
@@ -352,7 +353,7 @@ public static class GreyQueueEndpoints
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound>> GetByUnknownId(
Guid unknownId,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entry = await repository.GetByUnknownIdAsync(tenantId, unknownId, ct);
@@ -367,7 +368,7 @@ public static class GreyQueueEndpoints
private static async Task<Ok<GreyQueueListResponse>> GetReadyForProcessing(
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromQuery] int limit = 50,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entries = await repository.GetReadyForProcessingAsync(tenantId, limit, ct);
@@ -389,7 +390,7 @@ public static class GreyQueueEndpoints
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromQuery] string? version = null,
[FromQuery] int limit = 50,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entries = await repository.GetByFeedTriggerAsync(tenantId, feedId, version, limit, ct);
@@ -411,7 +412,7 @@ public static class GreyQueueEndpoints
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromQuery] string? version = null,
[FromQuery] int limit = 50,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entries = await repository.GetByToolTriggerAsync(tenantId, toolId, version, limit, ct);
@@ -432,7 +433,7 @@ public static class GreyQueueEndpoints
string cveId,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromQuery] int limit = 50,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var entries = await repository.GetByCveTriggerAsync(tenantId, cveId, limit, ct);
@@ -452,7 +453,7 @@ public static class GreyQueueEndpoints
private static async Task<Created<GreyQueueEntryDto>> EnqueueEntry(
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] EnqueueGreyQueueRequest request,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var evidence = request.Evidence is not null ? new GreyQueueEvidenceBundle
@@ -491,7 +492,7 @@ public static class GreyQueueEndpoints
private static async Task<Results<Ok<GreyQueueEntryDto>, NotFound>> StartProcessing(
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
try
@@ -510,7 +511,7 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] RecordResultRequest request,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
try
@@ -535,7 +536,7 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] ResolveEntryRequest request,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
try
@@ -559,7 +560,7 @@ public static class GreyQueueEndpoints
Guid id,
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
[FromBody] DismissEntryRequest request,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
try
@@ -581,7 +582,7 @@ public static class GreyQueueEndpoints
// Expire old entries
private static async Task<Ok<ExpireResultResponse>> ExpireOldEntries(
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var count = await repository.ExpireOldEntriesAsync(tenantId, ct);
@@ -591,7 +592,7 @@ public static class GreyQueueEndpoints
// Get summary
private static async Task<Ok<GreyQueueSummaryDto>> GetSummary(
[FromHeader(Name = "X-Tenant-Id")] string tenantId,
IGreyQueueRepository repository = null!,
[FromServices] IGreyQueueRepository repository = null!,
CancellationToken ct = default)
{
var summary = await repository.GetSummaryAsync(tenantId, ct);

View File

@@ -11,6 +11,7 @@ using StellaOps.Unknowns.Core.Models;
using StellaOps.Unknowns.Core.Repositories;
using StellaOps.Unknowns.WebService.Security;
using StellaOps.Auth.ServerIntegration.Tenancy;
using static StellaOps.Localization.T;
namespace StellaOps.Unknowns.WebService.Endpoints;
@@ -33,45 +34,45 @@ public static class UnknownsEndpoints
group.MapGet("/", ListUnknowns)
.WithName("ListUnknowns")
.WithSummary("List unknowns with pagination")
.WithDescription("Returns paginated list of open unknowns. Supports bitemporal query with asOf parameter.");
.WithDescription(_t("unknowns.unknown.list_description"));
// WS-005: GET /api/unknowns/{id} - Single with hints
group.MapGet("/{id:guid}", GetUnknownById)
.WithName("GetUnknownById")
.WithSummary("Get unknown by ID")
.WithDescription("Returns a single unknown with full provenance hints.");
.WithDescription(_t("unknowns.unknown.get_by_id_description"));
// WS-006: GET /api/unknowns/{id}/hints - Hints only
group.MapGet("/{id:guid}/hints", GetUnknownHints)
.WithName("GetUnknownHints")
.WithSummary("Get provenance hints for unknown")
.WithDescription("Returns only the provenance hints for an unknown.");
.WithDescription(_t("unknowns.unknown.get_hints_description"));
// Additional endpoints
group.MapGet("/{id:guid}/history", GetUnknownHistory)
.WithName("GetUnknownHistory")
.WithSummary("Get bitemporal history for unknown")
.WithDescription("Returns the bitemporal history of state changes for an unknown.");
.WithDescription(_t("unknowns.unknown.get_history_description"));
group.MapGet("/triage/{band}", GetByTriageBand)
.WithName("GetUnknownsByTriageBand")
.WithSummary("Get unknowns by triage band")
.WithDescription("Returns unknowns filtered by triage band (hot, warm, cold).");
.WithDescription(_t("unknowns.unknown.get_by_triage_band_description"));
group.MapGet("/hot-queue", GetHotQueue)
.WithName("GetHotQueue")
.WithSummary("Get HOT unknowns for immediate processing")
.WithDescription("Returns HOT unknowns ordered by composite score descending.");
.WithDescription(_t("unknowns.unknown.get_hot_queue_description"));
group.MapGet("/high-confidence", GetHighConfidenceHints)
.WithName("GetHighConfidenceHints")
.WithSummary("Get unknowns with high-confidence hints")
.WithDescription("Returns unknowns with provenance hints above confidence threshold.");
.WithDescription(_t("unknowns.unknown.get_high_confidence_description"));
group.MapGet("/summary", GetSummary)
.WithName("GetUnknownsSummary")
.WithSummary("Get unknowns summary statistics")
.WithDescription("Returns summary counts by kind, severity, and triage band.");
.WithDescription(_t("unknowns.unknown.summary_description"));
return routes;
}

View File

@@ -8,6 +8,7 @@
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.ServerIntegration;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Localization;
using StellaOps.Router.AspNet;
using StellaOps.Unknowns.WebService;
using StellaOps.Unknowns.WebService.Endpoints;
@@ -36,6 +37,8 @@ builder.Services.AddAuthorization(options =>
});
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(
@@ -55,6 +58,7 @@ if (app.Environment.IsDevelopment())
}
app.UseStellaOpsCors();
app.UseStellaOpsLocalization();
app.UseAuthentication();
app.UseAuthorization();
app.UseStellaOpsTenantMiddleware();
@@ -66,6 +70,7 @@ app.MapGreyQueueEndpoints();
app.MapHealthChecks("/health");
app.TryRefreshStellaRouterEndpoints(routerEnabled);
await app.LoadTranslationsAsync();
app.Run();
// Make Program class accessible for integration tests

View File

@@ -13,6 +13,10 @@
<ProjectReference Include="..\__Libraries\StellaOps.Unknowns.Persistence\StellaOps.Unknowns.Persistence.csproj" />
<ProjectReference Include="..\__Libraries\StellaOps.Unknowns.Persistence.EfCore\StellaOps.Unknowns.Persistence.EfCore.csproj" />
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
<ProjectReference Include="..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Translations\*.json" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,32 @@
{
"_meta": { "locale": "en-US", "namespace": "unknowns", "version": "1.0" },
"unknowns.grey_queue.assign_description": "Assigns an entry to a reviewer, transitioning to UnderReview state.",
"unknowns.grey_queue.dismiss_description": "Manually dismisses an entry from the queue.",
"unknowns.grey_queue.enqueue_description": "Creates a new grey queue entry with evidence bundle and trigger conditions.",
"unknowns.grey_queue.escalate_description": "Escalates an entry to the security team, transitioning to Escalated state.",
"unknowns.grey_queue.expire_description": "Expires entries that have exceeded their TTL.",
"unknowns.grey_queue.get_by_cve_description": "Returns entries that should be reprocessed due to a CVE update.",
"unknowns.grey_queue.get_by_feed_description": "Returns entries that should be reprocessed due to a feed update.",
"unknowns.grey_queue.get_by_id_description": "Returns a single grey queue entry with full evidence bundle.",
"unknowns.grey_queue.get_by_tool_description": "Returns entries that should be reprocessed due to a tool update.",
"unknowns.grey_queue.get_by_unknown_description": "Returns the grey queue entry for a specific unknown.",
"unknowns.grey_queue.list_description": "Returns paginated list of grey queue entries. Supports filtering by status and reason.",
"unknowns.grey_queue.process_description": "Marks an entry as currently being processed.",
"unknowns.grey_queue.ready_description": "Returns entries that are ready to be processed (pending, not exhausted, past next processing time).",
"unknowns.grey_queue.record_result_description": "Records the result of a processing attempt.",
"unknowns.grey_queue.reject_description": "Marks an entry as rejected (invalid or not actionable).",
"unknowns.grey_queue.reopen_description": "Reopens a rejected, failed, or dismissed entry back to pending.",
"unknowns.grey_queue.resolve_description": "Marks an entry as resolved with resolution type and reference.",
"unknowns.grey_queue.summary_description": "Returns summary counts by status, reason, and performance metrics.",
"unknowns.grey_queue.transitions_description": "Returns the valid next states for an entry based on current state.",
"unknowns.unknown.get_by_id_description": "Returns a single unknown with full provenance hints.",
"unknowns.unknown.get_high_confidence_description": "Returns unknowns with provenance hints above confidence threshold.",
"unknowns.unknown.get_history_description": "Returns the bitemporal history of state changes for an unknown.",
"unknowns.unknown.get_hints_description": "Returns only the provenance hints for an unknown.",
"unknowns.unknown.get_hot_queue_description": "Returns HOT unknowns ordered by composite score descending.",
"unknowns.unknown.get_by_triage_band_description": "Returns unknowns filtered by triage band (hot, warm, cold).",
"unknowns.unknown.list_description": "Returns paginated list of open unknowns. Supports bitemporal query with asOf parameter.",
"unknowns.unknown.summary_description": "Returns summary counts by kind, severity, and triage band."
}