save progress

This commit is contained in:
StellaOps Bot
2026-01-06 09:42:02 +02:00
parent 94d68bee8b
commit 37e11918e0
443 changed files with 85863 additions and 897 deletions

View File

@@ -23,6 +23,7 @@ internal static class AirGapCommandGroup
airgap.Add(BuildImportCommand(services, verboseOption, cancellationToken));
airgap.Add(BuildDiffCommand(services, verboseOption, cancellationToken));
airgap.Add(BuildStatusCommand(services, verboseOption, cancellationToken));
airgap.Add(BuildJobsCommand(services, verboseOption, cancellationToken));
return airgap;
}
@@ -104,7 +105,7 @@ internal static class AirGapCommandGroup
command.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption);
var output = parseResult.GetValue(outputOption) ?? $"knowledge-{DateTime.UtcNow:yyyyMMdd}.tar.gz";
var includeAdvisories = parseResult.GetValue(includeAdvisoriesOption);
var includeVex = parseResult.GetValue(includeVexOption);
var includePolicies = parseResult.GetValue(includePoliciesOption);
@@ -300,4 +301,179 @@ internal static class AirGapCommandGroup
return command;
}
/// <summary>
/// Builds the 'airgap jobs' subcommand group for HLC job sync bundles.
/// Sprint: SPRINT_20260105_002_003_ROUTER
/// </summary>
private static Command BuildJobsCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var jobs = new Command("jobs", "Manage HLC job sync bundles for offline/air-gap scenarios.");
jobs.Add(BuildJobsExportCommand(services, verboseOption, cancellationToken));
jobs.Add(BuildJobsImportCommand(services, verboseOption, cancellationToken));
jobs.Add(BuildJobsListCommand(services, verboseOption, cancellationToken));
return jobs;
}
private static Command BuildJobsExportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output file path for the job sync bundle."
};
var tenantOption = new Option<string>("--tenant", "-t")
{
Description = "Tenant ID for the export (required)."
}.SetDefaultValue("default");
var nodeOption = new Option<string?>("--node")
{
Description = "Specific node ID to export (default: current node)."
};
var signOption = new Option<bool>("--sign")
{
Description = "Sign the bundle with DSSE."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output result as JSON."
};
var command = new Command("export", "Export offline job logs to a sync bundle.")
{
outputOption,
tenantOption,
nodeOption,
signOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption) ?? string.Empty;
var tenant = parseResult.GetValue(tenantOption) ?? "default";
var node = parseResult.GetValue(nodeOption);
var sign = parseResult.GetValue(signOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleAirGapJobsExportAsync(
services,
output,
tenant,
node,
sign,
json,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildJobsImportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var bundleArg = new Argument<string>("bundle")
{
Description = "Path to the job sync bundle file."
};
var verifyOnlyOption = new Option<bool>("--verify-only")
{
Description = "Only verify the bundle without importing."
};
var forceOption = new Option<bool>("--force")
{
Description = "Force import even if validation fails."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output result as JSON."
};
var command = new Command("import", "Import a job sync bundle.")
{
bundleArg,
verifyOnlyOption,
forceOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var bundle = parseResult.GetValue(bundleArg) ?? string.Empty;
var verifyOnly = parseResult.GetValue(verifyOnlyOption);
var force = parseResult.GetValue(forceOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleAirGapJobsImportAsync(
services,
bundle,
verifyOnly,
force,
json,
verbose,
cancellationToken);
});
return command;
}
private static Command BuildJobsListCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var sourceOption = new Option<string?>("--source", "-s")
{
Description = "Source directory to scan for bundles (default: current directory)."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output result as JSON."
};
var command = new Command("list", "List available job sync bundles.")
{
sourceOption,
jsonOption,
verboseOption
};
command.SetAction(parseResult =>
{
var source = parseResult.GetValue(sourceOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleAirGapJobsListAsync(
services,
source,
json,
verbose,
cancellationToken);
});
return command;
}
}

View File

@@ -1,12 +1,18 @@
// -----------------------------------------------------------------------------
// CommandHandlers.AirGap.cs
// Sprint: SPRINT_4300_0001_0002_one_command_audit_replay
// Sprint: SPRINT_20260105_002_003_ROUTER (HLC Offline Merge Protocol)
// Description: Command handlers for airgap operations.
// -----------------------------------------------------------------------------
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
using StellaOps.AirGap.Sync;
using StellaOps.AirGap.Sync.Models;
using StellaOps.AirGap.Sync.Services;
using StellaOps.AirGap.Sync.Transport;
namespace StellaOps.Cli.Commands;
@@ -104,4 +110,371 @@ internal static partial class CommandHandlers
AnsiConsole.MarkupLine("[green]Airgap mode: Enabled[/]");
return 0;
}
#region Job Sync Commands (SPRINT_20260105_002_003_ROUTER)
/// <summary>
/// Handler for 'stella airgap jobs export' command.
/// Exports offline job logs for air-gap transfer.
/// </summary>
internal static async Task<int> HandleAirGapJobsExportAsync(
IServiceProvider services,
string output,
string tenantId,
string? nodeId,
bool sign,
bool emitJson,
bool verbose,
CancellationToken cancellationToken)
{
const int ExitSuccess = 0;
const int ExitGeneralError = 1;
await using var scope = services.CreateAsyncScope();
try
{
var exporter = scope.ServiceProvider.GetService<IAirGapBundleExporter>();
if (exporter is null)
{
AnsiConsole.MarkupLine("[red]Error:[/] Air-gap sync services not configured. Register with AddAirGapSyncServices().");
return ExitGeneralError;
}
if (verbose)
{
AnsiConsole.MarkupLine($"[grey]Exporting job logs for tenant: {Markup.Escape(tenantId)}[/]");
}
// Export bundle
var nodeIds = !string.IsNullOrWhiteSpace(nodeId) ? new[] { nodeId } : null;
var bundle = await exporter.ExportAsync(tenantId, nodeIds, cancellationToken).ConfigureAwait(false);
if (bundle.JobLogs.Count == 0)
{
AnsiConsole.MarkupLine("[yellow]Warning:[/] No offline job logs found to export.");
return ExitSuccess;
}
// Export to file
var outputPath = output;
if (string.IsNullOrWhiteSpace(outputPath))
{
outputPath = $"job-sync-{bundle.BundleId:N}.json";
}
await exporter.ExportToFileAsync(bundle, outputPath, cancellationToken).ConfigureAwait(false);
// Output result
if (emitJson)
{
var result = new
{
success = true,
bundleId = bundle.BundleId,
tenantId = bundle.TenantId,
outputPath,
createdAt = bundle.CreatedAt,
nodeCount = bundle.JobLogs.Count,
totalEntries = bundle.JobLogs.Sum(l => l.Entries.Count),
manifestDigest = bundle.ManifestDigest
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
AnsiConsole.MarkupLine($"[green]Exported job sync bundle:[/] {Markup.Escape(outputPath)}");
AnsiConsole.MarkupLine($" Bundle ID: [bold]{bundle.BundleId}[/]");
AnsiConsole.MarkupLine($" Tenant: {Markup.Escape(bundle.TenantId)}");
AnsiConsole.MarkupLine($" Node logs: {bundle.JobLogs.Count}");
AnsiConsole.MarkupLine($" Total entries: {bundle.JobLogs.Sum(l => l.Entries.Count)}");
AnsiConsole.MarkupLine($" Manifest digest: {Markup.Escape(bundle.ManifestDigest)}");
}
return ExitSuccess;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
if (verbose)
{
AnsiConsole.WriteException(ex);
}
return ExitGeneralError;
}
}
/// <summary>
/// Handler for 'stella airgap jobs import' command.
/// Imports job sync bundle from air-gap transfer.
/// </summary>
internal static async Task<int> HandleAirGapJobsImportAsync(
IServiceProvider services,
string bundlePath,
bool verifyOnly,
bool force,
bool emitJson,
bool verbose,
CancellationToken cancellationToken)
{
const int ExitSuccess = 0;
const int ExitGeneralError = 1;
const int ExitValidationFailed = 2;
await using var scope = services.CreateAsyncScope();
try
{
var importer = scope.ServiceProvider.GetService<IAirGapBundleImporter>();
if (importer is null)
{
AnsiConsole.MarkupLine("[red]Error:[/] Air-gap sync services not configured. Register with AddAirGapSyncServices().");
return ExitGeneralError;
}
if (!File.Exists(bundlePath))
{
AnsiConsole.MarkupLine($"[red]Error:[/] Bundle file not found: {Markup.Escape(bundlePath)}");
return ExitGeneralError;
}
if (verbose)
{
AnsiConsole.MarkupLine($"[grey]Importing job sync bundle: {Markup.Escape(bundlePath)}[/]");
}
// Import bundle
var bundle = await importer.ImportFromFileAsync(bundlePath, cancellationToken).ConfigureAwait(false);
// Validate bundle
var validation = importer.Validate(bundle);
if (!validation.IsValid)
{
if (emitJson)
{
var errorResult = new
{
success = false,
bundleId = bundle.BundleId,
validationPassed = false,
issues = validation.Issues
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(errorResult, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
AnsiConsole.MarkupLine("[red]Bundle validation failed![/]");
foreach (var issue in validation.Issues)
{
AnsiConsole.MarkupLine($" - {Markup.Escape(issue)}");
}
}
if (!force)
{
return ExitValidationFailed;
}
AnsiConsole.MarkupLine("[yellow]Warning:[/] Proceeding with import despite validation failures (--force).");
}
if (verifyOnly)
{
if (emitJson)
{
var verifyResult = new
{
success = true,
bundleId = bundle.BundleId,
tenantId = bundle.TenantId,
validationPassed = validation.IsValid,
nodeCount = bundle.JobLogs.Count,
totalEntries = bundle.JobLogs.Sum(l => l.Entries.Count),
manifestDigest = bundle.ManifestDigest
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(verifyResult, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
AnsiConsole.MarkupLine("[green]Bundle verification passed.[/]");
AnsiConsole.MarkupLine($" Bundle ID: [bold]{bundle.BundleId}[/]");
AnsiConsole.MarkupLine($" Tenant: {Markup.Escape(bundle.TenantId)}");
AnsiConsole.MarkupLine($" Node logs: {bundle.JobLogs.Count}");
AnsiConsole.MarkupLine($" Total entries: {bundle.JobLogs.Sum(l => l.Entries.Count)}");
}
return ExitSuccess;
}
// Sync to scheduler (if service available)
var syncService = scope.ServiceProvider.GetService<IAirGapSyncService>();
if (syncService is not null)
{
var syncResult = await syncService.SyncFromBundleAsync(bundle, cancellationToken).ConfigureAwait(false);
if (emitJson)
{
var result = new
{
success = true,
bundleId = syncResult.BundleId,
totalInBundle = syncResult.TotalInBundle,
appended = syncResult.Appended,
duplicates = syncResult.Duplicates,
newChainHead = syncResult.NewChainHead is not null ? Convert.ToBase64String(syncResult.NewChainHead) : null
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
AnsiConsole.MarkupLine("[green]Job sync bundle imported successfully.[/]");
AnsiConsole.MarkupLine($" Bundle ID: [bold]{syncResult.BundleId}[/]");
AnsiConsole.MarkupLine($" Jobs in bundle: {syncResult.TotalInBundle}");
AnsiConsole.MarkupLine($" Jobs appended: {syncResult.Appended}");
AnsiConsole.MarkupLine($" Duplicates skipped: {syncResult.Duplicates}");
}
}
else
{
// No sync service - just report the imported bundle
if (emitJson)
{
var result = new
{
success = true,
bundleId = bundle.BundleId,
tenantId = bundle.TenantId,
nodeCount = bundle.JobLogs.Count,
totalEntries = bundle.JobLogs.Sum(l => l.Entries.Count),
note = "Bundle imported but sync service not available"
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
AnsiConsole.MarkupLine("[green]Job sync bundle loaded.[/]");
AnsiConsole.MarkupLine($" Bundle ID: [bold]{bundle.BundleId}[/]");
AnsiConsole.MarkupLine($" Tenant: {Markup.Escape(bundle.TenantId)}");
AnsiConsole.MarkupLine($" Node logs: {bundle.JobLogs.Count}");
AnsiConsole.MarkupLine($" Total entries: {bundle.JobLogs.Sum(l => l.Entries.Count)}");
AnsiConsole.MarkupLine("[yellow]Note:[/] Sync service not available. Bundle validated but not synced to scheduler.");
}
}
return ExitSuccess;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
if (verbose)
{
AnsiConsole.WriteException(ex);
}
return ExitGeneralError;
}
}
/// <summary>
/// Handler for 'stella airgap jobs list' command.
/// Lists available job sync bundles.
/// </summary>
internal static async Task<int> HandleAirGapJobsListAsync(
IServiceProvider services,
string? source,
bool emitJson,
bool verbose,
CancellationToken cancellationToken)
{
const int ExitSuccess = 0;
const int ExitGeneralError = 1;
await using var scope = services.CreateAsyncScope();
try
{
var transport = scope.ServiceProvider.GetService<IJobSyncTransport>();
if (transport is null)
{
AnsiConsole.MarkupLine("[red]Error:[/] Job sync transport not configured. Register with AddFileBasedJobSyncTransport().");
return ExitGeneralError;
}
var sourcePath = source ?? ".";
var bundles = await transport.ListAvailableBundlesAsync(sourcePath, cancellationToken).ConfigureAwait(false);
if (emitJson)
{
var result = new
{
source = sourcePath,
bundles = bundles.Select(b => new
{
bundleId = b.BundleId,
tenantId = b.TenantId,
sourceNodeId = b.SourceNodeId,
createdAt = b.CreatedAt,
entryCount = b.EntryCount,
sizeBytes = b.SizeBytes
})
};
AnsiConsole.WriteLine(JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
}
else
{
if (bundles.Count == 0)
{
AnsiConsole.MarkupLine($"[grey]No job sync bundles found in: {Markup.Escape(sourcePath)}[/]");
}
else
{
var table = new Table { Border = TableBorder.Rounded };
table.AddColumn("Bundle ID");
table.AddColumn("Tenant");
table.AddColumn("Source Node");
table.AddColumn("Created");
table.AddColumn("Entries");
table.AddColumn("Size");
foreach (var b in bundles)
{
table.AddRow(
Markup.Escape(b.BundleId.ToString("N")[..8] + "..."),
Markup.Escape(b.TenantId),
Markup.Escape(b.SourceNodeId),
b.CreatedAt.ToString("yyyy-MM-dd HH:mm"),
b.EntryCount.ToString(),
FormatBytesCompact(b.SizeBytes));
}
AnsiConsole.Write(table);
}
}
return ExitSuccess;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
if (verbose)
{
AnsiConsole.WriteException(ex);
}
return ExitGeneralError;
}
}
private static string FormatBytesCompact(long bytes)
{
string[] sizes = ["B", "KB", "MB", "GB"];
double size = bytes;
var order = 0;
while (size >= 1024 && order < sizes.Length - 1)
{
order++;
size /= 1024;
}
return $"{size:0.#} {sizes[order]}";
}
#endregion
}

View File

@@ -2,13 +2,17 @@
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Attestation;
using StellaOps.Cli.Telemetry;
using StellaOps.Replay.Core.Models;
using Spectre.Console;
namespace StellaOps.Cli.Commands;
@@ -33,7 +37,8 @@ internal static partial class CommandHandlers
var logger = loggerFactory.CreateLogger("verify-bundle");
using var activity = CliActivitySource.Instance.StartActivity("cli.verify.bundle", ActivityKind.Client);
using var duration = CliMetrics.MeasureCommandDuration("verify bundle");
using var durationMetric = CliMetrics.MeasureCommandDuration("verify bundle");
var stopwatch = Stopwatch.StartNew();
var emitJson = string.Equals(outputFormat, "json", StringComparison.OrdinalIgnoreCase);
@@ -128,14 +133,40 @@ internal static partial class CommandHandlers
// 5. Verify DSSE signature (if present)
var signatureVerified = false;
string? signatureKeyId = null;
var dssePath = Path.Combine(workingDir, "outputs", "verdict.dsse.json");
if (File.Exists(dssePath))
{
logger.LogInformation("Verifying DSSE signature...");
signatureVerified = await VerifyDsseSignatureAsync(dssePath, workingDir, violations, logger, cancellationToken).ConfigureAwait(false);
var (verified, keyId) = await VerifyDsseSignatureAsync(dssePath, workingDir, violations, logger, cancellationToken).ConfigureAwait(false);
signatureVerified = verified;
signatureKeyId = keyId;
}
// 6. Output result
// 6. Compute bundle hash for replay proof
var bundleHash = await ComputeDirectoryHashAsync(workingDir, cancellationToken).ConfigureAwait(false);
// 7. Generate ReplayProof
var verdictMatches = replayedVerdictHash is not null
&& manifest.ExpectedOutputs.VerdictHash is not null
&& string.Equals(replayedVerdictHash, manifest.ExpectedOutputs.VerdictHash, StringComparison.OrdinalIgnoreCase);
var replayProof = ReplayProof.FromExecutionResult(
bundleHash: bundleHash,
policyVersion: manifest.Scan.PolicyDigest,
verdictRoot: replayedVerdictHash ?? manifest.ExpectedOutputs.VerdictHash ?? "unknown",
verdictMatches: verdictMatches,
durationMs: stopwatch.ElapsedMilliseconds,
replayedAt: DateTimeOffset.UtcNow,
engineVersion: "1.0.0",
artifactDigest: manifest.Scan.ImageDigest,
signatureVerified: signatureVerified,
signatureKeyId: signatureKeyId,
metadata: ImmutableDictionary<string, string>.Empty
.Add("bundleId", manifest.BundleId)
.Add("schemaVersion", manifest.SchemaVersion));
// 8. Output result
var passed = violations.Count == 0;
var exitCode = passed ? CliExitCodes.Success : CliExitCodes.GeneralError;
@@ -147,10 +178,12 @@ internal static partial class CommandHandlers
BundleId: manifest.BundleId,
BundlePath: workingDir,
SchemaVersion: manifest.SchemaVersion,
InputsValidated: violations.Count(v => v.Rule.StartsWith("input.hash")) == 0,
InputsValidated: violations.Count(v => v.Rule.StartsWith("input.hash", StringComparison.Ordinal)) == 0,
ReplayedVerdictHash: replayedVerdictHash,
ExpectedVerdictHash: manifest.ExpectedOutputs.VerdictHash,
SignatureVerified: signatureVerified,
ReplayProofCompact: replayProof.ToCompactString(),
ReplayProofJson: replayProof.ToCanonicalJson(),
Violations: violations),
cancellationToken)
.ConfigureAwait(false);
@@ -290,27 +323,80 @@ internal static partial class CommandHandlers
return await Task.FromResult<string?>(null).ConfigureAwait(false);
}
private static async Task<bool> VerifyDsseSignatureAsync(
private static async Task<(bool IsValid, string? KeyId)> VerifyDsseSignatureAsync(
string dssePath,
string bundleDir,
List<BundleViolation> violations,
ILogger logger,
CancellationToken cancellationToken)
{
// STUB: DSSE signature verification not yet available
// This would normally call:
// var signer = services.GetRequiredService<ISigner>();
// var dsseEnvelope = await File.ReadAllTextAsync(dssePath);
// var publicKey = await File.ReadAllTextAsync(Path.Combine(bundleDir, "attestation", "public-key.pem"));
// var result = await signer.VerifyAsync(dsseEnvelope, publicKey);
// return result.IsValid;
// Load the DSSE envelope
string envelopeJson;
try
{
envelopeJson = await File.ReadAllTextAsync(dssePath, cancellationToken).ConfigureAwait(false);
}
catch (IOException ex)
{
violations.Add(new BundleViolation(
"signature.file.read_error",
$"Failed to read DSSE envelope: {ex.Message}"));
return (false, null);
}
logger.LogWarning("DSSE signature verification not implemented - Signer service integration pending");
violations.Add(new BundleViolation(
"signature.verify.not_implemented",
"DSSE signature verification requires Signer service (not yet integrated)"));
// Look for public key in standard locations
var publicKeyPaths = new[]
{
Path.Combine(bundleDir, "attestation", "public-key.pem"),
Path.Combine(bundleDir, "keys", "public-key.pem"),
Path.Combine(bundleDir, "public-key.pem"),
};
return await Task.FromResult(false).ConfigureAwait(false);
string? publicKeyPem = null;
foreach (var keyPath in publicKeyPaths)
{
if (File.Exists(keyPath))
{
try
{
publicKeyPem = await File.ReadAllTextAsync(keyPath, cancellationToken).ConfigureAwait(false);
logger.LogDebug("Loaded public key from {KeyPath}", keyPath);
break;
}
catch (IOException ex)
{
logger.LogWarning(ex, "Failed to read public key from {KeyPath}", keyPath);
}
}
}
if (string.IsNullOrWhiteSpace(publicKeyPem))
{
violations.Add(new BundleViolation(
"signature.key.not_found",
"No public key found for DSSE signature verification"));
return (false, null);
}
// Use the DsseVerifier for verification
var verifier = new DsseVerifier(
Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance.CreateLogger<DsseVerifier>());
var result = await verifier.VerifyAsync(envelopeJson, publicKeyPem, cancellationToken).ConfigureAwait(false);
if (!result.IsValid)
{
foreach (var issue in result.Issues)
{
violations.Add(new BundleViolation($"signature.{issue}", issue));
}
}
else
{
logger.LogInformation("DSSE signature verified successfully. KeyId: {KeyId}", result.PrimaryKeyId ?? "unknown");
}
return (result.IsValid, result.PrimaryKeyId);
}
private static Task WriteVerifyBundleErrorAsync(
@@ -366,7 +452,7 @@ internal static partial class CommandHandlers
table.AddRow("Bundle ID", Markup.Escape(payload.BundleId));
table.AddRow("Bundle Path", Markup.Escape(payload.BundlePath));
table.AddRow("Schema Version", Markup.Escape(payload.SchemaVersion));
table.AddRow("Inputs Validated", payload.InputsValidated ? "[green][/]" : "[red][/]");
table.AddRow("Inputs Validated", payload.InputsValidated ? "[green]Yes[/]" : "[red]No[/]");
if (payload.ReplayedVerdictHash is not null)
{
@@ -378,7 +464,13 @@ internal static partial class CommandHandlers
table.AddRow("Expected Verdict Hash", Markup.Escape(payload.ExpectedVerdictHash));
}
table.AddRow("Signature Verified", payload.SignatureVerified ? "[green][/]" : "[yellow]N/A[/]");
table.AddRow("Signature Verified", payload.SignatureVerified ? "[green]Yes[/]" : "[yellow]N/A[/]");
if (!string.IsNullOrEmpty(payload.ReplayProofCompact))
{
table.AddRow("Replay Proof", Markup.Escape(payload.ReplayProofCompact));
}
AnsiConsole.Write(table);
if (payload.Violations.Count > 0)
@@ -406,6 +498,8 @@ internal static partial class CommandHandlers
string? ReplayedVerdictHash,
string? ExpectedVerdictHash,
bool SignatureVerified,
string? ReplayProofCompact,
string? ReplayProofJson,
IReadOnlyList<BundleViolation> Violations);
}

View File

@@ -10375,6 +10375,7 @@ internal static partial class CommandHandlers
var required = requiredSigners.EnumerateArray()
.Select(s => s.GetString())
.Where(s => s != null)
.Cast<string>()
.ToList();
var actualSigners = signatures.Select(s => s.KeyId).ToHashSet();
@@ -11730,7 +11731,6 @@ internal static partial class CommandHandlers
}
// Check 3: Integrity verification (root hash)
var integrityOk = false;
if (index.TryGetProperty("integrity", out var integrity) &&
integrity.TryGetProperty("rootHash", out var rootHashElem))
{
@@ -11750,7 +11750,6 @@ internal static partial class CommandHandlers
if (computedRootHash == expectedRootHash.ToLowerInvariant())
{
checks.Add(("Root Hash Integrity", "PASS", $"Root hash matches: {expectedRootHash[..16]}..."));
integrityOk = true;
}
else
{
@@ -13656,7 +13655,6 @@ internal static partial class CommandHandlers
CancellationToken cancellationToken)
{
const int ExitSuccess = 0;
const int ExitInputError = 4;
var workspacePath = Path.GetFullPath(path ?? ".");
var policyName = name ?? Path.GetFileName(workspacePath);

View File

@@ -181,7 +181,7 @@ internal static class FeedsCommandGroup
return CommandHandlers.HandleFeedsSnapshotExportAsync(
services,
snapshotId,
snapshotId!,
output!,
compression,
json,
@@ -230,7 +230,7 @@ internal static class FeedsCommandGroup
return CommandHandlers.HandleFeedsSnapshotImportAsync(
services,
input,
input!,
validate,
json,
verbose,
@@ -270,7 +270,7 @@ internal static class FeedsCommandGroup
return CommandHandlers.HandleFeedsSnapshotValidateAsync(
services,
snapshotId,
snapshotId!,
json,
verbose,
cancellationToken);

View File

@@ -122,7 +122,7 @@ public class KeyRotationCommandGroup
var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519";
var publicKeyPath = parseResult.GetValue(publicKeyOption);
var notes = parseResult.GetValue(notesOption);
Environment.ExitCode = await AddKeyAsync(anchorId, keyId, algorithm, publicKeyPath, notes, ct).ConfigureAwait(false);
Environment.ExitCode = await AddKeyAsync(anchorId, keyId!, algorithm, publicKeyPath, notes, ct).ConfigureAwait(false);
});
return addCommand;
@@ -171,7 +171,7 @@ public class KeyRotationCommandGroup
var reason = parseResult.GetValue(reasonOption) ?? "rotation-complete";
var effectiveAt = parseResult.GetValue(effectiveOption) ?? DateTimeOffset.UtcNow;
var force = parseResult.GetValue(forceOption);
Environment.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, effectiveAt, force, ct).ConfigureAwait(false);
Environment.ExitCode = await RevokeKeyAsync(anchorId, keyId!, reason, effectiveAt, force, ct).ConfigureAwait(false);
});
return revokeCommand;
@@ -227,7 +227,7 @@ public class KeyRotationCommandGroup
var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519";
var publicKeyPath = parseResult.GetValue(publicKeyOption);
var overlapDays = parseResult.GetValue(overlapOption);
Environment.ExitCode = await RotateKeyAsync(anchorId, oldKeyId, newKeyId, algorithm, publicKeyPath, overlapDays, ct).ConfigureAwait(false);
Environment.ExitCode = await RotateKeyAsync(anchorId, oldKeyId!, newKeyId!, algorithm, publicKeyPath, overlapDays, ct).ConfigureAwait(false);
});
return rotateCommand;
@@ -332,7 +332,7 @@ public class KeyRotationCommandGroup
var anchorId = parseResult.GetValue(anchorArg);
var keyId = parseResult.GetValue(keyIdArg);
var signedAt = parseResult.GetValue(signedAtOption) ?? DateTimeOffset.UtcNow;
Environment.ExitCode = await VerifyKeyAsync(anchorId, keyId, signedAt, ct).ConfigureAwait(false);
Environment.ExitCode = await VerifyKeyAsync(anchorId, keyId!, signedAt, ct).ConfigureAwait(false);
});
return verifyCommand;

View File

@@ -153,7 +153,7 @@ internal static class WitnessCommandGroup
var tierOption = new Option<string?>("--tier")
{
Description = "Filter by confidence tier: confirmed, likely, present, unreachable."
}?.FromAmong("confirmed", "likely", "present", "unreachable");
}.FromAmong("confirmed", "likely", "present", "unreachable");
var reachableOnlyOption = new Option<bool>("--reachable-only")
{

View File

@@ -223,13 +223,16 @@ internal static class CliErrorRenderer
return false;
}
if ((!error.Metadata.TryGetValue("reason_code", out reasonCode) || string.IsNullOrWhiteSpace(reasonCode)) &&
(!error.Metadata.TryGetValue("reasonCode", out reasonCode) || string.IsNullOrWhiteSpace(reasonCode)))
string? code1 = null;
string? code2 = null;
if ((!error.Metadata.TryGetValue("reason_code", out code1) || string.IsNullOrWhiteSpace(code1)) &&
(!error.Metadata.TryGetValue("reasonCode", out code2) || string.IsNullOrWhiteSpace(code2)))
{
return false;
}
reasonCode = OfflineKitReasonCodes.Normalize(reasonCode) ?? "";
reasonCode = OfflineKitReasonCodes.Normalize(code1 ?? code2 ?? "") ?? "";
return reasonCode.Length > 0;
}

View File

@@ -328,8 +328,8 @@ public sealed class OutputRenderer : IOutputRenderer
for (var i = 0; i < columns.Count; i++)
{
widths[i] = columns[i].Header.Length;
if (columns[i].MinWidth.HasValue)
widths[i] = Math.Max(widths[i], columns[i].MinWidth.Value);
if (columns[i].MinWidth is { } minWidth)
widths[i] = Math.Max(widths[i], minWidth);
}
// Get all values and update widths
@@ -340,9 +340,9 @@ public sealed class OutputRenderer : IOutputRenderer
for (var i = 0; i < columns.Count; i++)
{
var value = columns[i].ValueSelector(item) ?? "";
if (columns[i].MaxWidth.HasValue && value.Length > columns[i].MaxWidth.Value)
if (columns[i].MaxWidth is { } maxWidth && value.Length > maxWidth)
{
value = value[..(columns[i].MaxWidth.Value - 3)] + "...";
value = value[..(maxWidth - 3)] + "...";
}
row[i] = value;
widths[i] = Math.Max(widths[i], value.Length);

View File

@@ -359,7 +359,7 @@ internal sealed class ConcelierObservationsClient : IConcelierObservationsClient
private static (string Scope, string CacheKey) BuildScopeAndCacheKey(StellaOpsCliOptions options)
{
var baseScope = AuthorityTokenUtilities.ResolveScope(options);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnRead);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnView);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"

View File

@@ -65,7 +65,7 @@ public sealed class MirrorBundleImportService : IMirrorBundleImportService
// Register in catalog
var bundleId = GenerateBundleId(manifest);
var manifestDigest = ComputeDigest(File.ReadAllBytes(manifestResult.ManifestPath));
var manifestDigest = ComputeDigest(File.ReadAllBytes(manifestResult.ManifestPath!));
var catalogEntry = new ImportModels.BundleCatalogEntry(
request.TenantId ?? "default",

View File

@@ -861,7 +861,7 @@ internal sealed partial class PromotionAssembler : IPromotionAssembler
try
{
var certBytes = Convert.FromBase64String(sig.Cert);
using var cert = new System.Security.Cryptography.X509Certificates.X509Certificate2(certBytes);
using var cert = System.Security.Cryptography.X509Certificates.X509CertificateLoader.LoadCertificate(certBytes);
// Build PAE for verification
var pae = BuildPae(envelope.PayloadType, envelope.Payload);

View File

@@ -94,6 +94,10 @@
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.CallGraph/StellaOps.Scanner.CallGraph.csproj" />
<!-- Secrets Bundle CLI (SPRINT_20260104_003_SCANNER) -->
<ProjectReference Include="../../Scanner/__Libraries/StellaOps.Scanner.Analyzers.Secrets/StellaOps.Scanner.Analyzers.Secrets.csproj" />
<!-- Replay Infrastructure (SPRINT_20260105_002_001_REPLAY) -->
<ProjectReference Include="../../__Libraries/StellaOps.Replay.Core/StellaOps.Replay.Core.csproj" />
<!-- Air-Gap Job Sync (SPRINT_20260105_002_003_ROUTER) -->
<ProjectReference Include="../../AirGap/__Libraries/StellaOps.AirGap.Sync/StellaOps.AirGap.Sync.csproj" />
</ItemGroup>
<!-- GOST Crypto Plugins (Russia distribution) -->