Skip to content

Commit ba35257

Browse files
Merge pull request #43 from serilog-contrib/feature/json-config-fix
MaskProperties configuration support
2 parents fcee321 + 0606473 commit ba35257

14 files changed

+193
-80
lines changed

Changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog for Serilog.Enrichers.Sensitive
22

3+
## 2.0.0
4+
5+
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.
6+
See the [README](README.md) for more details on configuring specific property masking options.
7+
38
## 1.7.4
49

510
- Add masking options for properties [#29](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/29)

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>1.7.4.0</Version>
3+
<Version>2.0.0.0</Version>
44
<Authors>Sander van Vliet, Huibert Jan Nieuwkamer, Scott Toberman</Authors>
55
<Company>Codenizer BV</Company>
6-
<Copyright>2023 Sander van Vliet</Copyright>
6+
<Copyright>2025 Sander van Vliet</Copyright>
77
</PropertyGroup>
88
</Project>

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,66 @@ An example config file:
318318
```
319319

320320
> **Warning:** Contrary to what you might expect, for JSON configuration `Operators` should be used instead of `MaskingOperators`.
321+
322+
### Masking specific properties
323+
324+
You can configure specific options for property masking via the `MaskProperties` property via code as well as JSON.
325+
326+
From code:
327+
328+
```csharp
329+
var logger = new LoggerConfiguration()
330+
.Enrich.WithSensitiveDataMasking(options =>
331+
{
332+
options.MaskProperties.Add(MaskProperty.WithDefaults("Email"));
333+
})
334+
.WriteTo.Sink(inMemorySink)
335+
.CreateLogger();
336+
```
337+
338+
Here we're using `MaskProperty.WithDefaults()` to indicate we just want to mask the `Email` property. You can specify more options like so:
339+
340+
```csharp
341+
var logger = new LoggerConfiguration()
342+
.Enrich.WithSensitiveDataMasking(options =>
343+
{
344+
options.MaskProperties.Add(new MaskProperty
345+
{
346+
Name = "Email",
347+
Options = new MaskOptions {
348+
ShowFirst = 3
349+
}
350+
});
351+
})
352+
.WriteTo.Sink(inMemorySink)
353+
.CreateLogger();
354+
```
355+
356+
Via JSON configuration (for example `appsettings.json`) you can follow a similar approach:
357+
358+
```json
359+
{
360+
"Serilog": {
361+
"Using": [
362+
"Serilog.Enrichers.Sensitive"
363+
],
364+
"Enrich": [
365+
{
366+
"Name": "WithSensitiveDataMasking",
367+
"Args": {
368+
"options": {
369+
"MaskProperties": [
370+
{
371+
"Name": "someproperty",
372+
"Options": {
373+
"ShowFirst": 3
374+
}
375+
}
376+
]
377+
}
378+
}
379+
}
380+
]
381+
}
382+
}
383+
```
Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,58 @@
1-
namespace Serilog.Enrichers.Sensitive;
1+
using System;
22

3-
public class MaskOptions
3+
namespace Serilog.Enrichers.Sensitive;
4+
5+
public class MaskOptions : IEquatable<MaskOptions>
46
{
5-
public static readonly MaskOptions Default= new();
7+
public static readonly MaskOptions Default = new();
68
public const int NotSet = -1;
79
public int ShowFirst { get; set; } = NotSet;
810
public int ShowLast { get; set; } = NotSet;
911
public bool PreserveLength { get; set; } = true;
1012

13+
public bool Equals(MaskOptions? other)
14+
{
15+
if (other is null)
16+
{
17+
return false;
18+
}
19+
20+
if (ReferenceEquals(this, other))
21+
{
22+
return true;
23+
}
24+
25+
return ShowFirst == other.ShowFirst && ShowLast == other.ShowLast && PreserveLength == other.PreserveLength;
26+
}
27+
28+
public override bool Equals(object? obj)
29+
{
30+
if (obj is null)
31+
{
32+
return false;
33+
}
34+
35+
if (ReferenceEquals(this, obj))
36+
{
37+
return true;
38+
}
39+
40+
if (obj.GetType() != GetType())
41+
{
42+
return false;
43+
}
44+
45+
return Equals((MaskOptions)obj);
46+
}
47+
48+
public override int GetHashCode()
49+
{
50+
unchecked
51+
{
52+
var hashCode = ShowFirst;
53+
hashCode = (hashCode * 397) ^ ShowLast;
54+
hashCode = (hashCode * 397) ^ PreserveLength.GetHashCode();
55+
return hashCode;
56+
}
57+
}
1158
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Serilog.Enrichers.Sensitive;
2+
3+
public class MaskProperty
4+
{
5+
public string Name { get; set; }
6+
public MaskOptions Options { get; set; } = new();
7+
8+
public static MaskProperty WithDefaults(string propertyName)
9+
{
10+
return new MaskProperty { Name = propertyName };
11+
}
12+
}

src/Serilog.Enrichers.Sensitive/MaskPropertyCollection.cs

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal class SensitiveDataEnricher : ILogEventEnricher
1818
private readonly FieldInfo _messageTemplateBackingField;
1919
private readonly List<IMaskingOperator> _maskingOperators;
2020
private readonly string _maskValue;
21-
private readonly MaskPropertyCollection _maskProperties;
21+
private readonly List<MaskProperty> _maskProperties;
2222
private readonly List<string> _excludeProperties;
2323

2424
public SensitiveDataEnricher(SensitiveDataEnricherOptions options)
@@ -33,7 +33,7 @@ public SensitiveDataEnricher(
3333
MaskingMode.Globally,
3434
DefaultMaskValue,
3535
DefaultOperators.Select(o => o.GetType().AssemblyQualifiedName),
36-
new List<string>(),
36+
_maskProperties,
3737
new List<string>());
3838

3939
if (options != null)
@@ -114,21 +114,22 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
114114
return (false, null);
115115
}
116116

117-
if(_maskProperties.TryGetProperty(property.Key, out var options))
117+
var matchingProperty = _maskProperties.SingleOrDefault(p => p.Name.Equals(property.Key, StringComparison.OrdinalIgnoreCase));
118+
if(matchingProperty != null)
118119
{
119-
if (options == MaskOptions.Default)
120+
if (matchingProperty.Options == MaskOptions.Default)
120121
{
121122
return (true, new ScalarValue(_maskValue));
122123
}
123124

124125
switch (property.Value)
125126
{
126127
case ScalarValue { Value: string stringValue }:
127-
return (true, new ScalarValue(MaskWithOptions(_maskValue, options, stringValue)));
128-
case ScalarValue { Value: Uri uriValue } when options is UriMaskOptions uriMaskOptions:
128+
return (true, new ScalarValue(MaskWithOptions(_maskValue, matchingProperty.Options, stringValue)));
129+
case ScalarValue { Value: Uri uriValue } when matchingProperty.Options is UriMaskOptions uriMaskOptions:
129130
return (true, new ScalarValue(MaskWithUriOptions(_maskValue, uriMaskOptions, uriValue)));
130131
case ScalarValue { Value: Uri uriValue }:
131-
return (true, new ScalarValue(MaskWithOptions(_maskValue, options, uriValue.ToString())));
132+
return (true, new ScalarValue(MaskWithOptions(_maskValue, matchingProperty.Options, uriValue.ToString())));
132133
}
133134
}
134135

src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ public SensitiveDataEnricherOptions(
1818
MaskingMode mode = MaskingMode.Globally,
1919
string maskValue = SensitiveDataEnricher.DefaultMaskValue,
2020
IEnumerable<string>? maskingOperators = null,
21-
IEnumerable<string>? maskProperties = null,
21+
List<MaskProperty>? maskProperties = null,
2222
IEnumerable<string>? excludeProperties = null,
2323
// ReSharper disable once UnusedParameter.Local as this only exists to support JSON configuration, see the Operators property below
2424
IEnumerable<string>? operators = null)
2525
{
2626
Mode = mode;
2727
MaskValue = maskValue;
2828
MaskingOperators = maskingOperators == null ? new List<IMaskingOperator>() : ResolveMaskingOperators(maskingOperators);
29-
MaskProperties = maskProperties == null ? new MaskPropertyCollection() : MaskPropertyCollection.From(maskProperties);
29+
MaskProperties = maskProperties == null ? new List<MaskProperty>() : maskProperties.ToList();
3030
ExcludeProperties = excludeProperties?.ToList() ?? new List<string>();
3131
}
3232

@@ -90,7 +90,7 @@ private static List<IMaskingOperator> ResolveMaskingOperators(IEnumerable<string
9090
/// The list of properties that should always be masked regardless of whether they match the pattern of any of the masking operators
9191
/// </summary>
9292
/// <remarks>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</remarks>
93-
public MaskPropertyCollection MaskProperties { get; set; } = new();
93+
public List<MaskProperty> MaskProperties { get; set; } = new();
9494
/// <summary>
9595
/// The list of properties that should never be masked
9696
/// </summary>
@@ -130,19 +130,4 @@ public void Apply(SensitiveDataEnricherOptions other)
130130
other.Operators = Operators;
131131
}
132132
}
133-
134-
public class MaskProperty
135-
{
136-
public MaskProperty()
137-
{
138-
}
139-
140-
public MaskProperty(string propertyName)
141-
{
142-
Name = propertyName;
143-
}
144-
145-
public string Name { get; set; }
146-
public MaskOptions Options { get; set; } = MaskOptions.Default;
147-
}
148133
}

test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenConfiguringFromJson.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ public void ReproCaseIssue25()
5252
.WithProperty("secret")
5353
.WithValue("**SECRET**");
5454
}
55+
56+
[Fact]
57+
public void GivenMaskPropertyWithSpecificOptions_OptionsAreApplied()
58+
{
59+
var configuration = new ConfigurationBuilder()
60+
.AddJsonFile("enricher-config.json")
61+
.Build();
62+
63+
var inMemorySink = new InMemorySink();
64+
65+
var logger = new LoggerConfiguration()
66+
.ReadFrom.Configuration(configuration)
67+
.WriteTo.Sink(inMemorySink)
68+
.CreateLogger();
69+
70+
logger.Information("A test message {propwithoptions}", "1234567890");
71+
72+
inMemorySink
73+
.Should()
74+
.HaveMessage("A test message {propwithoptions}")
75+
.Appearing().Once()
76+
.WithProperty("propwithoptions")
77+
.WithValue("123*******");
78+
}
5579
}
5680

5781
public class MyTestMaskingOperator : IMaskingOperator

test/Serilog.Enrichers.Sensitive.Tests.Unit/WhenMaskingDestructuredObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public WhenMaskingDestructuredObject()
2424
.Enrich.WithSensitiveDataMasking(options =>
2525
{
2626
options.MaskingOperators = new List<IMaskingOperator> { new EmailAddressMaskingOperator() };
27-
options.MaskProperties.Add("SensitiveProperty");
27+
options.MaskProperties.Add(MaskProperty.WithDefaults("SensitiveProperty"));
2828
})
2929
.CreateLogger();
3030
}

0 commit comments

Comments
 (0)