Files
git.stella-ops.org/src/Cli/__Tests/StellaOps.Cli.Tests/Commands/CommandHandlersTests.cs
master 69c59defdc
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat: Implement Runtime Facts ingestion service and NDJSON reader
- Added RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts.
- Introduced IRuntimeFactsIngestionService interface and its implementation.
- Enhanced Program.cs to register new services and endpoints for runtime facts.
- Updated CallgraphIngestionService to include CAS URI in stored artifacts.
- Created RuntimeFactsValidationException for validation errors during ingestion.
- Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader.
- Implemented SignalsSealedModeMonitor for compliance checks in sealed mode.
- Updated project dependencies for testing utilities.
2025-11-10 07:56:15 +02:00

4010 lines
151 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
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.Services.Models.AdvisoryAi;
using StellaOps.Cli.Services.Models.Ruby;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Tests.Testing;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using StellaOps.Scanner.EntryTrace;
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 HandleScanEntryTraceAsync_RendersPlansAndNdjson()
{
var originalExit = Environment.ExitCode;
var console = new TestConsole();
var originalConsole = AnsiConsole.Console;
var graph = new EntryTraceGraph(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray.Create(new EntryTracePlan(
ImmutableArray.Create("/usr/bin/python", "app.py"),
ImmutableDictionary<string, string>.Empty,
"/workspace",
"appuser",
"/usr/bin/python",
EntryTraceTerminalType.Managed,
"python",
0.95,
ImmutableDictionary<string, string>.Empty)),
ImmutableArray.Create(new EntryTraceTerminal(
"/usr/bin/python",
EntryTraceTerminalType.Managed,
"python",
0.95,
ImmutableDictionary<string, string>.Empty,
"appuser",
"/workspace",
ImmutableArray<string>.Empty)));
var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null))
{
EntryTraceResponse = new EntryTraceResponseModel(
"scan-123",
"sha256:deadbeef",
DateTimeOffset.Parse("2025-11-02T12:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal),
graph,
new[] { "{\"type\":\"terminal\"}" })
};
var provider = BuildServiceProvider(backend);
AnsiConsole.Console = console;
try
{
await CommandHandlers.HandleScanEntryTraceAsync(
provider,
"scan-123",
includeNdjson: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Equal("scan-123", backend.LastEntryTraceScanId);
var output = console.Output;
Assert.Contains("scan-123", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("NDJSON Output", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("{\"type\":\"terminal\"}", output, StringComparison.Ordinal);
Assert.Contains("/usr/bin/python", output, StringComparison.OrdinalIgnoreCase);
}
finally
{
Environment.ExitCode = originalExit;
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandleScanEntryTraceAsync_WarnsWhenResultMissing()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null));
var loggerProvider = new TestLoggerProvider();
var provider = BuildServiceProvider(backend, loggerProvider: loggerProvider);
try
{
var output = await CaptureTestConsoleAsync(console => CommandHandlers.HandleScanEntryTraceAsync(
provider,
"scan-missing",
includeNdjson: false,
verbose: false,
cancellationToken: CancellationToken.None));
Assert.Equal(1, Environment.ExitCode);
Assert.Equal("scan-missing", backend.LastEntryTraceScanId);
Assert.Contains("No EntryTrace data", output.Combined, StringComparison.OrdinalIgnoreCase);
var warning = Assert.Single(loggerProvider.Entries.Where(entry => entry.Level == LogLevel.Warning));
Assert.Contains("No EntryTrace data", warning.Message, StringComparison.OrdinalIgnoreCase);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleNodeLockValidateAsync_RendersDeclaredOnlyAndMissingLock()
{
var originalExit = Environment.ExitCode;
using var fixture = new TempDirectory();
try
{
CreateNodeLockFixture(fixture.Path);
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleNodeLockValidateAsync(
provider,
fixture.Path,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(1, Environment.ExitCode);
using var document = JsonDocument.Parse(output.PlainBuffer);
var root = document.RootElement;
var declared = root.GetProperty("declaredOnly");
var missing = root.GetProperty("missingLockMetadata");
Assert.Contains(declared.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "declared-only", StringComparison.OrdinalIgnoreCase));
Assert.Contains(missing.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "runtime-only", StringComparison.OrdinalIgnoreCase));
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleNodeLockValidateAsync_SetsExitCodeWhenDirectoryMissing()
{
var originalExit = Environment.ExitCode;
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
var missingPath = Path.Combine(Path.GetTempPath(), $"stellaops-missing-{Guid.NewGuid():N}");
try
{
await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleNodeLockValidateAsync(
provider,
missingPath,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(71, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePythonLockValidateAsync_RendersDeclaredOnlyAndMissingLock()
{
var originalExit = Environment.ExitCode;
using var fixture = new TempDirectory();
try
{
await CreatePythonLockFixtureAsync(fixture.Path, CancellationToken.None);
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandlePythonLockValidateAsync(
provider,
fixture.Path,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(1, Environment.ExitCode);
using var document = JsonDocument.Parse(output.PlainBuffer);
var root = document.RootElement;
var declared = root.GetProperty("declaredOnly");
var missing = root.GetProperty("missingLockMetadata");
Assert.Contains(declared.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "declared-only", StringComparison.OrdinalIgnoreCase));
Assert.Contains(missing.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "runtime-only", StringComparison.OrdinalIgnoreCase));
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePythonLockValidateAsync_SetsExitCodeWhenDirectoryMissing()
{
var originalExit = Environment.ExitCode;
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
var missingPath = Path.Combine(Path.GetTempPath(), $"stellaops-missing-{Guid.NewGuid():N}");
try
{
await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandlePythonLockValidateAsync(
provider,
missingPath,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(71, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleJavaLockValidateAsync_RendersDeclaredOnlyAndMissingLock()
{
var originalExit = Environment.ExitCode;
using var fixture = new TempDirectory();
try
{
CreateJavaLockFixture(fixture.Path);
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleJavaLockValidateAsync(
provider,
fixture.Path,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(1, Environment.ExitCode);
using var document = JsonDocument.Parse(output.PlainBuffer);
var root = document.RootElement;
var declared = root.GetProperty("declaredOnly");
var missing = root.GetProperty("missingLockMetadata");
Assert.Contains(declared.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "declared-only", StringComparison.OrdinalIgnoreCase));
Assert.Contains(missing.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "runtime-only", StringComparison.OrdinalIgnoreCase));
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleJavaLockValidateAsync_SetsExitCodeWhenDirectoryMissing()
{
var originalExit = Environment.ExitCode;
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
var missingPath = Path.Combine(Path.GetTempPath(), $"stellaops-missing-{Guid.NewGuid():N}");
try
{
await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleJavaLockValidateAsync(
provider,
missingPath,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(71, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleRubyInspectAsync_RendersPackagesAndRuntime()
{
var originalExit = Environment.ExitCode;
using var fixture = new TempDirectory();
CreateRubyWorkspace(fixture.Path);
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
try
{
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleRubyInspectAsync(
provider,
fixture.Path,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(0, Environment.ExitCode);
using var document = JsonDocument.Parse(output.PlainBuffer);
var packages = document.RootElement.GetProperty("packages");
Assert.Contains(packages.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "rack", StringComparison.OrdinalIgnoreCase)
&& string.Equals(entry.GetProperty("lockfile").GetString(), "Gemfile.lock", StringComparison.OrdinalIgnoreCase));
Assert.Contains(packages.EnumerateArray(), entry =>
string.Equals(entry.GetProperty("name").GetString(), "zeitwerk", StringComparison.OrdinalIgnoreCase)
&& entry.GetProperty("runtimeFiles").EnumerateArray().Any());
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleRubyInspectAsync_WritesJson()
{
var originalExit = Environment.ExitCode;
using var fixture = new TempDirectory();
CreateRubyWorkspace(fixture.Path);
var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)));
try
{
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleRubyInspectAsync(
provider,
fixture.Path,
"json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(0, Environment.ExitCode);
using var document = JsonDocument.Parse(output.PlainBuffer);
var packages = document.RootElement.GetProperty("packages");
Assert.NotEmpty(packages.EnumerateArray());
var entry = packages.EnumerateArray().First(p => string.Equals(p.GetProperty("name").GetString(), "rack", StringComparison.OrdinalIgnoreCase));
Assert.True(entry.GetProperty("usedByEntrypoint").GetBoolean());
Assert.Contains(
"app.rb",
entry.GetProperty("runtimeEntrypoints").EnumerateArray().Select(e => e.GetString() ?? string.Empty),
StringComparer.OrdinalIgnoreCase);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleRubyResolveAsync_RendersGroupedPackages()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
RubyPackages = new[]
{
CreateRubyPackageArtifact("pkg-rack", "rack", "3.1.0", new[] { "default", "web" }, runtimeUsed: true),
CreateRubyPackageArtifact("pkg-sidekiq", "sidekiq", "7.2.1", groups: null, runtimeUsed: false, metadataOverrides: new Dictionary<string, string?>
{
["groups"] = "jobs",
["runtime.entrypoints"] = "config/jobs.rb",
["runtime.files"] = "config/jobs.rb"
})
}
};
var provider = BuildServiceProvider(backend);
try
{
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleRubyResolveAsync(
provider,
imageReference: null,
scanId: "scan-ruby",
format: "table",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(0, Environment.ExitCode);
Assert.Equal("scan-ruby", backend.LastRubyPackagesScanId);
Assert.Contains("scan-ruby", output.Combined, StringComparison.OrdinalIgnoreCase);
Assert.Contains("rack", output.Combined, StringComparison.OrdinalIgnoreCase);
Assert.Contains("jobs", output.Combined, StringComparison.OrdinalIgnoreCase);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleRubyResolveAsync_WritesJson()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
RubyPackages = new[]
{
CreateRubyPackageArtifact("pkg-rack-json", "rack", "3.1.0", new[] { "default" }, runtimeUsed: true)
}
};
var provider = BuildServiceProvider(backend);
const string identifier = "ruby-scan-json";
try
{
var output = await CaptureTestConsoleAsync(async _ =>
{
await CommandHandlers.HandleRubyResolveAsync(
provider,
imageReference: identifier,
scanId: null,
format: "json",
verbose: false,
cancellationToken: CancellationToken.None);
});
Assert.Equal(0, Environment.ExitCode);
Assert.Equal(identifier, backend.LastRubyPackagesScanId);
using var document = JsonDocument.Parse(output.PlainBuffer);
Assert.Equal(identifier, document.RootElement.GetProperty("scanId").GetString());
var group = document.RootElement.GetProperty("groups")[0];
Assert.Equal("default", group.GetProperty("group").GetString());
Assert.Equal("-", group.GetProperty("platform").GetString());
var package = group.GetProperty("packages")[0];
Assert.Equal("rubygems", package.GetProperty("source").GetString());
Assert.Equal("Gemfile.lock", package.GetProperty("lockfile").GetString());
var packageGroups = package.GetProperty("groups")
.EnumerateArray()
.Select(static p => p.GetString())
.Where(static g => !string.IsNullOrEmpty(g))
.Select(static g => g!)
.ToArray();
Assert.Contains("default", packageGroups, StringComparer.OrdinalIgnoreCase);
Assert.True(package.GetProperty("runtimeUsed").GetBoolean());
Assert.Contains("app.rb", package.GetProperty("runtimeEntrypoints")[0].GetString(), StringComparison.OrdinalIgnoreCase);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_WritesOutputAndSetsExitCode()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Summary.ToString(),
CacheKey = "cache-123",
PromptTemplate = "prompts/advisory/summary.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 512,
CompletionTokens = 128
},
Chunks = new[]
{
new PipelineChunkSummaryModel
{
DocumentId = "doc-1",
ChunkId = "chunk-1",
Section = "Summary",
DisplaySection = "Summary"
}
},
Vectors = new[]
{
new PipelineVectorSummaryModel
{
Query = "summary query",
Matches = new[]
{
new PipelineVectorMatchSummaryModel
{
ChunkId = "chunk-1",
Score = 0.9
}
}
}
},
Metadata = new Dictionary<string, string>
{
["profile"] = "default"
}
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Summary result",
Citations = new[]
{
new AdvisoryOutputCitationModel
{
Index = 0,
DocumentId = "doc-1",
ChunkId = "chunk-1"
}
},
Metadata = new Dictionary<string, string>
{
["confidence"] = "high"
},
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = false,
SanitizedPrompt = "Summary result",
Violations = Array.Empty<AdvisoryOutputGuardrailViolationModel>(),
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:aaa",
OutputHash = "sha256:bbb",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T12:00:00Z", CultureInfo.InvariantCulture),
PlanFromCache = false
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Summary,
" ADV-1 ",
null,
null,
null,
"default",
new[] { "impact", "impact " },
forceRefresh: false,
timeoutSeconds: 0,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Single(backend.AdvisoryPlanRequests);
var request = backend.AdvisoryPlanRequests[0];
Assert.Equal(AdvisoryAiTaskType.Summary, request.TaskType);
Assert.Equal("ADV-1", request.Request.AdvisoryKey);
Assert.NotNull(request.Request.PreferredSections);
Assert.Single(request.Request.PreferredSections!);
Assert.Equal("impact", request.Request.PreferredSections![0]);
Assert.Single(backend.AdvisoryOutputRequests);
Assert.Equal(planResponse.CacheKey, backend.AdvisoryOutputRequests[0].CacheKey);
Assert.Equal("default", backend.AdvisoryOutputRequests[0].Profile);
var output = testConsole.Output;
Assert.Contains("Advisory Output", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains(planResponse.CacheKey, output, StringComparison.Ordinal);
Assert.Contains("Summary result", output, StringComparison.Ordinal);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_ReturnsGuardrailExitCodeOnBlock()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var testConsole = new TestConsole();
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = testConsole;
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Remediation.ToString(),
CacheKey = "cache-guard",
PromptTemplate = "prompts/advisory/remediation.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 256,
CompletionTokens = 64
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var outputResponse = new AdvisoryPipelineOutputModel
{
CacheKey = planResponse.CacheKey,
TaskType = planResponse.TaskType,
Profile = "default",
Prompt = "Blocked output",
Citations = Array.Empty<AdvisoryOutputCitationModel>(),
Metadata = new Dictionary<string, string>(),
Guardrail = new AdvisoryOutputGuardrailModel
{
Blocked = true,
SanitizedPrompt = "Blocked output",
Violations = new[]
{
new AdvisoryOutputGuardrailViolationModel
{
Code = "PROMPT_INJECTION",
Message = "Detected prompt injection attempt."
}
},
Metadata = new Dictionary<string, string>()
},
Provenance = new AdvisoryOutputProvenanceModel
{
InputDigest = "sha256:ccc",
OutputHash = "sha256:ddd",
Signatures = Array.Empty<string>()
},
GeneratedAtUtc = DateTimeOffset.Parse("2025-11-06T13:05:00Z", CultureInfo.InvariantCulture),
PlanFromCache = true
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = outputResponse
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Remediation,
"ADV-2",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: true,
timeoutSeconds: 0,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(65, Environment.ExitCode);
Assert.Contains("Guardrail Violations", testConsole.Output, StringComparison.OrdinalIgnoreCase);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleAdviseRunAsync_TimesOutWhenOutputMissing()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
try
{
Environment.ExitCode = 0;
AnsiConsole.Console = new TestConsole();
var planResponse = new AdvisoryPipelinePlanResponseModel
{
TaskType = AdvisoryAiTaskType.Conflict.ToString(),
CacheKey = "cache-timeout",
PromptTemplate = "prompts/advisory/conflict.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 128,
CompletionTokens = 32
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>()
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
AdvisoryPlanResponse = planResponse,
AdvisoryOutputResponse = null
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAdviseRunAsync(
provider,
AdvisoryAiTaskType.Conflict,
"ADV-3",
null,
null,
null,
"default",
Array.Empty<string>(),
forceRefresh: false,
timeoutSeconds: 0,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(70, Environment.ExitCode);
Assert.Single(backend.AdvisoryOutputRequests);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[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;
}
}
[Fact]
public async Task HandleVulnObservationsAsync_WritesTableOutput()
{
var originalExit = Environment.ExitCode;
var response = new AdvisoryObservationsResponse
{
Observations = new[]
{
new AdvisoryObservationDocument
{
ObservationId = "tenant-a:ghsa:alpha:1",
Tenant = "tenant-a",
Source = new AdvisoryObservationSource
{
Vendor = "ghsa",
Stream = "advisories",
Api = "https://example.test/api"
},
Upstream = new AdvisoryObservationUpstream
{
UpstreamId = "GHSA-abcd-efgh"
},
Linkset = new AdvisoryObservationLinkset
{
Aliases = new[] { "cve-2025-0001" },
Purls = new[] { "pkg:npm/package-a@1.0.0" },
Cpes = new[] { "cpe:/a:vendor:product:1.0" }
},
CreatedAt = new DateTimeOffset(2025, 10, 27, 6, 0, 0, TimeSpan.Zero)
}
},
Linkset = new AdvisoryObservationLinksetAggregate
{
Aliases = new[] { "cve-2025-0001" },
Purls = new[] { "pkg:npm/package-a@1.0.0" },
Cpes = new[] { "cpe:/a:vendor:product:1.0" },
References = Array.Empty<AdvisoryObservationReference>()
}
};
var stubClient = new StubConcelierObservationsClient(response);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend, concelierClient: stubClient);
var console = new TestConsole();
var originalConsole = AnsiConsole.Console;
AnsiConsole.Console = console;
try
{
await CommandHandlers.HandleVulnObservationsAsync(
provider,
tenant: "Tenant-A ",
observationIds: new[] { "tenant-a:ghsa:alpha:1 " },
aliases: new[] { " CVE-2025-0001 " },
purls: new[] { " pkg:npm/package-a@1.0.0 " },
cpes: Array.Empty<string>(),
limit: null,
cursor: null,
emitJson: false,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
AnsiConsole.Console = originalConsole;
}
Assert.NotNull(stubClient.LastQuery);
var query = stubClient.LastQuery!;
Assert.Equal("tenant-a", query.Tenant);
Assert.Contains("cve-2025-0001", query.Aliases);
Assert.Contains("pkg:npm/package-a@1.0.0", query.Purls);
Assert.Null(query.Limit);
Assert.Null(query.Cursor);
var output = console.Output;
Assert.False(string.IsNullOrWhiteSpace(output));
}
[Fact]
public async Task HandleVulnObservationsAsync_WritesJsonOutput()
{
var originalExit = Environment.ExitCode;
var response = new AdvisoryObservationsResponse
{
Observations = new[]
{
new AdvisoryObservationDocument
{
ObservationId = "tenant-a:osv:beta:2",
Tenant = "tenant-a",
Source = new AdvisoryObservationSource
{
Vendor = "osv",
Stream = "osv",
Api = "https://example.test/osv"
},
Upstream = new AdvisoryObservationUpstream
{
UpstreamId = "OSV-2025-XYZ"
},
Linkset = new AdvisoryObservationLinkset
{
Aliases = new[] { "cve-2025-0101" },
Purls = new[] { "pkg:pypi/package-b@2.0.0" },
Cpes = Array.Empty<string>(),
References = new[]
{
new AdvisoryObservationReference { Type = "advisory", Url = "https://example.test/advisory" }
}
},
CreatedAt = new DateTimeOffset(2025, 10, 27, 7, 30, 0, TimeSpan.Zero)
}
},
Linkset = new AdvisoryObservationLinksetAggregate
{
Aliases = new[] { "cve-2025-0101" },
Purls = new[] { "pkg:pypi/package-b@2.0.0" },
Cpes = Array.Empty<string>(),
References = new[]
{
new AdvisoryObservationReference { Type = "advisory", Url = "https://example.test/advisory" }
}
}
};
var stubClient = new StubConcelierObservationsClient(response);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend, concelierClient: stubClient);
var writer = new StringWriter();
var originalOut = Console.Out;
Console.SetOut(writer);
try
{
await CommandHandlers.HandleVulnObservationsAsync(
provider,
tenant: "tenant-a",
observationIds: Array.Empty<string>(),
aliases: Array.Empty<string>(),
purls: Array.Empty<string>(),
cpes: Array.Empty<string>(),
limit: null,
cursor: null,
emitJson: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
Console.SetOut(originalOut);
}
var json = writer.ToString();
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.True(root.TryGetProperty("observations", out var observations));
Assert.Equal("tenant-a:osv:beta:2", observations[0].GetProperty("observationId").GetString());
Assert.Equal("pkg:pypi/package-b@2.0.0", observations[0].GetProperty("linkset").GetProperty("purls")[0].GetString());
}
[Fact]
public async Task HandleVulnObservationsAsync_WhenHasMore_PrintsCursorHint()
{
var originalExit = Environment.ExitCode;
var response = new AdvisoryObservationsResponse
{
Observations = new[]
{
new AdvisoryObservationDocument
{
ObservationId = "tenant-a:source:1",
Tenant = "tenant-a",
Linkset = new AdvisoryObservationLinkset(),
Source = new AdvisoryObservationSource(),
Upstream = new AdvisoryObservationUpstream(),
CreatedAt = DateTimeOffset.UtcNow
}
},
Linkset = new AdvisoryObservationLinksetAggregate(),
HasMore = true,
NextCursor = "cursor-token"
};
var stubClient = new StubConcelierObservationsClient(response);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend, concelierClient: stubClient);
var console = new TestConsole();
var originalConsole = AnsiConsole.Console;
AnsiConsole.Console = console;
try
{
await CommandHandlers.HandleVulnObservationsAsync(
provider,
tenant: "tenant-a",
observationIds: Array.Empty<string>(),
aliases: Array.Empty<string>(),
purls: Array.Empty<string>(),
cpes: Array.Empty<string>(),
limit: 1,
cursor: null,
emitJson: false,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
AnsiConsole.Console = originalConsole;
}
var output = console.Output;
Assert.Contains("--cursor", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("cursor-token", output, StringComparison.Ordinal);
}
[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;
}
}
[Fact]
public async Task HandlePolicyFindingsListAsync_WritesInteractiveTable()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
console.Interactive();
console.EmitAnsiSequences();
console.Width(140);
AnsiConsole.Console = console;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
FindingsPage = new PolicyFindingsPage(
new[]
{
new PolicyFindingDocument(
"P-7:S-42:pkg:npm/lodash@4.17.21:CVE-2021-23337",
"affected",
new PolicyFindingSeverity("High", 7.5),
"sbom:S-42",
new[] { "CVE-2021-23337", "GHSA-xxxx-yyyy" },
new PolicyFindingVexMetadata("VendorX-123", "vendor-x", "not_affected"),
4,
DateTimeOffset.Parse("2025-10-26T14:06:01Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
"run:P-7:2025-10-26:auto")
},
"cursor-42",
10)
};
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicyFindingsListAsync(
provider,
" P-7 ",
new[] { " sbom:S-42 " },
new[] { "Affected", "QUIETED" },
new[] { "High", "Critical" },
"2025-10-25T00:00:00Z",
" cursor-0 ",
page: 2,
pageSize: 100,
format: "table",
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.NotNull(backend.LastFindingsQuery);
var query = backend.LastFindingsQuery!;
Assert.Equal("P-7", query.PolicyId);
Assert.Contains("sbom:S-42", query.SbomIds);
Assert.Contains("affected", query.Statuses);
Assert.Contains("quieted", query.Statuses);
Assert.Contains("High", query.Severities);
Assert.Contains("Critical", query.Severities);
Assert.Equal(2, query.Page);
Assert.Equal(100, query.PageSize);
Assert.Equal("cursor-0", query.Cursor);
Assert.Equal(DateTimeOffset.Parse("2025-10-25T00:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal), query.Since);
var output = console.Output;
Assert.Contains("P-7:S-42", output, StringComparison.Ordinal);
Assert.Contains("High", output, StringComparison.Ordinal);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicyFindingsListAsync_WritesJson()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
FindingsPage = new PolicyFindingsPage(
new[]
{
new PolicyFindingDocument(
"finding-1",
"quieted",
new PolicyFindingSeverity("Medium", 5.1),
"sbom:S-99",
Array.Empty<string>(),
null,
3,
DateTimeOffset.MinValue,
null)
},
null,
null)
};
var provider = BuildServiceProvider(backend);
using var writer = new StringWriter();
var originalOut = Console.Out;
Console.SetOut(writer);
try
{
await CommandHandlers.HandlePolicyFindingsListAsync(
provider,
"P-9",
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<string>(),
null,
null,
page: null,
pageSize: null,
format: "json",
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
using var document = JsonDocument.Parse(writer.ToString());
var root = document.RootElement;
Assert.Equal("P-9", root.GetProperty("policyId").GetString());
var items = root.GetProperty("items");
Assert.Equal(1, items.GetArrayLength());
var first = items[0];
Assert.Equal("finding-1", first.GetProperty("findingId").GetString());
Assert.Equal("quieted", first.GetProperty("status").GetString());
Assert.Equal("Medium", first.GetProperty("severity").GetProperty("normalized").GetString());
}
finally
{
Console.SetOut(originalOut);
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicyFindingsGetAsync_WritesInteractiveTable()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
console.Interactive();
console.EmitAnsiSequences();
console.Width(120);
AnsiConsole.Console = console;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
FindingDocument = new PolicyFindingDocument(
"P-9:S-1:pkg:npm/leftpad@1.0.0:CVE-1111",
"affected",
new PolicyFindingSeverity("Critical", 9.1),
"sbom:S-1",
new[] { "CVE-1111" },
new PolicyFindingVexMetadata("VendorY-9", null, "affected"),
7,
DateTimeOffset.Parse("2025-10-26T12:34:56Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
"run:P-9:1234")
};
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicyFindingsGetAsync(
provider,
"P-9",
"P-9:S-1:pkg:npm/leftpad@1.0.0:CVE-1111",
format: "table",
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Equal(("P-9", "P-9:S-1:pkg:npm/leftpad@1.0.0:CVE-1111"), backend.LastFindingGet);
var output = console.Output;
Assert.Contains("Critical", output);
Assert.Contains("run:P-9:1234", output);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicyFindingsExplainAsync_WritesInteractiveTable()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
console.Interactive();
console.EmitAnsiSequences();
console.Width(140);
AnsiConsole.Console = console;
var steps = new[]
{
new PolicyFindingExplainStep(
"rule-block-critical",
"blocked",
"block",
9.1,
new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
{
["severity"] = "Critical",
["sealed"] = "false"
}),
new ReadOnlyDictionary<string, string>(new Dictionary<string, string>
{
["vex"] = "VendorY-9"
}))
};
var hints = new[]
{
new PolicyFindingExplainHint("Using cached EPSS percentile from bundle 2025-10-20")
};
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
ExplainResult = new PolicyFindingExplainResult(
"P-9:S-1:pkg:npm/leftpad@1.0.0:CVE-1111",
7,
new ReadOnlyCollection<PolicyFindingExplainStep>(steps),
new ReadOnlyCollection<PolicyFindingExplainHint>(hints))
};
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicyFindingsExplainAsync(
provider,
"P-9",
"P-9:S-1:pkg:npm/leftpad@1.0.0:CVE-1111",
mode: "verbose",
format: "table",
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.Equal(("P-9", "P-9:S-1:pkg:npm/leftpad@1.0.0:CVE-1111", "verbose"), backend.LastFindingExplain);
var output = console.Output;
Assert.Contains("rule-block-critical", output);
Assert.Contains("EPSS percentile", output);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicySimulateAsync_WritesInteractiveSummary()
{
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 severity = new ReadOnlyDictionary<string, PolicySimulationSeverityDelta>(new Dictionary<string, PolicySimulationSeverityDelta>(StringComparer.Ordinal)
{
["critical"] = new PolicySimulationSeverityDelta(1, null),
["high"] = new PolicySimulationSeverityDelta(null, 2)
});
var ruleHits = new ReadOnlyCollection<PolicySimulationRuleDelta>(new List<PolicySimulationRuleDelta>
{
new("rule-block-critical", "Block Critical", 1, 0),
new("rule-quiet-low", "Quiet Low", null, 2)
});
backend.SimulationResult = new PolicySimulationResult(
new PolicySimulationDiff(
"scheduler.policy-diff-summary@1",
2,
1,
10,
severity,
ruleHits),
"blob://policy/P-7/simulation.json");
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicySimulateAsync(
provider,
policyId: "P-7",
baseVersion: 3,
candidateVersion: 4,
sbomArguments: new[] { "sbom:A", "sbom:B" },
environmentArguments: new[] { "sealed=false", "exposure=internet" },
format: "table",
outputPath: null,
explain: true,
failOnDiff: false,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.NotNull(backend.LastPolicySimulation);
var simulation = backend.LastPolicySimulation!.Value;
Assert.Equal("P-7", simulation.PolicyId);
Assert.Equal(3, simulation.Input.BaseVersion);
Assert.Equal(4, simulation.Input.CandidateVersion);
Assert.True(simulation.Input.Explain);
Assert.Equal(new[] { "sbom:A", "sbom:B" }, simulation.Input.SbomSet);
Assert.True(simulation.Input.Environment.TryGetValue("sealed", out var sealedValue) && sealedValue is bool sealedFlag && sealedFlag == false);
Assert.True(simulation.Input.Environment.TryGetValue("exposure", out var exposureValue) && string.Equals(exposureValue as string, "internet", StringComparison.Ordinal));
var output = console.Output;
Assert.Contains("Severity", output, StringComparison.Ordinal);
Assert.Contains("critical", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Rule", output, StringComparison.Ordinal);
Assert.Contains("Block Critical", output, StringComparison.Ordinal);
}
finally
{
Environment.ExitCode = originalExit;
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandlePolicySimulateAsync_WritesJsonOutput()
{
var originalExit = Environment.ExitCode;
var originalOut = Console.Out;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
backend.SimulationResult = new PolicySimulationResult(
new PolicySimulationDiff(
"scheduler.policy-diff-summary@1",
0,
0,
5,
new ReadOnlyDictionary<string, PolicySimulationSeverityDelta>(new Dictionary<string, PolicySimulationSeverityDelta>(0, StringComparer.Ordinal)),
new ReadOnlyCollection<PolicySimulationRuleDelta>(Array.Empty<PolicySimulationRuleDelta>())),
null);
var provider = BuildServiceProvider(backend);
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
await CommandHandlers.HandlePolicySimulateAsync(
provider,
policyId: "P-9",
baseVersion: null,
candidateVersion: 5,
sbomArguments: Array.Empty<string>(),
environmentArguments: new[] { "sealed=true", "threshold=0.8" },
format: "json",
outputPath: null,
explain: false,
failOnDiff: false,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
using var document = JsonDocument.Parse(writer.ToString());
var root = document.RootElement;
Assert.Equal("P-9", root.GetProperty("policyId").GetString());
Assert.Equal(5, root.GetProperty("candidateVersion").GetInt32());
Assert.True(root.TryGetProperty("environment", out var envElement) && envElement.TryGetProperty("sealed", out var sealedElement) && sealedElement.GetBoolean());
Assert.True(envElement.TryGetProperty("threshold", out var thresholdElement) && Math.Abs(thresholdElement.GetDouble() - 0.8) < 0.0001);
}
finally
{
Console.SetOut(originalOut);
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicySimulateAsync_FailOnDiffSetsExitCode20()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
backend.SimulationResult = new PolicySimulationResult(
new PolicySimulationDiff(
null,
1,
0,
0,
new ReadOnlyDictionary<string, PolicySimulationSeverityDelta>(new Dictionary<string, PolicySimulationSeverityDelta>(0, StringComparer.Ordinal)),
new ReadOnlyCollection<PolicySimulationRuleDelta>(Array.Empty<PolicySimulationRuleDelta>())),
null);
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicySimulateAsync(
provider,
policyId: "P-11",
baseVersion: null,
candidateVersion: null,
sbomArguments: Array.Empty<string>(),
environmentArguments: Array.Empty<string>(),
format: "json",
outputPath: null,
explain: false,
failOnDiff: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(20, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicySimulateAsync_MapsErrorCodes()
{
var originalExit = Environment.ExitCode;
var originalOut = Console.Out;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
SimulationException = new PolicyApiException("Missing inputs", HttpStatusCode.BadRequest, "ERR_POL_003")
};
var provider = BuildServiceProvider(backend);
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
await CommandHandlers.HandlePolicySimulateAsync(
provider,
policyId: "P-12",
baseVersion: null,
candidateVersion: null,
sbomArguments: Array.Empty<string>(),
environmentArguments: Array.Empty<string>(),
format: "json",
outputPath: null,
explain: false,
failOnDiff: false,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(21, Environment.ExitCode);
}
finally
{
Console.SetOut(originalOut);
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleTaskRunnerSimulateAsync_WritesInteractiveSummary()
{
var originalExit = Environment.ExitCode;
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
console.Width(120);
console.Interactive();
console.EmitAnsiSequences();
AnsiConsole.Console = console;
const string manifest = """
apiVersion: stellaops.io/pack.v1
kind: TaskPack
metadata:
name: sample-pack
spec:
steps:
- id: prepare
run:
uses: builtin:prepare
- id: approval
gate:
approval:
id: security-review
message: Security approval required.
""";
using var manifestFile = new TempFile("pack.yaml", Encoding.UTF8.GetBytes(manifest));
var simulationResult = new TaskRunnerSimulationResult(
"hash-abc123",
new TaskRunnerSimulationFailurePolicy(3, 15, false),
new[]
{
new TaskRunnerSimulationStep(
"prepare",
"prepare",
"Run",
true,
"succeeded",
null,
"builtin:prepare",
null,
null,
null,
false,
Array.Empty<TaskRunnerSimulationStep>()),
new TaskRunnerSimulationStep(
"approval",
"approval",
"GateApproval",
true,
"pending",
"requires-approval",
null,
"security-review",
"Security approval required.",
null,
false,
Array.Empty<TaskRunnerSimulationStep>())
},
new[]
{
new TaskRunnerSimulationOutput("bundlePath", "file", false, "artifacts/report.json", null)
},
true);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
TaskRunnerSimulationResult = simulationResult
};
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandleTaskRunnerSimulateAsync(
provider,
manifestFile.Path,
inputsPath: null,
format: null,
outputPath: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.NotNull(backend.LastTaskRunnerSimulationRequest);
Assert.Contains("approval", console.Output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Plan Hash", console.Output, StringComparison.OrdinalIgnoreCase);
}
finally
{
AnsiConsole.Console = originalConsole;
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleTaskRunnerSimulateAsync_WritesJsonOutput()
{
var originalExit = Environment.ExitCode;
var originalOut = Console.Out;
const string manifest = """
apiVersion: stellaops.io/pack.v1
kind: TaskPack
metadata:
name: sample-pack
spec:
steps:
- id: prepare
run:
uses: builtin:prepare
""";
using var manifestFile = new TempFile("pack.yaml", Encoding.UTF8.GetBytes(manifest));
using var inputsFile = new TempFile("inputs.json", Encoding.UTF8.GetBytes("{\"dryRun\":false}"));
using var outputDirectory = new TempDirectory();
var outputPath = Path.Combine(outputDirectory.Path, "simulation.json");
var simulationResult = new TaskRunnerSimulationResult(
"hash-xyz789",
new TaskRunnerSimulationFailurePolicy(2, 10, true),
Array.Empty<TaskRunnerSimulationStep>(),
Array.Empty<TaskRunnerSimulationOutput>(),
false);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
TaskRunnerSimulationResult = simulationResult
};
var provider = BuildServiceProvider(backend);
using var writer = new StringWriter();
Console.SetOut(writer);
try
{
await CommandHandlers.HandleTaskRunnerSimulateAsync(
provider,
manifestFile.Path,
inputsFile.Path,
format: "json",
outputPath: outputPath,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.NotNull(backend.LastTaskRunnerSimulationRequest);
var consoleOutput = writer.ToString();
using (var consoleJson = JsonDocument.Parse(consoleOutput))
{
Assert.Equal("hash-xyz789", consoleJson.RootElement.GetProperty("planHash").GetString());
}
var fileOutput = await File.ReadAllTextAsync(outputPath);
using (var fileJson = JsonDocument.Parse(fileOutput))
{
Assert.Equal("hash-xyz789", fileJson.RootElement.GetProperty("planHash").GetString());
}
Assert.True(backend.LastTaskRunnerSimulationRequest!.Inputs!.TryGetPropertyValue("dryRun", out var dryRunNode));
Assert.False(dryRunNode!.GetValue<bool>());
}
finally
{
Console.SetOut(originalOut);
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicyActivateAsync_DisplaysInteractiveSummary()
{
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));
backend.ActivationResult = new PolicyActivationResult(
"activated",
new PolicyActivationRevision(
"P-7",
4,
"active",
true,
DateTimeOffset.Parse("2025-10-27T00:00:00Z", CultureInfo.InvariantCulture),
DateTimeOffset.Parse("2025-10-27T01:15:00Z", CultureInfo.InvariantCulture),
new ReadOnlyCollection<PolicyActivationApproval>(new List<PolicyActivationApproval>
{
new("user:alice", DateTimeOffset.Parse("2025-10-27T01:10:00Z", CultureInfo.InvariantCulture), "Primary"),
new("user:bob", DateTimeOffset.Parse("2025-10-27T01:12:00Z", CultureInfo.InvariantCulture), null)
})));
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicyActivateAsync(
provider,
policyId: "P-7",
version: 4,
note: "Rolling forward",
runNow: true,
scheduledAt: null,
priority: "high",
rollback: false,
incidentId: "INC-204",
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.NotNull(backend.LastPolicyActivation);
var activation = backend.LastPolicyActivation!.Value;
Assert.Equal("P-7", activation.PolicyId);
Assert.Equal(4, activation.Version);
Assert.True(activation.Request.RunNow);
Assert.Null(activation.Request.ScheduledAt);
Assert.Equal("high", activation.Request.Priority);
Assert.Equal("INC-204", activation.Request.IncidentId);
Assert.Equal("Rolling forward", activation.Request.Comment);
var output = console.Output;
Assert.Contains("activated", output, StringComparison.OrdinalIgnoreCase);
Assert.Contains("user:alice", output, StringComparison.Ordinal);
Assert.Contains("Rolling forward", output, StringComparison.Ordinal);
}
finally
{
Environment.ExitCode = originalExit;
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandlePolicyActivateAsync_PendingSecondApprovalSetsExitCode()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
backend.ActivationResult = new PolicyActivationResult(
"pending_second_approval",
new PolicyActivationRevision(
"P-7",
4,
"approved",
true,
DateTimeOffset.UtcNow,
null,
new ReadOnlyCollection<PolicyActivationApproval>(new List<PolicyActivationApproval>
{
new("user:alice", DateTimeOffset.UtcNow, "Primary")
})));
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicyActivateAsync(
provider,
policyId: "P-7",
version: 4,
note: null,
runNow: false,
scheduledAt: null,
priority: null,
rollback: false,
incidentId: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(75, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicyActivateAsync_ParsesScheduledTimestamp()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
backend.ActivationResult = new PolicyActivationResult(
"scheduled",
new PolicyActivationRevision(
"P-8",
5,
"approved",
false,
DateTimeOffset.Parse("2025-12-01T00:30:00Z", CultureInfo.InvariantCulture),
null,
new ReadOnlyCollection<PolicyActivationApproval>(Array.Empty<PolicyActivationApproval>())));
var provider = BuildServiceProvider(backend);
try
{
const string scheduledValue = "2025-12-01T03:00:00+02:00";
await CommandHandlers.HandlePolicyActivateAsync(
provider,
policyId: "P-8",
version: 5,
note: null,
runNow: false,
scheduledAt: scheduledValue,
priority: null,
rollback: false,
incidentId: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.NotNull(backend.LastPolicyActivation);
var activation = backend.LastPolicyActivation!.Value;
Assert.False(activation.Request.RunNow);
var expected = DateTimeOffset.Parse(
scheduledValue,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
Assert.Equal(expected, activation.Request.ScheduledAt);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandlePolicyActivateAsync_MapsErrorCodes()
{
var originalExit = Environment.ExitCode;
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
ActivationException = new PolicyApiException("Revision not approved", HttpStatusCode.BadRequest, "ERR_POL_002")
};
var provider = BuildServiceProvider(backend);
try
{
await CommandHandlers.HandlePolicyActivateAsync(
provider,
policyId: "P-9",
version: 2,
note: null,
runNow: false,
scheduledAt: null,
priority: null,
rollback: false,
incidentId: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(70, Environment.ExitCode);
}
finally
{
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);
[Fact]
public async Task HandleSourcesIngestAsync_NoViolations_WritesJsonReport()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
using var tempDir = new TempDirectory();
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
var originalOut = Console.Out;
using var writer = new StringWriter();
try
{
Environment.SetEnvironmentVariable("STELLA_TENANT", "tenant-alpha");
AnsiConsole.Console = console;
Console.SetOut(writer);
var inputPath = Path.Combine(tempDir.Path, "payload.json");
await File.WriteAllTextAsync(inputPath, "{ \"id\": 1 }");
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
DryRunResponse = new AocIngestDryRunResponse
{
Source = "redhat",
Tenant = "tenant-alpha",
Status = "ok",
Document = new AocIngestDryRunDocumentResult
{
ContentHash = "sha256:test"
},
Violations = Array.Empty<AocIngestDryRunViolation>()
}
};
var provider = BuildServiceProvider(backend);
var outputPath = Path.Combine(tempDir.Path, "dry-run.json");
await CommandHandlers.HandleSourcesIngestAsync(
provider,
dryRun: true,
source: "RedHat",
input: inputPath,
tenantOverride: null,
format: "json",
disableColor: true,
output: outputPath,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.True(File.Exists(outputPath));
Assert.NotNull(backend.LastDryRunRequest);
var request = backend.LastDryRunRequest!;
Assert.Equal("tenant-alpha", request.Tenant);
Assert.Equal("RedHat", request.Source);
Assert.Equal("payload.json", request.Document.Name);
Assert.Equal("application/json", request.Document.ContentType);
Assert.Null(request.Document.ContentEncoding);
using (var document = JsonDocument.Parse(request.Document.Content))
{
Assert.Equal(1, document.RootElement.GetProperty("id").GetInt32());
}
var consoleJson = writer.ToString();
Assert.Contains("\"status\": \"ok\"", consoleJson);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
AnsiConsole.Console = originalConsole;
Console.SetOut(originalOut);
}
}
[Fact]
public async Task HandleSourcesIngestAsync_ViolationMapsExitCode()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
using var tempDir = new TempDirectory();
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
try
{
Environment.SetEnvironmentVariable("STELLA_TENANT", "tenant-beta");
AnsiConsole.Console = console;
var inputPath = Path.Combine(tempDir.Path, "payload.json");
await File.WriteAllTextAsync(inputPath, "{ \"id\": 2 }");
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
DryRunResponse = new AocIngestDryRunResponse
{
Status = "error",
Violations = new[]
{
new AocIngestDryRunViolation
{
Code = "ERR_AOC_002",
Message = "merge detected",
Path = "/content/derived"
}
}
}
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleSourcesIngestAsync(
provider,
dryRun: true,
source: "osv",
input: inputPath,
tenantOverride: null,
format: "table",
disableColor: true,
output: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(12, Environment.ExitCode);
var output = console.Output;
Assert.Contains("ERR_AOC_002", output);
Assert.Contains("/content/derived", output);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandleSourcesIngestAsync_MissingTenant_ReturnsUsageError()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
using var tempDir = new TempDirectory();
try
{
Environment.SetEnvironmentVariable("STELLA_TENANT", null);
var inputPath = Path.Combine(tempDir.Path, "payload.json");
await File.WriteAllTextAsync(inputPath, "{ \"id\": 3 }");
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleSourcesIngestAsync(
provider,
dryRun: true,
source: "osv",
input: inputPath,
tenantOverride: null,
format: "table",
disableColor: true,
output: null,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(70, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
}
}
[Fact]
public async Task HandleAocVerifyAsync_NoViolations_WritesReportAndReturnsZero()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
using var tempDir = new TempDirectory();
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
var originalOut = Console.Out;
using var writer = new StringWriter();
try
{
AnsiConsole.Console = console;
Console.SetOut(writer);
Environment.SetEnvironmentVariable("STELLA_TENANT", "tenant-a");
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
VerifyResponse = new AocVerifyResponse
{
Tenant = "tenant-a",
Window = new AocVerifyWindow
{
From = DateTimeOffset.Parse("2025-10-25T12:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal),
To = DateTimeOffset.Parse("2025-10-26T12:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)
},
Checked = new AocVerifyChecked { Advisories = 4, Vex = 1 },
Metrics = new AocVerifyMetrics { IngestionWriteTotal = 5, AocViolationTotal = 0 },
Violations = Array.Empty<AocVerifyViolation>(),
Truncated = false
}
};
var provider = BuildServiceProvider(backend);
var exportPath = Path.Combine(tempDir.Path, "verify.json");
await CommandHandlers.HandleAocVerifyAsync(
provider,
sinceOption: "2025-10-25T12:00:00Z",
limitOption: 10,
sourcesOption: "RedHat,Ubuntu",
codesOption: "err_aoc_001",
format: "json",
exportPath: exportPath,
tenantOverride: null,
disableColor: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.True(File.Exists(exportPath));
Assert.NotNull(backend.LastVerifyRequest);
Assert.Equal("tenant-a", backend.LastVerifyRequest!.Tenant);
var expectedSince = DateTimeOffset.Parse("2025-10-25T12:00:00Z", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
var actualSince = DateTimeOffset.Parse(backend.LastVerifyRequest.Since!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
Assert.Equal(expectedSince, actualSince);
Assert.Equal(10, backend.LastVerifyRequest.Limit);
Assert.Equal(new[] { "redhat", "ubuntu" }, backend.LastVerifyRequest.Sources);
Assert.Equal(new[] { "ERR_AOC_001" }, backend.LastVerifyRequest.Codes);
var jsonOutput = writer.ToString();
Assert.Contains("\"tenant\": \"tenant-a\"", jsonOutput);
Assert.Contains("\"ingestion_write_total\": 5", jsonOutput);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
Console.SetOut(originalOut);
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandleAocVerifyAsync_WithViolations_MapsExitCode()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
try
{
AnsiConsole.Console = console;
Environment.SetEnvironmentVariable("STELLA_TENANT", "tenant-b");
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
VerifyResponse = new AocVerifyResponse
{
Violations = new[]
{
new AocVerifyViolation
{
Code = "ERR_AOC_003",
Count = 2,
Examples = new[]
{
new AocVerifyViolationExample
{
Source = "redhat",
DocumentId = "doc-1",
Path = "/content/raw"
}
}
}
}
}
};
var provider = BuildServiceProvider(backend);
var capturedBefore = DateTimeOffset.UtcNow;
await CommandHandlers.HandleAocVerifyAsync(
provider,
sinceOption: "24h",
limitOption: null,
sourcesOption: null,
codesOption: null,
format: "table",
exportPath: null,
tenantOverride: null,
disableColor: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(13, Environment.ExitCode);
Assert.NotNull(backend.LastVerifyRequest);
Assert.Equal(20, backend.LastVerifyRequest!.Limit);
Assert.Null(backend.LastVerifyRequest.Sources);
Assert.Null(backend.LastVerifyRequest.Codes);
var parsedSince = DateTimeOffset.Parse(backend.LastVerifyRequest.Since!, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal);
var expectedSince = capturedBefore.AddHours(-24);
Assert.InRange((expectedSince - parsedSince).Duration(), TimeSpan.Zero, TimeSpan.FromSeconds(10));
var output = console.Output;
Assert.Contains("ERR_AOC_003", output);
Assert.Contains("doc-1", output);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandleAocVerifyAsync_TruncatedWithoutViolations_ReturnsExitCode18()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
var originalConsole = AnsiConsole.Console;
var console = new TestConsole();
try
{
AnsiConsole.Console = console;
Environment.SetEnvironmentVariable("STELLA_TENANT", "tenant-c");
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null))
{
VerifyResponse = new AocVerifyResponse
{
Violations = Array.Empty<AocVerifyViolation>(),
Truncated = true
}
};
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAocVerifyAsync(
provider,
sinceOption: "2025-01-01T00:00:00Z",
limitOption: 0,
sourcesOption: null,
codesOption: null,
format: "table",
exportPath: null,
tenantOverride: null,
disableColor: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(18, Environment.ExitCode);
var output = console.Output;
Assert.Contains("Truncated", output);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
AnsiConsole.Console = originalConsole;
}
}
[Fact]
public async Task HandleAocVerifyAsync_MissingTenant_ReturnsUsageError()
{
var originalExitCode = Environment.ExitCode;
var originalTenant = Environment.GetEnvironmentVariable("STELLA_TENANT");
try
{
Environment.SetEnvironmentVariable("STELLA_TENANT", null);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
await CommandHandlers.HandleAocVerifyAsync(
provider,
sinceOption: "24h",
limitOption: null,
sourcesOption: null,
codesOption: null,
format: "table",
exportPath: null,
tenantOverride: null,
disableColor: true,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(71, Environment.ExitCode);
}
finally
{
Environment.ExitCode = originalExitCode;
Environment.SetEnvironmentVariable("STELLA_TENANT", originalTenant);
}
}
[Fact]
public async Task HandleKmsExportAsync_WritesKeyBundle()
{
using var kmsRoot = new TempDirectory();
using var exportRoot = new TempDirectory();
const string passphrase = "P@ssw0rd!";
using (var client = new FileKmsClient(new FileKmsOptions
{
RootPath = kmsRoot.Path,
Password = passphrase
}))
{
await client.RotateAsync("cli-export-key");
}
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
var outputPath = Path.Combine(exportRoot.Path, "export.json");
var originalExit = Environment.ExitCode;
try
{
await CommandHandlers.HandleKmsExportAsync(
provider,
kmsRoot.Path,
keyId: "cli-export-key",
versionId: null,
outputPath: outputPath,
overwrite: false,
passphrase: passphrase,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
Assert.True(File.Exists(outputPath));
var json = await File.ReadAllTextAsync(outputPath);
var material = JsonSerializer.Deserialize<KmsKeyMaterial>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Assert.NotNull(material);
Assert.Equal("cli-export-key", material!.KeyId);
Assert.False(string.IsNullOrWhiteSpace(material.VersionId));
Assert.NotNull(material.D);
}
finally
{
Environment.ExitCode = originalExit;
}
}
[Fact]
public async Task HandleKmsImportAsync_ImportsKeyBundle()
{
using var sourceRoot = new TempDirectory();
using var targetRoot = new TempDirectory();
const string passphrase = "AnotherP@ssw0rd!";
KmsKeyMaterial exported;
using (var sourceClient = new FileKmsClient(new FileKmsOptions
{
RootPath = sourceRoot.Path,
Password = passphrase
}))
{
await sourceClient.RotateAsync("cli-import-key");
exported = await sourceClient.ExportAsync("cli-import-key", null);
}
var exportPath = Path.Combine(sourceRoot.Path, "import.json");
var exportJson = JsonSerializer.Serialize(exported, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
await File.WriteAllTextAsync(exportPath, exportJson);
var backend = new StubBackendClient(new JobTriggerResult(true, "ok", null, null));
var provider = BuildServiceProvider(backend);
var originalExit = Environment.ExitCode;
try
{
await CommandHandlers.HandleKmsImportAsync(
provider,
targetRoot.Path,
keyId: "cli-import-key",
inputPath: exportPath,
versionOverride: null,
passphrase: passphrase,
verbose: false,
cancellationToken: CancellationToken.None);
Assert.Equal(0, Environment.ExitCode);
using var importedClient = new FileKmsClient(new FileKmsOptions
{
RootPath = targetRoot.Path,
Password = passphrase
});
var metadata = await importedClient.GetMetadataAsync("cli-import-key");
Assert.Equal(KmsKeyState.Active, metadata.State);
Assert.Single(metadata.Versions);
Assert.Equal(exported.VersionId, metadata.Versions[0].VersionId);
}
finally
{
Environment.ExitCode = originalExit;
}
}
private static void CreateJavaLockFixture(string root)
{
Directory.CreateDirectory(root);
var jarPath = Path.Combine(root, "runtime-only-1.0.0.jar");
CreateJavaJar(jarPath, "com.example", "runtime-only", "1.0.0");
var gradleLock = string.Join(
Environment.NewLine,
"# Gradle lockfile",
"com.example:declared-only:2.0.0=runtimeClasspath");
File.WriteAllText(Path.Combine(root, "gradle.lockfile"), gradleLock);
}
private static void CreateJavaJar(string path, string groupId, string artifactId, string version)
{
if (File.Exists(path))
{
File.Delete(path);
}
using var archive = ZipFile.Open(path, ZipArchiveMode.Create);
var pomEntryPath = $"META-INF/maven/{groupId}/{artifactId}/pom.properties";
var pomEntry = archive.CreateEntry(pomEntryPath);
using (var writer = new StreamWriter(pomEntry.Open(), Encoding.UTF8))
{
writer.WriteLine($"groupId={groupId}");
writer.WriteLine($"artifactId={artifactId}");
writer.WriteLine($"version={version}");
writer.WriteLine("packaging=jar");
writer.WriteLine($"name={artifactId}");
}
var manifestEntry = archive.CreateEntry("META-INF/MANIFEST.MF");
using (var writer = new StreamWriter(manifestEntry.Open(), Encoding.UTF8))
{
writer.WriteLine("Manifest-Version: 1.0");
writer.WriteLine($"Implementation-Title: {artifactId}");
writer.WriteLine($"Implementation-Version: {version}");
writer.WriteLine($"Implementation-Vendor: {groupId}");
}
var classEntry = archive.CreateEntry($"{artifactId.Replace('-', '_')}/Main.class");
using var classStream = classEntry.Open();
classStream.Write(new byte[] { 0xCA, 0xFE, 0xBA, 0xBE });
}
private static void CreateNodeLockFixture(string root)
{
Directory.CreateDirectory(root);
var packageJson = """
{
"name": "workspace-app",
"version": "1.0.0",
"dependencies": {
"declared-only": "9.9.9",
"runtime-only": "1.0.0"
}
}
""";
File.WriteAllText(Path.Combine(root, "package.json"), packageJson);
var runtimeDir = Path.Combine(root, "node_modules", "runtime-only");
Directory.CreateDirectory(runtimeDir);
var runtimePackageJson = """
{
"name": "runtime-only",
"version": "1.0.0"
}
""";
File.WriteAllText(Path.Combine(runtimeDir, "package.json"), runtimePackageJson);
var packageLock = """
{
"name": "workspace-app",
"version": "1.0.0",
"lockfileVersion": 3,
"packages": {
"": {
"name": "workspace-app",
"version": "1.0.0"
},
"node_modules/declared-only": {
"name": "declared-only",
"version": "9.9.9",
"resolved": "https://registry.example/declared-only-9.9.9.tgz",
"integrity": "sha512-DECLAREDONLY"
}
}
}
""";
File.WriteAllText(Path.Combine(root, "package-lock.json"), packageLock);
}
private static IServiceProvider BuildServiceProvider(
IBackendOperationsClient backend,
IScannerExecutor? executor = null,
IScannerInstaller? installer = null,
StellaOpsCliOptions? options = null,
IStellaOpsTokenClient? tokenClient = null,
IConcelierObservationsClient? concelierClient = null,
ILoggerProvider? loggerProvider = null)
{
var services = new ServiceCollection();
services.AddSingleton(backend);
services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Debug);
if (loggerProvider is not null)
{
builder.AddProvider(loggerProvider);
}
}));
services.AddSingleton(new VerbosityState());
services.AddHttpClient();
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);
}
services.AddSingleton<IConcelierObservationsClient>(
concelierClient ?? new StubConcelierObservationsClient());
return services.BuildServiceProvider();
}
private static async Task<CapturedConsoleOutput> CaptureTestConsoleAsync(Func<TestConsole, Task> action)
{
var testConsole = new TestConsole();
var originalConsole = AnsiConsole.Console;
var originalOut = Console.Out;
using var writer = new StringWriter();
try
{
AnsiConsole.Console = testConsole;
Console.SetOut(writer);
await action(testConsole).ConfigureAwait(false);
return new CapturedConsoleOutput(testConsole.Output.ToString(), writer.ToString());
}
finally
{
Console.SetOut(originalOut);
AnsiConsole.Console = originalConsole;
}
}
private static async Task CreatePythonLockFixtureAsync(string root, CancellationToken cancellationToken)
{
await CreatePythonPackageAsync(root, "locked", "1.0.0", cancellationToken).ConfigureAwait(false);
await CreatePythonPackageAsync(root, "runtime-only", "2.0.0", cancellationToken).ConfigureAwait(false);
var requirements = new StringBuilder()
.AppendLine("locked==1.0.0")
.AppendLine("declared-only==3.0.0")
.ToString();
var path = Path.Combine(root, "requirements.txt");
await File.WriteAllTextAsync(path, requirements, cancellationToken).ConfigureAwait(false);
}
private static async Task CreatePythonPackageAsync(string root, string name, string version, CancellationToken cancellationToken)
{
var sitePackages = Path.Combine(root, "lib", "python3.11", "site-packages");
Directory.CreateDirectory(sitePackages);
var packageDir = Path.Combine(sitePackages, name);
Directory.CreateDirectory(packageDir);
var modulePath = Path.Combine(packageDir, "__init__.py");
var moduleContent = $"__version__ = \"{version}\"{Environment.NewLine}";
await File.WriteAllTextAsync(modulePath, moduleContent, cancellationToken).ConfigureAwait(false);
var distInfoDir = Path.Combine(sitePackages, $"{name}-{version}.dist-info");
Directory.CreateDirectory(distInfoDir);
var metadataPath = Path.Combine(distInfoDir, "METADATA");
var metadataContent = $"Metadata-Version: 2.1{Environment.NewLine}Name: {name}{Environment.NewLine}Version: {version}{Environment.NewLine}";
await File.WriteAllTextAsync(metadataPath, metadataContent, cancellationToken).ConfigureAwait(false);
var wheelPath = Path.Combine(distInfoDir, "WHEEL");
await File.WriteAllTextAsync(wheelPath, "Wheel-Version: 1.0", cancellationToken).ConfigureAwait(false);
var entryPointsPath = Path.Combine(distInfoDir, "entry_points.txt");
await File.WriteAllTextAsync(entryPointsPath, string.Empty, cancellationToken).ConfigureAwait(false);
var recordPath = Path.Combine(distInfoDir, "RECORD");
var recordContent = new StringBuilder()
.AppendLine($"{name}/__init__.py,sha256={ComputeSha256Base64(modulePath)},{new FileInfo(modulePath).Length}")
.AppendLine($"{name}-{version}.dist-info/METADATA,sha256={ComputeSha256Base64(metadataPath)},{new FileInfo(metadataPath).Length}")
.AppendLine($"{name}-{version}.dist-info/RECORD,,")
.AppendLine($"{name}-{version}.dist-info/WHEEL,sha256={ComputeSha256Base64(wheelPath)},{new FileInfo(wheelPath).Length}")
.AppendLine($"{name}-{version}.dist-info/entry_points.txt,sha256={ComputeSha256Base64(entryPointsPath)},{new FileInfo(entryPointsPath).Length}")
.ToString();
await File.WriteAllTextAsync(recordPath, recordContent, cancellationToken).ConfigureAwait(false);
}
private static void CreateRubyWorkspace(string root)
{
Directory.CreateDirectory(root);
var gemfile = string.Join(
Environment.NewLine,
"source \"https://rubygems.org\"",
string.Empty,
"gem \"rack\", \"~> 3.1\"",
"gem \"zeitwerk\"");
File.WriteAllText(Path.Combine(root, "Gemfile"), gemfile);
var gemfileLock = string.Join(
Environment.NewLine,
"GEM",
" remote: https://rubygems.org/",
" specs:",
" rack (3.1.0)",
" zeitwerk (2.6.13)",
string.Empty,
"PLATFORMS",
" ruby",
string.Empty,
"DEPENDENCIES",
" rack",
" zeitwerk",
string.Empty,
"BUNDLED WITH",
" 2.5.4");
File.WriteAllText(Path.Combine(root, "Gemfile.lock"), gemfileLock);
var app = string.Join(
Environment.NewLine,
"require 'rack'",
string.Empty,
"Rack::Handler::WEBrick.run ->(env) { [200, { 'Content-Type' => 'text/plain' }, ['ok']] }");
File.WriteAllText(Path.Combine(root, "app.rb"), app);
}
private static RubyPackageArtifactModel CreateRubyPackageArtifact(
string id,
string name,
string version,
IReadOnlyList<string>? groups = null,
bool runtimeUsed = false,
string? platform = null,
IDictionary<string, string?>? metadataOverrides = null)
{
var normalizedGroups = groups?.Where(static g => !string.IsNullOrWhiteSpace(g)).Select(static g => g.Trim()).ToArray();
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["source"] = "rubygems",
["lockfile"] = "Gemfile.lock"
};
if (!string.IsNullOrWhiteSpace(platform))
{
metadata["platform"] = platform;
}
if (normalizedGroups is { Length: > 0 })
{
metadata["groups"] = string.Join(';', normalizedGroups);
}
if (runtimeUsed)
{
metadata["runtime.used"] = "true";
metadata["runtime.entrypoints"] = "app.rb";
metadata["runtime.files"] = "app.rb";
metadata["runtime.reasons"] = "require-static";
}
var mergedMetadata = new Dictionary<string, string?>(metadata, StringComparer.OrdinalIgnoreCase);
if (metadataOverrides is not null)
{
foreach (var pair in metadataOverrides)
{
mergedMetadata[pair.Key] = pair.Value;
}
}
var runtime = runtimeUsed
? new RubyPackageRuntime(new[] { "app.rb" }, new[] { "app.rb" }, new[] { "require-static" })
: null;
return new RubyPackageArtifactModel(
id,
name,
version,
"rubygems",
platform,
normalizedGroups,
DeclaredOnly: false,
RuntimeUsed: runtimeUsed,
new RubyPackageProvenance("rubygems", "Gemfile.lock", $"specs/{name}"),
runtime,
mergedMetadata);
}
private static string ComputeSha256Base64(string path)
{
using var sha = SHA256.Create();
using var stream = File.OpenRead(path);
var hash = sha.ComputeHash(stream);
return Convert.ToBase64String(hash);
}
private sealed record CapturedConsoleOutput(string SpectreBuffer, string PlainBuffer)
{
public string Combined => string.Concat(SpectreBuffer, PlainBuffer);
}
private sealed class TestLoggerProvider : ILoggerProvider
{
private readonly List<LogEntry> _entries = new();
public IReadOnlyList<LogEntry> Entries => _entries;
public ILogger CreateLogger(string categoryName) => new TestLogger(categoryName, _entries);
public void Dispose()
{
}
private sealed class TestLogger : ILogger
{
private readonly string _category;
private readonly List<LogEntry> _entries;
public TestLogger(string category, List<LogEntry> entries)
{
_category = category;
_entries = entries;
}
public IDisposable? BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var message = formatter(state, exception);
_entries.Add(new LogEntry(logLevel, _category, eventId, message, exception));
}
}
public sealed record LogEntry(LogLevel Level, string Category, EventId EventId, string Message, Exception? Exception);
}
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 IReadOnlyList<RubyPackageArtifactModel> RubyPackages { get; set; } = Array.Empty<RubyPackageArtifactModel>();
public Exception? RubyPackagesException { get; set; }
public string? LastRubyPackagesScanId { 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 PolicySimulationResult SimulationResult { get; set; } = new PolicySimulationResult(
new PolicySimulationDiff(
null,
0,
0,
0,
new ReadOnlyDictionary<string, PolicySimulationSeverityDelta>(new Dictionary<string, PolicySimulationSeverityDelta>(0, StringComparer.Ordinal)),
new ReadOnlyCollection<PolicySimulationRuleDelta>(Array.Empty<PolicySimulationRuleDelta>())),
null);
public PolicyApiException? SimulationException { get; set; }
public (string PolicyId, PolicySimulationInput Input)? LastPolicySimulation { get; private set; }
public TaskRunnerSimulationRequest? LastTaskRunnerSimulationRequest { get; private set; }
public TaskRunnerSimulationResult TaskRunnerSimulationResult { get; set; } = new(
string.Empty,
new TaskRunnerSimulationFailurePolicy(1, 0, false),
Array.Empty<TaskRunnerSimulationStep>(),
Array.Empty<TaskRunnerSimulationOutput>(),
false);
public Exception? TaskRunnerSimulationException { get; set; }
public PolicyActivationResult ActivationResult { get; set; } = new PolicyActivationResult(
"activated",
new PolicyActivationRevision(
"P-0",
1,
"active",
false,
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow,
new ReadOnlyCollection<PolicyActivationApproval>(Array.Empty<PolicyActivationApproval>())));
public PolicyApiException? ActivationException { get; set; }
public (string PolicyId, int Version, PolicyActivationRequest Request)? LastPolicyActivation { get; private set; }
public AocIngestDryRunResponse DryRunResponse { get; set; } = new();
public Exception? DryRunException { get; set; }
public AocIngestDryRunRequest? LastDryRunRequest { get; private set; }
public AocVerifyResponse VerifyResponse { get; set; } = new();
public Exception? VerifyException { get; set; }
public AocVerifyRequest? LastVerifyRequest { get; private set; }
public PolicyFindingsPage FindingsPage { get; set; } = new PolicyFindingsPage(Array.Empty<PolicyFindingDocument>(), null, null);
public PolicyFindingsQuery? LastFindingsQuery { get; private set; }
public PolicyApiException? FindingsListException { get; set; }
public PolicyFindingDocument FindingDocument { get; set; } = new PolicyFindingDocument(
"finding-default",
"affected",
new PolicyFindingSeverity("High", 7.5),
"sbom:default",
Array.Empty<string>(),
null,
1,
DateTimeOffset.UtcNow,
null);
public (string PolicyId, string FindingId)? LastFindingGet { get; private set; }
public PolicyApiException? FindingGetException { get; set; }
public PolicyFindingExplainResult ExplainResult { get; set; } = new PolicyFindingExplainResult(
"finding-default",
1,
new ReadOnlyCollection<PolicyFindingExplainStep>(Array.Empty<PolicyFindingExplainStep>()),
new ReadOnlyCollection<PolicyFindingExplainHint>(Array.Empty<PolicyFindingExplainHint>()));
public (string PolicyId, string FindingId, string? Mode)? LastFindingExplain { get; private set; }
public PolicyApiException? FindingExplainException { get; set; }
public EntryTraceResponseModel? EntryTraceResponse { get; set; }
public Exception? EntryTraceException { get; set; }
public string? LastEntryTraceScanId { get; private set; }
public List<(AdvisoryAiTaskType TaskType, AdvisoryPipelinePlanRequestModel Request)> AdvisoryPlanRequests { get; } = new();
public AdvisoryPipelinePlanResponseModel? AdvisoryPlanResponse { get; set; }
public Exception? AdvisoryPlanException { get; set; }
public Queue<AdvisoryPipelineOutputModel?> AdvisoryOutputQueue { get; } = new();
public AdvisoryPipelineOutputModel? AdvisoryOutputResponse { get; set; }
public Exception? AdvisoryOutputException { get; set; }
public List<(string CacheKey, AdvisoryAiTaskType TaskType, string Profile)> AdvisoryOutputRequests { get; } = new();
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<PolicySimulationResult> SimulatePolicyAsync(string policyId, PolicySimulationInput input, CancellationToken cancellationToken)
{
LastPolicySimulation = (policyId, input);
if (SimulationException is not null)
{
throw SimulationException;
}
return Task.FromResult(SimulationResult);
}
public Task<TaskRunnerSimulationResult> SimulateTaskRunnerAsync(TaskRunnerSimulationRequest request, CancellationToken cancellationToken)
{
LastTaskRunnerSimulationRequest = request;
if (TaskRunnerSimulationException is not null)
{
throw TaskRunnerSimulationException;
}
return Task.FromResult(TaskRunnerSimulationResult);
}
public Task<PolicyActivationResult> ActivatePolicyRevisionAsync(string policyId, int version, PolicyActivationRequest request, CancellationToken cancellationToken)
{
LastPolicyActivation = (policyId, version, request);
if (ActivationException is not null)
{
throw ActivationException;
}
return Task.FromResult(ActivationResult);
}
public Task<AocIngestDryRunResponse> ExecuteAocIngestDryRunAsync(AocIngestDryRunRequest request, CancellationToken cancellationToken)
{
LastDryRunRequest = request;
if (DryRunException is not null)
{
throw DryRunException;
}
return Task.FromResult(DryRunResponse);
}
public Task<AocVerifyResponse> ExecuteAocVerifyAsync(AocVerifyRequest request, CancellationToken cancellationToken)
{
LastVerifyRequest = request;
if (VerifyException is not null)
{
throw VerifyException;
}
return Task.FromResult(VerifyResponse);
}
public Task<PolicyFindingsPage> GetPolicyFindingsAsync(PolicyFindingsQuery query, CancellationToken cancellationToken)
{
LastFindingsQuery = query;
if (FindingsListException is not null)
{
throw FindingsListException;
}
return Task.FromResult(FindingsPage);
}
public Task<PolicyFindingDocument> GetPolicyFindingAsync(string policyId, string findingId, CancellationToken cancellationToken)
{
LastFindingGet = (policyId, findingId);
if (FindingGetException is not null)
{
throw FindingGetException;
}
return Task.FromResult(FindingDocument);
}
public Task<PolicyFindingExplainResult> GetPolicyFindingExplainAsync(string policyId, string findingId, string? mode, CancellationToken cancellationToken)
{
LastFindingExplain = (policyId, findingId, mode);
if (FindingExplainException is not null)
{
throw FindingExplainException;
}
return Task.FromResult(ExplainResult);
}
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();
public Task<EntryTraceResponseModel?> GetEntryTraceAsync(string scanId, CancellationToken cancellationToken)
{
LastEntryTraceScanId = scanId;
if (EntryTraceException is not null)
{
throw EntryTraceException;
}
return Task.FromResult(EntryTraceResponse);
}
public Task<IReadOnlyList<RubyPackageArtifactModel>> GetRubyPackagesAsync(string scanId, CancellationToken cancellationToken)
{
LastRubyPackagesScanId = scanId;
if (RubyPackagesException is not null)
{
throw RubyPackagesException;
}
return Task.FromResult(RubyPackages);
}
public Task<AdvisoryPipelinePlanResponseModel> CreateAdvisoryPipelinePlanAsync(AdvisoryAiTaskType taskType, AdvisoryPipelinePlanRequestModel request, CancellationToken cancellationToken)
{
AdvisoryPlanRequests.Add((taskType, request));
if (AdvisoryPlanException is not null)
{
throw AdvisoryPlanException;
}
var response = AdvisoryPlanResponse ?? new AdvisoryPipelinePlanResponseModel
{
TaskType = taskType.ToString(),
CacheKey = "stub-cache-key",
PromptTemplate = "prompts/advisory/stub.liquid",
Budget = new AdvisoryTaskBudgetModel
{
PromptTokens = 0,
CompletionTokens = 0
},
Chunks = Array.Empty<PipelineChunkSummaryModel>(),
Vectors = Array.Empty<PipelineVectorSummaryModel>(),
Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
};
return Task.FromResult(response);
}
public Task<AdvisoryPipelineOutputModel?> TryGetAdvisoryPipelineOutputAsync(string cacheKey, AdvisoryAiTaskType taskType, string profile, CancellationToken cancellationToken)
{
AdvisoryOutputRequests.Add((cacheKey, taskType, profile));
if (AdvisoryOutputException is not null)
{
throw AdvisoryOutputException;
}
if (AdvisoryOutputQueue.Count > 0)
{
return Task.FromResult(AdvisoryOutputQueue.Dequeue());
}
return Task.FromResult(AdvisoryOutputResponse);
}
}
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 IReadOnlyDictionary<string, string>? LastAdditionalParameters { 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, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
ClientCredentialRequests++;
LastAdditionalParameters = additionalParameters;
return Task.FromResult(_token);
}
public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, IReadOnlyDictionary<string, string>? additionalParameters = null, CancellationToken cancellationToken = default)
{
PasswordRequests++;
LastAdditionalParameters = additionalParameters;
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('/', '_');
}
private sealed class StubConcelierObservationsClient : IConcelierObservationsClient
{
private readonly AdvisoryObservationsResponse _response;
public StubConcelierObservationsClient(AdvisoryObservationsResponse? response = null)
{
_response = response ?? new AdvisoryObservationsResponse();
}
public AdvisoryObservationsQuery? LastQuery { get; private set; }
public Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken)
{
LastQuery = query;
return Task.FromResult(_response);
}
}
}