Frontend gaps fill work. Testing fixes work. Auditing in progress.

This commit is contained in:
StellaOps Bot
2025-12-30 01:22:58 +02:00
parent 1dc4bcbf10
commit 7a5210e2aa
928 changed files with 183942 additions and 3941 deletions

View File

@@ -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,