using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.WebService.Security;
namespace StellaOps.Scanner.WebService.Options;
///
/// Validation helpers for .
///
public static class ScannerWebServiceOptionsValidator
{
private static readonly HashSet SupportedStorageDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"mongo"
};
private static readonly HashSet SupportedQueueDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"redis",
"nats",
"rabbitmq"
};
private static readonly HashSet SupportedArtifactDrivers = new(StringComparer.OrdinalIgnoreCase)
{
"minio"
};
private static readonly HashSet 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.");
}
options.Events ??= new ScannerWebServiceOptions.EventsOptions();
ValidateEvents(options.Events);
}
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.");
}
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 values, bool toLower)
{
if (values is null || values.Count == 0)
{
return;
}
var seen = new HashSet(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.");
}
}
}