- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
440 lines
16 KiB
C#
440 lines
16 KiB
C#
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"
|
|
};
|
|
|
|
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.");
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|
|
}
|