275 lines
10 KiB
C#
275 lines
10 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Infrastructure.Postgres.Migrations;
|
|
|
|
/// <summary>
|
|
/// Represents a dependency between migrations in different modules.
|
|
/// </summary>
|
|
public sealed record MigrationDependency
|
|
{
|
|
/// <summary>
|
|
/// The module that has the dependency.
|
|
/// </summary>
|
|
public required string Module { get; init; }
|
|
|
|
/// <summary>
|
|
/// The migration file that has the dependency.
|
|
/// </summary>
|
|
public required string Migration { get; init; }
|
|
|
|
/// <summary>
|
|
/// The module being depended upon.
|
|
/// </summary>
|
|
public required string DependsOnModule { get; init; }
|
|
|
|
/// <summary>
|
|
/// The schema being depended upon.
|
|
/// </summary>
|
|
public required string DependsOnSchema { get; init; }
|
|
|
|
/// <summary>
|
|
/// The specific table or object being depended upon (optional).
|
|
/// </summary>
|
|
public string? DependsOnObject { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether this is a soft dependency (FK created conditionally).
|
|
/// </summary>
|
|
public bool IsSoft { get; init; }
|
|
|
|
/// <summary>
|
|
/// Description of why this dependency exists.
|
|
/// </summary>
|
|
public string? Description { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Module schema configuration for dependency resolution.
|
|
/// </summary>
|
|
public sealed record ModuleSchemaConfig
|
|
{
|
|
/// <summary>
|
|
/// The module name (e.g., "Authority", "Concelier").
|
|
/// </summary>
|
|
public required string Module { get; init; }
|
|
|
|
/// <summary>
|
|
/// The PostgreSQL schema name (e.g., "auth", "vuln").
|
|
/// </summary>
|
|
public required string Schema { get; init; }
|
|
|
|
/// <summary>
|
|
/// The WebService that owns this module's migrations.
|
|
/// </summary>
|
|
public string? OwnerService { get; init; }
|
|
|
|
/// <summary>
|
|
/// The assembly containing migrations for this module.
|
|
/// </summary>
|
|
public string? MigrationAssembly { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registry of module schemas and their dependencies.
|
|
/// </summary>
|
|
public sealed class ModuleDependencyRegistry
|
|
{
|
|
private readonly Dictionary<string, ModuleSchemaConfig> _modules = new(StringComparer.OrdinalIgnoreCase);
|
|
private readonly List<MigrationDependency> _dependencies = [];
|
|
|
|
/// <summary>
|
|
/// Gets all registered modules.
|
|
/// </summary>
|
|
public IReadOnlyDictionary<string, ModuleSchemaConfig> Modules => _modules;
|
|
|
|
/// <summary>
|
|
/// Gets all registered dependencies.
|
|
/// </summary>
|
|
public IReadOnlyList<MigrationDependency> Dependencies => _dependencies;
|
|
|
|
/// <summary>
|
|
/// Registers a module schema configuration.
|
|
/// </summary>
|
|
public ModuleDependencyRegistry RegisterModule(ModuleSchemaConfig config)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(config);
|
|
_modules[config.Module] = config;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a dependency between modules.
|
|
/// </summary>
|
|
public ModuleDependencyRegistry RegisterDependency(MigrationDependency dependency)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(dependency);
|
|
_dependencies.Add(dependency);
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the schema name for a module.
|
|
/// </summary>
|
|
public string? GetSchemaForModule(string moduleName)
|
|
{
|
|
return _modules.TryGetValue(moduleName, out var config) ? config.Schema : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the module name for a schema.
|
|
/// </summary>
|
|
public string? GetModuleForSchema(string schemaName)
|
|
{
|
|
return _modules.Values
|
|
.FirstOrDefault(m => string.Equals(m.Schema, schemaName, StringComparison.OrdinalIgnoreCase))
|
|
?.Module;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets dependencies for a specific module.
|
|
/// </summary>
|
|
public IReadOnlyList<MigrationDependency> GetDependenciesForModule(string moduleName)
|
|
{
|
|
return _dependencies
|
|
.Where(d => string.Equals(d.Module, moduleName, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets modules that depend on a specific module.
|
|
/// </summary>
|
|
public IReadOnlyList<MigrationDependency> GetDependentsOfModule(string moduleName)
|
|
{
|
|
return _dependencies
|
|
.Where(d => string.Equals(d.DependsOnModule, moduleName, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that all dependencies can be satisfied.
|
|
/// </summary>
|
|
public IReadOnlyList<string> ValidateDependencies()
|
|
{
|
|
var errors = new List<string>();
|
|
|
|
foreach (var dep in _dependencies)
|
|
{
|
|
// Check that the dependent module exists
|
|
if (!_modules.ContainsKey(dep.Module))
|
|
{
|
|
errors.Add($"Unknown module '{dep.Module}' in dependency declaration.");
|
|
}
|
|
|
|
// Check that the target module exists
|
|
if (!_modules.ContainsKey(dep.DependsOnModule))
|
|
{
|
|
errors.Add($"Unknown dependency target module '{dep.DependsOnModule}' from '{dep.Module}'.");
|
|
}
|
|
|
|
// Check that the target schema matches
|
|
if (_modules.TryGetValue(dep.DependsOnModule, out var targetConfig))
|
|
{
|
|
if (!string.Equals(targetConfig.Schema, dep.DependsOnSchema, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
errors.Add(
|
|
$"Schema mismatch for dependency '{dep.Module}' -> '{dep.DependsOnModule}': " +
|
|
$"expected schema '{targetConfig.Schema}', got '{dep.DependsOnSchema}'.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the default registry with all StellaOps modules.
|
|
/// </summary>
|
|
public static ModuleDependencyRegistry CreateDefault()
|
|
{
|
|
var registry = new ModuleDependencyRegistry();
|
|
|
|
// Register all modules with their schemas
|
|
registry
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Authority", Schema = "auth", OwnerService = "Authority.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Concelier", Schema = "vuln", OwnerService = "Concelier.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Excititor", Schema = "vex", OwnerService = "Excititor.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Policy", Schema = "policy", OwnerService = "Policy.Gateway" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Scheduler", Schema = "scheduler", OwnerService = "Scheduler.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Notify", Schema = "notify", OwnerService = "Notify.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Scanner", Schema = "scanner", OwnerService = "Scanner.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Attestor", Schema = "proofchain", OwnerService = "Attestor.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Signer", Schema = "signer", OwnerService = "Signer.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Signals", Schema = "signals", OwnerService = "Signals" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "EvidenceLocker", Schema = "evidence", OwnerService = "EvidenceLocker.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "ExportCenter", Schema = "export", OwnerService = "ExportCenter.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "IssuerDirectory", Schema = "issuer", OwnerService = "IssuerDirectory.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Orchestrator", Schema = "orchestrator", OwnerService = "Orchestrator.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Findings", Schema = "findings", OwnerService = "Findings.Ledger.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "BinaryIndex", Schema = "binaries", OwnerService = "Scanner.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "VexHub", Schema = "vexhub", OwnerService = "VexHub.WebService" })
|
|
.RegisterModule(new ModuleSchemaConfig { Module = "Unknowns", Schema = "unknowns", OwnerService = "Policy.Gateway" });
|
|
|
|
// Register known cross-module dependencies
|
|
registry
|
|
.RegisterDependency(new MigrationDependency
|
|
{
|
|
Module = "Signer",
|
|
Migration = "20251214000001_AddKeyManagementSchema.sql",
|
|
DependsOnModule = "Attestor",
|
|
DependsOnSchema = "proofchain",
|
|
DependsOnObject = "trust_anchors",
|
|
IsSoft = true,
|
|
Description = "Optional FK from signer.key_history to proofchain.trust_anchors"
|
|
})
|
|
.RegisterDependency(new MigrationDependency
|
|
{
|
|
Module = "Scanner",
|
|
Migration = "N/A",
|
|
DependsOnModule = "Concelier",
|
|
DependsOnSchema = "vuln",
|
|
IsSoft = true,
|
|
Description = "Scanner uses Concelier linksets for advisory data"
|
|
})
|
|
.RegisterDependency(new MigrationDependency
|
|
{
|
|
Module = "Policy",
|
|
Migration = "N/A",
|
|
DependsOnModule = "Concelier",
|
|
DependsOnSchema = "vuln",
|
|
IsSoft = true,
|
|
Description = "Policy uses vulnerability data from Concelier"
|
|
})
|
|
.RegisterDependency(new MigrationDependency
|
|
{
|
|
Module = "Policy",
|
|
Migration = "N/A",
|
|
DependsOnModule = "Excititor",
|
|
DependsOnSchema = "vex",
|
|
IsSoft = true,
|
|
Description = "Policy uses VEX data from Excititor"
|
|
});
|
|
|
|
return registry;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Serializes the registry to JSON.
|
|
/// </summary>
|
|
public string ToJson()
|
|
{
|
|
var data = new
|
|
{
|
|
modules = _modules.Values.ToList(),
|
|
dependencies = _dependencies
|
|
};
|
|
|
|
return JsonSerializer.Serialize(data, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
});
|
|
}
|
|
}
|