9354d69bf9
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.
165 lines
6.3 KiB
C#
165 lines
6.3 KiB
C#
namespace ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// Redacts sensitive information from log entries.
|
|
/// </summary>
|
|
public static class GatewayLogRedactor
|
|
{
|
|
/// <summary>Placeholder for redacted values.</summary>
|
|
public const string RedactedValue = "[redacted]";
|
|
|
|
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"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Authorization schemes whose name may survive redaction. Anything outside this list is
|
|
/// dropped whole: an unrecognized leading word is as likely to be credential material as it
|
|
/// is to be a scheme, so it is not worth the leak.
|
|
/// </summary>
|
|
private static readonly string[] KnownAuthorizationSchemes =
|
|
[
|
|
"Bearer",
|
|
"Basic",
|
|
"Digest",
|
|
"Negotiate",
|
|
"NTLM",
|
|
"ApiKey",
|
|
"Token",
|
|
];
|
|
|
|
/// <summary>Prefix identifying a gateway-issued API key.</summary>
|
|
private const string GatewayKeyPrefix = "mxgw_";
|
|
|
|
/// <summary>Upper bound on a key id kept in the clear; a longer run is treated as secret material.</summary>
|
|
private const int MaxKeyIdLength = 64;
|
|
|
|
/// <summary>
|
|
/// Determines whether a command method bears credentials.
|
|
/// </summary>
|
|
/// <param name="commandMethod">The command method name to check.</param>
|
|
/// <returns><see langword="true"/> if the command method carries credentials; otherwise <see langword="false"/>.</returns>
|
|
public static bool IsCredentialBearingCommand(string? commandMethod)
|
|
{
|
|
return commandMethod is not null
|
|
&& SensitiveCommandMethods.Contains(commandMethod);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redacts the credential portion of an authorization header value.
|
|
/// </summary>
|
|
/// <param name="authorizationHeader">The authorization header value to redact.</param>
|
|
/// <returns>The header with the credential redacted, or the original value when it is null or blank.</returns>
|
|
public static string? RedactApiKey(string? authorizationHeader)
|
|
{
|
|
return RedactClientIdentity(authorizationHeader);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redacts the credential carried by a client identity. Redaction fails closed: only a
|
|
/// gateway-issued API key keeps its <c>mxgw_<key-id>_</c> shape (so operators can tell keys
|
|
/// apart in logs), and only a recognized scheme keeps its name. Every other value — a foreign
|
|
/// bearer token, a scheme-less string, junk — is replaced whole, because nothing that reaches
|
|
/// this method is known to be safe to log.
|
|
/// </summary>
|
|
/// <param name="clientIdentity">The client identity string to redact.</param>
|
|
/// <returns>The redacted client identity, or the original value when it is null or blank.</returns>
|
|
public static string? RedactClientIdentity(string? clientIdentity)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(clientIdentity))
|
|
{
|
|
return clientIdentity;
|
|
}
|
|
|
|
ReadOnlySpan<char> value = clientIdentity.AsSpan().Trim();
|
|
int separatorIndex = value.IndexOf(' ');
|
|
|
|
if (separatorIndex < 0)
|
|
{
|
|
// A single token carries no scheme, so the token itself is the credential.
|
|
return RedactedValue;
|
|
}
|
|
|
|
ReadOnlySpan<char> scheme = value[..separatorIndex];
|
|
ReadOnlySpan<char> credential = value[(separatorIndex + 1)..].Trim();
|
|
|
|
if (credential.IsEmpty || !IsKnownAuthorizationScheme(scheme))
|
|
{
|
|
return RedactedValue;
|
|
}
|
|
|
|
return credential.StartsWith(GatewayKeyPrefix, StringComparison.OrdinalIgnoreCase)
|
|
? $"{scheme} {GatewayKeyPrefix}{RedactKeyId(credential)}"
|
|
: $"{scheme} {RedactedValue}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the trailing portion of a gateway API key: the key id when the key is well formed,
|
|
/// otherwise nothing but the placeholder.
|
|
/// </summary>
|
|
/// <param name="credential">The credential, known to start with the gateway key prefix.</param>
|
|
/// <returns>The <c><key-id>_[redacted]</c> tail, or just the placeholder.</returns>
|
|
private static string RedactKeyId(ReadOnlySpan<char> credential)
|
|
{
|
|
ReadOnlySpan<char> remainder = credential[GatewayKeyPrefix.Length..];
|
|
int secretIndex = remainder.IndexOf('_');
|
|
|
|
// No separator means no secret boundary to trust, so the whole remainder is treated as secret.
|
|
return secretIndex is <= 0 or > MaxKeyIdLength
|
|
? RedactedValue
|
|
: $"{remainder[..secretIndex]}_{RedactedValue}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether a leading word is a recognized authorization scheme.
|
|
/// </summary>
|
|
/// <param name="scheme">The candidate scheme word.</param>
|
|
/// <returns><see langword="true"/> when the word may survive redaction; otherwise <see langword="false"/>.</returns>
|
|
private static bool IsKnownAuthorizationScheme(ReadOnlySpan<char> scheme)
|
|
{
|
|
foreach (string knownScheme in KnownAuthorizationSchemes)
|
|
{
|
|
if (scheme.Equals(knownScheme, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redacts a command value if it contains credentials or value logging is disabled.
|
|
/// </summary>
|
|
/// <param name="commandMethod">The command method name to check for credentials.</param>
|
|
/// <param name="value">The command value to redact.</param>
|
|
/// <param name="valueLoggingEnabled">Whether value logging is enabled.</param>
|
|
/// <returns>The redacted placeholder, the original value, or <see langword="null"/> when <paramref name="value"/> is null.</returns>
|
|
public static object? RedactCommandValue(
|
|
string? commandMethod,
|
|
object? value,
|
|
bool valueLoggingEnabled = false)
|
|
{
|
|
if (value is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!valueLoggingEnabled || IsCredentialBearingCommand(commandMethod))
|
|
{
|
|
return RedactedValue;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|