Files
git.stella-ops.org/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Infrastructure/DependencyInjection/EvidenceLockerInfrastructureServiceCollectionExtensions.cs
2026-02-19 22:10:54 +02:00

239 lines
11 KiB
C#

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<EvidenceLockerOptions>()
.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<IOptions<EvidenceLockerOptions>>().Value;
var logger = provider.GetRequiredService<ILogger<EvidenceLockerDataSource>>();
return new EvidenceLockerDataSource(options.Database, logger);
});
services.AddSingleton<IEvidenceLockerMigrationRunner, EvidenceLockerMigrationRunner>();
services.AddHostedService<EvidenceLockerMigrationHostedService>();
services.AddSingleton<IMerkleTreeCalculator>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
var cryptoRegistry = provider.GetRequiredService<ICryptoProviderRegistry>();
return new MerkleTreeCalculator(
cryptoRegistry,
options.Crypto.HashAlgorithm,
options.Crypto.PreferredProvider);
});
services.AddScoped<IEvidenceBundleBuilder, EvidenceBundleBuilder>();
services.AddScoped<IEvidenceBundleRepository, EvidenceBundleRepository>();
services.AddScoped<IEvidenceGateArtifactRepository, EvidenceGateArtifactRepository>();
services.AddScoped<IEvidenceReindexService, EvidenceReindexService>();
// Verdict attestation repository
services.AddScoped<StellaOps.EvidenceLocker.Storage.IVerdictRepository>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresVerdictRepository>>();
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<StellaOps.EvidenceLocker.Storage.IEvidenceThreadRepository>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
var logger = provider.GetRequiredService<ILogger<StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository>>();
return new StellaOps.EvidenceLocker.Storage.PostgresEvidenceThreadRepository(
options.Database.ConnectionString,
logger);
});
services.AddSingleton<NullEvidenceTimelinePublisher>();
services.AddHttpClient<TimelineIndexerEvidenceTimelinePublisher>((provider, client) =>
{
var timeline = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().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<IEvidenceTimelinePublisher>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
if (options.Timeline?.Enabled is true)
{
return provider.GetRequiredService<TimelineIndexerEvidenceTimelinePublisher>();
}
return provider.GetRequiredService<NullEvidenceTimelinePublisher>();
});
services.TryAddSingleton<IEvidenceIncidentNotifier, NullEvidenceIncidentNotifier>();
services.AddSingleton<IncidentModeManager>();
services.AddSingleton<IIncidentModeState>(provider => provider.GetRequiredService<IncidentModeManager>());
services.TryAddSingleton<ITimestampAuthorityClient, NullTimestampAuthorityClient>();
services.AddScoped<IEvidenceSignatureService, EvidenceSignatureService>();
services.AddScoped<EvidenceSnapshotService>();
services.AddScoped<EvidenceGateArtifactService>();
services.AddScoped<EvidenceBundlePackagingService>();
services.AddScoped<EvidencePortableBundleService>();
services.AddSingleton<IEvidenceObjectStore>(provider =>
{
var options = provider.GetRequiredService<IOptions<EvidenceLockerOptions>>().Value;
var enforceWriteOnce = options.ObjectStore.EnforceWriteOnce;
return options.ObjectStore.Kind switch
{
ObjectStoreKind.FileSystem => CreateFileSystemStore(
options.ObjectStore.FileSystem!,
enforceWriteOnce,
provider.GetRequiredService<ILogger<FileSystemEvidenceObjectStore>>()),
ObjectStoreKind.AmazonS3 => CreateS3Store(
options.ObjectStore.AmazonS3!,
enforceWriteOnce,
provider.GetRequiredService<ILogger<S3EvidenceObjectStore>>()),
_ => 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<FileSystemEvidenceObjectStore> logger)
=> new FileSystemEvidenceObjectStore(options, enforceWriteOnce, logger);
private static IEvidenceObjectStore CreateS3Store(
AmazonS3StoreOptions options,
bool enforceWriteOnce,
ILogger<S3EvidenceObjectStore> logger)
{
var region = RegionEndpoint.GetBySystemName(options.Region);
var client = new AmazonS3Client(region);
return new S3EvidenceObjectStore(client, options, enforceWriteOnce, logger);
}
}