diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/FilenameMaskRuleProvider.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/FilenameMaskRuleProvider.cs index 5356ddd90d..fecd6ec580 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/FilenameMaskRuleProvider.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/FilenameMaskRuleProvider.cs @@ -25,6 +25,7 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider "env", "environment", "manifest", + "log", }.ToFrozenSet(StringComparer.OrdinalIgnoreCase); public IEnumerable GetRules() @@ -61,6 +62,11 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider return full; } + if (IsVersionSegment(file)) + { + return full; + } + string stem, ext; if (dot > 0 && dot < file.Length - 1) { @@ -106,4 +112,30 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider var maskedCount = Math.Max(1, stem.Length - keep); return stem[..keep] + new string('*', maskedCount); } + + private static bool IsVersionSegment(string file) + { + var dotIndex = file.IndexOf('.'); + if (dotIndex <= 0 || dotIndex == file.Length - 1) + { + return false; + } + + var hasDot = false; + foreach (var ch in file) + { + if (ch == '.') + { + hasDot = true; + continue; + } + + if (!char.IsDigit(ch)) + { + return false; + } + } + + return hasDot; + } } diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/NetworkRuleProvider.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/NetworkRuleProvider.cs index 4c352ff892..6be99b6ff6 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/NetworkRuleProvider.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/NetworkRuleProvider.cs @@ -11,7 +11,9 @@ internal sealed partial class NetworkRuleProvider : ISanitizationRuleProvider { public IEnumerable GetRules() { - yield return new(Ipv4Rx(), "[IP4_REDACTED]", "IP addresses"); + // Disabled for now as these rules can be too aggressive and cause over-sanitization, especially in scenarios like + // error report sanitization where we want to preserve as much useful information as possible while still protecting sensitive data. + // yield return new(Ipv4Rx(), "[IP4_REDACTED]", "IP addresses"); yield return new(Ipv6BracketedRx(), "[IP6_REDACTED]", "IPv6 addresses (bracketed/with port)"); yield return new(Ipv6Rx(), "[IP6_REDACTED]", "IPv6 addresses"); yield return new(MacAddressRx(), "[MAC_ADDRESS_REDACTED]", "MAC addresses"); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/PiiRuleProvider.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/PiiRuleProvider.cs index 964c6d83df..ae8f167f7c 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/PiiRuleProvider.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Services/Sanitizer/PiiRuleProvider.cs @@ -25,52 +25,56 @@ internal sealed partial class PiiRuleProvider : ISanitizationRuleProvider private static partial Regex EmailRx(); [GeneratedRegex(""" - (?xi) - # ---------- boundaries ---------- - (? require separators between blocks (avoid plain big ints) - (?:\(\d{1,4}\)|\d{1,4}) - (?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6} - ) + (?:\(\d{1,4}\)|\d{1,4}) + (?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6} + ) - # ---------- optional extension ---------- - (?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?\d{1,6}))? + # ---------- optional extension ---------- + (?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?\d{1,6}))? - (?!-\w) # don't end just before '-letter'/'-digit' - """, - SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace, SanitizerDefaults.DefaultMatchTimeoutMs)] + # ---------- end boundary (allow whitespace/newlines at edges) ---------- + (?!-\w) # don't end just before '-letter'/'-digit' + (?!\w) # don't be immediately followed by a word char + """, + SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace, + SanitizerDefaults.DefaultMatchTimeoutMs)] private static partial Regex PhoneRx(); [GeneratedRegex(@"\b\d{3}-\d{2}-\d{4}\b", diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.TestData.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.TestData.cs index 6d27172fa2..54c7ba92d7 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.TestData.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.TestData.cs @@ -6,42 +6,42 @@ namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer; public partial class ErrorReportSanitizerTests { - private static class TestData + internal static class TestData { internal static string Input => - $""" - HRESULT: 0x80004005 - HRESULT: -2147467259 + $""" + HRESULT: 0x80004005 + HRESULT: -2147467259 - Here is e-mail address - IPv4 address: 192.168.100.1 - IPv4 loopback address: 127.0.0.1 - MAC address: 00-14-22-01-23-45 - IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - IPv6 loopback address: ::1 - Password: P@ssw0rd123! - Password=secret - Api key: 1234567890abcdef - PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb - InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com; - X-API-key: 1234567890abcdef - Pet-Shop-Subscription-Key: 1234567890abcdef - Here is a user name {Environment.UserName} - And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder - Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal - Here is machine name {Environment.MachineName} - JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 - User email john.doe@company.com failed validation - File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt - Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test - Phone number 555-123-4567 is invalid - API key abc123def456ghi789jkl012mno345pqr678 expired - Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123 - Error accessing file://C:/Users/john.doe/Documents/confidential.pdf - JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret - FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv - Email service error: mailto:admin@internal-company.com?subject=Alert - """; + Here is e-mail address + IPv4 address: 192.168.100.1 + IPv4 loopback address: 127.0.0.1 + MAC address: 00-14-22-01-23-45 + IPv6 address: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + IPv6 loopback address: ::1 + Password: P@ssw0rd123! + Password=secret + Api key: 1234567890abcdef + PostgreSQL connection string: Host=localhost;Username=postgres;Password=secret;Database=mydb + InstrumentationKey=00000000-0000-0000-0000-000000000000;EndpointSuffix=ai.contoso.com; + X-API-key: 1234567890abcdef + Pet-Shop-Subscription-Key: 1234567890abcdef + Here is a user name {Environment.UserName} + And here is a profile path {Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)}\RandomFolder + Here is a local app data path {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal + Here is machine name {Environment.MachineName} + JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30 + User email john.doe@company.com failed validation + File not found: {Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)}\\secret.txt + Connection string: Server=localhost;User ID=admin;Password=secret123;Database=test + Phone number 555-123-4567 is invalid + API key abc123def456ghi789jkl012mno345pqr678 expired + Failed to connect to https://api.internal-company.com/users/12345?token=secret_abc123 + Error accessing file://C:/Users/john.doe/Documents/confidential.pdf + JDBC connection failed: jdbc://database-server:5432/userdb?user=admin&password=secret + FTP upload error: ftp://internal-server.company.com/uploads/user_data.csv + Email service error: mailto:admin@internal-company.com?subject=Alert + """; public const string Expected = $""" @@ -49,8 +49,8 @@ public partial class ErrorReportSanitizerTests HRESULT: -2147467259 Here is e-mail address <[EMAIL_REDACTED]> - IPv4 address: [IP4_REDACTED] - IPv4 loopback address: [IP4_REDACTED] + IPv4 address: 192.168.100.1 + IPv4 loopback address: 127.0.0.1 MAC address: [MAC_ADDRESS_REDACTED] IPv6 address: [IP6_REDACTED] IPv6 loopback address: [IP6_REDACTED] @@ -77,5 +77,55 @@ public partial class ErrorReportSanitizerTests FTP upload error: [URL_REDACTED] Email service error: mailto:[EMAIL_REDACTED]?subject=Alert """; + + internal static string Input2 => + $""" + ============================================================ + Hello World! Command Palette is starting. + + Application: + App version: 0.0.1.0 + Packaging flavor: Packaged + Is elevated: no + + Environment: + OS version: Microsoft Windows 10.0.26220 + OS architecture: X64 + Runtime identifier: win-x64 + Framework: .NET 9.0.13 + Process architecture: X64 + Culture: cs-CZ + UI culture: en-US + + Paths: + Log directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Microsoft\PowerToys\CmdPal\Logs\0.0.1.0 + Config directory: {Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}\Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState + ============================================================ + """; + + public const string Expected2 = + """ + ============================================================ + Hello World! Command Palette is starting. + + Application: + App version: 0.0.1.0 + Packaging flavor: Packaged + Is elevated: no + + Environment: + OS version: Microsoft Windows 10.0.26220 + OS architecture: X64 + Runtime identifier: win-x64 + Framework: .NET 9.0.13 + Process architecture: X64 + Culture: cs-CZ + UI culture: en-US + + Paths: + Log directory: [LOCALAPPLICATIONDATA_DIR]Microsoft\PowerToys\CmdPal\Logs\0.0.1.0 + Config directory: [LOCALAPPLICATIONDATA_DIR]Packages\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe\LocalState + ============================================================ + """; } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.cs index 1ab57acd2e..294279b5fc 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/ErrorReportSanitizerTests.cs @@ -22,4 +22,18 @@ public partial class ErrorReportSanitizerTests // Assert Assert.AreEqual(TestData.Expected, result); } + + [TestMethod] + public void Sanitize_ShouldNotMaskTooMuchPiiInErrorReport() + { + // Arrange + var reportSanitizer = new ErrorReportSanitizer(); + var input = TestData.Input2; + + // Act + var result = reportSanitizer.Sanitize(input); + + // Assert + Assert.AreEqual(TestData.Expected2, result); + } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/FilenameMaskRuleProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/FilenameMaskRuleProviderTests.cs new file mode 100644 index 0000000000..6c5875fd60 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/FilenameMaskRuleProviderTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Common.UnitTests.TestUtils; +using Microsoft.CmdPal.Core.Common.Services.Sanitizer; +using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction; + +namespace Microsoft.CmdPal.Common.UnitTests.Services.Sanitizer; + +[TestClass] +public class FilenameMaskRuleProviderTests +{ + [TestMethod] + public void GetRules_ShouldReturnExpectedRules() + { + // Arrange + var provider = new FilenameMaskRuleProvider(); + + // Act + var rules = provider.GetRules(); + + // Assert + var ruleList = new List(rules); + Assert.AreEqual(1, ruleList.Count); + Assert.AreEqual("Mask filename in any path", ruleList[0].Description); + } + + [DataTestMethod] + [DataRow(@"C:\Users\Alice\Documents\secret.txt", @"C:\Users\Alice\Documents\se****.txt")] + [DataRow(@"logs\error-report.log", @"logs\er**********.log")] + [DataRow(@"/var/logs/trace.json", @"/var/logs/tr***.json")] + public void FilenameRules_ShouldMaskFileNamesInPaths(string input, string expected) + { + // Arrange + var provider = new FilenameMaskRuleProvider(); + + // Act + var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules()); + + // Assert + Assert.AreEqual(expected, result); + } + + [DataTestMethod] + [DataRow("C:\\Users\\Alice\\Documents\\", "C:\\Users\\Alice\\Documents\\")] + [DataRow(@"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4", @"C:\Users\Alice\PowerToys\CmdPal\Logs\1.2.3.4")] + [DataRow(@"C:\Users\Alice\appsettings.json", @"C:\Users\Alice\appsettings.json")] + [DataRow(@"C:\Users\Alice\.env", @"C:\Users\Alice\.env")] + [DataRow(@"logs\readme", @"logs\readme")] + public void FilenameRules_ShouldNotMaskNonSensitivePatterns(string input, string expected) + { + // Arrange + var provider = new FilenameMaskRuleProvider(); + + // Act + var result = SanitizerTestHelper.ApplyRules(input, provider.GetRules()); + + // Assert + Assert.AreEqual(expected, result); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/PiiRuleProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/PiiRuleProviderTests.cs index ac490f5a6b..3f2d3c92e7 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/PiiRuleProviderTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Services/Sanitizer/PiiRuleProviderTests.cs @@ -54,6 +54,8 @@ public class PiiRuleProviderTests [DataRow("Two numbers: 123-456-7890 and +420 777123456", "Two numbers: [PHONE_REDACTED] and [PHONE_REDACTED]")] [DataRow("Czech phone +420 777 123 456", "Czech phone [PHONE_REDACTED]")] [DataRow("Slovak phone +421 777 12 34 56", "Slovak phone [PHONE_REDACTED]")] + [DataRow("Version 1.2.3.4", "Version 1.2.3.4")] + [DataRow("OS version: Microsoft Windows 10.0.26220", "OS version: Microsoft Windows 10.0.26220")] [DataRow("No phone number here", "No phone number here")] public void PhoneRules_ShouldMaskPhoneNumbers(string input, string expected) { @@ -104,6 +106,8 @@ public class PiiRuleProviderTests [DataRow("GUID: 123e4567-e89b-12d3-a456-426614174000", "GUID: 123e4567-e89b-12d3-a456-426614174000")] [DataRow("Timestamp: 2023-10-05T14:32:10Z", "Timestamp: 2023-10-05T14:32:10Z")] [DataRow("Version: 1.2.3", "Version: 1.2.3")] + [DataRow("Version: 1.2.3.4", "Version: 1.2.3.4")] + [DataRow("Version: 0.2.3.4", "Version: 0.2.3.4")] [DataRow("Version: 10.0.22631.3448", "Version: 10.0.22631.3448")] [DataRow("MAC: 00:1A:2B:3C:4D:5E", "MAC: 00:1A:2B:3C:4D:5E")] [DataRow("Date: 2023-10-05", "Date: 2023-10-05")]