- Introduced `sink-detect.js` with various security sink detection patterns categorized by type (e.g., command injection, SQL injection, file operations). - Implemented functions to build a lookup map for fast sink detection and to match sink calls against known patterns. - Added `package-lock.json` for dependency management.
622 lines
22 KiB
C#
622 lines
22 KiB
C#
// -----------------------------------------------------------------------------
|
|
// CommandHandlers.VerdictVerify.cs
|
|
// Sprint: SPRINT_4300_0001_0001_oci_verdict_attestation_push
|
|
// Description: Command handlers for verdict verification operations.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Cli.Configuration;
|
|
using StellaOps.Cli.Services;
|
|
using StellaOps.Cli.Telemetry;
|
|
using Spectre.Console;
|
|
|
|
namespace StellaOps.Cli.Commands;
|
|
|
|
internal static partial class CommandHandlers
|
|
{
|
|
private static readonly JsonSerializerOptions VerdictJsonOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
|
};
|
|
|
|
internal static async Task<int> HandleVerdictVerifyAsync(
|
|
IServiceProvider services,
|
|
string reference,
|
|
string? sbomDigest,
|
|
string? feedsDigest,
|
|
string? policyDigest,
|
|
string? expectedDecision,
|
|
bool strict,
|
|
string? trustPolicy,
|
|
string output,
|
|
bool verbose,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var scope = services.CreateAsyncScope();
|
|
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
|
var logger = loggerFactory.CreateLogger("verdict-verify");
|
|
var options = scope.ServiceProvider.GetRequiredService<StellaOpsCliOptions>();
|
|
|
|
using var activity = CliActivitySource.Instance.StartActivity("cli.verdict.verify", ActivityKind.Client);
|
|
using var duration = CliMetrics.MeasureCommandDuration("verdict verify");
|
|
|
|
if (!OfflineModeGuard.IsNetworkAllowed(options, "verdict verify"))
|
|
{
|
|
WriteVerdictVerifyError("Offline mode enabled. Use offline evidence verification instead.", output);
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(reference))
|
|
{
|
|
WriteVerdictVerifyError("Image reference is required.", output);
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
try
|
|
{
|
|
var verifier = scope.ServiceProvider.GetRequiredService<IVerdictAttestationVerifier>();
|
|
var request = new VerdictVerificationRequest
|
|
{
|
|
Reference = reference,
|
|
ExpectedSbomDigest = sbomDigest,
|
|
ExpectedFeedsDigest = feedsDigest,
|
|
ExpectedPolicyDigest = policyDigest,
|
|
ExpectedDecision = expectedDecision,
|
|
Strict = strict,
|
|
TrustPolicyPath = trustPolicy
|
|
};
|
|
|
|
var result = await verifier.VerifyAsync(request, cancellationToken).ConfigureAwait(false);
|
|
WriteVerdictVerifyResult(result, output, verbose);
|
|
|
|
var exitCode = result.IsValid ? 0 : 1;
|
|
Environment.ExitCode = exitCode;
|
|
return exitCode;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Verdict verify failed for {Reference}", reference);
|
|
WriteVerdictVerifyError($"Verification failed: {ex.Message}", output);
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
internal static async Task<int> HandleVerdictListAsync(
|
|
IServiceProvider services,
|
|
string reference,
|
|
string output,
|
|
bool verbose,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var scope = services.CreateAsyncScope();
|
|
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
|
var logger = loggerFactory.CreateLogger("verdict-list");
|
|
var options = scope.ServiceProvider.GetRequiredService<StellaOpsCliOptions>();
|
|
|
|
using var activity = CliActivitySource.Instance.StartActivity("cli.verdict.list", ActivityKind.Client);
|
|
using var duration = CliMetrics.MeasureCommandDuration("verdict list");
|
|
|
|
if (!OfflineModeGuard.IsNetworkAllowed(options, "verdict list"))
|
|
{
|
|
WriteVerdictListError("Offline mode enabled. Use offline evidence verification instead.", output);
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(reference))
|
|
{
|
|
WriteVerdictListError("Image reference is required.", output);
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
try
|
|
{
|
|
var verifier = scope.ServiceProvider.GetRequiredService<IVerdictAttestationVerifier>();
|
|
var verdicts = await verifier.ListAsync(reference, cancellationToken).ConfigureAwait(false);
|
|
WriteVerdictListResult(reference, verdicts, output, verbose);
|
|
|
|
Environment.ExitCode = 0;
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Verdict list failed for {Reference}", reference);
|
|
WriteVerdictListError($"Failed to list verdicts: {ex.Message}", output);
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle verdict push command.
|
|
/// Sprint: SPRINT_4300_0001_0001, Task: VERDICT-013
|
|
/// </summary>
|
|
internal static async Task<int> HandleVerdictPushAsync(
|
|
IServiceProvider services,
|
|
string reference,
|
|
string? verdictFile,
|
|
string? registry,
|
|
bool insecure,
|
|
bool dryRun,
|
|
bool force,
|
|
int timeout,
|
|
bool verbose,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await using var scope = services.CreateAsyncScope();
|
|
var loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
|
|
var logger = loggerFactory.CreateLogger("verdict-push");
|
|
var options = scope.ServiceProvider.GetRequiredService<StellaOpsCliOptions>();
|
|
var console = AnsiConsole.Console;
|
|
|
|
using var activity = CliActivitySource.Instance.StartActivity("cli.verdict.push", ActivityKind.Client);
|
|
using var duration = CliMetrics.MeasureCommandDuration("verdict push");
|
|
|
|
if (!OfflineModeGuard.IsNetworkAllowed(options, "verdict push"))
|
|
{
|
|
console.MarkupLine("[red]Error:[/] Offline mode enabled. Cannot push verdicts.");
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(reference))
|
|
{
|
|
console.MarkupLine("[red]Error:[/] Image reference is required.");
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(verdictFile))
|
|
{
|
|
console.MarkupLine("[red]Error:[/] Verdict file path is required (--verdict-file).");
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
if (!File.Exists(verdictFile))
|
|
{
|
|
console.MarkupLine($"[red]Error:[/] Verdict file not found: {Markup.Escape(verdictFile)}");
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
|
|
try
|
|
{
|
|
var verifier = scope.ServiceProvider.GetRequiredService<IVerdictAttestationVerifier>();
|
|
|
|
if (verbose)
|
|
{
|
|
console.MarkupLine($"Reference: [bold]{Markup.Escape(reference)}[/]");
|
|
console.MarkupLine($"Verdict file: [bold]{Markup.Escape(verdictFile)}[/]");
|
|
if (!string.IsNullOrWhiteSpace(registry))
|
|
{
|
|
console.MarkupLine($"Registry override: [bold]{Markup.Escape(registry)}[/]");
|
|
}
|
|
if (dryRun)
|
|
{
|
|
console.MarkupLine("[yellow]Dry run mode - no changes will be made[/]");
|
|
}
|
|
}
|
|
|
|
var request = new VerdictPushRequest
|
|
{
|
|
Reference = reference,
|
|
VerdictFilePath = verdictFile,
|
|
Registry = registry,
|
|
Insecure = insecure,
|
|
DryRun = dryRun,
|
|
Force = force,
|
|
TimeoutSeconds = timeout
|
|
};
|
|
|
|
var result = await verifier.PushAsync(request, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (result.Success)
|
|
{
|
|
if (result.DryRun)
|
|
{
|
|
console.MarkupLine("[green]Dry run:[/] Verdict would be pushed successfully.");
|
|
}
|
|
else
|
|
{
|
|
console.MarkupLine("[green]Success:[/] Verdict pushed successfully.");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(result.VerdictDigest))
|
|
{
|
|
console.MarkupLine($"Verdict digest: [bold]{Markup.Escape(result.VerdictDigest)}[/]");
|
|
}
|
|
if (!string.IsNullOrWhiteSpace(result.ManifestDigest))
|
|
{
|
|
console.MarkupLine($"Manifest digest: [bold]{Markup.Escape(result.ManifestDigest)}[/]");
|
|
}
|
|
|
|
Environment.ExitCode = 0;
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
console.MarkupLine($"[red]Error:[/] {Markup.Escape(result.Error ?? "Push failed")}");
|
|
Environment.ExitCode = 1;
|
|
return 1;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Verdict push failed for {Reference}", reference);
|
|
console.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
|
|
Environment.ExitCode = 2;
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
private static void WriteVerdictVerifyResult(VerdictVerificationResult result, string output, bool verbose)
|
|
{
|
|
var console = AnsiConsole.Console;
|
|
|
|
switch (output)
|
|
{
|
|
case "json":
|
|
console.WriteLine(JsonSerializer.Serialize(result, VerdictJsonOptions));
|
|
break;
|
|
case "sarif":
|
|
console.WriteLine(JsonSerializer.Serialize(BuildVerdictSarif(result), VerdictJsonOptions));
|
|
break;
|
|
default:
|
|
WriteVerdictVerifyTable(console, result, verbose);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void WriteVerdictVerifyError(string message, string output)
|
|
{
|
|
var console = AnsiConsole.Console;
|
|
if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var payload = new { status = "error", message };
|
|
console.WriteLine(JsonSerializer.Serialize(payload, VerdictJsonOptions));
|
|
return;
|
|
}
|
|
|
|
if (string.Equals(output, "sarif", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var sarif = new
|
|
{
|
|
version = "2.1.0",
|
|
schema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
runs = new[]
|
|
{
|
|
new
|
|
{
|
|
tool = new { driver = new { name = "StellaOps Verdict Verify", version = "1.0.0" } },
|
|
results = new[]
|
|
{
|
|
new { level = "error", message = new { text = message } }
|
|
}
|
|
}
|
|
}
|
|
};
|
|
console.WriteLine(JsonSerializer.Serialize(sarif, VerdictJsonOptions));
|
|
return;
|
|
}
|
|
|
|
console.MarkupLine($"[red]Error:[/] {Markup.Escape(message)}");
|
|
}
|
|
|
|
private static void WriteVerdictVerifyTable(IAnsiConsole console, VerdictVerificationResult result, bool verbose)
|
|
{
|
|
console.MarkupLine($"Image: [bold]{Markup.Escape(result.ImageReference)}[/]");
|
|
console.MarkupLine($"Image Digest: [bold]{Markup.Escape(result.ImageDigest)}[/]");
|
|
console.WriteLine();
|
|
|
|
if (result.VerdictFound)
|
|
{
|
|
console.MarkupLine($"Verdict Found: [green]Yes[/]");
|
|
console.MarkupLine($"Verdict Digest: {Markup.Escape(result.VerdictDigest ?? "-")}");
|
|
console.MarkupLine($"Decision: {FormatDecision(result.Decision)}");
|
|
console.WriteLine();
|
|
|
|
var table = new Table().AddColumns("Input", "Expected", "Actual", "Match");
|
|
table.AddRow("SBOM Digest", result.ExpectedSbomDigest ?? "-", result.ActualSbomDigest ?? "-", FormatMatch(result.SbomDigestMatches));
|
|
table.AddRow("Feeds Digest", result.ExpectedFeedsDigest ?? "-", result.ActualFeedsDigest ?? "-", FormatMatch(result.FeedsDigestMatches));
|
|
table.AddRow("Policy Digest", result.ExpectedPolicyDigest ?? "-", result.ActualPolicyDigest ?? "-", FormatMatch(result.PolicyDigestMatches));
|
|
table.AddRow("Decision", result.ExpectedDecision ?? "-", result.Decision ?? "-", FormatMatch(result.DecisionMatches));
|
|
console.Write(table);
|
|
console.WriteLine();
|
|
|
|
if (result.SignatureValid.HasValue)
|
|
{
|
|
console.MarkupLine($"Signature: {(result.SignatureValid.Value ? "[green]VALID[/]" : "[red]INVALID[/]")}");
|
|
if (!string.IsNullOrWhiteSpace(result.SignerIdentity))
|
|
{
|
|
console.MarkupLine($"Signer: {Markup.Escape(result.SignerIdentity)}");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
console.MarkupLine($"Verdict Found: [yellow]No[/]");
|
|
}
|
|
|
|
console.WriteLine();
|
|
var headline = result.IsValid ? "[green]Verification PASSED[/]" : "[red]Verification FAILED[/]";
|
|
console.MarkupLine(headline);
|
|
|
|
if (verbose && result.Errors.Count > 0)
|
|
{
|
|
console.MarkupLine("[red]Errors:[/]");
|
|
foreach (var error in result.Errors)
|
|
{
|
|
console.MarkupLine($" - {Markup.Escape(error)}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void WriteVerdictListResult(string reference, IReadOnlyList<VerdictSummary> verdicts, string output, bool verbose)
|
|
{
|
|
var console = AnsiConsole.Console;
|
|
|
|
if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var payload = new { imageReference = reference, verdicts };
|
|
console.WriteLine(JsonSerializer.Serialize(payload, VerdictJsonOptions));
|
|
return;
|
|
}
|
|
|
|
console.MarkupLine($"Image: [bold]{Markup.Escape(reference)}[/]");
|
|
console.WriteLine();
|
|
|
|
if (verdicts.Count == 0)
|
|
{
|
|
console.MarkupLine("[yellow]No verdict attestations found.[/]");
|
|
return;
|
|
}
|
|
|
|
var table = new Table().AddColumns("Digest", "Decision", "Created", "SBOM Digest", "Feeds Digest");
|
|
foreach (var verdict in verdicts)
|
|
{
|
|
table.AddRow(
|
|
TruncateDigest(verdict.Digest),
|
|
FormatDecision(verdict.Decision),
|
|
verdict.CreatedAt?.ToString("u") ?? "-",
|
|
TruncateDigest(verdict.SbomDigest),
|
|
TruncateDigest(verdict.FeedsDigest));
|
|
}
|
|
|
|
console.Write(table);
|
|
console.MarkupLine($"\nTotal: [bold]{verdicts.Count}[/] verdict(s)");
|
|
}
|
|
|
|
private static void WriteVerdictListError(string message, string output)
|
|
{
|
|
var console = AnsiConsole.Console;
|
|
if (string.Equals(output, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var payload = new { status = "error", message };
|
|
console.WriteLine(JsonSerializer.Serialize(payload, VerdictJsonOptions));
|
|
return;
|
|
}
|
|
|
|
console.MarkupLine($"[red]Error:[/] {Markup.Escape(message)}");
|
|
}
|
|
|
|
private static string FormatDecision(string? decision) => decision?.ToLowerInvariant() switch
|
|
{
|
|
"pass" => "[green]PASS[/]",
|
|
"warn" => "[yellow]WARN[/]",
|
|
"block" => "[red]BLOCK[/]",
|
|
_ => decision ?? "-"
|
|
};
|
|
|
|
private static string FormatMatch(bool? matches) => matches switch
|
|
{
|
|
true => "[green]PASS[/]",
|
|
false => "[red]FAIL[/]",
|
|
null => "[dim]-[/]"
|
|
};
|
|
|
|
private static string TruncateDigest(string? digest)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(digest))
|
|
{
|
|
return "-";
|
|
}
|
|
|
|
if (digest.Length > 20)
|
|
{
|
|
return $"{digest[..17]}...";
|
|
}
|
|
|
|
return digest;
|
|
}
|
|
|
|
private static object BuildVerdictSarif(VerdictVerificationResult result)
|
|
{
|
|
var results = new List<object>();
|
|
|
|
if (result.VerdictFound)
|
|
{
|
|
results.Add(new
|
|
{
|
|
ruleId = "stellaops.verdict.found",
|
|
level = "note",
|
|
message = new { text = $"Verdict found with decision: {result.Decision}" },
|
|
properties = new
|
|
{
|
|
verdict_digest = result.VerdictDigest,
|
|
decision = result.Decision
|
|
}
|
|
});
|
|
|
|
if (!result.SbomDigestMatches.GetValueOrDefault(true))
|
|
{
|
|
results.Add(new
|
|
{
|
|
ruleId = "stellaops.verdict.sbom_mismatch",
|
|
level = "error",
|
|
message = new { text = "SBOM digest does not match expected value" }
|
|
});
|
|
}
|
|
|
|
if (!result.FeedsDigestMatches.GetValueOrDefault(true))
|
|
{
|
|
results.Add(new
|
|
{
|
|
ruleId = "stellaops.verdict.feeds_mismatch",
|
|
level = "error",
|
|
message = new { text = "Feeds digest does not match expected value" }
|
|
});
|
|
}
|
|
|
|
if (!result.PolicyDigestMatches.GetValueOrDefault(true))
|
|
{
|
|
results.Add(new
|
|
{
|
|
ruleId = "stellaops.verdict.policy_mismatch",
|
|
level = "error",
|
|
message = new { text = "Policy digest does not match expected value" }
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
results.Add(new
|
|
{
|
|
ruleId = "stellaops.verdict.missing",
|
|
level = "error",
|
|
message = new { text = "No verdict attestation found for image" }
|
|
});
|
|
}
|
|
|
|
return new
|
|
{
|
|
version = "2.1.0",
|
|
schema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
runs = new[]
|
|
{
|
|
new
|
|
{
|
|
tool = new { driver = new { name = "StellaOps Verdict Verify", version = "1.0.0" } },
|
|
results = results.ToArray()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for verdict verification.
|
|
/// </summary>
|
|
public sealed record VerdictVerificationRequest
|
|
{
|
|
public required string Reference { get; init; }
|
|
public string? ExpectedSbomDigest { get; init; }
|
|
public string? ExpectedFeedsDigest { get; init; }
|
|
public string? ExpectedPolicyDigest { get; init; }
|
|
public string? ExpectedDecision { get; init; }
|
|
public bool Strict { get; init; }
|
|
public string? TrustPolicyPath { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of verdict verification.
|
|
/// </summary>
|
|
public sealed record VerdictVerificationResult
|
|
{
|
|
public required string ImageReference { get; init; }
|
|
public required string ImageDigest { get; init; }
|
|
public required bool VerdictFound { get; init; }
|
|
public required bool IsValid { get; init; }
|
|
public string? VerdictDigest { get; init; }
|
|
public string? Decision { get; init; }
|
|
public string? ExpectedSbomDigest { get; init; }
|
|
public string? ActualSbomDigest { get; init; }
|
|
public bool? SbomDigestMatches { get; init; }
|
|
public string? ExpectedFeedsDigest { get; init; }
|
|
public string? ActualFeedsDigest { get; init; }
|
|
public bool? FeedsDigestMatches { get; init; }
|
|
public string? ExpectedPolicyDigest { get; init; }
|
|
public string? ActualPolicyDigest { get; init; }
|
|
public bool? PolicyDigestMatches { get; init; }
|
|
public string? ExpectedDecision { get; init; }
|
|
public bool? DecisionMatches { get; init; }
|
|
public bool? SignatureValid { get; init; }
|
|
public string? SignerIdentity { get; init; }
|
|
public IReadOnlyList<string> Errors { get; init; } = Array.Empty<string>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Summary information about a verdict attestation.
|
|
/// </summary>
|
|
public sealed record VerdictSummary
|
|
{
|
|
public required string Digest { get; init; }
|
|
public string? Decision { get; init; }
|
|
public DateTimeOffset? CreatedAt { get; init; }
|
|
public string? SbomDigest { get; init; }
|
|
public string? FeedsDigest { get; init; }
|
|
public string? PolicyDigest { get; init; }
|
|
public string? GraphRevisionId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface for verdict attestation verification.
|
|
/// </summary>
|
|
public interface IVerdictAttestationVerifier
|
|
{
|
|
Task<VerdictVerificationResult> VerifyAsync(
|
|
VerdictVerificationRequest request,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<IReadOnlyList<VerdictSummary>> ListAsync(
|
|
string reference,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
/// <summary>
|
|
/// Push a verdict attestation to an OCI registry.
|
|
/// Sprint: SPRINT_4300_0001_0001, Task: VERDICT-013
|
|
/// </summary>
|
|
Task<VerdictPushResult> PushAsync(
|
|
VerdictPushRequest request,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request for verdict push.
|
|
/// Sprint: SPRINT_4300_0001_0001, Task: VERDICT-013
|
|
/// </summary>
|
|
public sealed record VerdictPushRequest
|
|
{
|
|
public required string Reference { get; init; }
|
|
public string? VerdictFilePath { get; init; }
|
|
public byte[]? VerdictBytes { get; init; }
|
|
public string? Registry { get; init; }
|
|
public bool Insecure { get; init; }
|
|
public bool DryRun { get; init; }
|
|
public bool Force { get; init; }
|
|
public int TimeoutSeconds { get; init; } = 300;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of verdict push.
|
|
/// Sprint: SPRINT_4300_0001_0001, Task: VERDICT-013
|
|
/// </summary>
|
|
public sealed record VerdictPushResult
|
|
{
|
|
public required bool Success { get; init; }
|
|
public string? VerdictDigest { get; init; }
|
|
public string? ManifestDigest { get; init; }
|
|
public string? Error { get; init; }
|
|
public bool DryRun { get; init; }
|
|
}
|