Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.

This commit is contained in:
StellaOps Bot
2025-12-26 21:54:17 +02:00
parent 335ff7da16
commit c2b9cd8d1f
3717 changed files with 264714 additions and 48202 deletions

View File

@@ -20,7 +20,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="Npgsql" />
</ItemGroup>
<Target Name="CopyPluginBinaries" AfterTargets="Build">

View File

@@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.48.0" />
<PackageReference Include="Spectre.Console" />
</ItemGroup>
<Target Name="CopyPluginBinaries" AfterTargets="Build">

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- StellaOps.Cli.Plugins.Verdict: CLI plugin for offline verdict verification -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<PluginOutputDirectory>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\plugins\cli\StellaOps.Cli.Plugins.Verdict\'))</PluginOutputDirectory>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Cli\StellaOps.Cli.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Verdict\StellaOps.Verdict.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" />
</ItemGroup>
<Target Name="CopyPluginBinaries" AfterTargets="Build">
<MakeDir Directories="$(PluginOutputDirectory)" />
<Copy SourceFiles="$(TargetDir)$(TargetFileName)" DestinationFolder="$(PluginOutputDirectory)" />
<Copy SourceFiles="$(TargetDir)$(TargetName).pdb"
DestinationFolder="$(PluginOutputDirectory)"
Condition="Exists('$(TargetDir)$(TargetName).pdb')" />
</Target>
</Project>

View File

@@ -0,0 +1,652 @@
// -----------------------------------------------------------------------------
// VerdictCliCommandModule.cs
// Sprint: SPRINT_1227_0014_0001_BE_stellaverdict_consolidation
// Task: CLI verify command - stella verify --verdict
// Description: CLI plugin module for offline verdict verification.
// -----------------------------------------------------------------------------
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;
using StellaOps.Verdict.Schema;
namespace StellaOps.Cli.Plugins.Verdict;
/// <summary>
/// CLI plugin module for verdict verification commands.
/// Provides 'stella verify --verdict' for offline and online verdict verification.
/// </summary>
public sealed class VerdictCliCommandModule : ICliCommandModule
{
public string Name => "stellaops.cli.plugins.verdict";
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(BuildVerifyCommand(services, verboseOption, options, cancellationToken));
}
private static Command BuildVerifyCommand(
IServiceProvider services,
Option<bool> verboseOption,
StellaOpsCliOptions options,
CancellationToken cancellationToken)
{
var verify = new Command("verify", "Verify signatures, attestations, and verdicts.");
// Add subcommands
verify.Add(BuildVerdictVerifyCommand(services, verboseOption, options, cancellationToken));
return verify;
}
private static Command BuildVerdictVerifyCommand(
IServiceProvider services,
Option<bool> verboseOption,
StellaOpsCliOptions options,
CancellationToken cancellationToken)
{
var verdictOption = new Option<string>("--verdict", new[] { "-v" })
{
Description = "Verdict ID (urn:stella:verdict:sha256:...) or path to verdict.json file",
Required = true
};
var replayOption = new Option<string?>("--replay")
{
Description = "Path to replay bundle directory for full verification"
};
var inputsOption = new Option<string?>("--inputs")
{
Description = "Path to knowledge-snapshot.json for inputs hash verification"
};
var trustedKeysOption = new Option<string?>("--trusted-keys")
{
Description = "Path to trusted public keys file (PEM or JSON)"
};
var showTraceOption = new Option<bool>("--show-trace")
{
Description = "Show full policy evaluation trace"
};
var showEvidenceOption = new Option<bool>("--show-evidence")
{
Description = "Show evidence graph details"
};
var formatOption = new Option<VerdictOutputFormat>("--format")
{
Description = "Output format",
DefaultValueFactory = _ => VerdictOutputFormat.Table
};
var outputOption = new Option<string?>("--output")
{
Description = "Output file path (default: stdout)"
};
var cmd = new Command("verdict", "Verify a StellaVerdict artifact.")
{
verdictOption,
replayOption,
inputsOption,
trustedKeysOption,
showTraceOption,
showEvidenceOption,
formatOption,
outputOption
};
cmd.SetAction(async (parseResult, ct) =>
{
var verdict = parseResult.GetValue(verdictOption);
var replay = parseResult.GetValue(replayOption);
var inputs = parseResult.GetValue(inputsOption);
var trustedKeys = parseResult.GetValue(trustedKeysOption);
var showTrace = parseResult.GetValue(showTraceOption);
var showEvidence = parseResult.GetValue(showEvidenceOption);
var format = parseResult.GetValue(formatOption);
var output = parseResult.GetValue(outputOption);
var verbose = parseResult.GetValue(verboseOption);
if (string.IsNullOrWhiteSpace(verdict))
{
AnsiConsole.MarkupLine("[red]Error:[/] --verdict is required.");
return 1;
}
return await RunVerdictVerifyAsync(
services,
verdict!,
replay,
inputs,
trustedKeys,
showTrace,
showEvidence,
format,
output,
verbose,
options,
ct);
});
return cmd;
}
private static async Task<int> RunVerdictVerifyAsync(
IServiceProvider services,
string verdictPath,
string? replayPath,
string? inputsPath,
string? trustedKeysPath,
bool showTrace,
bool showEvidence,
VerdictOutputFormat format,
string? outputPath,
bool verbose,
StellaOpsCliOptions options,
CancellationToken cancellationToken)
{
var logger = services.GetService<ILogger<VerdictCliCommandModule>>();
var result = new VerdictVerificationResult();
try
{
// Step 1: Load the verdict
StellaVerdict? loadedVerdict = null;
await AnsiConsole.Status()
.StartAsync("Loading verdict...", async ctx =>
{
ctx.Spinner(Spinner.Known.Dots);
if (verdictPath.StartsWith("urn:stella:verdict:", StringComparison.OrdinalIgnoreCase))
{
// Fetch from API
ctx.Status("Fetching verdict from API...");
loadedVerdict = await FetchVerdictFromApiAsync(services, verdictPath, options, cancellationToken);
}
else if (File.Exists(verdictPath))
{
// Load from file
ctx.Status("Loading verdict from file...");
var json = await File.ReadAllTextAsync(verdictPath, cancellationToken);
loadedVerdict = JsonSerializer.Deserialize<StellaVerdict>(json, JsonOptions);
}
else
{
result.Error = $"Verdict not found: {verdictPath}";
}
});
if (loadedVerdict is null)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {result.Error ?? "Failed to load verdict"}");
return 1;
}
result.VerdictId = loadedVerdict.VerdictId;
// Step 2: Verify content-addressable ID
await AnsiConsole.Status()
.StartAsync("Verifying content ID...", ctx =>
{
ctx.Spinner(Spinner.Known.Dots);
var computedId = loadedVerdict.ComputeVerdictId();
result.ContentIdValid = string.Equals(loadedVerdict.VerdictId, computedId, StringComparison.Ordinal);
if (!result.ContentIdValid)
{
result.ContentIdMismatch = $"Expected {computedId}, got {loadedVerdict.VerdictId}";
}
return Task.CompletedTask;
});
// Step 3: Check signature
await AnsiConsole.Status()
.StartAsync("Checking signatures...", ctx =>
{
ctx.Spinner(Spinner.Known.Dots);
result.HasSignatures = !loadedVerdict.Signatures.IsDefaultOrEmpty && loadedVerdict.Signatures.Length > 0;
result.SignatureCount = result.HasSignatures ? loadedVerdict.Signatures.Length : 0;
if (result.HasSignatures && !string.IsNullOrEmpty(trustedKeysPath))
{
// TODO: Implement full signature verification with trusted keys
result.SignaturesVerified = false;
result.SignatureMessage = "Signature verification with trusted keys not yet implemented";
}
else if (result.HasSignatures)
{
result.SignaturesVerified = false;
result.SignatureMessage = "Signatures present but no trusted keys provided for verification";
}
else
{
result.SignatureMessage = "Verdict has no signatures";
}
return Task.CompletedTask;
});
// Step 4: Verify inputs hash if provided
if (!string.IsNullOrEmpty(inputsPath))
{
await AnsiConsole.Status()
.StartAsync("Verifying inputs hash...", async ctx =>
{
ctx.Spinner(Spinner.Known.Dots);
if (File.Exists(inputsPath))
{
var inputsJson = await File.ReadAllTextAsync(inputsPath, cancellationToken);
var inputsHash = ComputeHash(inputsJson);
// Compare with verdict's deterministic inputs hash
var verdictInputsJson = JsonSerializer.Serialize(loadedVerdict.Inputs, JsonOptions);
var verdictInputsHash = ComputeHash(verdictInputsJson);
result.InputsHashValid = string.Equals(inputsHash, verdictInputsHash, StringComparison.OrdinalIgnoreCase);
result.InputsHashMessage = result.InputsHashValid == true
? "Inputs hash matches"
: $"Inputs hash mismatch: file={inputsHash[..16]}..., verdict={verdictInputsHash[..16]}...";
}
else
{
result.InputsHashValid = false;
result.InputsHashMessage = $"Inputs file not found: {inputsPath}";
}
});
}
// Step 5: Verify replay bundle if provided
if (!string.IsNullOrEmpty(replayPath))
{
await AnsiConsole.Status()
.StartAsync("Verifying replay bundle...", async ctx =>
{
ctx.Spinner(Spinner.Known.Dots);
if (Directory.Exists(replayPath))
{
// Check for manifest
var manifestPath = Path.Combine(replayPath, "manifest.json");
if (File.Exists(manifestPath))
{
var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken);
// TODO: Parse manifest and verify all referenced files
result.ReplayBundleValid = true;
result.ReplayBundleMessage = "Replay bundle structure valid";
}
else
{
result.ReplayBundleValid = false;
result.ReplayBundleMessage = "Replay bundle missing manifest.json";
}
}
else
{
result.ReplayBundleValid = false;
result.ReplayBundleMessage = $"Replay bundle directory not found: {replayPath}";
}
});
}
// Step 6: Check expiration
result.IsExpired = false;
if (!string.IsNullOrEmpty(loadedVerdict.Result.ExpiresAt))
{
if (DateTimeOffset.TryParse(loadedVerdict.Result.ExpiresAt, out var expiresAt))
{
result.IsExpired = expiresAt < DateTimeOffset.UtcNow;
result.ExpiresAt = expiresAt;
}
}
// Determine overall validity
result.IsValid = result.ContentIdValid
&& (!result.HasSignatures || result.SignaturesVerified == true)
&& !result.IsExpired
&& (string.IsNullOrEmpty(inputsPath) || result.InputsHashValid == true)
&& (string.IsNullOrEmpty(replayPath) || result.ReplayBundleValid == true);
// Output results
if (format == VerdictOutputFormat.Json)
{
var resultJson = JsonSerializer.Serialize(new
{
verdictId = result.VerdictId,
isValid = result.IsValid,
contentIdValid = result.ContentIdValid,
hasSignatures = result.HasSignatures,
signatureCount = result.SignatureCount,
signaturesVerified = result.SignaturesVerified,
isExpired = result.IsExpired,
expiresAt = result.ExpiresAt?.ToString("O"),
inputsHashValid = result.InputsHashValid,
replayBundleValid = result.ReplayBundleValid,
verdict = loadedVerdict
}, new JsonSerializerOptions { WriteIndented = true });
if (!string.IsNullOrEmpty(outputPath))
{
await File.WriteAllTextAsync(outputPath, resultJson, cancellationToken);
AnsiConsole.MarkupLine($"[green]Results written to:[/] {outputPath}");
}
else
{
Console.WriteLine(resultJson);
}
}
else
{
RenderTableResult(loadedVerdict, result, showTrace, showEvidence, verbose);
}
// Return appropriate exit code
if (!result.IsValid)
{
return 1; // Invalid
}
if (result.IsExpired)
{
return 2; // Expired
}
return 0; // Valid
}
catch (Exception ex)
{
logger?.LogError(ex, "Failed to verify verdict: {Path}", verdictPath);
AnsiConsole.MarkupLine($"[red]Error:[/] {ex.Message}");
return 1;
}
}
private static void RenderTableResult(
StellaVerdict verdict,
VerdictVerificationResult result,
bool showTrace,
bool showEvidence,
bool verbose)
{
// Status panel
var statusColor = result.IsValid ? "green" : (result.IsExpired ? "yellow" : "red");
var statusText = result.IsValid ? "VALID" : (result.IsExpired ? "EXPIRED" : "INVALID");
var statusPanel = new Panel(
new Markup($"[bold {statusColor}]{statusText}[/]"))
.Header("[bold]Verification Result[/]")
.Border(BoxBorder.Rounded)
.Padding(1, 0);
AnsiConsole.Write(statusPanel);
AnsiConsole.WriteLine();
// Subject info
var subjectTable = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Subject[/]")
.AddColumn("Property")
.AddColumn("Value");
subjectTable.AddRow("Verdict ID", verdict.VerdictId);
subjectTable.AddRow("Vulnerability", verdict.Subject.VulnerabilityId);
subjectTable.AddRow("Component", verdict.Subject.Purl);
if (!string.IsNullOrEmpty(verdict.Subject.ImageDigest))
{
subjectTable.AddRow("Image", verdict.Subject.ImageDigest);
}
AnsiConsole.Write(subjectTable);
AnsiConsole.WriteLine();
// Claim info
var claimTable = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Claim[/]")
.AddColumn("Property")
.AddColumn("Value");
var claimStatusColor = verdict.Claim.Status switch
{
VerdictStatus.Pass => "green",
VerdictStatus.Blocked => "red",
VerdictStatus.Warned => "yellow",
VerdictStatus.Ignored => "grey",
VerdictStatus.Deferred => "blue",
VerdictStatus.Escalated => "orange1",
VerdictStatus.RequiresVex => "purple",
_ => "white"
};
claimTable.AddRow("Status", $"[{claimStatusColor}]{verdict.Claim.Status}[/]");
claimTable.AddRow("Disposition", verdict.Result.Disposition);
claimTable.AddRow("Score", $"{verdict.Result.Score:F2}");
claimTable.AddRow("Confidence", $"{verdict.Claim.Confidence:P0}");
if (!string.IsNullOrEmpty(verdict.Claim.Reason))
{
claimTable.AddRow("Reason", verdict.Claim.Reason);
}
AnsiConsole.Write(claimTable);
AnsiConsole.WriteLine();
// Verification checks
var checksTable = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Verification Checks[/]")
.AddColumn("Check")
.AddColumn("Result")
.AddColumn("Details");
checksTable.AddRow(
"Content ID",
result.ContentIdValid ? "[green]PASS[/]" : "[red]FAIL[/]",
result.ContentIdValid ? "Hash matches" : result.ContentIdMismatch ?? "Hash mismatch");
checksTable.AddRow(
"Signatures",
result.HasSignatures
? (result.SignaturesVerified == true ? "[green]VERIFIED[/]" : "[yellow]PRESENT[/]")
: "[grey]NONE[/]",
result.SignatureMessage ?? (result.HasSignatures ? $"{result.SignatureCount} signature(s)" : "No signatures"));
if (result.InputsHashValid.HasValue)
{
checksTable.AddRow(
"Inputs Hash",
result.InputsHashValid.Value ? "[green]PASS[/]" : "[red]FAIL[/]",
result.InputsHashMessage ?? "");
}
if (result.ReplayBundleValid.HasValue)
{
checksTable.AddRow(
"Replay Bundle",
result.ReplayBundleValid.Value ? "[green]VALID[/]" : "[red]INVALID[/]",
result.ReplayBundleMessage ?? "");
}
checksTable.AddRow(
"Expiration",
result.IsExpired ? "[red]EXPIRED[/]" : "[green]VALID[/]",
result.ExpiresAt.HasValue
? (result.IsExpired ? $"Expired {result.ExpiresAt:g}" : $"Expires {result.ExpiresAt:g}")
: "No expiration");
AnsiConsole.Write(checksTable);
AnsiConsole.WriteLine();
// Policy trace
if (showTrace && !verdict.PolicyPath.IsDefaultOrEmpty)
{
var traceTable = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Policy Evaluation Trace[/]")
.AddColumn("#")
.AddColumn("Rule")
.AddColumn("Matched")
.AddColumn("Action")
.AddColumn("Reason");
foreach (var step in verdict.PolicyPath.OrderBy(s => s.Order))
{
traceTable.AddRow(
step.Order.ToString(),
step.RuleName ?? step.RuleId,
step.Matched ? "[green]Yes[/]" : "[grey]No[/]",
step.Action ?? "-",
step.Reason ?? "-");
}
AnsiConsole.Write(traceTable);
AnsiConsole.WriteLine();
}
// Evidence graph
if (showEvidence && verdict.EvidenceGraph is not null)
{
var evidenceTable = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Evidence Graph[/]")
.AddColumn("Node ID")
.AddColumn("Type")
.AddColumn("Label");
foreach (var node in verdict.EvidenceGraph.Nodes)
{
var shortId = node.Id.Length > 16 ? node.Id[..16] + "..." : node.Id;
evidenceTable.AddRow(
shortId,
node.Type,
node.Label ?? "-");
}
AnsiConsole.Write(evidenceTable);
AnsiConsole.WriteLine();
}
// Provenance
if (verbose)
{
var provTable = new Table()
.Border(TableBorder.Rounded)
.Title("[bold]Provenance[/]")
.AddColumn("Property")
.AddColumn("Value");
provTable.AddRow("Generator", verdict.Provenance.Generator);
if (!string.IsNullOrEmpty(verdict.Provenance.GeneratorVersion))
{
provTable.AddRow("Version", verdict.Provenance.GeneratorVersion);
}
if (!string.IsNullOrEmpty(verdict.Provenance.RunId))
{
provTable.AddRow("Run ID", verdict.Provenance.RunId);
}
provTable.AddRow("Created", verdict.Provenance.CreatedAt);
AnsiConsole.Write(provTable);
}
}
private static async Task<StellaVerdict?> FetchVerdictFromApiAsync(
IServiceProvider services,
string verdictId,
StellaOpsCliOptions options,
CancellationToken cancellationToken)
{
var httpClientFactory = services.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory?.CreateClient("verdict") ?? new HttpClient();
var baseUrl = options.BackendUrl?.TrimEnd('/')
?? Environment.GetEnvironmentVariable("STELLAOPS_BACKEND_URL")
?? "http://localhost:5000";
var escapedId = Uri.EscapeDataString(verdictId);
var url = $"{baseUrl}/v1/verdicts/{escapedId}";
try
{
var response = await httpClient.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
return null;
}
var json = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonSerializer.Deserialize<StellaVerdict>(json, JsonOptions);
}
catch
{
return null;
}
}
private static string ComputeHash(string content)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
WriteIndented = false
};
}
/// <summary>
/// Output format for verdict verification.
/// </summary>
public enum VerdictOutputFormat
{
Table,
Json
}
/// <summary>
/// Result of verdict verification.
/// </summary>
internal sealed class VerdictVerificationResult
{
public string? VerdictId { get; set; }
public bool IsValid { get; set; }
public bool ContentIdValid { get; set; }
public string? ContentIdMismatch { get; set; }
public bool HasSignatures { get; set; }
public int SignatureCount { get; set; }
public bool? SignaturesVerified { get; set; }
public string? SignatureMessage { get; set; }
public bool? InputsHashValid { get; set; }
public string? InputsHashMessage { get; set; }
public bool? ReplayBundleValid { get; set; }
public string? ReplayBundleMessage { get; set; }
public bool IsExpired { get; set; }
public DateTimeOffset? ExpiresAt { get; set; }
public string? Error { get; set; }
}

View File

@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.48.0" />
<PackageReference Include="Spectre.Console" />
</ItemGroup>
<Target Name="CopyPluginBinaries" AfterTargets="Build">

View File

@@ -62,18 +62,14 @@ public sealed class VexCliCommandModule : ICliCommandModule
StellaOpsCliOptions options,
CancellationToken cancellationToken)
{
var cmd = new Command("auto-downgrade", "Auto-downgrade VEX based on runtime observations.");
var imageOption = new Option<string>("--image")
var imageOption = new Option<string?>("--image")
{
Description = "Container image digest or reference to check",
IsRequired = false
Description = "Container image digest or reference to check"
};
var checkOption = new Option<string>("--check")
var checkOption = new Option<string?>("--check")
{
Description = "Image to check for hot vulnerable symbols",
IsRequired = false
Description = "Image to check for hot vulnerable symbols"
};
var dryRunOption = new Option<bool>("--dry-run")
@@ -84,20 +80,20 @@ public sealed class VexCliCommandModule : ICliCommandModule
var minObservationsOption = new Option<int>("--min-observations")
{
Description = "Minimum observation count threshold",
DefaultValueFactory = _ => 10
};
minObservationsOption.SetDefaultValue(10);
var minCpuOption = new Option<double>("--min-cpu")
{
Description = "Minimum CPU percentage threshold",
DefaultValueFactory = _ => 1.0
};
minCpuOption.SetDefaultValue(1.0);
var minConfidenceOption = new Option<double>("--min-confidence")
{
Description = "Minimum confidence threshold (0.0-1.0)",
DefaultValueFactory = _ => 0.7
};
minConfidenceOption.SetDefaultValue(0.7);
var outputOption = new Option<string?>("--output")
{
@@ -106,39 +102,40 @@ public sealed class VexCliCommandModule : ICliCommandModule
var formatOption = new Option<OutputFormat>("--format")
{
Description = "Output format"
Description = "Output format",
DefaultValueFactory = _ => OutputFormat.Table
};
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 cmd = new Command("auto-downgrade", "Auto-downgrade VEX based on runtime observations.")
{
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);
imageOption,
checkOption,
dryRunOption,
minObservationsOption,
minCpuOption,
minConfidenceOption,
outputOption,
formatOption
};
cmd.SetAction(async (parseResult, ct) =>
{
var image = parseResult.GetValue(imageOption);
var check = parseResult.GetValue(checkOption);
var dryRun = parseResult.GetValue(dryRunOption);
var minObs = parseResult.GetValue(minObservationsOption);
var minCpu = parseResult.GetValue(minCpuOption);
var minConf = parseResult.GetValue(minConfidenceOption);
var output = parseResult.GetValue(outputOption);
var format = parseResult.GetValue(formatOption);
var verbose = parseResult.GetValue(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;
return 1;
}
var logger = services.GetService<ILogger<VexCliCommandModule>>();
@@ -155,9 +152,9 @@ public sealed class VexCliCommandModule : ICliCommandModule
format,
verbose,
options,
cancellationToken);
ct);
context.ExitCode = 0;
return 0;
});
return cmd;
@@ -322,8 +319,6 @@ public sealed class VexCliCommandModule : ICliCommandModule
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"
@@ -334,25 +329,26 @@ public sealed class VexCliCommandModule : ICliCommandModule
Description = "CVE identifier to check"
};
cmd.AddOption(imageOption);
cmd.AddOption(cveOption);
cmd.AddOption(verboseOption);
cmd.SetHandler(async (context) =>
var cmd = new Command("check", "Check VEX status for an image or CVE.")
{
var image = context.ParseResult.GetValueForOption(imageOption);
var cve = context.ParseResult.GetValueForOption(cveOption);
var verbose = context.ParseResult.GetValueForOption(verboseOption);
imageOption,
cveOption
};
cmd.SetAction((parseResult, ct) =>
{
var image = parseResult.GetValue(imageOption);
var cve = parseResult.GetValue(cveOption);
var verbose = parseResult.GetValue(verboseOption);
if (string.IsNullOrWhiteSpace(image) && string.IsNullOrWhiteSpace(cve))
{
AnsiConsole.MarkupLine("[red]Error:[/] Either --image or --cve must be specified.");
context.ExitCode = 1;
return;
return Task.FromResult(1);
}
AnsiConsole.MarkupLine("[grey]VEX check not yet implemented[/]");
context.ExitCode = 0;
return Task.FromResult(0);
});
return cmd;
@@ -363,8 +359,6 @@ public sealed class VexCliCommandModule : ICliCommandModule
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var cmd = new Command("list", "List VEX statements.");
var productOption = new Option<string?>("--product")
{
Description = "Filter by product identifier"
@@ -377,23 +371,25 @@ public sealed class VexCliCommandModule : ICliCommandModule
var limitOption = new Option<int>("--limit")
{
Description = "Maximum number of results"
Description = "Maximum number of results",
DefaultValueFactory = _ => 100
};
limitOption.SetDefaultValue(100);
cmd.AddOption(productOption);
cmd.AddOption(statusOption);
cmd.AddOption(limitOption);
cmd.AddOption(verboseOption);
cmd.SetHandler(async (context) =>
var cmd = new Command("list", "List VEX statements.")
{
var product = context.ParseResult.GetValueForOption(productOption);
var status = context.ParseResult.GetValueForOption(statusOption);
var limit = context.ParseResult.GetValueForOption(limitOption);
productOption,
statusOption,
limitOption
};
cmd.SetAction((parseResult, ct) =>
{
var product = parseResult.GetValue(productOption);
var status = parseResult.GetValue(statusOption);
var limit = parseResult.GetValue(limitOption);
AnsiConsole.MarkupLine("[grey]VEX list not yet implemented[/]");
context.ExitCode = 0;
return Task.FromResult(0);
});
return cmd;
@@ -405,25 +401,23 @@ public sealed class VexCliCommandModule : ICliCommandModule
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
Required = true
};
var windowOption = new Option<int>("--window")
{
Description = "Observation window in hours"
Description = "Observation window in hours",
DefaultValueFactory = _ => 24
};
windowOption.SetDefaultValue(24);
var minConfidenceOption = new Option<double>("--min-confidence")
{
Description = "Minimum confidence threshold"
Description = "Minimum confidence threshold",
DefaultValueFactory = _ => 0.6
};
minConfidenceOption.SetDefaultValue(0.6);
var outputOption = new Option<string?>("--output")
{
@@ -435,27 +429,28 @@ public sealed class VexCliCommandModule : ICliCommandModule
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 cmd = new Command("not-reachable", "Generate VEX with not_reachable_at_runtime justification.")
{
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);
imageOption,
windowOption,
minConfidenceOption,
outputOption,
dryRunOption
};
cmd.SetAction(async (parseResult, ct) =>
{
var image = parseResult.GetValue(imageOption);
var window = parseResult.GetValue(windowOption);
var minConf = parseResult.GetValue(minConfidenceOption);
var output = parseResult.GetValue(outputOption);
var dryRun = parseResult.GetValue(dryRunOption);
var verbose = parseResult.GetValue(verboseOption);
if (string.IsNullOrWhiteSpace(image))
{
AnsiConsole.MarkupLine("[red]Error:[/] --image is required.");
context.ExitCode = 1;
return;
return 1;
}
await RunNotReachableAnalysisAsync(
@@ -467,9 +462,9 @@ public sealed class VexCliCommandModule : ICliCommandModule
dryRun,
verbose,
options,
cancellationToken);
ct);
context.ExitCode = 0;
return 0;
});
return cmd;
@@ -584,9 +579,8 @@ public sealed class VexCliCommandModule : ICliCommandModule
var httpClient = services.GetService<IHttpClientFactory>()?.CreateClient("autovex")
?? new HttpClient();
var baseUrl = options.ExcititorApiBaseUrl
?? Environment.GetEnvironmentVariable("STELLAOPS_EXCITITOR_URL")
?? "http://localhost:5080";
var baseUrl = Environment.GetEnvironmentVariable("STELLAOPS_EXCITITOR_URL")
?? (string.IsNullOrEmpty(options.BackendUrl) ? "http://localhost:5080" : options.BackendUrl);
return new AutoVexHttpClient(httpClient, baseUrl);
}