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:
master
2025-11-02 13:40:38 +02:00
parent 66cb6c4b8a
commit f98cea3bcf
516 changed files with 68157 additions and 24754 deletions

View File

@@ -1,18 +1,24 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Net;
using System.Net.Http.Json;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Scanner.EntryTrace;
using StellaOps.Scanner.EntryTrace.Serialization;
using StellaOps.Scanner.Storage.Catalog;
using StellaOps.Scanner.Storage.Repositories;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;
namespace StellaOps.Scanner.WebService.Tests;
@@ -126,14 +132,74 @@ public sealed class ScansEndpointsTests
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
Assert.NotNull(coordinator);
Assert.True(coordinator.TokenMatched);
Assert.True(coordinator.LastToken.CanBeCanceled);
}
private sealed class RecordingCoordinator : IScanCoordinator
{
private readonly IHttpContextAccessor accessor;
private readonly InMemoryScanCoordinator inner;
Assert.True(coordinator.TokenMatched);
Assert.True(coordinator.LastToken.CanBeCanceled);
}
[Fact]
public async Task EntryTraceEndpointReturnsStoredResult()
{
using var factory = new ScannerApplicationFactory();
var scanId = $"scan-entrytrace-{Guid.NewGuid():n}";
var graph = new EntryTraceGraph(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray.Create(new EntryTracePlan(
ImmutableArray.Create("/bin/bash", "-lc", "./start.sh"),
ImmutableDictionary<string, string>.Empty,
"/workspace",
"root",
"/bin/bash",
EntryTraceTerminalType.Script,
"bash",
0.9,
ImmutableDictionary<string, string>.Empty)),
ImmutableArray.Create(new EntryTraceTerminal(
"/bin/bash",
EntryTraceTerminalType.Script,
"bash",
0.9,
ImmutableDictionary<string, string>.Empty,
"root",
"/workspace",
ImmutableArray<string>.Empty)));
var ndjson = new List<string> { "{\"kind\":\"entry\"}" };
using (var scope = factory.Services.CreateScope())
{
var repository = scope.ServiceProvider.GetRequiredService<EntryTraceRepository>();
await repository.UpsertAsync(new EntryTraceDocument
{
ScanId = scanId,
ImageDigest = "sha256:entrytrace",
GeneratedAtUtc = DateTime.UtcNow,
GraphJson = EntryTraceGraphSerializer.Serialize(graph),
Ndjson = ndjson
}, CancellationToken.None).ConfigureAwait(false);
}
using var client = factory.CreateClient();
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>();
Assert.NotNull(payload);
Assert.Equal(scanId, payload!.ScanId);
Assert.Equal("sha256:entrytrace", payload.ImageDigest);
Assert.Equal(graph.Outcome, payload.Graph.Outcome);
Assert.Single(payload.Graph.Plans);
Assert.Equal("/bin/bash", payload.Graph.Plans[0].TerminalPath);
Assert.Single(payload.Graph.Terminals);
Assert.Equal(ndjson, payload.Ndjson);
}
private sealed class RecordingCoordinator : IScanCoordinator
{
private readonly IHttpContextAccessor accessor;
private readonly InMemoryScanCoordinator inner;
public RecordingCoordinator(IHttpContextAccessor accessor, TimeProvider timeProvider, IScanProgressPublisher publisher)
{
@@ -358,15 +424,111 @@ public sealed class ScansEndpointsTests
Assert.Equal(new[] { "alpha", "Beta", "zeta" }, names);
}
}
[Fact]
public async Task GetEntryTraceReturnsStoredResult()
{
var scanId = $"scan-{Guid.NewGuid():n}";
var generatedAt = new DateTimeOffset(2025, 11, 1, 12, 0, 0, TimeSpan.Zero);
var plan = new EntryTracePlan(
ImmutableArray.Create("/usr/local/bin/app"),
ImmutableDictionary<string, string>.Empty,
"/workspace",
"appuser",
"/usr/local/bin/app",
EntryTraceTerminalType.Native,
"go",
90d,
ImmutableDictionary<string, string>.Empty);
var terminal = new EntryTraceTerminal(
"/usr/local/bin/app",
EntryTraceTerminalType.Native,
"go",
90d,
ImmutableDictionary<string, string>.Empty,
"appuser",
"/workspace",
ImmutableArray<string>.Empty);
var graph = new EntryTraceGraph(
EntryTraceOutcome.Resolved,
ImmutableArray<EntryTraceNode>.Empty,
ImmutableArray<EntryTraceEdge>.Empty,
ImmutableArray<EntryTraceDiagnostic>.Empty,
ImmutableArray.Create(plan),
ImmutableArray.Create(terminal));
var ndjson = EntryTraceNdjsonWriter.Serialize(
graph,
new EntryTraceNdjsonMetadata(scanId, "sha256:test", generatedAt));
var storedResult = new EntryTraceResult(scanId, "sha256:test", generatedAt, graph, ndjson);
using var factory = new ScannerApplicationFactory(
configuration: null,
services =>
{
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(storedResult));
});
using var client = factory.CreateClient();
var response = await client.GetAsync($"/api/v1/scans/{scanId}/entrytrace");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<EntryTraceResponse>();
Assert.NotNull(payload);
Assert.Equal(storedResult.ScanId, payload!.ScanId);
Assert.Equal(storedResult.ImageDigest, payload.ImageDigest);
Assert.Equal(storedResult.GeneratedAtUtc, payload.GeneratedAt);
Assert.Equal(storedResult.Graph.Plans.Length, payload.Graph.Plans.Length);
Assert.Equal(storedResult.Ndjson, payload.Ndjson);
}
[Fact]
public async Task GetEntryTraceReturnsNotFoundWhenMissing()
{
using var factory = new ScannerApplicationFactory(
configuration: null,
services =>
{
services.AddSingleton<IEntryTraceResultStore>(new StubEntryTraceResultStore(null));
});
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/v1/scans/scan-missing/entrytrace");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private sealed record ProgressEnvelope(
string ScanId,
int Sequence,
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private sealed record ProgressEnvelope(
string ScanId,
int Sequence,
string State,
string? Message,
DateTimeOffset Timestamp,
string CorrelationId,
Dictionary<string, JsonElement> Data);
}
string CorrelationId,
Dictionary<string, JsonElement> Data);
private sealed class StubEntryTraceResultStore : IEntryTraceResultStore
{
private readonly EntryTraceResult? _result;
public StubEntryTraceResultStore(EntryTraceResult? result)
{
_result = result;
}
public Task<EntryTraceResult?> GetAsync(string scanId, CancellationToken cancellationToken)
{
if (_result is not null && string.Equals(_result.ScanId, scanId, StringComparison.Ordinal))
{
return Task.FromResult<EntryTraceResult?>(_result);
}
return Task.FromResult<EntryTraceResult?>(null);
}
public Task StoreAsync(EntryTraceResult result, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}