Restructure solution layout by module

This commit is contained in:
master
2025-10-28 15:10:40 +02:00
parent 95daa159c4
commit d870da18ce
4103 changed files with 192899 additions and 187024 deletions

View File

@@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
namespace StellaOps.Scanner.WebService.Options;
/// <summary>
/// Strongly typed configuration for the Scanner WebService host.
/// </summary>
public sealed class ScannerWebServiceOptions
{
public const string SectionName = "scanner";
/// <summary>
/// Schema version for configuration consumers to coordinate breaking changes.
/// </summary>
public int SchemaVersion { get; set; } = 1;
/// <summary>
/// Mongo storage configuration used for catalog and job state.
/// </summary>
public StorageOptions Storage { get; set; } = new();
/// <summary>
/// Queue configuration used to enqueue scan jobs.
/// </summary>
public QueueOptions Queue { get; set; } = new();
/// <summary>
/// Object store configuration for SBOM artefacts.
/// </summary>
public ArtifactStoreOptions ArtifactStore { get; set; } = new();
/// <summary>
/// Feature flags toggling optional behaviours.
/// </summary>
public FeatureFlagOptions Features { get; set; } = new();
/// <summary>
/// Plug-in loader configuration.
/// </summary>
public PluginOptions Plugins { get; set; } = new();
/// <summary>
/// Telemetry configuration for logs, metrics, traces.
/// </summary>
public TelemetryOptions Telemetry { get; set; } = new();
/// <summary>
/// Authority / authentication configuration.
/// </summary>
public AuthorityOptions Authority { get; set; } = new();
/// <summary>
/// Signing configuration for report envelopes and attestations.
/// </summary>
public SigningOptions Signing { get; set; } = new();
/// <summary>
/// API-specific settings such as base path.
/// </summary>
public ApiOptions Api { get; set; } = new();
/// <summary>
/// Platform event emission settings.
/// </summary>
public EventsOptions Events { get; set; } = new();
/// <summary>
/// Runtime ingestion configuration.
/// </summary>
public RuntimeOptions Runtime { get; set; } = new();
public sealed class StorageOptions
{
public string Driver { get; set; } = "mongo";
public string Dsn { get; set; } = string.Empty;
public string? Database { get; set; }
public int CommandTimeoutSeconds { get; set; } = 30;
public int HealthCheckTimeoutSeconds { get; set; } = 5;
public IList<string> Migrations { get; set; } = new List<string>();
}
public sealed class QueueOptions
{
public string Driver { get; set; } = "redis";
public string Dsn { get; set; } = string.Empty;
public string Namespace { get; set; } = "scanner";
public int VisibilityTimeoutSeconds { get; set; } = 300;
public int LeaseHeartbeatSeconds { get; set; } = 30;
public int MaxDeliveryAttempts { get; set; } = 5;
public IDictionary<string, string> DriverSettings { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed class ArtifactStoreOptions
{
public string Driver { get; set; } = "rustfs";
public string Endpoint { get; set; } = string.Empty;
public bool UseTls { get; set; } = true;
public bool AllowInsecureTls { get; set; }
= false;
public int TimeoutSeconds { get; set; } = 60;
public string AccessKey { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public string? SecretKeyFile { get; set; }
public string Bucket { get; set; } = "scanner-artifacts";
public string? Region { get; set; }
public bool EnableObjectLock { get; set; } = true;
public int ObjectLockRetentionDays { get; set; } = 30;
public string? ApiKey { get; set; }
public string ApiKeyHeader { get; set; } = string.Empty;
public IDictionary<string, string> Headers { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed class FeatureFlagOptions
{
public bool AllowAnonymousScanSubmission { get; set; }
public bool EnableSignedReports { get; set; } = true;
public bool EnablePolicyPreview { get; set; } = true;
public IDictionary<string, bool> Experimental { get; set; } = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
}
public sealed class PluginOptions
{
public string? BaseDirectory { get; set; }
public string? Directory { get; set; }
public IList<string> SearchPatterns { get; set; } = new List<string>();
public IList<string> OrderedPlugins { get; set; } = new List<string>();
}
public sealed class TelemetryOptions
{
public bool Enabled { get; set; } = true;
public bool EnableTracing { get; set; } = true;
public bool EnableMetrics { get; set; } = true;
public bool EnableLogging { get; set; } = true;
public bool EnableRequestLogging { get; set; } = true;
public string MinimumLogLevel { get; set; } = "Information";
public string? ServiceName { get; set; }
public string? OtlpEndpoint { get; set; }
public IDictionary<string, string> OtlpHeaders { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public IDictionary<string, string> ResourceAttributes { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed class AuthorityOptions
{
public bool Enabled { get; set; }
public bool AllowAnonymousFallback { get; set; } = true;
public string Issuer { get; set; } = string.Empty;
public string? MetadataAddress { get; set; }
public bool RequireHttpsMetadata { get; set; } = true;
public int BackchannelTimeoutSeconds { get; set; } = 30;
public int TokenClockSkewSeconds { get; set; } = 60;
public IList<string> Audiences { get; set; } = new List<string>();
public IList<string> RequiredScopes { get; set; } = new List<string>();
public IList<string> BypassNetworks { get; set; } = new List<string>();
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? ClientSecretFile { get; set; }
public IList<string> ClientScopes { get; set; } = new List<string>();
public ResilienceOptions Resilience { get; set; } = new();
public sealed class ResilienceOptions
{
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 SigningOptions
{
public bool Enabled { get; set; } = false;
public string KeyId { get; set; } = string.Empty;
public string Algorithm { get; set; } = "ed25519";
public string? Provider { get; set; }
public string? KeyPem { get; set; }
public string? KeyPemFile { get; set; }
public string? CertificatePem { get; set; }
public string? CertificatePemFile { get; set; }
public string? CertificateChainPem { get; set; }
public string? CertificateChainPemFile { get; set; }
public int EnvelopeTtlSeconds { get; set; } = 600;
}
public sealed class ApiOptions
{
public string BasePath { get; set; } = "/api/v1";
public string ScansSegment { get; set; } = "scans";
public string ReportsSegment { get; set; } = "reports";
public string PolicySegment { get; set; } = "policy";
public string RuntimeSegment { get; set; } = "runtime";
}
public sealed class EventsOptions
{
public bool Enabled { get; set; }
public string Driver { get; set; } = "redis";
public string Dsn { get; set; } = string.Empty;
public string Stream { get; set; } = "stella.events";
public double PublishTimeoutSeconds { get; set; } = 5;
public long MaxStreamLength { get; set; } = 10000;
public IDictionary<string, string> DriverSettings { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public sealed class RuntimeOptions
{
public int MaxBatchSize { get; set; } = 256;
public int MaxPayloadBytes { get; set; } = 1 * 1024 * 1024;
public int EventTtlDays { get; set; } = 45;
public double PerNodeEventsPerSecond { get; set; } = 50;
public int PerNodeBurst { get; set; } = 200;
public double PerTenantEventsPerSecond { get; set; } = 200;
public int PerTenantBurst { get; set; } = 1000;
public int PolicyCacheTtlSeconds { get; set; } = 300;
}
}

View File

@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace StellaOps.Scanner.WebService.Options;
/// <summary>
/// Post-configuration helpers for <see cref="ScannerWebServiceOptions"/>.
/// </summary>
public static class ScannerWebServiceOptionsPostConfigure
{
public static void Apply(ScannerWebServiceOptions options, string contentRootPath)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(contentRootPath);
options.Plugins ??= new ScannerWebServiceOptions.PluginOptions();
if (string.IsNullOrWhiteSpace(options.Plugins.Directory))
{
options.Plugins.Directory = Path.Combine("plugins", "scanner");
}
options.Authority ??= new ScannerWebServiceOptions.AuthorityOptions();
var authority = options.Authority;
if (string.IsNullOrWhiteSpace(authority.ClientSecret)
&& !string.IsNullOrWhiteSpace(authority.ClientSecretFile))
{
authority.ClientSecret = ReadSecretFile(authority.ClientSecretFile!, contentRootPath);
}
options.ArtifactStore ??= new ScannerWebServiceOptions.ArtifactStoreOptions();
var artifactStore = options.ArtifactStore;
if (string.IsNullOrWhiteSpace(artifactStore.SecretKey)
&& !string.IsNullOrWhiteSpace(artifactStore.SecretKeyFile))
{
artifactStore.SecretKey = ReadSecretFile(artifactStore.SecretKeyFile!, contentRootPath);
}
options.Signing ??= new ScannerWebServiceOptions.SigningOptions();
var signing = options.Signing;
if (string.IsNullOrWhiteSpace(signing.KeyPem)
&& !string.IsNullOrWhiteSpace(signing.KeyPemFile))
{
signing.KeyPem = ReadAllText(signing.KeyPemFile!, contentRootPath);
}
if (string.IsNullOrWhiteSpace(signing.CertificatePem)
&& !string.IsNullOrWhiteSpace(signing.CertificatePemFile))
{
signing.CertificatePem = ReadAllText(signing.CertificatePemFile!, contentRootPath);
}
if (string.IsNullOrWhiteSpace(signing.CertificateChainPem)
&& !string.IsNullOrWhiteSpace(signing.CertificateChainPemFile))
{
signing.CertificateChainPem = ReadAllText(signing.CertificateChainPemFile!, contentRootPath);
}
options.Events ??= new ScannerWebServiceOptions.EventsOptions();
var eventsOptions = options.Events;
eventsOptions.DriverSettings ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(eventsOptions.Driver))
{
eventsOptions.Driver = "redis";
}
if (string.IsNullOrWhiteSpace(eventsOptions.Stream))
{
eventsOptions.Stream = "stella.events";
}
if (string.IsNullOrWhiteSpace(eventsOptions.Dsn)
&& string.Equals(options.Queue?.Driver, "redis", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(options.Queue?.Dsn))
{
eventsOptions.Dsn = options.Queue!.Dsn;
}
options.Runtime ??= new ScannerWebServiceOptions.RuntimeOptions();
}
private static string ReadSecretFile(string path, string contentRootPath)
{
var resolvedPath = ResolvePath(path, contentRootPath);
if (!File.Exists(resolvedPath))
{
throw new InvalidOperationException($"Secret file '{resolvedPath}' was not found.");
}
var secret = File.ReadAllText(resolvedPath).Trim();
if (string.IsNullOrEmpty(secret))
{
throw new InvalidOperationException($"Secret file '{resolvedPath}' is empty.");
}
return secret;
}
private static string ReadAllText(string path, string contentRootPath)
{
var resolvedPath = ResolvePath(path, contentRootPath);
if (!File.Exists(resolvedPath))
{
throw new InvalidOperationException($"File '{resolvedPath}' was not found.");
}
return File.ReadAllText(resolvedPath);
}
private static string ResolvePath(string path, string contentRootPath)
=> Path.IsPathRooted(path)
? path
: Path.GetFullPath(Path.Combine(contentRootPath, path));
}

View File

@@ -0,0 +1,466 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.WebService.Security;
namespace StellaOps.Scanner.WebService.Options;
/// <summary>
/// Validation helpers for <see cref="ScannerWebServiceOptions"/>.
/// </summary>
public static class ScannerWebServiceOptionsValidator
{
private static readonly HashSet<string> SupportedStorageDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"mongo"
};
private static readonly HashSet<string> SupportedQueueDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"redis",
"nats",
"rabbitmq"
};
private static readonly HashSet<string> SupportedArtifactDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"minio",
"s3",
"rustfs"
};
private static readonly HashSet<string> SupportedEventDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"redis"
};
public static void Validate(ScannerWebServiceOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (options.SchemaVersion <= 0)
{
throw new InvalidOperationException("Scanner configuration requires a positive schemaVersion.");
}
options.Storage ??= new ScannerWebServiceOptions.StorageOptions();
ValidateStorage(options.Storage);
options.Queue ??= new ScannerWebServiceOptions.QueueOptions();
ValidateQueue(options.Queue);
options.ArtifactStore ??= new ScannerWebServiceOptions.ArtifactStoreOptions();
ValidateArtifactStore(options.ArtifactStore);
options.Features ??= new ScannerWebServiceOptions.FeatureFlagOptions();
options.Plugins ??= new ScannerWebServiceOptions.PluginOptions();
options.Telemetry ??= new ScannerWebServiceOptions.TelemetryOptions();
ValidateTelemetry(options.Telemetry);
options.Authority ??= new ScannerWebServiceOptions.AuthorityOptions();
ValidateAuthority(options.Authority);
options.Signing ??= new ScannerWebServiceOptions.SigningOptions();
ValidateSigning(options.Signing);
options.Api ??= new ScannerWebServiceOptions.ApiOptions();
if (string.IsNullOrWhiteSpace(options.Api.BasePath))
{
throw new InvalidOperationException("API basePath must be configured.");
}
if (string.IsNullOrWhiteSpace(options.Api.ScansSegment))
{
throw new InvalidOperationException("API scansSegment must be configured.");
}
if (string.IsNullOrWhiteSpace(options.Api.ReportsSegment))
{
throw new InvalidOperationException("API reportsSegment must be configured.");
}
if (string.IsNullOrWhiteSpace(options.Api.PolicySegment))
{
throw new InvalidOperationException("API policySegment must be configured.");
}
if (string.IsNullOrWhiteSpace(options.Api.RuntimeSegment))
{
throw new InvalidOperationException("API runtimeSegment must be configured.");
}
options.Events ??= new ScannerWebServiceOptions.EventsOptions();
ValidateEvents(options.Events);
options.Runtime ??= new ScannerWebServiceOptions.RuntimeOptions();
ValidateRuntime(options.Runtime);
}
private static void ValidateStorage(ScannerWebServiceOptions.StorageOptions storage)
{
if (!SupportedStorageDrivers.Contains(storage.Driver))
{
throw new InvalidOperationException($"Unsupported storage driver '{storage.Driver}'. Supported drivers: mongo.");
}
if (string.IsNullOrWhiteSpace(storage.Dsn))
{
throw new InvalidOperationException("Storage DSN must be configured.");
}
if (storage.CommandTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Storage commandTimeoutSeconds must be greater than zero.");
}
if (storage.HealthCheckTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Storage healthCheckTimeoutSeconds must be greater than zero.");
}
}
private static void ValidateQueue(ScannerWebServiceOptions.QueueOptions queue)
{
if (!SupportedQueueDrivers.Contains(queue.Driver))
{
throw new InvalidOperationException($"Unsupported queue driver '{queue.Driver}'. Supported drivers: redis, nats, rabbitmq.");
}
if (string.IsNullOrWhiteSpace(queue.Dsn))
{
throw new InvalidOperationException("Queue DSN must be configured.");
}
if (string.IsNullOrWhiteSpace(queue.Namespace))
{
throw new InvalidOperationException("Queue namespace must be configured.");
}
if (queue.VisibilityTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Queue visibilityTimeoutSeconds must be greater than zero.");
}
if (queue.LeaseHeartbeatSeconds <= 0)
{
throw new InvalidOperationException("Queue leaseHeartbeatSeconds must be greater than zero.");
}
if (queue.MaxDeliveryAttempts <= 0)
{
throw new InvalidOperationException("Queue maxDeliveryAttempts must be greater than zero.");
}
}
private static void ValidateArtifactStore(ScannerWebServiceOptions.ArtifactStoreOptions artifactStore)
{
if (!SupportedArtifactDrivers.Contains(artifactStore.Driver))
{
throw new InvalidOperationException($"Unsupported artifact store driver '{artifactStore.Driver}'. Supported drivers: minio, s3, rustfs.");
}
if (string.Equals(artifactStore.Driver, "rustfs", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(artifactStore.Endpoint))
{
throw new InvalidOperationException("Artifact store endpoint must be configured for RustFS.");
}
if (!Uri.TryCreate(artifactStore.Endpoint, UriKind.Absolute, out _))
{
throw new InvalidOperationException("Artifact store endpoint must be an absolute URI for RustFS.");
}
if (artifactStore.TimeoutSeconds <= 0)
{
throw new InvalidOperationException("Artifact store timeoutSeconds must be greater than zero for RustFS.");
}
if (string.IsNullOrWhiteSpace(artifactStore.Bucket))
{
throw new InvalidOperationException("Artifact store bucket must be configured.");
}
return;
}
if (string.IsNullOrWhiteSpace(artifactStore.Endpoint))
{
throw new InvalidOperationException("Artifact store endpoint must be configured.");
}
if (string.IsNullOrWhiteSpace(artifactStore.Bucket))
{
throw new InvalidOperationException("Artifact store bucket must be configured.");
}
if (artifactStore.EnableObjectLock && artifactStore.ObjectLockRetentionDays <= 0)
{
throw new InvalidOperationException("Artifact store objectLockRetentionDays must be greater than zero when object lock is enabled.");
}
}
private static void ValidateEvents(ScannerWebServiceOptions.EventsOptions eventsOptions)
{
if (!eventsOptions.Enabled)
{
return;
}
if (!SupportedEventDrivers.Contains(eventsOptions.Driver))
{
throw new InvalidOperationException($"Unsupported events driver '{eventsOptions.Driver}'. Supported drivers: redis.");
}
if (string.IsNullOrWhiteSpace(eventsOptions.Dsn))
{
throw new InvalidOperationException("Events DSN must be configured when event emission is enabled.");
}
if (string.IsNullOrWhiteSpace(eventsOptions.Stream))
{
throw new InvalidOperationException("Events stream must be configured when event emission is enabled.");
}
if (eventsOptions.PublishTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Events publishTimeoutSeconds must be greater than zero.");
}
if (eventsOptions.MaxStreamLength < 0)
{
throw new InvalidOperationException("Events maxStreamLength must be zero or greater.");
}
}
private static void ValidateTelemetry(ScannerWebServiceOptions.TelemetryOptions telemetry)
{
if (string.IsNullOrWhiteSpace(telemetry.MinimumLogLevel))
{
throw new InvalidOperationException("Telemetry minimumLogLevel must be configured.");
}
if (!Enum.TryParse(telemetry.MinimumLogLevel, ignoreCase: true, out LogLevel _))
{
throw new InvalidOperationException($"Telemetry minimumLogLevel '{telemetry.MinimumLogLevel}' is invalid.");
}
if (!string.IsNullOrWhiteSpace(telemetry.OtlpEndpoint) && !Uri.TryCreate(telemetry.OtlpEndpoint, UriKind.Absolute, out _))
{
throw new InvalidOperationException("Telemetry OTLP endpoint must be an absolute URI when specified.");
}
foreach (var attribute in telemetry.ResourceAttributes)
{
if (string.IsNullOrWhiteSpace(attribute.Key))
{
throw new InvalidOperationException("Telemetry resource attribute keys must be non-empty.");
}
}
foreach (var header in telemetry.OtlpHeaders)
{
if (string.IsNullOrWhiteSpace(header.Key))
{
throw new InvalidOperationException("Telemetry OTLP header keys must be non-empty.");
}
}
}
private static void ValidateAuthority(ScannerWebServiceOptions.AuthorityOptions authority)
{
authority.Resilience ??= new ScannerWebServiceOptions.AuthorityOptions.ResilienceOptions();
NormalizeList(authority.Audiences, toLower: false);
NormalizeList(authority.RequiredScopes, toLower: true);
NormalizeList(authority.BypassNetworks, toLower: false);
NormalizeList(authority.ClientScopes, toLower: true);
NormalizeResilience(authority.Resilience);
if (authority.RequiredScopes.Count == 0)
{
authority.RequiredScopes.Add(ScannerAuthorityScopes.ScansEnqueue);
}
if (authority.ClientScopes.Count == 0)
{
foreach (var scope in authority.RequiredScopes)
{
authority.ClientScopes.Add(scope);
}
}
if (authority.BackchannelTimeoutSeconds <= 0)
{
throw new InvalidOperationException("Authority backchannelTimeoutSeconds must be greater than zero.");
}
if (authority.TokenClockSkewSeconds < 0 || authority.TokenClockSkewSeconds > 300)
{
throw new InvalidOperationException("Authority tokenClockSkewSeconds must be between 0 and 300 seconds.");
}
if (!authority.Enabled)
{
return;
}
if (string.IsNullOrWhiteSpace(authority.Issuer))
{
throw new InvalidOperationException("Authority issuer must be configured when authority is enabled.");
}
if (!Uri.TryCreate(authority.Issuer, UriKind.Absolute, out var issuerUri))
{
throw new InvalidOperationException("Authority issuer must be an absolute URI.");
}
if (authority.RequireHttpsMetadata && !issuerUri.IsLoopback && !string.Equals(issuerUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Authority issuer must use HTTPS when requireHttpsMetadata is enabled.");
}
if (!string.IsNullOrWhiteSpace(authority.MetadataAddress) && !Uri.TryCreate(authority.MetadataAddress, UriKind.Absolute, out _))
{
throw new InvalidOperationException("Authority metadataAddress must be an absolute URI when specified.");
}
if (authority.Audiences.Count == 0)
{
throw new InvalidOperationException("Authority audiences must include at least one entry when authority is enabled.");
}
if (!authority.AllowAnonymousFallback)
{
if (string.IsNullOrWhiteSpace(authority.ClientId))
{
throw new InvalidOperationException("Authority clientId must be configured when anonymous fallback is disabled.");
}
if (string.IsNullOrWhiteSpace(authority.ClientSecret))
{
throw new InvalidOperationException("Authority clientSecret must be configured when anonymous fallback is disabled.");
}
}
}
private static void ValidateSigning(ScannerWebServiceOptions.SigningOptions signing)
{
if (signing.EnvelopeTtlSeconds <= 0)
{
throw new InvalidOperationException("Signing envelopeTtlSeconds must be greater than zero.");
}
if (!signing.Enabled)
{
return;
}
if (string.IsNullOrWhiteSpace(signing.KeyId))
{
throw new InvalidOperationException("Signing keyId must be configured when signing is enabled.");
}
if (string.IsNullOrWhiteSpace(signing.Algorithm))
{
throw new InvalidOperationException("Signing algorithm must be configured when signing is enabled.");
}
if (string.IsNullOrWhiteSpace(signing.KeyPem) && string.IsNullOrWhiteSpace(signing.KeyPemFile))
{
throw new InvalidOperationException("Signing requires keyPem or keyPemFile when enabled.");
}
}
private static void NormalizeList(IList<string> values, bool toLower)
{
if (values is null || values.Count == 0)
{
return;
}
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var i = values.Count - 1; i >= 0; i--)
{
var entry = values[i];
if (string.IsNullOrWhiteSpace(entry))
{
values.RemoveAt(i);
continue;
}
var normalized = toLower ? entry.Trim().ToLowerInvariant() : entry.Trim();
if (!seen.Add(normalized))
{
values.RemoveAt(i);
continue;
}
values[i] = normalized;
}
}
private static void NormalizeResilience(ScannerWebServiceOptions.AuthorityOptions.ResilienceOptions resilience)
{
if (resilience.RetryDelays is null)
{
return;
}
foreach (var delay in resilience.RetryDelays.ToArray())
{
if (delay <= TimeSpan.Zero)
{
throw new InvalidOperationException("Authority resilience retryDelays must be greater than zero.");
}
}
if (resilience.OfflineCacheTolerance.HasValue && resilience.OfflineCacheTolerance.Value < TimeSpan.Zero)
{
throw new InvalidOperationException("Authority resilience offlineCacheTolerance must be greater than or equal to zero.");
}
}
private static void ValidateRuntime(ScannerWebServiceOptions.RuntimeOptions runtime)
{
if (runtime.MaxBatchSize <= 0)
{
throw new InvalidOperationException("Runtime maxBatchSize must be greater than zero.");
}
if (runtime.MaxPayloadBytes <= 0)
{
throw new InvalidOperationException("Runtime maxPayloadBytes must be greater than zero.");
}
if (runtime.EventTtlDays <= 0)
{
throw new InvalidOperationException("Runtime eventTtlDays must be greater than zero.");
}
if (runtime.PerNodeEventsPerSecond <= 0)
{
throw new InvalidOperationException("Runtime perNodeEventsPerSecond must be greater than zero.");
}
if (runtime.PerNodeBurst <= 0)
{
throw new InvalidOperationException("Runtime perNodeBurst must be greater than zero.");
}
if (runtime.PerTenantEventsPerSecond <= 0)
{
throw new InvalidOperationException("Runtime perTenantEventsPerSecond must be greater than zero.");
}
if (runtime.PerTenantBurst <= 0)
{
throw new InvalidOperationException("Runtime perTenantBurst must be greater than zero.");
}
if (runtime.PolicyCacheTtlSeconds <= 0)
{
throw new InvalidOperationException("Runtime policyCacheTtlSeconds must be greater than zero.");
}
}
}