using System.Text.Json; using System.Text.Json.Nodes; namespace ZB.MOM.WW.ScadaBridge.ManagementService; /// /// Elides secret-bearing values inside opaque connection-config JSON blobs before /// they leave the management boundary (responses AND audit afterState), mirroring /// the SmtpConfigPublicShape/SmsConfigPublicShape pattern for structured entities. /// A property is secret-bearing when its name contains one of the fragments below /// (case-insensitive) and its value is a JSON string. MergeSentinels lets a client /// round-trip a scrubbed config through update without wiping stored secrets. /// internal static class ConfigSecretScrubber { public const string Sentinel = "***REDACTED***"; private static readonly string[] SecretNameFragments = ["password", "secret", "token", "apikey", "credential", "passphrase"]; private static bool IsSecretName(string propertyName) { foreach (var fragment in SecretNameFragments) { if (propertyName.Contains(fragment, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Replaces every secret-bearing string value (at any depth) with . /// Null/empty/parse-failure inputs are returned unchanged with hadSecrets=false. /// public static (string? scrubbed, bool hadSecrets) Scrub(string? json) { if (string.IsNullOrEmpty(json)) { return (json, false); } JsonNode? root; try { root = JsonNode.Parse(json); } catch (JsonException) { return (json, false); } if (root is null) { return (json, false); } var hadSecrets = ScrubNode(root, propertyIsSecret: false); if (!hadSecrets) { return (json, false); } return (root.ToJsonString(), true); } private static bool ScrubNode(JsonNode? node, bool propertyIsSecret) { switch (node) { case JsonObject obj: { var had = false; // Snapshot keys so we can reassign values while iterating. foreach (var name in obj.Select(kvp => kvp.Key).ToList()) { var child = obj[name]; if (child is JsonValue && IsSecretName(name) && child.GetValueKind() == JsonValueKind.String) { obj[name] = Sentinel; had = true; } else { had |= ScrubNode(child, IsSecretName(name)); } } return had; } case JsonArray arr: { var had = false; for (var i = 0; i < arr.Count; i++) { var child = arr[i]; if (propertyIsSecret && child is JsonValue && child.GetValueKind() == JsonValueKind.String) { arr[i] = Sentinel; had = true; } else { had |= ScrubNode(child, propertyIsSecret); } } return had; } default: return false; } } /// /// Walks the incoming config; wherever a string value equals , /// substitutes the value at the same path in (if absent, the /// Sentinel is left in place — an update would store it verbatim, which is inert). /// Null/empty/parse-failure inputs pass through unchanged. /// public static string? MergeSentinels(string? incoming, string? stored) { if (string.IsNullOrEmpty(incoming)) { return incoming; } JsonNode? incomingRoot; try { incomingRoot = JsonNode.Parse(incoming); } catch (JsonException) { return incoming; } if (incomingRoot is null) { return incoming; } JsonNode? storedRoot = null; if (!string.IsNullOrEmpty(stored)) { try { storedRoot = JsonNode.Parse(stored); } catch (JsonException) { storedRoot = null; } } MergeNode(incomingRoot, storedRoot); return incomingRoot.ToJsonString(); } private static void MergeNode(JsonNode? incoming, JsonNode? stored) { switch (incoming) { case JsonObject obj: { var storedObj = stored as JsonObject; foreach (var name in obj.Select(kvp => kvp.Key).ToList()) { var child = obj[name]; JsonNode? storedChild = null; storedObj?.TryGetPropertyValue(name, out storedChild); if (child is JsonValue && child.GetValueKind() == JsonValueKind.String && child.GetValue() == Sentinel) { if (storedChild is not null) { obj[name] = storedChild.DeepClone(); } } else { MergeNode(child, storedChild); } } break; } case JsonArray arr: { var storedArr = stored as JsonArray; for (var i = 0; i < arr.Count; i++) { var child = arr[i]; var storedChild = storedArr is not null && i < storedArr.Count ? storedArr[i] : null; if (child is JsonValue && child.GetValueKind() == JsonValueKind.String && child.GetValue() == Sentinel) { if (storedChild is not null) { arr[i] = storedChild.DeepClone(); } } else { MergeNode(child, storedChild); } } break; } } } }