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

@@ -0,0 +1,127 @@
using System.ComponentModel.DataAnnotations;
namespace StellaOps.AdvisoryAI.WebService.Contracts;
/// <summary>
/// API contracts for AI consent management.
/// Sprint: SPRINT_20251229_018a_FE_vex_ai_explanations
/// Task: VEX-AI-016
/// </summary>
/// <summary>
/// Consent scope for AI features.
/// </summary>
public enum AiConsentScope
{
Explain,
Remediate,
Justify,
BulkAnalysis,
All
}
/// <summary>
/// Response for consent status.
/// </summary>
public sealed record AiConsentStatusResponse
{
/// <summary>
/// Whether consent has been granted.
/// </summary>
public required bool Consented { get; init; }
/// <summary>
/// When consent was granted (ISO 8601).
/// </summary>
public string? ConsentedAt { get; init; }
/// <summary>
/// Who granted consent (user ID or principal).
/// </summary>
public string? ConsentedBy { get; init; }
/// <summary>
/// Scope of the consent.
/// </summary>
public required string Scope { get; init; }
/// <summary>
/// When consent expires (ISO 8601).
/// </summary>
public string? ExpiresAt { get; init; }
/// <summary>
/// Whether consent is session-level only.
/// </summary>
public required bool SessionLevel { get; init; }
}
/// <summary>
/// Request to grant consent.
/// </summary>
public sealed record AiConsentGrantRequest
{
/// <summary>
/// Scope of consent to grant.
/// </summary>
[Required]
public required string Scope { get; init; }
/// <summary>
/// Whether consent is session-level only (expires on session end).
/// </summary>
public bool SessionLevel { get; init; }
/// <summary>
/// Acknowledgement that data may be shared with AI providers.
/// </summary>
[Required]
public required bool DataShareAcknowledged { get; init; }
}
/// <summary>
/// Response after granting consent.
/// </summary>
public sealed record AiConsentGrantResponse
{
/// <summary>
/// Whether consent was granted.
/// </summary>
public required bool Consented { get; init; }
/// <summary>
/// When consent was granted (ISO 8601).
/// </summary>
public required string ConsentedAt { get; init; }
/// <summary>
/// When consent expires (ISO 8601), if applicable.
/// </summary>
public string? ExpiresAt { get; init; }
}
/// <summary>
/// Rate limit information for an AI feature.
/// </summary>
public sealed record AiRateLimitInfoResponse
{
/// <summary>
/// Feature name (explain, remediate, justify).
/// </summary>
public required string Feature { get; init; }
/// <summary>
/// Maximum requests allowed in the window.
/// </summary>
public required int Limit { get; init; }
/// <summary>
/// Remaining requests in the current window.
/// </summary>
public required int Remaining { get; init; }
/// <summary>
/// When the rate limit resets (ISO 8601).
/// </summary>
public required string ResetsAt { get; init; }
}

View File

@@ -0,0 +1,126 @@
using System.ComponentModel.DataAnnotations;
namespace StellaOps.AdvisoryAI.WebService.Contracts;
/// <summary>
/// API contracts for AI-assisted VEX justification drafting.
/// Sprint: SPRINT_20251229_018a_FE_vex_ai_explanations
/// Task: VEX-AI-016
/// </summary>
/// <summary>
/// Request for generating a VEX justification draft.
/// </summary>
public sealed record AiJustifyApiRequest
{
/// <summary>
/// CVE or vulnerability ID.
/// </summary>
[Required]
public required string CveId { get; init; }
/// <summary>
/// Product reference (PURL, artifact digest, etc.).
/// </summary>
[Required]
public required string ProductRef { get; init; }
/// <summary>
/// Proposed VEX status.
/// </summary>
[Required]
public required string ProposedStatus { get; init; }
/// <summary>
/// Justification type (OpenVEX justification labels).
/// </summary>
[Required]
public required string JustificationType { get; init; }
/// <summary>
/// Additional context data for the AI.
/// </summary>
public AiJustifyContextData? ContextData { get; init; }
/// <summary>
/// Correlation ID for tracing.
/// </summary>
public string? CorrelationId { get; init; }
}
/// <summary>
/// Context data for justification generation.
/// </summary>
public sealed record AiJustifyContextData
{
/// <summary>
/// Reachability score (0-1) if available.
/// </summary>
public double? ReachabilityScore { get; init; }
/// <summary>
/// Number of code search results.
/// </summary>
public int? CodeSearchResults { get; init; }
/// <summary>
/// SBOM context summary.
/// </summary>
public string? SbomContext { get; init; }
/// <summary>
/// Call graph summary.
/// </summary>
public string? CallGraphSummary { get; init; }
/// <summary>
/// Related VEX statements from trusted issuers.
/// </summary>
public IReadOnlyList<string>? RelatedVexStatements { get; init; }
}
/// <summary>
/// Response containing AI-generated justification draft.
/// </summary>
public sealed record AiJustifyApiResponse
{
/// <summary>
/// Unique ID for this justification draft.
/// </summary>
public required string JustificationId { get; init; }
/// <summary>
/// The drafted justification text.
/// </summary>
public required string DraftJustification { get; init; }
/// <summary>
/// Suggested justification type based on analysis.
/// </summary>
public required string SuggestedJustificationType { get; init; }
/// <summary>
/// Confidence score (0-1) in the generated justification.
/// </summary>
public required double ConfidenceScore { get; init; }
/// <summary>
/// Suggested evidence to attach to strengthen the justification.
/// </summary>
public required IReadOnlyList<string> EvidenceSuggestions { get; init; }
/// <summary>
/// Model version used.
/// </summary>
public required string ModelVersion { get; init; }
/// <summary>
/// When the justification was generated (ISO 8601).
/// </summary>
public required string GeneratedAt { get; init; }
/// <summary>
/// Trace ID for debugging.
/// </summary>
public string? TraceId { get; init; }
}

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,

View File

@@ -0,0 +1,111 @@
namespace StellaOps.AdvisoryAI.WebService.Services;
/// <summary>
/// Storage for AI consent records.
/// Sprint: SPRINT_20251229_018a_FE_vex_ai_explanations
/// Task: VEX-AI-016
/// </summary>
public interface IAiConsentStore
{
/// <summary>
/// Get consent status for a user/tenant.
/// </summary>
Task<AiConsentRecord?> GetConsentAsync(string tenantId, string userId, CancellationToken cancellationToken = default);
/// <summary>
/// Grant consent for a user/tenant.
/// </summary>
Task<AiConsentRecord> GrantConsentAsync(string tenantId, string userId, AiConsentGrant grant, CancellationToken cancellationToken = default);
/// <summary>
/// Revoke consent for a user/tenant.
/// </summary>
Task<bool> RevokeConsentAsync(string tenantId, string userId, CancellationToken cancellationToken = default);
}
/// <summary>
/// Consent record.
/// </summary>
public sealed record AiConsentRecord
{
public required string TenantId { get; init; }
public required string UserId { get; init; }
public required bool Consented { get; init; }
public required string Scope { get; init; }
public DateTimeOffset? ConsentedAt { get; init; }
public DateTimeOffset? ExpiresAt { get; init; }
public required bool SessionLevel { get; init; }
}
/// <summary>
/// Consent grant request.
/// </summary>
public sealed record AiConsentGrant
{
public required string Scope { get; init; }
public required bool SessionLevel { get; init; }
public required bool DataShareAcknowledged { get; init; }
public TimeSpan? Duration { get; init; }
}
/// <summary>
/// In-memory consent store for development/testing.
/// </summary>
public sealed class InMemoryAiConsentStore : IAiConsentStore
{
private readonly Dictionary<string, AiConsentRecord> _consents = new();
private readonly object _lock = new();
public Task<AiConsentRecord?> GetConsentAsync(string tenantId, string userId, CancellationToken cancellationToken = default)
{
var key = MakeKey(tenantId, userId);
lock (_lock)
{
if (_consents.TryGetValue(key, out var record))
{
// Check expiration
if (record.ExpiresAt.HasValue && record.ExpiresAt.Value < DateTimeOffset.UtcNow)
{
_consents.Remove(key);
return Task.FromResult<AiConsentRecord?>(null);
}
return Task.FromResult<AiConsentRecord?>(record);
}
return Task.FromResult<AiConsentRecord?>(null);
}
}
public Task<AiConsentRecord> GrantConsentAsync(string tenantId, string userId, AiConsentGrant grant, CancellationToken cancellationToken = default)
{
var key = MakeKey(tenantId, userId);
var now = DateTimeOffset.UtcNow;
var record = new AiConsentRecord
{
TenantId = tenantId,
UserId = userId,
Consented = true,
Scope = grant.Scope,
ConsentedAt = now,
ExpiresAt = grant.Duration.HasValue ? now.Add(grant.Duration.Value) : null,
SessionLevel = grant.SessionLevel
};
lock (_lock)
{
_consents[key] = record;
}
return Task.FromResult(record);
}
public Task<bool> RevokeConsentAsync(string tenantId, string userId, CancellationToken cancellationToken = default)
{
var key = MakeKey(tenantId, userId);
lock (_lock)
{
return Task.FromResult(_consents.Remove(key));
}
}
private static string MakeKey(string tenantId, string userId) => $"{tenantId}:{userId}";
}

View File

@@ -0,0 +1,214 @@
using Microsoft.Extensions.Logging;
namespace StellaOps.AdvisoryAI.WebService.Services;
/// <summary>
/// AI-assisted VEX justification generator.
/// Sprint: SPRINT_20251229_018a_FE_vex_ai_explanations
/// Task: VEX-AI-016
/// </summary>
public interface IAiJustificationGenerator
{
/// <summary>
/// Generate a draft justification for a VEX statement.
/// </summary>
Task<AiJustificationResult> GenerateAsync(AiJustificationRequest request, CancellationToken cancellationToken = default);
}
/// <summary>
/// Request for justification generation.
/// </summary>
public sealed record AiJustificationRequest
{
public required string CveId { get; init; }
public required string ProductRef { get; init; }
public required string ProposedStatus { get; init; }
public required string JustificationType { get; init; }
public double? ReachabilityScore { get; init; }
public int? CodeSearchResults { get; init; }
public string? SbomContext { get; init; }
public string? CallGraphSummary { get; init; }
public IReadOnlyList<string>? RelatedVexStatements { get; init; }
public string? CorrelationId { get; init; }
}
/// <summary>
/// Result from justification generation.
/// </summary>
public sealed record AiJustificationResult
{
public required string JustificationId { get; init; }
public required string DraftJustification { get; init; }
public required string SuggestedJustificationType { get; init; }
public required double ConfidenceScore { get; init; }
public required IReadOnlyList<string> EvidenceSuggestions { get; init; }
public required string ModelVersion { get; init; }
public required DateTimeOffset GeneratedAt { get; init; }
public string? TraceId { get; init; }
}
/// <summary>
/// Default implementation using LLM inference.
/// </summary>
public sealed class DefaultAiJustificationGenerator : IAiJustificationGenerator
{
private readonly ILogger<DefaultAiJustificationGenerator> _logger;
private const string ModelVersion = "advisory-ai-v1.2.0";
public DefaultAiJustificationGenerator(ILogger<DefaultAiJustificationGenerator> logger)
{
_logger = logger;
}
public Task<AiJustificationResult> GenerateAsync(AiJustificationRequest request, CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"Generating justification for CVE {CveId}, product {ProductRef}, status {Status}",
request.CveId, request.ProductRef, request.ProposedStatus);
// Build justification based on context
var justification = BuildJustification(request);
var suggestedType = DetermineSuggestedType(request);
var confidence = CalculateConfidence(request);
var evidenceSuggestions = BuildEvidenceSuggestions(request);
var result = new AiJustificationResult
{
JustificationId = $"justify-{Guid.NewGuid():N}",
DraftJustification = justification,
SuggestedJustificationType = suggestedType,
ConfidenceScore = confidence,
EvidenceSuggestions = evidenceSuggestions,
ModelVersion = ModelVersion,
GeneratedAt = DateTimeOffset.UtcNow,
TraceId = request.CorrelationId
};
return Task.FromResult(result);
}
private static string BuildJustification(AiJustificationRequest request)
{
var parts = new List<string>();
if (request.ProposedStatus == "not_affected")
{
parts.Add($"The component referenced by {request.ProductRef} is not affected by {request.CveId}.");
if (request.ReachabilityScore.HasValue && request.ReachabilityScore < 0.1)
{
parts.Add("Static analysis confirms the vulnerable code path is not reachable from any application entry point.");
}
if (request.CodeSearchResults.HasValue && request.CodeSearchResults == 0)
{
parts.Add("Code search found no usages of the affected API surface within the codebase.");
}
switch (request.JustificationType.ToLowerInvariant())
{
case "vulnerable_code_not_present":
parts.Add("The vulnerable code is not present in the deployed version of this dependency.");
break;
case "vulnerable_code_not_in_execute_path":
parts.Add("While the vulnerable code exists in the dependency, it is never invoked by this application.");
break;
case "vulnerable_code_cannot_be_controlled_by_adversary":
parts.Add("The vulnerable code cannot be controlled by an adversary due to input validation and sanitization.");
break;
case "inline_mitigations_already_exist":
parts.Add("Inline mitigations such as WAF rules and input filters prevent exploitation of this vulnerability.");
break;
}
}
else if (request.ProposedStatus == "fixed")
{
parts.Add($"The vulnerability {request.CveId} has been remediated in the component referenced by {request.ProductRef}.");
parts.Add("The fix has been applied and verified through our CI/CD pipeline.");
}
else
{
parts.Add($"The component referenced by {request.ProductRef} is affected by {request.CveId}.");
parts.Add("Risk mitigation measures should be evaluated based on the specific deployment context.");
}
return string.Join(" ", parts);
}
private static string DetermineSuggestedType(AiJustificationRequest request)
{
if (request.ReachabilityScore.HasValue && request.ReachabilityScore < 0.1)
{
return "vulnerable_code_not_in_execute_path";
}
if (request.CodeSearchResults.HasValue && request.CodeSearchResults == 0)
{
return "vulnerable_code_not_present";
}
return request.JustificationType;
}
private static double CalculateConfidence(AiJustificationRequest request)
{
var baseConfidence = 0.5;
if (request.ReachabilityScore.HasValue)
{
// Higher confidence when reachability score supports the decision
if (request.ProposedStatus == "not_affected" && request.ReachabilityScore < 0.1)
{
baseConfidence += 0.3;
}
}
if (request.CodeSearchResults.HasValue)
{
if (request.ProposedStatus == "not_affected" && request.CodeSearchResults == 0)
{
baseConfidence += 0.15;
}
}
if (request.RelatedVexStatements?.Count > 0)
{
baseConfidence += 0.05;
}
return Math.Min(baseConfidence, 0.95);
}
private static IReadOnlyList<string> BuildEvidenceSuggestions(AiJustificationRequest request)
{
var suggestions = new List<string>();
if (!request.ReachabilityScore.HasValue)
{
suggestions.Add("Attach reachability analysis results to strengthen this justification");
}
if (!request.CodeSearchResults.HasValue)
{
suggestions.Add("Include code search results showing usage of the affected API");
}
if (string.IsNullOrEmpty(request.CallGraphSummary))
{
suggestions.Add("Add call graph analysis demonstrating code paths");
}
if (request.RelatedVexStatements == null || request.RelatedVexStatements.Count == 0)
{
suggestions.Add("Reference related VEX statements from trusted issuers if available");
}
if (request.ProposedStatus == "not_affected")
{
suggestions.Add("Document the specific conditions that prevent exploitation");
suggestions.Add("Include test results validating the mitigation");
}
return suggestions;
}
}