Add Authority Advisory AI and API Lifecycle Configuration
- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings. - Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations. - Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration. - Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options. - Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations. - Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client. - Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -25,6 +26,7 @@ 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;
|
||||
|
||||
@@ -82,11 +84,11 @@ public sealed class CommandHandlersTests
|
||||
}
|
||||
|
||||
[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));
|
||||
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
|
||||
@@ -117,13 +119,114 @@ public sealed class CommandHandlersTests
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = original;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAuthLoginAsync_UsesClientCredentialsFlow()
|
||||
{
|
||||
var original = Environment.ExitCode;
|
||||
}
|
||||
}
|
||||
|
||||
[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 console = new TestConsole();
|
||||
var originalConsole = AnsiConsole.Console;
|
||||
|
||||
var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null));
|
||||
var provider = BuildServiceProvider(backend);
|
||||
AnsiConsole.Console = console;
|
||||
|
||||
try
|
||||
{
|
||||
await 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", console.Output, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.ExitCode = originalExit;
|
||||
AnsiConsole.Console = originalConsole;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAuthLoginAsync_UsesClientCredentialsFlow()
|
||||
{
|
||||
var original = Environment.ExitCode;
|
||||
using var tempDir = new TempDirectory();
|
||||
|
||||
try
|
||||
@@ -2327,13 +2430,16 @@ public sealed class CommandHandlersTests
|
||||
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 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 Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken)
|
||||
=> throw new NotImplementedException();
|
||||
@@ -2445,27 +2551,37 @@ public sealed class CommandHandlersTests
|
||||
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)
|
||||
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<OfflineKitImportResult> ImportOfflineKitAsync(OfflineKitImportRequest request, CancellationToken cancellationToken)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task<OfflineKitStatus> GetOfflineKitStatusAsync(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);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StubExecutor : IScannerExecutor
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user