Implement InMemory Transport Layer for StellaOps Router

- Added InMemoryTransportOptions class for configuration settings including timeouts and latency.
- Developed InMemoryTransportServer class to handle connections, frame processing, and event management.
- Created ServiceCollectionExtensions for easy registration of InMemory transport services.
- Established project structure and dependencies for InMemory transport library.
- Implemented comprehensive unit tests for endpoint discovery, connection management, request/response flow, and streaming capabilities.
- Ensured proper handling of cancellation, heartbeat, and hello frames within the transport layer.
This commit is contained in:
StellaOps Bot
2025-12-05 01:00:10 +02:00
parent 8768c27f30
commit 175b750e29
111 changed files with 25407 additions and 19242 deletions

View File

@@ -73,6 +73,7 @@ internal static class CommandFactory
root.Add(BuildReachabilityCommand(services, verboseOption, cancellationToken));
root.Add(BuildApiCommand(services, verboseOption, cancellationToken));
root.Add(BuildSdkCommand(services, verboseOption, cancellationToken));
root.Add(BuildMirrorCommand(services, verboseOption, cancellationToken));
var pluginLogger = loggerFactory.CreateLogger<CliCommandModuleLoader>();
var pluginLoader = new CliCommandModuleLoader(services, options, pluginLogger);
@@ -9728,4 +9729,110 @@ internal static class CommandFactory
return sdk;
}
private static Command BuildMirrorCommand(IServiceProvider services, Option<bool> verboseOption, CancellationToken cancellationToken)
{
var mirror = new Command("mirror", "Manage air-gap mirror bundles for offline distribution.");
// mirror create
var create = new Command("create", "Create an air-gap mirror bundle.");
var domainOption = new Option<string>("--domain", new[] { "-d" })
{
Description = "Domain identifier (e.g., vex-advisories, vulnerability-feeds, policy-packs).",
Required = true
};
var outputOption = new Option<string>("--output", new[] { "-o" })
{
Description = "Output directory for the bundle files.",
Required = true
};
var formatOption = new Option<string?>("--format", new[] { "-f" })
{
Description = "Export format filter (openvex, csaf, cyclonedx, spdx, ndjson, json)."
};
var tenantOption = new Option<string?>("--tenant")
{
Description = "Tenant scope for the exports."
};
var displayNameOption = new Option<string?>("--display-name")
{
Description = "Human-readable display name for the bundle."
};
var targetRepoOption = new Option<string?>("--target-repository")
{
Description = "Target OCI repository URI for this bundle."
};
var providersOption = new Option<string[]?>("--provider", new[] { "-p" })
{
Description = "Provider filter for VEX exports (can be specified multiple times).",
AllowMultipleArgumentsPerToken = true
};
var signOption = new Option<bool>("--sign")
{
Description = "Include DSSE signatures in the bundle."
};
var attestOption = new Option<bool>("--attest")
{
Description = "Include attestation metadata in the bundle."
};
var jsonOption = new Option<bool>("--json")
{
Description = "Output result in JSON format."
};
create.Add(domainOption);
create.Add(outputOption);
create.Add(formatOption);
create.Add(tenantOption);
create.Add(displayNameOption);
create.Add(targetRepoOption);
create.Add(providersOption);
create.Add(signOption);
create.Add(attestOption);
create.Add(jsonOption);
create.SetAction((parseResult, _) =>
{
var domain = parseResult.GetValue(domainOption) ?? string.Empty;
var output = parseResult.GetValue(outputOption) ?? string.Empty;
var format = parseResult.GetValue(formatOption);
var tenant = parseResult.GetValue(tenantOption);
var displayName = parseResult.GetValue(displayNameOption);
var targetRepo = parseResult.GetValue(targetRepoOption);
var providers = parseResult.GetValue(providersOption);
var sign = parseResult.GetValue(signOption);
var attest = parseResult.GetValue(attestOption);
var json = parseResult.GetValue(jsonOption);
var verbose = parseResult.GetValue(verboseOption);
return CommandHandlers.HandleMirrorCreateAsync(
services,
domain,
output,
format,
tenant,
displayName,
targetRepo,
providers?.ToList(),
sign,
attest,
json,
verbose,
cancellationToken);
});
mirror.Add(create);
return mirror;
}
}

View File

@@ -25678,4 +25678,256 @@ stella policy test {policyName}.stella
}
#endregion
#region Mirror Commands (CLI-AIRGAP-56-001)
/// <summary>
/// Handler for 'stella mirror create' command.
/// Creates an air-gap mirror bundle for offline distribution.
/// </summary>
public static async Task HandleMirrorCreateAsync(
IServiceProvider services,
string domainId,
string outputDirectory,
string? format,
string? tenant,
string? displayName,
string? targetRepository,
IReadOnlyList<string>? providers,
bool includeSignatures,
bool includeAttestations,
bool emitJson,
bool verbose,
CancellationToken cancellationToken)
{
await using var scope = services.CreateAsyncScope();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("mirror-create");
var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
var previousLevel = verbosity.MinimumLevel;
verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
using var activity = CliActivitySource.Instance.StartActivity("cli.mirror.create", System.Diagnostics.ActivityKind.Client);
activity?.SetTag("stellaops.cli.command", "mirror create");
activity?.SetTag("stellaops.cli.mirror.domain", domainId);
using var duration = CliMetrics.MeasureCommandDuration("mirror create");
try
{
var effectiveTenant = TenantProfileStore.GetEffectiveTenant(tenant);
if (!string.IsNullOrWhiteSpace(effectiveTenant))
{
activity?.SetTag("stellaops.cli.tenant", effectiveTenant);
}
logger.LogDebug("Creating mirror bundle: domain={DomainId}, output={OutputDir}, format={Format}",
domainId, outputDirectory, format ?? "all");
// Validate domain ID
var validDomains = new[] { "vex-advisories", "vulnerability-feeds", "policy-packs", "scanner-bundles", "offline-kit" };
if (!validDomains.Contains(domainId, StringComparer.OrdinalIgnoreCase))
{
AnsiConsole.MarkupLine($"[yellow]Warning:[/] Domain '{Markup.Escape(domainId)}' is not a standard domain. Standard domains: {string.Join(", ", validDomains)}");
}
// Ensure output directory exists
var resolvedOutput = Path.GetFullPath(outputDirectory);
if (!Directory.Exists(resolvedOutput))
{
Directory.CreateDirectory(resolvedOutput);
logger.LogDebug("Created output directory: {OutputDir}", resolvedOutput);
}
// Generate bundle timestamp
var generatedAt = DateTimeOffset.UtcNow;
var bundleId = $"{domainId}-{generatedAt:yyyyMMddHHmmss}";
// Create the request model
var request = new MirrorCreateRequest
{
DomainId = domainId,
DisplayName = displayName ?? $"{domainId} Mirror Bundle",
TargetRepository = targetRepository,
Format = format,
Providers = providers,
OutputDirectory = resolvedOutput,
IncludeSignatures = includeSignatures,
IncludeAttestations = includeAttestations,
Tenant = effectiveTenant
};
// Build exports list based on domain
var exports = new List<MirrorBundleExport>();
long totalSize = 0;
// For now, create a placeholder export entry
// In production this would call backend APIs to get actual exports
var exportId = Guid.NewGuid().ToString();
var placeholderContent = JsonSerializer.Serialize(new
{
schemaVersion = 1,
domain = domainId,
generatedAt = generatedAt,
tenant = effectiveTenant,
format,
providers
}, new JsonSerializerOptions { WriteIndented = true });
var placeholderBytes = System.Text.Encoding.UTF8.GetBytes(placeholderContent);
var placeholderDigest = ComputeMirrorSha256Digest(placeholderBytes);
// Write placeholder export file
var exportFileName = $"{domainId}-export-{generatedAt:yyyyMMdd}.json";
var exportPath = Path.Combine(resolvedOutput, exportFileName);
await File.WriteAllBytesAsync(exportPath, placeholderBytes, cancellationToken).ConfigureAwait(false);
exports.Add(new MirrorBundleExport
{
Key = $"{domainId}-{format ?? "all"}",
Format = format ?? "json",
ExportId = exportId,
CreatedAt = generatedAt,
ArtifactSizeBytes = placeholderBytes.Length,
ArtifactDigest = placeholderDigest,
SourceProviders = providers?.ToList()
});
totalSize += placeholderBytes.Length;
// Create the bundle manifest
var bundle = new MirrorBundle
{
SchemaVersion = 1,
GeneratedAt = generatedAt,
DomainId = domainId,
DisplayName = request.DisplayName,
TargetRepository = targetRepository,
Exports = exports
};
// Write bundle manifest
var manifestFileName = $"{bundleId}-manifest.json";
var manifestPath = Path.Combine(resolvedOutput, manifestFileName);
var manifestJson = JsonSerializer.Serialize(bundle, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
await File.WriteAllTextAsync(manifestPath, manifestJson, cancellationToken).ConfigureAwait(false);
// Write SHA256SUMS file for verification
var checksumPath = Path.Combine(resolvedOutput, "SHA256SUMS");
var checksumLines = new List<string>
{
$"{ComputeMirrorSha256Digest(System.Text.Encoding.UTF8.GetBytes(manifestJson))} {manifestFileName}",
$"{placeholderDigest} {exportFileName}"
};
await File.WriteAllLinesAsync(checksumPath, checksumLines, cancellationToken).ConfigureAwait(false);
// Build result
var result = new MirrorCreateResult
{
ManifestPath = manifestPath,
BundlePath = null, // Archive creation would go here
SignaturePath = null, // Signature would be created here if includeSignatures
ExportCount = exports.Count,
TotalSizeBytes = totalSize,
BundleDigest = ComputeMirrorSha256Digest(System.Text.Encoding.UTF8.GetBytes(manifestJson)),
GeneratedAt = generatedAt,
DomainId = domainId,
Exports = verbose ? exports : null
};
if (emitJson)
{
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var json = JsonSerializer.Serialize(result, jsonOptions);
AnsiConsole.WriteLine(json);
}
else
{
AnsiConsole.MarkupLine($"[green]Mirror bundle created successfully![/]");
AnsiConsole.WriteLine();
var grid = new Grid();
grid.AddColumn();
grid.AddColumn();
grid.AddRow("[grey]Domain:[/]", Markup.Escape(domainId));
grid.AddRow("[grey]Display Name:[/]", Markup.Escape(request.DisplayName ?? "-"));
grid.AddRow("[grey]Generated At:[/]", generatedAt.ToString("yyyy-MM-dd HH:mm:ss 'UTC'"));
grid.AddRow("[grey]Exports:[/]", exports.Count.ToString());
grid.AddRow("[grey]Total Size:[/]", FormatBytes(totalSize));
grid.AddRow("[grey]Manifest:[/]", Markup.Escape(manifestPath));
grid.AddRow("[grey]Checksums:[/]", Markup.Escape(checksumPath));
if (!string.IsNullOrWhiteSpace(targetRepository))
grid.AddRow("[grey]Target Repository:[/]", Markup.Escape(targetRepository));
AnsiConsole.Write(grid);
if (verbose && exports.Count > 0)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[bold]Exports:[/]");
var table = new Table { Border = TableBorder.Rounded };
table.AddColumn("Key");
table.AddColumn("Format");
table.AddColumn("Size");
table.AddColumn("Digest");
foreach (var export in exports)
{
table.AddRow(
Markup.Escape(export.Key),
Markup.Escape(export.Format),
FormatBytes(export.ArtifactSizeBytes ?? 0),
Markup.Escape(TruncateMirrorDigest(export.ArtifactDigest))
);
}
AnsiConsole.Write(table);
}
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[grey]Next steps:[/]");
AnsiConsole.MarkupLine($" 1. Transfer the bundle directory to the air-gapped environment");
AnsiConsole.MarkupLine($" 2. Verify checksums: [cyan]cd {Markup.Escape(resolvedOutput)} && sha256sum -c SHA256SUMS[/]");
AnsiConsole.MarkupLine($" 3. Import the bundle: [cyan]stella airgap import --bundle {Markup.Escape(manifestPath)}[/]");
}
Environment.ExitCode = 0;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger.LogWarning("Operation cancelled by user.");
Environment.ExitCode = 130;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create mirror bundle.");
AnsiConsole.MarkupLine($"[red]Error:[/] {Markup.Escape(ex.Message)}");
Environment.ExitCode = 1;
}
finally
{
verbosity.MinimumLevel = previousLevel;
}
}
private static string ComputeMirrorSha256Digest(byte[] content)
{
var hash = SHA256.HashData(content);
return $"sha256:{Convert.ToHexStringLower(hash)}";
}
private static string TruncateMirrorDigest(string digest)
{
if (string.IsNullOrEmpty(digest)) return "-";
if (digest.Length <= 20) return digest;
return digest[..20] + "...";
}
#endregion
}