Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ManagementService/ConfigSecretScrubber.cs
T

223 lines
6.6 KiB
C#

using System.Text.Json;
using System.Text.Json.Nodes;
namespace ZB.MOM.WW.ScadaBridge.ManagementService;
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Replaces every secret-bearing string value (at any depth) with <see cref="Sentinel"/>.
/// Null/empty/parse-failure inputs are returned unchanged with hadSecrets=false.
/// </summary>
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;
}
}
/// <summary>
/// Walks the incoming config; wherever a string value equals <see cref="Sentinel"/>,
/// substitutes the value at the same path in <paramref name="stored"/> (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.
/// </summary>
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<string>() == 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<string>() == Sentinel)
{
if (storedChild is not null)
{
arr[i] = storedChild.DeepClone();
}
}
else
{
MergeNode(child, storedChild);
}
}
break;
}
}
}
}