Files
git.stella-ops.org/src/StellaOps.Cli.Tests/Commands/CommandHandlersTests.cs
master 48f3071e2a Add tests and implement StubBearer authentication for Signer endpoints
- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints.
- Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication.
- Developed ConcelierExporterClient for managing Trivy DB settings and export operations.
- Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering.
- Implemented styles and HTML structure for Trivy DB settings page.
- Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
2025-10-21 09:37:07 +03:00

1070 lines
42 KiB
C#

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<string>(),
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<IDictionary<string, object?>>(backend.LastExcititorPayload);
Assert.Equal(true, payload["resume"]);
var providers = Assert.IsAssignableFrom<IEnumerable<string>>(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<IDictionary<string, object?>>(backend.LastExcititorPayload);
Assert.Equal("export-123", payload["exportId"]);
Assert.Equal("sha256:abc", payload["digest"]);
var attestation = Assert.IsAssignableFrom<IDictionary<string, object?>>(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<string, RuntimePolicyImageDecision>(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<string, object?>(new Dictionary<string, object?>(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<string, object?>(new Dictionary<string, object?>(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<string, object?>(new Dictionary<string, object?>(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<string, RuntimePolicyImageDecision>(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<string, RuntimePolicyImageDecision>(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<string, object?>(new Dictionary<string, object?>(StringComparer.Ordinal)
{
["source"] = "baseline",
["confidence"] = 0.66
})),
["sha256:json-b"] = new RuntimePolicyImageDecision(
"audit",
true,
false,
Array.AsReadOnly(Array.Empty<string>()),
new RuntimePolicyRekorReference(null, null, null),
new ReadOnlyDictionary<string, object?>(new Dictionary<string, object?>(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<string, RuntimePolicyImageDecision>(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<string>(),
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<RevocationArtifactPaths> 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<string, object>
{
["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<ILoggerFactory>(_ => 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<IScannerExecutor>(resolvedExecutor);
services.AddSingleton<IScannerInstaller>(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<string, RuntimePolicyImageDecision>(
new Dictionary<string, RuntimePolicyImageDecision>()));
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<ExcititorProviderSummary> ProviderSummaries { get; set; } = Array.Empty<ExcititorProviderSummary>();
public RuntimePolicyEvaluationResult RuntimePolicyResult { get; set; } = DefaultRuntimePolicyResult;
public Task<ScannerArtifactResult> 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<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken)
{
LastJobKind = jobKind;
return Task.FromResult(_jobResult);
}
public Task<ExcititorOperationResult> 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<ExcititorExportDownloadResult> 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<IReadOnlyList<ExcititorProviderSummary>> GetExcititorProvidersAsync(bool includeDisabled, CancellationToken cancellationToken)
=> Task.FromResult(ProviderSummaries);
public Task<RuntimePolicyEvaluationResult> EvaluateRuntimePolicyAsync(RuntimePolicyEvaluationRequest request, CancellationToken cancellationToken)
=> Task.FromResult(RuntimePolicyResult);
public Task<OfflineKitDownloadResult> DownloadOfflineKitAsync(string? bundleId, string destinationDirectory, bool overwrite, bool resume, CancellationToken cancellationToken)
=> throw new NotSupportedException();
public Task<OfflineKitImportResult> ImportOfflineKitAsync(OfflineKitImportRequest request, CancellationToken cancellationToken)
=> throw new NotSupportedException();
public Task<OfflineKitStatus> GetOfflineKitStatusAsync(CancellationToken cancellationToken)
=> throw new NotSupportedException();
}
private sealed class StubExecutor : IScannerExecutor
{
private readonly ScannerExecutionResult _result;
public StubExecutor(ScannerExecutionResult result)
{
_result = result;
}
public Task<ScannerExecutionResult> RunAsync(string runner, string entry, string targetDirectory, string resultsDirectory, IReadOnlyList<string> 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<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
=> Task.FromResult(new JsonWebKeySet("{\"keys\":[]}"));
public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(CachedEntry);
public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, CancellationToken cancellationToken = default)
{
ClientCredentialRequests++;
return Task.FromResult(_token);
}
public Task<StellaOpsTokenResult> 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<string, object?>(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('/', '_');
}
}