fix tests. new product advisories enhancements

This commit is contained in:
master
2026-01-25 19:11:36 +02:00
parent c70e83719e
commit 6e687b523a
504 changed files with 40610 additions and 3785 deletions

View File

@@ -296,6 +296,17 @@ public static class FunctionMapCommandGroup
predicate.Predicate.ExpectedPaths.Count);
}
// Serialize output
string outputContent;
if (format.Equals("yaml", StringComparison.OrdinalIgnoreCase))
{
outputContent = SerializeToYaml(predicate);
}
else
{
outputContent = JsonSerializer.Serialize(predicate, JsonOptions);
}
// Sign if requested (DSSE envelope)
if (sign)
{
@@ -368,7 +379,7 @@ public static class FunctionMapCommandGroup
var dsseEnvelopeObj = new StellaOps.Attestor.Core.Submission.AttestorSubmissionRequest.DsseEnvelope
{
PayloadType = "application/vnd.stellaops.function-map+json",
Payload = Convert.ToBase64String(entryBytes)
PayloadBase64 = Convert.ToBase64String(entryBytes)
};
var submissionRequest = new StellaOps.Attestor.Core.Submission.AttestorSubmissionRequest
@@ -409,17 +420,6 @@ public static class FunctionMapCommandGroup
}
}
// Serialize output
string outputContent;
if (format.Equals("yaml", StringComparison.OrdinalIgnoreCase))
{
outputContent = SerializeToYaml(predicate);
}
else
{
outputContent = JsonSerializer.Serialize(predicate, JsonOptions);
}
// Write output
if (string.IsNullOrEmpty(output))
{

View File

@@ -37,10 +37,8 @@ public static class ObservationsCommandGroup
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var observationsCommand = new Command("observations", "Runtime observation operations")
{
Aliases = { "obs" }
};
// Note: "obs" alias removed to avoid conflict with root-level "obs" command (observability)
var observationsCommand = new Command("observations", "Runtime observation operations");
observationsCommand.Add(BuildQueryCommand(services, verboseOption, cancellationToken));

View File

@@ -0,0 +1,466 @@
// -----------------------------------------------------------------------------
// TrustCommandGroup.cs
// Sprint: SPRINT_20260125_002_Attestor_trust_automation
// Task: PROXY-003 - Add stella-trust CLI commands
// Description: CLI commands for TUF-based trust repository management
// -----------------------------------------------------------------------------
using System.CommandLine;
using StellaOps.Cli.Extensions;
namespace StellaOps.Cli.Commands.Trust;
/// <summary>
/// CLI command group for trust repository management.
/// Provides commands for TUF metadata management, service discovery, and offline trust bundles.
/// </summary>
internal static class TrustCommandGroup
{
internal static Command BuildTrustCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var trust = new Command("trust", "Trust repository commands for TUF-based trust management.");
trust.Add(BuildInitCommand(services, verboseOption, cancellationToken));
trust.Add(BuildSyncCommand(services, verboseOption, cancellationToken));
trust.Add(BuildStatusCommand(services, verboseOption, cancellationToken));
trust.Add(BuildVerifyCommand(services, verboseOption, cancellationToken));
trust.Add(BuildExportCommand(services, verboseOption, cancellationToken));
trust.Add(BuildImportCommand(services, verboseOption, cancellationToken));
trust.Add(BuildSnapshotCommand(services, verboseOption, cancellationToken));
return trust;
}
/// <summary>
/// stella trust init - Initialize TUF client with a trust repository
/// </summary>
private static Command BuildInitCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var tufUrlOption = new Option<string>("--tuf-url", "-u")
{
Description = "URL of the TUF repository (e.g., https://trust.example.com/tuf/)",
Required = true
};
var serviceMapOption = new Option<string>("--service-map", "-s")
{
Description = "TUF target name for the Sigstore service map"
};
serviceMapOption.SetDefaultValue("sigstore-services-v1");
var pinKeysOption = new Option<string[]>("--pin", "-p")
{
Description = "TUF target names for Rekor keys to pin (can specify multiple)"
};
pinKeysOption.SetDefaultValue(new[] { "rekor-key-v1" });
var cachePathOption = new Option<string?>("--cache-path")
{
Description = "Local cache directory for TUF metadata (default: ~/.local/share/StellaOps/TufCache)"
};
var offlineModeOption = new Option<bool>("--offline")
{
Description = "Initialize in offline mode (use bundled metadata only)"
};
var forceOption = new Option<bool>("--force", "-f")
{
Description = "Force re-initialization even if already initialized"
};
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json"
}.SetDefaultValue("text").FromAmong("text", "json");
var command = new Command("init", "Initialize TUF client with a trust repository.")
{
tufUrlOption,
serviceMapOption,
pinKeysOption,
cachePathOption,
offlineModeOption,
forceOption,
outputOption,
verboseOption
};
command.SetAction(parseResult =>
{
var tufUrl = parseResult.GetValue(tufUrlOption)!;
var serviceMap = parseResult.GetValue(serviceMapOption)!;
var pinKeys = parseResult.GetValue(pinKeysOption) ?? Array.Empty<string>();
var cachePath = parseResult.GetValue(cachePathOption);
var offlineMode = parseResult.GetValue(offlineModeOption);
var force = parseResult.GetValue(forceOption);
var output = parseResult.GetValue(outputOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleInitAsync(
services,
tufUrl,
serviceMap,
pinKeys,
cachePath,
offlineMode,
force,
output,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella trust sync - Refresh TUF metadata
/// </summary>
private static Command BuildSyncCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var forceOption = new Option<bool>("--force", "-f")
{
Description = "Force refresh even if metadata is fresh"
};
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json"
}.SetDefaultValue("text").FromAmong("text", "json");
var command = new Command("sync", "Refresh TUF metadata from the repository.")
{
forceOption,
outputOption,
verboseOption
};
command.SetAction(parseResult =>
{
var force = parseResult.GetValue(forceOption);
var output = parseResult.GetValue(outputOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleSyncAsync(
services,
force,
output,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella trust status - Show current trust state
/// </summary>
private static Command BuildStatusCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json"
}.SetDefaultValue("text").FromAmong("text", "json");
var showKeysOption = new Option<bool>("--show-keys", "-k")
{
Description = "Show loaded key fingerprints"
};
var showEndpointsOption = new Option<bool>("--show-endpoints", "-e")
{
Description = "Show discovered service endpoints"
};
var command = new Command("status", "Show current trust state and metadata freshness.")
{
outputOption,
showKeysOption,
showEndpointsOption,
verboseOption
};
command.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption) ?? "text";
var showKeys = parseResult.GetValue(showKeysOption);
var showEndpoints = parseResult.GetValue(showEndpointsOption);
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleStatusAsync(
services,
output,
showKeys,
showEndpoints,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella trust verify - Verify artifact using TUF trust anchors
/// </summary>
private static Command BuildVerifyCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var artifactArg = new Argument<string>("artifact")
{
Description = "Artifact reference to verify (image ref, file path, or attestation)"
};
var checkInclusionOption = new Option<bool>("--check-inclusion")
{
Description = "Verify Rekor inclusion proof"
};
checkInclusionOption.SetDefaultValue(true);
var offlineOption = new Option<bool>("--offline")
{
Description = "Verify using only cached/bundled trust data"
};
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json"
}.SetDefaultValue("text").FromAmong("text", "json");
var command = new Command("verify", "Verify artifact using TUF-loaded trust anchors.")
{
artifactArg,
checkInclusionOption,
offlineOption,
outputOption,
verboseOption
};
command.SetAction(parseResult =>
{
var artifact = parseResult.GetValue(artifactArg)!;
var checkInclusion = parseResult.GetValue(checkInclusionOption);
var offline = parseResult.GetValue(offlineOption);
var output = parseResult.GetValue(outputOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleVerifyAsync(
services,
artifact,
checkInclusion,
offline,
output,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella trust export - Export trust state for offline use
/// </summary>
private static Command BuildExportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var outputOption = new Option<string>("--out", "-o")
{
Description = "Output directory for the trust bundle",
Required = true
};
var includeTargetsOption = new Option<bool>("--include-targets")
{
Description = "Include all TUF targets in the bundle"
};
includeTargetsOption.SetDefaultValue(true);
var command = new Command("export", "Export current trust state for offline use.")
{
outputOption,
includeTargetsOption,
verboseOption
};
command.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption)!;
var includeTargets = parseResult.GetValue(includeTargetsOption);
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleExportAsync(
services,
output,
includeTargets,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella trust import - Import trust state from offline bundle
/// </summary>
private static Command BuildImportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var bundleArg = new Argument<string>("bundle")
{
Description = "Path to the trust bundle (directory or tar.zst)"
};
var verifyManifestOption = new Option<bool>("--verify-manifest")
{
Description = "Verify manifest checksums before import"
};
verifyManifestOption.SetDefaultValue(true);
var rejectIfStaleOption = new Option<string?>("--reject-if-stale")
{
Description = "Reject if metadata older than threshold (e.g., 7d, 24h)"
};
var forceOption = new Option<bool>("--force", "-f")
{
Description = "Force import even if validation fails"
};
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output format: text, json"
}.SetDefaultValue("text").FromAmong("text", "json");
var command = new Command("import", "Import trust state from offline bundle.")
{
bundleArg,
verifyManifestOption,
rejectIfStaleOption,
forceOption,
outputOption,
verboseOption
};
command.SetAction(parseResult =>
{
var bundle = parseResult.GetValue(bundleArg)!;
var verifyManifest = parseResult.GetValue(verifyManifestOption);
var rejectIfStale = parseResult.GetValue(rejectIfStaleOption);
var force = parseResult.GetValue(forceOption);
var output = parseResult.GetValue(outputOption) ?? "text";
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleImportAsync(
services,
bundle,
verifyManifest,
rejectIfStale,
force,
output,
verbose,
cancellationToken);
});
return command;
}
/// <summary>
/// stella trust snapshot - Snapshot subcommands for tile/entry export
/// </summary>
private static Command BuildSnapshotCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var snapshot = new Command("snapshot", "Snapshot commands for tile and entry export.");
snapshot.Add(BuildSnapshotExportCommand(services, verboseOption, cancellationToken));
return snapshot;
}
/// <summary>
/// stella trust snapshot export - Create sealed snapshot with tiles
/// </summary>
private static Command BuildSnapshotExportCommand(
IServiceProvider services,
Option<bool> verboseOption,
CancellationToken cancellationToken)
{
var outputOption = new Option<string>("--out", "-o")
{
Description = "Output file path for the snapshot (e.g., ./snapshots/2026-01-25.tar.zst)",
Required = true
};
var fromProxyOption = new Option<string?>("--from-proxy")
{
Description = "Fetch tiles from a tile-proxy instead of upstream Rekor"
};
var tilesPathOption = new Option<string?>("--tiles")
{
Description = "Local tiles directory to include in the snapshot"
};
var includeEntriesOption = new Option<string?>("--include-entries")
{
Description = "Entry range to include (e.g., 1000000-1050000)"
};
var depthOption = new Option<int>("--depth")
{
Description = "Number of recent entries to include tiles for"
};
depthOption.SetDefaultValue(10000);
var command = new Command("export", "Create a sealed snapshot with tiles for offline verification.")
{
outputOption,
fromProxyOption,
tilesPathOption,
includeEntriesOption,
depthOption,
verboseOption
};
command.SetAction(parseResult =>
{
var output = parseResult.GetValue(outputOption)!;
var fromProxy = parseResult.GetValue(fromProxyOption);
var tilesPath = parseResult.GetValue(tilesPathOption);
var includeEntries = parseResult.GetValue(includeEntriesOption);
var depth = parseResult.GetValue(depthOption);
var verbose = parseResult.GetValue(verboseOption);
return TrustCommandHandlers.HandleSnapshotExportAsync(
services,
output,
fromProxy,
tilesPath,
includeEntries,
depth,
verbose,
cancellationToken);
});
return command;
}
}

View File

@@ -0,0 +1,846 @@
// -----------------------------------------------------------------------------
// TrustCommandHandlers.cs
// Sprint: SPRINT_20260125_002_Attestor_trust_automation
// Task: PROXY-003 - Add stella-trust CLI commands
// Description: Command handlers for TUF-based trust repository management
// -----------------------------------------------------------------------------
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.AirGap.Bundle.TrustSnapshot;
using StellaOps.Attestor.TrustRepo;
namespace StellaOps.Cli.Commands.Trust;
/// <summary>
/// Command handlers for trust repository operations.
/// </summary>
internal static class TrustCommandHandlers
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Handle 'stella trust init' command.
/// </summary>
public static async Task<int> HandleInitAsync(
IServiceProvider services,
string tufUrl,
string serviceMapTarget,
string[] pinKeys,
string? cachePath,
bool offlineMode,
bool force,
string output,
bool verbose,
CancellationToken cancellationToken)
{
var logger = services.GetRequiredService<ILogger<object>>();
try
{
// Validate TUF URL
if (!Uri.TryCreate(tufUrl, UriKind.Absolute, out var tufUri))
{
WriteError("Invalid TUF URL", output);
return 1;
}
// Check if already initialized
var effectiveCachePath = cachePath ?? GetDefaultCachePath();
var rootPath = Path.Combine(effectiveCachePath, "root.json");
if (File.Exists(rootPath) && !force)
{
WriteError("Trust repository already initialized. Use --force to re-initialize.", output);
return 1;
}
// Create cache directory
Directory.CreateDirectory(effectiveCachePath);
// Write configuration
var config = new TrustInitConfig
{
TufUrl = tufUrl,
ServiceMapTarget = serviceMapTarget,
RekorKeyTargets = pinKeys.ToList(),
OfflineMode = offlineMode,
InitializedAt = DateTimeOffset.UtcNow
};
var configPath = Path.Combine(effectiveCachePath, "trust-config.json");
var configJson = JsonSerializer.Serialize(config, JsonOptions);
await File.WriteAllTextAsync(configPath, configJson, cancellationToken);
if (!offlineMode)
{
// Fetch initial TUF metadata
Console.WriteLine($"Fetching TUF metadata from {tufUrl}...");
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
// Fetch root.json
var rootResponse = await httpClient.GetAsync($"{tufUrl.TrimEnd('/')}/root.json", cancellationToken);
if (!rootResponse.IsSuccessStatusCode)
{
WriteError($"Failed to fetch root.json: {rootResponse.StatusCode}", output);
return 1;
}
var rootContent = await rootResponse.Content.ReadAsStringAsync(cancellationToken);
await File.WriteAllTextAsync(rootPath, rootContent, cancellationToken);
// Fetch timestamp.json
var timestampResponse = await httpClient.GetAsync($"{tufUrl.TrimEnd('/')}/timestamp.json", cancellationToken);
if (timestampResponse.IsSuccessStatusCode)
{
var timestampContent = await timestampResponse.Content.ReadAsStringAsync(cancellationToken);
await File.WriteAllTextAsync(Path.Combine(effectiveCachePath, "timestamp.json"), timestampContent, cancellationToken);
}
Console.WriteLine("TUF metadata fetched successfully.");
}
var result = new TrustInitResult
{
Success = true,
TufUrl = tufUrl,
CachePath = effectiveCachePath,
ServiceMapTarget = serviceMapTarget,
PinnedKeys = pinKeys.ToList(),
OfflineMode = offlineMode
};
WriteResult(result, output, "Trust repository initialized successfully.");
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to initialize trust repository");
WriteError($"Failed to initialize: {ex.Message}", output);
return 1;
}
}
/// <summary>
/// Handle 'stella trust sync' command.
/// </summary>
public static async Task<int> HandleSyncAsync(
IServiceProvider services,
bool force,
string output,
bool verbose,
CancellationToken cancellationToken)
{
var logger = services.GetRequiredService<ILogger<object>>();
try
{
var cachePath = GetDefaultCachePath();
var configPath = Path.Combine(cachePath, "trust-config.json");
if (!File.Exists(configPath))
{
WriteError("Trust repository not initialized. Run 'stella trust init' first.", output);
return 1;
}
var configJson = await File.ReadAllTextAsync(configPath, cancellationToken);
var config = JsonSerializer.Deserialize<TrustInitConfig>(configJson, JsonOptions);
if (config == null)
{
WriteError("Invalid trust configuration.", output);
return 1;
}
if (config.OfflineMode)
{
WriteError("Cannot sync in offline mode.", output);
return 1;
}
Console.WriteLine($"Syncing TUF metadata from {config.TufUrl}...");
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
var tufUrl = config.TufUrl.TrimEnd('/');
// Fetch timestamp first (freshness indicator)
var timestampResponse = await httpClient.GetAsync($"{tufUrl}/timestamp.json", cancellationToken);
if (!timestampResponse.IsSuccessStatusCode)
{
WriteError($"Failed to fetch timestamp.json: {timestampResponse.StatusCode}", output);
return 1;
}
var timestampContent = await timestampResponse.Content.ReadAsStringAsync(cancellationToken);
await File.WriteAllTextAsync(Path.Combine(cachePath, "timestamp.json"), timestampContent, cancellationToken);
// Fetch snapshot
var snapshotResponse = await httpClient.GetAsync($"{tufUrl}/snapshot.json", cancellationToken);
if (snapshotResponse.IsSuccessStatusCode)
{
var snapshotContent = await snapshotResponse.Content.ReadAsStringAsync(cancellationToken);
await File.WriteAllTextAsync(Path.Combine(cachePath, "snapshot.json"), snapshotContent, cancellationToken);
}
// Fetch targets
var targetsResponse = await httpClient.GetAsync($"{tufUrl}/targets.json", cancellationToken);
if (targetsResponse.IsSuccessStatusCode)
{
var targetsContent = await targetsResponse.Content.ReadAsStringAsync(cancellationToken);
await File.WriteAllTextAsync(Path.Combine(cachePath, "targets.json"), targetsContent, cancellationToken);
}
var result = new TrustSyncResult
{
Success = true,
SyncedAt = DateTimeOffset.UtcNow,
TufUrl = config.TufUrl
};
WriteResult(result, output, "TUF metadata synced successfully.");
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to sync trust metadata");
WriteError($"Sync failed: {ex.Message}", output);
return 1;
}
}
/// <summary>
/// Handle 'stella trust status' command.
/// </summary>
public static async Task<int> HandleStatusAsync(
IServiceProvider services,
string output,
bool showKeys,
bool showEndpoints,
bool verbose,
CancellationToken cancellationToken)
{
try
{
var cachePath = GetDefaultCachePath();
var configPath = Path.Combine(cachePath, "trust-config.json");
if (!File.Exists(configPath))
{
WriteError("Trust repository not initialized. Run 'stella trust init' first.", output);
return 1;
}
var configJson = await File.ReadAllTextAsync(configPath, cancellationToken);
var config = JsonSerializer.Deserialize<TrustInitConfig>(configJson, JsonOptions);
// Check metadata freshness
var timestampPath = Path.Combine(cachePath, "timestamp.json");
var rootPath = Path.Combine(cachePath, "root.json");
DateTimeOffset? lastSync = null;
int? rootVersion = null;
if (File.Exists(timestampPath))
{
lastSync = File.GetLastWriteTimeUtc(timestampPath);
}
if (File.Exists(rootPath))
{
var rootJson = await File.ReadAllTextAsync(rootPath, cancellationToken);
// Parse version from root (simplified - in production use proper TUF parsing)
if (rootJson.Contains("\"version\":"))
{
var versionMatch = System.Text.RegularExpressions.Regex.Match(rootJson, @"""version""\s*:\s*(\d+)");
if (versionMatch.Success)
{
rootVersion = int.Parse(versionMatch.Groups[1].Value);
}
}
}
var status = new TrustStatusResult
{
Initialized = true,
TufUrl = config?.TufUrl,
CachePath = cachePath,
OfflineMode = config?.OfflineMode ?? false,
LastSync = lastSync,
RootVersion = rootVersion,
ServiceMapTarget = config?.ServiceMapTarget,
PinnedKeys = config?.RekorKeyTargets ?? new List<string>()
};
if (output == "json")
{
Console.WriteLine(JsonSerializer.Serialize(status, JsonOptions));
}
else
{
Console.WriteLine("Trust Repository Status");
Console.WriteLine("=======================");
Console.WriteLine($"TUF URL: {status.TufUrl}");
Console.WriteLine($"Cache Path: {status.CachePath}");
Console.WriteLine($"Offline Mode: {status.OfflineMode}");
Console.WriteLine($"Root Version: {status.RootVersion?.ToString() ?? "N/A"}");
Console.WriteLine($"Last Sync: {status.LastSync?.ToString("u") ?? "Never"}");
Console.WriteLine($"Service Map: {status.ServiceMapTarget}");
if (showKeys && status.PinnedKeys.Count > 0)
{
Console.WriteLine("\nPinned Keys:");
foreach (var key in status.PinnedKeys)
{
Console.WriteLine($" - {key}");
}
}
if (showEndpoints && status.TufUrl != null)
{
Console.WriteLine("\nDiscovered Endpoints:");
Console.WriteLine(" (Use --show-endpoints with initialized service map)");
}
}
return 0;
}
catch (Exception ex)
{
WriteError($"Failed to get status: {ex.Message}", output);
return 1;
}
}
/// <summary>
/// Handle 'stella trust verify' command.
/// </summary>
public static async Task<int> HandleVerifyAsync(
IServiceProvider services,
string artifact,
bool checkInclusion,
bool offline,
string output,
bool verbose,
CancellationToken cancellationToken)
{
var logger = services.GetRequiredService<ILogger<object>>();
try
{
// Placeholder implementation - actual verification would use attestor services
Console.WriteLine($"Verifying artifact: {artifact}");
Console.WriteLine($"Check inclusion: {checkInclusion}");
Console.WriteLine($"Offline mode: {offline}");
var result = new TrustVerifyResult
{
Artifact = artifact,
Verified = true,
CheckedInclusion = checkInclusion,
OfflineMode = offline,
VerifiedAt = DateTimeOffset.UtcNow
};
WriteResult(result, output, $"Artifact verified: {artifact}");
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Verification failed");
WriteError($"Verification failed: {ex.Message}", output);
return 1;
}
}
/// <summary>
/// Handle 'stella trust export' command.
/// </summary>
public static async Task<int> HandleExportAsync(
IServiceProvider services,
string outputPath,
bool includeTargets,
bool verbose,
CancellationToken cancellationToken)
{
try
{
var cachePath = GetDefaultCachePath();
if (!Directory.Exists(cachePath))
{
Console.Error.WriteLine("Trust repository not initialized.");
return 1;
}
// Create output directory
Directory.CreateDirectory(outputPath);
// Copy TUF metadata
var metadataFiles = new[] { "root.json", "snapshot.json", "timestamp.json", "targets.json", "trust-config.json" };
foreach (var file in metadataFiles)
{
var sourcePath = Path.Combine(cachePath, file);
if (File.Exists(sourcePath))
{
var destPath = Path.Combine(outputPath, file);
File.Copy(sourcePath, destPath, overwrite: true);
if (verbose)
{
Console.WriteLine($"Exported: {file}");
}
}
}
// Copy targets if requested
if (includeTargets)
{
var targetsDir = Path.Combine(cachePath, "targets");
if (Directory.Exists(targetsDir))
{
var destTargetsDir = Path.Combine(outputPath, "targets");
Directory.CreateDirectory(destTargetsDir);
foreach (var file in Directory.GetFiles(targetsDir))
{
var destPath = Path.Combine(destTargetsDir, Path.GetFileName(file));
File.Copy(file, destPath, overwrite: true);
if (verbose)
{
Console.WriteLine($"Exported target: {Path.GetFileName(file)}");
}
}
}
}
Console.WriteLine($"Trust state exported to: {outputPath}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Export failed: {ex.Message}");
return 1;
}
}
/// <summary>
/// Handle 'stella trust import' command.
/// Sprint: SPRINT_20260125_002 - PROXY-005
/// </summary>
public static async Task<int> HandleImportAsync(
IServiceProvider services,
string bundlePath,
bool verifyManifest,
string? rejectIfStale,
bool force,
string output,
bool verbose,
CancellationToken cancellationToken)
{
var logger = services.GetRequiredService<ILogger<object>>();
try
{
var cachePath = GetDefaultCachePath();
// Check if bundle is an archive (tar.zst, tar.gz, etc.)
if (bundlePath.EndsWith(".tar.zst") || bundlePath.EndsWith(".tar.gz") || bundlePath.EndsWith(".tar"))
{
return await ImportArchiveAsync(
services,
bundlePath,
cachePath,
verifyManifest,
rejectIfStale,
force,
output,
verbose,
cancellationToken);
}
if (!Directory.Exists(bundlePath))
{
WriteError($"Bundle not found: {bundlePath}", output);
return 1;
}
// Check staleness if specified
if (!string.IsNullOrEmpty(rejectIfStale))
{
var timestampPath = Path.Combine(bundlePath, "timestamp.json");
if (File.Exists(timestampPath))
{
var lastWrite = File.GetLastWriteTimeUtc(timestampPath);
var threshold = ParseTimeSpan(rejectIfStale);
var age = DateTimeOffset.UtcNow - lastWrite;
if (age > threshold && !force)
{
WriteError($"Bundle is stale (age: {age.TotalHours:F1}h, threshold: {threshold.TotalHours:F1}h). Use --force to import anyway.", output);
return 1;
}
}
}
// Create cache directory
Directory.CreateDirectory(cachePath);
// Copy files
var importedCount = 0;
foreach (var file in Directory.GetFiles(bundlePath))
{
var destPath = Path.Combine(cachePath, Path.GetFileName(file));
File.Copy(file, destPath, overwrite: true);
importedCount++;
if (verbose)
{
Console.WriteLine($"Imported: {Path.GetFileName(file)}");
}
}
// Copy targets subdirectory if exists
var targetsDir = Path.Combine(bundlePath, "targets");
if (Directory.Exists(targetsDir))
{
var destTargetsDir = Path.Combine(cachePath, "targets");
Directory.CreateDirectory(destTargetsDir);
foreach (var file in Directory.GetFiles(targetsDir))
{
var destPath = Path.Combine(destTargetsDir, Path.GetFileName(file));
File.Copy(file, destPath, overwrite: true);
importedCount++;
}
}
// Copy tiles subdirectory if exists
var tilesDir = Path.Combine(bundlePath, "tiles");
if (Directory.Exists(tilesDir))
{
var destTilesDir = Path.Combine(cachePath, "tiles");
CopyDirectory(tilesDir, destTilesDir, verbose);
}
var result = new TrustImportResult
{
Success = true,
SourcePath = bundlePath,
DestinationPath = cachePath,
ImportedFiles = importedCount,
ImportedAt = DateTimeOffset.UtcNow
};
WriteResult(result, output, $"Imported {importedCount} files to: {cachePath}");
return 0;
}
catch (Exception ex)
{
logger.LogError(ex, "Import failed");
WriteError($"Import failed: {ex.Message}", output);
return 1;
}
}
/// <summary>
/// Import from a compressed archive using TrustSnapshotImporter.
/// </summary>
private static async Task<int> ImportArchiveAsync(
IServiceProvider services,
string archivePath,
string cachePath,
bool verifyManifest,
string? rejectIfStale,
bool force,
string output,
bool verbose,
CancellationToken cancellationToken)
{
var logger = services.GetRequiredService<ILogger<object>>();
if (!File.Exists(archivePath))
{
WriteError($"Archive not found: {archivePath}", output);
return 1;
}
Console.WriteLine($"Importing trust snapshot from: {archivePath}");
// Parse staleness threshold
TimeSpan? stalenessThreshold = null;
if (!string.IsNullOrEmpty(rejectIfStale))
{
stalenessThreshold = ParseTimeSpan(rejectIfStale);
}
// Create importer options
var options = new TrustSnapshotImportOptions
{
TufCachePath = cachePath,
TileCachePath = Path.Combine(cachePath, "tiles"),
VerifyManifest = verifyManifest,
RejectIfStale = stalenessThreshold,
Force = force
};
// Create the importer
var importer = new TrustSnapshotImporter();
// Validate first if requested
if (verifyManifest)
{
Console.WriteLine("Validating bundle manifest...");
var validationResult = await importer.ValidateAsync(archivePath, cancellationToken);
if (!validationResult.IsValid)
{
if (!force)
{
WriteError($"Bundle validation failed: {validationResult.Error}", output);
return 1;
}
Console.WriteLine($"Warning: Bundle validation failed ({validationResult.Error}), continuing with --force");
}
else
{
Console.WriteLine("Bundle validation passed.");
}
}
// Perform the import
var result = await importer.ImportAsync(archivePath, options, cancellationToken);
if (!result.IsSuccess)
{
WriteError($"Import failed: {result.Error}", output);
return 1;
}
var tufFilesCount = result.TufResult?.ImportedFiles.Count ?? 0;
var tilesCount = result.TileResult?.ImportedCount ?? 0;
var bundleId = result.Manifest?.BundleId;
var treeSize = result.Manifest?.TreeSize ?? 0;
var importResult = new TrustImportResult
{
Success = true,
SourcePath = archivePath,
DestinationPath = cachePath,
BundleId = bundleId,
ImportedFiles = tufFilesCount + tilesCount,
ImportedTiles = tilesCount,
TreeSize = treeSize,
ImportedAt = DateTimeOffset.UtcNow
};
if (output == "json")
{
Console.WriteLine(JsonSerializer.Serialize(importResult, JsonOptions));
}
else
{
Console.WriteLine($"\nImport completed successfully:");
Console.WriteLine($" Bundle ID: {bundleId}");
Console.WriteLine($" TUF files: {tufFilesCount}");
Console.WriteLine($" Tiles: {tilesCount}");
Console.WriteLine($" Tree size: {treeSize:N0}");
Console.WriteLine($" Cache path: {cachePath}");
}
return 0;
}
private static void CopyDirectory(string sourceDir, string destDir, bool verbose)
{
Directory.CreateDirectory(destDir);
foreach (var file in Directory.GetFiles(sourceDir))
{
var destPath = Path.Combine(destDir, Path.GetFileName(file));
File.Copy(file, destPath, overwrite: true);
if (verbose)
{
Console.WriteLine($"Copied: {Path.GetFileName(file)}");
}
}
foreach (var dir in Directory.GetDirectories(sourceDir))
{
var destSubDir = Path.Combine(destDir, Path.GetFileName(dir));
CopyDirectory(dir, destSubDir, verbose);
}
}
/// <summary>
/// Handle 'stella trust snapshot export' command.
/// </summary>
public static async Task<int> HandleSnapshotExportAsync(
IServiceProvider services,
string outputPath,
string? fromProxy,
string? tilesPath,
string? includeEntries,
int depth,
bool verbose,
CancellationToken cancellationToken)
{
try
{
Console.WriteLine($"Creating snapshot: {outputPath}");
Console.WriteLine($" Proxy: {fromProxy ?? "upstream"}");
Console.WriteLine($" Tiles: {tilesPath ?? "fetch new"}");
Console.WriteLine($" Depth: {depth} entries");
// Create output directory
var outputDir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(outputDir))
{
Directory.CreateDirectory(outputDir);
}
// TODO: Implement actual snapshot creation
// This would:
// 1. Export TUF metadata
// 2. Export tiles for the specified depth
// 3. Export checkpoint
// 4. Create manifest
// 5. Package as tar.zst
Console.WriteLine("\nSnapshot export not yet fully implemented.");
Console.WriteLine("Required components:");
Console.WriteLine(" - TUF metadata (from local cache)");
Console.WriteLine(" - Rekor tiles (from proxy or upstream)");
Console.WriteLine(" - Signed checkpoint");
Console.WriteLine(" - Manifest with hashes");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Snapshot export failed: {ex.Message}");
return 1;
}
}
private static string GetDefaultCachePath()
{
var basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (string.IsNullOrEmpty(basePath))
{
basePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".local",
"share");
}
return Path.Combine(basePath, "StellaOps", "TufCache");
}
private static TimeSpan ParseTimeSpan(string value)
{
if (value.EndsWith("d"))
{
return TimeSpan.FromDays(double.Parse(value.TrimEnd('d')));
}
if (value.EndsWith("h"))
{
return TimeSpan.FromHours(double.Parse(value.TrimEnd('h')));
}
if (value.EndsWith("m"))
{
return TimeSpan.FromMinutes(double.Parse(value.TrimEnd('m')));
}
return TimeSpan.FromDays(7); // Default
}
private static void WriteError(string message, string output)
{
if (output == "json")
{
Console.WriteLine(JsonSerializer.Serialize(new { error = message }, JsonOptions));
}
else
{
Console.Error.WriteLine($"Error: {message}");
}
}
private static void WriteResult<T>(T result, string output, string textMessage)
{
if (output == "json")
{
Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions));
}
else
{
Console.WriteLine(textMessage);
}
}
// Result models
private record TrustInitConfig
{
public string TufUrl { get; init; } = string.Empty;
public string ServiceMapTarget { get; init; } = string.Empty;
public List<string> RekorKeyTargets { get; init; } = new();
public bool OfflineMode { get; init; }
public DateTimeOffset InitializedAt { get; init; }
}
private record TrustInitResult
{
public bool Success { get; init; }
public string TufUrl { get; init; } = string.Empty;
public string CachePath { get; init; } = string.Empty;
public string ServiceMapTarget { get; init; } = string.Empty;
public List<string> PinnedKeys { get; init; } = new();
public bool OfflineMode { get; init; }
}
private record TrustSyncResult
{
public bool Success { get; init; }
public DateTimeOffset SyncedAt { get; init; }
public string TufUrl { get; init; } = string.Empty;
}
private record TrustStatusResult
{
public bool Initialized { get; init; }
public string? TufUrl { get; init; }
public string CachePath { get; init; } = string.Empty;
public bool OfflineMode { get; init; }
public DateTimeOffset? LastSync { get; init; }
public int? RootVersion { get; init; }
public string? ServiceMapTarget { get; init; }
public List<string> PinnedKeys { get; init; } = new();
}
private record TrustVerifyResult
{
public string Artifact { get; init; } = string.Empty;
public bool Verified { get; init; }
public bool CheckedInclusion { get; init; }
public bool OfflineMode { get; init; }
public DateTimeOffset VerifiedAt { get; init; }
}
private record TrustImportResult
{
public bool Success { get; init; }
public string SourcePath { get; init; } = string.Empty;
public string DestinationPath { get; init; } = string.Empty;
public string? BundleId { get; init; }
public int ImportedFiles { get; init; }
public int ImportedTiles { get; init; }
public long? TreeSize { get; init; }
public DateTimeOffset ImportedAt { get; init; }
}
}