sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 21:46:08 +02:00
parent 43e2af88f6
commit b9f71fc7e9
161 changed files with 29566 additions and 527 deletions

View File

@@ -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.");

View File

@@ -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,