239 lines
11 KiB
C#
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);
|
|
}
|
|
}
|