using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Globalization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using StellaOps.Auth.Abstractions; using StellaOps.Auth.Client; using StellaOps.Cli.Commands; using StellaOps.Cli.Configuration; using StellaOps.Cli.Services; using StellaOps.Cli.Services.Models; using StellaOps.Cli.Telemetry; using StellaOps.Cli.Tests.Testing; using StellaOps.Cryptography; using Spectre.Console; using Spectre.Console.Testing; namespace StellaOps.Cli.Tests.Commands; public sealed class CommandHandlersTests { [Fact] public async Task HandleExportJobAsync_SetsExitCodeZeroOnSuccess() { var original = Environment.ExitCode; try { var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", "/jobs/export:json/1", null)); var provider = BuildServiceProvider(backend); await CommandHandlers.HandleExportJobAsync( provider, format: "json", delta: false, publishFull: null, publishDelta: null, includeFull: null, includeDelta: null, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); Assert.Equal("export:json", backend.LastJobKind); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleMergeJobAsync_SetsExitCodeOnFailure() { var original = Environment.ExitCode; try { var backend = new StubBackendClient(new JobTriggerResult(false, "Job already running", null, null)); var provider = BuildServiceProvider(backend); await CommandHandlers.HandleMergeJobAsync(provider, verbose: false, CancellationToken.None); Assert.Equal(1, Environment.ExitCode); Assert.Equal("merge:reconcile", backend.LastJobKind); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleScannerRunAsync_AutomaticallyUploadsResults() { using var tempDir = new TempDirectory(); var resultsFile = Path.Combine(tempDir.Path, "results", "scan.json"); var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null)); var metadataFile = Path.Combine(tempDir.Path, "results", "scan-run.json"); var executor = new StubExecutor(new ScannerExecutionResult(0, resultsFile, metadataFile)); var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results") }; var provider = BuildServiceProvider(backend, executor, new StubInstaller(), options); Directory.CreateDirectory(Path.Combine(tempDir.Path, "target")); var original = Environment.ExitCode; try { await CommandHandlers.HandleScannerRunAsync( provider, runner: "docker", entry: "scanner-image", targetDirectory: Path.Combine(tempDir.Path, "target"), arguments: Array.Empty(), verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); Assert.Equal(resultsFile, backend.LastUploadPath); Assert.True(File.Exists(metadataFile)); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthLoginAsync_UsesClientCredentialsFlow() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", ClientSecret = "secret", Scope = "concelier.jobs.trigger", TokenCacheDirectory = tempDir.Path } }; var tokenClient = new StubTokenClient(); var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient); await CommandHandlers.HandleAuthLoginAsync(provider, options, verbose: false, force: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); Assert.Equal(1, tokenClient.ClientCredentialRequests); Assert.NotNull(tokenClient.CachedEntry); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthLoginAsync_FailsWhenPasswordMissing() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", Username = "user", TokenCacheDirectory = tempDir.Path } }; var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient()); await CommandHandlers.HandleAuthLoginAsync(provider, options, verbose: false, force: false, cancellationToken: CancellationToken.None); Assert.Equal(1, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthStatusAsync_ReportsMissingToken() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", TokenCacheDirectory = tempDir.Path } }; var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient()); await CommandHandlers.HandleAuthStatusAsync(provider, options, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(1, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleExcititorInitAsync_CallsBackend() { var original = Environment.ExitCode; try { var backend = new StubBackendClient(new JobTriggerResult(true, "accepted", null, null)); var provider = BuildServiceProvider(backend); await CommandHandlers.HandleExcititorInitAsync( provider, new[] { "redhat" }, resume: true, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); Assert.Equal("init", backend.LastExcititorRoute); Assert.Equal(HttpMethod.Post, backend.LastExcititorMethod); var payload = Assert.IsAssignableFrom>(backend.LastExcititorPayload); Assert.Equal(true, payload["resume"]); var providers = Assert.IsAssignableFrom>(payload["providers"]!); Assert.Contains("redhat", providers, StringComparer.OrdinalIgnoreCase); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleExcititorListProvidersAsync_WritesOutput() { var original = Environment.ExitCode; try { var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null)) { ProviderSummaries = new[] { new ExcititorProviderSummary("redhat", "distro", "Red Hat", "vendor", true, DateTimeOffset.UtcNow) } }; var provider = BuildServiceProvider(backend); await CommandHandlers.HandleExcititorListProvidersAsync(provider, includeDisabled: false, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleExcititorVerifyAsync_FailsWithoutArguments() { var original = Environment.ExitCode; try { var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null)); var provider = BuildServiceProvider(backend); await CommandHandlers.HandleExcititorVerifyAsync(provider, null, null, null, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(1, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleExcititorVerifyAsync_AttachesAttestationFile() { var original = Environment.ExitCode; using var tempFile = new TempFile("attestation.json", Encoding.UTF8.GetBytes("{\"ok\":true}")); try { var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null)); var provider = BuildServiceProvider(backend); await CommandHandlers.HandleExcititorVerifyAsync( provider, exportId: "export-123", digest: "sha256:abc", attestationPath: tempFile.Path, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); Assert.Equal("verify", backend.LastExcititorRoute); var payload = Assert.IsAssignableFrom>(backend.LastExcititorPayload); Assert.Equal("export-123", payload["exportId"]); Assert.Equal("sha256:abc", payload["digest"]); var attestation = Assert.IsAssignableFrom>(payload["attestation"]!); Assert.Equal(Path.GetFileName(tempFile.Path), attestation["fileName"]); Assert.NotNull(attestation["base64"]); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleExcititorExportAsync_DownloadsWhenOutputProvided() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null)); const string manifestJson = """ { "exportId": "exports/20251019T101530Z/abcdef1234567890", "format": "openvex", "createdAt": "2025-10-19T10:15:30Z", "artifact": { "algorithm": "sha256", "digest": "abcdef1234567890" }, "fromCache": false, "sizeBytes": 2048, "attestation": { "rekor": { "location": "https://rekor.example/api/v1/log/entries/123", "logIndex": "123" } } } """; backend.ExcititorResult = new ExcititorOperationResult(true, "ok", null, JsonDocument.Parse(manifestJson).RootElement.Clone()); var provider = BuildServiceProvider(backend); var outputPath = Path.Combine(tempDir.Path, "export.json"); await CommandHandlers.HandleExcititorExportAsync( provider, format: "openvex", delta: false, scope: null, since: null, provider: null, outputPath: outputPath, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); Assert.Single(backend.ExportDownloads); var request = backend.ExportDownloads[0]; Assert.Equal("exports/20251019T101530Z/abcdef1234567890", request.ExportId); Assert.Equal(Path.GetFullPath(outputPath), request.DestinationPath); Assert.Equal("sha256", request.Algorithm); Assert.Equal("abcdef1234567890", request.Digest); } finally { Environment.ExitCode = original; } } [Theory] [InlineData(null)] [InlineData("default")] [InlineData("libsodium")] public async Task HandleAuthRevokeVerifyAsync_VerifiesBundlesUsingProviderRegistry(string? providerHint) { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var artifacts = await WriteRevocationArtifactsAsync(tempDir, providerHint); await CommandHandlers.HandleAuthRevokeVerifyAsync( artifacts.BundlePath, artifacts.SignaturePath, artifacts.KeyPath, verbose: true, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthStatusAsync_ReportsCachedToken() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", TokenCacheDirectory = tempDir.Path } }; var tokenClient = new StubTokenClient(); tokenClient.CachedEntry = new StellaOpsTokenCacheEntry( "token", "Bearer", DateTimeOffset.UtcNow.AddMinutes(30), new[] { StellaOpsScopes.ConcelierJobsTrigger }); var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient); await CommandHandlers.HandleAuthStatusAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthWhoAmIAsync_ReturnsErrorWhenTokenMissing() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", TokenCacheDirectory = tempDir.Path } }; var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient()); await CommandHandlers.HandleAuthWhoAmIAsync(provider, options, verbose: false, cancellationToken: CancellationToken.None); Assert.Equal(1, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthWhoAmIAsync_ReportsClaimsForJwtToken() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", TokenCacheDirectory = tempDir.Path } }; var tokenClient = new StubTokenClient(); tokenClient.CachedEntry = new StellaOpsTokenCacheEntry( CreateUnsignedJwt( ("sub", "cli-user"), ("aud", "concelier"), ("iss", "https://authority.example"), ("iat", 1_700_000_000), ("nbf", 1_700_000_000)), "Bearer", DateTimeOffset.UtcNow.AddMinutes(30), new[] { StellaOpsScopes.ConcelierJobsTrigger }); var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient); await CommandHandlers.HandleAuthWhoAmIAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None); Assert.Equal(0, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleAuthLogoutAsync_ClearsToken() { var original = Environment.ExitCode; using var tempDir = new TempDirectory(); try { var options = new StellaOpsCliOptions { ResultsDirectory = Path.Combine(tempDir.Path, "results"), Authority = new StellaOpsCliAuthorityOptions { Url = "https://authority.example", ClientId = "cli", TokenCacheDirectory = tempDir.Path } }; var tokenClient = new StubTokenClient(); tokenClient.CachedEntry = new StellaOpsTokenCacheEntry( "token", "Bearer", DateTimeOffset.UtcNow.AddMinutes(5), new[] { StellaOpsScopes.ConcelierJobsTrigger }); var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient); await CommandHandlers.HandleAuthLogoutAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None); Assert.Null(tokenClient.CachedEntry); Assert.Equal(1, tokenClient.ClearRequests); Assert.Equal(0, Environment.ExitCode); } finally { Environment.ExitCode = original; } } [Fact] public async Task HandleRuntimePolicyTestAsync_WritesInteractiveTable() { var originalExit = Environment.ExitCode; var originalConsole = AnsiConsole.Console; var console = new TestConsole(); console.Width(120); console.Interactive(); console.EmitAnsiSequences(); AnsiConsole.Console = console; var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null)); var decisions = new Dictionary(StringComparer.Ordinal) { ["sha256:aaa"] = new RuntimePolicyImageDecision( "allow", true, true, Array.AsReadOnly(new[] { "trusted baseline" }), new RuntimePolicyRekorReference("uuid-allow", "https://rekor.example/entries/uuid-allow", true), new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal) { ["source"] = "baseline", ["quieted"] = false, ["confidence"] = 0.97, ["confidenceBand"] = "high" })), ["sha256:bbb"] = new RuntimePolicyImageDecision( "block", false, false, Array.AsReadOnly(new[] { "missing attestation" }), new RuntimePolicyRekorReference("uuid-block", "https://rekor.example/entries/uuid-block", false), new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal) { ["source"] = "policy", ["quieted"] = false, ["confidence"] = 0.12, ["confidenceBand"] = "low" })), ["sha256:ccc"] = new RuntimePolicyImageDecision( "audit", true, false, Array.AsReadOnly(new[] { "pending sbom sync" }), new RuntimePolicyRekorReference(null, null, null), new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal) { ["source"] = "mirror", ["quieted"] = true, ["quietedBy"] = "allow-temporary", ["confidence"] = 0.42, ["confidenceBand"] = "medium" })) }; backend.RuntimePolicyResult = new RuntimePolicyEvaluationResult( 300, DateTimeOffset.Parse("2025-10-19T12:00:00Z", CultureInfo.InvariantCulture), "rev-42", new ReadOnlyDictionary(decisions)); var provider = BuildServiceProvider(backend); try { await CommandHandlers.HandleRuntimePolicyTestAsync( provider, namespaceValue: "prod", imageArguments: new[] { "sha256:aaa", "sha256:bbb" }, filePath: null, labelArguments: new[] { "app=frontend" }, outputJson: false, verbose: false, cancellationToken: CancellationToken.None); var output = console.Output; Assert.Equal(0, Environment.ExitCode); Assert.Contains("Image", output, StringComparison.Ordinal); Assert.Contains("Verdict", output, StringComparison.Ordinal); Assert.Contains("SBOM Ref", output, StringComparison.Ordinal); Assert.Contains("Quieted", output, StringComparison.Ordinal); Assert.Contains("Confidence", output, StringComparison.Ordinal); Assert.Contains("sha256:aaa", output, StringComparison.Ordinal); Assert.Contains("uuid-allow", output, StringComparison.Ordinal); Assert.Contains("(verified)", output, StringComparison.Ordinal); Assert.Contains("0.97 (high)", output, StringComparison.Ordinal); Assert.Contains("sha256:bbb", output, StringComparison.Ordinal); Assert.Contains("uuid-block", output, StringComparison.Ordinal); Assert.Contains("(unverified)", output, StringComparison.Ordinal); Assert.Contains("sha256:ccc", output, StringComparison.Ordinal); Assert.Contains("yes", output, StringComparison.Ordinal); Assert.Contains("allow-temporary", output, StringComparison.Ordinal); Assert.True( output.IndexOf("sha256:aaa", StringComparison.Ordinal) < output.IndexOf("sha256:ccc", StringComparison.Ordinal)); } finally { Environment.ExitCode = originalExit; AnsiConsole.Console = originalConsole; } } [Fact] public async Task HandleRuntimePolicyTestAsync_WritesDeterministicJson() { var originalExit = Environment.ExitCode; var originalOut = Console.Out; var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null)); var decisions = new Dictionary(StringComparer.Ordinal) { ["sha256:json-a"] = new RuntimePolicyImageDecision( "allow", true, true, Array.AsReadOnly(new[] { "baseline allow" }), new RuntimePolicyRekorReference("uuid-json-allow", "https://rekor.example/entries/uuid-json-allow", true), new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal) { ["source"] = "baseline", ["confidence"] = 0.66 })), ["sha256:json-b"] = new RuntimePolicyImageDecision( "audit", true, false, Array.AsReadOnly(Array.Empty()), new RuntimePolicyRekorReference(null, null, null), new ReadOnlyDictionary(new Dictionary(StringComparer.Ordinal) { ["source"] = "mirror", ["quieted"] = true, ["quietedBy"] = "risk-accepted" })) }; backend.RuntimePolicyResult = new RuntimePolicyEvaluationResult( 600, DateTimeOffset.Parse("2025-10-20T00:00:00Z", CultureInfo.InvariantCulture), "rev-json-7", new ReadOnlyDictionary(decisions)); var provider = BuildServiceProvider(backend); using var writer = new StringWriter(); Console.SetOut(writer); try { await CommandHandlers.HandleRuntimePolicyTestAsync( provider, namespaceValue: "staging", imageArguments: new[] { "sha256:json-a", "sha256:json-b" }, filePath: null, labelArguments: Array.Empty(), outputJson: true, verbose: false, cancellationToken: CancellationToken.None); var output = writer.ToString().Trim(); Assert.Equal(0, Environment.ExitCode); Assert.False(string.IsNullOrWhiteSpace(output)); using var document = JsonDocument.Parse(output); var root = document.RootElement; Assert.Equal(600, root.GetProperty("ttlSeconds").GetInt32()); Assert.Equal("rev-json-7", root.GetProperty("policyRevision").GetString()); var expiresAt = root.GetProperty("expiresAtUtc").GetString(); Assert.NotNull(expiresAt); Assert.Equal( DateTimeOffset.Parse("2025-10-20T00:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), DateTimeOffset.Parse(expiresAt!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); var results = root.GetProperty("results"); var keys = results.EnumerateObject().Select(p => p.Name).ToArray(); Assert.Equal(new[] { "sha256:json-a", "sha256:json-b" }, keys); var first = results.GetProperty("sha256:json-a"); Assert.Equal("allow", first.GetProperty("policyVerdict").GetString()); Assert.True(first.GetProperty("signed").GetBoolean()); Assert.True(first.GetProperty("hasSbomReferrers").GetBoolean()); var rekor = first.GetProperty("rekor"); Assert.Equal("uuid-json-allow", rekor.GetProperty("uuid").GetString()); Assert.True(rekor.GetProperty("verified").GetBoolean()); Assert.Equal("baseline", first.GetProperty("source").GetString()); Assert.Equal(0.66, first.GetProperty("confidence").GetDouble(), 3); var second = results.GetProperty("sha256:json-b"); Assert.Equal("audit", second.GetProperty("policyVerdict").GetString()); Assert.True(second.GetProperty("signed").GetBoolean()); Assert.False(second.GetProperty("hasSbomReferrers").GetBoolean()); Assert.Equal("mirror", second.GetProperty("source").GetString()); Assert.True(second.GetProperty("quieted").GetBoolean()); Assert.Equal("risk-accepted", second.GetProperty("quietedBy").GetString()); Assert.False(second.TryGetProperty("rekor", out _)); } finally { Console.SetOut(originalOut); Environment.ExitCode = originalExit; } } private static async Task WriteRevocationArtifactsAsync(TempDirectory temp, string? providerHint) { var (bundleBytes, signature, keyPem) = await BuildRevocationArtifactsAsync(providerHint); var bundlePath = Path.Combine(temp.Path, "revocation-bundle.json"); var signaturePath = Path.Combine(temp.Path, "revocation-bundle.json.jws"); var keyPath = Path.Combine(temp.Path, "revocation-key.pem"); await File.WriteAllBytesAsync(bundlePath, bundleBytes); await File.WriteAllTextAsync(signaturePath, signature); await File.WriteAllTextAsync(keyPath, keyPem); return new RevocationArtifactPaths(bundlePath, signaturePath, keyPath); } private static async Task<(byte[] Bundle, string Signature, string KeyPem)> BuildRevocationArtifactsAsync(string? providerHint) { var bundleBytes = Encoding.UTF8.GetBytes("{\"revocations\":[]}"); using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); var parameters = ecdsa.ExportParameters(includePrivateParameters: true); var signingKey = new CryptoSigningKey( new CryptoKeyReference("revocation-test"), SignatureAlgorithms.Es256, privateParameters: in parameters, createdAt: DateTimeOffset.UtcNow); var provider = new DefaultCryptoProvider(); provider.UpsertSigningKey(signingKey); var signer = provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference); var header = new Dictionary { ["alg"] = SignatureAlgorithms.Es256, ["kid"] = signingKey.Reference.KeyId, ["typ"] = "application/vnd.stellaops.revocation-bundle+jws", ["b64"] = false, ["crit"] = new[] { "b64" } }; if (!string.IsNullOrWhiteSpace(providerHint)) { header["provider"] = providerHint; } var serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = null, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; var headerJson = JsonSerializer.Serialize(header, serializerOptions); var encodedHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(headerJson)); var signingInput = new byte[encodedHeader.Length + 1 + bundleBytes.Length]; var headerBytes = Encoding.ASCII.GetBytes(encodedHeader); Buffer.BlockCopy(headerBytes, 0, signingInput, 0, headerBytes.Length); signingInput[headerBytes.Length] = (byte)'.'; Buffer.BlockCopy(bundleBytes, 0, signingInput, headerBytes.Length + 1, bundleBytes.Length); var signatureBytes = await signer.SignAsync(signingInput); var encodedSignature = Base64UrlEncoder.Encode(signatureBytes); var jws = string.Concat(encodedHeader, "..", encodedSignature); var publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); var keyPem = new string(PemEncoding.Write("PUBLIC KEY", publicKeyBytes)); return (bundleBytes, jws, keyPem); } private sealed record RevocationArtifactPaths(string BundlePath, string SignaturePath, string KeyPath); private static IServiceProvider BuildServiceProvider( IBackendOperationsClient backend, IScannerExecutor? executor = null, IScannerInstaller? installer = null, StellaOpsCliOptions? options = null, IStellaOpsTokenClient? tokenClient = null) { var services = new ServiceCollection(); services.AddSingleton(backend); services.AddSingleton(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug))); services.AddSingleton(new VerbosityState()); var resolvedOptions = options ?? new StellaOpsCliOptions { ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}") }; services.AddSingleton(resolvedOptions); var resolvedExecutor = executor ?? CreateDefaultExecutor(); services.AddSingleton(resolvedExecutor); services.AddSingleton(installer ?? new StubInstaller()); if (tokenClient is not null) { services.AddSingleton(tokenClient); } return services.BuildServiceProvider(); } private static IScannerExecutor CreateDefaultExecutor() { var tempResultsFile = Path.GetTempFileName(); var tempMetadataFile = Path.Combine( Path.GetDirectoryName(tempResultsFile)!, $"{Path.GetFileNameWithoutExtension(tempResultsFile)}-run.json"); return new StubExecutor(new ScannerExecutionResult(0, tempResultsFile, tempMetadataFile)); } private sealed class StubBackendClient : IBackendOperationsClient { private readonly JobTriggerResult _jobResult; private static readonly RuntimePolicyEvaluationResult DefaultRuntimePolicyResult = new RuntimePolicyEvaluationResult( 0, null, null, new ReadOnlyDictionary( new Dictionary())); public StubBackendClient(JobTriggerResult result) { _jobResult = result; } public string? LastJobKind { get; private set; } public string? LastUploadPath { get; private set; } public string? LastExcititorRoute { get; private set; } public HttpMethod? LastExcititorMethod { get; private set; } public object? LastExcititorPayload { get; private set; } public List<(string ExportId, string DestinationPath, string? Algorithm, string? Digest)> ExportDownloads { get; } = new(); public ExcititorOperationResult? ExcititorResult { get; set; } = new ExcititorOperationResult(true, "ok", null, null); public IReadOnlyList ProviderSummaries { get; set; } = Array.Empty(); public RuntimePolicyEvaluationResult RuntimePolicyResult { get; set; } = DefaultRuntimePolicyResult; public Task DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken) { LastUploadPath = filePath; return Task.CompletedTask; } public Task TriggerJobAsync(string jobKind, IDictionary parameters, CancellationToken cancellationToken) { LastJobKind = jobKind; return Task.FromResult(_jobResult); } public Task ExecuteExcititorOperationAsync(string route, HttpMethod method, object? payload, CancellationToken cancellationToken) { LastExcititorRoute = route; LastExcititorMethod = method; LastExcititorPayload = payload; return Task.FromResult(ExcititorResult ?? new ExcititorOperationResult(true, "ok", null, null)); } public Task DownloadExcititorExportAsync(string exportId, string destinationPath, string? expectedDigestAlgorithm, string? expectedDigest, CancellationToken cancellationToken) { var fullPath = Path.GetFullPath(destinationPath); var directory = Path.GetDirectoryName(fullPath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } File.WriteAllText(fullPath, "{}"); var info = new FileInfo(fullPath); ExportDownloads.Add((exportId, fullPath, expectedDigestAlgorithm, expectedDigest)); return Task.FromResult(new ExcititorExportDownloadResult(fullPath, info.Length, false)); } public Task> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken) => Task.FromResult(ProviderSummaries); public Task EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken) => Task.FromResult(RuntimePolicyResult); public Task DownloadOfflineKitAsync(string? bundleId, string destinationDirectory, bool overwrite, bool resume, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task ImportOfflineKitAsync(OfflineKitImportRequest request, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task GetOfflineKitStatusAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); } private sealed class StubExecutor : IScannerExecutor { private readonly ScannerExecutionResult _result; public StubExecutor(ScannerExecutionResult result) { _result = result; } public Task RunAsync(string runner, string entry, string targetDirectory, string resultsDirectory, IReadOnlyList arguments, bool verbose, CancellationToken cancellationToken) { Directory.CreateDirectory(Path.GetDirectoryName(_result.ResultsPath)!); if (!File.Exists(_result.ResultsPath)) { File.WriteAllText(_result.ResultsPath, "{}"); } Directory.CreateDirectory(Path.GetDirectoryName(_result.RunMetadataPath)!); if (!File.Exists(_result.RunMetadataPath)) { File.WriteAllText(_result.RunMetadataPath, "{}"); } return Task.FromResult(_result); } } private sealed class StubInstaller : IScannerInstaller { public Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken) => Task.CompletedTask; } private sealed class StubTokenClient : IStellaOpsTokenClient { private readonly StellaOpsTokenResult _token; public StubTokenClient() { _token = new StellaOpsTokenResult( "token-123", "Bearer", DateTimeOffset.UtcNow.AddMinutes(30), new[] { StellaOpsScopes.ConcelierJobsTrigger }); } public int ClientCredentialRequests { get; private set; } public int PasswordRequests { get; private set; } public int ClearRequests { get; private set; } public StellaOpsTokenCacheEntry? CachedEntry { get; set; } public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default) { CachedEntry = entry; return ValueTask.CompletedTask; } public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default) { ClearRequests++; CachedEntry = null; return ValueTask.CompletedTask; } public Task GetJsonWebKeySetAsync(CancellationToken cancellationToken = default) => Task.FromResult(new JsonWebKeySet("{\"keys\":[]}")); public ValueTask GetCachedTokenAsync(string key, CancellationToken cancellationToken = default) => ValueTask.FromResult(CachedEntry); public Task RequestClientCredentialsTokenAsync(string? scope = null, CancellationToken cancellationToken = default) { ClientCredentialRequests++; return Task.FromResult(_token); } public Task RequestPasswordTokenAsync(string username, string password, string? scope = null, CancellationToken cancellationToken = default) { PasswordRequests++; return Task.FromResult(_token); } } private static string CreateUnsignedJwt(params (string Key, object Value)[] claims) { var headerJson = "{\"alg\":\"none\",\"typ\":\"JWT\"}"; var payload = new Dictionary(StringComparer.Ordinal); foreach (var claim in claims) { payload[claim.Key] = claim.Value; } var payloadJson = JsonSerializer.Serialize(payload); return $"{Base64UrlEncode(headerJson)}.{Base64UrlEncode(payloadJson)}."; } private static string Base64UrlEncode(string value) { var bytes = Encoding.UTF8.GetBytes(value); return Convert.ToBase64String(bytes) .TrimEnd('=') .Replace('+', '-') .Replace('/', '_'); } }