This commit is contained in:
StellaOps Bot
2025-12-07 22:49:53 +02:00
parent 11597679ed
commit 7c24ed96ee
204 changed files with 23313 additions and 1430 deletions

View File

@@ -27,17 +27,17 @@ using StellaOps.Cli.Configuration;
using StellaOps.Cli.Output;
using StellaOps.Cli.Prompts;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Bun;
using StellaOps.Cli.Services.Models.Ruby;
using StellaOps.Cli.Telemetry;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Kms;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Policies;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Bun;
using StellaOps.Cli.Services.Models.Ruby;
using StellaOps.Cli.Telemetry;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Kms;
using StellaOps.Policy.Scoring;
using StellaOps.Policy.Scoring.Engine;
using StellaOps.Policy.Scoring.Policies;
using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Java;
using StellaOps.Scanner.Analyzers.Lang.Node;
@@ -70,17 +70,17 @@ internal static class CommandHandlers
/// <summary>
/// JSON serializer options for output (alias for JsonOptions).
/// </summary>
private static readonly JsonSerializerOptions JsonOutputOptions = JsonOptions;
private static readonly JsonSerializerOptions CompactJson = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
private static readonly JsonSerializerOptions JsonOutputOptions = JsonOptions;
private static readonly JsonSerializerOptions CompactJson = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
/// <summary>
/// Sets the verbosity level for logging.
/// </summary>
private static void SetVerbosity(IServiceProvider services, bool verbose)
private static void SetVerbosity(IServiceProvider services, bool verbose)
{
// Configure logging level based on verbose flag
var loggerFactory = services.GetService<ILoggerFactory>();
@@ -90,215 +90,215 @@ internal static class CommandHandlers
var logger = loggerFactory.CreateLogger("StellaOps.Cli.Commands.CommandHandlers");
logger.LogDebug("Verbose logging enabled");
}
}
public static async Task HandleCvssScoreAsync(
IServiceProvider services,
string vulnerabilityId,
string policyPath,
string vector,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-score");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var policyJson = await File.ReadAllTextAsync(policyPath, cancellationToken).ConfigureAwait(false);
var loader = new CvssPolicyLoader();
var policyResult = loader.Load(policyJson, cancellationToken);
if (!policyResult.IsValid || policyResult.Policy is null || string.IsNullOrWhiteSpace(policyResult.Hash))
{
var errors = string.Join("; ", policyResult.Errors.Select(e => $"{e.Path}: {e.Message}"));
throw new InvalidOperationException($"Policy invalid: {errors}");
}
var policy = policyResult.Policy with { Hash = policyResult.Hash };
var engine = scope.ServiceProvider.GetRequiredService<ICvssV4Engine>();
var parsed = engine.ParseVector(vector);
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var request = new CreateCvssReceipt(
vulnerabilityId,
policy,
parsed.BaseMetrics,
parsed.ThreatMetrics,
parsed.EnvironmentalMetrics,
parsed.SupplementalMetrics,
Array.Empty<CvssEvidenceItem>(),
SigningKey: null,
CreatedBy: "cli",
CreatedAt: DateTimeOffset.UtcNow);
var receipt = await client.CreateReceiptAsync(request, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("CVSS receipt creation failed.");
if (json)
{
Console.WriteLine(JsonSerializer.Serialize(receipt, CompactJson));
}
else
{
Console.WriteLine($"✔ CVSS receipt {receipt.ReceiptId} created | Severity {receipt.Severity} | Effective {receipt.Scores.EffectiveScore:0.0}");
Console.WriteLine($"Vector: {receipt.VectorString}");
Console.WriteLine($"Policy: {receipt.PolicyRef.PolicyId} v{receipt.PolicyRef.Version} ({receipt.PolicyRef.Hash})");
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create CVSS receipt");
Environment.ExitCode = 1;
if (json)
{
var problem = new { error = "cvss_score_failed", message = ex.Message };
Console.WriteLine(JsonSerializer.Serialize(problem, CompactJson));
}
}
}
public static async Task HandleCvssShowAsync(
IServiceProvider services,
string receiptId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-show");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var receipt = await client.GetReceiptAsync(receiptId, cancellationToken).ConfigureAwait(false);
if (receipt is null)
{
Environment.ExitCode = 5;
Console.WriteLine(json
? JsonSerializer.Serialize(new { error = "not_found", receiptId }, CompactJson)
: $"✖ Receipt {receiptId} not found");
return;
}
if (json)
{
Console.WriteLine(JsonSerializer.Serialize(receipt, CompactJson));
}
else
{
Console.WriteLine($"Receipt {receipt.ReceiptId} | Severity {receipt.Severity} | Effective {receipt.Scores.EffectiveScore:0.0}");
Console.WriteLine($"Created {receipt.CreatedAt:u} by {receipt.CreatedBy}");
Console.WriteLine($"Vector: {receipt.VectorString}");
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to fetch CVSS receipt {ReceiptId}", receiptId);
Environment.ExitCode = 1;
}
}
public static async Task HandleCvssHistoryAsync(
IServiceProvider services,
string receiptId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-history");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var history = await client.GetHistoryAsync(receiptId, cancellationToken).ConfigureAwait(false);
if (json)
{
Console.WriteLine(JsonSerializer.Serialize(history, CompactJson));
}
else
{
if (history.Count == 0)
{
Console.WriteLine("(no history)");
}
else
{
foreach (var entry in history.OrderBy(h => h.Timestamp))
{
Console.WriteLine($"{entry.Timestamp:u} | {entry.Actor} | {entry.ChangeType} {entry.Field} => {entry.NewValue ?? ""} ({entry.Reason})");
}
}
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to fetch CVSS receipt history {ReceiptId}", receiptId);
Environment.ExitCode = 1;
}
}
public static async Task HandleCvssExportAsync(
IServiceProvider services,
string receiptId,
string format,
string? output,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-export");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var receipt = await client.GetReceiptAsync(receiptId, cancellationToken).ConfigureAwait(false);
if (receipt is null)
{
Environment.ExitCode = 5;
Console.WriteLine($"✖ Receipt {receiptId} not found");
return;
}
if (!string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
Environment.ExitCode = 9;
Console.WriteLine("Only json export is supported at this time.");
return;
}
var targetPath = string.IsNullOrWhiteSpace(output)
? $"cvss-receipt-{receipt.ReceiptId}.json"
: output!;
var jsonPayload = JsonSerializer.Serialize(receipt, CompactJson);
await File.WriteAllTextAsync(targetPath, jsonPayload, cancellationToken).ConfigureAwait(false);
Console.WriteLine($"✔ Exported receipt to {targetPath}");
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to export CVSS receipt {ReceiptId}", receiptId);
Environment.ExitCode = 1;
}
}
}
public static async Task HandleCvssScoreAsync(
IServiceProvider services,
string vulnerabilityId,
string policyPath,
string vector,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-score");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var policyJson = await File.ReadAllTextAsync(policyPath, cancellationToken).ConfigureAwait(false);
var loader = new CvssPolicyLoader();
var policyResult = loader.Load(policyJson, cancellationToken);
if (!policyResult.IsValid || policyResult.Policy is null || string.IsNullOrWhiteSpace(policyResult.Hash))
{
var errors = string.Join("; ", policyResult.Errors.Select(e => $"{e.Path}: {e.Message}"));
throw new InvalidOperationException($"Policy invalid: {errors}");
}
var policy = policyResult.Policy with { Hash = policyResult.Hash };
var engine = scope.ServiceProvider.GetRequiredService<ICvssV4Engine>();
var parsed = engine.ParseVector(vector);
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var request = new CreateCvssReceipt(
vulnerabilityId,
policy,
parsed.BaseMetrics,
parsed.ThreatMetrics,
parsed.EnvironmentalMetrics,
parsed.SupplementalMetrics,
Array.Empty<CvssEvidenceItem>(),
SigningKey: null,
CreatedBy: "cli",
CreatedAt: DateTimeOffset.UtcNow);
var receipt = await client.CreateReceiptAsync(request, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("CVSS receipt creation failed.");
if (json)
{
Console.WriteLine(JsonSerializer.Serialize(receipt, CompactJson));
}
else
{
Console.WriteLine($"✔ CVSS receipt {receipt.ReceiptId} created | Severity {receipt.Severity} | Effective {receipt.Scores.EffectiveScore:0.0}");
Console.WriteLine($"Vector: {receipt.VectorString}");
Console.WriteLine($"Policy: {receipt.PolicyRef.PolicyId} v{receipt.PolicyRef.Version} ({receipt.PolicyRef.Hash})");
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create CVSS receipt");
Environment.ExitCode = 1;
if (json)
{
var problem = new { error = "cvss_score_failed", message = ex.Message };
Console.WriteLine(JsonSerializer.Serialize(problem, CompactJson));
}
}
}
public static async Task HandleCvssShowAsync(
IServiceProvider services,
string receiptId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-show");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var receipt = await client.GetReceiptAsync(receiptId, cancellationToken).ConfigureAwait(false);
if (receipt is null)
{
Environment.ExitCode = 5;
Console.WriteLine(json
? JsonSerializer.Serialize(new { error = "not_found", receiptId }, CompactJson)
: $"✖ Receipt {receiptId} not found");
return;
}
if (json)
{
Console.WriteLine(JsonSerializer.Serialize(receipt, CompactJson));
}
else
{
Console.WriteLine($"Receipt {receipt.ReceiptId} | Severity {receipt.Severity} | Effective {receipt.Scores.EffectiveScore:0.0}");
Console.WriteLine($"Created {receipt.CreatedAt:u} by {receipt.CreatedBy}");
Console.WriteLine($"Vector: {receipt.VectorString}");
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to fetch CVSS receipt {ReceiptId}", receiptId);
Environment.ExitCode = 1;
}
}
public static async Task HandleCvssHistoryAsync(
IServiceProvider services,
string receiptId,
bool json,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-history");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var history = await client.GetHistoryAsync(receiptId, cancellationToken).ConfigureAwait(false);
if (json)
{
Console.WriteLine(JsonSerializer.Serialize(history, CompactJson));
}
else
{
if (history.Count == 0)
{
Console.WriteLine("(no history)");
}
else
{
foreach (var entry in history.OrderBy(h => h.Timestamp))
{
Console.WriteLine($"{entry.Timestamp:u} | {entry.Actor} | {entry.ChangeType} {entry.Field} => {entry.NewValue ?? ""} ({entry.Reason})");
}
}
}
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to fetch CVSS receipt history {ReceiptId}", receiptId);
Environment.ExitCode = 1;
}
}
public static async Task HandleCvssExportAsync(
IServiceProvider services,
string receiptId,
string format,
string? output,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("cvss-export");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
try
{
var client = scope.ServiceProvider.GetRequiredService<ICvssClient>();
var receipt = await client.GetReceiptAsync(receiptId, cancellationToken).ConfigureAwait(false);
if (receipt is null)
{
Environment.ExitCode = 5;
Console.WriteLine($"✖ Receipt {receiptId} not found");
return;
}
if (!string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
Environment.ExitCode = 9;
Console.WriteLine("Only json export is supported at this time.");
return;
}
var targetPath = string.IsNullOrWhiteSpace(output)
? $"cvss-receipt-{receipt.ReceiptId}.json"
: output!;
var jsonPayload = JsonSerializer.Serialize(receipt, CompactJson);
await File.WriteAllTextAsync(targetPath, jsonPayload, cancellationToken).ConfigureAwait(false);
Console.WriteLine($"✔ Exported receipt to {targetPath}");
Environment.ExitCode = 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to export CVSS receipt {ReceiptId}", receiptId);
Environment.ExitCode = 1;
}
}
private static async Task VerifyBundleAsync(string path, ILogger logger, CancellationToken cancellationToken)
{
@@ -29676,4 +29676,105 @@ stella policy test {policyName}.stella
}
#endregion
#region DevPortal Commands
/// <summary>
/// Handler for 'stella devportal verify' command (DVOFF-64-002).
/// Verifies integrity of a DevPortal/evidence bundle before import.
/// Exit codes: 0 success, 2 checksum mismatch, 3 signature failure, 4 TSA missing, 5 unexpected.
/// </summary>
public static async Task<int> HandleDevPortalVerifyAsync(
IServiceProvider services,
string bundlePath,
bool offline,
bool emitJson,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<DevPortalBundleVerifier>();
var verifier = new DevPortalBundleVerifier(logger);
using var activity = CliActivitySource.Instance.StartActivity("cli.devportal.verify", System.Diagnostics.ActivityKind.Client);
activity?.SetTag("stellaops.cli.command", "devportal verify");
activity?.SetTag("stellaops.cli.devportal.offline", offline);
using var duration = CliMetrics.MeasureCommandDuration("devportal verify");
try
{
var resolvedPath = Path.GetFullPath(bundlePath);
if (verbose)
{
AnsiConsole.MarkupLine($"[grey]Verifying bundle: {Markup.Escape(resolvedPath)}[/]");
if (offline)
{
AnsiConsole.MarkupLine("[grey]Mode: offline (TSA verification skipped)[/]");
}
}
var result = await verifier.VerifyBundleAsync(resolvedPath, offline, cancellationToken)
.ConfigureAwait(false);
activity?.SetTag("stellaops.cli.devportal.status", result.Status);
activity?.SetTag("stellaops.cli.devportal.exit_code", (int)result.ExitCode);
if (emitJson)
{
Console.WriteLine(result.ToJson());
}
else
{
if (result.ExitCode == DevPortalVerifyExitCode.Success)
{
AnsiConsole.MarkupLine("[green]Bundle verification successful.[/]");
AnsiConsole.MarkupLine($" Bundle ID: {Markup.Escape(result.BundleId ?? "unknown")}");
AnsiConsole.MarkupLine($" Root Hash: {Markup.Escape(result.RootHash ?? "unknown")}");
AnsiConsole.MarkupLine($" Entries: {result.Entries}");
AnsiConsole.MarkupLine($" Created: {result.CreatedAt?.ToString("O") ?? "unknown"}");
AnsiConsole.MarkupLine($" Portable: {(result.Portable ? "yes" : "no")}");
}
else
{
AnsiConsole.MarkupLine($"[red]Bundle verification failed:[/] {Markup.Escape(result.ErrorMessage ?? "Unknown error")}");
if (!string.IsNullOrEmpty(result.ErrorDetail))
{
AnsiConsole.MarkupLine($" [grey]{Markup.Escape(result.ErrorDetail)}[/]");
}
}
}
return (int)result.ExitCode;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
if (!emitJson)
{
AnsiConsole.MarkupLine("[yellow]Operation cancelled.[/]");
}
return 130;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to verify bundle");
if (emitJson)
{
var errorResult = DevPortalBundleVerificationResult.Failed(
DevPortalVerifyExitCode.Unexpected,
ex.Message);
Console.WriteLine(errorResult.ToJson());
}
else
{
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
}
return (int)DevPortalVerifyExitCode.Unexpected;
}
}
#endregion
}