using System.Linq; using Serilog; using ZB.MOM.WW.LmxOpcUa.Host.OpcUa; namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration { /// /// Validates and logs effective configuration at startup. (SVC-003, SVC-005) /// public static class ConfigurationValidator { private static readonly ILogger Log = Serilog.Log.ForContext(typeof(ConfigurationValidator)); /// /// Validates the effective host configuration and writes the resolved values to the startup log before service initialization continues. /// /// The bound service configuration that drives OPC UA hosting, MXAccess connectivity, Galaxy queries, and dashboard behavior. /// when the required settings are present and within supported bounds; otherwise, . public static bool ValidateAndLog(AppConfiguration config) { bool valid = true; Log.Information("=== Effective Configuration ==="); // OPC UA Log.Information("OpcUa.BindAddress={BindAddress}, Port={Port}, EndpointPath={EndpointPath}, ServerName={ServerName}, GalaxyName={GalaxyName}", config.OpcUa.BindAddress, config.OpcUa.Port, config.OpcUa.EndpointPath, config.OpcUa.ServerName, config.OpcUa.GalaxyName); Log.Information("OpcUa.MaxSessions={MaxSessions}, SessionTimeoutMinutes={SessionTimeout}", config.OpcUa.MaxSessions, config.OpcUa.SessionTimeoutMinutes); if (config.OpcUa.Port < 1 || config.OpcUa.Port > 65535) { Log.Error("OpcUa.Port must be between 1 and 65535"); valid = false; } if (string.IsNullOrWhiteSpace(config.OpcUa.GalaxyName)) { Log.Error("OpcUa.GalaxyName must not be empty"); valid = false; } // MxAccess Log.Information("MxAccess.ClientName={ClientName}, ReadTimeout={ReadTimeout}s, WriteTimeout={WriteTimeout}s, MaxConcurrent={MaxConcurrent}", config.MxAccess.ClientName, config.MxAccess.ReadTimeoutSeconds, config.MxAccess.WriteTimeoutSeconds, config.MxAccess.MaxConcurrentOperations); Log.Information("MxAccess.MonitorInterval={MonitorInterval}s, AutoReconnect={AutoReconnect}, ProbeTag={ProbeTag}, ProbeStaleThreshold={ProbeStale}s", config.MxAccess.MonitorIntervalSeconds, config.MxAccess.AutoReconnect, config.MxAccess.ProbeTag ?? "(none)", config.MxAccess.ProbeStaleThresholdSeconds); if (string.IsNullOrWhiteSpace(config.MxAccess.ClientName)) { Log.Error("MxAccess.ClientName must not be empty"); valid = false; } // Galaxy Repository Log.Information("GalaxyRepository.ConnectionString={ConnectionString}, ChangeDetectionInterval={ChangeInterval}s, CommandTimeout={CmdTimeout}s, ExtendedAttributes={ExtendedAttributes}", config.GalaxyRepository.ConnectionString, config.GalaxyRepository.ChangeDetectionIntervalSeconds, config.GalaxyRepository.CommandTimeoutSeconds, config.GalaxyRepository.ExtendedAttributes); if (string.IsNullOrWhiteSpace(config.GalaxyRepository.ConnectionString)) { Log.Error("GalaxyRepository.ConnectionString must not be empty"); valid = false; } // Dashboard Log.Information("Dashboard.Enabled={Enabled}, Port={Port}, RefreshInterval={Refresh}s", config.Dashboard.Enabled, config.Dashboard.Port, config.Dashboard.RefreshIntervalSeconds); // Security Log.Information("Security.Profiles=[{Profiles}], AutoAcceptClientCertificates={AutoAccept}, RejectSHA1={RejectSHA1}, MinKeySize={MinKeySize}", string.Join(", ", config.Security.Profiles), config.Security.AutoAcceptClientCertificates, config.Security.RejectSHA1Certificates, config.Security.MinimumCertificateKeySize); if (config.Security.PkiRootPath != null) Log.Information("Security.PkiRootPath={PkiRootPath}", config.Security.PkiRootPath); if (config.Security.CertificateSubject != null) Log.Information("Security.CertificateSubject={CertificateSubject}", config.Security.CertificateSubject); var unknownProfiles = config.Security.Profiles .Where(p => !SecurityProfileResolver.ValidProfileNames.Contains(p, System.StringComparer.OrdinalIgnoreCase)) .ToList(); if (unknownProfiles.Count > 0) { Log.Warning("Unknown security profile(s): {Profiles}. Valid values: {ValidProfiles}", string.Join(", ", unknownProfiles), string.Join(", ", SecurityProfileResolver.ValidProfileNames)); } if (config.Security.MinimumCertificateKeySize < 2048) { Log.Error("Security.MinimumCertificateKeySize must be at least 2048"); valid = false; } if (config.Security.AutoAcceptClientCertificates) { Log.Warning("Security.AutoAcceptClientCertificates is enabled — client certificate trust is not enforced. Set to false in production"); } if (config.Security.Profiles.Count == 1 && config.Security.Profiles[0].Equals("None", System.StringComparison.OrdinalIgnoreCase)) { Log.Warning("Only the 'None' security profile is configured — transport security is disabled"); } Log.Information("=== Configuration {Status} ===", valid ? "Valid" : "INVALID"); return valid; } } }