Skip to content

Commit ff4ac0c

Browse files
Add support to always mask a property
1 parent d1059e6 commit ff4ac0c

File tree

7 files changed

+155
-11
lines changed

7 files changed

+155
-11
lines changed

CHANGELOG

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## 1.1.0
44

5-
- Add support to supply a custom mask value
5+
- Add support to supply a custom mask value [#6](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/6)
6+
- Add support to always mask a property regardless of value [#7](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/7)
67

78
## 1.0.0
89

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ In case the default mask value `***MASKED***` is not what you want, you can supp
101101

102102
```csharp
103103
var logger = new LoggerConfiguration()
104-
.Enrich.WithSensitiveDataMasking(mask: "**")
104+
.Enrich.WithSensitiveDataMasking(options => options.MaskValue = "**")
105105
.WriteTo.Console()
106106
.CreateLogger();
107107
```
@@ -112,6 +112,27 @@ A example rendered message would then look like:
112112

113113
You can specify any mask string as long as it's non-null or an empty string.
114114

115+
## Always mask a property
116+
117+
It may be that you always want to mask the value of a property regardless of whether it matches a pattern for any of the masking operators. In that case you can specify that the property is always masked:
118+
119+
```csharp
120+
var logger = new LoggerConfiguration()
121+
.Enrich.WithSensitiveDataMasking(options => options.MaskProperties.Add("email"))
122+
.WriteTo.Console()
123+
.CreateLogger();
124+
```
125+
126+
> **Note:** The property names are treated case-insensitive. If you specify `EMAIL` and the property name is `eMaIL` it will still be masked.
127+
128+
When you log any message with an `email` property it will be masked:
129+
130+
```csharp
131+
logger.Information("This is a sensitive {Email}", "this doesn't match the regex at all");
132+
```
133+
134+
the rendered log message comes out as: `"This is a sensitive ***MASKED***"`
135+
115136
## Extending to additional use cases
116137

117138
Extending this enricher is a fairly straight forward process.

src/Serilog.Enrichers.Sensitive/ExtensionMethods.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,13 @@ public static LoggerConfiguration WithSensitiveDataMasking(
4343
return loggerConfiguration
4444
.With(new SensitiveDataEnricher(mode, operators, mask));
4545
}
46+
47+
public static LoggerConfiguration WithSensitiveDataMasking(
48+
this LoggerEnrichmentConfiguration loggerConfiguration,
49+
Action<SensitiveDataEnricherOptions> options)
50+
{
51+
return loggerConfiguration
52+
.With(new SensitiveDataEnricher(options));
53+
}
4654
}
4755
}

src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5+
using System.Threading.Tasks;
56
using Serilog.Core;
67
using Serilog.Events;
78
using Serilog.Parsing;
@@ -17,23 +18,45 @@ internal class SensitiveDataEnricher : ILogEventEnricher
1718
private readonly FieldInfo _messageTemplateBackingField;
1819
private readonly List<IMaskingOperator> _maskingOperators;
1920
private readonly string _maskValue;
21+
private readonly List<string> _maskProperties;
2022

21-
public SensitiveDataEnricher(MaskingMode maskingMode, IEnumerable<IMaskingOperator> maskingOperators,
22-
string mask = DefaultMaskValue)
23+
public SensitiveDataEnricher(
24+
Action<SensitiveDataEnricherOptions> options)
2325
{
24-
if (string.IsNullOrEmpty(mask))
26+
var enricherOptions = new SensitiveDataEnricherOptions();
27+
28+
if (options != null)
2529
{
26-
throw new ArgumentNullException(nameof(mask), "The mask must be a non-empty string");
30+
options(enricherOptions);
2731
}
2832

29-
_maskingMode = maskingMode;
30-
_maskValue = mask;
33+
if (string.IsNullOrEmpty(enricherOptions.MaskValue))
34+
{
35+
throw new ArgumentNullException("mask", "The mask must be a non-empty string");
36+
}
37+
38+
_maskingMode = enricherOptions.Mode;
39+
_maskValue = enricherOptions.MaskValue;
40+
_maskProperties = enricherOptions.MaskProperties ?? new List<string>();
3141

3242
var fields = typeof(LogEvent).GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
3343

3444
_messageTemplateBackingField = fields.SingleOrDefault(f => f.Name.Contains("<MessageTemplate>"));
3545

36-
_maskingOperators = maskingOperators.ToList();
46+
_maskingOperators = enricherOptions.MaskingOperators.ToList();
47+
}
48+
49+
public SensitiveDataEnricher(
50+
MaskingMode maskingMode,
51+
IEnumerable<IMaskingOperator> maskingOperators,
52+
string mask = DefaultMaskValue)
53+
: this(options =>
54+
{
55+
options.MaskValue = mask;
56+
options.Mode = maskingMode;
57+
options.MaskingOperators = maskingOperators.ToList();
58+
})
59+
{
3760
}
3861

3962
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
@@ -46,7 +69,14 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
4669

4770
foreach (var property in logEvent.Properties.ToList())
4871
{
49-
if (property.Value is ScalarValue scalar && scalar.Value is string stringValue)
72+
if (_maskProperties.Contains(property.Key, StringComparer.InvariantCultureIgnoreCase))
73+
{
74+
logEvent.AddOrUpdateProperty(
75+
new LogEventProperty(
76+
property.Key,
77+
new ScalarValue(_maskValue)));
78+
}
79+
else if (property.Value is ScalarValue scalar && scalar.Value is string stringValue)
5080
{
5181
logEvent.AddOrUpdateProperty(
5282
new LogEventProperty(
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace Serilog.Enrichers.Sensitive
5+
{
6+
public class SensitiveDataEnricherOptions
7+
{
8+
/// <summary>
9+
/// Sets whether masking should happen for all log messages ('Globally') or only in sensitive areas ('SensitiveArea')
10+
/// </summary>
11+
public MaskingMode Mode { get; set; }
12+
/// <summary>
13+
/// The string that replaces the sensitive value, defaults to '***MASKED***'
14+
/// </summary>
15+
public string MaskValue { get; set; } = SensitiveDataEnricher.DefaultMaskValue;
16+
/// <summary>
17+
/// The list of masking operators that are available
18+
/// </summary>
19+
/// <remarks>By default this list contains <see cref="SensitiveDataEnricher.DefaultOperators"/>, if you want to have only your specific enricher(s) supply a new list instead of calling <c>Add()</c></remarks>
20+
public List<IMaskingOperator> MaskingOperators { get; set; } = SensitiveDataEnricher.DefaultOperators.ToList();
21+
/// <summary>
22+
/// The list of properties that should always be masked regardless of whether they match the pattern of any of the masking operators
23+
/// </summary>
24+
/// <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>
25+
public List<string> MaskProperties { get; set; } = new List<string>();
26+
}
27+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using Serilog.Sinks.InMemory;
2+
using Serilog.Sinks.InMemory.Assertions;
3+
using Xunit;
4+
5+
namespace Serilog.Enrichers.Sensitive.Tests.Unit
6+
{
7+
public class WhenMaskingSensitiveDataBasedOnPropertyName
8+
{
9+
[Fact]
10+
public void GivenLogMessageHasSpecificProperty_PropertyValueIsMasked()
11+
{
12+
var inMemorySink = new InMemorySink();
13+
14+
var logger = new LoggerConfiguration()
15+
.Enrich.WithSensitiveDataMasking(options =>
16+
{
17+
options.MaskProperties.Add("Email");
18+
})
19+
.WriteTo.Sink(inMemorySink)
20+
.CreateLogger();
21+
22+
logger.Information("Example {Email}", "this doesn't match the e-mail regex");
23+
24+
inMemorySink
25+
.Should()
26+
.HaveMessage("Example {Email}")
27+
.Appearing()
28+
.Once()
29+
.WithProperty("Email")
30+
.WithValue("***MASKED***");
31+
}
32+
33+
[Fact]
34+
public void GivenLogMessageHasSpecificPropertyAndLogMessageHasPropertyButLowerCase_PropertyValueIsMasked()
35+
{
36+
var inMemorySink = new InMemorySink();
37+
38+
var logger = new LoggerConfiguration()
39+
.Enrich.WithSensitiveDataMasking(options =>
40+
{
41+
options.MaskProperties.Add("Email");
42+
})
43+
.WriteTo.Sink(inMemorySink)
44+
.CreateLogger();
45+
46+
logger.Information("Example {email}", "this doesn't match the e-mail regex");
47+
48+
inMemorySink
49+
.Should()
50+
.HaveMessage("Example {email}")
51+
.Appearing()
52+
.Once()
53+
.WithProperty("email")
54+
.WithValue("***MASKED***");
55+
}
56+
}
57+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class WhenMaskingWithCustomMaskValue
1212
public void GivenMaskValueIsNull_ArgumentNullExceptionIsThrown()
1313
{
1414
Action action = () => new LoggerConfiguration()
15-
.Enrich.WithSensitiveDataMasking(null)
15+
.Enrich.WithSensitiveDataMasking(options => options.MaskValue = null)
1616
.CreateLogger();
1717

1818
action

0 commit comments

Comments
 (0)