Skip to content

Commit dfcfa19

Browse files
[#29] Add mask options
1 parent 0dc043f commit dfcfa19

File tree

6 files changed

+260
-6
lines changed

6 files changed

+260
-6
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Serilog.Enrichers.Sensitive;
2+
3+
public class MaskOptions
4+
{
5+
public static readonly MaskOptions Default= new();
6+
public const int NotSet = -1;
7+
public int ShowFirst { get; set; } = NotSet;
8+
public int ShowLast { get; set; } = NotSet;
9+
public bool PreserveLength { get; set; } = true;
10+
11+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Collections.Generic;
2+
3+
namespace Serilog.Enrichers.Sensitive;
4+
5+
public class MaskPropertyCollection : List<MaskProperty>
6+
{
7+
private readonly Dictionary<string, MaskOptions> _properties = new();
8+
9+
public void Add(string propertyName)
10+
{
11+
_properties.Add(propertyName.ToLower(), MaskOptions.Default);
12+
}
13+
14+
public void Add(string propertyName, MaskOptions maskOptions)
15+
{
16+
_properties.Add(propertyName.ToLower(), maskOptions);
17+
}
18+
19+
public bool TryGetProperty(string propertyName, out MaskOptions options)
20+
{
21+
return _properties.TryGetValue(propertyName.ToLower(), out options);
22+
}
23+
24+
public static MaskPropertyCollection From(IEnumerable<string> enricherOptionsMaskProperties)
25+
{
26+
var collection = new MaskPropertyCollection();
27+
28+
foreach (var x in enricherOptionsMaskProperties)
29+
{
30+
collection.Add(x, MaskOptions.Default);
31+
}
32+
33+
return collection;
34+
}
35+
}

src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ internal class SensitiveDataEnricher : ILogEventEnricher
1212
{
1313
private readonly MaskingMode _maskingMode;
1414
public const string DefaultMaskValue = "***MASKED***";
15+
private const string DefaultMaskPad = "***";
1516

1617
private static readonly MessageTemplateParser Parser = new();
1718
private readonly FieldInfo _messageTemplateBackingField;
1819
private readonly List<IMaskingOperator> _maskingOperators;
1920
private readonly string _maskValue;
20-
private readonly List<string> _maskProperties;
21+
private readonly MaskPropertyCollection _maskProperties;
2122
private readonly List<string> _excludeProperties;
2223

2324
public SensitiveDataEnricher(SensitiveDataEnricherOptions options)
@@ -113,9 +114,22 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
113114
return (false, null);
114115
}
115116

116-
if (_maskProperties.Contains(property.Key, StringComparer.InvariantCultureIgnoreCase))
117+
if(_maskProperties.TryGetProperty(property.Key, out var options))
117118
{
118-
return (true, new ScalarValue(_maskValue));
119+
if (options == MaskOptions.Default)
120+
{
121+
return (true, new ScalarValue(_maskValue));
122+
}
123+
124+
switch (property.Value)
125+
{
126+
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:
129+
return (true, new ScalarValue(MaskWithUriOptions(_maskValue, uriMaskOptions, uriValue)));
130+
case ScalarValue { Value: Uri uriValue }:
131+
return (true, new ScalarValue(MaskWithOptions(_maskValue, options, uriValue.ToString())));
132+
}
119133
}
120134

121135
switch (property.Value)
@@ -213,6 +227,101 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
213227
}
214228
}
215229

230+
private string MaskWithUriOptions(string maskValue, UriMaskOptions options, Uri uri)
231+
{
232+
var scheme = options.ShowScheme
233+
? uri.Scheme
234+
: DefaultMaskPad;
235+
236+
var host = options.ShowHost
237+
? uri.Host
238+
: DefaultMaskPad;
239+
240+
var path = options.ShowPath
241+
? uri.AbsolutePath
242+
: "/" + DefaultMaskPad;
243+
244+
var queryString = !string.IsNullOrEmpty(uri.Query)
245+
? options.ShowQueryString
246+
? uri.Query
247+
: "?" + DefaultMaskPad
248+
: "";
249+
250+
return $"{scheme}://{host}{path}{queryString}";
251+
}
252+
253+
private string MaskWithOptions(string maskValue, MaskOptions options, string input)
254+
{
255+
if (options is { ShowFirst: >= 0, ShowLast: < 0 })
256+
{
257+
var start = options.ShowFirst;
258+
if (start >= input.Length)
259+
{
260+
start = 1;
261+
}
262+
263+
var first = input.Substring(0, start);
264+
if (options.PreserveLength)
265+
{
266+
return first.PadRight(input.Length, '*');
267+
}
268+
269+
return first + DefaultMaskPad;
270+
}
271+
272+
if (options is { ShowFirst: < 0, ShowLast: >= 0 })
273+
{
274+
var end = input.Length - options.ShowLast;
275+
if (end <= 0)
276+
{
277+
end = input.Length - 1;
278+
}
279+
280+
var last = input.Substring(end);
281+
282+
if (options.PreserveLength)
283+
{
284+
return last.PadLeft(input.Length, '*');
285+
}
286+
287+
return DefaultMaskPad + last;
288+
}
289+
290+
if (options is { ShowFirst: >= 0, ShowLast: >= 0 })
291+
{
292+
if (options.ShowFirst + options.ShowLast >= input.Length)
293+
{
294+
if (input.Length > 3)
295+
{
296+
return input[0] + DefaultMaskPad + input.Last();
297+
}
298+
299+
if (input.Length == 3)
300+
{
301+
return input[0] + DefaultMaskPad;
302+
}
303+
304+
if (input.Length <= 2)
305+
{
306+
return maskValue;
307+
}
308+
}
309+
310+
var start = options.ShowFirst;
311+
var end = input.Length - options.ShowLast;
312+
var pad = input.Length - (input.Length - end);
313+
314+
if (options.PreserveLength)
315+
{
316+
return input.Substring(0, start).PadRight(pad, '*') + input.Substring(end);
317+
}
318+
319+
return input.Substring(0, start) + DefaultMaskPad + input.Substring(end);
320+
}
321+
322+
return maskValue;
323+
}
324+
216325
private (bool, string) ReplaceSensitiveDataFromString(string input)
217326
{
218327
var isMasked = false;

src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public SensitiveDataEnricherOptions(
2626
Mode = mode;
2727
MaskValue = maskValue;
2828
MaskingOperators = maskingOperators == null ? new List<IMaskingOperator>() : ResolveMaskingOperators(maskingOperators);
29-
MaskProperties = maskProperties?.ToList() ?? new List<string>();
29+
MaskProperties = maskProperties == null ? new MaskPropertyCollection() : MaskPropertyCollection.From(maskProperties);
3030
ExcludeProperties = excludeProperties?.ToList() ?? new List<string>();
3131
}
3232

@@ -90,15 +90,15 @@ 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 List<string> MaskProperties { get; set; } = new List<string>();
93+
public MaskPropertyCollection MaskProperties { get; set; } = new();
9494
/// <summary>
9595
/// The list of properties that should never be masked
9696
/// </summary>
9797
/// <remarks>
9898
/// <para>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.</para>
9999
/// <para>This property takes precedence over <see cref="MaskProperties"/> and the masking operators.</para>
100100
/// </remarks>
101-
public List<string> ExcludeProperties { get; set; } = new List<string>();
101+
public List<string> ExcludeProperties { get; set; } = new();
102102

103103
/// <remarks>
104104
/// This property only exists to support JSON configuration of the enricher. If you are configuring the enricher from code you'll want <see cref="MaskingOperators"/> instead.
@@ -130,4 +130,19 @@ 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+
}
133148
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Serilog.Enrichers.Sensitive;
2+
3+
public class UriMaskOptions : MaskOptions
4+
{
5+
public bool ShowScheme { get; set; } = true;
6+
public bool ShowHost { get; set; } = true;
7+
public bool ShowPath { get; set; } = false;
8+
public bool ShowQueryString { get; set; } = false;
9+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using Serilog.Sinks.InMemory;
3+
using Serilog.Sinks.InMemory.Assertions;
4+
using Xunit;
5+
6+
namespace Serilog.Enrichers.Sensitive.Tests.Unit
7+
{
8+
public class WhenMaskingWithOptions
9+
{
10+
[Theory]
11+
[InlineData("1234567890", 2, 2, true, "12******90")]
12+
[InlineData("1234567890", 2, 2, false, "12***90")]
13+
[InlineData("1234567890", 2, -5, false, "12***")]
14+
[InlineData("1234567890", 2, -5, true, "12********")]
15+
[InlineData("1234567890", -5, 2, false, "***90")]
16+
[InlineData("1234567890", -5, 2, true, "********90")]
17+
[InlineData("1234", 2, 2, true, "1***4")]
18+
[InlineData("1234", 2, 3, true, "1***4")]
19+
[InlineData("124", 2, 2, true, "1***")]
20+
[InlineData("12", 2, 2, true, SensitiveDataEnricher.DefaultMaskValue)]
21+
[InlineData("1234", 5, MaskOptions.NotSet, true, "1***")]
22+
[InlineData("1234", MaskOptions.NotSet, 5, true, "***4")]
23+
public void GivenMaskOptionsWithInputShorterThanNumberOfCharactersThatShouldBeShown(string input, int showFirst, int showLast, bool preserveLength, string expectedValue)
24+
{
25+
var inMemorySink = new InMemorySink();
26+
var logger = new LoggerConfiguration()
27+
.Enrich.WithSensitiveDataMasking(
28+
options => options.MaskProperties.Add("Prop", new MaskOptions{ ShowFirst = showFirst, ShowLast = showLast, PreserveLength = preserveLength}))
29+
.WriteTo.Sink(inMemorySink)
30+
.CreateLogger();
31+
32+
logger.Information("{Prop}", input);
33+
34+
inMemorySink
35+
.Should()
36+
.HaveMessage("{Prop}")
37+
.Appearing().Once()
38+
.WithProperty("Prop")
39+
.WithValue(expectedValue);
40+
}
41+
42+
[Theory]
43+
[InlineData(true, false ,false, false, "https://***/***?***")]
44+
[InlineData(true, true ,false, false, "https://example.com/***?***")]
45+
[InlineData(true, true,true, false, "https://example.com/some/sensitive/path?***")]
46+
[InlineData(true, false,true, true, "https://***/some/sensitive/path?foo=bar")]
47+
[InlineData(false, false,true, true, "***://***/some/sensitive/path?foo=bar")]
48+
[InlineData(false, false,true, false, "***://***/some/sensitive/path?***")]
49+
public void GivenUriMaskOptions(bool showScheme, bool showHost, bool showPath, bool showQuery,
50+
string expectedValue)
51+
{
52+
var inMemorySink = new InMemorySink();
53+
var logger = new LoggerConfiguration()
54+
.Enrich.WithSensitiveDataMasking(
55+
options => options.MaskProperties.Add("Prop", new UriMaskOptions
56+
{
57+
ShowScheme = showScheme,
58+
ShowHost = showHost,
59+
ShowPath = showPath,
60+
ShowQueryString = showQuery
61+
}))
62+
.WriteTo.Sink(inMemorySink)
63+
.CreateLogger();
64+
65+
logger.Information("{Prop}", new Uri("https://example.com/some/sensitive/path?foo=bar"));
66+
67+
inMemorySink
68+
.Should()
69+
.HaveMessage("{Prop}")
70+
.Appearing().Once()
71+
.WithProperty("Prop")
72+
.WithValue(expectedValue);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)