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

@@ -0,0 +1,321 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
namespace StellaOps.Scanner.EntryTrace.Runtime;
public sealed class EntryTraceRuntimeReconciler
{
private static readonly HashSet<string> WrapperNames = new(StringComparer.OrdinalIgnoreCase)
{
"tini",
"tini-static",
"tini64",
"dumb-init",
"dumbinit",
"gosu",
"su-exec",
"chpst",
"s6-supervise",
"s6-svscan",
"s6-svscanctl",
"s6-rc-init",
"runsv",
"runsvdir",
"supervisord",
"sh",
"bash",
"dash",
"ash",
"env"
};
public EntryTraceGraph Reconcile(EntryTraceGraph graph, ProcGraph? procGraph)
{
if (graph is null)
{
throw new ArgumentNullException(nameof(graph));
}
var diagnostics = graph.Diagnostics.ToBuilder();
diagnostics.RemoveAll(d =>
d.Reason is EntryTraceUnknownReason.RuntimeSnapshotUnavailable
or EntryTraceUnknownReason.RuntimeProcessNotFound
or EntryTraceUnknownReason.RuntimeMatch
or EntryTraceUnknownReason.RuntimeMismatch);
if (procGraph is null || procGraph.Processes.Count == 0)
{
diagnostics.Add(new EntryTraceDiagnostic(
EntryTraceDiagnosticSeverity.Info,
EntryTraceUnknownReason.RuntimeSnapshotUnavailable,
"Runtime process snapshot unavailable; static confidence retained.",
Span: null,
RelatedPath: null));
return graph with { Diagnostics = diagnostics.ToImmutable() };
}
var runtimeTerminals = BuildRuntimeTerminals(procGraph);
if (runtimeTerminals.Length == 0)
{
diagnostics.Add(new EntryTraceDiagnostic(
EntryTraceDiagnosticSeverity.Warning,
EntryTraceUnknownReason.RuntimeProcessNotFound,
"Runtime process snapshot did not reveal a terminal executable.",
Span: null,
RelatedPath: null));
return graph with { Diagnostics = diagnostics.ToImmutable() };
}
var planBuilder = ImmutableArray.CreateBuilder<EntryTracePlan>(graph.Plans.Length);
var terminalBuilder = ImmutableArray.CreateBuilder<EntryTraceTerminal>(graph.Terminals.Length);
var terminalIndexMap = BuildTerminalIndexMap(graph.Terminals, terminalBuilder);
foreach (var plan in graph.Plans)
{
var match = SelectBestRuntimeMatch(plan.TerminalPath, runtimeTerminals);
var confidence = EvaluateConfidence(plan.TerminalPath, match?.Path);
planBuilder.Add(plan with { Confidence = confidence.Score });
if (terminalIndexMap.TryGetValue(plan.TerminalPath, out var indices) && indices.Count > 0)
{
var index = indices.Dequeue();
terminalBuilder[index] = terminalBuilder[index] with { Confidence = confidence.Score };
}
diagnostics.Add(BuildDiagnostic(confidence, plan.TerminalPath));
}
// Update any terminals that were not tied to plans.
for (var i = 0; i < terminalBuilder.Count; i++)
{
if (terminalBuilder[i].Confidence > 0)
{
continue;
}
var terminal = terminalBuilder[i];
var match = SelectBestRuntimeMatch(terminal.Path, runtimeTerminals);
var confidence = EvaluateConfidence(terminal.Path, match?.Path);
terminalBuilder[i] = terminal with { Confidence = confidence.Score };
}
return graph with
{
Plans = planBuilder.ToImmutable(),
Terminals = terminalBuilder.ToImmutable(),
Diagnostics = diagnostics.ToImmutable()
};
}
private static ImmutableArray<RuntimeTerminal> BuildRuntimeTerminals(ProcGraph graph)
{
var allCandidates = new List<RuntimeTerminal>();
foreach (var process in graph.Processes.Values
.OrderBy(p => p.StartTimeTicks == 0 ? ulong.MaxValue : p.StartTimeTicks)
.ThenBy(p => p.Pid))
{
var normalizedPath = NormalizeRuntimePath(process);
if (string.IsNullOrWhiteSpace(normalizedPath))
{
continue;
}
allCandidates.Add(new RuntimeTerminal(normalizedPath, process));
}
if (allCandidates.Count == 0)
{
return ImmutableArray<RuntimeTerminal>.Empty;
}
var preferred = allCandidates.Where(candidate => !IsWrapper(candidate.Process)).ToList();
var source = preferred.Count > 0 ? preferred : allCandidates;
var unique = new Dictionary<string, RuntimeTerminal>(StringComparer.Ordinal);
foreach (var candidate in source)
{
if (!unique.ContainsKey(candidate.Path))
{
unique[candidate.Path] = candidate;
}
}
return unique.Values.OrderBy(candidate => candidate.Process.StartTimeTicks == 0 ? ulong.MaxValue : candidate.Process.StartTimeTicks)
.ThenBy(candidate => candidate.Process.Pid)
.ToImmutableArray();
}
private static RuntimeTerminal? SelectBestRuntimeMatch(string predictedPath, ImmutableArray<RuntimeTerminal> runtimeTerminals)
{
if (runtimeTerminals.IsDefaultOrEmpty)
{
return null;
}
RuntimeTerminal? best = null;
double bestScore = double.MinValue;
foreach (var runtime in runtimeTerminals)
{
var (score, _, _) = EvaluateConfidence(predictedPath, runtime.Path);
if (score > bestScore)
{
bestScore = score;
best = runtime;
if (score >= 95d)
{
break;
}
}
}
return best;
}
private static Dictionary<string, Queue<int>> BuildTerminalIndexMap(
ImmutableArray<EntryTraceTerminal> terminals,
ImmutableArray<EntryTraceTerminal>.Builder terminalBuilder)
{
var map = new Dictionary<string, Queue<int>>(StringComparer.Ordinal);
for (var i = 0; i < terminals.Length; i++)
{
var terminal = terminals[i];
terminalBuilder.Add(terminal);
if (!map.TryGetValue(terminal.Path, out var indices))
{
indices = new Queue<int>();
map[terminal.Path] = indices;
}
indices.Enqueue(i);
}
return map;
}
private static ConfidenceResult EvaluateConfidence(string predictedPath, string? runtimePath)
{
if (string.IsNullOrWhiteSpace(runtimePath))
{
return new ConfidenceResult(60d, ConfidenceLevel.Low, string.Empty);
}
var normalizedPredicted = NormalizeComparisonPath(predictedPath);
var normalizedRuntime = NormalizeComparisonPath(runtimePath);
if (string.Equals(normalizedPredicted, normalizedRuntime, StringComparison.Ordinal))
{
return new ConfidenceResult(95d, ConfidenceLevel.High, runtimePath);
}
var predictedName = Path.GetFileName(normalizedPredicted);
var runtimeName = Path.GetFileName(normalizedRuntime);
if (!string.IsNullOrEmpty(predictedName) &&
string.Equals(predictedName, runtimeName, StringComparison.Ordinal))
{
return new ConfidenceResult(90d, ConfidenceLevel.High, runtimePath);
}
if (!string.IsNullOrEmpty(predictedName) &&
string.Equals(predictedName, runtimeName, StringComparison.OrdinalIgnoreCase))
{
return new ConfidenceResult(80d, ConfidenceLevel.Medium, runtimePath);
}
return new ConfidenceResult(60d, ConfidenceLevel.Low, runtimePath);
}
private static EntryTraceDiagnostic BuildDiagnostic(ConfidenceResult result, string predictedPath)
{
var runtimePath = string.IsNullOrWhiteSpace(result.RuntimePath) ? "<unknown>" : result.RuntimePath;
var severity = result.Level == ConfidenceLevel.High
? EntryTraceDiagnosticSeverity.Info
: EntryTraceDiagnosticSeverity.Warning;
var reason = result.Level == ConfidenceLevel.High
? EntryTraceUnknownReason.RuntimeMatch
: EntryTraceUnknownReason.RuntimeMismatch;
var message = result.Level == ConfidenceLevel.High
? $"Runtime process '{runtimePath}' matches EntryTrace prediction '{predictedPath}'."
: $"Runtime process '{runtimePath}' diverges from EntryTrace prediction '{predictedPath}'.";
return new EntryTraceDiagnostic(
severity,
reason,
message,
Span: null,
RelatedPath: string.IsNullOrWhiteSpace(result.RuntimePath) ? null : result.RuntimePath);
}
private static bool IsWrapper(ProcProcess process)
{
var command = GetCommandName(process);
return command.Length > 0 && WrapperNames.Contains(command);
}
private static string GetCommandName(ProcProcess process)
{
if (!string.IsNullOrWhiteSpace(process.CommandName))
{
return process.CommandName;
}
if (!process.CommandLine.IsDefaultOrEmpty && process.CommandLine.Length > 0)
{
return Path.GetFileName(process.CommandLine[0]);
}
if (!string.IsNullOrWhiteSpace(process.ExecutablePath))
{
return Path.GetFileName(process.ExecutablePath);
}
return string.Empty;
}
private static string NormalizeRuntimePath(ProcProcess process)
{
if (!string.IsNullOrWhiteSpace(process.ExecutablePath))
{
return process.ExecutablePath;
}
if (!process.CommandLine.IsDefaultOrEmpty && process.CommandLine.Length > 0)
{
return process.CommandLine[0];
}
return process.CommandName;
}
private static string NormalizeComparisonPath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
var trimmed = path.Trim();
var normalized = trimmed.Replace('\\', '/');
return normalized;
}
private readonly record struct RuntimeTerminal(string Path, ProcProcess Process);
private readonly record struct ConfidenceResult(double Score, ConfidenceLevel Level, string RuntimePath);
private enum ConfidenceLevel
{
High,
Medium,
Low
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace StellaOps.Scanner.EntryTrace.Runtime;
public sealed class ProcFileSystemSnapshot : IProcSnapshotProvider
{
private readonly string _rootPath;
public ProcFileSystemSnapshot(string? rootPath = null)
{
_rootPath = string.IsNullOrWhiteSpace(rootPath) ? "/proc" : rootPath;
}
public IEnumerable<int> EnumerateProcessIds()
{
if (!Directory.Exists(_rootPath))
{
yield break;
}
foreach (var directory in Directory.EnumerateDirectories(_rootPath).OrderBy(d => d, StringComparer.Ordinal))
{
var name = Path.GetFileName(directory);
if (int.TryParse(name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pid))
{
yield return pid;
}
}
}
public bool TryReadProcess(int pid, out ProcProcess process)
{
process = null!;
try
{
var statPath = Path.Combine(_rootPath, pid.ToString(CultureInfo.InvariantCulture), "stat");
var cmdPath = Path.Combine(_rootPath, pid.ToString(CultureInfo.InvariantCulture), "cmdline");
var exePath = Path.Combine(_rootPath, pid.ToString(CultureInfo.InvariantCulture), "exe");
if (!File.Exists(statPath))
{
return false;
}
var statContent = File.ReadAllText(statPath);
var commandName = ParseCommandName(statContent);
var parentPid = ParseParentPid(statContent);
var startTime = ParseStartTime(statContent);
var commandLine = ReadCommandLine(cmdPath, statContent);
var executable = ResolveExecutablePath(exePath, commandLine);
process = new ProcProcess(pid, parentPid, executable, commandLine, commandName, startTime);
return true;
}
catch
{
return false;
}
}
private static string ParseCommandName(string statContent)
{
if (string.IsNullOrWhiteSpace(statContent))
{
return string.Empty;
}
var openIndex = statContent.IndexOf('(');
var closeIndex = statContent.LastIndexOf(')');
if (openIndex >= 0 && closeIndex > openIndex)
{
return statContent.Substring(openIndex + 1, closeIndex - openIndex - 1);
}
return string.Empty;
}
private static int ParseParentPid(string statContent)
{
if (string.IsNullOrWhiteSpace(statContent))
{
return 0;
}
var closeIndex = statContent.LastIndexOf(')');
if (closeIndex < 0 || closeIndex + 2 >= statContent.Length)
{
return 0;
}
var after = statContent.Substring(closeIndex + 2);
var parts = after.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
return 0;
}
return int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var parentPid)
? parentPid
: 0;
}
private static ulong ParseStartTime(string statContent)
{
if (string.IsNullOrWhiteSpace(statContent))
{
return 0;
}
var closeIndex = statContent.LastIndexOf(')');
if (closeIndex < 0 || closeIndex + 2 >= statContent.Length)
{
return 0;
}
var after = statContent.Substring(closeIndex + 2);
var parts = after.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 19)
{
return 0;
}
return ulong.TryParse(parts[18], NumberStyles.Integer, CultureInfo.InvariantCulture, out var startTime)
? startTime
: 0;
}
private static ImmutableArray<string> ReadCommandLine(string path, string statContent)
{
try
{
if (File.Exists(path))
{
var bytes = File.ReadAllBytes(path);
if (bytes.Length > 0)
{
var segments = SplitNullTerminated(bytes)
.Where(segment => segment.Length > 0)
.ToImmutableArray();
if (!segments.IsDefaultOrEmpty)
{
return segments;
}
}
}
}
catch
{
// ignore
}
var openIndex = statContent.IndexOf('(');
var closeIndex = statContent.LastIndexOf(')');
if (openIndex >= 0 && closeIndex > openIndex)
{
var command = statContent.Substring(openIndex + 1, closeIndex - openIndex - 1);
return ImmutableArray.Create(command);
}
return ImmutableArray<string>.Empty;
}
private static string ResolveExecutablePath(string path, ImmutableArray<string> commandLine)
{
try
{
if (File.Exists(path))
{
try
{
var info = File.ResolveLinkTarget(path, returnFinalTarget: true);
if (info is not null)
{
return info.FullName;
}
}
catch
{
// fall through to command line fallback
}
var fullPath = Path.GetFullPath(path);
if (!string.IsNullOrWhiteSpace(fullPath))
{
return fullPath;
}
}
}
catch
{
// ignore failures to resolve symlinks
}
if (!commandLine.IsDefaultOrEmpty && commandLine[0].Contains('/'))
{
return commandLine[0];
}
return string.Empty;
}
private static IEnumerable<string> SplitNullTerminated(byte[] buffer)
{
var start = 0;
for (var i = 0; i < buffer.Length; i++)
{
if (buffer[i] == 0)
{
var length = i - start;
if (length > 0)
{
yield return Encoding.UTF8.GetString(buffer, start, length);
}
start = i + 1;
}
}
if (start < buffer.Length)
{
yield return Encoding.UTF8.GetString(buffer, start, buffer.Length - start);
}
}
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace.Runtime;
public sealed record ProcGraph(
int RootPid,
ImmutableDictionary<int, ProcProcess> Processes,
ImmutableDictionary<int, ImmutableArray<int>> Children);

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace StellaOps.Scanner.EntryTrace.Runtime;
public interface IProcSnapshotProvider
{
IEnumerable<int> EnumerateProcessIds();
bool TryReadProcess(int pid, out ProcProcess process);
}
public static class ProcGraphBuilder
{
public static ProcGraph? Build(IProcSnapshotProvider provider)
{
if (provider is null)
{
throw new ArgumentNullException(nameof(provider));
}
var processes = new Dictionary<int, ProcProcess>();
foreach (var pid in provider.EnumerateProcessIds().OrderBy(pid => pid))
{
if (pid <= 0)
{
continue;
}
if (provider.TryReadProcess(pid, out var process))
{
processes[pid] = process;
}
}
if (processes.Count == 0)
{
return null;
}
var rootPid = DetermineRootPid(processes);
var children = BuildChildren(processes);
return new ProcGraph(
rootPid,
processes.ToImmutableDictionary(pair => pair.Key, pair => pair.Value),
children);
}
private static int DetermineRootPid(Dictionary<int, ProcProcess> processes)
{
if (processes.ContainsKey(1))
{
return 1;
}
var childPids = new HashSet<int>();
foreach (var process in processes.Values)
{
if (processes.ContainsKey(process.ParentPid))
{
childPids.Add(process.Pid);
}
}
var rootCandidates = processes.Keys.Where(pid => !childPids.Contains(pid));
return rootCandidates.OrderBy(pid => pid).FirstOrDefault(processes.Keys.Min());
}
private static ImmutableDictionary<int, ImmutableArray<int>> BuildChildren(Dictionary<int, ProcProcess> processes)
{
var map = new Dictionary<int, List<int>>(processes.Count);
foreach (var process in processes.Values)
{
if (!processes.ContainsKey(process.ParentPid))
{
continue;
}
if (!map.TryGetValue(process.ParentPid, out var list))
{
list = new List<int>();
map[process.ParentPid] = list;
}
list.Add(process.Pid);
}
return map.ToImmutableDictionary(
pair => pair.Key,
pair => pair.Value
.OrderBy(pid => NormalizeStartTime(processes[pid]))
.ThenBy(pid => pid)
.Select(pid => pid)
.ToImmutableArray());
}
private static ulong NormalizeStartTime(ProcProcess process)
{
return process.StartTimeTicks == 0 ? ulong.MaxValue : process.StartTimeTicks;
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Immutable;
namespace StellaOps.Scanner.EntryTrace.Runtime;
public sealed record ProcProcess(
int Pid,
int ParentPid,
string ExecutablePath,
ImmutableArray<string> CommandLine,
string CommandName,
ulong StartTimeTicks);