diff --git a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs index 0beab89..7976a61 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Diagnostics/GatewayLogRedactorSeam.cs @@ -10,8 +10,15 @@ public sealed class GatewayLogRedactorSeam : ILogRedactor { private static readonly string[] IdentityKeys = ["ClientIdentity", "authorization", "Authorization"]; + /// Property name carrying the MXAccess command method, which gates value redaction. + private const string CommandMethodProperty = "CommandMethod"; + + /// Property name carrying a command payload value that may bear credentials. + private const string CommandValueProperty = "CommandValue"; + /// - /// Masks API-key/credential material in known identity-bearing log properties. + /// Masks API-key/credential material in known identity-bearing log properties, and any + /// command payload value. /// /// The log event property dictionary to redact in place. public void Redact(IDictionary properties) @@ -24,5 +31,26 @@ public sealed class GatewayLogRedactorSeam : ILogRedactor properties[key] = GatewayLogRedactor.RedactClientIdentity(s); } } + + RedactCommandValue(properties); + } + + /// + /// Masks a command payload value. Value logging is off here — the seam exposes no opt-in — so + /// every non-null value is masked, credential-bearing command or not. + /// + /// The log event property dictionary to redact in place. + private static void RedactCommandValue(IDictionary properties) + { + if (!properties.TryGetValue(CommandValueProperty, out object? value) || value is null) + { + return; + } + + string? commandMethod = properties.TryGetValue(CommandMethodProperty, out object? method) + ? method as string + : null; + + properties[CommandValueProperty] = GatewayLogRedactor.RedactCommandValue(commandMethod, value); } } diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs index 16f52c0..31e4071 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Diagnostics/GatewayLogRedactorSeamTests.cs @@ -13,4 +13,75 @@ public sealed class GatewayLogRedactorSeamTests redactor.Redact(props); Assert.Equal("Bearer mxgw_operator01_[redacted]", props["ClientIdentity"]); } + + /// A command value carried by a credential-bearing command never reaches a sink in the clear. + [Fact] + public void Redact_MasksCommandValueForCredentialBearingCommand() + { + GatewayLogRedactorSeam redactor = new(); + Dictionary props = new() + { + ["CommandMethod"] = "AuthenticateUser", + ["CommandValue"] = "operator01:hunter2" + }; + + redactor.Redact(props); + + Assert.Equal(GatewayLogRedactor.RedactedValue, props["CommandValue"]); + } + + /// + /// Value logging is off by default, so an ordinary command value is masked too — the seam + /// exposes no opt-in, matching 's default. + /// + [Fact] + public void Redact_MasksCommandValueForOrdinaryCommand() + { + GatewayLogRedactorSeam redactor = new(); + Dictionary props = new() + { + ["CommandMethod"] = "Read", + ["CommandValue"] = "plaintext-tag-value" + }; + + redactor.Redact(props); + + Assert.Equal(GatewayLogRedactor.RedactedValue, props["CommandValue"]); + } + + /// A command value with no accompanying method is still masked — an unknown method cannot be cleared. + [Fact] + public void Redact_MasksCommandValueWhenCommandMethodAbsent() + { + GatewayLogRedactorSeam redactor = new(); + Dictionary props = new() { ["CommandValue"] = "plaintext-tag-value" }; + + redactor.Redact(props); + + Assert.Equal(GatewayLogRedactor.RedactedValue, props["CommandValue"]); + } + + /// A null command value stays null rather than becoming the redaction placeholder. + [Fact] + public void Redact_LeavesNullCommandValueNull() + { + GatewayLogRedactorSeam redactor = new(); + Dictionary props = new() { ["CommandValue"] = null }; + + redactor.Redact(props); + + Assert.Null(props["CommandValue"]); + } + + /// An event carrying no command value is untouched — the seam must not invent the property. + [Fact] + public void Redact_DoesNotAddCommandValueWhenAbsent() + { + GatewayLogRedactorSeam redactor = new(); + Dictionary props = new() { ["ClientIdentity"] = "anonymous" }; + + redactor.Redact(props); + + Assert.False(props.ContainsKey("CommandValue")); + } }