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