using System.Reflection; using System.Text.Json; namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// /// Host-003 regression: secrets must not be committed in plaintext in the /// shipped appsettings.Central.json. Connection-string passwords, the LDAP /// service-account password and the JWT signing key must be supplied via /// environment variables (or another secret store) at deployment time — the /// committed file may only carry non-sensitive structural defaults or /// placeholder values. /// public class ConfigSecretsTests { private static string FindHostProjectDirectory() { var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var dir = new DirectoryInfo(assemblyDir); while (dir != null) { var hostPath = Path.Combine(dir.FullName, "src", "ZB.MOM.WW.ScadaBridge.Host"); if (Directory.Exists(hostPath)) return hostPath; dir = dir.Parent; } throw new DirectoryNotFoundException("Could not locate src/ZB.MOM.WW.ScadaBridge.Host"); } private static JsonElement ScadaBridgeSection() { var path = Path.Combine(FindHostProjectDirectory(), "appsettings.Central.json"); var json = File.ReadAllText(path); using var doc = JsonDocument.Parse(json); return doc.RootElement.GetProperty("ScadaBridge").Clone(); } [Fact] public void CentralConfig_ConnectionStrings_ContainNoPlaintextPassword() { var db = ScadaBridgeSection().GetProperty("Database"); foreach (var prop in db.EnumerateObject()) { var value = prop.Value.GetString() ?? string.Empty; // A committed connection string must not carry a literal Password= value. // Either the password is delivered via an environment variable or the // whole connection string is. A placeholder reference is acceptable. var idx = value.IndexOf("Password=", StringComparison.OrdinalIgnoreCase); if (idx >= 0) { var after = value[(idx + "Password=".Length)..]; var literal = after.Split(';')[0]; Assert.True( literal.Length == 0 || literal.Contains('{') || literal.Contains('$'), $"appsettings.Central.json '{prop.Name}' contains a plaintext Password value '{literal}'. " + "Move the secret to an environment variable."); } } } [Fact] public void CentralConfig_LdapServiceAccountPassword_IsNotCommitted() { // Task 1.4 cutover: the LDAP service-account password moved out of the flat // Security:LdapServiceAccountPassword key into the nested Security:Ldap // sub-section (Security:Ldap:ServiceAccountPassword), bound to the shared // ZB.MOM.WW.Auth LdapOptions. Walk into Security:Ldap and guard the nested // key — checking the deleted flat key would pass vacuously. var security = ScadaBridgeSection().GetProperty("Security"); var ldap = security.GetProperty("Ldap"); if (ldap.TryGetProperty("ServiceAccountPassword", out var pw)) { var value = pw.GetString() ?? string.Empty; Assert.True( value.Length == 0 || value.Contains('{') || value.Contains('$'), $"appsettings.Central.json carries a plaintext Security:Ldap:ServiceAccountPassword '{value}'. " + "Move it to an environment variable (ScadaBridge__Security__Ldap__ServiceAccountPassword)."); } } [Fact] public void CentralConfig_JwtSigningKey_IsNotCommitted() { var security = ScadaBridgeSection().GetProperty("Security"); if (security.TryGetProperty("JwtSigningKey", out var key)) { var value = key.GetString() ?? string.Empty; Assert.True( value.Length == 0 || value.Contains('{') || value.Contains('$'), $"appsettings.Central.json carries a committed JwtSigningKey '{value}'. " + "A committed signing key lets anyone with repo access forge session tokens. " + "Move it to an environment variable."); } } }