Files
git.stella-ops.org/src/StellaOps.Cli.Tests/Commands/CommandHandlersTests.cs
master d099a90f9b feat: Initialize Zastava Webhook service with TLS and Authority authentication
- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint.
- Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately.
- Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly.
- Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
2025-10-19 18:36:22 +03:00

840 lines
31 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
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 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;
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;
}
}
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;
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 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)
{
var empty = new ReadOnlyDictionary<string, RuntimePolicyImageDecision>(new Dictionary<string, RuntimePolicyImageDecision>());
return Task.FromResult(new RuntimePolicyEvaluationResult(0, null, null, empty));
}
}
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('/', '_');
}
}