up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-13 00:20:26 +02:00
parent e1f1bef4c1
commit 564df71bfb
2376 changed files with 334389 additions and 328032 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,38 @@
using System;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Cli.Configuration;
internal static class AuthorityTokenUtilities
{
public static string ResolveScope(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var scope = options.Authority?.Scope;
return string.IsNullOrWhiteSpace(scope)
? StellaOpsScopes.ConcelierJobsTrigger
: scope.Trim();
}
public static string BuildCacheKey(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.Authority is null)
{
return string.Empty;
}
var scope = ResolveScope(options);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
var cacheKey = $"{options.Authority.Url}|{credential}|{scope}";
using System;
using System.Security.Cryptography;
using System.Text;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Cli.Configuration;
internal static class AuthorityTokenUtilities
{
public static string ResolveScope(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var scope = options.Authority?.Scope;
return string.IsNullOrWhiteSpace(scope)
? StellaOpsScopes.ConcelierJobsTrigger
: scope.Trim();
}
public static string BuildCacheKey(StellaOpsCliOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.Authority is null)
{
return string.Empty;
}
var scope = ResolveScope(options);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
var cacheKey = $"{options.Authority.Url}|{credential}|{scope}";
if (!string.IsNullOrWhiteSpace(scope))
{
if (scope.Contains("orch:operate", StringComparison.OrdinalIgnoreCase))
@@ -59,10 +59,10 @@ internal static class AuthorityTokenUtilities
{
return "none";
}
var trimmed = value.Trim();
var bytes = Encoding.UTF8.GetBytes(trimmed);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}
var trimmed = value.Trim();
var bytes = Encoding.UTF8.GetBytes(trimmed);
var hash = SHA256.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}

View File

@@ -1,118 +1,118 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Configuration;
using StellaOps.Configuration;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Cli.Configuration;
public static class CliBootstrapper
{
public static (StellaOpsCliOptions Options, IConfigurationRoot Configuration) Build(string[] args)
{
var bootstrap = StellaOpsConfigurationBootstrapper.Build<StellaOpsCliOptions>(options =>
{
options.BindingSection = "StellaOps";
options.ConfigureBuilder = builder =>
{
if (args.Length > 0)
{
builder.AddCommandLine(args);
}
};
options.PostBind = (cliOptions, configuration) =>
{
cliOptions.ApiKey = ResolveWithFallback(cliOptions.ApiKey, configuration, "API_KEY", "StellaOps:ApiKey", "ApiKey");
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Configuration;
using StellaOps.Configuration;
using StellaOps.Auth.Abstractions;
namespace StellaOps.Cli.Configuration;
public static class CliBootstrapper
{
public static (StellaOpsCliOptions Options, IConfigurationRoot Configuration) Build(string[] args)
{
var bootstrap = StellaOpsConfigurationBootstrapper.Build<StellaOpsCliOptions>(options =>
{
options.BindingSection = "StellaOps";
options.ConfigureBuilder = builder =>
{
if (args.Length > 0)
{
builder.AddCommandLine(args);
}
};
options.PostBind = (cliOptions, configuration) =>
{
cliOptions.ApiKey = ResolveWithFallback(cliOptions.ApiKey, configuration, "API_KEY", "StellaOps:ApiKey", "ApiKey");
cliOptions.BackendUrl = ResolveWithFallback(cliOptions.BackendUrl, configuration, "STELLAOPS_BACKEND_URL", "StellaOps:BackendUrl", "BackendUrl");
cliOptions.ConcelierUrl = ResolveWithFallback(cliOptions.ConcelierUrl, configuration, "STELLAOPS_CONCELIER_URL", "StellaOps:ConcelierUrl", "ConcelierUrl");
cliOptions.AdvisoryAiUrl = ResolveWithFallback(cliOptions.AdvisoryAiUrl, configuration, "STELLAOPS_ADVISORYAI_URL", "StellaOps:AdvisoryAiUrl", "AdvisoryAiUrl");
cliOptions.ScannerSignaturePublicKeyPath = ResolveWithFallback(cliOptions.ScannerSignaturePublicKeyPath, configuration, "SCANNER_PUBLIC_KEY", "STELLAOPS_SCANNER_PUBLIC_KEY", "StellaOps:ScannerSignaturePublicKeyPath", "ScannerSignaturePublicKeyPath");
cliOptions.ApiKey = cliOptions.ApiKey?.Trim() ?? string.Empty;
cliOptions.BackendUrl = cliOptions.BackendUrl?.Trim() ?? string.Empty;
cliOptions.ScannerSignaturePublicKeyPath = ResolveWithFallback(cliOptions.ScannerSignaturePublicKeyPath, configuration, "SCANNER_PUBLIC_KEY", "STELLAOPS_SCANNER_PUBLIC_KEY", "StellaOps:ScannerSignaturePublicKeyPath", "ScannerSignaturePublicKeyPath");
cliOptions.ApiKey = cliOptions.ApiKey?.Trim() ?? string.Empty;
cliOptions.BackendUrl = cliOptions.BackendUrl?.Trim() ?? string.Empty;
cliOptions.ConcelierUrl = cliOptions.ConcelierUrl?.Trim() ?? string.Empty;
cliOptions.AdvisoryAiUrl = cliOptions.AdvisoryAiUrl?.Trim() ?? string.Empty;
cliOptions.ScannerSignaturePublicKeyPath = cliOptions.ScannerSignaturePublicKeyPath?.Trim() ?? string.Empty;
var attemptsRaw = ResolveWithFallback(
string.Empty,
configuration,
"SCANNER_DOWNLOAD_ATTEMPTS",
"STELLAOPS_SCANNER_DOWNLOAD_ATTEMPTS",
"StellaOps:ScannerDownloadAttempts",
"ScannerDownloadAttempts");
if (string.IsNullOrWhiteSpace(attemptsRaw))
{
attemptsRaw = cliOptions.ScannerDownloadAttempts.ToString(CultureInfo.InvariantCulture);
}
if (int.TryParse(attemptsRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedAttempts) && parsedAttempts > 0)
{
cliOptions.ScannerDownloadAttempts = parsedAttempts;
}
if (cliOptions.ScannerDownloadAttempts <= 0)
{
cliOptions.ScannerDownloadAttempts = 3;
}
cliOptions.Authority ??= new StellaOpsCliAuthorityOptions();
var authority = cliOptions.Authority;
authority.Url = ResolveWithFallback(
authority.Url,
configuration,
"STELLAOPS_AUTHORITY_URL",
"StellaOps:Authority:Url",
"Authority:Url",
"Authority:Issuer");
authority.ClientId = ResolveWithFallback(
authority.ClientId,
configuration,
"STELLAOPS_AUTHORITY_CLIENT_ID",
"StellaOps:Authority:ClientId",
"Authority:ClientId");
authority.ClientSecret = ResolveWithFallback(
authority.ClientSecret ?? string.Empty,
configuration,
"STELLAOPS_AUTHORITY_CLIENT_SECRET",
"StellaOps:Authority:ClientSecret",
"Authority:ClientSecret");
authority.Username = ResolveWithFallback(
authority.Username,
configuration,
"STELLAOPS_AUTHORITY_USERNAME",
"StellaOps:Authority:Username",
"Authority:Username");
authority.Password = ResolveWithFallback(
authority.Password ?? string.Empty,
configuration,
"STELLAOPS_AUTHORITY_PASSWORD",
"StellaOps:Authority:Password",
"Authority:Password");
authority.Scope = ResolveWithFallback(
authority.Scope,
configuration,
"STELLAOPS_AUTHORITY_SCOPE",
"StellaOps:Authority:Scope",
"Authority:Scope");
authority.OperatorReason = ResolveWithFallback(
authority.OperatorReason,
configuration,
"STELLAOPS_ORCH_REASON",
"StellaOps:Authority:OperatorReason",
"Authority:OperatorReason");
cliOptions.ScannerSignaturePublicKeyPath = cliOptions.ScannerSignaturePublicKeyPath?.Trim() ?? string.Empty;
var attemptsRaw = ResolveWithFallback(
string.Empty,
configuration,
"SCANNER_DOWNLOAD_ATTEMPTS",
"STELLAOPS_SCANNER_DOWNLOAD_ATTEMPTS",
"StellaOps:ScannerDownloadAttempts",
"ScannerDownloadAttempts");
if (string.IsNullOrWhiteSpace(attemptsRaw))
{
attemptsRaw = cliOptions.ScannerDownloadAttempts.ToString(CultureInfo.InvariantCulture);
}
if (int.TryParse(attemptsRaw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedAttempts) && parsedAttempts > 0)
{
cliOptions.ScannerDownloadAttempts = parsedAttempts;
}
if (cliOptions.ScannerDownloadAttempts <= 0)
{
cliOptions.ScannerDownloadAttempts = 3;
}
cliOptions.Authority ??= new StellaOpsCliAuthorityOptions();
var authority = cliOptions.Authority;
authority.Url = ResolveWithFallback(
authority.Url,
configuration,
"STELLAOPS_AUTHORITY_URL",
"StellaOps:Authority:Url",
"Authority:Url",
"Authority:Issuer");
authority.ClientId = ResolveWithFallback(
authority.ClientId,
configuration,
"STELLAOPS_AUTHORITY_CLIENT_ID",
"StellaOps:Authority:ClientId",
"Authority:ClientId");
authority.ClientSecret = ResolveWithFallback(
authority.ClientSecret ?? string.Empty,
configuration,
"STELLAOPS_AUTHORITY_CLIENT_SECRET",
"StellaOps:Authority:ClientSecret",
"Authority:ClientSecret");
authority.Username = ResolveWithFallback(
authority.Username,
configuration,
"STELLAOPS_AUTHORITY_USERNAME",
"StellaOps:Authority:Username",
"Authority:Username");
authority.Password = ResolveWithFallback(
authority.Password ?? string.Empty,
configuration,
"STELLAOPS_AUTHORITY_PASSWORD",
"StellaOps:Authority:Password",
"Authority:Password");
authority.Scope = ResolveWithFallback(
authority.Scope,
configuration,
"STELLAOPS_AUTHORITY_SCOPE",
"StellaOps:Authority:Scope",
"Authority:Scope");
authority.OperatorReason = ResolveWithFallback(
authority.OperatorReason,
configuration,
"STELLAOPS_ORCH_REASON",
"StellaOps:Authority:OperatorReason",
"Authority:OperatorReason");
authority.OperatorTicket = ResolveWithFallback(
authority.OperatorTicket,
configuration,
@@ -139,298 +139,298 @@ public static class CliBootstrapper
configuration,
"STELLAOPS_AUTHORITY_TOKEN_CACHE_DIR",
"StellaOps:Authority:TokenCacheDirectory",
"Authority:TokenCacheDirectory");
authority.Url = authority.Url?.Trim() ?? string.Empty;
authority.ClientId = authority.ClientId?.Trim() ?? string.Empty;
authority.ClientSecret = string.IsNullOrWhiteSpace(authority.ClientSecret) ? null : authority.ClientSecret.Trim();
authority.Username = authority.Username?.Trim() ?? string.Empty;
"Authority:TokenCacheDirectory");
authority.Url = authority.Url?.Trim() ?? string.Empty;
authority.ClientId = authority.ClientId?.Trim() ?? string.Empty;
authority.ClientSecret = string.IsNullOrWhiteSpace(authority.ClientSecret) ? null : authority.ClientSecret.Trim();
authority.Username = authority.Username?.Trim() ?? string.Empty;
authority.Password = string.IsNullOrWhiteSpace(authority.Password) ? null : authority.Password.Trim();
authority.Scope = string.IsNullOrWhiteSpace(authority.Scope) ? StellaOpsScopes.ConcelierJobsTrigger : authority.Scope.Trim();
authority.OperatorReason = authority.OperatorReason?.Trim() ?? string.Empty;
authority.OperatorTicket = authority.OperatorTicket?.Trim() ?? string.Empty;
authority.BackfillReason = authority.BackfillReason?.Trim() ?? string.Empty;
authority.BackfillTicket = authority.BackfillTicket?.Trim() ?? string.Empty;
authority.Resilience ??= new StellaOpsCliAuthorityResilienceOptions();
authority.Resilience.RetryDelays ??= new List<TimeSpan>();
var resilience = authority.Resilience;
if (!resilience.EnableRetries.HasValue)
{
var raw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_ENABLE_RETRIES",
"StellaOps:Authority:Resilience:EnableRetries",
"StellaOps:Authority:EnableRetries",
"Authority:Resilience:EnableRetries",
"Authority:EnableRetries");
if (TryParseBoolean(raw, out var parsed))
{
resilience.EnableRetries = parsed;
}
}
var retryDelaysRaw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_RETRY_DELAYS",
"StellaOps:Authority:Resilience:RetryDelays",
"StellaOps:Authority:RetryDelays",
"Authority:Resilience:RetryDelays",
"Authority:RetryDelays");
if (!string.IsNullOrWhiteSpace(retryDelaysRaw))
{
resilience.RetryDelays.Clear();
foreach (var delay in ParseRetryDelays(retryDelaysRaw))
{
if (delay > TimeSpan.Zero)
{
resilience.RetryDelays.Add(delay);
}
}
}
if (!resilience.AllowOfflineCacheFallback.HasValue)
{
var raw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_ALLOW_OFFLINE_CACHE_FALLBACK",
"StellaOps:Authority:Resilience:AllowOfflineCacheFallback",
"StellaOps:Authority:AllowOfflineCacheFallback",
"Authority:Resilience:AllowOfflineCacheFallback",
"Authority:AllowOfflineCacheFallback");
if (TryParseBoolean(raw, out var parsed))
{
resilience.AllowOfflineCacheFallback = parsed;
}
}
if (!resilience.OfflineCacheTolerance.HasValue)
{
var raw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_OFFLINE_CACHE_TOLERANCE",
"StellaOps:Authority:Resilience:OfflineCacheTolerance",
"StellaOps:Authority:OfflineCacheTolerance",
"Authority:Resilience:OfflineCacheTolerance",
"Authority:OfflineCacheTolerance");
if (TimeSpan.TryParse(raw, CultureInfo.InvariantCulture, out var tolerance) && tolerance >= TimeSpan.Zero)
{
resilience.OfflineCacheTolerance = tolerance;
}
}
var defaultTokenCache = GetDefaultTokenCacheDirectory();
if (string.IsNullOrWhiteSpace(authority.TokenCacheDirectory))
{
authority.TokenCacheDirectory = defaultTokenCache;
}
else
{
authority.TokenCacheDirectory = Path.GetFullPath(authority.TokenCacheDirectory);
}
cliOptions.Offline ??= new StellaOpsCliOfflineOptions();
var offline = cliOptions.Offline;
var kitsDirectory = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_OFFLINE_KITS_DIRECTORY",
"STELLAOPS_OFFLINE_KITS_DIR",
"StellaOps:Offline:KitsDirectory",
"StellaOps:Offline:KitDirectory",
"Offline:KitsDirectory",
"Offline:KitDirectory");
if (string.IsNullOrWhiteSpace(kitsDirectory))
{
kitsDirectory = offline.KitsDirectory ?? "offline-kits";
}
offline.KitsDirectory = Path.GetFullPath(kitsDirectory);
if (!Directory.Exists(offline.KitsDirectory))
{
Directory.CreateDirectory(offline.KitsDirectory);
}
var mirror = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_OFFLINE_MIRROR_URL",
"StellaOps:Offline:KitMirror",
"Offline:KitMirror",
"Offline:MirrorUrl");
offline.MirrorUrl = string.IsNullOrWhiteSpace(mirror) ? null : mirror.Trim();
cliOptions.Plugins ??= new StellaOpsCliPluginOptions();
var pluginOptions = cliOptions.Plugins;
pluginOptions.BaseDirectory = ResolveWithFallback(
pluginOptions.BaseDirectory,
configuration,
"STELLAOPS_CLI_PLUGIN_BASE_DIRECTORY",
"StellaOps:Plugins:BaseDirectory",
"Plugins:BaseDirectory");
pluginOptions.BaseDirectory = (pluginOptions.BaseDirectory ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.BaseDirectory))
{
pluginOptions.BaseDirectory = AppContext.BaseDirectory;
}
pluginOptions.BaseDirectory = Path.GetFullPath(pluginOptions.BaseDirectory);
pluginOptions.Directory = ResolveWithFallback(
pluginOptions.Directory,
configuration,
"STELLAOPS_CLI_PLUGIN_DIRECTORY",
"StellaOps:Plugins:Directory",
"Plugins:Directory");
pluginOptions.Directory = (pluginOptions.Directory ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.Directory))
{
pluginOptions.Directory = Path.Combine("plugins", "cli");
}
if (!Path.IsPathRooted(pluginOptions.Directory))
{
pluginOptions.Directory = Path.GetFullPath(Path.Combine(pluginOptions.BaseDirectory, pluginOptions.Directory));
}
else
{
pluginOptions.Directory = Path.GetFullPath(pluginOptions.Directory);
}
pluginOptions.ManifestSearchPattern = ResolveWithFallback(
pluginOptions.ManifestSearchPattern,
configuration,
"STELLAOPS_CLI_PLUGIN_MANIFEST_PATTERN",
"StellaOps:Plugins:ManifestSearchPattern",
"Plugins:ManifestSearchPattern");
pluginOptions.ManifestSearchPattern = (pluginOptions.ManifestSearchPattern ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern))
{
pluginOptions.ManifestSearchPattern = "*.manifest.json";
}
if (pluginOptions.SearchPatterns is null || pluginOptions.SearchPatterns.Count == 0)
{
pluginOptions.SearchPatterns = new List<string> { "StellaOps.Cli.Plugin.*.dll" };
}
else
{
pluginOptions.SearchPatterns = pluginOptions.SearchPatterns
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.Select(pattern => pattern.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (pluginOptions.SearchPatterns.Count == 0)
{
pluginOptions.SearchPatterns.Add("StellaOps.Cli.Plugin.*.dll");
}
}
if (pluginOptions.PluginOrder is null)
{
pluginOptions.PluginOrder = new List<string>();
}
else
{
pluginOptions.PluginOrder = pluginOptions.PluginOrder
.Where(name => !string.IsNullOrWhiteSpace(name))
.Select(name => name.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
};
});
return (bootstrap.Options, bootstrap.Configuration);
}
private static string ResolveWithFallback(string currentValue, IConfiguration configuration, params string[] keys)
{
if (!string.IsNullOrWhiteSpace(currentValue))
{
return currentValue;
}
foreach (var key in keys)
{
var value = configuration[key];
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return string.Empty;
}
private static bool TryParseBoolean(string value, out bool parsed)
{
if (string.IsNullOrWhiteSpace(value))
{
parsed = default;
return false;
}
if (bool.TryParse(value, out parsed))
{
return true;
}
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric))
{
parsed = numeric != 0;
return true;
}
parsed = default;
return false;
}
private static IEnumerable<TimeSpan> ParseRetryDelays(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
yield break;
}
var separators = new[] { ',', ';', ' ' };
foreach (var token in raw.Split(separators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (TimeSpan.TryParse(token, CultureInfo.InvariantCulture, out var delay) && delay > TimeSpan.Zero)
{
yield return delay;
}
}
}
private static string GetDefaultTokenCacheDirectory()
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrWhiteSpace(home))
{
home = AppContext.BaseDirectory;
}
return Path.GetFullPath(Path.Combine(home, ".stellaops", "tokens"));
}
}
authority.Resilience ??= new StellaOpsCliAuthorityResilienceOptions();
authority.Resilience.RetryDelays ??= new List<TimeSpan>();
var resilience = authority.Resilience;
if (!resilience.EnableRetries.HasValue)
{
var raw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_ENABLE_RETRIES",
"StellaOps:Authority:Resilience:EnableRetries",
"StellaOps:Authority:EnableRetries",
"Authority:Resilience:EnableRetries",
"Authority:EnableRetries");
if (TryParseBoolean(raw, out var parsed))
{
resilience.EnableRetries = parsed;
}
}
var retryDelaysRaw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_RETRY_DELAYS",
"StellaOps:Authority:Resilience:RetryDelays",
"StellaOps:Authority:RetryDelays",
"Authority:Resilience:RetryDelays",
"Authority:RetryDelays");
if (!string.IsNullOrWhiteSpace(retryDelaysRaw))
{
resilience.RetryDelays.Clear();
foreach (var delay in ParseRetryDelays(retryDelaysRaw))
{
if (delay > TimeSpan.Zero)
{
resilience.RetryDelays.Add(delay);
}
}
}
if (!resilience.AllowOfflineCacheFallback.HasValue)
{
var raw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_ALLOW_OFFLINE_CACHE_FALLBACK",
"StellaOps:Authority:Resilience:AllowOfflineCacheFallback",
"StellaOps:Authority:AllowOfflineCacheFallback",
"Authority:Resilience:AllowOfflineCacheFallback",
"Authority:AllowOfflineCacheFallback");
if (TryParseBoolean(raw, out var parsed))
{
resilience.AllowOfflineCacheFallback = parsed;
}
}
if (!resilience.OfflineCacheTolerance.HasValue)
{
var raw = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_AUTHORITY_OFFLINE_CACHE_TOLERANCE",
"StellaOps:Authority:Resilience:OfflineCacheTolerance",
"StellaOps:Authority:OfflineCacheTolerance",
"Authority:Resilience:OfflineCacheTolerance",
"Authority:OfflineCacheTolerance");
if (TimeSpan.TryParse(raw, CultureInfo.InvariantCulture, out var tolerance) && tolerance >= TimeSpan.Zero)
{
resilience.OfflineCacheTolerance = tolerance;
}
}
var defaultTokenCache = GetDefaultTokenCacheDirectory();
if (string.IsNullOrWhiteSpace(authority.TokenCacheDirectory))
{
authority.TokenCacheDirectory = defaultTokenCache;
}
else
{
authority.TokenCacheDirectory = Path.GetFullPath(authority.TokenCacheDirectory);
}
cliOptions.Offline ??= new StellaOpsCliOfflineOptions();
var offline = cliOptions.Offline;
var kitsDirectory = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_OFFLINE_KITS_DIRECTORY",
"STELLAOPS_OFFLINE_KITS_DIR",
"StellaOps:Offline:KitsDirectory",
"StellaOps:Offline:KitDirectory",
"Offline:KitsDirectory",
"Offline:KitDirectory");
if (string.IsNullOrWhiteSpace(kitsDirectory))
{
kitsDirectory = offline.KitsDirectory ?? "offline-kits";
}
offline.KitsDirectory = Path.GetFullPath(kitsDirectory);
if (!Directory.Exists(offline.KitsDirectory))
{
Directory.CreateDirectory(offline.KitsDirectory);
}
var mirror = ResolveWithFallback(
string.Empty,
configuration,
"STELLAOPS_OFFLINE_MIRROR_URL",
"StellaOps:Offline:KitMirror",
"Offline:KitMirror",
"Offline:MirrorUrl");
offline.MirrorUrl = string.IsNullOrWhiteSpace(mirror) ? null : mirror.Trim();
cliOptions.Plugins ??= new StellaOpsCliPluginOptions();
var pluginOptions = cliOptions.Plugins;
pluginOptions.BaseDirectory = ResolveWithFallback(
pluginOptions.BaseDirectory,
configuration,
"STELLAOPS_CLI_PLUGIN_BASE_DIRECTORY",
"StellaOps:Plugins:BaseDirectory",
"Plugins:BaseDirectory");
pluginOptions.BaseDirectory = (pluginOptions.BaseDirectory ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.BaseDirectory))
{
pluginOptions.BaseDirectory = AppContext.BaseDirectory;
}
pluginOptions.BaseDirectory = Path.GetFullPath(pluginOptions.BaseDirectory);
pluginOptions.Directory = ResolveWithFallback(
pluginOptions.Directory,
configuration,
"STELLAOPS_CLI_PLUGIN_DIRECTORY",
"StellaOps:Plugins:Directory",
"Plugins:Directory");
pluginOptions.Directory = (pluginOptions.Directory ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.Directory))
{
pluginOptions.Directory = Path.Combine("plugins", "cli");
}
if (!Path.IsPathRooted(pluginOptions.Directory))
{
pluginOptions.Directory = Path.GetFullPath(Path.Combine(pluginOptions.BaseDirectory, pluginOptions.Directory));
}
else
{
pluginOptions.Directory = Path.GetFullPath(pluginOptions.Directory);
}
pluginOptions.ManifestSearchPattern = ResolveWithFallback(
pluginOptions.ManifestSearchPattern,
configuration,
"STELLAOPS_CLI_PLUGIN_MANIFEST_PATTERN",
"StellaOps:Plugins:ManifestSearchPattern",
"Plugins:ManifestSearchPattern");
pluginOptions.ManifestSearchPattern = (pluginOptions.ManifestSearchPattern ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern))
{
pluginOptions.ManifestSearchPattern = "*.manifest.json";
}
if (pluginOptions.SearchPatterns is null || pluginOptions.SearchPatterns.Count == 0)
{
pluginOptions.SearchPatterns = new List<string> { "StellaOps.Cli.Plugin.*.dll" };
}
else
{
pluginOptions.SearchPatterns = pluginOptions.SearchPatterns
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.Select(pattern => pattern.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (pluginOptions.SearchPatterns.Count == 0)
{
pluginOptions.SearchPatterns.Add("StellaOps.Cli.Plugin.*.dll");
}
}
if (pluginOptions.PluginOrder is null)
{
pluginOptions.PluginOrder = new List<string>();
}
else
{
pluginOptions.PluginOrder = pluginOptions.PluginOrder
.Where(name => !string.IsNullOrWhiteSpace(name))
.Select(name => name.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
};
});
return (bootstrap.Options, bootstrap.Configuration);
}
private static string ResolveWithFallback(string currentValue, IConfiguration configuration, params string[] keys)
{
if (!string.IsNullOrWhiteSpace(currentValue))
{
return currentValue;
}
foreach (var key in keys)
{
var value = configuration[key];
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
return string.Empty;
}
private static bool TryParseBoolean(string value, out bool parsed)
{
if (string.IsNullOrWhiteSpace(value))
{
parsed = default;
return false;
}
if (bool.TryParse(value, out parsed))
{
return true;
}
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric))
{
parsed = numeric != 0;
return true;
}
parsed = default;
return false;
}
private static IEnumerable<TimeSpan> ParseRetryDelays(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
{
yield break;
}
var separators = new[] { ',', ';', ' ' };
foreach (var token in raw.Split(separators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (TimeSpan.TryParse(token, CultureInfo.InvariantCulture, out var delay) && delay > TimeSpan.Zero)
{
yield return delay;
}
}
}
private static string GetDefaultTokenCacheDirectory()
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrWhiteSpace(home))
{
home = AppContext.BaseDirectory;
}
return Path.GetFullPath(Path.Combine(home, ".stellaops", "tokens"));
}
}

View File

@@ -1,106 +1,106 @@
using System;
using System.Collections.Generic;
using System.IO;
using StellaOps.Auth.Abstractions;
using StellaOps.Configuration;
namespace StellaOps.Cli.Configuration;
public sealed class StellaOpsCliOptions
{
public string ApiKey { get; set; } = string.Empty;
public string BackendUrl { get; set; } = string.Empty;
public string ConcelierUrl { get; set; } = string.Empty;
public string AdvisoryAiUrl { get; set; } = string.Empty;
public string ScannerCacheDirectory { get; set; } = "scanners";
public string ResultsDirectory { get; set; } = "results";
public string DefaultRunner { get; set; } = "docker";
public string ScannerSignaturePublicKeyPath { get; set; } = string.Empty;
public int ScannerDownloadAttempts { get; set; } = 3;
public int ScanUploadAttempts { get; set; } = 3;
public StellaOpsCliAuthorityOptions Authority { get; set; } = new();
public StellaOpsCliOfflineOptions Offline { get; set; } = new();
public StellaOpsCliPluginOptions Plugins { get; set; } = new();
public StellaOpsCryptoOptions Crypto { get; set; } = new();
/// <summary>
/// Indicates if CLI is running in offline mode.
/// </summary>
public bool IsOffline { get; set; }
/// <summary>
/// Directory containing offline kits when in offline mode.
/// </summary>
public string? OfflineKitDirectory { get; set; }
}
public sealed class StellaOpsCliAuthorityOptions
{
public string Url { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string? ClientSecret { get; set; }
public string Username { get; set; } = string.Empty;
public string? Password { get; set; }
public string Scope { get; set; } = StellaOpsScopes.ConcelierJobsTrigger;
public string OperatorReason { get; set; } = string.Empty;
public string OperatorTicket { get; set; } = string.Empty;
public string BackfillReason { get; set; } = string.Empty;
public string BackfillTicket { get; set; } = string.Empty;
public string TokenCacheDirectory { get; set; } = string.Empty;
public StellaOpsCliAuthorityResilienceOptions Resilience { get; set; } = new();
}
public sealed class StellaOpsCliAuthorityResilienceOptions
{
public bool? EnableRetries { get; set; }
public IList<TimeSpan> RetryDelays { get; set; } = new List<TimeSpan>();
public bool? AllowOfflineCacheFallback { get; set; }
public TimeSpan? OfflineCacheTolerance { get; set; }
}
public sealed class StellaOpsCliOfflineOptions
{
public string KitsDirectory { get; set; } = "offline-kits";
public string? MirrorUrl { get; set; }
}
public sealed class StellaOpsCliPluginOptions
{
public string BaseDirectory { get; set; } = string.Empty;
public string Directory { get; set; } = "plugins/cli";
public IList<string> SearchPatterns { get; set; } = new List<string>();
public IList<string> PluginOrder { get; set; } = new List<string>();
public string ManifestSearchPattern { get; set; } = "*.manifest.json";
}
using System;
using System.Collections.Generic;
using System.IO;
using StellaOps.Auth.Abstractions;
using StellaOps.Configuration;
namespace StellaOps.Cli.Configuration;
public sealed class StellaOpsCliOptions
{
public string ApiKey { get; set; } = string.Empty;
public string BackendUrl { get; set; } = string.Empty;
public string ConcelierUrl { get; set; } = string.Empty;
public string AdvisoryAiUrl { get; set; } = string.Empty;
public string ScannerCacheDirectory { get; set; } = "scanners";
public string ResultsDirectory { get; set; } = "results";
public string DefaultRunner { get; set; } = "docker";
public string ScannerSignaturePublicKeyPath { get; set; } = string.Empty;
public int ScannerDownloadAttempts { get; set; } = 3;
public int ScanUploadAttempts { get; set; } = 3;
public StellaOpsCliAuthorityOptions Authority { get; set; } = new();
public StellaOpsCliOfflineOptions Offline { get; set; } = new();
public StellaOpsCliPluginOptions Plugins { get; set; } = new();
public StellaOpsCryptoOptions Crypto { get; set; } = new();
/// <summary>
/// Indicates if CLI is running in offline mode.
/// </summary>
public bool IsOffline { get; set; }
/// <summary>
/// Directory containing offline kits when in offline mode.
/// </summary>
public string? OfflineKitDirectory { get; set; }
}
public sealed class StellaOpsCliAuthorityOptions
{
public string Url { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
public string? ClientSecret { get; set; }
public string Username { get; set; } = string.Empty;
public string? Password { get; set; }
public string Scope { get; set; } = StellaOpsScopes.ConcelierJobsTrigger;
public string OperatorReason { get; set; } = string.Empty;
public string OperatorTicket { get; set; } = string.Empty;
public string BackfillReason { get; set; } = string.Empty;
public string BackfillTicket { get; set; } = string.Empty;
public string TokenCacheDirectory { get; set; } = string.Empty;
public StellaOpsCliAuthorityResilienceOptions Resilience { get; set; } = new();
}
public sealed class StellaOpsCliAuthorityResilienceOptions
{
public bool? EnableRetries { get; set; }
public IList<TimeSpan> RetryDelays { get; set; } = new List<TimeSpan>();
public bool? AllowOfflineCacheFallback { get; set; }
public TimeSpan? OfflineCacheTolerance { get; set; }
}
public sealed class StellaOpsCliOfflineOptions
{
public string KitsDirectory { get; set; } = "offline-kits";
public string? MirrorUrl { get; set; }
}
public sealed class StellaOpsCliPluginOptions
{
public string BaseDirectory { get; set; } = string.Empty;
public string Directory { get; set; } = "plugins/cli";
public IList<string> SearchPatterns { get; set; } = new List<string>();
public IList<string> PluginOrder { get; set; } = new List<string>();
public string ManifestSearchPattern { get; set; } = "*.manifest.json";
}

View File

@@ -1,278 +1,278 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Configuration;
using StellaOps.Plugin.Hosting;
namespace StellaOps.Cli.Plugins;
internal sealed class CliCommandModuleLoader
{
private readonly IServiceProvider _services;
private readonly StellaOpsCliOptions _options;
private readonly ILogger<CliCommandModuleLoader> _logger;
private readonly RestartOnlyCliPluginGuard _guard = new();
private IReadOnlyList<ICliCommandModule> _modules = Array.Empty<ICliCommandModule>();
private bool _loaded;
public CliCommandModuleLoader(
IServiceProvider services,
StellaOpsCliOptions options,
ILogger<CliCommandModuleLoader> logger)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public IReadOnlyList<ICliCommandModule> LoadModules()
{
if (_loaded)
{
return _modules;
}
var pluginOptions = _options.Plugins ?? new StellaOpsCliPluginOptions();
var baseDirectory = ResolveBaseDirectory(pluginOptions);
var pluginsDirectory = ResolvePluginsDirectory(pluginOptions, baseDirectory);
var searchPatterns = ResolveSearchPatterns(pluginOptions);
var manifestPattern = string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern)
? "*.manifest.json"
: pluginOptions.ManifestSearchPattern;
_logger.LogDebug("Loading CLI plug-ins from '{Directory}' (base: '{Base}').", pluginsDirectory, baseDirectory);
var manifestLoader = new CliPluginManifestLoader(pluginsDirectory, manifestPattern);
IReadOnlyList<CliPluginManifest> manifests;
try
{
manifests = manifestLoader.LoadAsync(CancellationToken.None).GetAwaiter().GetResult();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to enumerate CLI plug-in manifests from '{Directory}'.", pluginsDirectory);
manifests = Array.Empty<CliPluginManifest>();
}
if (manifests.Count == 0)
{
_logger.LogInformation("No CLI plug-in manifests discovered under '{Directory}'.", pluginsDirectory);
_loaded = true;
_guard.Seal();
_modules = Array.Empty<ICliCommandModule>();
return _modules;
}
var hostOptions = new PluginHostOptions
{
BaseDirectory = baseDirectory,
PluginsDirectory = pluginsDirectory,
EnsureDirectoryExists = false,
RecursiveSearch = true,
PrimaryPrefix = "StellaOps.Cli"
};
foreach (var pattern in searchPatterns)
{
hostOptions.SearchPatterns.Add(pattern);
}
foreach (var ordered in pluginOptions.PluginOrder ?? Array.Empty<string>())
{
if (!string.IsNullOrWhiteSpace(ordered))
{
hostOptions.PluginOrder.Add(ordered);
}
}
var loadResult = PluginHost.LoadPlugins(hostOptions, _logger);
var assemblies = loadResult.Plugins.ToDictionary(
descriptor => Normalize(descriptor.AssemblyPath),
descriptor => descriptor.Assembly,
StringComparer.OrdinalIgnoreCase);
var modules = new List<ICliCommandModule>(manifests.Count);
foreach (var manifest in manifests)
{
try
{
var assemblyPath = ResolveAssemblyPath(manifest);
_guard.EnsureRegistrationAllowed(assemblyPath);
if (!assemblies.TryGetValue(assemblyPath, out var assembly))
{
if (!File.Exists(assemblyPath))
{
throw new FileNotFoundException($"Plug-in assembly '{assemblyPath}' referenced by manifest '{manifest.Id}' was not found.");
}
assembly = Assembly.LoadFrom(assemblyPath);
assemblies[assemblyPath] = assembly;
}
var module = CreateModule(assembly, manifest);
if (module is null)
{
continue;
}
modules.Add(module);
_logger.LogInformation("Registered CLI plug-in '{PluginId}' ({PluginName}) from '{AssemblyPath}'.", manifest.Id, module.Name, assemblyPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register CLI plug-in '{PluginId}'.", manifest.Id);
}
}
_modules = modules;
_loaded = true;
_guard.Seal();
return _modules;
}
public void RegisterModules(RootCommand root, Option<bool> verboseOption, CancellationToken cancellationToken)
{
if (root is null)
{
throw new ArgumentNullException(nameof(root));
}
if (verboseOption is null)
{
throw new ArgumentNullException(nameof(verboseOption));
}
var modules = LoadModules();
if (modules.Count == 0)
{
return;
}
foreach (var module in modules)
{
if (!module.IsAvailable(_services))
{
_logger.LogDebug("CLI plug-in '{Name}' reported unavailable; skipping registration.", module.Name);
continue;
}
try
{
module.RegisterCommands(root, _services, _options, verboseOption, cancellationToken);
_logger.LogInformation("CLI plug-in '{Name}' commands registered.", module.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "CLI plug-in '{Name}' failed to register commands.", module.Name);
}
}
}
private static string ResolveAssemblyPath(CliPluginManifest manifest)
{
if (manifest.EntryPoint is null)
{
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' does not define an entry point.");
}
var assemblyPath = manifest.EntryPoint.Assembly;
if (string.IsNullOrWhiteSpace(assemblyPath))
{
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' specifies an empty assembly path.");
}
if (!Path.IsPathRooted(assemblyPath))
{
if (string.IsNullOrWhiteSpace(manifest.SourceDirectory))
{
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' cannot resolve relative assembly path without source directory metadata.");
}
assemblyPath = Path.Combine(manifest.SourceDirectory, assemblyPath);
}
return Normalize(assemblyPath);
}
private ICliCommandModule? CreateModule(Assembly assembly, CliPluginManifest manifest)
{
if (manifest.EntryPoint is null)
{
return null;
}
var type = assembly.GetType(manifest.EntryPoint.TypeName, throwOnError: true);
if (type is null)
{
throw new InvalidOperationException($"Plug-in type '{manifest.EntryPoint.TypeName}' could not be loaded from assembly '{assembly.FullName}'.");
}
var module = ActivatorUtilities.CreateInstance(_services, type) as ICliCommandModule;
if (module is null)
{
throw new InvalidOperationException($"Plug-in type '{manifest.EntryPoint.TypeName}' does not implement {nameof(ICliCommandModule)}.");
}
return module;
}
private static string ResolveBaseDirectory(StellaOpsCliPluginOptions options)
{
var baseDirectory = options.BaseDirectory;
if (string.IsNullOrWhiteSpace(baseDirectory))
{
baseDirectory = AppContext.BaseDirectory;
}
return Path.GetFullPath(baseDirectory);
}
private static string ResolvePluginsDirectory(StellaOpsCliPluginOptions options, string baseDirectory)
{
var directory = options.Directory;
if (string.IsNullOrWhiteSpace(directory))
{
directory = Path.Combine("plugins", "cli");
}
directory = directory.Trim();
if (!Path.IsPathRooted(directory))
{
directory = Path.Combine(baseDirectory, directory);
}
return Path.GetFullPath(directory);
}
private static IReadOnlyList<string> ResolveSearchPatterns(StellaOpsCliPluginOptions options)
{
if (options.SearchPatterns is null || options.SearchPatterns.Count == 0)
{
return new[] { "StellaOps.Cli.Plugin.*.dll" };
}
return options.SearchPatterns
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.Select(pattern => pattern.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string Normalize(string path)
{
var full = Path.GetFullPath(path);
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Cli.Configuration;
using StellaOps.Plugin.Hosting;
namespace StellaOps.Cli.Plugins;
internal sealed class CliCommandModuleLoader
{
private readonly IServiceProvider _services;
private readonly StellaOpsCliOptions _options;
private readonly ILogger<CliCommandModuleLoader> _logger;
private readonly RestartOnlyCliPluginGuard _guard = new();
private IReadOnlyList<ICliCommandModule> _modules = Array.Empty<ICliCommandModule>();
private bool _loaded;
public CliCommandModuleLoader(
IServiceProvider services,
StellaOpsCliOptions options,
ILogger<CliCommandModuleLoader> logger)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public IReadOnlyList<ICliCommandModule> LoadModules()
{
if (_loaded)
{
return _modules;
}
var pluginOptions = _options.Plugins ?? new StellaOpsCliPluginOptions();
var baseDirectory = ResolveBaseDirectory(pluginOptions);
var pluginsDirectory = ResolvePluginsDirectory(pluginOptions, baseDirectory);
var searchPatterns = ResolveSearchPatterns(pluginOptions);
var manifestPattern = string.IsNullOrWhiteSpace(pluginOptions.ManifestSearchPattern)
? "*.manifest.json"
: pluginOptions.ManifestSearchPattern;
_logger.LogDebug("Loading CLI plug-ins from '{Directory}' (base: '{Base}').", pluginsDirectory, baseDirectory);
var manifestLoader = new CliPluginManifestLoader(pluginsDirectory, manifestPattern);
IReadOnlyList<CliPluginManifest> manifests;
try
{
manifests = manifestLoader.LoadAsync(CancellationToken.None).GetAwaiter().GetResult();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to enumerate CLI plug-in manifests from '{Directory}'.", pluginsDirectory);
manifests = Array.Empty<CliPluginManifest>();
}
if (manifests.Count == 0)
{
_logger.LogInformation("No CLI plug-in manifests discovered under '{Directory}'.", pluginsDirectory);
_loaded = true;
_guard.Seal();
_modules = Array.Empty<ICliCommandModule>();
return _modules;
}
var hostOptions = new PluginHostOptions
{
BaseDirectory = baseDirectory,
PluginsDirectory = pluginsDirectory,
EnsureDirectoryExists = false,
RecursiveSearch = true,
PrimaryPrefix = "StellaOps.Cli"
};
foreach (var pattern in searchPatterns)
{
hostOptions.SearchPatterns.Add(pattern);
}
foreach (var ordered in pluginOptions.PluginOrder ?? Array.Empty<string>())
{
if (!string.IsNullOrWhiteSpace(ordered))
{
hostOptions.PluginOrder.Add(ordered);
}
}
var loadResult = PluginHost.LoadPlugins(hostOptions, _logger);
var assemblies = loadResult.Plugins.ToDictionary(
descriptor => Normalize(descriptor.AssemblyPath),
descriptor => descriptor.Assembly,
StringComparer.OrdinalIgnoreCase);
var modules = new List<ICliCommandModule>(manifests.Count);
foreach (var manifest in manifests)
{
try
{
var assemblyPath = ResolveAssemblyPath(manifest);
_guard.EnsureRegistrationAllowed(assemblyPath);
if (!assemblies.TryGetValue(assemblyPath, out var assembly))
{
if (!File.Exists(assemblyPath))
{
throw new FileNotFoundException($"Plug-in assembly '{assemblyPath}' referenced by manifest '{manifest.Id}' was not found.");
}
assembly = Assembly.LoadFrom(assemblyPath);
assemblies[assemblyPath] = assembly;
}
var module = CreateModule(assembly, manifest);
if (module is null)
{
continue;
}
modules.Add(module);
_logger.LogInformation("Registered CLI plug-in '{PluginId}' ({PluginName}) from '{AssemblyPath}'.", manifest.Id, module.Name, assemblyPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register CLI plug-in '{PluginId}'.", manifest.Id);
}
}
_modules = modules;
_loaded = true;
_guard.Seal();
return _modules;
}
public void RegisterModules(RootCommand root, Option<bool> verboseOption, CancellationToken cancellationToken)
{
if (root is null)
{
throw new ArgumentNullException(nameof(root));
}
if (verboseOption is null)
{
throw new ArgumentNullException(nameof(verboseOption));
}
var modules = LoadModules();
if (modules.Count == 0)
{
return;
}
foreach (var module in modules)
{
if (!module.IsAvailable(_services))
{
_logger.LogDebug("CLI plug-in '{Name}' reported unavailable; skipping registration.", module.Name);
continue;
}
try
{
module.RegisterCommands(root, _services, _options, verboseOption, cancellationToken);
_logger.LogInformation("CLI plug-in '{Name}' commands registered.", module.Name);
}
catch (Exception ex)
{
_logger.LogError(ex, "CLI plug-in '{Name}' failed to register commands.", module.Name);
}
}
}
private static string ResolveAssemblyPath(CliPluginManifest manifest)
{
if (manifest.EntryPoint is null)
{
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' does not define an entry point.");
}
var assemblyPath = manifest.EntryPoint.Assembly;
if (string.IsNullOrWhiteSpace(assemblyPath))
{
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' specifies an empty assembly path.");
}
if (!Path.IsPathRooted(assemblyPath))
{
if (string.IsNullOrWhiteSpace(manifest.SourceDirectory))
{
throw new InvalidOperationException($"Manifest '{manifest.SourcePath}' cannot resolve relative assembly path without source directory metadata.");
}
assemblyPath = Path.Combine(manifest.SourceDirectory, assemblyPath);
}
return Normalize(assemblyPath);
}
private ICliCommandModule? CreateModule(Assembly assembly, CliPluginManifest manifest)
{
if (manifest.EntryPoint is null)
{
return null;
}
var type = assembly.GetType(manifest.EntryPoint.TypeName, throwOnError: true);
if (type is null)
{
throw new InvalidOperationException($"Plug-in type '{manifest.EntryPoint.TypeName}' could not be loaded from assembly '{assembly.FullName}'.");
}
var module = ActivatorUtilities.CreateInstance(_services, type) as ICliCommandModule;
if (module is null)
{
throw new InvalidOperationException($"Plug-in type '{manifest.EntryPoint.TypeName}' does not implement {nameof(ICliCommandModule)}.");
}
return module;
}
private static string ResolveBaseDirectory(StellaOpsCliPluginOptions options)
{
var baseDirectory = options.BaseDirectory;
if (string.IsNullOrWhiteSpace(baseDirectory))
{
baseDirectory = AppContext.BaseDirectory;
}
return Path.GetFullPath(baseDirectory);
}
private static string ResolvePluginsDirectory(StellaOpsCliPluginOptions options, string baseDirectory)
{
var directory = options.Directory;
if (string.IsNullOrWhiteSpace(directory))
{
directory = Path.Combine("plugins", "cli");
}
directory = directory.Trim();
if (!Path.IsPathRooted(directory))
{
directory = Path.Combine(baseDirectory, directory);
}
return Path.GetFullPath(directory);
}
private static IReadOnlyList<string> ResolveSearchPatterns(StellaOpsCliPluginOptions options)
{
if (options.SearchPatterns is null || options.SearchPatterns.Count == 0)
{
return new[] { "StellaOps.Cli.Plugin.*.dll" };
}
return options.SearchPatterns
.Where(pattern => !string.IsNullOrWhiteSpace(pattern))
.Select(pattern => pattern.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string Normalize(string path)
{
var full = Path.GetFullPath(path);
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}

View File

@@ -1,39 +1,39 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Plugins;
public sealed record CliPluginManifest
{
public const string CurrentSchemaVersion = "1.0";
public string SchemaVersion { get; init; } = CurrentSchemaVersion;
public string Id { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public string Version { get; init; } = "0.0.0";
public bool RequiresRestart { get; init; } = true;
public CliPluginEntryPoint? EntryPoint { get; init; }
public IReadOnlyList<string> Capabilities { get; init; } = Array.Empty<string>();
public IReadOnlyDictionary<string, string> Metadata { get; init; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string? SourcePath { get; init; }
public string? SourceDirectory { get; init; }
}
public sealed record CliPluginEntryPoint
{
public string Type { get; init; } = "dotnet";
public string Assembly { get; init; } = string.Empty;
public string TypeName { get; init; } = string.Empty;
}
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Plugins;
public sealed record CliPluginManifest
{
public const string CurrentSchemaVersion = "1.0";
public string SchemaVersion { get; init; } = CurrentSchemaVersion;
public string Id { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public string Version { get; init; } = "0.0.0";
public bool RequiresRestart { get; init; } = true;
public CliPluginEntryPoint? EntryPoint { get; init; }
public IReadOnlyList<string> Capabilities { get; init; } = Array.Empty<string>();
public IReadOnlyDictionary<string, string> Metadata { get; init; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string? SourcePath { get; init; }
public string? SourceDirectory { get; init; }
}
public sealed record CliPluginEntryPoint
{
public string Type { get; init; } = "dotnet";
public string Assembly { get; init; } = string.Empty;
public string TypeName { get; init; } = string.Empty;
}

View File

@@ -1,150 +1,150 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Plugins;
internal sealed class CliPluginManifestLoader
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true
};
private readonly string _directory;
private readonly string _searchPattern;
public CliPluginManifestLoader(string directory, string searchPattern)
{
if (string.IsNullOrWhiteSpace(directory))
{
throw new ArgumentException("Plug-in manifest directory is required.", nameof(directory));
}
if (string.IsNullOrWhiteSpace(searchPattern))
{
throw new ArgumentException("Manifest search pattern is required.", nameof(searchPattern));
}
_directory = Path.GetFullPath(directory);
_searchPattern = searchPattern;
}
public async Task<IReadOnlyList<CliPluginManifest>> LoadAsync(CancellationToken cancellationToken)
{
if (!Directory.Exists(_directory))
{
return Array.Empty<CliPluginManifest>();
}
var manifests = new List<CliPluginManifest>();
foreach (var file in Directory.EnumerateFiles(_directory, _searchPattern, SearchOption.AllDirectories))
{
if (IsHidden(file))
{
continue;
}
var manifest = await DeserializeAsync(file, cancellationToken).ConfigureAwait(false);
manifests.Add(manifest);
}
return manifests
.OrderBy(static m => m.Id, StringComparer.OrdinalIgnoreCase)
.ThenBy(static m => m.Version, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static bool IsHidden(string path)
{
var directory = Path.GetDirectoryName(path);
while (!string.IsNullOrEmpty(directory))
{
var name = Path.GetFileName(directory);
if (name.StartsWith(".", StringComparison.Ordinal))
{
return true;
}
directory = Path.GetDirectoryName(directory);
}
return false;
}
private static async Task<CliPluginManifest> DeserializeAsync(string file, CancellationToken cancellationToken)
{
await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
CliPluginManifest? manifest;
try
{
manifest = await JsonSerializer.DeserializeAsync<CliPluginManifest>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
}
catch (JsonException ex)
{
throw new InvalidOperationException($"Failed to parse CLI plug-in manifest '{file}'.", ex);
}
if (manifest is null)
{
throw new InvalidOperationException($"CLI plug-in manifest '{file}' is empty or invalid.");
}
ValidateManifest(manifest, file);
var directory = Path.GetDirectoryName(file);
return manifest with
{
SourcePath = file,
SourceDirectory = directory
};
}
private static void ValidateManifest(CliPluginManifest manifest, string file)
{
if (!string.Equals(manifest.SchemaVersion, CliPluginManifest.CurrentSchemaVersion, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Manifest '{file}' uses unsupported schema version '{manifest.SchemaVersion}'. Expected '{CliPluginManifest.CurrentSchemaVersion}'.");
}
if (string.IsNullOrWhiteSpace(manifest.Id))
{
throw new InvalidOperationException($"Manifest '{file}' must specify a non-empty 'id'.");
}
if (manifest.EntryPoint is null)
{
throw new InvalidOperationException($"Manifest '{file}' must specify an 'entryPoint'.");
}
if (!string.Equals(manifest.EntryPoint.Type, "dotnet", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Manifest '{file}' entry point type '{manifest.EntryPoint.Type}' is not supported. Expected 'dotnet'.");
}
if (string.IsNullOrWhiteSpace(manifest.EntryPoint.Assembly))
{
throw new InvalidOperationException($"Manifest '{file}' must specify an entry point assembly.");
}
if (string.IsNullOrWhiteSpace(manifest.EntryPoint.TypeName))
{
throw new InvalidOperationException($"Manifest '{file}' must specify an entry point type.");
}
if (!manifest.RequiresRestart)
{
throw new InvalidOperationException($"Manifest '{file}' must set 'requiresRestart' to true.");
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Plugins;
internal sealed class CliPluginManifestLoader
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true
};
private readonly string _directory;
private readonly string _searchPattern;
public CliPluginManifestLoader(string directory, string searchPattern)
{
if (string.IsNullOrWhiteSpace(directory))
{
throw new ArgumentException("Plug-in manifest directory is required.", nameof(directory));
}
if (string.IsNullOrWhiteSpace(searchPattern))
{
throw new ArgumentException("Manifest search pattern is required.", nameof(searchPattern));
}
_directory = Path.GetFullPath(directory);
_searchPattern = searchPattern;
}
public async Task<IReadOnlyList<CliPluginManifest>> LoadAsync(CancellationToken cancellationToken)
{
if (!Directory.Exists(_directory))
{
return Array.Empty<CliPluginManifest>();
}
var manifests = new List<CliPluginManifest>();
foreach (var file in Directory.EnumerateFiles(_directory, _searchPattern, SearchOption.AllDirectories))
{
if (IsHidden(file))
{
continue;
}
var manifest = await DeserializeAsync(file, cancellationToken).ConfigureAwait(false);
manifests.Add(manifest);
}
return manifests
.OrderBy(static m => m.Id, StringComparer.OrdinalIgnoreCase)
.ThenBy(static m => m.Version, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static bool IsHidden(string path)
{
var directory = Path.GetDirectoryName(path);
while (!string.IsNullOrEmpty(directory))
{
var name = Path.GetFileName(directory);
if (name.StartsWith(".", StringComparison.Ordinal))
{
return true;
}
directory = Path.GetDirectoryName(directory);
}
return false;
}
private static async Task<CliPluginManifest> DeserializeAsync(string file, CancellationToken cancellationToken)
{
await using var stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous);
CliPluginManifest? manifest;
try
{
manifest = await JsonSerializer.DeserializeAsync<CliPluginManifest>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
}
catch (JsonException ex)
{
throw new InvalidOperationException($"Failed to parse CLI plug-in manifest '{file}'.", ex);
}
if (manifest is null)
{
throw new InvalidOperationException($"CLI plug-in manifest '{file}' is empty or invalid.");
}
ValidateManifest(manifest, file);
var directory = Path.GetDirectoryName(file);
return manifest with
{
SourcePath = file,
SourceDirectory = directory
};
}
private static void ValidateManifest(CliPluginManifest manifest, string file)
{
if (!string.Equals(manifest.SchemaVersion, CliPluginManifest.CurrentSchemaVersion, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Manifest '{file}' uses unsupported schema version '{manifest.SchemaVersion}'. Expected '{CliPluginManifest.CurrentSchemaVersion}'.");
}
if (string.IsNullOrWhiteSpace(manifest.Id))
{
throw new InvalidOperationException($"Manifest '{file}' must specify a non-empty 'id'.");
}
if (manifest.EntryPoint is null)
{
throw new InvalidOperationException($"Manifest '{file}' must specify an 'entryPoint'.");
}
if (!string.Equals(manifest.EntryPoint.Type, "dotnet", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Manifest '{file}' entry point type '{manifest.EntryPoint.Type}' is not supported. Expected 'dotnet'.");
}
if (string.IsNullOrWhiteSpace(manifest.EntryPoint.Assembly))
{
throw new InvalidOperationException($"Manifest '{file}' must specify an entry point assembly.");
}
if (string.IsNullOrWhiteSpace(manifest.EntryPoint.TypeName))
{
throw new InvalidOperationException($"Manifest '{file}' must specify an entry point type.");
}
if (!manifest.RequiresRestart)
{
throw new InvalidOperationException($"Manifest '{file}' must set 'requiresRestart' to true.");
}
}
}

View File

@@ -1,20 +1,20 @@
using System;
using System.CommandLine;
using System.Threading;
using StellaOps.Cli.Configuration;
namespace StellaOps.Cli.Plugins;
public interface ICliCommandModule
{
string Name { get; }
bool IsAvailable(IServiceProvider services);
void RegisterCommands(
RootCommand root,
IServiceProvider services,
StellaOpsCliOptions options,
Option<bool> verboseOption,
CancellationToken cancellationToken);
}
using System;
using System.CommandLine;
using System.Threading;
using StellaOps.Cli.Configuration;
namespace StellaOps.Cli.Plugins;
public interface ICliCommandModule
{
string Name { get; }
bool IsAvailable(IServiceProvider services);
void RegisterCommands(
RootCommand root,
IServiceProvider services,
StellaOpsCliOptions options,
Option<bool> verboseOption,
CancellationToken cancellationToken);
}

View File

@@ -1,41 +1,41 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace StellaOps.Cli.Plugins;
internal sealed class RestartOnlyCliPluginGuard
{
private readonly ConcurrentDictionary<string, byte> _plugins = new(StringComparer.OrdinalIgnoreCase);
private bool _sealed;
public IReadOnlyCollection<string> KnownPlugins => _plugins.Keys.ToArray();
public bool IsSealed => Volatile.Read(ref _sealed);
public void EnsureRegistrationAllowed(string pluginPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginPath);
var normalized = Normalize(pluginPath);
if (IsSealed && !_plugins.ContainsKey(normalized))
{
throw new InvalidOperationException($"Plug-in '{pluginPath}' cannot be registered after startup. Restart required.");
}
_plugins.TryAdd(normalized, 0);
}
public void Seal()
{
Volatile.Write(ref _sealed, true);
}
private static string Normalize(string path)
{
var full = Path.GetFullPath(path);
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
namespace StellaOps.Cli.Plugins;
internal sealed class RestartOnlyCliPluginGuard
{
private readonly ConcurrentDictionary<string, byte> _plugins = new(StringComparer.OrdinalIgnoreCase);
private bool _sealed;
public IReadOnlyCollection<string> KnownPlugins => _plugins.Keys.ToArray();
public bool IsSealed => Volatile.Read(ref _sealed);
public void EnsureRegistrationAllowed(string pluginPath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pluginPath);
var normalized = Normalize(pluginPath);
if (IsSealed && !_plugins.ContainsKey(normalized))
{
throw new InvalidOperationException($"Plug-in '{pluginPath}' cannot be registered after startup. Restart required.");
}
_plugins.TryAdd(normalized, 0);
}
public void Seal()
{
Volatile.Write(ref _sealed, true);
}
private static string Normalize(string path)
{
var full = Path.GetFullPath(path);
return full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}

View File

@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Cli.Tests")]
[assembly: InternalsVisibleTo("StellaOps.Cli.Plugins.NonCore")]

View File

@@ -1,223 +1,223 @@
using System;
using System.Buffers.Text;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal sealed class AuthorityRevocationClient : IAuthorityRevocationClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);
private readonly HttpClient httpClient;
private readonly StellaOpsCliOptions options;
private readonly ILogger<AuthorityRevocationClient> logger;
private readonly IStellaOpsTokenClient? tokenClient;
private readonly object tokenSync = new();
private string? cachedAccessToken;
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
public AuthorityRevocationClient(
HttpClient httpClient,
StellaOpsCliOptions options,
ILogger<AuthorityRevocationClient> logger,
IStellaOpsTokenClient? tokenClient = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.tokenClient = tokenClient;
if (!string.IsNullOrWhiteSpace(options.Authority?.Url) && httpClient.BaseAddress is null && Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var authorityUri))
{
httpClient.BaseAddress = authorityUri;
}
}
public async Task<AuthorityRevocationExportResult> ExportAsync(bool verbose, CancellationToken cancellationToken)
{
EnsureAuthorityConfigured();
using var request = new HttpRequestMessage(HttpMethod.Get, "internal/revocations/export");
var accessToken = await AcquireAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var message = $"Authority export request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}";
throw new InvalidOperationException(message);
}
var payload = await JsonSerializer.DeserializeAsync<ExportResponseDto>(
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
SerializerOptions,
cancellationToken).ConfigureAwait(false);
if (payload is null)
{
throw new InvalidOperationException("Authority export response payload was empty.");
}
var bundleBytes = Convert.FromBase64String(payload.Bundle.Data);
var digest = payload.Digest?.Value ?? string.Empty;
if (verbose)
{
logger.LogInformation(
"Received revocation export sequence {Sequence} (sha256:{Digest}, signing key {KeyId}, provider {Provider}).",
payload.Sequence,
digest,
payload.SigningKeyId ?? "<unspecified>",
string.IsNullOrWhiteSpace(payload.Signature?.Provider) ? "default" : payload.Signature!.Provider);
}
return new AuthorityRevocationExportResult
{
BundleBytes = bundleBytes,
Signature = payload.Signature?.Value ?? string.Empty,
Digest = digest,
Sequence = payload.Sequence,
IssuedAt = payload.IssuedAt,
SigningKeyId = payload.SigningKeyId,
SigningProvider = payload.Signature?.Provider
};
}
private async Task<string?> AcquireAccessTokenAsync(CancellationToken cancellationToken)
{
if (tokenClient is null)
{
return null;
}
lock (tokenSync)
{
if (!string.IsNullOrEmpty(cachedAccessToken) && cachedAccessTokenExpiresAt - TokenRefreshSkew > DateTimeOffset.UtcNow)
{
return cachedAccessToken;
}
}
var scope = AuthorityTokenUtilities.ResolveScope(options);
var token = await RequestAccessTokenAsync(scope, cancellationToken).ConfigureAwait(false);
lock (tokenSync)
{
cachedAccessToken = token.AccessToken;
cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
return cachedAccessToken;
}
}
private async Task<StellaOpsTokenResult> RequestAccessTokenAsync(string scope, CancellationToken cancellationToken)
{
if (options.Authority is null)
{
throw new InvalidOperationException("Authority credentials are not configured.");
}
if (!string.IsNullOrWhiteSpace(options.Authority.Username))
{
if (string.IsNullOrWhiteSpace(options.Authority.Password))
{
throw new InvalidOperationException("Authority password must be configured or run 'auth login'.");
}
return await tokenClient!.RequestPasswordTokenAsync(
options.Authority.Username,
options.Authority.Password!,
scope,
null,
cancellationToken).ConfigureAwait(false);
}
return await tokenClient!.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
private void EnsureAuthorityConfigured()
{
if (string.IsNullOrWhiteSpace(options.Authority?.Url))
{
throw new InvalidOperationException("Authority URL is not configured. Set STELLAOPS_AUTHORITY_URL or update stellaops.yaml.");
}
if (httpClient.BaseAddress is null)
{
if (!Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var baseUri))
{
throw new InvalidOperationException("Authority URL is invalid.");
}
httpClient.BaseAddress = baseUri;
}
}
private sealed class ExportResponseDto
{
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; } = string.Empty;
[JsonPropertyName("bundleId")]
public string BundleId { get; set; } = string.Empty;
[JsonPropertyName("sequence")]
public long Sequence { get; set; }
[JsonPropertyName("issuedAt")]
public DateTimeOffset IssuedAt { get; set; }
[JsonPropertyName("signingKeyId")]
public string? SigningKeyId { get; set; }
[JsonPropertyName("bundle")]
public ExportPayloadDto Bundle { get; set; } = new();
[JsonPropertyName("signature")]
public ExportSignatureDto? Signature { get; set; }
[JsonPropertyName("digest")]
public ExportDigestDto? Digest { get; set; }
}
private sealed class ExportPayloadDto
{
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
}
private sealed class ExportSignatureDto
{
[JsonPropertyName("algorithm")]
public string Algorithm { get; set; } = string.Empty;
[JsonPropertyName("keyId")]
public string KeyId { get; set; } = string.Empty;
[JsonPropertyName("provider")]
public string Provider { get; set; } = string.Empty;
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
}
private sealed class ExportDigestDto
{
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
}
}
using System;
using System.Buffers.Text;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal sealed class AuthorityRevocationClient : IAuthorityRevocationClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);
private readonly HttpClient httpClient;
private readonly StellaOpsCliOptions options;
private readonly ILogger<AuthorityRevocationClient> logger;
private readonly IStellaOpsTokenClient? tokenClient;
private readonly object tokenSync = new();
private string? cachedAccessToken;
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
public AuthorityRevocationClient(
HttpClient httpClient,
StellaOpsCliOptions options,
ILogger<AuthorityRevocationClient> logger,
IStellaOpsTokenClient? tokenClient = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.tokenClient = tokenClient;
if (!string.IsNullOrWhiteSpace(options.Authority?.Url) && httpClient.BaseAddress is null && Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var authorityUri))
{
httpClient.BaseAddress = authorityUri;
}
}
public async Task<AuthorityRevocationExportResult> ExportAsync(bool verbose, CancellationToken cancellationToken)
{
EnsureAuthorityConfigured();
using var request = new HttpRequestMessage(HttpMethod.Get, "internal/revocations/export");
var accessToken = await AcquireAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var message = $"Authority export request failed with {(int)response.StatusCode} {response.ReasonPhrase}: {body}";
throw new InvalidOperationException(message);
}
var payload = await JsonSerializer.DeserializeAsync<ExportResponseDto>(
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
SerializerOptions,
cancellationToken).ConfigureAwait(false);
if (payload is null)
{
throw new InvalidOperationException("Authority export response payload was empty.");
}
var bundleBytes = Convert.FromBase64String(payload.Bundle.Data);
var digest = payload.Digest?.Value ?? string.Empty;
if (verbose)
{
logger.LogInformation(
"Received revocation export sequence {Sequence} (sha256:{Digest}, signing key {KeyId}, provider {Provider}).",
payload.Sequence,
digest,
payload.SigningKeyId ?? "<unspecified>",
string.IsNullOrWhiteSpace(payload.Signature?.Provider) ? "default" : payload.Signature!.Provider);
}
return new AuthorityRevocationExportResult
{
BundleBytes = bundleBytes,
Signature = payload.Signature?.Value ?? string.Empty,
Digest = digest,
Sequence = payload.Sequence,
IssuedAt = payload.IssuedAt,
SigningKeyId = payload.SigningKeyId,
SigningProvider = payload.Signature?.Provider
};
}
private async Task<string?> AcquireAccessTokenAsync(CancellationToken cancellationToken)
{
if (tokenClient is null)
{
return null;
}
lock (tokenSync)
{
if (!string.IsNullOrEmpty(cachedAccessToken) && cachedAccessTokenExpiresAt - TokenRefreshSkew > DateTimeOffset.UtcNow)
{
return cachedAccessToken;
}
}
var scope = AuthorityTokenUtilities.ResolveScope(options);
var token = await RequestAccessTokenAsync(scope, cancellationToken).ConfigureAwait(false);
lock (tokenSync)
{
cachedAccessToken = token.AccessToken;
cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
return cachedAccessToken;
}
}
private async Task<StellaOpsTokenResult> RequestAccessTokenAsync(string scope, CancellationToken cancellationToken)
{
if (options.Authority is null)
{
throw new InvalidOperationException("Authority credentials are not configured.");
}
if (!string.IsNullOrWhiteSpace(options.Authority.Username))
{
if (string.IsNullOrWhiteSpace(options.Authority.Password))
{
throw new InvalidOperationException("Authority password must be configured or run 'auth login'.");
}
return await tokenClient!.RequestPasswordTokenAsync(
options.Authority.Username,
options.Authority.Password!,
scope,
null,
cancellationToken).ConfigureAwait(false);
}
return await tokenClient!.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
private void EnsureAuthorityConfigured()
{
if (string.IsNullOrWhiteSpace(options.Authority?.Url))
{
throw new InvalidOperationException("Authority URL is not configured. Set STELLAOPS_AUTHORITY_URL or update stellaops.yaml.");
}
if (httpClient.BaseAddress is null)
{
if (!Uri.TryCreate(options.Authority.Url, UriKind.Absolute, out var baseUri))
{
throw new InvalidOperationException("Authority URL is invalid.");
}
httpClient.BaseAddress = baseUri;
}
}
private sealed class ExportResponseDto
{
[JsonPropertyName("schemaVersion")]
public string SchemaVersion { get; set; } = string.Empty;
[JsonPropertyName("bundleId")]
public string BundleId { get; set; } = string.Empty;
[JsonPropertyName("sequence")]
public long Sequence { get; set; }
[JsonPropertyName("issuedAt")]
public DateTimeOffset IssuedAt { get; set; }
[JsonPropertyName("signingKeyId")]
public string? SigningKeyId { get; set; }
[JsonPropertyName("bundle")]
public ExportPayloadDto Bundle { get; set; } = new();
[JsonPropertyName("signature")]
public ExportSignatureDto? Signature { get; set; }
[JsonPropertyName("digest")]
public ExportDigestDto? Digest { get; set; }
}
private sealed class ExportPayloadDto
{
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
}
private sealed class ExportSignatureDto
{
[JsonPropertyName("algorithm")]
public string Algorithm { get; set; } = string.Empty;
[JsonPropertyName("keyId")]
public string KeyId { get; set; } = string.Empty;
[JsonPropertyName("provider")]
public string Provider { get; set; } = string.Empty;
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
}
private sealed class ExportDigestDto
{
[JsonPropertyName("value")]
public string Value { get; set; } = string.Empty;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,392 +1,392 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal sealed class ConcelierObservationsClient : IConcelierObservationsClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);
private readonly HttpClient httpClient;
private readonly StellaOpsCliOptions options;
private readonly ILogger<ConcelierObservationsClient> logger;
private readonly IStellaOpsTokenClient? tokenClient;
private readonly object tokenSync = new();
private string? cachedAccessToken;
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
public ConcelierObservationsClient(
HttpClient httpClient,
StellaOpsCliOptions options,
ILogger<ConcelierObservationsClient> logger,
IStellaOpsTokenClient? tokenClient = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.tokenClient = tokenClient;
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl) && httpClient.BaseAddress is null)
{
if (Uri.TryCreate(options.ConcelierUrl, UriKind.Absolute, out var baseUri))
{
httpClient.BaseAddress = baseUri;
}
}
}
public async Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
EnsureConfigured();
var requestUri = BuildRequestUri(query);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to query observations (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<AdvisoryObservationsResponse>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new AdvisoryObservationsResponse();
}
/// <summary>
/// Gets advisory linkset with conflict information.
/// Per CLI-LNM-22-001.
/// </summary>
public async Task<AdvisoryLinksetResponse> GetLinksetAsync(
AdvisoryLinksetQuery query,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
EnsureConfigured();
var requestUri = BuildLinksetRequestUri(query);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to query linkset (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<AdvisoryLinksetResponse>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new AdvisoryLinksetResponse();
}
/// <summary>
/// Gets a single observation by ID.
/// Per CLI-LNM-22-001.
/// </summary>
public async Task<AdvisoryLinksetObservation?> GetObservationByIdAsync(
string tenant,
string observationId,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenant);
ArgumentException.ThrowIfNullOrWhiteSpace(observationId);
EnsureConfigured();
var requestUri = $"/concelier/observations/{Uri.EscapeDataString(observationId)}?tenant={Uri.EscapeDataString(tenant)}";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to get observation (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer
.DeserializeAsync<AdvisoryLinksetObservation>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
}
private static string BuildRequestUri(AdvisoryObservationsQuery query)
{
var builder = new StringBuilder("/concelier/observations?tenant=");
builder.Append(Uri.EscapeDataString(query.Tenant));
AppendValues(builder, "observationId", query.ObservationIds);
AppendValues(builder, "alias", query.Aliases);
AppendValues(builder, "purl", query.Purls);
AppendValues(builder, "cpe", query.Cpes);
if (query.Limit.HasValue && query.Limit.Value > 0)
{
builder.Append('&');
builder.Append("limit=");
builder.Append(query.Limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(query.Cursor))
{
builder.Append('&');
builder.Append("cursor=");
builder.Append(Uri.EscapeDataString(query.Cursor));
}
return builder.ToString();
static void AppendValues(StringBuilder builder, string name, IReadOnlyList<string> values)
{
if (values is null || values.Count == 0)
{
return;
}
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
builder.Append('&');
builder.Append(name);
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
}
}
private static string BuildLinksetRequestUri(AdvisoryLinksetQuery query)
{
var builder = new StringBuilder("/concelier/linkset?tenant=");
builder.Append(Uri.EscapeDataString(query.Tenant));
AppendValues(builder, "observationId", query.ObservationIds);
AppendValues(builder, "alias", query.Aliases);
AppendValues(builder, "purl", query.Purls);
AppendValues(builder, "cpe", query.Cpes);
AppendValues(builder, "source", query.Sources);
if (!string.IsNullOrWhiteSpace(query.Severity))
{
builder.Append("&severity=");
builder.Append(Uri.EscapeDataString(query.Severity));
}
if (query.KevOnly.HasValue)
{
builder.Append("&kevOnly=");
builder.Append(query.KevOnly.Value ? "true" : "false");
}
if (query.HasFix.HasValue)
{
builder.Append("&hasFix=");
builder.Append(query.HasFix.Value ? "true" : "false");
}
if (query.Limit.HasValue && query.Limit.Value > 0)
{
builder.Append("&limit=");
builder.Append(query.Limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(query.Cursor))
{
builder.Append("&cursor=");
builder.Append(Uri.EscapeDataString(query.Cursor));
}
return builder.ToString();
static void AppendValues(StringBuilder builder, string name, IReadOnlyList<string> values)
{
if (values is null || values.Count == 0)
{
return;
}
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
builder.Append('&');
builder.Append(name);
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
}
}
private void EnsureConfigured()
{
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl))
{
return;
}
throw new InvalidOperationException(
"ConcelierUrl is not configured. Set StellaOps:ConcelierUrl or STELLAOPS_CONCELIER_URL.");
}
private async Task AuthorizeRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await ResolveAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private async Task<string?> ResolveAccessTokenAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrWhiteSpace(options.ApiKey))
{
return options.ApiKey;
}
if (tokenClient is null || string.IsNullOrWhiteSpace(options.Authority.Url))
{
return null;
}
var now = DateTimeOffset.UtcNow;
lock (tokenSync)
{
if (!string.IsNullOrEmpty(cachedAccessToken) && now < cachedAccessTokenExpiresAt - TokenRefreshSkew)
{
return cachedAccessToken;
}
}
var (scope, cacheKey) = BuildScopeAndCacheKey(options);
var cachedEntry = await tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (cachedEntry is not null && now < cachedEntry.ExpiresAtUtc - TokenRefreshSkew)
{
lock (tokenSync)
{
cachedAccessToken = cachedEntry.AccessToken;
cachedAccessTokenExpiresAt = cachedEntry.ExpiresAtUtc;
return cachedAccessToken;
}
}
StellaOpsTokenResult token;
if (!string.IsNullOrWhiteSpace(options.Authority.Username))
{
if (string.IsNullOrWhiteSpace(options.Authority.Password))
{
throw new InvalidOperationException("Authority password must be configured when username is provided.");
}
token = await tokenClient.RequestPasswordTokenAsync(
options.Authority.Username,
options.Authority.Password!,
scope,
null,
cancellationToken).ConfigureAwait(false);
}
else
{
token = await tokenClient.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
await tokenClient.CacheTokenAsync(cacheKey, token.ToCacheEntry(), cancellationToken).ConfigureAwait(false);
lock (tokenSync)
{
cachedAccessToken = token.AccessToken;
cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
return cachedAccessToken;
}
}
private static (string Scope, string CacheKey) BuildScopeAndCacheKey(StellaOpsCliOptions options)
{
var baseScope = AuthorityTokenUtilities.ResolveScope(options);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnRead);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
var cacheKey = $"{options.Authority.Url}|{credential}|{finalScope}";
return (finalScope, cacheKey);
}
private static string EnsureScope(string scopes, string required)
{
if (string.IsNullOrWhiteSpace(scopes))
{
return required;
}
var parts = scopes
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(static scope => scope.ToLowerInvariant())
.Distinct(StringComparer.Ordinal)
.ToList();
if (!parts.Contains(required, StringComparer.Ordinal))
{
parts.Add(required);
}
return string.Join(' ', parts);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
internal sealed class ConcelierObservationsClient : IConcelierObservationsClient
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private static readonly TimeSpan TokenRefreshSkew = TimeSpan.FromSeconds(30);
private readonly HttpClient httpClient;
private readonly StellaOpsCliOptions options;
private readonly ILogger<ConcelierObservationsClient> logger;
private readonly IStellaOpsTokenClient? tokenClient;
private readonly object tokenSync = new();
private string? cachedAccessToken;
private DateTimeOffset cachedAccessTokenExpiresAt = DateTimeOffset.MinValue;
public ConcelierObservationsClient(
HttpClient httpClient,
StellaOpsCliOptions options,
ILogger<ConcelierObservationsClient> logger,
IStellaOpsTokenClient? tokenClient = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.options = options ?? throw new ArgumentNullException(nameof(options));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.tokenClient = tokenClient;
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl) && httpClient.BaseAddress is null)
{
if (Uri.TryCreate(options.ConcelierUrl, UriKind.Absolute, out var baseUri))
{
httpClient.BaseAddress = baseUri;
}
}
}
public async Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
EnsureConfigured();
var requestUri = BuildRequestUri(query);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to query observations (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<AdvisoryObservationsResponse>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new AdvisoryObservationsResponse();
}
/// <summary>
/// Gets advisory linkset with conflict information.
/// Per CLI-LNM-22-001.
/// </summary>
public async Task<AdvisoryLinksetResponse> GetLinksetAsync(
AdvisoryLinksetQuery query,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
EnsureConfigured();
var requestUri = BuildLinksetRequestUri(query);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to query linkset (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer
.DeserializeAsync<AdvisoryLinksetResponse>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
return result ?? new AdvisoryLinksetResponse();
}
/// <summary>
/// Gets a single observation by ID.
/// Per CLI-LNM-22-001.
/// </summary>
public async Task<AdvisoryLinksetObservation?> GetObservationByIdAsync(
string tenant,
string observationId,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenant);
ArgumentException.ThrowIfNullOrWhiteSpace(observationId);
EnsureConfigured();
var requestUri = $"/concelier/observations/{Uri.EscapeDataString(observationId)}?tenant={Uri.EscapeDataString(tenant)}";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
await AuthorizeRequestAsync(request, cancellationToken).ConfigureAwait(false);
using var response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
if (!response.IsSuccessStatusCode)
{
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError(
"Failed to get observation (status {StatusCode}). Response: {Payload}",
(int)response.StatusCode,
string.IsNullOrWhiteSpace(payload) ? "<empty>" : payload);
response.EnsureSuccessStatusCode();
}
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await JsonSerializer
.DeserializeAsync<AdvisoryLinksetObservation>(stream, SerializerOptions, cancellationToken)
.ConfigureAwait(false);
}
private static string BuildRequestUri(AdvisoryObservationsQuery query)
{
var builder = new StringBuilder("/concelier/observations?tenant=");
builder.Append(Uri.EscapeDataString(query.Tenant));
AppendValues(builder, "observationId", query.ObservationIds);
AppendValues(builder, "alias", query.Aliases);
AppendValues(builder, "purl", query.Purls);
AppendValues(builder, "cpe", query.Cpes);
if (query.Limit.HasValue && query.Limit.Value > 0)
{
builder.Append('&');
builder.Append("limit=");
builder.Append(query.Limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(query.Cursor))
{
builder.Append('&');
builder.Append("cursor=");
builder.Append(Uri.EscapeDataString(query.Cursor));
}
return builder.ToString();
static void AppendValues(StringBuilder builder, string name, IReadOnlyList<string> values)
{
if (values is null || values.Count == 0)
{
return;
}
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
builder.Append('&');
builder.Append(name);
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
}
}
private static string BuildLinksetRequestUri(AdvisoryLinksetQuery query)
{
var builder = new StringBuilder("/concelier/linkset?tenant=");
builder.Append(Uri.EscapeDataString(query.Tenant));
AppendValues(builder, "observationId", query.ObservationIds);
AppendValues(builder, "alias", query.Aliases);
AppendValues(builder, "purl", query.Purls);
AppendValues(builder, "cpe", query.Cpes);
AppendValues(builder, "source", query.Sources);
if (!string.IsNullOrWhiteSpace(query.Severity))
{
builder.Append("&severity=");
builder.Append(Uri.EscapeDataString(query.Severity));
}
if (query.KevOnly.HasValue)
{
builder.Append("&kevOnly=");
builder.Append(query.KevOnly.Value ? "true" : "false");
}
if (query.HasFix.HasValue)
{
builder.Append("&hasFix=");
builder.Append(query.HasFix.Value ? "true" : "false");
}
if (query.Limit.HasValue && query.Limit.Value > 0)
{
builder.Append("&limit=");
builder.Append(query.Limit.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrWhiteSpace(query.Cursor))
{
builder.Append("&cursor=");
builder.Append(Uri.EscapeDataString(query.Cursor));
}
return builder.ToString();
static void AppendValues(StringBuilder builder, string name, IReadOnlyList<string> values)
{
if (values is null || values.Count == 0)
{
return;
}
foreach (var value in values)
{
if (string.IsNullOrWhiteSpace(value))
{
continue;
}
builder.Append('&');
builder.Append(name);
builder.Append('=');
builder.Append(Uri.EscapeDataString(value));
}
}
}
private void EnsureConfigured()
{
if (!string.IsNullOrWhiteSpace(options.ConcelierUrl))
{
return;
}
throw new InvalidOperationException(
"ConcelierUrl is not configured. Set StellaOps:ConcelierUrl or STELLAOPS_CONCELIER_URL.");
}
private async Task AuthorizeRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await ResolveAccessTokenAsync(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(token))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private async Task<string?> ResolveAccessTokenAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrWhiteSpace(options.ApiKey))
{
return options.ApiKey;
}
if (tokenClient is null || string.IsNullOrWhiteSpace(options.Authority.Url))
{
return null;
}
var now = DateTimeOffset.UtcNow;
lock (tokenSync)
{
if (!string.IsNullOrEmpty(cachedAccessToken) && now < cachedAccessTokenExpiresAt - TokenRefreshSkew)
{
return cachedAccessToken;
}
}
var (scope, cacheKey) = BuildScopeAndCacheKey(options);
var cachedEntry = await tokenClient.GetCachedTokenAsync(cacheKey, cancellationToken).ConfigureAwait(false);
if (cachedEntry is not null && now < cachedEntry.ExpiresAtUtc - TokenRefreshSkew)
{
lock (tokenSync)
{
cachedAccessToken = cachedEntry.AccessToken;
cachedAccessTokenExpiresAt = cachedEntry.ExpiresAtUtc;
return cachedAccessToken;
}
}
StellaOpsTokenResult token;
if (!string.IsNullOrWhiteSpace(options.Authority.Username))
{
if (string.IsNullOrWhiteSpace(options.Authority.Password))
{
throw new InvalidOperationException("Authority password must be configured when username is provided.");
}
token = await tokenClient.RequestPasswordTokenAsync(
options.Authority.Username,
options.Authority.Password!,
scope,
null,
cancellationToken).ConfigureAwait(false);
}
else
{
token = await tokenClient.RequestClientCredentialsTokenAsync(scope, null, cancellationToken).ConfigureAwait(false);
}
await tokenClient.CacheTokenAsync(cacheKey, token.ToCacheEntry(), cancellationToken).ConfigureAwait(false);
lock (tokenSync)
{
cachedAccessToken = token.AccessToken;
cachedAccessTokenExpiresAt = token.ExpiresAtUtc;
return cachedAccessToken;
}
}
private static (string Scope, string CacheKey) BuildScopeAndCacheKey(StellaOpsCliOptions options)
{
var baseScope = AuthorityTokenUtilities.ResolveScope(options);
var finalScope = EnsureScope(baseScope, StellaOpsScopes.VulnRead);
var credential = !string.IsNullOrWhiteSpace(options.Authority.Username)
? $"user:{options.Authority.Username}"
: $"client:{options.Authority.ClientId}";
var cacheKey = $"{options.Authority.Url}|{credential}|{finalScope}";
return (finalScope, cacheKey);
}
private static string EnsureScope(string scopes, string required)
{
if (string.IsNullOrWhiteSpace(scopes))
{
return required;
}
var parts = scopes
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(static scope => scope.ToLowerInvariant())
.Distinct(StringComparer.Ordinal)
.ToList();
if (!parts.Contains(required, StringComparer.Ordinal))
{
parts.Add(required);
}
return string.Join(' ', parts);
}
}

View File

@@ -1,35 +1,35 @@
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
/// <summary>
/// Client for Concelier advisory observations API.
/// Per CLI-LNM-22-001, supports obs get, linkset show, and export operations.
/// </summary>
internal interface IConcelierObservationsClient
{
/// <summary>
/// Gets advisory observations matching the query.
/// </summary>
Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken);
/// <summary>
/// Gets advisory linkset with conflict information.
/// Per CLI-LNM-22-001, includes conflict display.
/// </summary>
Task<AdvisoryLinksetResponse> GetLinksetAsync(
AdvisoryLinksetQuery query,
CancellationToken cancellationToken);
/// <summary>
/// Gets a single observation by ID.
/// </summary>
Task<AdvisoryLinksetObservation?> GetObservationByIdAsync(
string tenant,
string observationId,
CancellationToken cancellationToken);
}
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cli.Services.Models;
namespace StellaOps.Cli.Services;
/// <summary>
/// Client for Concelier advisory observations API.
/// Per CLI-LNM-22-001, supports obs get, linkset show, and export operations.
/// </summary>
internal interface IConcelierObservationsClient
{
/// <summary>
/// Gets advisory observations matching the query.
/// </summary>
Task<AdvisoryObservationsResponse> GetObservationsAsync(
AdvisoryObservationsQuery query,
CancellationToken cancellationToken);
/// <summary>
/// Gets advisory linkset with conflict information.
/// Per CLI-LNM-22-001, includes conflict display.
/// </summary>
Task<AdvisoryLinksetResponse> GetLinksetAsync(
AdvisoryLinksetQuery query,
CancellationToken cancellationToken);
/// <summary>
/// Gets a single observation by ID.
/// </summary>
Task<AdvisoryLinksetObservation?> GetObservationByIdAsync(
string tenant,
string observationId,
CancellationToken cancellationToken);
}

View File

@@ -1,11 +1,11 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Services;
internal interface IScannerExecutor
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Services;
internal interface IScannerExecutor
{
Task<ScannerExecutionResult> RunAsync(
string runner,
string entry,

View File

@@ -1,9 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Services;
internal interface IScannerInstaller
{
Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken);
}
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cli.Services;
internal interface IScannerInstaller
{
Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken);
}

View File

@@ -1,117 +1,117 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed record AdvisoryObservationsQuery(
string Tenant,
IReadOnlyList<string> ObservationIds,
IReadOnlyList<string> Aliases,
IReadOnlyList<string> Purls,
IReadOnlyList<string> Cpes,
int? Limit,
string? Cursor);
internal sealed class AdvisoryObservationsResponse
{
[JsonPropertyName("observations")]
public IReadOnlyList<AdvisoryObservationDocument> Observations { get; init; } =
Array.Empty<AdvisoryObservationDocument>();
[JsonPropertyName("linkset")]
public AdvisoryObservationLinksetAggregate Linkset { get; init; } =
new();
[JsonPropertyName("nextCursor")]
public string? NextCursor { get; init; }
[JsonPropertyName("hasMore")]
public bool HasMore { get; init; }
}
internal sealed class AdvisoryObservationDocument
{
[JsonPropertyName("observationId")]
public string ObservationId { get; init; } = string.Empty;
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("source")]
public AdvisoryObservationSource Source { get; init; } = new();
[JsonPropertyName("upstream")]
public AdvisoryObservationUpstream Upstream { get; init; } = new();
[JsonPropertyName("linkset")]
public AdvisoryObservationLinkset Linkset { get; init; } = new();
[JsonPropertyName("createdAt")]
public DateTimeOffset CreatedAt { get; init; }
}
internal sealed class AdvisoryObservationSource
{
[JsonPropertyName("vendor")]
public string Vendor { get; init; } = string.Empty;
[JsonPropertyName("stream")]
public string Stream { get; init; } = string.Empty;
[JsonPropertyName("api")]
public string Api { get; init; } = string.Empty;
[JsonPropertyName("collectorVersion")]
public string? CollectorVersion { get; init; }
}
internal sealed class AdvisoryObservationUpstream
{
[JsonPropertyName("upstreamId")]
public string UpstreamId { get; init; } = string.Empty;
[JsonPropertyName("documentVersion")]
public string? DocumentVersion { get; init; }
}
internal sealed class AdvisoryObservationLinkset
{
[JsonPropertyName("aliases")]
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
[JsonPropertyName("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[JsonPropertyName("cpes")]
public IReadOnlyList<string> Cpes { get; init; } = Array.Empty<string>();
[JsonPropertyName("references")]
public IReadOnlyList<AdvisoryObservationReference> References { get; init; } =
Array.Empty<AdvisoryObservationReference>();
}
internal sealed class AdvisoryObservationReference
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("url")]
public string Url { get; init; } = string.Empty;
}
internal sealed class AdvisoryObservationLinksetAggregate
{
[JsonPropertyName("aliases")]
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
[JsonPropertyName("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[JsonPropertyName("cpes")]
public IReadOnlyList<string> Cpes { get; init; } = Array.Empty<string>();
[JsonPropertyName("references")]
public IReadOnlyList<AdvisoryObservationReference> References { get; init; } =
Array.Empty<AdvisoryObservationReference>();
}
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed record AdvisoryObservationsQuery(
string Tenant,
IReadOnlyList<string> ObservationIds,
IReadOnlyList<string> Aliases,
IReadOnlyList<string> Purls,
IReadOnlyList<string> Cpes,
int? Limit,
string? Cursor);
internal sealed class AdvisoryObservationsResponse
{
[JsonPropertyName("observations")]
public IReadOnlyList<AdvisoryObservationDocument> Observations { get; init; } =
Array.Empty<AdvisoryObservationDocument>();
[JsonPropertyName("linkset")]
public AdvisoryObservationLinksetAggregate Linkset { get; init; } =
new();
[JsonPropertyName("nextCursor")]
public string? NextCursor { get; init; }
[JsonPropertyName("hasMore")]
public bool HasMore { get; init; }
}
internal sealed class AdvisoryObservationDocument
{
[JsonPropertyName("observationId")]
public string ObservationId { get; init; } = string.Empty;
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("source")]
public AdvisoryObservationSource Source { get; init; } = new();
[JsonPropertyName("upstream")]
public AdvisoryObservationUpstream Upstream { get; init; } = new();
[JsonPropertyName("linkset")]
public AdvisoryObservationLinkset Linkset { get; init; } = new();
[JsonPropertyName("createdAt")]
public DateTimeOffset CreatedAt { get; init; }
}
internal sealed class AdvisoryObservationSource
{
[JsonPropertyName("vendor")]
public string Vendor { get; init; } = string.Empty;
[JsonPropertyName("stream")]
public string Stream { get; init; } = string.Empty;
[JsonPropertyName("api")]
public string Api { get; init; } = string.Empty;
[JsonPropertyName("collectorVersion")]
public string? CollectorVersion { get; init; }
}
internal sealed class AdvisoryObservationUpstream
{
[JsonPropertyName("upstreamId")]
public string UpstreamId { get; init; } = string.Empty;
[JsonPropertyName("documentVersion")]
public string? DocumentVersion { get; init; }
}
internal sealed class AdvisoryObservationLinkset
{
[JsonPropertyName("aliases")]
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
[JsonPropertyName("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[JsonPropertyName("cpes")]
public IReadOnlyList<string> Cpes { get; init; } = Array.Empty<string>();
[JsonPropertyName("references")]
public IReadOnlyList<AdvisoryObservationReference> References { get; init; } =
Array.Empty<AdvisoryObservationReference>();
}
internal sealed class AdvisoryObservationReference
{
[JsonPropertyName("type")]
public string Type { get; init; } = string.Empty;
[JsonPropertyName("url")]
public string Url { get; init; } = string.Empty;
}
internal sealed class AdvisoryObservationLinksetAggregate
{
[JsonPropertyName("aliases")]
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
[JsonPropertyName("purls")]
public IReadOnlyList<string> Purls { get; init; } = Array.Empty<string>();
[JsonPropertyName("cpes")]
public IReadOnlyList<string> Cpes { get; init; } = Array.Empty<string>();
[JsonPropertyName("references")]
public IReadOnlyList<AdvisoryObservationReference> References { get; init; } =
Array.Empty<AdvisoryObservationReference>();
}

View File

@@ -1,93 +1,93 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed class AocIngestDryRunRequest
{
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
[JsonPropertyName("document")]
public AocIngestDryRunDocument Document { get; init; } = new();
}
internal sealed class AocIngestDryRunDocument
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("content")]
public string Content { get; init; } = string.Empty;
[JsonPropertyName("contentType")]
public string ContentType { get; init; } = "application/json";
[JsonPropertyName("contentEncoding")]
public string? ContentEncoding { get; init; }
}
internal sealed class AocIngestDryRunResponse
{
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("tenant")]
public string? Tenant { get; init; }
[JsonPropertyName("guardVersion")]
public string? GuardVersion { get; init; }
[JsonPropertyName("status")]
public string? Status { get; init; }
[JsonPropertyName("document")]
public AocIngestDryRunDocumentResult Document { get; init; } = new();
[JsonPropertyName("violations")]
public IReadOnlyList<AocIngestDryRunViolation> Violations { get; init; } =
Array.Empty<AocIngestDryRunViolation>();
}
internal sealed class AocIngestDryRunDocumentResult
{
[JsonPropertyName("contentHash")]
public string? ContentHash { get; init; }
[JsonPropertyName("supersedes")]
public string? Supersedes { get; init; }
[JsonPropertyName("provenance")]
public AocIngestDryRunProvenance Provenance { get; init; } = new();
}
internal sealed class AocIngestDryRunProvenance
{
[JsonPropertyName("signature")]
public AocIngestDryRunSignature Signature { get; init; } = new();
}
internal sealed class AocIngestDryRunSignature
{
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("present")]
public bool Present { get; init; }
}
internal sealed class AocIngestDryRunViolation
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;
[JsonPropertyName("path")]
public string? Path { get; init; }
}
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed class AocIngestDryRunRequest
{
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("source")]
public string Source { get; init; } = string.Empty;
[JsonPropertyName("document")]
public AocIngestDryRunDocument Document { get; init; } = new();
}
internal sealed class AocIngestDryRunDocument
{
[JsonPropertyName("name")]
public string? Name { get; init; }
[JsonPropertyName("content")]
public string Content { get; init; } = string.Empty;
[JsonPropertyName("contentType")]
public string ContentType { get; init; } = "application/json";
[JsonPropertyName("contentEncoding")]
public string? ContentEncoding { get; init; }
}
internal sealed class AocIngestDryRunResponse
{
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("tenant")]
public string? Tenant { get; init; }
[JsonPropertyName("guardVersion")]
public string? GuardVersion { get; init; }
[JsonPropertyName("status")]
public string? Status { get; init; }
[JsonPropertyName("document")]
public AocIngestDryRunDocumentResult Document { get; init; } = new();
[JsonPropertyName("violations")]
public IReadOnlyList<AocIngestDryRunViolation> Violations { get; init; } =
Array.Empty<AocIngestDryRunViolation>();
}
internal sealed class AocIngestDryRunDocumentResult
{
[JsonPropertyName("contentHash")]
public string? ContentHash { get; init; }
[JsonPropertyName("supersedes")]
public string? Supersedes { get; init; }
[JsonPropertyName("provenance")]
public AocIngestDryRunProvenance Provenance { get; init; } = new();
}
internal sealed class AocIngestDryRunProvenance
{
[JsonPropertyName("signature")]
public AocIngestDryRunSignature Signature { get; init; } = new();
}
internal sealed class AocIngestDryRunSignature
{
[JsonPropertyName("format")]
public string? Format { get; init; }
[JsonPropertyName("present")]
public bool Present { get; init; }
}
internal sealed class AocIngestDryRunViolation
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;
[JsonPropertyName("path")]
public string? Path { get; init; }
}

View File

@@ -1,100 +1,100 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed class AocVerifyRequest
{
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("since")]
public string? Since { get; init; }
[JsonPropertyName("limit")]
public int? Limit { get; init; }
[JsonPropertyName("sources")]
public IReadOnlyList<string>? Sources { get; init; }
[JsonPropertyName("codes")]
public IReadOnlyList<string>? Codes { get; init; }
}
internal sealed class AocVerifyResponse
{
[JsonPropertyName("tenant")]
public string? Tenant { get; init; }
[JsonPropertyName("window")]
public AocVerifyWindow Window { get; init; } = new();
[JsonPropertyName("checked")]
public AocVerifyChecked Checked { get; init; } = new();
[JsonPropertyName("violations")]
public IReadOnlyList<AocVerifyViolation> Violations { get; init; } =
Array.Empty<AocVerifyViolation>();
[JsonPropertyName("metrics")]
public AocVerifyMetrics Metrics { get; init; } = new();
[JsonPropertyName("truncated")]
public bool? Truncated { get; init; }
}
internal sealed class AocVerifyWindow
{
[JsonPropertyName("from")]
public DateTimeOffset? From { get; init; }
[JsonPropertyName("to")]
public DateTimeOffset? To { get; init; }
}
internal sealed class AocVerifyChecked
{
[JsonPropertyName("advisories")]
public int Advisories { get; init; }
[JsonPropertyName("vex")]
public int Vex { get; init; }
}
internal sealed class AocVerifyViolation
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("count")]
public int Count { get; init; }
[JsonPropertyName("examples")]
public IReadOnlyList<AocVerifyViolationExample> Examples { get; init; } =
Array.Empty<AocVerifyViolationExample>();
}
internal sealed class AocVerifyViolationExample
{
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("documentId")]
public string? DocumentId { get; init; }
[JsonPropertyName("contentHash")]
public string? ContentHash { get; init; }
[JsonPropertyName("path")]
public string? Path { get; init; }
}
internal sealed class AocVerifyMetrics
{
[JsonPropertyName("ingestion_write_total")]
public int? IngestionWriteTotal { get; init; }
[JsonPropertyName("aoc_violation_total")]
public int? AocViolationTotal { get; init; }
}
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models;
internal sealed class AocVerifyRequest
{
[JsonPropertyName("tenant")]
public string Tenant { get; init; } = string.Empty;
[JsonPropertyName("since")]
public string? Since { get; init; }
[JsonPropertyName("limit")]
public int? Limit { get; init; }
[JsonPropertyName("sources")]
public IReadOnlyList<string>? Sources { get; init; }
[JsonPropertyName("codes")]
public IReadOnlyList<string>? Codes { get; init; }
}
internal sealed class AocVerifyResponse
{
[JsonPropertyName("tenant")]
public string? Tenant { get; init; }
[JsonPropertyName("window")]
public AocVerifyWindow Window { get; init; } = new();
[JsonPropertyName("checked")]
public AocVerifyChecked Checked { get; init; } = new();
[JsonPropertyName("violations")]
public IReadOnlyList<AocVerifyViolation> Violations { get; init; } =
Array.Empty<AocVerifyViolation>();
[JsonPropertyName("metrics")]
public AocVerifyMetrics Metrics { get; init; } = new();
[JsonPropertyName("truncated")]
public bool? Truncated { get; init; }
}
internal sealed class AocVerifyWindow
{
[JsonPropertyName("from")]
public DateTimeOffset? From { get; init; }
[JsonPropertyName("to")]
public DateTimeOffset? To { get; init; }
}
internal sealed class AocVerifyChecked
{
[JsonPropertyName("advisories")]
public int Advisories { get; init; }
[JsonPropertyName("vex")]
public int Vex { get; init; }
}
internal sealed class AocVerifyViolation
{
[JsonPropertyName("code")]
public string Code { get; init; } = string.Empty;
[JsonPropertyName("count")]
public int Count { get; init; }
[JsonPropertyName("examples")]
public IReadOnlyList<AocVerifyViolationExample> Examples { get; init; } =
Array.Empty<AocVerifyViolationExample>();
}
internal sealed class AocVerifyViolationExample
{
[JsonPropertyName("source")]
public string? Source { get; init; }
[JsonPropertyName("documentId")]
public string? DocumentId { get; init; }
[JsonPropertyName("contentHash")]
public string? ContentHash { get; init; }
[JsonPropertyName("path")]
public string? Path { get; init; }
}
internal sealed class AocVerifyMetrics
{
[JsonPropertyName("ingestion_write_total")]
public int? IngestionWriteTotal { get; init; }
[JsonPropertyName("aoc_violation_total")]
public int? AocViolationTotal { get; init; }
}

View File

@@ -1,6 +1,6 @@
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorExportDownloadResult(
string Path,
long SizeBytes,
bool FromCache);
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorExportDownloadResult(
string Path,
long SizeBytes,
bool FromCache);

View File

@@ -1,9 +1,9 @@
using System.Text.Json;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorOperationResult(
bool Success,
string Message,
string? Location,
JsonElement? Payload);
using System.Text.Json;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorOperationResult(
bool Success,
string Message,
string? Location,
JsonElement? Payload);

View File

@@ -1,11 +1,11 @@
using System;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorProviderSummary(
string Id,
string Kind,
string DisplayName,
string TrustTier,
bool Enabled,
DateTimeOffset? LastIngestedAt);
using System;
namespace StellaOps.Cli.Services.Models;
internal sealed record ExcititorProviderSummary(
string Id,
string Kind,
string DisplayName,
string TrustTier,
bool Enabled,
DateTimeOffset? LastIngestedAt);

View File

@@ -1,9 +1,9 @@
using StellaOps.Cli.Services.Models.Transport;
namespace StellaOps.Cli.Services.Models;
internal sealed record JobTriggerResult(
bool Success,
string Message,
string? Location,
JobRunResponse? Run);
using StellaOps.Cli.Services.Models.Transport;
namespace StellaOps.Cli.Services.Models;
internal sealed record JobTriggerResult(
bool Success,
string Message,
string? Location,
JobRunResponse? Run);

View File

@@ -1,111 +1,111 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record OfflineKitBundleDescriptor(
string BundleId,
string BundleName,
string BundleSha256,
long BundleSize,
Uri BundleDownloadUri,
string ManifestName,
string ManifestSha256,
Uri ManifestDownloadUri,
DateTimeOffset CapturedAt,
string? Channel,
string? Kind,
bool IsDelta,
string? BaseBundleId,
string? BundleSignatureName,
Uri? BundleSignatureDownloadUri,
string? ManifestSignatureName,
Uri? ManifestSignatureDownloadUri,
long? ManifestSize);
internal sealed record OfflineKitDownloadResult(
OfflineKitBundleDescriptor Descriptor,
string BundlePath,
string ManifestPath,
string? BundleSignaturePath,
string? ManifestSignaturePath,
string MetadataPath,
bool FromCache);
internal sealed record OfflineKitImportRequest(
string BundlePath,
string? ManifestPath,
string? BundleSignaturePath,
string? ManifestSignaturePath,
string? BundleId,
string? BundleSha256,
long? BundleSize,
DateTimeOffset? CapturedAt,
string? Channel,
string? Kind,
bool? IsDelta,
string? BaseBundleId,
string? ManifestSha256,
long? ManifestSize);
internal sealed record OfflineKitImportResult(
string? ImportId,
string? Status,
DateTimeOffset SubmittedAt,
string? Message);
internal sealed record OfflineKitStatus(
string? BundleId,
string? Channel,
string? Kind,
bool IsDelta,
string? BaseBundleId,
DateTimeOffset? CapturedAt,
DateTimeOffset? ImportedAt,
string? BundleSha256,
long? BundleSize,
IReadOnlyList<OfflineKitComponentStatus> Components);
internal sealed record OfflineKitComponentStatus(
string Name,
string? Version,
string? Digest,
DateTimeOffset? CapturedAt,
long? SizeBytes);
internal sealed record OfflineKitMetadataDocument
{
public string? BundleId { get; init; }
public string BundleName { get; init; } = string.Empty;
public string BundleSha256 { get; init; } = string.Empty;
public long BundleSize { get; init; }
public string BundlePath { get; init; } = string.Empty;
public DateTimeOffset CapturedAt { get; init; }
public DateTimeOffset DownloadedAt { get; init; }
public string? Channel { get; init; }
public string? Kind { get; init; }
public bool IsDelta { get; init; }
public string? BaseBundleId { get; init; }
public string ManifestName { get; init; } = string.Empty;
public string ManifestSha256 { get; init; } = string.Empty;
public long? ManifestSize { get; init; }
public string ManifestPath { get; init; } = string.Empty;
public string? BundleSignaturePath { get; init; }
public string? ManifestSignaturePath { get; init; }
}
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record OfflineKitBundleDescriptor(
string BundleId,
string BundleName,
string BundleSha256,
long BundleSize,
Uri BundleDownloadUri,
string ManifestName,
string ManifestSha256,
Uri ManifestDownloadUri,
DateTimeOffset CapturedAt,
string? Channel,
string? Kind,
bool IsDelta,
string? BaseBundleId,
string? BundleSignatureName,
Uri? BundleSignatureDownloadUri,
string? ManifestSignatureName,
Uri? ManifestSignatureDownloadUri,
long? ManifestSize);
internal sealed record OfflineKitDownloadResult(
OfflineKitBundleDescriptor Descriptor,
string BundlePath,
string ManifestPath,
string? BundleSignaturePath,
string? ManifestSignaturePath,
string MetadataPath,
bool FromCache);
internal sealed record OfflineKitImportRequest(
string BundlePath,
string? ManifestPath,
string? BundleSignaturePath,
string? ManifestSignaturePath,
string? BundleId,
string? BundleSha256,
long? BundleSize,
DateTimeOffset? CapturedAt,
string? Channel,
string? Kind,
bool? IsDelta,
string? BaseBundleId,
string? ManifestSha256,
long? ManifestSize);
internal sealed record OfflineKitImportResult(
string? ImportId,
string? Status,
DateTimeOffset SubmittedAt,
string? Message);
internal sealed record OfflineKitStatus(
string? BundleId,
string? Channel,
string? Kind,
bool IsDelta,
string? BaseBundleId,
DateTimeOffset? CapturedAt,
DateTimeOffset? ImportedAt,
string? BundleSha256,
long? BundleSize,
IReadOnlyList<OfflineKitComponentStatus> Components);
internal sealed record OfflineKitComponentStatus(
string Name,
string? Version,
string? Digest,
DateTimeOffset? CapturedAt,
long? SizeBytes);
internal sealed record OfflineKitMetadataDocument
{
public string? BundleId { get; init; }
public string BundleName { get; init; } = string.Empty;
public string BundleSha256 { get; init; } = string.Empty;
public long BundleSize { get; init; }
public string BundlePath { get; init; } = string.Empty;
public DateTimeOffset CapturedAt { get; init; }
public DateTimeOffset DownloadedAt { get; init; }
public string? Channel { get; init; }
public string? Kind { get; init; }
public bool IsDelta { get; init; }
public string? BaseBundleId { get; init; }
public string ManifestName { get; init; } = string.Empty;
public string ManifestSha256 { get; init; } = string.Empty;
public long? ManifestSize { get; init; }
public string ManifestPath { get; init; } = string.Empty;
public string? BundleSignaturePath { get; init; }
public string? ManifestSignaturePath { get; init; }
}

View File

@@ -1,30 +1,30 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicyActivationRequest(
bool RunNow,
DateTimeOffset? ScheduledAt,
string? Priority,
bool Rollback,
string? IncidentId,
string? Comment);
internal sealed record PolicyActivationResult(
string Status,
PolicyActivationRevision Revision);
internal sealed record PolicyActivationRevision(
string PolicyId,
int Version,
string Status,
bool RequiresTwoPersonApproval,
DateTimeOffset CreatedAt,
DateTimeOffset? ActivatedAt,
IReadOnlyList<PolicyActivationApproval> Approvals);
internal sealed record PolicyActivationApproval(
string ActorId,
DateTimeOffset ApprovedAt,
string? Comment);
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicyActivationRequest(
bool RunNow,
DateTimeOffset? ScheduledAt,
string? Priority,
bool Rollback,
string? IncidentId,
string? Comment);
internal sealed record PolicyActivationResult(
string Status,
PolicyActivationRevision Revision);
internal sealed record PolicyActivationRevision(
string PolicyId,
int Version,
string Status,
bool RequiresTwoPersonApproval,
DateTimeOffset CreatedAt,
DateTimeOffset? ActivatedAt,
IReadOnlyList<PolicyActivationApproval> Approvals);
internal sealed record PolicyActivationApproval(
string ActorId,
DateTimeOffset ApprovedAt,
string? Comment);

View File

@@ -1,50 +1,50 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicyFindingsQuery(
string PolicyId,
IReadOnlyList<string> SbomIds,
IReadOnlyList<string> Statuses,
IReadOnlyList<string> Severities,
string? Cursor,
int? Page,
int? PageSize,
DateTimeOffset? Since);
internal sealed record PolicyFindingsPage(
IReadOnlyList<PolicyFindingDocument> Items,
string? NextCursor,
int? TotalCount);
internal sealed record PolicyFindingDocument(
string FindingId,
string Status,
PolicyFindingSeverity Severity,
string SbomId,
IReadOnlyList<string> AdvisoryIds,
PolicyFindingVexMetadata? Vex,
int PolicyVersion,
DateTimeOffset UpdatedAt,
string? RunId);
internal sealed record PolicyFindingSeverity(string Normalized, double? Score);
internal sealed record PolicyFindingVexMetadata(string? WinningStatementId, string? Source, string? Status);
internal sealed record PolicyFindingExplainResult(
string FindingId,
int PolicyVersion,
IReadOnlyList<PolicyFindingExplainStep> Steps,
IReadOnlyList<PolicyFindingExplainHint> SealedHints);
internal sealed record PolicyFindingExplainStep(
string Rule,
string? Status,
string? Action,
double? Score,
IReadOnlyDictionary<string, string> Inputs,
IReadOnlyDictionary<string, string>? Evidence);
internal sealed record PolicyFindingExplainHint(string Message);
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record PolicyFindingsQuery(
string PolicyId,
IReadOnlyList<string> SbomIds,
IReadOnlyList<string> Statuses,
IReadOnlyList<string> Severities,
string? Cursor,
int? Page,
int? PageSize,
DateTimeOffset? Since);
internal sealed record PolicyFindingsPage(
IReadOnlyList<PolicyFindingDocument> Items,
string? NextCursor,
int? TotalCount);
internal sealed record PolicyFindingDocument(
string FindingId,
string Status,
PolicyFindingSeverity Severity,
string SbomId,
IReadOnlyList<string> AdvisoryIds,
PolicyFindingVexMetadata? Vex,
int PolicyVersion,
DateTimeOffset UpdatedAt,
string? RunId);
internal sealed record PolicyFindingSeverity(string Normalized, double? Score);
internal sealed record PolicyFindingVexMetadata(string? WinningStatementId, string? Source, string? Status);
internal sealed record PolicyFindingExplainResult(
string FindingId,
int PolicyVersion,
IReadOnlyList<PolicyFindingExplainStep> Steps,
IReadOnlyList<PolicyFindingExplainHint> SealedHints);
internal sealed record PolicyFindingExplainStep(
string Rule,
string? Status,
string? Action,
double? Score,
IReadOnlyDictionary<string, string> Inputs,
IReadOnlyDictionary<string, string>? Evidence);
internal sealed record PolicyFindingExplainHint(string Message);

View File

@@ -1,63 +1,63 @@
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
// CLI-POLICY-27-003: Enhanced simulation modes
internal enum PolicySimulationMode
{
Quick,
Batch
}
/// <summary>
/// Input for policy simulation.
/// Per CLI-EXC-25-002, supports exception preview via WithExceptions/WithoutExceptions.
/// Per CLI-POLICY-27-003, supports mode (quick/batch), SBOM selectors, heatmap, and manifest download.
/// Per CLI-SIG-26-002, supports reachability overrides for vulnerability/package state and score.
/// </summary>
internal sealed record PolicySimulationInput(
int? BaseVersion,
int? CandidateVersion,
IReadOnlyList<string> SbomSet,
IReadOnlyDictionary<string, object?> Environment,
bool Explain,
IReadOnlyList<string>? WithExceptions = null,
IReadOnlyList<string>? WithoutExceptions = null,
PolicySimulationMode? Mode = null,
IReadOnlyList<string>? SbomSelectors = null,
bool IncludeHeatmap = false,
bool IncludeManifest = false,
IReadOnlyList<ReachabilityOverride>? ReachabilityOverrides = null);
internal sealed record PolicySimulationResult(
PolicySimulationDiff Diff,
string? ExplainUri,
PolicySimulationHeatmap? Heatmap = null,
string? ManifestDownloadUri = null,
string? ManifestDigest = null);
internal sealed record PolicySimulationDiff(
string? SchemaVersion,
int Added,
int Removed,
int Unchanged,
IReadOnlyDictionary<string, PolicySimulationSeverityDelta> BySeverity,
IReadOnlyList<PolicySimulationRuleDelta> RuleHits);
internal sealed record PolicySimulationSeverityDelta(int? Up, int? Down);
internal sealed record PolicySimulationRuleDelta(string RuleId, string RuleName, int? Up, int? Down);
// CLI-POLICY-27-003: Heatmap summary for quick severity visualization
internal sealed record PolicySimulationHeatmap(
int Critical,
int High,
int Medium,
int Low,
int Info,
IReadOnlyList<PolicySimulationHeatmapBucket> Buckets);
internal sealed record PolicySimulationHeatmapBucket(
string Label,
int Count,
string? Color);
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
// CLI-POLICY-27-003: Enhanced simulation modes
internal enum PolicySimulationMode
{
Quick,
Batch
}
/// <summary>
/// Input for policy simulation.
/// Per CLI-EXC-25-002, supports exception preview via WithExceptions/WithoutExceptions.
/// Per CLI-POLICY-27-003, supports mode (quick/batch), SBOM selectors, heatmap, and manifest download.
/// Per CLI-SIG-26-002, supports reachability overrides for vulnerability/package state and score.
/// </summary>
internal sealed record PolicySimulationInput(
int? BaseVersion,
int? CandidateVersion,
IReadOnlyList<string> SbomSet,
IReadOnlyDictionary<string, object?> Environment,
bool Explain,
IReadOnlyList<string>? WithExceptions = null,
IReadOnlyList<string>? WithoutExceptions = null,
PolicySimulationMode? Mode = null,
IReadOnlyList<string>? SbomSelectors = null,
bool IncludeHeatmap = false,
bool IncludeManifest = false,
IReadOnlyList<ReachabilityOverride>? ReachabilityOverrides = null);
internal sealed record PolicySimulationResult(
PolicySimulationDiff Diff,
string? ExplainUri,
PolicySimulationHeatmap? Heatmap = null,
string? ManifestDownloadUri = null,
string? ManifestDigest = null);
internal sealed record PolicySimulationDiff(
string? SchemaVersion,
int Added,
int Removed,
int Unchanged,
IReadOnlyDictionary<string, PolicySimulationSeverityDelta> BySeverity,
IReadOnlyList<PolicySimulationRuleDelta> RuleHits);
internal sealed record PolicySimulationSeverityDelta(int? Up, int? Down);
internal sealed record PolicySimulationRuleDelta(string RuleId, string RuleName, int? Up, int? Down);
// CLI-POLICY-27-003: Heatmap summary for quick severity visualization
internal sealed record PolicySimulationHeatmap(
int Critical,
int High,
int Medium,
int Low,
int Info,
IReadOnlyList<PolicySimulationHeatmapBucket> Buckets);
internal sealed record PolicySimulationHeatmapBucket(
string Label,
int Count,
string? Color);

View File

@@ -1,25 +1,25 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record RuntimePolicyEvaluationRequest(
string? Namespace,
IReadOnlyDictionary<string, string> Labels,
IReadOnlyList<string> Images);
internal sealed record RuntimePolicyEvaluationResult(
int TtlSeconds,
DateTimeOffset? ExpiresAtUtc,
string? PolicyRevision,
IReadOnlyDictionary<string, RuntimePolicyImageDecision> Decisions);
internal sealed record RuntimePolicyImageDecision(
string PolicyVerdict,
bool? Signed,
bool? HasSbomReferrers,
IReadOnlyList<string> Reasons,
RuntimePolicyRekorReference? Rekor,
IReadOnlyDictionary<string, object?> AdditionalProperties);
internal sealed record RuntimePolicyRekorReference(string? Uuid, string? Url, bool? Verified);
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models;
internal sealed record RuntimePolicyEvaluationRequest(
string? Namespace,
IReadOnlyDictionary<string, string> Labels,
IReadOnlyList<string> Images);
internal sealed record RuntimePolicyEvaluationResult(
int TtlSeconds,
DateTimeOffset? ExpiresAtUtc,
string? PolicyRevision,
IReadOnlyDictionary<string, RuntimePolicyImageDecision> Decisions);
internal sealed record RuntimePolicyImageDecision(
string PolicyVerdict,
bool? Signed,
bool? HasSbomReferrers,
IReadOnlyList<string> Reasons,
RuntimePolicyRekorReference? Rekor,
IReadOnlyDictionary<string, object?> AdditionalProperties);
internal sealed record RuntimePolicyRekorReference(string? Uuid, string? Url, bool? Verified);

View File

@@ -1,3 +1,3 @@
namespace StellaOps.Cli.Services.Models;
internal sealed record ScannerArtifactResult(string Path, long SizeBytes, bool FromCache);
namespace StellaOps.Cli.Services.Models;
internal sealed record ScannerArtifactResult(string Path, long SizeBytes, bool FromCache);

View File

@@ -1,36 +1,36 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace StellaOps.Cli.Services.Models;
internal sealed record TaskRunnerSimulationRequest(string Manifest, JsonObject? Inputs);
internal sealed record TaskRunnerSimulationResult(
string PlanHash,
TaskRunnerSimulationFailurePolicy FailurePolicy,
IReadOnlyList<TaskRunnerSimulationStep> Steps,
IReadOnlyList<TaskRunnerSimulationOutput> Outputs,
bool HasPendingApprovals);
internal sealed record TaskRunnerSimulationFailurePolicy(int MaxAttempts, int BackoffSeconds, bool ContinueOnError);
internal sealed record TaskRunnerSimulationStep(
string Id,
string TemplateId,
string Kind,
bool Enabled,
string Status,
string? StatusReason,
string? Uses,
string? ApprovalId,
string? GateMessage,
int? MaxParallel,
bool ContinueOnError,
IReadOnlyList<TaskRunnerSimulationStep> Children);
internal sealed record TaskRunnerSimulationOutput(
string Name,
string Type,
bool RequiresRuntimeValue,
string? PathExpression,
string? ValueExpression);
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace StellaOps.Cli.Services.Models;
internal sealed record TaskRunnerSimulationRequest(string Manifest, JsonObject? Inputs);
internal sealed record TaskRunnerSimulationResult(
string PlanHash,
TaskRunnerSimulationFailurePolicy FailurePolicy,
IReadOnlyList<TaskRunnerSimulationStep> Steps,
IReadOnlyList<TaskRunnerSimulationOutput> Outputs,
bool HasPendingApprovals);
internal sealed record TaskRunnerSimulationFailurePolicy(int MaxAttempts, int BackoffSeconds, bool ContinueOnError);
internal sealed record TaskRunnerSimulationStep(
string Id,
string TemplateId,
string Kind,
bool Enabled,
string Status,
string? StatusReason,
string? Uses,
string? ApprovalId,
string? GateMessage,
int? MaxParallel,
bool ContinueOnError,
IReadOnlyList<TaskRunnerSimulationStep> Children);
internal sealed record TaskRunnerSimulationOutput(
string Name,
string Type,
bool RequiresRuntimeValue,
string? PathExpression,
string? ValueExpression);

View File

@@ -1,27 +1,27 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class JobRunResponse
{
public Guid RunId { get; set; }
public string Kind { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string Trigger { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? StartedAt { get; set; }
public DateTimeOffset? CompletedAt { get; set; }
public string? Error { get; set; }
public TimeSpan? Duration { get; set; }
public IReadOnlyDictionary<string, object?> Parameters { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
}
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class JobRunResponse
{
public Guid RunId { get; set; }
public string Kind { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public string Trigger { get; set; } = string.Empty;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? StartedAt { get; set; }
public DateTimeOffset? CompletedAt { get; set; }
public string? Error { get; set; }
public TimeSpan? Duration { get; set; }
public IReadOnlyDictionary<string, object?> Parameters { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
}

View File

@@ -1,10 +1,10 @@
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class JobTriggerRequest
{
public string Trigger { get; set; } = "cli";
public Dictionary<string, object?> Parameters { get; set; } = new(StringComparer.Ordinal);
}
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class JobTriggerRequest
{
public string Trigger { get; set; } = "cli";
public Dictionary<string, object?> Parameters { get; set; } = new(StringComparer.Ordinal);
}

View File

@@ -1,103 +1,103 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class OfflineKitBundleDescriptorTransport
{
public string? BundleId { get; set; }
public string? BundleName { get; set; }
public string? BundleSha256 { get; set; }
public long BundleSize { get; set; }
public string? BundleUrl { get; set; }
public string? BundlePath { get; set; }
public string? BundleSignatureName { get; set; }
public string? BundleSignatureUrl { get; set; }
public string? BundleSignaturePath { get; set; }
public string? ManifestName { get; set; }
public string? ManifestSha256 { get; set; }
public long? ManifestSize { get; set; }
public string? ManifestUrl { get; set; }
public string? ManifestPath { get; set; }
public string? ManifestSignatureName { get; set; }
public string? ManifestSignatureUrl { get; set; }
public string? ManifestSignaturePath { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public string? Channel { get; set; }
public string? Kind { get; set; }
public bool? IsDelta { get; set; }
public string? BaseBundleId { get; set; }
}
internal sealed class OfflineKitStatusBundleTransport
{
public string? BundleId { get; set; }
public string? Channel { get; set; }
public string? Kind { get; set; }
public bool? IsDelta { get; set; }
public string? BaseBundleId { get; set; }
public string? BundleSha256 { get; set; }
public long? BundleSize { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public DateTimeOffset? ImportedAt { get; set; }
}
internal sealed class OfflineKitStatusTransport
{
public OfflineKitStatusBundleTransport? Current { get; set; }
public List<OfflineKitComponentStatusTransport>? Components { get; set; }
}
internal sealed class OfflineKitComponentStatusTransport
{
public string? Name { get; set; }
public string? Version { get; set; }
public string? Digest { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public long? SizeBytes { get; set; }
}
internal sealed class OfflineKitImportResponseTransport
{
public string? ImportId { get; set; }
public string? Status { get; set; }
public DateTimeOffset? SubmittedAt { get; set; }
public string? Message { get; set; }
}
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class OfflineKitBundleDescriptorTransport
{
public string? BundleId { get; set; }
public string? BundleName { get; set; }
public string? BundleSha256 { get; set; }
public long BundleSize { get; set; }
public string? BundleUrl { get; set; }
public string? BundlePath { get; set; }
public string? BundleSignatureName { get; set; }
public string? BundleSignatureUrl { get; set; }
public string? BundleSignaturePath { get; set; }
public string? ManifestName { get; set; }
public string? ManifestSha256 { get; set; }
public long? ManifestSize { get; set; }
public string? ManifestUrl { get; set; }
public string? ManifestPath { get; set; }
public string? ManifestSignatureName { get; set; }
public string? ManifestSignatureUrl { get; set; }
public string? ManifestSignaturePath { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public string? Channel { get; set; }
public string? Kind { get; set; }
public bool? IsDelta { get; set; }
public string? BaseBundleId { get; set; }
}
internal sealed class OfflineKitStatusBundleTransport
{
public string? BundleId { get; set; }
public string? Channel { get; set; }
public string? Kind { get; set; }
public bool? IsDelta { get; set; }
public string? BaseBundleId { get; set; }
public string? BundleSha256 { get; set; }
public long? BundleSize { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public DateTimeOffset? ImportedAt { get; set; }
}
internal sealed class OfflineKitStatusTransport
{
public OfflineKitStatusBundleTransport? Current { get; set; }
public List<OfflineKitComponentStatusTransport>? Components { get; set; }
}
internal sealed class OfflineKitComponentStatusTransport
{
public string? Name { get; set; }
public string? Version { get; set; }
public string? Digest { get; set; }
public DateTimeOffset? CapturedAt { get; set; }
public long? SizeBytes { get; set; }
}
internal sealed class OfflineKitImportResponseTransport
{
public string? ImportId { get; set; }
public string? Status { get; set; }
public DateTimeOffset? SubmittedAt { get; set; }
public string? Message { get; set; }
}

View File

@@ -1,52 +1,52 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicyActivationRequestDocument
{
public string? Comment { get; set; }
public bool? RunNow { get; set; }
public DateTimeOffset? ScheduledAt { get; set; }
public string? Priority { get; set; }
public bool? Rollback { get; set; }
public string? IncidentId { get; set; }
}
internal sealed class PolicyActivationResponseDocument
{
public string? Status { get; set; }
public PolicyActivationRevisionDocument? Revision { get; set; }
}
internal sealed class PolicyActivationRevisionDocument
{
public string? PackId { get; set; }
public int? Version { get; set; }
public string? Status { get; set; }
public bool? RequiresTwoPersonApproval { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public DateTimeOffset? ActivatedAt { get; set; }
public List<PolicyActivationApprovalDocument>? Approvals { get; set; }
}
internal sealed class PolicyActivationApprovalDocument
{
public string? ActorId { get; set; }
public DateTimeOffset? ApprovedAt { get; set; }
public string? Comment { get; set; }
}
using System;
using System.Collections.Generic;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicyActivationRequestDocument
{
public string? Comment { get; set; }
public bool? RunNow { get; set; }
public DateTimeOffset? ScheduledAt { get; set; }
public string? Priority { get; set; }
public bool? Rollback { get; set; }
public string? IncidentId { get; set; }
}
internal sealed class PolicyActivationResponseDocument
{
public string? Status { get; set; }
public PolicyActivationRevisionDocument? Revision { get; set; }
}
internal sealed class PolicyActivationRevisionDocument
{
public string? PackId { get; set; }
public int? Version { get; set; }
public string? Status { get; set; }
public bool? RequiresTwoPersonApproval { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public DateTimeOffset? ActivatedAt { get; set; }
public List<PolicyActivationApprovalDocument>? Approvals { get; set; }
}
internal sealed class PolicyActivationApprovalDocument
{
public string? ActorId { get; set; }
public DateTimeOffset? ApprovedAt { get; set; }
public string? Comment { get; set; }
}

View File

@@ -1,82 +1,82 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicyFindingsResponseDocument
{
public List<PolicyFindingDocumentDocument>? Items { get; set; }
public string? NextCursor { get; set; }
public int? TotalCount { get; set; }
}
internal sealed class PolicyFindingDocumentDocument
{
public string? FindingId { get; set; }
public string? Status { get; set; }
public PolicyFindingSeverityDocument? Severity { get; set; }
public string? SbomId { get; set; }
public List<string>? AdvisoryIds { get; set; }
public PolicyFindingVexDocument? Vex { get; set; }
public int? PolicyVersion { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
public string? RunId { get; set; }
}
internal sealed class PolicyFindingSeverityDocument
{
public string? Normalized { get; set; }
public double? Score { get; set; }
}
internal sealed class PolicyFindingVexDocument
{
public string? WinningStatementId { get; set; }
public string? Source { get; set; }
public string? Status { get; set; }
}
internal sealed class PolicyFindingExplainResponseDocument
{
public string? FindingId { get; set; }
public int? PolicyVersion { get; set; }
public List<PolicyFindingExplainStepDocument>? Steps { get; set; }
public List<PolicyFindingExplainHintDocument>? SealedHints { get; set; }
}
internal sealed class PolicyFindingExplainStepDocument
{
public string? Rule { get; set; }
public string? Status { get; set; }
public string? Action { get; set; }
public double? Score { get; set; }
public Dictionary<string, JsonElement>? Inputs { get; set; }
public Dictionary<string, JsonElement>? Evidence { get; set; }
}
internal sealed class PolicyFindingExplainHintDocument
{
public string? Message { get; set; }
}
using System;
using System.Collections.Generic;
using System.Text.Json;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicyFindingsResponseDocument
{
public List<PolicyFindingDocumentDocument>? Items { get; set; }
public string? NextCursor { get; set; }
public int? TotalCount { get; set; }
}
internal sealed class PolicyFindingDocumentDocument
{
public string? FindingId { get; set; }
public string? Status { get; set; }
public PolicyFindingSeverityDocument? Severity { get; set; }
public string? SbomId { get; set; }
public List<string>? AdvisoryIds { get; set; }
public PolicyFindingVexDocument? Vex { get; set; }
public int? PolicyVersion { get; set; }
public DateTimeOffset? UpdatedAt { get; set; }
public string? RunId { get; set; }
}
internal sealed class PolicyFindingSeverityDocument
{
public string? Normalized { get; set; }
public double? Score { get; set; }
}
internal sealed class PolicyFindingVexDocument
{
public string? WinningStatementId { get; set; }
public string? Source { get; set; }
public string? Status { get; set; }
}
internal sealed class PolicyFindingExplainResponseDocument
{
public string? FindingId { get; set; }
public int? PolicyVersion { get; set; }
public List<PolicyFindingExplainStepDocument>? Steps { get; set; }
public List<PolicyFindingExplainHintDocument>? SealedHints { get; set; }
}
internal sealed class PolicyFindingExplainStepDocument
{
public string? Rule { get; set; }
public string? Status { get; set; }
public string? Action { get; set; }
public double? Score { get; set; }
public Dictionary<string, JsonElement>? Inputs { get; set; }
public Dictionary<string, JsonElement>? Evidence { get; set; }
}
internal sealed class PolicyFindingExplainHintDocument
{
public string? Message { get; set; }
}

View File

@@ -1,98 +1,98 @@
using System.Collections.Generic;
using System.Text.Json;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicySimulationRequestDocument
{
public int? BaseVersion { get; set; }
public int? CandidateVersion { get; set; }
public IReadOnlyList<string>? SbomSet { get; set; }
public Dictionary<string, JsonElement>? Env { get; set; }
public bool? Explain { get; set; }
// CLI-POLICY-27-003: Enhanced simulation options
public string? Mode { get; set; }
public IReadOnlyList<string>? SbomSelectors { get; set; }
public bool? IncludeHeatmap { get; set; }
public bool? IncludeManifest { get; set; }
}
internal sealed class PolicySimulationResponseDocument
{
public PolicySimulationDiffDocument? Diff { get; set; }
public string? ExplainUri { get; set; }
// CLI-POLICY-27-003: Enhanced response fields
public PolicySimulationHeatmapDocument? Heatmap { get; set; }
public string? ManifestDownloadUri { get; set; }
public string? ManifestDigest { get; set; }
}
internal sealed class PolicySimulationDiffDocument
{
public string? SchemaVersion { get; set; }
public int? Added { get; set; }
public int? Removed { get; set; }
public int? Unchanged { get; set; }
public Dictionary<string, PolicySimulationSeverityDeltaDocument>? BySeverity { get; set; }
public List<PolicySimulationRuleDeltaDocument>? RuleHits { get; set; }
}
internal sealed class PolicySimulationSeverityDeltaDocument
{
public int? Up { get; set; }
public int? Down { get; set; }
}
internal sealed class PolicySimulationRuleDeltaDocument
{
public string? RuleId { get; set; }
public string? RuleName { get; set; }
public int? Up { get; set; }
public int? Down { get; set; }
}
// CLI-POLICY-27-003: Heatmap response documents
internal sealed class PolicySimulationHeatmapDocument
{
public int? Critical { get; set; }
public int? High { get; set; }
public int? Medium { get; set; }
public int? Low { get; set; }
public int? Info { get; set; }
public List<PolicySimulationHeatmapBucketDocument>? Buckets { get; set; }
}
internal sealed class PolicySimulationHeatmapBucketDocument
{
public string? Label { get; set; }
public int? Count { get; set; }
public string? Color { get; set; }
}
using System.Collections.Generic;
using System.Text.Json;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class PolicySimulationRequestDocument
{
public int? BaseVersion { get; set; }
public int? CandidateVersion { get; set; }
public IReadOnlyList<string>? SbomSet { get; set; }
public Dictionary<string, JsonElement>? Env { get; set; }
public bool? Explain { get; set; }
// CLI-POLICY-27-003: Enhanced simulation options
public string? Mode { get; set; }
public IReadOnlyList<string>? SbomSelectors { get; set; }
public bool? IncludeHeatmap { get; set; }
public bool? IncludeManifest { get; set; }
}
internal sealed class PolicySimulationResponseDocument
{
public PolicySimulationDiffDocument? Diff { get; set; }
public string? ExplainUri { get; set; }
// CLI-POLICY-27-003: Enhanced response fields
public PolicySimulationHeatmapDocument? Heatmap { get; set; }
public string? ManifestDownloadUri { get; set; }
public string? ManifestDigest { get; set; }
}
internal sealed class PolicySimulationDiffDocument
{
public string? SchemaVersion { get; set; }
public int? Added { get; set; }
public int? Removed { get; set; }
public int? Unchanged { get; set; }
public Dictionary<string, PolicySimulationSeverityDeltaDocument>? BySeverity { get; set; }
public List<PolicySimulationRuleDeltaDocument>? RuleHits { get; set; }
}
internal sealed class PolicySimulationSeverityDeltaDocument
{
public int? Up { get; set; }
public int? Down { get; set; }
}
internal sealed class PolicySimulationRuleDeltaDocument
{
public string? RuleId { get; set; }
public string? RuleName { get; set; }
public int? Up { get; set; }
public int? Down { get; set; }
}
// CLI-POLICY-27-003: Heatmap response documents
internal sealed class PolicySimulationHeatmapDocument
{
public int? Critical { get; set; }
public int? High { get; set; }
public int? Medium { get; set; }
public int? Low { get; set; }
public int? Info { get; set; }
public List<PolicySimulationHeatmapBucketDocument>? Buckets { get; set; }
}
internal sealed class PolicySimulationHeatmapBucketDocument
{
public string? Label { get; set; }
public int? Count { get; set; }
public string? Color { get; set; }
}

View File

@@ -1,184 +1,184 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models.Transport;
/// <summary>
/// RFC 7807 Problem Details response.
/// </summary>
internal sealed class ProblemDocument
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("detail")]
public string? Detail { get; set; }
[JsonPropertyName("status")]
public int? Status { get; set; }
[JsonPropertyName("instance")]
public string? Instance { get; set; }
[JsonPropertyName("extensions")]
public Dictionary<string, object?>? Extensions { get; set; }
}
/// <summary>
/// Standardized API error envelope with error.code and trace_id.
/// CLI-SDK-62-002: Supports surfacing structured error information.
/// </summary>
internal sealed class ApiErrorEnvelope
{
/// <summary>
/// Error details.
/// </summary>
[JsonPropertyName("error")]
public ApiErrorDetail? Error { get; set; }
/// <summary>
/// Distributed trace identifier.
/// </summary>
[JsonPropertyName("trace_id")]
public string? TraceId { get; set; }
/// <summary>
/// Request identifier.
/// </summary>
[JsonPropertyName("request_id")]
public string? RequestId { get; set; }
/// <summary>
/// Timestamp of the error.
/// </summary>
[JsonPropertyName("timestamp")]
public string? Timestamp { get; set; }
}
/// <summary>
/// Error detail within the standardized envelope.
/// </summary>
internal sealed class ApiErrorDetail
{
/// <summary>
/// Machine-readable error code (e.g., "ERR_AUTH_INVALID_SCOPE").
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; set; }
/// <summary>
/// Human-readable error message.
/// </summary>
[JsonPropertyName("message")]
public string? Message { get; set; }
/// <summary>
/// Detailed description of the error.
/// </summary>
[JsonPropertyName("detail")]
public string? Detail { get; set; }
/// <summary>
/// Target of the error (field name, resource identifier).
/// </summary>
[JsonPropertyName("target")]
public string? Target { get; set; }
/// <summary>
/// Inner errors for nested error details.
/// </summary>
[JsonPropertyName("inner_errors")]
public IReadOnlyList<ApiErrorDetail>? InnerErrors { get; set; }
/// <summary>
/// Additional metadata about the error.
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, object?>? Metadata { get; set; }
/// <summary>
/// Help URL for more information.
/// </summary>
[JsonPropertyName("help_url")]
public string? HelpUrl { get; set; }
/// <summary>
/// Retry-after hint in seconds (for rate limiting).
/// </summary>
[JsonPropertyName("retry_after")]
public int? RetryAfter { get; set; }
}
/// <summary>
/// Parsed API error result combining multiple error formats.
/// </summary>
internal sealed class ParsedApiError
{
/// <summary>
/// Error code (from envelope, problem, or HTTP status).
/// </summary>
public required string Code { get; init; }
/// <summary>
/// Error message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Detailed error description.
/// </summary>
public string? Detail { get; init; }
/// <summary>
/// Trace ID for distributed tracing.
/// </summary>
public string? TraceId { get; init; }
/// <summary>
/// Request ID.
/// </summary>
public string? RequestId { get; init; }
/// <summary>
/// HTTP status code.
/// </summary>
public int HttpStatus { get; init; }
/// <summary>
/// Target of the error.
/// </summary>
public string? Target { get; init; }
/// <summary>
/// Help URL for more information.
/// </summary>
public string? HelpUrl { get; init; }
/// <summary>
/// Retry-after hint in seconds.
/// </summary>
public int? RetryAfter { get; init; }
/// <summary>
/// Inner errors.
/// </summary>
public IReadOnlyList<ApiErrorDetail>? InnerErrors { get; init; }
/// <summary>
/// Additional metadata.
/// </summary>
public Dictionary<string, object?>? Metadata { get; init; }
/// <summary>
/// Original problem document if parsed.
/// </summary>
public ProblemDocument? ProblemDocument { get; init; }
/// <summary>
/// Original error envelope if parsed.
/// </summary>
public ApiErrorEnvelope? ErrorEnvelope { get; init; }
}
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models.Transport;
/// <summary>
/// RFC 7807 Problem Details response.
/// </summary>
internal sealed class ProblemDocument
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("detail")]
public string? Detail { get; set; }
[JsonPropertyName("status")]
public int? Status { get; set; }
[JsonPropertyName("instance")]
public string? Instance { get; set; }
[JsonPropertyName("extensions")]
public Dictionary<string, object?>? Extensions { get; set; }
}
/// <summary>
/// Standardized API error envelope with error.code and trace_id.
/// CLI-SDK-62-002: Supports surfacing structured error information.
/// </summary>
internal sealed class ApiErrorEnvelope
{
/// <summary>
/// Error details.
/// </summary>
[JsonPropertyName("error")]
public ApiErrorDetail? Error { get; set; }
/// <summary>
/// Distributed trace identifier.
/// </summary>
[JsonPropertyName("trace_id")]
public string? TraceId { get; set; }
/// <summary>
/// Request identifier.
/// </summary>
[JsonPropertyName("request_id")]
public string? RequestId { get; set; }
/// <summary>
/// Timestamp of the error.
/// </summary>
[JsonPropertyName("timestamp")]
public string? Timestamp { get; set; }
}
/// <summary>
/// Error detail within the standardized envelope.
/// </summary>
internal sealed class ApiErrorDetail
{
/// <summary>
/// Machine-readable error code (e.g., "ERR_AUTH_INVALID_SCOPE").
/// </summary>
[JsonPropertyName("code")]
public string? Code { get; set; }
/// <summary>
/// Human-readable error message.
/// </summary>
[JsonPropertyName("message")]
public string? Message { get; set; }
/// <summary>
/// Detailed description of the error.
/// </summary>
[JsonPropertyName("detail")]
public string? Detail { get; set; }
/// <summary>
/// Target of the error (field name, resource identifier).
/// </summary>
[JsonPropertyName("target")]
public string? Target { get; set; }
/// <summary>
/// Inner errors for nested error details.
/// </summary>
[JsonPropertyName("inner_errors")]
public IReadOnlyList<ApiErrorDetail>? InnerErrors { get; set; }
/// <summary>
/// Additional metadata about the error.
/// </summary>
[JsonPropertyName("metadata")]
public Dictionary<string, object?>? Metadata { get; set; }
/// <summary>
/// Help URL for more information.
/// </summary>
[JsonPropertyName("help_url")]
public string? HelpUrl { get; set; }
/// <summary>
/// Retry-after hint in seconds (for rate limiting).
/// </summary>
[JsonPropertyName("retry_after")]
public int? RetryAfter { get; set; }
}
/// <summary>
/// Parsed API error result combining multiple error formats.
/// </summary>
internal sealed class ParsedApiError
{
/// <summary>
/// Error code (from envelope, problem, or HTTP status).
/// </summary>
public required string Code { get; init; }
/// <summary>
/// Error message.
/// </summary>
public required string Message { get; init; }
/// <summary>
/// Detailed error description.
/// </summary>
public string? Detail { get; init; }
/// <summary>
/// Trace ID for distributed tracing.
/// </summary>
public string? TraceId { get; init; }
/// <summary>
/// Request ID.
/// </summary>
public string? RequestId { get; init; }
/// <summary>
/// HTTP status code.
/// </summary>
public int HttpStatus { get; init; }
/// <summary>
/// Target of the error.
/// </summary>
public string? Target { get; init; }
/// <summary>
/// Help URL for more information.
/// </summary>
public string? HelpUrl { get; init; }
/// <summary>
/// Retry-after hint in seconds.
/// </summary>
public int? RetryAfter { get; init; }
/// <summary>
/// Inner errors.
/// </summary>
public IReadOnlyList<ApiErrorDetail>? InnerErrors { get; init; }
/// <summary>
/// Additional metadata.
/// </summary>
public Dictionary<string, object?>? Metadata { get; init; }
/// <summary>
/// Original problem document if parsed.
/// </summary>
public ProblemDocument? ProblemDocument { get; init; }
/// <summary>
/// Original error envelope if parsed.
/// </summary>
public ApiErrorEnvelope? ErrorEnvelope { get; init; }
}

View File

@@ -1,72 +1,72 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class RuntimePolicyEvaluationRequestDocument
{
[JsonPropertyName("namespace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Namespace { get; set; }
[JsonPropertyName("labels")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string>? Labels { get; set; }
[JsonPropertyName("images")]
public List<string> Images { get; set; } = new();
}
internal sealed class RuntimePolicyEvaluationResponseDocument
{
[JsonPropertyName("ttlSeconds")]
public int? TtlSeconds { get; set; }
[JsonPropertyName("expiresAtUtc")]
public DateTimeOffset? ExpiresAtUtc { get; set; }
[JsonPropertyName("policyRevision")]
public string? PolicyRevision { get; set; }
[JsonPropertyName("results")]
public Dictionary<string, RuntimePolicyEvaluationImageDocument>? Results { get; set; }
}
internal sealed class RuntimePolicyEvaluationImageDocument
{
[JsonPropertyName("policyVerdict")]
public string? PolicyVerdict { get; set; }
[JsonPropertyName("signed")]
public bool? Signed { get; set; }
[JsonPropertyName("hasSbomReferrers")]
public bool? HasSbomReferrers { get; set; }
// Legacy field kept for pre-contract-sync services.
[JsonPropertyName("hasSbom")]
public bool? HasSbomLegacy { get; set; }
[JsonPropertyName("reasons")]
public List<string>? Reasons { get; set; }
[JsonPropertyName("rekor")]
public RuntimePolicyRekorDocument? Rekor { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
}
internal sealed class RuntimePolicyRekorDocument
{
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("verified")]
public bool? Verified { get; set; }
}
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class RuntimePolicyEvaluationRequestDocument
{
[JsonPropertyName("namespace")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Namespace { get; set; }
[JsonPropertyName("labels")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary<string, string>? Labels { get; set; }
[JsonPropertyName("images")]
public List<string> Images { get; set; } = new();
}
internal sealed class RuntimePolicyEvaluationResponseDocument
{
[JsonPropertyName("ttlSeconds")]
public int? TtlSeconds { get; set; }
[JsonPropertyName("expiresAtUtc")]
public DateTimeOffset? ExpiresAtUtc { get; set; }
[JsonPropertyName("policyRevision")]
public string? PolicyRevision { get; set; }
[JsonPropertyName("results")]
public Dictionary<string, RuntimePolicyEvaluationImageDocument>? Results { get; set; }
}
internal sealed class RuntimePolicyEvaluationImageDocument
{
[JsonPropertyName("policyVerdict")]
public string? PolicyVerdict { get; set; }
[JsonPropertyName("signed")]
public bool? Signed { get; set; }
[JsonPropertyName("hasSbomReferrers")]
public bool? HasSbomReferrers { get; set; }
// Legacy field kept for pre-contract-sync services.
[JsonPropertyName("hasSbom")]
public bool? HasSbomLegacy { get; set; }
[JsonPropertyName("reasons")]
public List<string>? Reasons { get; set; }
[JsonPropertyName("rekor")]
public RuntimePolicyRekorDocument? Rekor { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
}
internal sealed class RuntimePolicyRekorDocument
{
[JsonPropertyName("uuid")]
public string? Uuid { get; set; }
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("verified")]
public bool? Verified { get; set; }
}

View File

@@ -1,73 +1,73 @@
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class TaskRunnerSimulationRequestDocument
{
public string Manifest { get; set; } = string.Empty;
public JsonObject? Inputs { get; set; }
}
internal sealed class TaskRunnerSimulationResponseDocument
{
public string PlanHash { get; set; } = string.Empty;
public TaskRunnerSimulationFailurePolicyDocument? FailurePolicy { get; set; }
public List<TaskRunnerSimulationStepDocument>? Steps { get; set; }
public List<TaskRunnerSimulationOutputDocument>? Outputs { get; set; }
public bool HasPendingApprovals { get; set; }
}
internal sealed class TaskRunnerSimulationFailurePolicyDocument
{
public int MaxAttempts { get; set; }
public int BackoffSeconds { get; set; }
public bool ContinueOnError { get; set; }
}
internal sealed class TaskRunnerSimulationStepDocument
{
public string Id { get; set; } = string.Empty;
public string TemplateId { get; set; } = string.Empty;
public string Kind { get; set; } = string.Empty;
public bool Enabled { get; set; }
public string Status { get; set; } = string.Empty;
public string? StatusReason { get; set; }
public string? Uses { get; set; }
public string? ApprovalId { get; set; }
public string? GateMessage { get; set; }
public int? MaxParallel { get; set; }
public bool ContinueOnError { get; set; }
public List<TaskRunnerSimulationStepDocument>? Children { get; set; }
}
internal sealed class TaskRunnerSimulationOutputDocument
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool RequiresRuntimeValue { get; set; }
public string? PathExpression { get; set; }
public string? ValueExpression { get; set; }
}
using System.Collections.Generic;
using System.Text.Json.Nodes;
namespace StellaOps.Cli.Services.Models.Transport;
internal sealed class TaskRunnerSimulationRequestDocument
{
public string Manifest { get; set; } = string.Empty;
public JsonObject? Inputs { get; set; }
}
internal sealed class TaskRunnerSimulationResponseDocument
{
public string PlanHash { get; set; } = string.Empty;
public TaskRunnerSimulationFailurePolicyDocument? FailurePolicy { get; set; }
public List<TaskRunnerSimulationStepDocument>? Steps { get; set; }
public List<TaskRunnerSimulationOutputDocument>? Outputs { get; set; }
public bool HasPendingApprovals { get; set; }
}
internal sealed class TaskRunnerSimulationFailurePolicyDocument
{
public int MaxAttempts { get; set; }
public int BackoffSeconds { get; set; }
public bool ContinueOnError { get; set; }
}
internal sealed class TaskRunnerSimulationStepDocument
{
public string Id { get; set; } = string.Empty;
public string TemplateId { get; set; } = string.Empty;
public string Kind { get; set; } = string.Empty;
public bool Enabled { get; set; }
public string Status { get; set; } = string.Empty;
public string? StatusReason { get; set; }
public string? Uses { get; set; }
public string? ApprovalId { get; set; }
public string? GateMessage { get; set; }
public int? MaxParallel { get; set; }
public bool ContinueOnError { get; set; }
public List<TaskRunnerSimulationStepDocument>? Children { get; set; }
}
internal sealed class TaskRunnerSimulationOutputDocument
{
public string Name { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public bool RequiresRuntimeValue { get; set; }
public string? PathExpression { get; set; }
public string? ValueExpression { get; set; }
}

View File

@@ -1,18 +1,18 @@
using System;
using System.Net;
namespace StellaOps.Cli.Services;
internal sealed class PolicyApiException : Exception
{
public PolicyApiException(string message, HttpStatusCode statusCode, string? errorCode, Exception? innerException = null)
: base(message, innerException)
{
StatusCode = statusCode;
ErrorCode = errorCode;
}
public HttpStatusCode StatusCode { get; }
public string? ErrorCode { get; }
}
using System;
using System.Net;
namespace StellaOps.Cli.Services;
internal sealed class PolicyApiException : Exception
{
public PolicyApiException(string message, HttpStatusCode statusCode, string? errorCode, Exception? innerException = null)
: base(message, innerException)
{
StatusCode = statusCode;
ErrorCode = errorCode;
}
public HttpStatusCode StatusCode { get; }
public string? ErrorCode { get; }
}

View File

@@ -1,3 +1,3 @@
namespace StellaOps.Cli.Services;
namespace StellaOps.Cli.Services;
internal sealed record ScannerExecutionResult(int ExitCode, string ResultsPath, string RunMetadataPath);

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -11,86 +11,86 @@ using System.Text.Json;
namespace StellaOps.Cli.Services;
internal sealed class ScannerExecutor : IScannerExecutor
{
private readonly ILogger<ScannerExecutor> _logger;
public ScannerExecutor(ILogger<ScannerExecutor> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<ScannerExecutionResult> RunAsync(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> arguments,
bool verbose,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(targetDirectory))
{
throw new ArgumentException("Target directory must be provided.", nameof(targetDirectory));
}
runner = string.IsNullOrWhiteSpace(runner) ? "docker" : runner.Trim().ToLowerInvariant();
entry = entry?.Trim() ?? string.Empty;
var normalizedTarget = Path.GetFullPath(targetDirectory);
if (!Directory.Exists(normalizedTarget))
{
throw new DirectoryNotFoundException($"Scan target directory '{normalizedTarget}' does not exist.");
}
resultsDirectory = string.IsNullOrWhiteSpace(resultsDirectory)
? Path.Combine(Directory.GetCurrentDirectory(), "scan-results")
: Path.GetFullPath(resultsDirectory);
Directory.CreateDirectory(resultsDirectory);
{
private readonly ILogger<ScannerExecutor> _logger;
public ScannerExecutor(ILogger<ScannerExecutor> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<ScannerExecutionResult> RunAsync(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> arguments,
bool verbose,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(targetDirectory))
{
throw new ArgumentException("Target directory must be provided.", nameof(targetDirectory));
}
runner = string.IsNullOrWhiteSpace(runner) ? "docker" : runner.Trim().ToLowerInvariant();
entry = entry?.Trim() ?? string.Empty;
var normalizedTarget = Path.GetFullPath(targetDirectory);
if (!Directory.Exists(normalizedTarget))
{
throw new DirectoryNotFoundException($"Scan target directory '{normalizedTarget}' does not exist.");
}
resultsDirectory = string.IsNullOrWhiteSpace(resultsDirectory)
? Path.Combine(Directory.GetCurrentDirectory(), "scan-results")
: Path.GetFullPath(resultsDirectory);
Directory.CreateDirectory(resultsDirectory);
var executionTimestamp = DateTimeOffset.UtcNow;
var baselineFiles = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
var baseline = new HashSet<string>(baselineFiles, StringComparer.OrdinalIgnoreCase);
var startInfo = BuildProcessStartInfo(runner, entry, normalizedTarget, resultsDirectory, arguments);
using var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
stdout.Add(args.Data);
if (verbose)
{
_logger.LogInformation("[scan] {Line}", args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
stderr.Add(args.Data);
_logger.LogError("[scan] {Line}", args.Data);
};
_logger.LogInformation("Launching scanner via {Runner} (entry: {Entry})...", runner, entry);
if (!process.Start())
{
throw new InvalidOperationException("Failed to start scanner process.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
using var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
var stdout = new List<string>();
var stderr = new List<string>();
process.OutputDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
stdout.Add(args.Data);
if (verbose)
{
_logger.LogInformation("[scan] {Line}", args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
stderr.Add(args.Data);
_logger.LogError("[scan] {Line}", args.Data);
};
_logger.LogInformation("Launching scanner via {Runner} (entry: {Entry})...", runner, entry);
if (!process.Start())
{
throw new InvalidOperationException("Failed to start scanner process.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
var completionTimestamp = DateTimeOffset.UtcNow;
@@ -98,11 +98,11 @@ internal sealed class ScannerExecutor : IScannerExecutor
{
_logger.LogInformation("Scanner completed successfully.");
}
else
{
_logger.LogWarning("Scanner exited with code {Code}.", process.ExitCode);
}
else
{
_logger.LogWarning("Scanner exited with code {Code}.", process.ExitCode);
}
var resultsPath = ResolveResultsPath(resultsDirectory, executionTimestamp, baseline);
if (string.IsNullOrWhiteSpace(resultsPath))
{
@@ -124,161 +124,161 @@ internal sealed class ScannerExecutor : IScannerExecutor
return new ScannerExecutionResult(process.ExitCode, resultsPath, metadataPath);
}
private ProcessStartInfo BuildProcessStartInfo(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> args)
{
return runner switch
{
"self" or "native" => BuildNativeStartInfo(entry, args),
"dotnet" => BuildDotNetStartInfo(entry, args),
"docker" => BuildDockerStartInfo(entry, targetDirectory, resultsDirectory, args),
_ => BuildCustomRunnerStartInfo(runner, entry, args)
};
}
private static ProcessStartInfo BuildNativeStartInfo(string binaryPath, IReadOnlyList<string> args)
{
if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
{
throw new FileNotFoundException("Scanner entrypoint not found.", binaryPath);
}
var startInfo = new ProcessStartInfo
{
FileName = binaryPath,
WorkingDirectory = Directory.GetCurrentDirectory()
};
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildDotNetStartInfo(string binaryPath, IReadOnlyList<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = Directory.GetCurrentDirectory()
};
startInfo.ArgumentList.Add(binaryPath);
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildDockerStartInfo(string image, string targetDirectory, string resultsDirectory, IReadOnlyList<string> args)
{
if (string.IsNullOrWhiteSpace(image))
{
throw new ArgumentException("Docker image must be provided when runner is 'docker'.", nameof(image));
}
var cwd = Directory.GetCurrentDirectory();
var startInfo = new ProcessStartInfo
{
FileName = "docker",
WorkingDirectory = cwd
};
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--rm");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{cwd}:{cwd}");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{targetDirectory}:/scan-target:ro");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{resultsDirectory}:/scan-results");
startInfo.ArgumentList.Add("-w");
startInfo.ArgumentList.Add(cwd);
startInfo.ArgumentList.Add(image);
startInfo.ArgumentList.Add("--target");
startInfo.ArgumentList.Add("/scan-target");
startInfo.ArgumentList.Add("--output");
startInfo.ArgumentList.Add("/scan-results/scan.json");
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildCustomRunnerStartInfo(string runner, string entry, IReadOnlyList<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = runner,
WorkingDirectory = Directory.GetCurrentDirectory()
};
if (!string.IsNullOrWhiteSpace(entry))
{
startInfo.ArgumentList.Add(entry);
}
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static string ResolveResultsPath(string resultsDirectory, DateTimeOffset startTimestamp, HashSet<string> baseline)
{
var candidates = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
string? newest = null;
DateTimeOffset newestTimestamp = startTimestamp;
foreach (var candidate in candidates)
{
if (baseline.Contains(candidate))
{
continue;
}
var info = new FileInfo(candidate);
if (info.LastWriteTimeUtc >= newestTimestamp)
{
newestTimestamp = info.LastWriteTimeUtc;
newest = candidate;
}
}
return newest ?? string.Empty;
}
private ProcessStartInfo BuildProcessStartInfo(
string runner,
string entry,
string targetDirectory,
string resultsDirectory,
IReadOnlyList<string> args)
{
return runner switch
{
"self" or "native" => BuildNativeStartInfo(entry, args),
"dotnet" => BuildDotNetStartInfo(entry, args),
"docker" => BuildDockerStartInfo(entry, targetDirectory, resultsDirectory, args),
_ => BuildCustomRunnerStartInfo(runner, entry, args)
};
}
private static ProcessStartInfo BuildNativeStartInfo(string binaryPath, IReadOnlyList<string> args)
{
if (string.IsNullOrWhiteSpace(binaryPath) || !File.Exists(binaryPath))
{
throw new FileNotFoundException("Scanner entrypoint not found.", binaryPath);
}
var startInfo = new ProcessStartInfo
{
FileName = binaryPath,
WorkingDirectory = Directory.GetCurrentDirectory()
};
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildDotNetStartInfo(string binaryPath, IReadOnlyList<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
WorkingDirectory = Directory.GetCurrentDirectory()
};
startInfo.ArgumentList.Add(binaryPath);
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildDockerStartInfo(string image, string targetDirectory, string resultsDirectory, IReadOnlyList<string> args)
{
if (string.IsNullOrWhiteSpace(image))
{
throw new ArgumentException("Docker image must be provided when runner is 'docker'.", nameof(image));
}
var cwd = Directory.GetCurrentDirectory();
var startInfo = new ProcessStartInfo
{
FileName = "docker",
WorkingDirectory = cwd
};
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--rm");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{cwd}:{cwd}");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{targetDirectory}:/scan-target:ro");
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add($"{resultsDirectory}:/scan-results");
startInfo.ArgumentList.Add("-w");
startInfo.ArgumentList.Add(cwd);
startInfo.ArgumentList.Add(image);
startInfo.ArgumentList.Add("--target");
startInfo.ArgumentList.Add("/scan-target");
startInfo.ArgumentList.Add("--output");
startInfo.ArgumentList.Add("/scan-results/scan.json");
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static ProcessStartInfo BuildCustomRunnerStartInfo(string runner, string entry, IReadOnlyList<string> args)
{
var startInfo = new ProcessStartInfo
{
FileName = runner,
WorkingDirectory = Directory.GetCurrentDirectory()
};
if (!string.IsNullOrWhiteSpace(entry))
{
startInfo.ArgumentList.Add(entry);
}
foreach (var argument in args)
{
startInfo.ArgumentList.Add(argument);
}
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.UseShellExecute = false;
return startInfo;
}
private static string ResolveResultsPath(string resultsDirectory, DateTimeOffset startTimestamp, HashSet<string> baseline)
{
var candidates = Directory.GetFiles(resultsDirectory, "*", SearchOption.AllDirectories);
string? newest = null;
DateTimeOffset newestTimestamp = startTimestamp;
foreach (var candidate in candidates)
{
if (baseline.Contains(candidate))
{
continue;
}
var info = new FileInfo(candidate);
if (info.LastWriteTimeUtc >= newestTimestamp)
{
newestTimestamp = info.LastWriteTimeUtc;
newest = candidate;
}
}
return newest ?? string.Empty;
}
private static string CreatePlaceholderResult(string resultsDirectory)
{
var fileName = $"scan-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}.json";

View File

@@ -1,79 +1,79 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Services;
internal sealed class ScannerInstaller : IScannerInstaller
{
private readonly ILogger<ScannerInstaller> _logger;
public ScannerInstaller(ILogger<ScannerInstaller> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(artifactPath) || !File.Exists(artifactPath))
{
throw new FileNotFoundException("Scanner artifact not found for installation.", artifactPath);
}
// Current implementation assumes docker-based scanner bundle.
var processInfo = new ProcessStartInfo
{
FileName = "docker",
ArgumentList = { "load", "-i", artifactPath },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = new Process { StartInfo = processInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
if (verbose)
{
_logger.LogInformation("[install] {Line}", args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
_logger.LogError("[install] {Line}", args.Data);
};
_logger.LogInformation("Installing scanner container from {Path}...", artifactPath);
if (!process.Start())
{
throw new InvalidOperationException("Failed to start container installation process.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"Container installation failed with exit code {process.ExitCode}.");
}
_logger.LogInformation("Scanner container installed successfully.");
}
}
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Services;
internal sealed class ScannerInstaller : IScannerInstaller
{
private readonly ILogger<ScannerInstaller> _logger;
public ScannerInstaller(ILogger<ScannerInstaller> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(artifactPath) || !File.Exists(artifactPath))
{
throw new FileNotFoundException("Scanner artifact not found for installation.", artifactPath);
}
// Current implementation assumes docker-based scanner bundle.
var processInfo = new ProcessStartInfo
{
FileName = "docker",
ArgumentList = { "load", "-i", artifactPath },
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var process = new Process { StartInfo = processInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
if (verbose)
{
_logger.LogInformation("[install] {Line}", args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if (args.Data is null)
{
return;
}
_logger.LogError("[install] {Line}", args.Data);
};
_logger.LogInformation("Installing scanner container from {Path}...", artifactPath);
if (!process.Start())
{
throw new InvalidOperationException("Failed to start container installation process.");
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (process.ExitCode != 0)
{
throw new InvalidOperationException($"Container installation failed with exit code {process.ExitCode}.");
}
_logger.LogInformation("Scanner container installed successfully.");
}
}

View File

@@ -1,8 +1,8 @@
using System.Diagnostics;
namespace StellaOps.Cli.Telemetry;
internal static class CliActivitySource
{
public static readonly ActivitySource Instance = new("StellaOps.Cli");
}
using System.Diagnostics;
namespace StellaOps.Cli.Telemetry;
internal static class CliActivitySource
{
public static readonly ActivitySource Instance = new("StellaOps.Cli");
}

View File

@@ -1,8 +1,8 @@
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Telemetry;
internal sealed class VerbosityState
{
public LogLevel MinimumLevel { get; set; } = LogLevel.Information;
}
using Microsoft.Extensions.Logging;
namespace StellaOps.Cli.Telemetry;
internal sealed class VerbosityState
{
public LogLevel MinimumLevel { get; set; } = LogLevel.Information;
}