fix(diagnostics): redact WriteSecuredBulk/WriteSecured2Bulk payloads (#193)
Adds both secured bulk kinds to GatewayLogRedactor's credential-bearing set, plus a contract cross-check test that walks every MxCommand payload descriptor and fails if a secured or password-bearing command kind is not redacted.
This commit is contained in:
@@ -11,8 +11,11 @@ public static class GatewayLogRedactor
|
||||
private static readonly HashSet<string> SensitiveCommandMethods = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"AuthenticateUser",
|
||||
// Carry a credential-sensitive write value (single item and bulk).
|
||||
"WriteSecured",
|
||||
"WriteSecured2",
|
||||
"WriteSecuredBulk",
|
||||
"WriteSecured2Bulk",
|
||||
// Carry operator_password.
|
||||
"AcknowledgeAlarm",
|
||||
"AcknowledgeAlarmByName"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Google.Protobuf.Reflection;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
|
||||
@@ -118,6 +120,8 @@ public sealed class GatewayLogRedactorTests
|
||||
[InlineData("AuthenticateUser")]
|
||||
[InlineData("WriteSecured")]
|
||||
[InlineData("WriteSecured2")]
|
||||
[InlineData("WriteSecuredBulk")]
|
||||
[InlineData("WriteSecured2Bulk")]
|
||||
[InlineData("AcknowledgeAlarm")]
|
||||
[InlineData("AcknowledgeAlarmByName")]
|
||||
public void IsCredentialBearingCommand_IdentifiesSensitiveMxAccessCommands(string commandMethod)
|
||||
@@ -146,6 +150,87 @@ public sealed class GatewayLogRedactorTests
|
||||
Assert.Equal("[redacted]", redacted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the secured bulk writes are redacted even with value logging enabled. These
|
||||
/// carry the same credential-sensitive value as the single-item secured writes (#193).
|
||||
/// </summary>
|
||||
/// <param name="kind">The secured bulk command kind.</param>
|
||||
[Theory]
|
||||
[InlineData(MxCommandKind.WriteSecuredBulk)]
|
||||
[InlineData(MxCommandKind.WriteSecured2Bulk)]
|
||||
public void RedactCommandValue_RedactsSecuredBulkWriteEvenWhenValueLoggingIsEnabled(MxCommandKind kind)
|
||||
{
|
||||
// WorkerClient logs the method as MxCommandKind.ToString(); exercise that exact spelling.
|
||||
object? redacted = GatewayLogRedactor.RedactCommandValue(
|
||||
kind.ToString(),
|
||||
"credential-bearing-bulk-value",
|
||||
valueLoggingEnabled: true);
|
||||
|
||||
Assert.Equal("[redacted]", redacted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross-checks the redactor against the contract: every <see cref="MxCommand"/> payload that
|
||||
/// carries a password field, or is a secured write, must map to a credential-bearing command
|
||||
/// kind. A new credential-bearing command added to the proto without a redactor entry fails here.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void IsCredentialBearingCommand_CoversEveryCredentialBearingPayloadInTheContract()
|
||||
{
|
||||
List<string> sensitivePayloads = [];
|
||||
foreach (FieldDescriptor field in MxCommand.Descriptor.Oneofs.Single().Fields)
|
||||
{
|
||||
if (field.FieldType != FieldType.Message)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
MessageDescriptor payload = field.MessageType;
|
||||
if (payload.Name.Contains("Secured", StringComparison.Ordinal)
|
||||
|| CarriesPassword(payload, []))
|
||||
{
|
||||
sensitivePayloads.Add(payload.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Positive control: the walk must find the known credential carriers, or it proves nothing.
|
||||
Assert.Contains("AuthenticateUserCommand", sensitivePayloads);
|
||||
Assert.Contains("WriteSecuredBulkCommand", sensitivePayloads);
|
||||
Assert.Contains("AcknowledgeAlarmByNameCommand", sensitivePayloads);
|
||||
|
||||
foreach (string payloadName in sensitivePayloads)
|
||||
{
|
||||
string kindName = payloadName[..^"Command".Length];
|
||||
Assert.True(Enum.TryParse(kindName, out MxCommandKind _), $"No MxCommandKind named {kindName}.");
|
||||
Assert.True(
|
||||
GatewayLogRedactor.IsCredentialBearingCommand(kindName),
|
||||
$"{kindName} carries credential material but is not redacted.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CarriesPassword(MessageDescriptor message, HashSet<string> visited)
|
||||
{
|
||||
if (!visited.Add(message.FullName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (FieldDescriptor field in message.Fields.InDeclarationOrder())
|
||||
{
|
||||
if (field.Name.Contains("password", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (field.FieldType == FieldType.Message && CarriesPassword(field.MessageType, visited))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Verifies that RedactCommandValue allows non-sensitive values only when value logging is enabled.</summary>
|
||||
[Fact]
|
||||
public void RedactCommandValue_AllowsNonSensitiveValueOnlyWhenValueLoggingIsEnabled()
|
||||
|
||||
Reference in New Issue
Block a user