sprints work
This commit is contained in:
@@ -363,11 +363,107 @@ internal static class CommandFactory
|
||||
|
||||
scan.Add(sarifExport);
|
||||
|
||||
// Replay command with explicit hashes (Task RCG-9200-021 through RCG-9200-024)
|
||||
var replay = BuildScanReplayCommand(services, verboseOption, cancellationToken);
|
||||
scan.Add(replay);
|
||||
|
||||
scan.Add(run);
|
||||
scan.Add(upload);
|
||||
return scan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the scan replay subcommand for deterministic verdict replay.
|
||||
/// </summary>
|
||||
private static Command BuildScanReplayCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var replay = new Command("replay", "Replay a scan with explicit hashes for deterministic verdict reproduction.");
|
||||
|
||||
// Required options for deterministic replay
|
||||
var artifactOption = new Option<string>("--artifact")
|
||||
{
|
||||
Description = "Artifact digest (sha256:...) to replay.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var manifestOption = new Option<string>("--manifest")
|
||||
{
|
||||
Description = "Run manifest hash for configuration.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var feedsOption = new Option<string>("--feeds")
|
||||
{
|
||||
Description = "Feed snapshot hash.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
var policyOption = new Option<string>("--policy")
|
||||
{
|
||||
Description = "Policy ruleset hash.",
|
||||
Required = true
|
||||
};
|
||||
|
||||
// Optional options
|
||||
var snapshotOption = new Option<string?>("--snapshot")
|
||||
{
|
||||
Description = "Knowledge snapshot ID for offline replay."
|
||||
};
|
||||
|
||||
var offlineOption = new Option<bool>("--offline")
|
||||
{
|
||||
Description = "Run in offline/air-gapped mode. Requires all inputs to be locally available."
|
||||
};
|
||||
|
||||
var verifyInputsOption = new Option<bool>("--verify-inputs")
|
||||
{
|
||||
Description = "Verify all input hashes before starting replay."
|
||||
};
|
||||
|
||||
var outputOption = new Option<string?>("--output", new[] { "-o" })
|
||||
{
|
||||
Description = "Output file path for verdict JSON (defaults to stdout)."
|
||||
};
|
||||
|
||||
replay.Add(artifactOption);
|
||||
replay.Add(manifestOption);
|
||||
replay.Add(feedsOption);
|
||||
replay.Add(policyOption);
|
||||
replay.Add(snapshotOption);
|
||||
replay.Add(offlineOption);
|
||||
replay.Add(verifyInputsOption);
|
||||
replay.Add(outputOption);
|
||||
replay.Add(verboseOption);
|
||||
|
||||
replay.SetAction(async (parseResult, _) =>
|
||||
{
|
||||
var artifact = parseResult.GetValue(artifactOption) ?? string.Empty;
|
||||
var manifest = parseResult.GetValue(manifestOption) ?? string.Empty;
|
||||
var feeds = parseResult.GetValue(feedsOption) ?? string.Empty;
|
||||
var policy = parseResult.GetValue(policyOption) ?? string.Empty;
|
||||
var snapshot = parseResult.GetValue(snapshotOption);
|
||||
var offline = parseResult.GetValue(offlineOption);
|
||||
var verifyInputs = parseResult.GetValue(verifyInputsOption);
|
||||
var output = parseResult.GetValue(outputOption);
|
||||
var verbose = parseResult.GetValue(verboseOption);
|
||||
|
||||
return await CommandHandlers.HandleScanReplayAsync(
|
||||
services,
|
||||
artifact,
|
||||
manifest,
|
||||
feeds,
|
||||
policy,
|
||||
snapshot,
|
||||
offline,
|
||||
verifyInputs,
|
||||
output,
|
||||
verbose,
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
return replay;
|
||||
}
|
||||
|
||||
private static Command BuildRubyCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
|
||||
{
|
||||
var ruby = new Command("ruby", "Work with Ruby analyzer outputs.");
|
||||
|
||||
@@ -800,6 +800,181 @@ internal static partial class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle scan replay command for deterministic verdict reproduction.
|
||||
/// Task: RCG-9200-021 through RCG-9200-024
|
||||
/// </summary>
|
||||
public static async Task<int> HandleScanReplayAsync(
|
||||
IServiceProvider services,
|
||||
string artifact,
|
||||
string manifest,
|
||||
string feeds,
|
||||
string policy,
|
||||
string? snapshot,
|
||||
bool offline,
|
||||
bool verifyInputs,
|
||||
string? outputPath,
|
||||
bool verbose,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("scan-replay");
|
||||
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
|
||||
var previousLevel = verbosity.MinimumLevel;
|
||||
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
|
||||
using var activity = CliActivitySource.Instance.StartActivity("cli.scan.replay", ActivityKind.Client);
|
||||
activity?.SetTag("stellaops.cli.command", "scan replay");
|
||||
activity?.SetTag("stellaops.cli.artifact", artifact);
|
||||
activity?.SetTag("stellaops.cli.manifest", manifest);
|
||||
activity?.SetTag("stellaops.cli.offline", offline);
|
||||
using var duration = CliMetrics.MeasureCommandDuration("scan replay");
|
||||
|
||||
try
|
||||
{
|
||||
// Display input hashes for confirmation
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[bold]Replay Configuration[/]");
|
||||
AnsiConsole.MarkupLine($" Artifact: [cyan]{Markup.Escape(artifact)}[/]");
|
||||
AnsiConsole.MarkupLine($" Manifest: [cyan]{Markup.Escape(manifest)}[/]");
|
||||
AnsiConsole.MarkupLine($" Feeds: [cyan]{Markup.Escape(feeds)}[/]");
|
||||
AnsiConsole.MarkupLine($" Policy: [cyan]{Markup.Escape(policy)}[/]");
|
||||
if (!string.IsNullOrEmpty(snapshot))
|
||||
{
|
||||
AnsiConsole.MarkupLine($" Snapshot: [cyan]{Markup.Escape(snapshot)}[/]");
|
||||
}
|
||||
AnsiConsole.MarkupLine($" Mode: [cyan]{(offline ? "offline" : "online")}[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
}
|
||||
|
||||
// Verify input hashes if requested
|
||||
if (verifyInputs)
|
||||
{
|
||||
logger.LogInformation("Verifying input hashes before replay...");
|
||||
var hashVerificationFailed = false;
|
||||
|
||||
// Validate artifact digest format
|
||||
if (!artifact.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) &&
|
||||
!artifact.StartsWith("sha512:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Artifact digest must start with sha256: or sha512:");
|
||||
hashVerificationFailed = true;
|
||||
}
|
||||
|
||||
// Validate hash lengths (SHA256 = 64 hex chars, SHA512 = 128 hex chars)
|
||||
var manifestHashLength = manifest.Length;
|
||||
if (manifestHashLength != 64 && manifestHashLength != 128)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Error:[/] Manifest hash has invalid length. Expected 64 (SHA256) or 128 (SHA512) characters.");
|
||||
hashVerificationFailed = true;
|
||||
}
|
||||
|
||||
if (hashVerificationFailed)
|
||||
{
|
||||
Environment.ExitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine("[green]✓[/] Input hash format verified");
|
||||
}
|
||||
|
||||
// In offline mode, verify all inputs are locally available
|
||||
if (offline)
|
||||
{
|
||||
logger.LogInformation("Running in offline mode. Checking local availability...");
|
||||
|
||||
// TODO: Implement actual offline verification
|
||||
// For now, just log that we're in offline mode
|
||||
AnsiConsole.MarkupLine("[yellow]Note:[/] Offline mode requires all inputs to be cached locally.");
|
||||
AnsiConsole.MarkupLine(" Use 'stella offline prepare' to pre-fetch required data.");
|
||||
}
|
||||
|
||||
// Build the replay result
|
||||
var replayResult = new ScanReplayResult
|
||||
{
|
||||
Status = "pending",
|
||||
ArtifactDigest = artifact,
|
||||
ManifestHash = manifest,
|
||||
FeedSnapshotHash = feeds,
|
||||
PolicyHash = policy,
|
||||
KnowledgeSnapshotId = snapshot,
|
||||
OfflineMode = offline,
|
||||
StartedAt = DateTimeOffset.UtcNow,
|
||||
Message = "Replay execution not yet implemented. Use 'stella replay --manifest <file>' for manifest-based replay."
|
||||
};
|
||||
|
||||
// Note: Full replay execution requires integration with ReplayRunner service
|
||||
// For now, output the configuration and a message directing to existing replay
|
||||
logger.LogWarning("Full scan replay with explicit hashes is not yet implemented.");
|
||||
logger.LogInformation("Use 'stella replay --manifest <file>' for manifest-based replay.");
|
||||
|
||||
var resultJson = JsonSerializer.Serialize(replayResult, JsonOptions);
|
||||
|
||||
if (!string.IsNullOrEmpty(outputPath))
|
||||
{
|
||||
await File.WriteAllTextAsync(outputPath, resultJson, cancellationToken).ConfigureAwait(false);
|
||||
AnsiConsole.MarkupLine($"[green]Replay result written to {Markup.Escape(outputPath)}[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(resultJson);
|
||||
}
|
||||
|
||||
Environment.ExitCode = 0;
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to execute scan replay.");
|
||||
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
|
||||
Environment.ExitCode = 1;
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
verbosity.MinimumLevel = previousLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of scan replay operation.
|
||||
/// </summary>
|
||||
private sealed record ScanReplayResult
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public required string Status { get; init; }
|
||||
|
||||
[JsonPropertyName("artifactDigest")]
|
||||
public required string ArtifactDigest { get; init; }
|
||||
|
||||
[JsonPropertyName("manifestHash")]
|
||||
public required string ManifestHash { get; init; }
|
||||
|
||||
[JsonPropertyName("feedSnapshotHash")]
|
||||
public required string FeedSnapshotHash { get; init; }
|
||||
|
||||
[JsonPropertyName("policyHash")]
|
||||
public required string PolicyHash { get; init; }
|
||||
|
||||
[JsonPropertyName("knowledgeSnapshotId")]
|
||||
public string? KnowledgeSnapshotId { get; init; }
|
||||
|
||||
[JsonPropertyName("offlineMode")]
|
||||
public bool OfflineMode { get; init; }
|
||||
|
||||
[JsonPropertyName("startedAt")]
|
||||
public DateTimeOffset StartedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("completedAt")]
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("verdict")]
|
||||
public object? Verdict { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
}
|
||||
|
||||
public static async Task HandleScanUploadAsync(
|
||||
IServiceProvider services,
|
||||
string file,
|
||||
|
||||
Reference in New Issue
Block a user