Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user