Frontend gaps fill work. Testing fixes work. Auditing in progress.
This commit is contained in:
@@ -20,6 +20,7 @@ using StellaOps.AdvisoryAI.Queue;
|
||||
using StellaOps.AdvisoryAI.PolicyStudio;
|
||||
using StellaOps.AdvisoryAI.Remediation;
|
||||
using StellaOps.AdvisoryAI.WebService.Contracts;
|
||||
using StellaOps.AdvisoryAI.WebService.Services;
|
||||
using StellaOps.Router.AspNet;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -30,6 +31,11 @@ builder.Configuration
|
||||
.AddEnvironmentVariables(prefix: "ADVISORYAI__");
|
||||
|
||||
builder.Services.AddAdvisoryAiCore(builder.Configuration);
|
||||
|
||||
// VEX-AI-016: Consent and justification services
|
||||
builder.Services.AddSingleton<IAiConsentStore, InMemoryAiConsentStore>();
|
||||
builder.Services.AddSingleton<IAiJustificationGenerator, DefaultAiJustificationGenerator>();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddProblemDetails();
|
||||
@@ -121,6 +127,28 @@ app.MapPost("/v1/advisory-ai/policy/studio/validate", HandlePolicyValidate)
|
||||
app.MapPost("/v1/advisory-ai/policy/studio/compile", HandlePolicyCompile)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// VEX-AI-016: Consent endpoints
|
||||
app.MapGet("/v1/advisory-ai/consent", HandleGetConsent)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.MapPost("/v1/advisory-ai/consent", HandleGrantConsent)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
app.MapDelete("/v1/advisory-ai/consent", HandleRevokeConsent)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// VEX-AI-016: Justification endpoint
|
||||
app.MapPost("/v1/advisory-ai/justify", HandleJustify)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// VEX-AI-016: Remediate alias (maps to remediation/plan)
|
||||
app.MapPost("/v1/advisory-ai/remediate", HandleRemediate)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// VEX-AI-016: Rate limits endpoint
|
||||
app.MapGet("/v1/advisory-ai/rate-limits", HandleGetRateLimits)
|
||||
.RequireRateLimiting("advisory-ai");
|
||||
|
||||
// Refresh Router endpoint cache
|
||||
app.TryRefreshStellaRouterEndpoints(routerOptions);
|
||||
|
||||
@@ -649,6 +677,235 @@ static Task<IResult> HandlePolicyCompile(
|
||||
return Task.FromResult(Results.Ok(response));
|
||||
}
|
||||
|
||||
// VEX-AI-016: Consent handler functions
|
||||
static string GetTenantId(HttpContext context)
|
||||
{
|
||||
return context.Request.Headers.TryGetValue("X-StellaOps-Tenant", out var value)
|
||||
? value.ToString()
|
||||
: "default";
|
||||
}
|
||||
|
||||
static string GetUserId(HttpContext context)
|
||||
{
|
||||
return context.Request.Headers.TryGetValue("X-StellaOps-User", out var value)
|
||||
? value.ToString()
|
||||
: "anonymous";
|
||||
}
|
||||
|
||||
static async Task<IResult> HandleGetConsent(
|
||||
HttpContext httpContext,
|
||||
IAiConsentStore consentStore,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = GetTenantId(httpContext);
|
||||
var userId = GetUserId(httpContext);
|
||||
|
||||
var record = await consentStore.GetConsentAsync(tenantId, userId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (record is null)
|
||||
{
|
||||
return Results.Ok(new AiConsentStatusResponse
|
||||
{
|
||||
Consented = false,
|
||||
Scope = "all",
|
||||
SessionLevel = false
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new AiConsentStatusResponse
|
||||
{
|
||||
Consented = record.Consented,
|
||||
ConsentedAt = record.ConsentedAt?.ToString("O"),
|
||||
ConsentedBy = record.UserId,
|
||||
Scope = record.Scope,
|
||||
ExpiresAt = record.ExpiresAt?.ToString("O"),
|
||||
SessionLevel = record.SessionLevel
|
||||
});
|
||||
}
|
||||
|
||||
static async Task<IResult> HandleGrantConsent(
|
||||
HttpContext httpContext,
|
||||
AiConsentGrantRequest request,
|
||||
IAiConsentStore consentStore,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!request.DataShareAcknowledged)
|
||||
{
|
||||
return Results.BadRequest(new { error = "Data sharing acknowledgement is required" });
|
||||
}
|
||||
|
||||
var tenantId = GetTenantId(httpContext);
|
||||
var userId = GetUserId(httpContext);
|
||||
|
||||
var grant = new AiConsentGrant
|
||||
{
|
||||
Scope = request.Scope,
|
||||
SessionLevel = request.SessionLevel,
|
||||
DataShareAcknowledged = request.DataShareAcknowledged,
|
||||
Duration = request.SessionLevel ? TimeSpan.FromHours(24) : null
|
||||
};
|
||||
|
||||
var record = await consentStore.GrantConsentAsync(tenantId, userId, grant, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new AiConsentGrantResponse
|
||||
{
|
||||
Consented = record.Consented,
|
||||
ConsentedAt = record.ConsentedAt?.ToString("O") ?? DateTimeOffset.UtcNow.ToString("O"),
|
||||
ExpiresAt = record.ExpiresAt?.ToString("O")
|
||||
});
|
||||
}
|
||||
|
||||
static async Task<IResult> HandleRevokeConsent(
|
||||
HttpContext httpContext,
|
||||
IAiConsentStore consentStore,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = GetTenantId(httpContext);
|
||||
var userId = GetUserId(httpContext);
|
||||
|
||||
await consentStore.RevokeConsentAsync(tenantId, userId, cancellationToken).ConfigureAwait(false);
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
// VEX-AI-016: Justification handler
|
||||
static bool EnsureJustifyAuthorized(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:justify");
|
||||
}
|
||||
|
||||
static async Task<IResult> HandleJustify(
|
||||
HttpContext httpContext,
|
||||
AiJustifyApiRequest request,
|
||||
IAiJustificationGenerator justificationGenerator,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.justify", System.Diagnostics.ActivityKind.Server);
|
||||
activity?.SetTag("advisory.cve_id", request.CveId);
|
||||
activity?.SetTag("advisory.product_ref", request.ProductRef);
|
||||
activity?.SetTag("advisory.proposed_status", request.ProposedStatus);
|
||||
|
||||
if (!EnsureJustifyAuthorized(httpContext))
|
||||
{
|
||||
return Results.StatusCode(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var domainRequest = new AiJustificationRequest
|
||||
{
|
||||
CveId = request.CveId,
|
||||
ProductRef = request.ProductRef,
|
||||
ProposedStatus = request.ProposedStatus,
|
||||
JustificationType = request.JustificationType,
|
||||
ReachabilityScore = request.ContextData?.ReachabilityScore,
|
||||
CodeSearchResults = request.ContextData?.CodeSearchResults,
|
||||
SbomContext = request.ContextData?.SbomContext,
|
||||
CallGraphSummary = request.ContextData?.CallGraphSummary,
|
||||
RelatedVexStatements = request.ContextData?.RelatedVexStatements,
|
||||
CorrelationId = request.CorrelationId
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await justificationGenerator.GenerateAsync(domainRequest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
activity?.SetTag("advisory.justification_id", result.JustificationId);
|
||||
activity?.SetTag("advisory.confidence", result.ConfidenceScore);
|
||||
|
||||
return Results.Ok(new AiJustifyApiResponse
|
||||
{
|
||||
JustificationId = result.JustificationId,
|
||||
DraftJustification = result.DraftJustification,
|
||||
SuggestedJustificationType = result.SuggestedJustificationType,
|
||||
ConfidenceScore = result.ConfidenceScore,
|
||||
EvidenceSuggestions = result.EvidenceSuggestions,
|
||||
ModelVersion = result.ModelVersion,
|
||||
GeneratedAt = result.GeneratedAt.ToString("O"),
|
||||
TraceId = result.TraceId
|
||||
});
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// VEX-AI-016: Remediate alias (delegates to remediation/plan)
|
||||
static async Task<IResult> HandleRemediate(
|
||||
HttpContext httpContext,
|
||||
RemediationPlanApiRequest request,
|
||||
IRemediationPlanner remediationPlanner,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.remediate", System.Diagnostics.ActivityKind.Server);
|
||||
activity?.SetTag("advisory.finding_id", request.FindingId);
|
||||
activity?.SetTag("advisory.vulnerability_id", request.VulnerabilityId);
|
||||
|
||||
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());
|
||||
|
||||
return Results.Ok(RemediationPlanApiResponse.FromDomain(plan));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return Results.BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
// VEX-AI-016: Rate limits handler
|
||||
static Task<IResult> HandleGetRateLimits(
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Return current rate limit info for each feature
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var resetTime = now.AddMinutes(1);
|
||||
|
||||
var limits = new List<AiRateLimitInfoResponse>
|
||||
{
|
||||
new AiRateLimitInfoResponse
|
||||
{
|
||||
Feature = "explain",
|
||||
Limit = 10,
|
||||
Remaining = 10,
|
||||
ResetsAt = resetTime.ToString("O")
|
||||
},
|
||||
new AiRateLimitInfoResponse
|
||||
{
|
||||
Feature = "remediate",
|
||||
Limit = 5,
|
||||
Remaining = 5,
|
||||
ResetsAt = resetTime.ToString("O")
|
||||
},
|
||||
new AiRateLimitInfoResponse
|
||||
{
|
||||
Feature = "justify",
|
||||
Limit = 3,
|
||||
Remaining = 3,
|
||||
ResetsAt = resetTime.ToString("O")
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(Results.Ok(limits));
|
||||
}
|
||||
|
||||
internal sealed record PipelinePlanRequest(
|
||||
AdvisoryTaskType? TaskType,
|
||||
string AdvisoryKey,
|
||||
|
||||
Reference in New Issue
Block a user