Code/SboxSuperuser/Engine/Audit/AuditRedaction.cs

Utility for audit redaction. It computes redacted argument values and target player Steam IDs for commands, hides sensitive argument names and only exposes safe primitive values or marks them as [redacted].

using System.Globalization;

namespace SboxSuperuser.Engine.Audit;

internal static class AuditRedaction
{
	internal static IReadOnlyDictionary<string, string> Arguments( CommandDescriptor descriptor, CommandArguments arguments )
	{
		var values = new Dictionary<string, string>( StringComparer.Ordinal );
		foreach ( var argument in descriptor?.Arguments ?? Array.Empty<CommandArgumentDescriptor>() )
		{
			if ( arguments is not null && !arguments.Contains( argument.Name ) )
			{
				continue;
			}

			values[ argument.Name ] = argument.Audit == CommandArgumentAudit.SafeValue && !SensitiveName( argument.Name )
				&& arguments is not null ? SafeValue( argument, arguments ) : "[redacted]";
		}

		return values;
	}

	internal static IReadOnlyList<string> Targets( CommandDescriptor descriptor, CommandArguments arguments )
	{
		return arguments is null ? Array.Empty<string>() : descriptor.Arguments
			.Where( argument => argument.Kind == CommandArgumentKind.Player && arguments.Contains( argument.Name ) )
			.Select( argument => arguments.Get<PlayerReference>( argument.Name ).SteamId ).Distinct( StringComparer.Ordinal ).ToArray();
	}

	internal static bool SensitiveName( string name ) => name.Contains( "password", StringComparison.OrdinalIgnoreCase )
		|| name.Contains( "secret", StringComparison.OrdinalIgnoreCase ) || name.Contains( "token", StringComparison.OrdinalIgnoreCase )
		|| name.Contains( "credential", StringComparison.OrdinalIgnoreCase ) || name.Contains( "apikey", StringComparison.OrdinalIgnoreCase )
		|| name.Contains( "api_key", StringComparison.OrdinalIgnoreCase ) || name == "pin";

	private static string SafeValue( CommandArgumentDescriptor descriptor, CommandArguments arguments )
	{
		return descriptor.Kind switch
		{
			CommandArgumentKind.Integer => arguments.Get<long>( descriptor.Name ).ToString( CultureInfo.InvariantCulture ),
			CommandArgumentKind.Number => arguments.Get<double>( descriptor.Name ).ToString( "R", CultureInfo.InvariantCulture ),
			CommandArgumentKind.Boolean => arguments.Get<bool>( descriptor.Name ) ? "true" : "false",
			CommandArgumentKind.Enum => arguments.Get<string>( descriptor.Name ),
			CommandArgumentKind.Player => arguments.Get<PlayerReference>( descriptor.Name ).SteamId,
			CommandArgumentKind.Duration => Duration( arguments.Get<CommandDuration>( descriptor.Name ) ),
			_ => "[redacted]"
		};
	}

	private static string Duration( CommandDuration duration ) => duration.IsPermanent ? "permanent"
		: (duration.Value.Value.Ticks / TimeSpan.TicksPerSecond).ToString( CultureInfo.InvariantCulture ) + "s";
}