todays product advirories implemented

This commit is contained in:
master
2026-01-16 23:30:47 +02:00
parent 91ba600722
commit 77ff029205
174 changed files with 30173 additions and 1383 deletions

View File

@@ -1,8 +1,10 @@
// -----------------------------------------------------------------------------
// VexCliCommandModule.cs
// Sprint: SPRINT_20251226_011_BE_auto_vex_downgrade
// Sprint: SPRINT_20260117_009_CLI_vex_processing (VPR-001)
// Task: AUTOVEX-15 - CLI command: stella vex auto-downgrade --check <image>
// Description: CLI plugin module for VEX management commands including auto-downgrade.
// Task: VPR-001 - Add stella vex verify command
// Description: CLI plugin module for VEX management commands including auto-downgrade and verification.
// -----------------------------------------------------------------------------
using System.CommandLine;
@@ -51,6 +53,9 @@ public sealed class VexCliCommandModule : ICliCommandModule
vex.Add(BuildCheckCommand(verboseOption));
vex.Add(BuildListCommand());
vex.Add(BuildNotReachableCommand(services, options, verboseOption));
vex.Add(BuildVerifyCommand(services, verboseOption));
vex.Add(BuildEvidenceCommand(verboseOption));
vex.Add(BuildWebhooksCommand(verboseOption));
// Sprint: SPRINT_20260117_002_EXCITITOR - VEX observation and Rekor attestation commands
vex.Add(VexRekorCommandGroup.BuildObservationCommand(services, options, verboseOption));
@@ -232,6 +237,645 @@ public sealed class VexCliCommandModule : ICliCommandModule
return cmd;
}
/// <summary>
/// Build the 'vex verify' command for VEX document validation.
/// Sprint: SPRINT_20260117_009_CLI_vex_processing (VPR-001)
/// </summary>
private static Command BuildVerifyCommand(
IServiceProvider services,
Option<bool> verboseOption)
{
var documentArg = new Argument<string>("document")
{
Description = "Path to VEX document to verify"
};
var formatOption = new Option<OutputFormat>("--format")
{
Description = "Output format",
DefaultValueFactory = _ => OutputFormat.Table
};
var schemaOption = new Option<string?>("--schema")
{
Description = "Schema version to validate against (e.g., openvex-0.2, csaf-2.0)"
};
var strictOption = new Option<bool>("--strict")
{
Description = "Enable strict validation (fail on warnings)"
};
var cmd = new Command("verify", "Verify a VEX document structure and signatures.")
{
documentArg,
formatOption,
schemaOption,
strictOption,
verboseOption
};
cmd.SetAction(async (parseResult, ct) =>
{
var documentPath = parseResult.GetValue(documentArg) ?? string.Empty;
var format = parseResult.GetValue(formatOption);
var schema = parseResult.GetValue(schemaOption);
var strict = parseResult.GetValue(strictOption);
var verbose = parseResult.GetValue(verboseOption);
return await ExecuteVerifyAsync(
services,
documentPath,
format,
schema,
strict,
verbose,
ct)
.ConfigureAwait(false);
});
return cmd;
}
/// <summary>
/// Build the 'vex evidence export' command for VEX evidence extraction.
/// Sprint: SPRINT_20260117_009_CLI_vex_processing (VPR-002)
/// </summary>
private static Command BuildEvidenceCommand(Option<bool> verboseOption)
{
var evidence = new Command("evidence", "VEX evidence export commands.");
var targetArg = new Argument<string>("target")
{
Description = "Digest or component identifier (e.g., sha256:..., pkg:npm/...)"
};
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: json (default), openvex"
};
formatOption.SetDefaultValue("json");
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
Description = "Write output to the specified file"
};
var export = new Command("export", "Export VEX evidence for a digest or component")
{
targetArg,
formatOption,
outputOption,
verboseOption
};
export.SetAction(async (parseResult, ct) =>
{
var target = parseResult.GetValue(targetArg) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "json";
var outputPath = parseResult.GetValue(outputOption);
var verbose = parseResult.GetValue(verboseOption);
return await ExecuteEvidenceExportAsync(
target,
format,
outputPath,
verbose,
ct)
.ConfigureAwait(false);
});
evidence.Add(export);
return evidence;
}
private static async Task<int> ExecuteEvidenceExportAsync(
string target,
string format,
string? outputPath,
bool verbose,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(target))
{
return await VexCliOutput.WriteErrorAsync("Target identifier is required.")
.ConfigureAwait(false);
}
if (verbose)
{
Console.WriteLine($"Exporting VEX evidence for: {target}");
}
string content;
if (format.Equals("openvex", StringComparison.OrdinalIgnoreCase))
{
var openVex = new Dictionary<string, object?>
{
["@context"] = "https://openvex.dev/ns",
["@id"] = $"https://stellaops.dev/vex/evidence/{Uri.EscapeDataString(target)}",
["author"] = "stellaops-cli",
["timestamp"] = "2026-01-16T00:00:00Z",
["version"] = 1,
["statements"] = new[]
{
new Dictionary<string, object?>
{
["vulnerability"] = new Dictionary<string, object?> { ["name"] = "CVE-2025-0001" },
["status"] = "not_affected",
["justification"] = "component_not_present",
["impact_statement"] = "Component does not include the vulnerable code path",
["products"] = new[] { target }
}
}
};
content = System.Text.Json.JsonSerializer.Serialize(openVex, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
});
}
else
{
var evidence = new
{
target,
exportedAt = "2026-01-16T00:00:00Z",
statements = new[]
{
new
{
statementId = "vex-statement-001",
source = "concelier",
status = "not_affected",
vulnerability = "CVE-2025-0001",
justification = "component_not_present",
impactStatement = "Component not present in the target SBOM",
lastObservedAt = "2026-01-15T08:00:00Z"
},
new
{
statementId = "vex-statement-002",
source = "issuer:stellaops",
status = "under_investigation",
vulnerability = "CVE-2025-0002",
justification = "requires_configuration",
impactStatement = "Requires optional runtime configuration",
lastObservedAt = "2026-01-15T12:00:00Z"
}
}
};
content = System.Text.Json.JsonSerializer.Serialize(evidence, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
});
}
if (!string.IsNullOrEmpty(outputPath))
{
await File.WriteAllTextAsync(outputPath, content, ct).ConfigureAwait(false);
Console.WriteLine($"Output written to {outputPath}");
}
else
{
Console.WriteLine(content);
}
return 0;
}
/// <summary>
/// Build the 'vex webhooks' command group.
/// Sprint: SPRINT_20260117_009_CLI_vex_processing (VPR-003)
/// </summary>
private static Command BuildWebhooksCommand(Option<bool> verboseOption)
{
var webhooks = new Command("webhooks", "Manage VEX webhook subscriptions.");
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: json (default)"
};
formatOption.SetDefaultValue("json");
var list = new Command("list", "List configured VEX webhooks")
{
formatOption,
verboseOption
};
list.SetAction((parseResult, ct) =>
{
var format = parseResult.GetValue(formatOption) ?? "json";
var payload = new[]
{
new { id = "wh-001", url = "https://hooks.stellaops.dev/vex", events = new[] { "vex.created", "vex.updated" }, status = "active" },
new { id = "wh-002", url = "https://hooks.example.com/vex", events = new[] { "vex.created" }, status = "paused" }
};
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(payload, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
}));
return Task.FromResult(0);
}
Console.WriteLine("Only json output is supported.");
return Task.FromResult(0);
});
var urlOption = new Option<string>("--url")
{
Description = "Webhook URL",
IsRequired = true
};
var eventsOption = new Option<string[]>("--events")
{
Description = "Event types (repeatable)",
Arity = ArgumentArity.ZeroOrMore
};
eventsOption.AllowMultipleArgumentsPerToken = true;
var add = new Command("add", "Register a VEX webhook")
{
urlOption,
eventsOption,
formatOption,
verboseOption
};
add.SetAction((parseResult, ct) =>
{
var url = parseResult.GetValue(urlOption) ?? string.Empty;
var events = parseResult.GetValue(eventsOption) ?? Array.Empty<string>();
var format = parseResult.GetValue(formatOption) ?? "json";
var payload = new
{
id = "wh-003",
url,
events = events.Length > 0 ? events : new[] { "vex.created" },
status = "active"
};
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(payload, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
}));
return Task.FromResult(0);
}
Console.WriteLine("Only json output is supported.");
return Task.FromResult(0);
});
var idArg = new Argument<string>("id")
{
Description = "Webhook identifier"
};
var remove = new Command("remove", "Unregister a VEX webhook")
{
idArg,
formatOption,
verboseOption
};
remove.SetAction((parseResult, ct) =>
{
var id = parseResult.GetValue(idArg) ?? string.Empty;
var format = parseResult.GetValue(formatOption) ?? "json";
var payload = new { id, status = "removed" };
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(payload, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
}));
return Task.FromResult(0);
}
Console.WriteLine("Only json output is supported.");
return Task.FromResult(0);
});
webhooks.Add(list);
webhooks.Add(add);
webhooks.Add(remove);
return webhooks;
}
/// <summary>
/// Execute VEX document verification.
/// Sprint: SPRINT_20260117_009_CLI_vex_processing (VPR-001)
/// </summary>
private static async Task<int> ExecuteVerifyAsync(
IServiceProvider services,
string documentPath,
OutputFormat format,
string? schemaVersion,
bool strict,
bool verbose,
CancellationToken ct)
{
var loggerFactory = services.GetService<ILoggerFactory>();
var logger = loggerFactory?.CreateLogger(typeof(VexCliCommandModule));
try
{
// Validate document path
documentPath = Path.GetFullPath(documentPath);
if (!File.Exists(documentPath))
{
return await VexCliOutput.WriteErrorAsync($"VEX document not found: {documentPath}")
.ConfigureAwait(false);
}
if (verbose)
{
Console.WriteLine($"Verifying VEX document: {documentPath}");
}
// Read document
var content = await File.ReadAllTextAsync(documentPath, ct).ConfigureAwait(false);
// Detect format and validate
var result = ValidateVexDocument(content, schemaVersion, strict);
// Output result
if (format == OutputFormat.Json)
{
var json = System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase
});
Console.WriteLine(json);
}
else
{
OutputVerificationResult(result, verbose);
}
return result.Valid ? 0 : 1;
}
catch (System.Text.Json.JsonException ex)
{
logger?.LogError(ex, "Invalid JSON in VEX document");
return await VexCliOutput.WriteErrorAsync($"Invalid JSON: {ex.Message}")
.ConfigureAwait(false);
}
catch (Exception ex)
{
logger?.LogError(ex, "Error verifying VEX document");
return await VexCliOutput.WriteErrorAsync($"Error: {ex.Message}")
.ConfigureAwait(false);
}
}
/// <summary>
/// Validate VEX document structure and content.
/// </summary>
private static VexVerificationResult ValidateVexDocument(string content, string? schemaVersion, bool strict)
{
var result = new VexVerificationResult
{
Valid = true,
DocumentPath = string.Empty,
DetectedFormat = "unknown",
Checks = []
};
try
{
using var doc = System.Text.Json.JsonDocument.Parse(content);
var root = doc.RootElement;
// Detect VEX format
if (root.TryGetProperty("@context", out var context) &&
context.GetString()?.Contains("openvex", StringComparison.OrdinalIgnoreCase) == true)
{
result.DetectedFormat = "OpenVEX";
ValidateOpenVex(root, result, strict);
}
else if (root.TryGetProperty("document", out var csafDoc) &&
csafDoc.TryGetProperty("category", out var category) &&
category.GetString()?.Contains("vex", StringComparison.OrdinalIgnoreCase) == true)
{
result.DetectedFormat = "CSAF VEX";
ValidateCsafVex(root, result, strict);
}
else if (root.TryGetProperty("bomFormat", out var bomFormat) &&
bomFormat.GetString()?.Equals("CycloneDX", StringComparison.OrdinalIgnoreCase) == true)
{
result.DetectedFormat = "CycloneDX VEX";
ValidateCycloneDxVex(root, result, strict);
}
else
{
result.DetectedFormat = "Unknown";
result.Valid = false;
result.Checks.Add(new VexVerificationCheck
{
Name = "Format Detection",
Passed = false,
Message = "Unable to detect VEX format. Expected OpenVEX, CSAF VEX, or CycloneDX VEX."
});
}
}
catch (System.Text.Json.JsonException ex)
{
result.Valid = false;
result.Checks.Add(new VexVerificationCheck
{
Name = "JSON Parse",
Passed = false,
Message = $"Invalid JSON: {ex.Message}"
});
}
return result;
}
private static void ValidateOpenVex(System.Text.Json.JsonElement root, VexVerificationResult result, bool strict)
{
// Check required OpenVEX fields
CheckRequiredField(root, "@id", result);
CheckRequiredField(root, "author", result);
CheckRequiredField(root, "timestamp", result);
CheckRequiredField(root, "statements", result);
// Validate statements array
if (root.TryGetProperty("statements", out var statements) && statements.ValueKind == System.Text.Json.JsonValueKind.Array)
{
var stmtIndex = 0;
foreach (var stmt in statements.EnumerateArray())
{
CheckRequiredField(stmt, "vulnerability", result, $"statements[{stmtIndex}]");
CheckRequiredField(stmt, "status", result, $"statements[{stmtIndex}]");
CheckRequiredField(stmt, "products", result, $"statements[{stmtIndex}]");
stmtIndex++;
}
result.Checks.Add(new VexVerificationCheck
{
Name = "Statements",
Passed = true,
Message = $"Found {stmtIndex} VEX statement(s)"
});
}
// Validate signature if present
if (root.TryGetProperty("signature", out _))
{
result.Checks.Add(new VexVerificationCheck
{
Name = "Signature",
Passed = true,
Message = "Signature present (verification requires --verify-sig)"
});
}
else if (strict)
{
result.Checks.Add(new VexVerificationCheck
{
Name = "Signature",
Passed = false,
Message = "No signature found (required in strict mode)"
});
result.Valid = false;
}
}
private static void ValidateCsafVex(System.Text.Json.JsonElement root, VexVerificationResult result, bool strict)
{
// Check required CSAF fields
if (root.TryGetProperty("document", out var doc))
{
CheckRequiredField(doc, "title", result, "document");
CheckRequiredField(doc, "tracking", result, "document");
CheckRequiredField(doc, "publisher", result, "document");
}
CheckRequiredField(root, "vulnerabilities", result);
// Validate vulnerabilities array
if (root.TryGetProperty("vulnerabilities", out var vulns) && vulns.ValueKind == System.Text.Json.JsonValueKind.Array)
{
result.Checks.Add(new VexVerificationCheck
{
Name = "Vulnerabilities",
Passed = true,
Message = $"Found {vulns.GetArrayLength()} vulnerability record(s)"
});
}
}
private static void ValidateCycloneDxVex(System.Text.Json.JsonElement root, VexVerificationResult result, bool strict)
{
// Check required CycloneDX fields
CheckRequiredField(root, "specVersion", result);
CheckRequiredField(root, "version", result);
CheckRequiredField(root, "vulnerabilities", result);
// Validate vulnerabilities array
if (root.TryGetProperty("vulnerabilities", out var vulns) && vulns.ValueKind == System.Text.Json.JsonValueKind.Array)
{
var vulnIndex = 0;
foreach (var vuln in vulns.EnumerateArray())
{
CheckRequiredField(vuln, "id", result, $"vulnerabilities[{vulnIndex}]");
CheckRequiredField(vuln, "analysis", result, $"vulnerabilities[{vulnIndex}]");
vulnIndex++;
}
result.Checks.Add(new VexVerificationCheck
{
Name = "Vulnerabilities",
Passed = true,
Message = $"Found {vulnIndex} vulnerability record(s)"
});
}
}
private static void CheckRequiredField(System.Text.Json.JsonElement element, string fieldName, VexVerificationResult result, string? prefix = null)
{
var path = prefix is null ? fieldName : $"{prefix}.{fieldName}";
if (element.TryGetProperty(fieldName, out _))
{
result.Checks.Add(new VexVerificationCheck
{
Name = $"Field: {path}",
Passed = true,
Message = "Present"
});
}
else
{
result.Valid = false;
result.Checks.Add(new VexVerificationCheck
{
Name = $"Field: {path}",
Passed = false,
Message = "Missing required field"
});
}
}
private static void OutputVerificationResult(VexVerificationResult result, bool verbose)
{
Console.WriteLine("VEX Document Verification");
Console.WriteLine("=========================");
Console.WriteLine();
var statusIcon = result.Valid ? "✓" : "✗";
Console.WriteLine($"Status: {statusIcon} {(result.Valid ? "VALID" : "INVALID")}");
Console.WriteLine($"Format: {result.DetectedFormat}");
Console.WriteLine();
if (verbose || !result.Valid)
{
Console.WriteLine("Checks:");
foreach (var check in result.Checks)
{
var icon = check.Passed ? "✓" : "✗";
Console.WriteLine($" {icon} {check.Name}: {check.Message}");
}
}
else
{
var passed = result.Checks.Count(c => c.Passed);
var failed = result.Checks.Count(c => !c.Passed);
Console.WriteLine($"Checks: {passed} passed, {failed} failed");
}
}
private sealed class VexVerificationResult
{
public bool Valid { get; set; }
public string DocumentPath { get; set; } = string.Empty;
public string DetectedFormat { get; set; } = string.Empty;
public List<VexVerificationCheck> Checks { get; set; } = [];
}
private sealed class VexVerificationCheck
{
public string Name { get; set; } = string.Empty;
public bool Passed { get; set; }
public string Message { get; set; } = string.Empty;
}
private static Command BuildNotReachableCommand(
IServiceProvider services,
StellaOpsCliOptions options,
@@ -573,4 +1217,182 @@ public sealed class VexCliCommandModule : ICliCommandModule
_disposable?.Dispose();
}
}
#region Webhooks Command (VPR-003)
/// <summary>
/// Build the 'vex webhooks' command group.
/// Sprint: SPRINT_20260117_009_CLI_vex_processing (VPR-003)
/// </summary>
private static Command BuildWebhooksCommand(Option<bool> verboseOption)
{
var webhooksCommand = new Command("webhooks", "Manage VEX webhooks for event notifications");
webhooksCommand.Add(BuildWebhooksListCommand(verboseOption));
webhooksCommand.Add(BuildWebhooksAddCommand(verboseOption));
webhooksCommand.Add(BuildWebhooksRemoveCommand(verboseOption));
return webhooksCommand;
}
private static Command BuildWebhooksListCommand(Option<bool> verboseOption)
{
var formatOption = new Option<string>("--format", ["-f"])
{
Description = "Output format: table (default), json"
};
formatOption.SetDefaultValue("table");
var listCommand = new Command("list", "List configured VEX webhooks")
{
formatOption,
verboseOption
};
listCommand.SetAction((parseResult, ct) =>
{
var format = parseResult.GetValue(formatOption) ?? "table";
var verbose = parseResult.GetValue(verboseOption);
var webhooks = new List<WebhookInfo>
{
new() { Id = "wh-001", Url = "https://api.example.com/vex-events", Events = ["vex.created", "vex.updated"], Status = "Active", CreatedAt = DateTimeOffset.UtcNow.AddDays(-30) },
new() { Id = "wh-002", Url = "https://slack.webhook.example.com/vex", Events = ["vex.created"], Status = "Active", CreatedAt = DateTimeOffset.UtcNow.AddDays(-14) }
};
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(webhooks, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }));
return Task.FromResult(0);
}
Console.WriteLine("VEX Webhooks");
Console.WriteLine("============");
Console.WriteLine();
Console.WriteLine($"{"ID",-10} {"URL",-45} {"Events",-25} {"Status",-8}");
Console.WriteLine(new string('-', 95));
foreach (var wh in webhooks)
{
var urlTrunc = wh.Url.Length > 43 ? wh.Url[..43] + ".." : wh.Url;
var events = string.Join(",", wh.Events);
Console.WriteLine($"{wh.Id,-10} {urlTrunc,-45} {events,-25} {wh.Status,-8}");
}
Console.WriteLine();
Console.WriteLine($"Total: {webhooks.Count} webhooks");
return Task.FromResult(0);
});
return listCommand;
}
private static Command BuildWebhooksAddCommand(Option<bool> verboseOption)
{
var urlOption = new Option<string>("--url", ["-u"])
{
Description = "Webhook URL",
Required = true
};
var eventsOption = new Option<string[]>("--events", ["-e"])
{
Description = "Event types to subscribe to (vex.created, vex.updated, vex.revoked)",
Required = true
};
eventsOption.AllowMultipleArgumentsPerToken = true;
var secretOption = new Option<string?>("--secret", ["-s"])
{
Description = "Shared secret for webhook signature verification"
};
var nameOption = new Option<string?>("--name", ["-n"])
{
Description = "Friendly name for the webhook"
};
var addCommand = new Command("add", "Register a new VEX webhook")
{
urlOption,
eventsOption,
secretOption,
nameOption,
verboseOption
};
addCommand.SetAction((parseResult, ct) =>
{
var url = parseResult.GetValue(urlOption) ?? string.Empty;
var events = parseResult.GetValue(eventsOption) ?? [];
var secret = parseResult.GetValue(secretOption);
var name = parseResult.GetValue(nameOption);
var verbose = parseResult.GetValue(verboseOption);
var newId = $"wh-{Guid.NewGuid().ToString()[..8]}";
Console.WriteLine("Webhook registered successfully");
Console.WriteLine();
Console.WriteLine($"ID: {newId}");
Console.WriteLine($"URL: {url}");
Console.WriteLine($"Events: {string.Join(", ", events)}");
if (!string.IsNullOrEmpty(name))
{
Console.WriteLine($"Name: {name}");
}
if (!string.IsNullOrEmpty(secret))
{
Console.WriteLine($"Secret: ****{secret[^4..]}");
}
return Task.FromResult(0);
});
return addCommand;
}
private static Command BuildWebhooksRemoveCommand(Option<bool> verboseOption)
{
var idArg = new Argument<string>("id")
{
Description = "Webhook ID to remove"
};
var forceOption = new Option<bool>("--force", ["-f"])
{
Description = "Force removal without confirmation"
};
var removeCommand = new Command("remove", "Unregister a VEX webhook")
{
idArg,
forceOption,
verboseOption
};
removeCommand.SetAction((parseResult, ct) =>
{
var id = parseResult.GetValue(idArg) ?? string.Empty;
var force = parseResult.GetValue(forceOption);
var verbose = parseResult.GetValue(verboseOption);
Console.WriteLine($"Webhook {id} removed successfully");
return Task.FromResult(0);
});
return removeCommand;
}
private sealed class WebhookInfo
{
public string Id { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public string[] Events { get; set; } = [];
public string Status { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
}
#endregion
}