416 lines
18 KiB
C#
416 lines
18 KiB
C#
using System;
|
|
using System.CommandLine;
|
|
using System.CommandLine.Invocation;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Auth.Client;
|
|
using StellaOps.Cli.Commands;
|
|
using StellaOps.Cli.Commands.Scan;
|
|
using StellaOps.Cli.Configuration;
|
|
using StellaOps.Cli.Services;
|
|
using StellaOps.Cli.Telemetry;
|
|
using StellaOps.AirGap.Policy;
|
|
using StellaOps.Configuration;
|
|
using StellaOps.Attestor.StandardPredicates.BinaryDiff;
|
|
using StellaOps.Policy.Scoring.Engine;
|
|
using StellaOps.ExportCenter.Client;
|
|
using StellaOps.ExportCenter.Core.EvidenceCache;
|
|
using StellaOps.Verdict;
|
|
using StellaOps.Excititor.Core.Evidence;
|
|
using StellaOps.Scanner.Storage.Oci;
|
|
using StellaOps.Scanner.PatchVerification.DependencyInjection;
|
|
using StellaOps.Scanner.Analyzers.Native;
|
|
using StellaOps.Doctor.DependencyInjection;
|
|
using StellaOps.Doctor.Plugins.Core.DependencyInjection;
|
|
using StellaOps.Doctor.Plugins.Database.DependencyInjection;
|
|
#if DEBUG || STELLAOPS_ENABLE_SIMULATOR
|
|
using StellaOps.Cryptography.Plugin.SimRemote.DependencyInjection;
|
|
#endif
|
|
|
|
namespace StellaOps.Cli;
|
|
|
|
internal static class Program
|
|
{
|
|
internal static async Task<int> Main(string[] args)
|
|
{
|
|
var (options, configuration) = CliBootstrapper.Build(args);
|
|
|
|
var services = new ServiceCollection();
|
|
services.AddSingleton(configuration);
|
|
services.AddSingleton(options);
|
|
services.AddOptions();
|
|
|
|
var verbosityState = new VerbosityState();
|
|
services.AddSingleton(verbosityState);
|
|
services.AddAirGapEgressPolicy(configuration);
|
|
services.AddStellaOpsCrypto(options.Crypto);
|
|
|
|
// Conditionally register regional crypto plugins based on distribution build
|
|
#if STELLAOPS_ENABLE_GOST
|
|
services.AddGostCryptoProviders(configuration);
|
|
#endif
|
|
|
|
#if STELLAOPS_ENABLE_EIDAS
|
|
services.AddEidasCryptoProviders(configuration);
|
|
#endif
|
|
|
|
#if STELLAOPS_ENABLE_SM
|
|
services.AddSmSoftCryptoProvider(configuration);
|
|
services.AddSmRemoteCryptoProvider(configuration);
|
|
#endif
|
|
|
|
#if DEBUG || STELLAOPS_ENABLE_SIMULATOR
|
|
services.AddSimRemoteCryptoProvider(configuration);
|
|
#endif
|
|
|
|
// CLI-AIRGAP-56-002: Add sealed mode telemetry for air-gapped operation
|
|
services.AddSealedModeTelemetryIfOffline(
|
|
options.IsOffline,
|
|
options.IsOffline ? Path.Combine(options.Offline.KitsDirectory, "telemetry") : null);
|
|
|
|
services.AddLogging(builder =>
|
|
{
|
|
builder.ClearProviders();
|
|
builder.AddSimpleConsole(logOptions =>
|
|
{
|
|
logOptions.TimestampFormat = "HH:mm:ss ";
|
|
logOptions.SingleLine = true;
|
|
});
|
|
builder.AddFilter((category, level) => level >= verbosityState.MinimumLevel);
|
|
});
|
|
|
|
if (!string.IsNullOrWhiteSpace(options.Authority.Url))
|
|
{
|
|
services.AddStellaOpsAuthClient(clientOptions =>
|
|
{
|
|
clientOptions.Authority = options.Authority.Url;
|
|
clientOptions.ClientId = options.Authority.ClientId ?? string.Empty;
|
|
clientOptions.ClientSecret = options.Authority.ClientSecret;
|
|
clientOptions.DefaultScopes.Clear();
|
|
clientOptions.DefaultScopes.Add(string.IsNullOrWhiteSpace(options.Authority.Scope)
|
|
? StellaOps.Auth.Abstractions.StellaOpsScopes.ConcelierJobsTrigger
|
|
: options.Authority.Scope);
|
|
|
|
var resilience = options.Authority.Resilience ?? new StellaOpsCliAuthorityResilienceOptions();
|
|
clientOptions.EnableRetries = resilience.EnableRetries ?? true;
|
|
|
|
if (resilience.RetryDelays is { Count: > 0 })
|
|
{
|
|
clientOptions.RetryDelays.Clear();
|
|
foreach (var delay in resilience.RetryDelays)
|
|
{
|
|
if (delay > TimeSpan.Zero)
|
|
{
|
|
clientOptions.RetryDelays.Add(delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (resilience.AllowOfflineCacheFallback.HasValue)
|
|
{
|
|
clientOptions.AllowOfflineCacheFallback = resilience.AllowOfflineCacheFallback.Value;
|
|
}
|
|
|
|
if (resilience.OfflineCacheTolerance.HasValue && resilience.OfflineCacheTolerance.Value >= TimeSpan.Zero)
|
|
{
|
|
clientOptions.OfflineCacheTolerance = resilience.OfflineCacheTolerance.Value;
|
|
}
|
|
});
|
|
|
|
var cacheDirectory = options.Authority.TokenCacheDirectory;
|
|
if (!string.IsNullOrWhiteSpace(cacheDirectory))
|
|
{
|
|
Directory.CreateDirectory(cacheDirectory);
|
|
services.AddStellaOpsFileTokenCache(cacheDirectory);
|
|
}
|
|
|
|
services.AddHttpClient<IAuthorityRevocationClient, AuthorityRevocationClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(2);
|
|
if (Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var authorityUri))
|
|
{
|
|
client.BaseAddress = authorityUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "authority-revocation");
|
|
|
|
services.AddHttpClient<IAuthorityConsoleClient, AuthorityConsoleClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
if (Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var authorityUri))
|
|
{
|
|
client.BaseAddress = authorityUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "authority-console");
|
|
}
|
|
|
|
services.AddHttpClient<IBackendOperationsClient, BackendOperationsClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(5);
|
|
if (!string.IsNullOrWhiteSpace(options.BackendUrl) &&
|
|
Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var backendUri))
|
|
{
|
|
client.BaseAddress = backendUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "backend-api");
|
|
|
|
services.AddHttpClient<IExportCenterClient, ExportCenterClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(10);
|
|
if (!string.IsNullOrWhiteSpace(options.BackendUrl) &&
|
|
Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var exportCenterUri))
|
|
{
|
|
client.BaseAddress = exportCenterUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "export-center-api");
|
|
|
|
services.AddHttpClient<IConcelierObservationsClient, ConcelierObservationsClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl) &&
|
|
Uri.TryCreate(options.ConcelierUrl, UriKind.Absolute, out var concelierUri))
|
|
{
|
|
client.BaseAddress = concelierUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "concelier-api");
|
|
|
|
services.AddHttpClient("stellaops-cli.ingest-download")
|
|
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
|
{
|
|
AutomaticDecompression = DecompressionMethods.All
|
|
})
|
|
.AddEgressPolicyGuard("stellaops-cli", "sources-ingest");
|
|
|
|
services.AddSingleton<IScannerExecutor, ScannerExecutor>();
|
|
services.AddSingleton<IScannerInstaller, ScannerInstaller>();
|
|
services.AddSingleton<MigrationCommandService>();
|
|
services.AddSingleton(TimeProvider.System);
|
|
services.AddSingleton<IEvidenceCacheService, LocalEvidenceCacheService>();
|
|
services.AddVexEvidenceLinking(configuration);
|
|
|
|
// Doctor diagnostics engine
|
|
services.AddDoctorEngine();
|
|
services.AddDoctorCorePlugin();
|
|
services.AddDoctorDatabasePlugin();
|
|
|
|
// CLI-FORENSICS-53-001: Forensic snapshot client
|
|
services.AddHttpClient<IForensicSnapshotClient, ForensicSnapshotClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(5);
|
|
if (!string.IsNullOrWhiteSpace(options.BackendUrl) &&
|
|
Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var backendUri))
|
|
{
|
|
client.BaseAddress = backendUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "forensic-api");
|
|
|
|
// CLI-FORENSICS-54-001: Forensic verifier (local only, no HTTP)
|
|
services.AddSingleton<IForensicVerifier, ForensicVerifier>();
|
|
|
|
// CLI-FORENSICS-54-002: Attestation reader (local only, no HTTP)
|
|
services.AddSingleton<IAttestationReader, AttestationReader>();
|
|
|
|
// CLI-LNM-22-002: VEX observations client
|
|
services.AddHttpClient<IVexObservationsClient, VexObservationsClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(2);
|
|
if (!string.IsNullOrWhiteSpace(options.BackendUrl) &&
|
|
Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var backendUri))
|
|
{
|
|
client.BaseAddress = backendUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "vex-api");
|
|
|
|
// CLI-PROMO-70-001: Promotion assembler (local, may call crane/cosign)
|
|
services.AddHttpClient<IPromotionAssembler, PromotionAssembler>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(5);
|
|
});
|
|
|
|
// CLI-DETER-70-003: Determinism harness (local only, executes docker)
|
|
services.AddSingleton<IDeterminismHarness, DeterminismHarness>();
|
|
|
|
// CLI-OBS-51-001: Observability client for health metrics
|
|
services.AddHttpClient<IObservabilityClient, ObservabilityClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "observability-api");
|
|
|
|
// CLI-PACKS-42-001: Pack client for Task Pack operations
|
|
services.AddHttpClient<IPackClient, PackClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(10); // Pack operations may take longer
|
|
}).AddEgressPolicyGuard("stellaops-cli", "packs-api");
|
|
|
|
// CLI-EXC-25-001: Exception client for exception governance operations
|
|
services.AddHttpClient<IExceptionClient, ExceptionClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(60);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "exceptions-api");
|
|
|
|
// CLI-ORCH-32-001: Orchestrator client for source/job management
|
|
services.AddHttpClient<IOrchestratorClient, OrchestratorClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(60);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "orchestrator-api");
|
|
|
|
// CLI-PARITY-41-001: SBOM client for SBOM explorer
|
|
services.AddHttpClient<ISbomClient, SbomClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(60);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "sbom-api");
|
|
|
|
// VRR-021: Rationale client for verdict rationale
|
|
services.AddHttpClient<IRationaleClient, RationaleClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "triage-api");
|
|
|
|
// CLI-VERIFY-43-001: OCI registry client for verify image
|
|
services.AddHttpClient<IOciRegistryClient, OciRegistryClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(2);
|
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("StellaOps.Cli/verify-image");
|
|
}).AddEgressPolicyGuard("stellaops-cli", "oci-registry");
|
|
|
|
services.AddOciImageInspector(configuration.GetSection("OciRegistry"));
|
|
|
|
// CLI-DIFF-0001: Binary diff predicates and native analyzer support
|
|
services.AddBinaryDiffPredicates();
|
|
services.AddNativeAnalyzer(configuration);
|
|
services.AddSingleton<IBinaryDiffService, BinaryDiffService>();
|
|
services.AddSingleton<IBinaryDiffRenderer, BinaryDiffRenderer>();
|
|
|
|
services.AddSingleton<ITrustPolicyLoader, TrustPolicyLoader>();
|
|
services.AddSingleton<IDsseSignatureVerifier, DsseSignatureVerifier>();
|
|
services.AddSingleton<IImageAttestationVerifier, ImageAttestationVerifier>();
|
|
|
|
// CLI-PARITY-41-002: Notify client for notification management
|
|
services.AddHttpClient<INotifyClient, NotifyClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(60);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "notify-api");
|
|
|
|
// CLI-SBOM-60-001: Sbomer client for layer/compose operations
|
|
services.AddHttpClient<ISbomerClient, SbomerClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(5); // Composition may take longer
|
|
}).AddEgressPolicyGuard("stellaops-cli", "sbomer-api");
|
|
|
|
// CLI-CVSS-190-010: CVSS receipt client (talks to Policy Gateway /api/cvss)
|
|
services.AddHttpClient<ICvssClient, CvssClient>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(60);
|
|
}).AddEgressPolicyGuard("stellaops-cli", "cvss-api");
|
|
|
|
services.AddSingleton<ICvssV4Engine, CvssV4Engine>();
|
|
|
|
// RPL-003: VerdictBuilder for replay infrastructure (SPRINT_20260105_002_001_REPLAY)
|
|
services.AddVerdictBuilderAirGap();
|
|
|
|
// RPL-016/017: Timeline and bundle store adapters for stella prove command
|
|
services.AddHttpClient<StellaOps.Cli.Commands.ITimelineQueryAdapter,
|
|
StellaOps.Cli.Replay.TimelineQueryAdapter>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromSeconds(30);
|
|
if (!string.IsNullOrWhiteSpace(options.BackendUrl) &&
|
|
Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var backendUri))
|
|
{
|
|
client.BaseAddress = backendUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "timeline-api");
|
|
|
|
services.AddHttpClient<StellaOps.Cli.Commands.IReplayBundleStoreAdapter,
|
|
StellaOps.Cli.Replay.ReplayBundleStoreAdapter>(client =>
|
|
{
|
|
client.Timeout = TimeSpan.FromMinutes(5); // Bundle downloads may take longer
|
|
if (!string.IsNullOrWhiteSpace(options.BackendUrl) &&
|
|
Uri.TryCreate(options.BackendUrl, UriKind.Absolute, out var backendUri))
|
|
{
|
|
client.BaseAddress = backendUri;
|
|
}
|
|
}).AddEgressPolicyGuard("stellaops-cli", "replay-bundle-api");
|
|
|
|
// CLI-AIRGAP-56-001: Mirror bundle import service for air-gap operations
|
|
services.AddSingleton<StellaOps.AirGap.Importer.Repositories.IBundleCatalogRepository,
|
|
StellaOps.AirGap.Importer.Repositories.InMemoryBundleCatalogRepository>();
|
|
services.AddSingleton<StellaOps.AirGap.Importer.Repositories.IBundleItemRepository,
|
|
StellaOps.AirGap.Importer.Repositories.InMemoryBundleItemRepository>();
|
|
services.AddSingleton<IMirrorBundleImportService, MirrorBundleImportService>();
|
|
|
|
// CLI-CRYPTO-4100-001: Crypto profile validator
|
|
services.AddSingleton<CryptoProfileValidator>();
|
|
|
|
// CLI-PATCHVERIFY-001-004: Patch verification services (SPRINT_20260111_001_004)
|
|
services.AddPatchVerification();
|
|
|
|
await using var serviceProvider = services.BuildServiceProvider();
|
|
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
|
var startupLogger = loggerFactory.CreateLogger("StellaOps.Cli.Startup");
|
|
AuthorityDiagnosticsReporter.Emit(configuration, startupLogger);
|
|
|
|
// CLI-CRYPTO-4100-001: Validate crypto configuration on startup
|
|
var cryptoValidator = serviceProvider.GetRequiredService<CryptoProfileValidator>();
|
|
var cryptoValidation = cryptoValidator.Validate(serviceProvider);
|
|
if (cryptoValidation.HasWarnings)
|
|
{
|
|
foreach (var warning in cryptoValidation.Warnings)
|
|
{
|
|
startupLogger.LogWarning("Crypto: {Warning}", warning);
|
|
}
|
|
}
|
|
if (cryptoValidation.HasErrors)
|
|
{
|
|
foreach (var error in cryptoValidation.Errors)
|
|
{
|
|
startupLogger.LogError("Crypto: {Error}", error);
|
|
}
|
|
}
|
|
using var cts = new CancellationTokenSource();
|
|
Console.CancelKeyPress += (_, eventArgs) =>
|
|
{
|
|
eventArgs.Cancel = true;
|
|
cts.Cancel();
|
|
};
|
|
|
|
var rootCommand = CommandFactory.Create(serviceProvider, options, cts.Token, loggerFactory);
|
|
int commandExit;
|
|
try
|
|
{
|
|
var parseResult = rootCommand.Parse(args);
|
|
commandExit = await parseResult.InvokeAsync().ConfigureAwait(false);
|
|
}
|
|
catch (AirGapEgressBlockedException ex)
|
|
{
|
|
var guardLogger = loggerFactory.CreateLogger("StellaOps.Cli.AirGap");
|
|
guardLogger.LogError("{ErrorCode}: {Reason} Remediation: {Remediation}", AirGapEgressBlockedException.ErrorCode, ex.Reason, ex.Remediation);
|
|
|
|
if (!string.IsNullOrWhiteSpace(ex.DocumentationUrl))
|
|
{
|
|
guardLogger.LogInformation("Documentation: {DocumentationUrl}", ex.DocumentationUrl);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(ex.SupportContact))
|
|
{
|
|
guardLogger.LogInformation("Support contact: {SupportContact}", ex.SupportContact);
|
|
}
|
|
|
|
Console.Error.WriteLine(ex.Message);
|
|
return 1;
|
|
}
|
|
|
|
var finalExit = Environment.ExitCode != 0 ? Environment.ExitCode : commandExit;
|
|
if (cts.IsCancellationRequested && finalExit == 0)
|
|
{
|
|
finalExit = 130; // Typical POSIX cancellation exit code
|
|
}
|
|
|
|
return finalExit;
|
|
}
|
|
|
|
}
|