diff --git a/Changelog.md b/Changelog.md index 361b4a6..5201f5d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,10 @@ # Changelog for Serilog.Enrichers.Sensitive +## 2.0.0 + +This release introduces support for configuring `MaskProperties` via JSON. As the version number indicates this is a breaking change as the C# equivalent had to be changed to match. +See the [README](README.md) for more details on configuring specific property masking options. + ## 1.7.4 - Add masking options for properties [#29](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/29) diff --git a/Directory.Build.props b/Directory.Build.props index 120f5a8..1465d73 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,8 @@ - 1.7.4.0 + 2.0.0.0 Sander van Vliet, Huibert Jan Nieuwkamer, Scott Toberman Codenizer BV - 2023 Sander van Vliet + 2025 Sander van Vliet \ No newline at end of file diff --git a/README.md b/README.md index 3f182d4..9236c49 100644 --- a/README.md +++ b/README.md @@ -318,3 +318,66 @@ An example config file: ``` > **Warning:** Contrary to what you might expect, for JSON configuration `Operators` should be used instead of `MaskingOperators`. + +### Masking specific properties + +You can configure specific options for property masking via the `MaskProperties` property via code as well as JSON. + +From code: + +```csharp +var logger = new LoggerConfiguration() + .Enrich.WithSensitiveDataMasking(options => + { + options.MaskProperties.Add(MaskProperty.WithDefaults("Email")); + }) + .WriteTo.Sink(inMemorySink) + .CreateLogger(); +``` + +Here we're using `MaskProperty.WithDefaults()` to indicate we just want to mask the `Email` property. You can specify more options like so: + +```csharp +var logger = new LoggerConfiguration() + .Enrich.WithSensitiveDataMasking(options => + { + options.MaskProperties.Add(new MaskProperty + { + Name = "Email", + Options = new MaskOptions { + ShowFirst = 3 + } + }); + }) + .WriteTo.Sink(inMemorySink) + .CreateLogger(); +``` + +Via JSON configuration (for example `appsettings.json`) you can follow a similar approach: + +```json +{ + "Serilog": { + "Using": [ + "Serilog.Enrichers.Sensitive" + ], + "Enrich": [ + { + "Name": "WithSensitiveDataMasking", + "Args": { + "options": { + "MaskProperties": [ + { + "Name": "someproperty", + "Options": { + "ShowFirst": 3 + } + } + ] + } + } + } + ] + } +} +``` \ No newline at end of file diff --git a/src/Serilog.Enrichers.Sensitive/MaskOptions.cs b/src/Serilog.Enrichers.Sensitive/MaskOptions.cs index c351abc..544f048 100644 --- a/src/Serilog.Enrichers.Sensitive/MaskOptions.cs +++ b/src/Serilog.Enrichers.Sensitive/MaskOptions.cs @@ -1,11 +1,58 @@ -namespace Serilog.Enrichers.Sensitive; +using System; -public class MaskOptions +namespace Serilog.Enrichers.Sensitive; + +public class MaskOptions : IEquatable { - public static readonly MaskOptions Default= new(); + public static readonly MaskOptions Default = new(); public const int NotSet = -1; public int ShowFirst { get; set; } = NotSet; public int ShowLast { get; set; } = NotSet; public bool PreserveLength { get; set; } = true; + public bool Equals(MaskOptions? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return ShowFirst == other.ShowFirst && ShowLast == other.ShowLast && PreserveLength == other.PreserveLength; + } + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((MaskOptions)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = ShowFirst; + hashCode = (hashCode * 397) ^ ShowLast; + hashCode = (hashCode * 397) ^ PreserveLength.GetHashCode(); + return hashCode; + } + } } \ No newline at end of file diff --git a/src/Serilog.Enrichers.Sensitive/MaskProperty.cs b/src/Serilog.Enrichers.Sensitive/MaskProperty.cs new file mode 100644 index 0000000..56761bb --- /dev/null +++ b/src/Serilog.Enrichers.Sensitive/MaskProperty.cs @@ -0,0 +1,12 @@ +namespace Serilog.Enrichers.Sensitive; + +public class MaskProperty +{ + public string Name { get; set; } + public MaskOptions Options { get; set; } = new(); + + public static MaskProperty WithDefaults(string propertyName) + { + return new MaskProperty { Name = propertyName }; + } +} \ No newline at end of file diff --git a/src/Serilog.Enrichers.Sensitive/MaskPropertyCollection.cs b/src/Serilog.Enrichers.Sensitive/MaskPropertyCollection.cs deleted file mode 100644 index bc963cc..0000000 --- a/src/Serilog.Enrichers.Sensitive/MaskPropertyCollection.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; - -namespace Serilog.Enrichers.Sensitive; - -public class MaskPropertyCollection : List -{ - private readonly Dictionary _properties = new(); - - public void Add(string propertyName) - { - _properties.Add(propertyName.ToLower(), MaskOptions.Default); - } - - public void Add(string propertyName, MaskOptions maskOptions) - { - _properties.Add(propertyName.ToLower(), maskOptions); - } - - public bool TryGetProperty(string propertyName, out MaskOptions options) - { - return _properties.TryGetValue(propertyName.ToLower(), out options); - } - - public static MaskPropertyCollection From(IEnumerable enricherOptionsMaskProperties) - { - var collection = new MaskPropertyCollection(); - - foreach (var x in enricherOptionsMaskProperties) - { - collection.Add(x, MaskOptions.Default); - } - - return collection; - } -} \ No newline at end of file diff --git a/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs b/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs index 75176f3..2445f0d 100644 --- a/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs +++ b/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs @@ -18,7 +18,7 @@ internal class SensitiveDataEnricher : ILogEventEnricher private readonly FieldInfo _messageTemplateBackingField; private readonly List _maskingOperators; private readonly string _maskValue; - private readonly MaskPropertyCollection _maskProperties; + private readonly List _maskProperties; private readonly List _excludeProperties; public SensitiveDataEnricher(SensitiveDataEnricherOptions options) @@ -33,7 +33,7 @@ public SensitiveDataEnricher( MaskingMode.Globally, DefaultMaskValue, DefaultOperators.Select(o => o.GetType().AssemblyQualifiedName), - new List(), + _maskProperties, new List()); if (options != null) @@ -114,9 +114,10 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) return (false, null); } - if(_maskProperties.TryGetProperty(property.Key, out var options)) + var matchingProperty = _maskProperties.SingleOrDefault(p => p.Name.Equals(property.Key, StringComparison.OrdinalIgnoreCase)); + if(matchingProperty != null) { - if (options == MaskOptions.Default) + if (matchingProperty.Options == MaskOptions.Default) { return (true, new ScalarValue(_maskValue)); } @@ -124,11 +125,11 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) switch (property.Value) { case ScalarValue { Value: string stringValue }: - return (true, new ScalarValue(MaskWithOptions(_maskValue, options, stringValue))); - case ScalarValue { Value: Uri uriValue } when options is UriMaskOptions uriMaskOptions: + return (true, new ScalarValue(MaskWithOptions(_maskValue, matchingProperty.Options, stringValue))); + case ScalarValue { Value: Uri uriValue } when matchingProperty.Options is UriMaskOptions uriMaskOptions: return (true, new ScalarValue(MaskWithUriOptions(_maskValue, uriMaskOptions, uriValue))); case ScalarValue { Value: Uri uriValue }: - return (true, new ScalarValue(MaskWithOptions(_maskValue, options, uriValue.ToString()))); + return (true, new ScalarValue(MaskWithOptions(_maskValue, matchingProperty.Options, uriValue.ToString()))); } } diff --git a/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs b/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs index e492a0a..7e41798 100644 --- a/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs +++ b/src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs @@ -18,7 +18,7 @@ public SensitiveDataEnricherOptions( MaskingMode mode = MaskingMode.Globally, string maskValue = SensitiveDataEnricher.DefaultMaskValue, IEnumerable? maskingOperators = null, - IEnumerable? maskProperties = null, + List? maskProperties = null, IEnumerable? excludeProperties = null, // ReSharper disable once UnusedParameter.Local as this only exists to support JSON configuration, see the Operators property below IEnumerable? operators = null) @@ -26,7 +26,7 @@ public SensitiveDataEnricherOptions( Mode = mode; MaskValue = maskValue; MaskingOperators = maskingOperators == null ? new List() : ResolveMaskingOperators(maskingOperators); - MaskProperties = maskProperties == null ? new MaskPropertyCollection() : MaskPropertyCollection.From(maskProperties); + MaskProperties = maskProperties == null ? new List() : maskProperties.ToList(); ExcludeProperties = excludeProperties?.ToList() ?? new List(); } @@ -90,7 +90,7 @@ private static List ResolveMaskingOperators(IEnumerable /// The property name is case-insensitive, when the property is present on the log message it will always be masked even if it is empty - public MaskPropertyCollection MaskProperties { get; set; } = new(); + public List MaskProperties { get; set; } = new(); /// /// The list of properties that should never be masked /// @@ -130,19 +130,4 @@ public void Apply(SensitiveDataEnricherOptions other) other.Operators = Operators; } } - - public class MaskProperty - { - public MaskProperty() - { - } - - public MaskProperty(string propertyName) - { - Name = propertyName; - } - - public string Name { get; set; } - public MaskOptions Options { get; set; } = MaskOptions.Default; - } } \ No newline at end of file diff --git a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenConfiguringFromJson.cs b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenConfiguringFromJson.cs index 65e2327..5a9f732 100644 --- a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenConfiguringFromJson.cs +++ b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenConfiguringFromJson.cs @@ -52,6 +52,30 @@ public void ReproCaseIssue25() .WithProperty("secret") .WithValue("**SECRET**"); } + + [Fact] + public void GivenMaskPropertyWithSpecificOptions_OptionsAreApplied() + { + var configuration = new ConfigurationBuilder() + .AddJsonFile("enricher-config.json") + .Build(); + + var inMemorySink = new InMemorySink(); + + var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .WriteTo.Sink(inMemorySink) + .CreateLogger(); + + logger.Information("A test message {propwithoptions}", "1234567890"); + + inMemorySink + .Should() + .HaveMessage("A test message {propwithoptions}") + .Appearing().Once() + .WithProperty("propwithoptions") + .WithValue("123*******"); + } } public class MyTestMaskingOperator : IMaskingOperator diff --git a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingDestructuredObject.cs b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingDestructuredObject.cs index 97e33dd..d4de42d 100644 --- a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingDestructuredObject.cs +++ b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingDestructuredObject.cs @@ -24,7 +24,7 @@ public WhenMaskingDestructuredObject() .Enrich.WithSensitiveDataMasking(options => { options.MaskingOperators = new List { new EmailAddressMaskingOperator() }; - options.MaskProperties.Add("SensitiveProperty"); + options.MaskProperties.Add(MaskProperty.WithDefaults("SensitiveProperty")); }) .CreateLogger(); } diff --git a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingSensitiveDataBasedOnPropertyName.cs b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingSensitiveDataBasedOnPropertyName.cs index 71f32f2..dff1f70 100644 --- a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingSensitiveDataBasedOnPropertyName.cs +++ b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingSensitiveDataBasedOnPropertyName.cs @@ -14,7 +14,7 @@ public void GivenLogMessageHasSpecificProperty_PropertyValueIsMasked() var logger = new LoggerConfiguration() .Enrich.WithSensitiveDataMasking(options => { - options.MaskProperties.Add("Email"); + options.MaskProperties.Add(MaskProperty.WithDefaults("Email")); }) .WriteTo.Sink(inMemorySink) .CreateLogger(); @@ -38,7 +38,7 @@ public void GivenLogMessageHasSpecificPropertyAndLogMessageHasPropertyButLowerCa var logger = new LoggerConfiguration() .Enrich.WithSensitiveDataMasking(options => { - options.MaskProperties.Add("Email"); + options.MaskProperties.Add(MaskProperty.WithDefaults("Email")); }) .WriteTo.Sink(inMemorySink) .CreateLogger(); @@ -86,7 +86,7 @@ public void GivenLogMessageHasSpecificPropertyAndPropertyIsExcludedAndAlsoInclud var logger = new LoggerConfiguration() .Enrich.WithSensitiveDataMasking(options => { - options.MaskProperties.Add("Email"); + options.MaskProperties.Add(MaskProperty.WithDefaults("Email")); options.ExcludeProperties.Add("Email"); }) .WriteTo.Sink(inMemorySink) diff --git a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingWithOptions.cs b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingWithOptions.cs index 84e3252..ff940da 100644 --- a/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingWithOptions.cs +++ b/test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingWithOptions.cs @@ -25,7 +25,7 @@ public void GivenMaskOptionsWithInputShorterThanNumberOfCharactersThatShouldBeSh var inMemorySink = new InMemorySink(); var logger = new LoggerConfiguration() .Enrich.WithSensitiveDataMasking( - options => options.MaskProperties.Add("Prop", new MaskOptions{ ShowFirst = showFirst, ShowLast = showLast, PreserveLength = preserveLength})) + options => options.MaskProperties.Add(new MaskProperty { Name = "Prop", Options = new MaskOptions{ ShowFirst = showFirst, ShowLast = showLast, PreserveLength = preserveLength}})) .WriteTo.Sink(inMemorySink) .CreateLogger(); @@ -52,13 +52,13 @@ public void GivenUriMaskOptions(bool showScheme, bool showHost, bool showPath, b var inMemorySink = new InMemorySink(); var logger = new LoggerConfiguration() .Enrich.WithSensitiveDataMasking( - options => options.MaskProperties.Add("Prop", new UriMaskOptions + options => options.MaskProperties.Add(new MaskProperty { Name ="Prop", Options = new UriMaskOptions { ShowScheme = showScheme, ShowHost = showHost, ShowPath = showPath, ShowQueryString = showQuery - })) + }})) .WriteTo.Sink(inMemorySink) .CreateLogger(); diff --git a/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-config.json b/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-config.json index c3415a8..d2291ea 100644 --- a/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-config.json +++ b/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-config.json @@ -10,7 +10,12 @@ "options": { "MaskValue": "**SECRET**", "MaskProperties": [ - "secret" + { "Name": "secret"}, + { "Name": "propwithoptions", + "Options": { + "ShowFirst": 3 + } + } ] } } diff --git a/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-operator-config.json b/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-operator-config.json index 5fea330..fff71cc 100644 --- a/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-operator-config.json +++ b/test/Serilog.Enrichers.Sensitive.Tests.Unit/enricher-operator-config.json @@ -1,14 +1,20 @@ { "Serilog": { - "Using": [ "Serilog.Enrichers.Sensitive" ], - "Enrich": [ { + "Using": [ + "Serilog.Enrichers.Sensitive" + ], + "Enrich": [ + { "Name": "WithSensitiveDataMasking", "Args": { - "options": { - "MaskValue": "MASK FROM JSON", - "Operators": [ "Serilog.Enrichers.Sensitive.Tests.Unit.MyTestMaskingOperator, Serilog.Enrichers.Sensitive.Tests.Unit" ] - } + "options": { + "MaskValue": "MASK FROM JSON", + "Operators": [ + "Serilog.Enrichers.Sensitive.Tests.Unit.MyTestMaskingOperator, Serilog.Enrichers.Sensitive.Tests.Unit" + ] + } } - }] + } + ] } } \ No newline at end of file