Skip to content

Commit 382349d

Browse files
Add extension point to perform additional checks on a match to decide whether to mask it
1 parent ff4ac0c commit 382349d

File tree

5 files changed

+137
-17
lines changed

5 files changed

+137
-17
lines changed

CHANGELOG

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

3+
## 1.2.0
4+
5+
- Add `ShouldMaskMatch` to the `RegexMaskingOperator` to allow implementors to perform further checks on the sensitive value in order to decide whether or not to perform masking
6+
37
## 1.1.0
48

59
- Add support to supply a custom mask value [#6](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/6)

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>1.1.0.0</Version>
3+
<Version>1.2.0.0</Version>
44
<Authors>Sander van Vliet, Huibert Jan Nieuwkamer, Scott Toberman</Authors>
55
<Company>Codenizer BV</Company>
66
<Copyright>2022 Sander van Vliet</Copyright>

README.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,23 +135,24 @@ the rendered log message comes out as: `"This is a sensitive ***MASKED***"`
135135

136136
## Extending to additional use cases
137137

138-
Extending this enricher is a fairly straight forward process.
138+
Depending on the type of masking operation you want to perform, the `RegexMaskingOperator` base class is most likely your best starting point. It provides a number of extension points:
139139

140-
1. Create your new class and inherit from the RegexMaskingOperator base class
141-
1. Pass your regex pattern to the base constructor
142-
2. To control if the regex replacement should even take place, override ShouldMaskInput, returning `true` if the mask should be applied, and `false` if it should not.
143-
3. Override PreprocessInput if your use case requires adjusting the input string before the regex match is applied.
144-
4. Override PreprocessMask if your use case requires adjusting the mask that is applied (for instance, if your regex includes named groups). See the [CreditCardMaskingOperator](src/Serilog.Enrichers.Sensitive/CreditCardMaskingOperator.cs) for an example.
145-
2. When configuring your logger, pass your new encricher in the collection of masking operators
140+
| Method | Purpose |
141+
|--------|---------|
142+
| ShouldMaskInput | Indicate whether the operator should continue with masking the input |
143+
| PreprocessInput | Perform any operations on the input value before masking the input |
144+
| PreprocessMask | Perform any operations on the mask before masking the matched value |
145+
| ShouldMaskMatch | Indicate whether the operator should continue with masking the matched value from the input |
146+
147+
To implement your own masking operator, inherit from `RegexMaskingOperator`, supply the regex through the base constructor and where necessary override any of the above extension points.
148+
149+
Then, when configuring your logger, pass your new encricher in the collection of masking operators:
146150

147151
```csharp
148152
var logger = new LoggerConfiguration()
149-
.Enrich.WithSensitiveDataMasking(MaskingMode.InArea, new IMaskingOperator[]
150-
{
151-
new EmailAddressMaskingOperator(),
152-
new IbanMaskingOperator(),
153-
new CreditCardMaskingOperator(false),
154-
new YourMaskingOperator()
153+
.Enrich.WithSensitiveDataMasking(options => {
154+
// Add your masking operator:
155+
options.MaskingOperators.Add(new YourMaskingOperator());
155156
})
156157
.WriteTo.Console()
157158
.CreateLogger();

src/Serilog.Enrichers.Sensitive/RegexMaskingOperator.cs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33

44
namespace Serilog.Enrichers.Sensitive
55
{
6+
/// <summary>
7+
/// A masking operator that uses regular expressions to match the value from the input to mask
8+
/// </summary>
69
public abstract class RegexMaskingOperator : IMaskingOperator
710
{
811
private readonly Regex _regex;
912

10-
protected RegexMaskingOperator(string regexString) : this(regexString, RegexOptions.Compiled)
13+
protected RegexMaskingOperator(string regexString)
14+
: this(regexString, RegexOptions.Compiled)
1115
{
1216
}
1317

1418
protected RegexMaskingOperator(string regexString, RegexOptions options)
1519
{
1620
_regex = new Regex(regexString ?? throw new ArgumentNullException(nameof(regexString)), options);
21+
1722
if (string.IsNullOrWhiteSpace(regexString))
1823
{
1924
throw new ArgumentOutOfRangeException(nameof(regexString), "Regex pattern cannot be empty or whitespace.");
@@ -23,12 +28,22 @@ protected RegexMaskingOperator(string regexString, RegexOptions options)
2328
public MaskingResult Mask(string input, string mask)
2429
{
2530
var preprocessedInput = PreprocessInput(input);
31+
2632
if (!ShouldMaskInput(preprocessedInput))
2733
{
2834
return MaskingResult.NoMatch;
2935
}
3036

31-
var maskedResult = _regex.Replace(preprocessedInput, PreprocessMask(mask));
37+
var maskedResult = _regex.Replace(preprocessedInput, match =>
38+
{
39+
if (ShouldMaskMatch(match))
40+
{
41+
return match.Result(PreprocessMask(mask));
42+
}
43+
44+
return match.Value;
45+
});
46+
3247
var result = new MaskingResult
3348
{
3449
Result = maskedResult,
@@ -38,10 +53,35 @@ public MaskingResult Mask(string input, string mask)
3853
return result;
3954
}
4055

56+
/// <summary>
57+
/// Indicate whether the operator should continue with masking the input
58+
/// </summary>
59+
/// <param name="input">The message template or the value of a property on the log event</param>
60+
/// <returns><c>true</c> when the input should be masked, otherwise <c>false</c>. Defaults to <c>true</c></returns>
61+
/// <remarks>This method provides an extension point to short-circuit the masking operation before the regular expression matching is performed</remarks>
4162
protected virtual bool ShouldMaskInput(string input) => true;
4263

64+
/// <summary>
65+
/// Perform any operations on the input value before masking the input
66+
/// </summary>
67+
/// <param name="input">The message template or the value of a property on the log event</param>
68+
/// <returns>The processed input, defaults to no pre-processing and returns the input</returns>
69+
/// <remarks>Use this method if the input is encoded using URL encoding for example</remarks>
4370
protected virtual string PreprocessInput(string input) => input;
4471

72+
/// <summary>
73+
/// Perform any operations on the mask before masking the matched value
74+
/// </summary>
75+
/// <param name="mask">The mask value as specified on the <see cref="SensitiveDataEnricherOptions"/></param>
76+
/// <returns>The processed mask, defaults to no pre-processing and returns the input</returns>
4577
protected virtual string PreprocessMask(string mask) => mask;
46-
}
78+
79+
/// <summary>
80+
/// Indicate whether the operator should continue with masking the matched value from the input
81+
/// </summary>
82+
/// <param name="match">The match found by the regular expression of this operator</param>
83+
/// <returns><c>true</c> when the match should be masked, otherwise <c>false</c>. Defaults to <c>true</c></returns>
84+
/// <remarks>This method provides an extension point to short-circuit the masking operation if the value matches the regular expression but does not satisfy some additional criteria</remarks>
85+
protected virtual bool ShouldMaskMatch(Match match) => true;
86+
}
4787
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Collections.Generic;
2+
using System.Text.RegularExpressions;
3+
using Serilog.Sinks.InMemory;
4+
using Serilog.Sinks.InMemory.Assertions;
5+
using Xunit;
6+
7+
namespace Serilog.Enrichers.Sensitive.Tests.Unit
8+
{
9+
public class WhenMaskingWithInputFilter
10+
{
11+
[Fact]
12+
public void GivenTestPropertyHasTestValue_ValueIsMasked()
13+
{
14+
var inMemorySink = new InMemorySink();
15+
16+
var logger = new LoggerConfiguration()
17+
.Enrich.WithSensitiveDataMasking(options =>
18+
{
19+
options.MaskingOperators = new List<IMaskingOperator>
20+
{
21+
new SpecificValueMaskingOperator("[A-Za-z]*")
22+
};
23+
})
24+
.WriteTo.Sink(inMemorySink)
25+
.CreateLogger();
26+
27+
logger.Information("TestValue");
28+
29+
inMemorySink
30+
.Should()
31+
.HaveMessage("***MASKED***")
32+
.Appearing().Once();
33+
}
34+
35+
[Fact]
36+
public void GivenTestPropertyHasSomeOtherValue_ValueIsNotMasked()
37+
{
38+
var inMemorySink = new InMemorySink();
39+
40+
var logger = new LoggerConfiguration()
41+
.Enrich.WithSensitiveDataMasking(options =>
42+
{
43+
options.MaskingOperators = new List<IMaskingOperator>
44+
{
45+
new SpecificValueMaskingOperator("[A-Za-z]*")
46+
};
47+
})
48+
.WriteTo.Sink(inMemorySink)
49+
.CreateLogger();
50+
51+
logger.Information("SomeOtherValue");
52+
53+
inMemorySink
54+
.Should()
55+
.HaveMessage("SomeOtherValue")
56+
.Appearing().Once();
57+
}
58+
}
59+
60+
internal class SpecificValueMaskingOperator : RegexMaskingOperator
61+
{
62+
public SpecificValueMaskingOperator(string regexString) : base(regexString)
63+
{
64+
}
65+
66+
public SpecificValueMaskingOperator(string regexString, RegexOptions options) : base(regexString, options)
67+
{
68+
}
69+
70+
protected override bool ShouldMaskMatch(Match match)
71+
{
72+
return match.Value == "TestValue";
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)