diff --git a/src/GmRelay.Bot/Infrastructure/Logging/SecretRedactor.cs b/src/GmRelay.Bot/Infrastructure/Logging/SecretRedactor.cs new file mode 100644 index 0000000..680e66f --- /dev/null +++ b/src/GmRelay.Bot/Infrastructure/Logging/SecretRedactor.cs @@ -0,0 +1,47 @@ +using System.Text.RegularExpressions; +using Npgsql; + +namespace GmRelay.Bot.Infrastructure.Logging; + +public static partial class SecretRedactor +{ + public static string RedactConnectionString(string? connectionString) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + return string.Empty; + } + + try + { + var builder = new NpgsqlConnectionStringBuilder(connectionString); + if (!string.IsNullOrWhiteSpace(builder.Password)) + { + builder.Password = "***"; + } + + return builder.ToString(); + } + catch (ArgumentException) + { + return RedactText(connectionString); + } + } + + public static string RedactText(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return string.Empty; + } + + return SecretKeyValueRegex().Replace( + text, + static match => $"{match.Groups["key"].Value}={GetRedactedValue()}"); + } + + private static string GetRedactedValue() => "***"; + + [GeneratedRegex(@"(?password|pwd|passwd|token|secret|api[-_]?key)\s*=\s*(?[^;\s,]+)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex SecretKeyValueRegex(); +} diff --git a/src/GmRelay.Bot/Program.cs b/src/GmRelay.Bot/Program.cs index 5f8a7bb..9cba23f 100644 --- a/src/GmRelay.Bot/Program.cs +++ b/src/GmRelay.Bot/Program.cs @@ -4,6 +4,7 @@ using GmRelay.Bot.Features.Reminders.SendJoinLink; using GmRelay.Bot.Features.Sessions.CreateSession; using GmRelay.Bot.Features.Sessions.RescheduleSession; using GmRelay.Bot.Infrastructure.Database; +using GmRelay.Bot.Infrastructure.Logging; using GmRelay.Bot.Infrastructure.Scheduling; using GmRelay.Bot.Infrastructure.Telegram; using Npgsql; @@ -20,11 +21,16 @@ builder.AddServiceDefaults(); builder.Services.AddSingleton(sp => { var config = sp.GetRequiredService(); + var loggerFactory = sp.GetRequiredService(); var connectionString = config.GetConnectionString("gmrelaydb") ?? throw new InvalidOperationException( "ConnectionStrings:gmrelaydb is required. Set via environment variable ConnectionStrings__gmrelaydb."); - - Console.WriteLine($"[DBG] Master ConnectionString => {connectionString}"); + + var logger = loggerFactory.CreateLogger("GmRelay.Bot.Startup"); + logger.LogInformation( + "Configured PostgreSQL data source with connection string {ConnectionString}", + SecretRedactor.RedactConnectionString(connectionString)); + return NpgsqlDataSource.Create(connectionString); }); diff --git a/tests/GmRelay.Bot.Tests/Infrastructure/Logging/SecretRedactorTests.cs b/tests/GmRelay.Bot.Tests/Infrastructure/Logging/SecretRedactorTests.cs new file mode 100644 index 0000000..c031700 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Infrastructure/Logging/SecretRedactorTests.cs @@ -0,0 +1,31 @@ +using GmRelay.Bot.Infrastructure.Logging; + +namespace GmRelay.Bot.Tests.Infrastructure.Logging; + +public sealed class SecretRedactorTests +{ + [Fact] + public void RedactConnectionString_ShouldMaskDatabasePassword() + { + var result = SecretRedactor.RedactConnectionString( + "Host=localhost;Port=5432;Database=gmrelay;Username=gmrelay;Password=super-secret"); + + Assert.Contains("Password=***", result); + Assert.DoesNotContain("super-secret", result); + Assert.Contains("Host=localhost", result); + } + + [Fact] + public void RedactText_ShouldMaskKnownSecretKeys() + { + var result = SecretRedactor.RedactText( + "Password=super-secret Token=telegram-token apiKey=service-key"); + + Assert.DoesNotContain("super-secret", result); + Assert.DoesNotContain("telegram-token", result); + Assert.DoesNotContain("service-key", result); + Assert.Contains("Password=***", result); + Assert.Contains("Token=***", result); + Assert.Contains("apiKey=***", result); + } +}