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

@@ -11,8 +11,8 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="..\..\src\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\..\Scanner\__Libraries\StellaOps.Scanner.Analyzers.Lang\StellaOps.Scanner.Analyzers.Lang.csproj" />
<ProjectReference Include="..\..\Scanner\__Libraries\StellaOps.Scanner.Core\StellaOps.Scanner.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -11,22 +11,75 @@ using StellaOps.Scanner.Analyzers.Lang;
using StellaOps.Scanner.Analyzers.Lang.Plugin;
using StellaOps.Scanner.Core.Security;
internal sealed record SmokeScenario(string Name, string[] UsageHintRelatives)
{
public IReadOnlyList<string> ResolveUsageHints(string scenarioRoot)
=> UsageHintRelatives.Select(relative => Path.GetFullPath(Path.Combine(scenarioRoot, relative))).ToArray();
}
internal sealed class SmokeOptions
{
public string RepoRoot { get; set; } = Directory.GetCurrentDirectory();
public string PluginDirectoryName { get; set; } = "StellaOps.Scanner.Analyzers.Lang.Python";
public string FixtureRelativePath { get; set; } = Path.Combine("src", "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "Fixtures", "lang", "python");
public static SmokeOptions Parse(string[] args)
{
var options = new SmokeOptions();
internal sealed record SmokeScenario(string Name, string[] UsageHintRelatives)
{
public IReadOnlyList<string> ResolveUsageHints(string scenarioRoot)
=> UsageHintRelatives.Select(relative => Path.GetFullPath(Path.Combine(scenarioRoot, relative))).ToArray();
}
internal sealed record AnalyzerProfile(
string DisplayName,
string AnalyzerId,
string PluginDirectory,
string FixtureRelativePath,
string ExpectedPluginId,
string ExpectedEntryPointType,
IReadOnlyList<string> RequiredCapabilities,
SmokeScenario[] Scenarios);
internal static class AnalyzerProfileCatalog
{
private static readonly SmokeScenario[] PythonScenarios =
{
new("simple-venv", new[] { Path.Combine("bin", "simple-tool") }),
new("pip-cache", new[] { Path.Combine("lib", "python3.11", "site-packages", "cache_pkg-1.2.3.data", "scripts", "cache-tool") }),
new("layered-editable", new[] { Path.Combine("layer1", "usr", "bin", "layered-cli") }),
};
private static readonly SmokeScenario[] RustScenarios =
{
new("simple", new[] { Path.Combine("usr", "local", "bin", "my_app") }),
new("heuristics", new[] { Path.Combine("usr", "local", "bin", "heuristic_app") }),
new("fallback", new[] { Path.Combine("usr", "local", "bin", "opaque_bin") }),
};
public static readonly IReadOnlyDictionary<string, AnalyzerProfile> Profiles =
new Dictionary<string, AnalyzerProfile>(StringComparer.OrdinalIgnoreCase)
{
["python"] = new AnalyzerProfile(
DisplayName: "Python",
AnalyzerId: "python",
PluginDirectory: "StellaOps.Scanner.Analyzers.Lang.Python",
FixtureRelativePath: Path.Combine("src", "Scanner", "__Tests", "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "Fixtures", "lang", "python"),
ExpectedPluginId: "stellaops.analyzer.lang.python",
ExpectedEntryPointType: "StellaOps.Scanner.Analyzers.Lang.Python.PythonAnalyzerPlugin",
RequiredCapabilities: new[] { "python" },
Scenarios: PythonScenarios),
["rust"] = new AnalyzerProfile(
DisplayName: "Rust",
AnalyzerId: "rust",
PluginDirectory: "StellaOps.Scanner.Analyzers.Lang.Rust",
FixtureRelativePath: Path.Combine("src", "Scanner", "__Tests", "StellaOps.Scanner.Analyzers.Lang.Tests", "Fixtures", "lang", "rust"),
ExpectedPluginId: "stellaops.analyzer.lang.rust",
ExpectedEntryPointType: "StellaOps.Scanner.Analyzers.Lang.Rust.RustAnalyzerPlugin",
RequiredCapabilities: new[] { "rust", "cargo" },
Scenarios: RustScenarios),
};
}
internal sealed class SmokeOptions
{
public string RepoRoot { get; set; } = Directory.GetCurrentDirectory();
public string AnalyzerId { get; set; } = "python";
public string PluginDirectoryName { get; set; } = "StellaOps.Scanner.Analyzers.Lang.Python";
public string FixtureRelativePath { get; set; } = Path.Combine("src", "Scanner", "__Tests", "StellaOps.Scanner.Analyzers.Lang.Python.Tests", "Fixtures", "lang", "python");
public bool PluginDirectoryExplicit { get; private set; }
public bool FixturePathExplicit { get; private set; }
public static SmokeOptions Parse(string[] args)
{
var options = new SmokeOptions();
for (var index = 0; index < args.Length; index++)
{
var current = args[index];
@@ -36,30 +89,52 @@ internal sealed class SmokeOptions
case "-r":
options.RepoRoot = RequireValue(args, ref index, current);
break;
case "--plugin-directory":
case "-p":
options.PluginDirectoryName = RequireValue(args, ref index, current);
break;
case "--fixture-path":
case "-f":
options.FixtureRelativePath = RequireValue(args, ref index, current);
break;
case "--help":
case "-h":
PrintUsage();
Environment.Exit(0);
case "--plugin-directory":
case "-p":
options.PluginDirectoryName = RequireValue(args, ref index, current);
options.PluginDirectoryExplicit = true;
break;
case "--fixture-path":
case "-f":
options.FixtureRelativePath = RequireValue(args, ref index, current);
options.FixturePathExplicit = true;
break;
case "--analyzer":
case "-a":
options.AnalyzerId = RequireValue(args, ref index, current);
break;
case "--help":
case "-h":
PrintUsage();
Environment.Exit(0);
break;
default:
throw new ArgumentException($"Unknown argument '{current}'. Use --help for usage.");
}
}
options.RepoRoot = Path.GetFullPath(options.RepoRoot);
return options;
}
private static string RequireValue(string[] args, ref int index, string switchName)
{
}
options.RepoRoot = Path.GetFullPath(options.RepoRoot);
if (!AnalyzerProfileCatalog.Profiles.TryGetValue(options.AnalyzerId, out var profile))
{
throw new ArgumentException($"Unsupported analyzer '{options.AnalyzerId}'.");
}
if (!options.PluginDirectoryExplicit)
{
options.PluginDirectoryName = profile.PluginDirectory;
}
if (!options.FixturePathExplicit)
{
options.FixtureRelativePath = profile.FixtureRelativePath;
}
return options;
}
private static string RequireValue(string[] args, ref int index, string switchName)
{
if (index + 1 >= args.Length)
{
throw new ArgumentException($"Missing value for '{switchName}'.");
@@ -76,16 +151,17 @@ internal sealed class SmokeOptions
}
private static void PrintUsage()
{
Console.WriteLine("Language Analyzer Smoke Harness");
Console.WriteLine("Usage: dotnet run --project src/Tools/LanguageAnalyzerSmoke -- [options]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -r, --repo-root <path> Repository root (defaults to current working directory)");
Console.WriteLine(" -p, --plugin-directory <name> Analyzer plug-in directory under plugins/scanner/analyzers/lang (defaults to StellaOps.Scanner.Analyzers.Lang.Python)");
Console.WriteLine(" -f, --fixture-path <path> Relative path to fixtures root (defaults to src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Fixtures/lang/python)");
Console.WriteLine(" -h, --help Show usage information");
}
{
Console.WriteLine("Language Analyzer Smoke Harness");
Console.WriteLine("Usage: dotnet run --project src/Tools/LanguageAnalyzerSmoke -- [options]");
Console.WriteLine();
Console.WriteLine("Options:");
Console.WriteLine(" -a, --analyzer <name> Analyzer to exercise (python, rust). Defaults to python.");
Console.WriteLine(" -r, --repo-root <path> Repository root (defaults to current working directory)");
Console.WriteLine(" -p, --plugin-directory <name> Analyzer plug-in directory under plugins/scanner/analyzers/lang (defaults to StellaOps.Scanner.Analyzers.Lang.Python)");
Console.WriteLine(" -f, --fixture-path <path> Relative path to fixtures root (defaults to src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Lang.Python.Tests/Fixtures/lang/python)");
Console.WriteLine(" -h, --help Show usage information");
}
}
internal sealed record PluginManifest
@@ -137,28 +213,33 @@ file static class Program
};
public static async Task<int> Main(string[] args)
{
try
{
var options = SmokeOptions.Parse(args);
await RunAsync(options).ConfigureAwait(false);
Console.WriteLine("✅ Python analyzer smoke checks passed");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"❌ {ex.Message}");
{
try
{
var options = SmokeOptions.Parse(args);
var profile = await RunAsync(options).ConfigureAwait(false);
Console.WriteLine($"✅ {profile.DisplayName} analyzer smoke checks passed");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"❌ {ex.Message}");
return 1;
}
}
private static async Task RunAsync(SmokeOptions options)
{
ValidateOptions(options);
var pluginRoot = Path.Combine(options.RepoRoot, "plugins", "scanner", "analyzers", "lang", options.PluginDirectoryName);
var manifestPath = Path.Combine(pluginRoot, "manifest.json");
if (!File.Exists(manifestPath))
}
}
private static async Task<AnalyzerProfile> RunAsync(SmokeOptions options)
{
if (!AnalyzerProfileCatalog.Profiles.TryGetValue(options.AnalyzerId, out var profile))
{
throw new ArgumentException($"Analyzer '{options.AnalyzerId}' is not supported.");
}
ValidateOptions(options);
var pluginRoot = Path.Combine(options.RepoRoot, "plugins", "scanner", "analyzers", "lang", options.PluginDirectoryName);
var manifestPath = Path.Combine(pluginRoot, "manifest.json");
if (!File.Exists(manifestPath))
{
throw new FileNotFoundException($"Plug-in manifest not found at '{manifestPath}'.", manifestPath);
}
@@ -166,16 +247,16 @@ file static class Program
using var manifestStream = File.OpenRead(manifestPath);
var manifest = JsonSerializer.Deserialize<PluginManifest>(manifestStream, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
}) ?? throw new InvalidOperationException($"Unable to parse manifest '{manifestPath}'.");
ValidateManifest(manifest, options.PluginDirectoryName);
var pluginAssemblyPath = Path.Combine(pluginRoot, manifest.EntryPoint.Assembly);
if (!File.Exists(pluginAssemblyPath))
{
throw new FileNotFoundException($"Plug-in assembly '{manifest.EntryPoint.Assembly}' not found under '{pluginRoot}'.", pluginAssemblyPath);
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
}) ?? throw new InvalidOperationException($"Unable to parse manifest '{manifestPath}'.");
ValidateManifest(manifest, profile, options.PluginDirectoryName);
var pluginAssemblyPath = Path.Combine(pluginRoot, manifest.EntryPoint.Assembly);
if (!File.Exists(pluginAssemblyPath))
{
throw new FileNotFoundException($"Plug-in assembly '{manifest.EntryPoint.Assembly}' not found under '{pluginRoot}'.", pluginAssemblyPath);
}
var sha256 = ComputeSha256(pluginAssemblyPath);
@@ -191,30 +272,32 @@ file static class Program
}
var analyzerSet = catalog.CreateAnalyzers(serviceProvider);
if (analyzerSet.Count == 0)
{
throw new InvalidOperationException("Language analyzer plug-ins reported no analyzers.");
}
var analyzerIds = analyzerSet.Select(analyzer => analyzer.Id).ToArray();
Console.WriteLine($"→ Loaded analyzers: {string.Join(", ", analyzerIds)}");
if (!analyzerIds.Contains("python", StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Python analyzer was not created by the plug-in.");
}
var fixtureRoot = Path.GetFullPath(Path.Combine(options.RepoRoot, options.FixtureRelativePath));
if (!Directory.Exists(fixtureRoot))
{
throw new DirectoryNotFoundException($"Fixture directory '{fixtureRoot}' does not exist.");
}
foreach (var scenario in PythonScenarios)
{
await RunScenarioAsync(scenario, fixtureRoot, catalog, serviceProvider).ConfigureAwait(false);
}
}
if (analyzerSet.Count == 0)
{
throw new InvalidOperationException("Language analyzer plug-ins reported no analyzers.");
}
var analyzerIds = analyzerSet.Select(analyzer => analyzer.Id).ToArray();
Console.WriteLine($"→ Loaded analyzers: {string.Join(", ", analyzerIds)}");
if (!analyzerIds.Contains(profile.AnalyzerId, StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"{profile.DisplayName} analyzer was not created by the plug-in.");
}
var fixtureRoot = Path.GetFullPath(Path.Combine(options.RepoRoot, options.FixtureRelativePath));
if (!Directory.Exists(fixtureRoot))
{
throw new DirectoryNotFoundException($"Fixture directory '{fixtureRoot}' does not exist.");
}
foreach (var scenario in profile.Scenarios)
{
await RunScenarioAsync(scenario, fixtureRoot, catalog, serviceProvider).ConfigureAwait(false);
}
return profile;
}
private static ServiceProvider BuildServiceProvider()
{
@@ -300,12 +383,12 @@ file static class Program
}
}
private static void ValidateManifest(PluginManifest manifest, string expectedDirectory)
{
if (!string.Equals(manifest.SchemaVersion, "1.0", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unexpected manifest schema version '{manifest.SchemaVersion}'.");
}
private static void ValidateManifest(PluginManifest manifest, AnalyzerProfile profile, string pluginDirectoryName)
{
if (!string.Equals(manifest.SchemaVersion, "1.0", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unexpected manifest schema version '{manifest.SchemaVersion}'.");
}
if (!manifest.RequiresRestart)
{
@@ -313,25 +396,28 @@ file static class Program
}
if (!string.Equals(manifest.EntryPoint.Type, "dotnet", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Unsupported entry point type '{manifest.EntryPoint.Type}'.");
}
if (!manifest.Capabilities.Contains("python", StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Manifest capabilities do not include 'python'.");
}
if (!string.Equals(manifest.EntryPoint.TypeName, "StellaOps.Scanner.Analyzers.Lang.Python.PythonAnalyzerPlugin", StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unexpected entry point type name '{manifest.EntryPoint.TypeName}'.");
}
if (!string.Equals(manifest.Id, "stellaops.analyzer.lang.python", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Manifest id '{manifest.Id}' does not match expected plug-in id for directory '{expectedDirectory}'.");
}
}
{
throw new InvalidOperationException($"Unsupported entry point type '{manifest.EntryPoint.Type}'.");
}
foreach (var capability in profile.RequiredCapabilities)
{
if (!manifest.Capabilities.Contains(capability, StringComparer.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Manifest capabilities do not include required capability '{capability}'.");
}
}
if (!string.Equals(manifest.EntryPoint.TypeName, profile.ExpectedEntryPointType, StringComparison.Ordinal))
{
throw new InvalidOperationException($"Unexpected entry point type name '{manifest.EntryPoint.TypeName}'.");
}
if (!string.Equals(manifest.Id, profile.ExpectedPluginId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Manifest id '{manifest.Id}' does not match expected plug-in id for directory '{pluginDirectoryName}'.");
}
}
private static string ComputeSha256(string path)
{