using Amazon; using Amazon.S3; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Cryptography; using StellaOps.Cryptography.DependencyInjection; using StellaOps.Cryptography.Plugin.BouncyCastle; using StellaOps.EvidenceLocker.Core.Builders; using StellaOps.EvidenceLocker.Core.Configuration; using StellaOps.EvidenceLocker.Core.Incident; using StellaOps.EvidenceLocker.Core.Notifications; using StellaOps.EvidenceLocker.Core.Reindexing; using StellaOps.EvidenceLocker.Core.Repositories; using StellaOps.EvidenceLocker.Core.Signing; using StellaOps.EvidenceLocker.Core.Storage; using StellaOps.EvidenceLocker.Core.Timeline; using StellaOps.EvidenceLocker.Infrastructure.Builders; using StellaOps.EvidenceLocker.Infrastructure.Db; using StellaOps.EvidenceLocker.Infrastructure.Reindexing; using StellaOps.EvidenceLocker.Infrastructure.Repositories; using StellaOps.EvidenceLocker.Infrastructure.Services; using StellaOps.EvidenceLocker.Infrastructure.Signing; using StellaOps.EvidenceLocker.Infrastructure.Storage; using StellaOps.EvidenceLocker.Infrastructure.Timeline; using System; using System.Net.Http; using System.Net.Http.Headers; namespace StellaOps.EvidenceLocker.Infrastructure.DependencyInjection; public static class EvidenceLockerInfrastructureServiceCollectionExtensions { public static IServiceCollection AddEvidenceLockerInfrastructure( this IServiceCollection services, IConfiguration configuration) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); services .AddOptions() .Bind(configuration.GetSection(EvidenceLockerOptions.SectionName)) .ValidateDataAnnotations() .Validate(static options => options.Signing is not null, "Signing options must be provided.") .Validate(static options => ValidateObjectStore(options.ObjectStore), "Invalid object-store configuration.") .Validate(static options => ValidateTimeline(options.Timeline), "Invalid timeline configuration.") .Validate(static options => ValidateIncident(options.Incident), "Invalid incident configuration.") .Validate(static options => ValidatePortable(options.Portable), "Invalid portable configuration."); services.AddStellaOpsCrypto(); services.AddBouncyCastleEd25519Provider(); services.TryAddSingleton(TimeProvider.System); services.AddSingleton(provider => { var options = provider.GetRequiredService>().Value; var logger = provider.GetRequiredService>(); return new EvidenceLockerDataSource(options.Database, logger); }); services.AddSingleton(); services.AddHostedService(); services.AddSingleton(provider => { var options = provider.GetRequiredService>().Value; var cryptoRegistry = provider.GetRequiredService(); return new MerkleTreeCalculator( cryptoRegistry, options.Crypto.HashAlgorithm, options.Crypto.PreferredProvider); }); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Verdict attestation repository services.AddScoped(provider => { var options = provider.GetRequiredService>().Value; var logger = provider.GetRequiredService>(); return new StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository( options.Database.ConnectionString, logger); }); // Evidence Thread repository (Artifact Canonical Record API) // Sprint: SPRINT_20260219_009 (CID-04) services.AddScoped(provider => { var options = provider.GetRequiredService>().Value; var logger = provider.GetRequiredService>(); return new StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository( options.Database.ConnectionString, logger); }); services.AddSingleton(); services.AddHttpClient((provider, client) => { var timeline = provider.GetRequiredService>().Value.Timeline!; client.BaseAddress = new Uri(timeline.Endpoint!, UriKind.Absolute); client.Timeout = TimeSpan.FromSeconds(timeline.RequestTimeoutSeconds); var auth = timeline.Authentication; if (auth?.Token is { Length: > 0 }) { if (string.Equals(auth.HeaderName, "Authorization", StringComparison.OrdinalIgnoreCase)) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(auth.Scheme, auth.Token); } else { var value = string.IsNullOrWhiteSpace(auth.Scheme) ? auth.Token : $"{auth.Scheme} {auth.Token}"; client.DefaultRequestHeaders.Remove(auth.HeaderName); client.DefaultRequestHeaders.Add(auth.HeaderName, value); } } }) .ConfigurePrimaryHttpMessageHandler(static () => new SocketsHttpHandler { AutomaticDecompression = System.Net.DecompressionMethods.All }); services.AddSingleton(provider => { var options = provider.GetRequiredService>().Value; if (options.Timeline?.Enabled is true) { return provider.GetRequiredService(); } return provider.GetRequiredService(); }); services.TryAddSingleton(); services.AddSingleton(); services.AddSingleton(provider => provider.GetRequiredService()); services.TryAddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(provider => { var options = provider.GetRequiredService>().Value; var enforceWriteOnce = options.ObjectStore.EnforceWriteOnce; return options.ObjectStore.Kind switch { ObjectStoreKind.FileSystem => CreateFileSystemStore( options.ObjectStore.FileSystem!, enforceWriteOnce, provider.GetRequiredService>()), ObjectStoreKind.AmazonS3 => CreateS3Store( options.ObjectStore.AmazonS3!, enforceWriteOnce, provider.GetRequiredService>()), _ => throw new InvalidOperationException($"Unsupported object-store kind '{options.ObjectStore.Kind}'.") }; }); return services; } private static bool ValidateObjectStore(ObjectStoreOptions options) { return options.Kind switch { ObjectStoreKind.FileSystem => options.FileSystem is not null && !string.IsNullOrWhiteSpace(options.FileSystem.RootPath), ObjectStoreKind.AmazonS3 => options.AmazonS3 is not null && !string.IsNullOrWhiteSpace(options.AmazonS3.BucketName) && !string.IsNullOrWhiteSpace(options.AmazonS3.Region), _ => false }; } private static bool ValidateTimeline(TimelineOptions? options) { if (options is null || !options.Enabled) { return true; } return !string.IsNullOrWhiteSpace(options.Endpoint); } private static bool ValidateIncident(IncidentModeOptions? options) { if (options is null || !options.Enabled) { return true; } return options.RetentionExtensionDays >= 1; } private static bool ValidatePortable(PortableOptions? options) { if (options is null) { return true; } return !string.IsNullOrWhiteSpace(options.ArtifactName) && !string.IsNullOrWhiteSpace(options.MetadataFileName) && !string.IsNullOrWhiteSpace(options.InstructionsFileName) && !string.IsNullOrWhiteSpace(options.OfflineScriptFileName); } private static IEvidenceObjectStore CreateFileSystemStore( FileSystemStoreOptions options, bool enforceWriteOnce, ILogger logger) => new FileSystemEvidenceObjectStore(options, enforceWriteOnce, logger); private static IEvidenceObjectStore CreateS3Store( AmazonS3StoreOptions options, bool enforceWriteOnce, ILogger logger) { var region = RegionEndpoint.GetBySystemName(options.Region); var client = new AmazonS3Client(region); return new S3EvidenceObjectStore(client, options, enforceWriteOnce, logger); } }