Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism
- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency. - Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling. - Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies. - Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification. - Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
@@ -0,0 +1,844 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// VexCliCommandModule.cs
|
||||
// Sprint: SPRINT_20251226_011_BE_auto_vex_downgrade
|
||||
// Task: AUTOVEX-15 — CLI command: stella vex auto-downgrade --check <image>
|
||||
// Description: CLI plugin module for VEX management commands including auto-downgrade.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.CommandLine;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Spectre.Console;
|
||||
using StellaOps.Cli.Configuration;
|
||||
using StellaOps.Cli.Plugins;
|
||||
|
||||
namespace StellaOps.Cli.Plugins.Vex;
|
||||
|
||||
/// <summary>
|
||||
/// CLI plugin module for VEX management commands.
|
||||
/// Provides 'stella vex auto-downgrade', 'stella vex check', 'stella vex list' commands.
|
||||
/// </summary>
|
||||
public sealed class VexCliCommandModule : ICliCommandModule
|
||||
{
|
||||
public string Name => "stellaops.cli.plugins.vex";
|
||||
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
public void RegisterCommands(
|
||||
RootCommand root,
|
||||
IServiceProvider services,
|
||||
StellaOpsCliOptions options,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(root);
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(verboseOption);
|
||||
|
||||
root.Add(BuildVexCommand(services, verboseOption, options, cancellationToken));
|
||||
}
|
||||
|
||||
private static Command BuildVexCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
StellaOpsCliOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var vex = new Command("vex", "VEX management and auto-downgrade commands.");
|
||||
|
||||
// Add subcommands
|
||||
vex.Add(BuildAutoDowngradeCommand(services, verboseOption, options, cancellationToken));
|
||||
vex.Add(BuildCheckCommand(services, verboseOption, cancellationToken));
|
||||
vex.Add(BuildListCommand(services, verboseOption, cancellationToken));
|
||||
vex.Add(BuildNotReachableCommand(services, verboseOption, options, cancellationToken));
|
||||
|
||||
return vex;
|
||||
}
|
||||
|
||||
private static Command BuildAutoDowngradeCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
StellaOpsCliOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cmd = new Command("auto-downgrade", "Auto-downgrade VEX based on runtime observations.");
|
||||
|
||||
var imageOption = new Option<string>("--image")
|
||||
{
|
||||
Description = "Container image digest or reference to check",
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
var checkOption = new Option<string>("--check")
|
||||
{
|
||||
Description = "Image to check for hot vulnerable symbols",
|
||||
IsRequired = false
|
||||
};
|
||||
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
Description = "Dry run mode - show what would be downgraded without making changes"
|
||||
};
|
||||
|
||||
var minObservationsOption = new Option<int>("--min-observations")
|
||||
{
|
||||
Description = "Minimum observation count threshold",
|
||||
};
|
||||
minObservationsOption.SetDefaultValue(10);
|
||||
|
||||
var minCpuOption = new Option<double>("--min-cpu")
|
||||
{
|
||||
Description = "Minimum CPU percentage threshold",
|
||||
};
|
||||
minCpuOption.SetDefaultValue(1.0);
|
||||
|
||||
var minConfidenceOption = new Option<double>("--min-confidence")
|
||||
{
|
||||
Description = "Minimum confidence threshold (0.0-1.0)",
|
||||
};
|
||||
minConfidenceOption.SetDefaultValue(0.7);
|
||||
|
||||
var outputOption = new Option<string?>("--output")
|
||||
{
|
||||
Description = "Output file path for results (default: stdout)"
|
||||
};
|
||||
|
||||
var formatOption = new Option<OutputFormat>("--format")
|
||||
{
|
||||
Description = "Output format"
|
||||
};
|
||||
formatOption.SetDefaultValue(OutputFormat.Table);
|
||||
|
||||
cmd.AddOption(imageOption);
|
||||
cmd.AddOption(checkOption);
|
||||
cmd.AddOption(dryRunOption);
|
||||
cmd.AddOption(minObservationsOption);
|
||||
cmd.AddOption(minCpuOption);
|
||||
cmd.AddOption(minConfidenceOption);
|
||||
cmd.AddOption(outputOption);
|
||||
cmd.AddOption(formatOption);
|
||||
cmd.AddOption(verboseOption);
|
||||
|
||||
cmd.SetHandler(async (context) =>
|
||||
{
|
||||
var image = context.ParseResult.GetValueForOption(imageOption);
|
||||
var check = context.ParseResult.GetValueForOption(checkOption);
|
||||
var dryRun = context.ParseResult.GetValueForOption(dryRunOption);
|
||||
var minObs = context.ParseResult.GetValueForOption(minObservationsOption);
|
||||
var minCpu = context.ParseResult.GetValueForOption(minCpuOption);
|
||||
var minConf = context.ParseResult.GetValueForOption(minConfidenceOption);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption);
|
||||
var format = context.ParseResult.GetValueForOption(formatOption);
|
||||
var verbose = context.ParseResult.GetValueForOption(verboseOption);
|
||||
|
||||
// Use --check if --image not provided
|
||||
var targetImage = image ?? check;
|
||||
if (string.IsNullOrWhiteSpace(targetImage))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Either --image or --check must be specified.");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
var logger = services.GetService<ILogger<VexCliCommandModule>>();
|
||||
logger?.LogInformation("Running auto-downgrade check for image {Image}", targetImage);
|
||||
|
||||
await RunAutoDowngradeAsync(
|
||||
services,
|
||||
targetImage,
|
||||
dryRun,
|
||||
minObs,
|
||||
minCpu,
|
||||
minConf,
|
||||
output,
|
||||
format,
|
||||
verbose,
|
||||
options,
|
||||
cancellationToken);
|
||||
|
||||
context.ExitCode = 0;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static async Task RunAutoDowngradeAsync(
|
||||
IServiceProvider services,
|
||||
string image,
|
||||
bool dryRun,
|
||||
int minObservations,
|
||||
double minCpu,
|
||||
double minConfidence,
|
||||
string? outputPath,
|
||||
OutputFormat format,
|
||||
bool verbose,
|
||||
StellaOpsCliOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var logger = services.GetService<ILogger<VexCliCommandModule>>();
|
||||
|
||||
await AnsiConsole.Status()
|
||||
.StartAsync("Checking for hot vulnerable symbols...", async ctx =>
|
||||
{
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
|
||||
// Create client and check for downgrades
|
||||
var client = CreateAutoVexClient(services, options);
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[grey]Image: {image}[/]");
|
||||
AnsiConsole.MarkupLine($"[grey]Min observations: {minObservations}[/]");
|
||||
AnsiConsole.MarkupLine($"[grey]Min CPU%: {minCpu}[/]");
|
||||
AnsiConsole.MarkupLine($"[grey]Min confidence: {minConfidence}[/]");
|
||||
}
|
||||
|
||||
var result = await client.CheckAutoDowngradeAsync(
|
||||
image,
|
||||
minObservations,
|
||||
minCpu,
|
||||
minConfidence,
|
||||
cancellationToken);
|
||||
|
||||
ctx.Status("Processing results...");
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {result.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display results
|
||||
if (format == OutputFormat.Json)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
await File.WriteAllTextAsync(outputPath, json, cancellationToken);
|
||||
AnsiConsole.MarkupLine($"[green]Results written to:[/] {outputPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.WriteLine(json);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderTableResults(result, dryRun);
|
||||
}
|
||||
|
||||
// Execute downgrades if not dry run
|
||||
if (!dryRun && result.Candidates?.Count > 0)
|
||||
{
|
||||
ctx.Status("Generating VEX downgrades...");
|
||||
|
||||
var downgradeResult = await client.ExecuteAutoDowngradeAsync(
|
||||
result.Candidates,
|
||||
cancellationToken);
|
||||
|
||||
if (downgradeResult.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine(
|
||||
$"[green]✓[/] Generated {downgradeResult.DowngradeCount} VEX downgrade(s)");
|
||||
|
||||
if (downgradeResult.Notifications > 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine(
|
||||
$"[blue]📨[/] Sent {downgradeResult.Notifications} notification(s)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error during downgrade:[/] {downgradeResult.Error}");
|
||||
}
|
||||
}
|
||||
else if (dryRun && result.Candidates?.Count > 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[yellow]Dry run:[/] {result.Candidates.Count} candidate(s) would be downgraded");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void RenderTableResults(AutoDowngradeCheckResult result, bool dryRun)
|
||||
{
|
||||
if (result.Candidates == null || result.Candidates.Count == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[green]✓[/] No hot vulnerable symbols detected");
|
||||
return;
|
||||
}
|
||||
|
||||
var table = new Table();
|
||||
table.Border = TableBorder.Rounded;
|
||||
table.Title = new TableTitle(
|
||||
dryRun ? "[yellow]Auto-Downgrade Candidates (Dry Run)[/]" : "[red]Hot Vulnerable Symbols[/]");
|
||||
|
||||
table.AddColumn("CVE");
|
||||
table.AddColumn("Symbol");
|
||||
table.AddColumn("CPU%");
|
||||
table.AddColumn("Observations");
|
||||
table.AddColumn("Confidence");
|
||||
table.AddColumn("Status");
|
||||
|
||||
foreach (var candidate in result.Candidates)
|
||||
{
|
||||
var cpuColor = candidate.CpuPercentage >= 10.0 ? "red" :
|
||||
candidate.CpuPercentage >= 5.0 ? "yellow" : "white";
|
||||
|
||||
var confidenceColor = candidate.Confidence >= 0.9 ? "green" :
|
||||
candidate.Confidence >= 0.7 ? "yellow" : "red";
|
||||
|
||||
table.AddRow(
|
||||
$"[bold]{candidate.CveId}[/]",
|
||||
candidate.Symbol.Length > 40
|
||||
? candidate.Symbol[..37] + "..."
|
||||
: candidate.Symbol,
|
||||
$"[{cpuColor}]{candidate.CpuPercentage:F1}%[/]",
|
||||
candidate.ObservationCount.ToString(),
|
||||
$"[{confidenceColor}]{candidate.Confidence:F2}[/]",
|
||||
dryRun ? "[yellow]pending[/]" : "[red]downgrade[/]"
|
||||
);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
|
||||
// Summary
|
||||
var panel = new Panel(
|
||||
$"Total candidates: {result.Candidates.Count}\n" +
|
||||
$"Highest CPU: {result.Candidates.Max(c => c.CpuPercentage):F1}%\n" +
|
||||
$"Image: {result.ImageDigest}")
|
||||
.Header("[bold]Summary[/]")
|
||||
.Border(BoxBorder.Rounded);
|
||||
|
||||
AnsiConsole.Write(panel);
|
||||
}
|
||||
|
||||
private static Command BuildCheckCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cmd = new Command("check", "Check VEX status for an image or CVE.");
|
||||
|
||||
var imageOption = new Option<string?>("--image")
|
||||
{
|
||||
Description = "Container image to check"
|
||||
};
|
||||
|
||||
var cveOption = new Option<string?>("--cve")
|
||||
{
|
||||
Description = "CVE identifier to check"
|
||||
};
|
||||
|
||||
cmd.AddOption(imageOption);
|
||||
cmd.AddOption(cveOption);
|
||||
cmd.AddOption(verboseOption);
|
||||
|
||||
cmd.SetHandler(async (context) =>
|
||||
{
|
||||
var image = context.ParseResult.GetValueForOption(imageOption);
|
||||
var cve = context.ParseResult.GetValueForOption(cveOption);
|
||||
var verbose = context.ParseResult.GetValueForOption(verboseOption);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(image) && string.IsNullOrWhiteSpace(cve))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Either --image or --cve must be specified.");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine("[grey]VEX check not yet implemented[/]");
|
||||
context.ExitCode = 0;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command BuildListCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cmd = new Command("list", "List VEX statements.");
|
||||
|
||||
var productOption = new Option<string?>("--product")
|
||||
{
|
||||
Description = "Filter by product identifier"
|
||||
};
|
||||
|
||||
var statusOption = new Option<string?>("--status")
|
||||
{
|
||||
Description = "Filter by VEX status (affected, not_affected, fixed, under_investigation)"
|
||||
};
|
||||
|
||||
var limitOption = new Option<int>("--limit")
|
||||
{
|
||||
Description = "Maximum number of results"
|
||||
};
|
||||
limitOption.SetDefaultValue(100);
|
||||
|
||||
cmd.AddOption(productOption);
|
||||
cmd.AddOption(statusOption);
|
||||
cmd.AddOption(limitOption);
|
||||
cmd.AddOption(verboseOption);
|
||||
|
||||
cmd.SetHandler(async (context) =>
|
||||
{
|
||||
var product = context.ParseResult.GetValueForOption(productOption);
|
||||
var status = context.ParseResult.GetValueForOption(statusOption);
|
||||
var limit = context.ParseResult.GetValueForOption(limitOption);
|
||||
|
||||
AnsiConsole.MarkupLine("[grey]VEX list not yet implemented[/]");
|
||||
context.ExitCode = 0;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static Command BuildNotReachableCommand(
|
||||
IServiceProvider services,
|
||||
Option<bool> verboseOption,
|
||||
StellaOpsCliOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cmd = new Command("not-reachable", "Generate VEX with not_reachable_at_runtime justification.");
|
||||
|
||||
var imageOption = new Option<string>("--image")
|
||||
{
|
||||
Description = "Container image to analyze",
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
var windowOption = new Option<int>("--window")
|
||||
{
|
||||
Description = "Observation window in hours"
|
||||
};
|
||||
windowOption.SetDefaultValue(24);
|
||||
|
||||
var minConfidenceOption = new Option<double>("--min-confidence")
|
||||
{
|
||||
Description = "Minimum confidence threshold"
|
||||
};
|
||||
minConfidenceOption.SetDefaultValue(0.6);
|
||||
|
||||
var outputOption = new Option<string?>("--output")
|
||||
{
|
||||
Description = "Output file path for generated VEX statements"
|
||||
};
|
||||
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
Description = "Dry run - analyze but don't generate VEX"
|
||||
};
|
||||
|
||||
cmd.AddOption(imageOption);
|
||||
cmd.AddOption(windowOption);
|
||||
cmd.AddOption(minConfidenceOption);
|
||||
cmd.AddOption(outputOption);
|
||||
cmd.AddOption(dryRunOption);
|
||||
cmd.AddOption(verboseOption);
|
||||
|
||||
cmd.SetHandler(async (context) =>
|
||||
{
|
||||
var image = context.ParseResult.GetValueForOption(imageOption);
|
||||
var window = context.ParseResult.GetValueForOption(windowOption);
|
||||
var minConf = context.ParseResult.GetValueForOption(minConfidenceOption);
|
||||
var output = context.ParseResult.GetValueForOption(outputOption);
|
||||
var dryRun = context.ParseResult.GetValueForOption(dryRunOption);
|
||||
var verbose = context.ParseResult.GetValueForOption(verboseOption);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] --image is required.");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await RunNotReachableAnalysisAsync(
|
||||
services,
|
||||
image,
|
||||
TimeSpan.FromHours(window),
|
||||
minConf,
|
||||
output,
|
||||
dryRun,
|
||||
verbose,
|
||||
options,
|
||||
cancellationToken);
|
||||
|
||||
context.ExitCode = 0;
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
private static async Task RunNotReachableAnalysisAsync(
|
||||
IServiceProvider services,
|
||||
string image,
|
||||
TimeSpan window,
|
||||
double minConfidence,
|
||||
string? outputPath,
|
||||
bool dryRun,
|
||||
bool verbose,
|
||||
StellaOpsCliOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await AnsiConsole.Status()
|
||||
.StartAsync("Analyzing unreached vulnerable symbols...", async ctx =>
|
||||
{
|
||||
ctx.Spinner(Spinner.Known.Dots);
|
||||
|
||||
var client = CreateAutoVexClient(services, options);
|
||||
|
||||
var result = await client.AnalyzeNotReachableAsync(
|
||||
image,
|
||||
window,
|
||||
minConfidence,
|
||||
cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {result.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Analyses == null || result.Analyses.Count == 0)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[green]✓[/] No unreached vulnerable symbols found requiring VEX");
|
||||
return;
|
||||
}
|
||||
|
||||
// Display results
|
||||
var table = new Table();
|
||||
table.Border = TableBorder.Rounded;
|
||||
table.Title = new TableTitle("[green]Symbols Not Reachable at Runtime[/]");
|
||||
|
||||
table.AddColumn("CVE");
|
||||
table.AddColumn("Symbol");
|
||||
table.AddColumn("Component");
|
||||
table.AddColumn("Confidence");
|
||||
table.AddColumn("Reason");
|
||||
|
||||
foreach (var analysis in result.Analyses)
|
||||
{
|
||||
var reason = analysis.PrimaryReason ?? "Unknown";
|
||||
table.AddRow(
|
||||
$"[bold]{analysis.CveId}[/]",
|
||||
analysis.Symbol.Length > 30 ? analysis.Symbol[..27] + "..." : analysis.Symbol,
|
||||
analysis.ComponentPath.Length > 25 ? "..." + analysis.ComponentPath[^22..] : analysis.ComponentPath,
|
||||
$"[green]{analysis.Confidence:F2}[/]",
|
||||
reason
|
||||
);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(table);
|
||||
|
||||
if (!dryRun)
|
||||
{
|
||||
ctx.Status("Generating VEX statements...");
|
||||
|
||||
var vexResult = await client.GenerateNotReachableVexAsync(
|
||||
result.Analyses,
|
||||
cancellationToken);
|
||||
|
||||
if (vexResult.Success)
|
||||
{
|
||||
AnsiConsole.MarkupLine(
|
||||
$"[green]✓[/] Generated {vexResult.StatementCount} VEX statement(s)");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
var json = JsonSerializer.Serialize(vexResult.Statements, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
await File.WriteAllTextAsync(outputPath, json, cancellationToken);
|
||||
AnsiConsole.MarkupLine($"[green]Written to:[/] {outputPath}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {vexResult.Error}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[yellow]Dry run:[/] Would generate {result.Analyses.Count} VEX statement(s)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static IAutoVexClient CreateAutoVexClient(IServiceProvider services, StellaOpsCliOptions options)
|
||||
{
|
||||
// Try to get from DI first
|
||||
var client = services.GetService<IAutoVexClient>();
|
||||
if (client != null)
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
// Create HTTP client for API calls
|
||||
var httpClient = services.GetService<IHttpClientFactory>()?.CreateClient("autovex")
|
||||
?? new HttpClient();
|
||||
|
||||
var baseUrl = options.ExcititorApiBaseUrl
|
||||
?? Environment.GetEnvironmentVariable("STELLAOPS_EXCITITOR_URL")
|
||||
?? "http://localhost:5080";
|
||||
|
||||
return new AutoVexHttpClient(httpClient, baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Output format for CLI commands.
|
||||
/// </summary>
|
||||
public enum OutputFormat
|
||||
{
|
||||
Table,
|
||||
Json,
|
||||
Csv
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client interface for auto-VEX operations.
|
||||
/// </summary>
|
||||
public interface IAutoVexClient
|
||||
{
|
||||
Task<AutoDowngradeCheckResult> CheckAutoDowngradeAsync(
|
||||
string image,
|
||||
int minObservations,
|
||||
double minCpu,
|
||||
double minConfidence,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AutoDowngradeExecuteResult> ExecuteAutoDowngradeAsync(
|
||||
IReadOnlyList<AutoDowngradeCandidate> candidates,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotReachableAnalysisResult> AnalyzeNotReachableAsync(
|
||||
string image,
|
||||
TimeSpan window,
|
||||
double minConfidence,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<NotReachableVexGenerationResult> GenerateNotReachableVexAsync(
|
||||
IReadOnlyList<NotReachableAnalysisEntry> analyses,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of checking for auto-downgrade candidates.
|
||||
/// </summary>
|
||||
public sealed record AutoDowngradeCheckResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public string? ImageDigest { get; init; }
|
||||
public IReadOnlyList<AutoDowngradeCandidate>? Candidates { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A candidate for auto-downgrade.
|
||||
/// </summary>
|
||||
public sealed record AutoDowngradeCandidate
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string ProductId { get; init; }
|
||||
public required string Symbol { get; init; }
|
||||
public required string ComponentPath { get; init; }
|
||||
public required double CpuPercentage { get; init; }
|
||||
public required int ObservationCount { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public required string BuildId { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of executing auto-downgrades.
|
||||
/// </summary>
|
||||
public sealed record AutoDowngradeExecuteResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public int DowngradeCount { get; init; }
|
||||
public int Notifications { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of not-reachable analysis.
|
||||
/// </summary>
|
||||
public sealed record NotReachableAnalysisResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public IReadOnlyList<NotReachableAnalysisEntry>? Analyses { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry for not-reachable analysis.
|
||||
/// </summary>
|
||||
public sealed record NotReachableAnalysisEntry
|
||||
{
|
||||
public required string CveId { get; init; }
|
||||
public required string ProductId { get; init; }
|
||||
public required string Symbol { get; init; }
|
||||
public required string ComponentPath { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public string? PrimaryReason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of generating not-reachable VEX statements.
|
||||
/// </summary>
|
||||
public sealed record NotReachableVexGenerationResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public int StatementCount { get; init; }
|
||||
public IReadOnlyList<object>? Statements { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client implementation for auto-VEX API.
|
||||
/// </summary>
|
||||
internal sealed class AutoVexHttpClient : IAutoVexClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public AutoVexHttpClient(HttpClient httpClient, string baseUrl)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_baseUrl = baseUrl?.TrimEnd('/') ?? throw new ArgumentNullException(nameof(baseUrl));
|
||||
}
|
||||
|
||||
public async Task<AutoDowngradeCheckResult> CheckAutoDowngradeAsync(
|
||||
string image,
|
||||
int minObservations,
|
||||
double minCpu,
|
||||
double minConfidence,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{_baseUrl}/api/v1/vex/auto-downgrade/check?" +
|
||||
$"image={Uri.EscapeDataString(image)}&" +
|
||||
$"minObservations={minObservations}&" +
|
||||
$"minCpu={minCpu}&" +
|
||||
$"minConfidence={minConfidence}";
|
||||
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return JsonSerializer.Deserialize<AutoDowngradeCheckResult>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new AutoDowngradeCheckResult { Success = false, Error = "Failed to deserialize response" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new AutoDowngradeCheckResult
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AutoDowngradeExecuteResult> ExecuteAutoDowngradeAsync(
|
||||
IReadOnlyList<AutoDowngradeCandidate> candidates,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{_baseUrl}/api/v1/vex/auto-downgrade/execute";
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(candidates),
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync(url, content, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return JsonSerializer.Deserialize<AutoDowngradeExecuteResult>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new AutoDowngradeExecuteResult { Success = false, Error = "Failed to deserialize response" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new AutoDowngradeExecuteResult
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<NotReachableAnalysisResult> AnalyzeNotReachableAsync(
|
||||
string image,
|
||||
TimeSpan window,
|
||||
double minConfidence,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{_baseUrl}/api/v1/vex/not-reachable/analyze?" +
|
||||
$"image={Uri.EscapeDataString(image)}&" +
|
||||
$"windowHours={window.TotalHours}&" +
|
||||
$"minConfidence={minConfidence}";
|
||||
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return JsonSerializer.Deserialize<NotReachableAnalysisResult>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new NotReachableAnalysisResult { Success = false, Error = "Failed to deserialize response" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new NotReachableAnalysisResult
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<NotReachableVexGenerationResult> GenerateNotReachableVexAsync(
|
||||
IReadOnlyList<NotReachableAnalysisEntry> analyses,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"{_baseUrl}/api/v1/vex/not-reachable/generate";
|
||||
var content = new StringContent(
|
||||
JsonSerializer.Serialize(analyses),
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync(url, content, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return JsonSerializer.Deserialize<NotReachableVexGenerationResult>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new NotReachableVexGenerationResult { Success = false, Error = "Failed to deserialize response" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new NotReachableVexGenerationResult
|
||||
{
|
||||
Success = false,
|
||||
Error = ex.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user