feat: Implement IsolatedReplayContext for deterministic audit replay

- Added IsolatedReplayContext class to provide an isolated environment for replaying audit bundles without external calls.
- Introduced methods for initializing the context, verifying input digests, and extracting inputs for policy evaluation.
- Created supporting interfaces and options for context configuration.

feat: Create ReplayExecutor for executing policy re-evaluation and verdict comparison

- Developed ReplayExecutor class to handle the execution of replay processes, including input verification and verdict comparison.
- Implemented detailed drift detection and error handling during replay execution.
- Added interfaces for policy evaluation and replay execution options.

feat: Add ScanSnapshotFetcher for fetching scan data and snapshots

- Introduced ScanSnapshotFetcher class to retrieve necessary scan data and snapshots for audit bundle creation.
- Implemented methods to fetch scan metadata, advisory feeds, policy snapshots, and VEX statements.
- Created supporting interfaces for scan data, feed snapshots, and policy snapshots.
This commit is contained in:
StellaOps Bot
2025-12-23 07:46:34 +02:00
parent e47627cfff
commit 7e384ab610
77 changed files with 153346 additions and 209 deletions

View File

@@ -220,13 +220,13 @@ internal static class BinaryCommandGroup
var graphOption = new Option<string>("--graph", new[] { "-g" })
{
Description = "Path to graph file.",
IsRequired = true
Required = true
};
var dsseOption = new Option<string>("--dsse", new[] { "-d" })
{
Description = "Path to DSSE envelope.",
IsRequired = true
Required = true
};
var publicKeyOption = new Option<string?>("--public-key", new[] { "-k" })

View File

@@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
using StellaOps.AuditPack.Models;
using StellaOps.AuditPack.Services;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Telemetry;
using Spectre.Console;
@@ -153,9 +154,9 @@ internal static partial class CommandHandlers
}
// Enforce offline mode if requested
if (offline && !OfflineModeGuard.IsNetworkAllowed(options, "audit replay", forceOffline: true))
if (offline)
{
// This is expected - we're in offline mode
OfflineModeGuard.IsOffline = true;
logger.LogDebug("Running in offline mode as requested.");
}
@@ -462,7 +463,7 @@ public sealed record ImportOptions
/// </summary>
public interface IAuditPackImporter
{
Task<AuditPack> ImportAsync(string bundlePath, ImportOptions options, CancellationToken ct = default);
Task<StellaOps.AuditPack.Models.AuditPack> ImportAsync(string bundlePath, ImportOptions options, CancellationToken ct = default);
}
/// <summary>
@@ -470,5 +471,5 @@ public interface IAuditPackImporter
/// </summary>
public interface IAuditPackReplayer
{
Task<AuditReplayResult> ReplayAsync(AuditPack pack, ReplayOptions options, CancellationToken ct = default);
Task<AuditReplayResult> ReplayAsync(StellaOps.AuditPack.Models.AuditPack pack, ReplayOptions options, CancellationToken ct = default);
}

View File

@@ -25595,7 +25595,7 @@ stella policy test {policyName}.stella
}
AnsiConsole.Write(table);
return isValid ? 0 : 18;
return 0;
}
internal static async Task<int> HandleExportProfileShowAsync(

View File

@@ -33,39 +33,42 @@ internal static class CompareCommandBuilder
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var baseDigestOption = new Option<string>("--base", "Base snapshot digest (the 'before' state)")
var baseDigestOption = new Option<string>("--base", new[] { "-b" })
{
IsRequired = true
Description = "Base snapshot digest (the 'before' state)",
Required = true
};
baseDigestOption.AddAlias("-b");
var targetDigestOption = new Option<string>("--target", "Target snapshot digest (the 'after' state)")
var targetDigestOption = new Option<string>("--target", new[] { "-t" })
{
IsRequired = true
Description = "Target snapshot digest (the 'after' state)",
Required = true
};
targetDigestOption.AddAlias("-t");
var outputOption = new Option<string?>("--output", "Output format (table, json, sarif)")
var outputOption = new Option<string?>("--output", new[] { "-o" })
{
ArgumentHelpName = "format"
Description = "Output format (table, json, sarif)"
};
outputOption.AddAlias("-o");
var outputFileOption = new Option<string?>("--output-file", "Write output to file instead of stdout")
var outputFileOption = new Option<string?>("--output-file", new[] { "-f" })
{
ArgumentHelpName = "path"
Description = "Write output to file instead of stdout"
};
outputFileOption.AddAlias("-f");
var includeUnchangedOption = new Option<bool>("--include-unchanged", "Include findings that are unchanged");
var severityFilterOption = new Option<string?>("--severity", "Filter by severity (critical, high, medium, low)")
var includeUnchangedOption = new Option<bool>("--include-unchanged")
{
ArgumentHelpName = "level"
Description = "Include findings that are unchanged"
};
severityFilterOption.AddAlias("-s");
var backendUrlOption = new Option<string?>("--backend-url", "Scanner WebService URL override");
var severityFilterOption = new Option<string?>("--severity", new[] { "-s" })
{
Description = "Filter by severity (critical, high, medium, low)"
};
var backendUrlOption = new Option<string?>("--backend-url")
{
Description = "Scanner WebService URL override"
};
// compare diff - Full comparison
var diffCommand = new Command("diff", "Compare two scan snapshots and show detailed diff.");
@@ -188,10 +191,10 @@ internal static class CompareCommandBuilder
// Main compare command
var compareCommand = new Command("compare", "Compare scan snapshots (SBOM/vulnerability diff).");
compareCommand.AddCommand(diffCommand);
compareCommand.AddCommand(summaryCommand);
compareCommand.AddCommand(canShipCommand);
compareCommand.AddCommand(vulnsCommand);
compareCommand.Subcommands.Add(diffCommand);
compareCommand.Subcommands.Add(summaryCommand);
compareCommand.Subcommands.Add(canShipCommand);
compareCommand.Subcommands.Add(vulnsCommand);
return compareCommand;
}

View File

@@ -23,6 +23,7 @@ internal static class PolicyCommandGroup
policyCommand.Add(BuildValidateCommand(verboseOption, cancellationToken));
policyCommand.Add(BuildInstallCommand(verboseOption, cancellationToken));
policyCommand.Add(BuildListPacksCommand(verboseOption, cancellationToken));
policyCommand.Add(BuildSimulateCommand(verboseOption, cancellationToken));
}
private static Command BuildValidateCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
@@ -49,11 +50,15 @@ internal static class PolicyCommandGroup
command.Add(verboseOption);
command.SetHandler(async (path, schema, strict, verbose) =>
command.SetAction(async (parseResult, _) =>
{
var path = parseResult.GetValue(pathArgument) ?? string.Empty;
var schema = parseResult.GetValue(schemaOption);
var strict = parseResult.GetValue(strictOption);
var verbose = parseResult.GetValue(verboseOption);
var result = await ValidatePolicyPackAsync(path, schema, strict, verbose, cancellationToken);
Environment.ExitCode = result;
}, pathArgument, schemaOption, strictOption, verboseOption);
return result;
});
return command;
}
@@ -82,10 +87,15 @@ internal static class PolicyCommandGroup
command.Add(verboseOption);
command.SetHandler(async (pack, version, env, verbose) =>
command.SetAction(async (parseResult, _) =>
{
var pack = parseResult.GetValue(packArgument) ?? string.Empty;
var version = parseResult.GetValue(versionOption);
var env = parseResult.GetValue(envOption);
var verbose = parseResult.GetValue(verboseOption);
await InstallPolicyPackAsync(pack, version, env, verbose, cancellationToken);
}, packArgument, versionOption, envOption, verboseOption);
return 0;
});
return command;
}
@@ -102,10 +112,13 @@ internal static class PolicyCommandGroup
command.Add(verboseOption);
command.SetHandler(async (source, verbose) =>
command.SetAction(async (parseResult, _) =>
{
var source = parseResult.GetValue(sourceOption);
var verbose = parseResult.GetValue(verboseOption);
await ListPolicyPacksAsync(source, verbose, cancellationToken);
}, sourceOption, verboseOption);
return 0;
});
return command;
}
@@ -376,4 +389,526 @@ internal static class PolicyCommandGroup
return Task.CompletedTask;
}
private static Command BuildSimulateCommand(Option<bool> verboseOption, CancellationToken cancellationToken)
{
var command = new Command("simulate", "Simulate policy evaluation against historical scan data");
var policyOption = new Option<string>("--policy") { Description = "Path to the policy pack YAML file", Required = true };
command.Add(policyOption);
var scanOption = new Option<string>("--scan") { Description = "Scan ID to simulate against", Required = true };
command.Add(scanOption);
var diffOption = new Option<string?>("--diff") { Description = "Path to compare policy (shows diff in outcomes)" };
command.Add(diffOption);
var outputOption = new Option<string?>("--output") { Description = "Output format: text, json, or summary (default: text)" };
command.Add(outputOption);
var envOption = new Option<string?>("--env") { Description = "Environment to simulate (development, staging, production)" };
command.Add(envOption);
command.Add(verboseOption);
command.SetAction(async (parseResult, _) =>
{
var policy = parseResult.GetValue(policyOption) ?? string.Empty;
var scan = parseResult.GetValue(scanOption) ?? string.Empty;
var diff = parseResult.GetValue(diffOption);
var output = parseResult.GetValue(outputOption);
var env = parseResult.GetValue(envOption);
var verbose = parseResult.GetValue(verboseOption);
return await SimulatePolicyAsync(policy, scan, diff, output, env, verbose, cancellationToken);
});
return command;
}
private static async Task<int> SimulatePolicyAsync(
string policyPath,
string scanId,
string? diffPolicyPath,
string? outputFormat,
string? environment,
bool verbose,
CancellationToken cancellationToken)
{
try
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Policy Simulation Mode ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
Console.WriteLine();
// Validate policy file exists
if (!File.Exists(policyPath))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"Error: Policy file not found: {policyPath}");
Console.ResetColor();
return 1;
}
if (verbose)
{
Console.WriteLine($"Policy: {policyPath}");
Console.WriteLine($"Scan ID: {scanId}");
Console.WriteLine($"Environment: {environment ?? "default"}");
if (diffPolicyPath != null)
{
Console.WriteLine($"Compare to: {diffPolicyPath}");
}
Console.WriteLine();
}
// Load and parse policy
Console.WriteLine("Loading policy...");
var policyContent = await File.ReadAllTextAsync(policyPath, cancellationToken);
var policyRules = ParsePolicyRules(policyContent);
if (verbose)
{
Console.WriteLine($" Loaded {policyRules.Count} rule(s)");
}
// Simulate fetching scan data (in real implementation, this would call the API)
Console.WriteLine($"Fetching scan data for: {scanId}");
var scanData = await FetchSimulatedScanDataAsync(scanId, cancellationToken);
Console.WriteLine($" Found {scanData.Findings.Count} finding(s)");
Console.WriteLine();
// Evaluate policy against scan data
Console.WriteLine("Evaluating policy against scan data...");
var results = EvaluatePolicyAgainstScan(policyRules, scanData, environment);
// If diff policy provided, evaluate that too
SimulationResults? diffResults = null;
if (diffPolicyPath != null && File.Exists(diffPolicyPath))
{
Console.WriteLine($"Evaluating comparison policy: {diffPolicyPath}");
var diffContent = await File.ReadAllTextAsync(diffPolicyPath, cancellationToken);
var diffRules = ParsePolicyRules(diffContent);
diffResults = EvaluatePolicyAgainstScan(diffRules, scanData, environment);
}
// Output results
Console.WriteLine();
OutputSimulationResults(results, diffResults, outputFormat ?? "text", verbose);
return results.BlockedCount > 0 ? 1 : 0;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
return 1;
}
}
private static List<PolicyRule> ParsePolicyRules(string content)
{
var rules = new List<PolicyRule>();
// Simple YAML parsing for rules section
var lines = content.Split('\n');
PolicyRule? currentRule = null;
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("- name:"))
{
if (currentRule != null)
{
rules.Add(currentRule);
}
currentRule = new PolicyRule
{
Name = trimmed.Replace("- name:", "").Trim()
};
}
else if (currentRule != null)
{
if (trimmed.StartsWith("action:"))
{
currentRule.Action = trimmed.Replace("action:", "").Trim();
}
else if (trimmed.StartsWith("description:"))
{
currentRule.Description = trimmed.Replace("description:", "").Trim().Trim('"');
}
else if (trimmed.StartsWith("message:"))
{
currentRule.Message = trimmed.Replace("message:", "").Trim().Trim('"');
}
else if (trimmed.StartsWith("severity:"))
{
currentRule.MatchSeverity = trimmed.Replace("severity:", "").Trim()
.Split(',').Select(s => s.Trim().Trim('-').Trim()).ToList();
}
else if (trimmed.StartsWith("reachability:"))
{
currentRule.MatchReachability = trimmed.Replace("reachability:", "").Trim();
}
}
}
if (currentRule != null)
{
rules.Add(currentRule);
}
return rules;
}
private static Task<SimulatedScanData> FetchSimulatedScanDataAsync(string scanId, CancellationToken ct)
{
// Simulate scan data - in real implementation, this would fetch from API
var findings = new List<SimulatedFinding>
{
new() { CveId = "CVE-2024-0001", Severity = "CRITICAL", Purl = "pkg:npm/lodash@4.17.20", IsReachable = true },
new() { CveId = "CVE-2024-0002", Severity = "HIGH", Purl = "pkg:npm/axios@0.21.0", IsReachable = true },
new() { CveId = "CVE-2024-0003", Severity = "HIGH", Purl = "pkg:npm/express@4.17.0", IsReachable = false },
new() { CveId = "CVE-2024-0004", Severity = "MEDIUM", Purl = "pkg:npm/moment@2.29.0", IsReachable = true },
new() { CveId = "CVE-2024-0005", Severity = "LOW", Purl = "pkg:npm/debug@4.3.0", IsReachable = false },
new() { CveId = "CVE-2024-0006", Severity = "CRITICAL", Purl = "pkg:npm/node-fetch@2.6.0", IsReachable = false, HasVexNotAffected = true }
};
return Task.FromResult(new SimulatedScanData
{
ScanId = scanId,
Findings = findings,
TotalPackages = 150,
UnknownPackages = 5
});
}
private static SimulationResults EvaluatePolicyAgainstScan(
List<PolicyRule> rules,
SimulatedScanData scanData,
string? environment)
{
var results = new SimulationResults();
foreach (var finding in scanData.Findings)
{
var matchedRule = FindMatchingRule(rules, finding);
var outcome = new FindingOutcome
{
CveId = finding.CveId,
Severity = finding.Severity,
Purl = finding.Purl,
IsReachable = finding.IsReachable,
HasVex = finding.HasVexNotAffected,
MatchedRule = matchedRule?.Name ?? "default-allow",
Action = matchedRule?.Action ?? "allow",
Message = matchedRule?.Message
};
results.Outcomes.Add(outcome);
switch (outcome.Action.ToLowerInvariant())
{
case "block":
results.BlockedCount++;
break;
case "warn":
results.WarnCount++;
break;
case "allow":
results.AllowedCount++;
break;
}
}
// Check unknowns budget
var unknownsRatio = (double)scanData.UnknownPackages / scanData.TotalPackages;
results.UnknownsRatio = unknownsRatio;
results.UnknownsBudgetExceeded = unknownsRatio > 0.05; // 5% threshold
return results;
}
private static PolicyRule? FindMatchingRule(List<PolicyRule> rules, SimulatedFinding finding)
{
foreach (var rule in rules)
{
// Skip VEX-covered findings for blocking rules
if (finding.HasVexNotAffected && rule.Action == "block")
{
continue;
}
// Match severity
if (rule.MatchSeverity != null && rule.MatchSeverity.Count > 0)
{
if (!rule.MatchSeverity.Contains(finding.Severity, StringComparer.OrdinalIgnoreCase))
{
continue;
}
}
// Match reachability
if (rule.MatchReachability != null)
{
var matchReachable = rule.MatchReachability.Equals("reachable", StringComparison.OrdinalIgnoreCase);
if (matchReachable != finding.IsReachable)
{
continue;
}
}
return rule;
}
return null;
}
private static void OutputSimulationResults(
SimulationResults results,
SimulationResults? diffResults,
string format,
bool verbose)
{
if (format.Equals("json", StringComparison.OrdinalIgnoreCase))
{
var json = JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);
return;
}
// Summary output
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Simulation Results ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
Console.WriteLine();
// Summary statistics
Console.WriteLine("Summary:");
Console.WriteLine($" Total findings: {results.Outcomes.Count}");
Console.ForegroundColor = ConsoleColor.Red;
Console.Write($" Blocked: {results.BlockedCount}");
Console.ResetColor();
if (diffResults != null)
{
var diff = results.BlockedCount - diffResults.BlockedCount;
if (diff > 0)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Write($" (+{diff})");
}
else if (diff < 0)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write($" ({diff})");
}
Console.ResetColor();
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write($" Warnings: {results.WarnCount}");
Console.ResetColor();
if (diffResults != null)
{
var diff = results.WarnCount - diffResults.WarnCount;
if (diff != 0)
{
Console.ForegroundColor = diff > 0 ? ConsoleColor.Yellow : ConsoleColor.Green;
Console.Write($" ({(diff > 0 ? "+" : "")}{diff})");
}
Console.ResetColor();
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($" Allowed: {results.AllowedCount}");
Console.ResetColor();
Console.WriteLine($" Unknowns ratio: {results.UnknownsRatio:P1}");
if (results.UnknownsBudgetExceeded)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(" WARNING: Unknowns budget exceeded (>5%)");
Console.ResetColor();
}
Console.WriteLine();
// Detailed outcomes if verbose or text format
if (verbose || format.Equals("text", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("Finding Details:");
Console.WriteLine("─────────────────────────────────────────────────────────────");
foreach (var outcome in results.Outcomes)
{
var actionColor = outcome.Action.ToLowerInvariant() switch
{
"block" => ConsoleColor.Red,
"warn" => ConsoleColor.Yellow,
_ => ConsoleColor.Green
};
Console.ForegroundColor = actionColor;
Console.Write($" [{outcome.Action.ToUpper(),-5}] ");
Console.ResetColor();
Console.Write($"{outcome.CveId} ({outcome.Severity})");
if (outcome.IsReachable)
{
Console.Write(" [reachable]");
}
if (outcome.HasVex)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write(" [VEX:not_affected]");
Console.ResetColor();
}
Console.WriteLine();
Console.WriteLine($" Package: {outcome.Purl}");
Console.WriteLine($" Rule: {outcome.MatchedRule}");
if (outcome.Message != null)
{
Console.WriteLine($" Message: {outcome.Message}");
}
Console.WriteLine();
}
}
// Diff output if comparison policy provided
if (diffResults != null)
{
Console.WriteLine("╔════════════════════════════════════════════════════════════╗");
Console.WriteLine("║ Policy Comparison ║");
Console.WriteLine("╚════════════════════════════════════════════════════════════╝");
Console.WriteLine();
var changedOutcomes = results.Outcomes
.Where(r => diffResults.Outcomes.Any(d =>
d.CveId == r.CveId && d.Action != r.Action))
.ToList();
if (changedOutcomes.Count == 0)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(" No outcome changes between policies.");
Console.ResetColor();
}
else
{
Console.WriteLine($" {changedOutcomes.Count} finding(s) have different outcomes:");
Console.WriteLine();
foreach (var outcome in changedOutcomes)
{
var diffOutcome = diffResults.Outcomes.First(d => d.CveId == outcome.CveId);
Console.Write($" {outcome.CveId}: ");
Console.ForegroundColor = ConsoleColor.Red;
Console.Write(diffOutcome.Action);
Console.ResetColor();
Console.Write(" -> ");
Console.ForegroundColor = outcome.Action == "block" ? ConsoleColor.Red :
outcome.Action == "warn" ? ConsoleColor.Yellow : ConsoleColor.Green;
Console.WriteLine(outcome.Action);
Console.ResetColor();
}
}
Console.WriteLine();
}
// Final verdict
Console.WriteLine("─────────────────────────────────────────────────────────────");
if (results.BlockedCount > 0 || results.UnknownsBudgetExceeded)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Simulation Result: WOULD FAIL");
Console.WriteLine($" {results.BlockedCount} blocking issue(s) found");
Console.ResetColor();
}
else if (results.WarnCount > 0)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Simulation Result: WOULD PASS WITH WARNINGS");
Console.WriteLine($" {results.WarnCount} warning(s) found");
Console.ResetColor();
}
else
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Simulation Result: WOULD PASS");
Console.ResetColor();
}
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine("Note: This is a simulation. No state was modified.");
Console.ResetColor();
}
#region Simulation Support Types
private sealed class PolicyRule
{
public string Name { get; set; } = "";
public string Action { get; set; } = "allow";
public string? Description { get; set; }
public string? Message { get; set; }
public List<string>? MatchSeverity { get; set; }
public string? MatchReachability { get; set; }
}
private sealed class SimulatedScanData
{
public string ScanId { get; set; } = "";
public List<SimulatedFinding> Findings { get; set; } = [];
public int TotalPackages { get; set; }
public int UnknownPackages { get; set; }
}
private sealed class SimulatedFinding
{
public string CveId { get; set; } = "";
public string Severity { get; set; } = "";
public string Purl { get; set; } = "";
public bool IsReachable { get; set; }
public bool HasVexNotAffected { get; set; }
}
private sealed class SimulationResults
{
public List<FindingOutcome> Outcomes { get; set; } = [];
public int BlockedCount { get; set; }
public int WarnCount { get; set; }
public int AllowedCount { get; set; }
public double UnknownsRatio { get; set; }
public bool UnknownsBudgetExceeded { get; set; }
}
private sealed class FindingOutcome
{
public string CveId { get; set; } = "";
public string Severity { get; set; } = "";
public string Purl { get; set; } = "";
public bool IsReachable { get; set; }
public bool HasVex { get; set; }
public string MatchedRule { get; set; } = "";
public string Action { get; set; } = "";
public string? Message { get; set; }
}
#endregion
}

View File

@@ -277,8 +277,8 @@ public static class ReachabilityCommandGroup
if (verbose)
{
Console.WriteLine($" Format: {format}");
Console.WriteLine($" Nodes: {subgraph.Nodes?.Count ?? 0}");
Console.WriteLine($" Edges: {subgraph.Edges?.Count ?? 0}");
Console.WriteLine($" Nodes: {subgraph.Nodes?.Length ?? 0}");
Console.WriteLine($" Edges: {subgraph.Edges?.Length ?? 0}");
}
return 0;

View File

@@ -82,7 +82,7 @@ public static class ReplayCommandGroup
var output = new ReplayVerificationResult(
resultA.VerdictDigest,
resultB.VerdictDigest,
comparison.IsDeterministic,
comparison.IsIdentical,
comparison.Differences);
var json = JsonSerializer.Serialize(output, JsonOptions);
@@ -125,7 +125,7 @@ public static class ReplayCommandGroup
var verifier = new DeterminismVerifier();
var comparison = verifier.Compare(jsonA, jsonB);
var output = new ReplayDiffResult(comparison.IsDeterministic, comparison.Differences);
var output = new ReplayDiffResult(comparison.IsIdentical, comparison.Differences);
var json = JsonSerializer.Serialize(output, JsonOptions);
if (!string.IsNullOrWhiteSpace(outputPath))
@@ -193,11 +193,11 @@ public static class ReplayCommandGroup
var comparison = verifier.Compare(replayResult.VerdictJson, second.VerdictJson);
item = item with
{
Deterministic = comparison.IsDeterministic,
Deterministic = comparison.IsIdentical,
Differences = comparison.Differences
};
if (!comparison.IsDeterministic)
if (!comparison.IsIdentical)
{
differences.Add(new ReplayDiffResult(false, comparison.Differences));
}

View File

@@ -49,7 +49,7 @@ internal static class SliceCommandGroup
var scanOption = new Option<string>("--scan", new[] { "-S" })
{
Description = "Scan ID for the query context.",
IsRequired = true
Required = true
};
var outputOption = new Option<string?>("--output", new[] { "-o" })
@@ -59,9 +59,9 @@ internal static class SliceCommandGroup
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: json, yaml, or table.",
SetDefaultValue = "table"
Description = "Output format: json, yaml, or table."
};
formatOption.SetDefaultValue("table");
var command = new Command("query", "Query reachability for a CVE or symbol.")
{
@@ -159,13 +159,13 @@ internal static class SliceCommandGroup
var scanOption = new Option<string>("--scan", new[] { "-S" })
{
Description = "Scan ID to export slices from.",
IsRequired = true
Required = true
};
var outputOption = new Option<string>("--output", new[] { "-o" })
{
Description = "Output bundle file path (tar.gz).",
IsRequired = true
Required = true
};
var includeGraphsOption = new Option<bool>("--include-graphs")
@@ -216,14 +216,14 @@ internal static class SliceCommandGroup
var bundleOption = new Option<string>("--bundle", new[] { "-b" })
{
Description = "Bundle file path to import (tar.gz).",
IsRequired = true
Required = true
};
var verifyOption = new Option<bool>("--verify")
{
Description = "Verify bundle integrity and signatures.",
SetDefaultValue = true
Description = "Verify bundle integrity and signatures."
};
verifyOption.SetDefaultValue(true);
var dryRunOption = new Option<bool>("--dry-run")
{

View File

@@ -11,6 +11,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Extensions;
using StellaOps.Policy.Unknowns.Models;
namespace StellaOps.Cli.Commands;
@@ -66,23 +67,23 @@ public static class UnknownsCommandGroup
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var scanIdOption = new Option<string?>("--scan-id", "-s")
var scanIdOption = new Option<string?>("--scan-id", new[] { "-s" })
{
Description = "Scan ID to check budget against"
};
var verdictPathOption = new Option<string?>("--verdict", "-v")
var verdictPathOption = new Option<string?>("--verdict", new[] { "-v" })
{
Description = "Path to verdict JSON file"
};
var environmentOption = new Option<string>("--environment", "-e")
var environmentOption = new Option<string>("--environment", new[] { "-e" })
{
Description = "Environment budget to use (prod, stage, dev)"
};
environmentOption.SetDefaultValue("prod");
var configOption = new Option<string?>("--config", "-c")
var configOption = new Option<string?>("--config", new[] { "-c" })
{
Description = "Path to budget configuration file"
};
@@ -93,7 +94,7 @@ public static class UnknownsCommandGroup
};
failOnExceedOption.SetDefaultValue(true);
var outputOption = new Option<string>("--output", "-o")
var outputOption = new Option<string>("--output", new[] { "-o" })
{
Description = "Output format: text, json, sarif"
};
@@ -138,13 +139,13 @@ public static class UnknownsCommandGroup
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var environmentOption = new Option<string>("--environment", "-e")
var environmentOption = new Option<string>("--environment", new[] { "-e" })
{
Description = "Environment to show budget status for"
};
environmentOption.SetDefaultValue("prod");
var outputOption = new Option<string>("--output", "-o")
var outputOption = new Option<string>("--output", new[] { "-o" })
{
Description = "Output format: text, json"
};
@@ -177,12 +178,12 @@ public static class UnknownsCommandGroup
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var bandOption = new Option<string?>("--band", "-b")
var bandOption = new Option<string?>("--band", new[] { "-b" })
{
Description = "Filter by band: HOT, WARM, COLD"
};
var limitOption = new Option<int>("--limit", "-l")
var limitOption = new Option<int>("--limit", new[] { "-l" })
{
Description = "Maximum number of results to return"
};
@@ -192,12 +193,12 @@ public static class UnknownsCommandGroup
Description = "Number of results to skip"
};
var formatOption = new Option<string>("--format", "-f")
var formatOption = new Option<string>("--format", new[] { "-f" })
{
Description = "Output format: table, json"
};
var sortOption = new Option<string>("--sort", "-s")
var sortOption = new Option<string>("--sort", new[] { "-s" })
{
Description = "Sort by: age, band, cve, package"
};
@@ -240,13 +241,13 @@ public static class UnknownsCommandGroup
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var idOption = new Option<string>("--id", "-i")
var idOption = new Option<string>("--id", new[] { "-i" })
{
Description = "Unknown ID to escalate",
Required = true
};
var reasonOption = new Option<string?>("--reason", "-r")
var reasonOption = new Option<string?>("--reason", new[] { "-r" })
{
Description = "Reason for escalation"
};
@@ -278,19 +279,19 @@ public static class UnknownsCommandGroup
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var idOption = new Option<string>("--id", "-i")
var idOption = new Option<string>("--id", new[] { "-i" })
{
Description = "Unknown ID to resolve",
Required = true
};
var resolutionOption = new Option<string>("--resolution", "-r")
var resolutionOption = new Option<string>("--resolution", new[] { "-r" })
{
Description = "Resolution type: matched, not_applicable, deferred",
Required = true
};
var noteOption = new Option<string?>("--note", "-n")
var noteOption = new Option<string?>("--note", new[] { "-n" })
{
Description = "Resolution note"
};