Skip to content

Commit a2912f9

Browse files
Merge pull request #2 from stoberman37/feature/creditcardmask
Introduces masking of credit card numbers in log entries.
2 parents cf57c4f + c898841 commit a2912f9

File tree

15 files changed

+433
-83
lines changed

15 files changed

+433
-83
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,29 @@ The effect is that the log message will be rendered as:
9292
`This is a sensitive ***MASKED***`
9393

9494
See the [Serilog.Enrichers.Sensitive.Demo](src/Serilog.Enrichers.Sensitive.Demo/Program.cs) app for a code example of the above.
95+
96+
## Extending to additional use cases
97+
98+
Extending this enricher is a fairly straight forward process.
99+
100+
1. Create your new class and inherit from the RegexMaskingOperator base class
101+
1. Pass your regex pattern to the base constructor
102+
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.
103+
3. Override PreprocessInput if your use case requires adjusting the input string before the regex match is applied.
104+
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.
105+
1. When configuring your logger, pass your new encricher in the collection of masking operators
106+
107+
```csharp
108+
var logger = new LoggerConfiguration()
109+
.Enrich.WithSensitiveDataMasking(MaskingMode.InArea, new IMaskingOperator[]
110+
{
111+
new EmailAddressMaskingOperator(),
112+
new IbanMaskingOperator(),
113+
new CreditCardMaskingOperator(false),
114+
new YourMaskingOperator()
115+
})
116+
.WriteTo.Console()
117+
.CreateLogger();
118+
```
119+
120+

Serilog.Enrichers.Sensitive.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5953C623-0
1313
EndProject
1414
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Enrichers.Sensitive.Tests.Unit", "test\Serilog.Enrichers.Sensitive.Tests.Unit\Serilog.Enrichers.Sensitive.Tests.Unit.csproj", "{8784B51D-900B-44E5-A032-CEF6027CD178}"
1515
EndProject
16-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Enrichers.Sensitive.Tests.Benchmark", "test\Serilog.Enrichers.Sensitive.Tests.Benchmark\Serilog.Enrichers.Sensitive.Tests.Benchmark.csproj", "{1C6B2313-3EBB-482F-8A78-2BB7C6FCFA06}"
16+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Enrichers.Sensitive.Tests.Benchmark", "test\Serilog.Enrichers.Sensitive.Tests.Benchmark\Serilog.Enrichers.Sensitive.Tests.Benchmark.csproj", "{1C6B2313-3EBB-482F-8A78-2BB7C6FCFA06}"
1717
EndProject
1818
Global
1919
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,93 @@
11
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Serilog.Core;
25

36
namespace Serilog.Enrichers.Sensitive.Demo
47
{
5-
class Program
6-
{
7-
static void Main(string[] args)
8-
{
9-
var logger = new LoggerConfiguration()
10-
.Enrich.WithSensitiveDataMasking()
11-
.WriteTo.Console()
12-
.CreateLogger();
13-
14-
logger.Information("Hello, world");
15-
16-
using (logger.EnterSensitiveArea())
17-
{
18-
// An e-mail address in text
19-
logger.Information("This is a secret email address: james.bond@universal-exports.co.uk");
20-
21-
// Works for properties too
22-
logger.Information("This is a secret email address: {Email}", "james.bond@universal-exports.co.uk");
23-
24-
// IBANs are also masked
25-
logger.Information("Bank transfer from Felix Leiter on NL02ABNA0123456789");
26-
27-
// IBANs are also masked
28-
logger.Information("Bank transfer from Felix Leiter on {BankAccount}", "NL02ABNA0123456789");
29-
}
30-
31-
// But outside the sensitive area nothing is masked
32-
logger.Information("Felix can be reached at felix@cia.gov");
33-
34-
Console.ReadLine();
35-
}
36-
}
8+
class Program
9+
{
10+
static async Task Main(string[] args)
11+
{
12+
var logger = new LoggerConfiguration()
13+
.Enrich.WithSensitiveDataMasking(MaskingMode.InArea, new IMaskingOperator[]
14+
{
15+
new EmailAddressMaskingOperator(),
16+
new IbanMaskingOperator(),
17+
new CreditCardMaskingOperator(false)
18+
})
19+
.WriteTo.Console()
20+
.CreateLogger();
21+
22+
logger.Information("Hello, world");
23+
24+
using (logger.EnterSensitiveArea())
25+
{
26+
// An e-mail address in text
27+
logger.Information("This is a secret email address: james.bond@universal-exports.co.uk");
28+
29+
// Works for properties too
30+
logger.Information("This is a secret email address: {Email}", "james.bond@universal-exports.co.uk");
31+
32+
// IBANs are also masked
33+
logger.Information("Bank transfer from Felix Leiter on NL02ABNA0123456789");
34+
35+
// IBANs are also masked
36+
logger.Information("Bank transfer from Felix Leiter on {BankAccount}", "NL02ABNA0123456789");
37+
38+
// Credit card numbers too
39+
logger.Information("Credit Card Number: 4111111111111111");
40+
41+
// even works in an embedded XML string
42+
var x = new
43+
{
44+
Key = 12345, XmlValue = "<MyElement><CreditCard>4111111111111111</CreditCard></MyElement>"
45+
};
46+
logger.Information("Object dump with embedded credit card: {x}", x);
47+
48+
}
49+
50+
// But outside the sensitive area nothing is masked
51+
logger.Information("Felix can be reached at felix@cia.gov");
52+
53+
54+
// Now, show that this works for async contexts too
55+
logger.Information("Now, show the Async works");
56+
57+
var t1 = LogAsSensitiveAsync(logger);
58+
var t2 = LogAsUnsensitiveAsync(logger);
59+
60+
await Task.WhenAll(t1, t2).ConfigureAwait(false);
61+
62+
Console.ReadLine();
63+
}
64+
65+
private static async Task LogAsSensitiveAsync(Logger logger)
66+
{
67+
using (logger.EnterSensitiveArea())
68+
{
69+
await Task.Delay(new Random().Next(1000,2000)).ConfigureAwait(false);
70+
// Put in a delay, so we are sure these run basically simultaneiously
71+
// An e-mail address in text
72+
logger.Information("Sensitive: This is a secret email address: james.bond@universal-exports.co.uk");
73+
74+
// Works for properties too
75+
await Task.Delay(new Random().Next(1000, 2000)).ConfigureAwait(false);
76+
logger.Information("Sensitive: This is a secret email address: {Email}",
77+
"james.bond@universal-exports.co.uk");
78+
}
79+
}
80+
81+
private static async Task LogAsUnsensitiveAsync(Logger logger)
82+
{
83+
// Put in a delay, so we are sure these run basically simultaneiously
84+
// An e-mail address in text
85+
await Task.Delay(new Random().Next(1000, 2000)).ConfigureAwait(false);
86+
logger.Information("This is a secret email address: james.bond@universal-exports.co.uk");
87+
88+
// Works for properties too
89+
await Task.Delay(new Random().Next(1000, 2000)).ConfigureAwait(false);
90+
logger.Information("This is a secret email address: {Email}", "james.bond@universal-exports.co.uk");
91+
}
92+
}
3793
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace Serilog.Enrichers.Sensitive
4+
{
5+
public class CreditCardMaskingOperator : RegexMaskingOperator
6+
{
7+
private const string CreditCardPartialReplacePattern =
8+
@"(?<leading4>\d{4}(?<sep>[ -]?))(?<toMask>\d{4}\k<sep>*\d{2})(?<trailing6>\d{2}\k<sep>*\d{4})";
9+
10+
private const string CreditCardFullReplacePattern =
11+
@"(?<toMask>\d{4}(?<sep>[ -]?)\d{4}\k<sep>*\d{4}\k<sep>*\d{4})";
12+
13+
private readonly string _replacementPattern;
14+
15+
public CreditCardMaskingOperator() : this(true)
16+
{
17+
}
18+
19+
public CreditCardMaskingOperator(bool fullMask)
20+
: base(fullMask ? CreditCardFullReplacePattern : CreditCardPartialReplacePattern, RegexOptions.IgnoreCase | RegexOptions.Compiled)
21+
{
22+
_replacementPattern = fullMask ? "{0}" : "${{leading4}}{0}${{trailing6}}";
23+
}
24+
25+
protected override string PreprocessMask(string mask) => string.Format(_replacementPattern, mask);
26+
}
27+
}

src/Serilog.Enrichers.Sensitive/EmailAddressMaskingOperator.cs

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,27 @@
22

33
namespace Serilog.Enrichers.Sensitive
44
{
5-
public class EmailAddressMaskingOperator : IMaskingOperator
5+
public class EmailAddressMaskingOperator : RegexMaskingOperator
66
{
7-
private static readonly Regex EmailReplaceRegex = new Regex(
8-
"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])",
9-
RegexOptions.IgnoreCase | RegexOptions.Compiled);
7+
private const string EmailPattern =
8+
"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])";
109

11-
public MaskingResult Mask(string input, string mask)
10+
public EmailAddressMaskingOperator() : base(EmailPattern, RegexOptions.IgnoreCase | RegexOptions.Compiled)
1211
{
13-
// Naive approach to deal with URL encoded values
14-
// most likely this should properly URL decode once it
15-
// finds this marker in the input string
16-
if (input.Contains("%40"))
17-
{
18-
input = input.Replace("%40", "@");
19-
}
12+
}
2013

21-
// Early exit so we avoid the regex.
22-
// Probably needs a benchmark to see
23-
// if this actually helps.
24-
if (!input.Contains("@"))
25-
{
26-
return MaskingResult.NoMatch;
27-
}
14+
protected override string PreprocessInput(string input)
15+
{
16+
if (input.Contains("%40"))
17+
{
18+
input = input.Replace("%40", "@");
19+
}
20+
return input;
21+
}
2822

29-
// Note that if we get here we _always_ assume
30-
// a successful replacement.
31-
return new MaskingResult
32-
{
33-
Result = EmailReplaceRegex.Replace(input, mask),
34-
Match = true
35-
};
23+
protected override bool ShouldMaskInput(string input)
24+
{
25+
return input.Contains("@");
3626
}
3727
}
3828
}

src/Serilog.Enrichers.Sensitive/ExtensionMethods.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Serilog.Configuration;
1+
using System.Collections.Generic;
2+
using Serilog.Configuration;
23

34
namespace Serilog.Enrichers.Sensitive
45
{
@@ -15,14 +16,20 @@ public static SensitiveArea EnterSensitiveArea(this ILogger logger)
1516

1617
public static LoggerConfiguration WithSensitiveDataMasking(this LoggerEnrichmentConfiguration loggerConfiguration)
1718
{
18-
return loggerConfiguration
19-
.With(new SensitiveDataEnricher(MaskingMode.Globally, SensitiveDataEnricher.DefaultOperators));
19+
return loggerConfiguration.WithSensitiveDataMasking(MaskingMode.Globally, SensitiveDataEnricher.DefaultOperators);
2020
}
2121

2222
public static LoggerConfiguration WithSensitiveDataMaskingInArea(this LoggerEnrichmentConfiguration loggerConfiguration)
2323
{
24-
return loggerConfiguration
25-
.With(new SensitiveDataEnricher(MaskingMode.InArea, SensitiveDataEnricher.DefaultOperators));
24+
return loggerConfiguration.WithSensitiveDataMasking(MaskingMode.InArea, SensitiveDataEnricher.DefaultOperators);
25+
}
26+
27+
public static LoggerConfiguration WithSensitiveDataMasking(
28+
this LoggerEnrichmentConfiguration loggerConfiguration, MaskingMode mode,
29+
IEnumerable<IMaskingOperator> operators)
30+
{
31+
return loggerConfiguration
32+
.With(new SensitiveDataEnricher(mode, operators));
2633
}
2734
}
2835
}

src/Serilog.Enrichers.Sensitive/IMaskingOperator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace Serilog.Enrichers.Sensitive
22
{
3-
public interface IMaskingOperator
3+
4+
public interface IMaskingOperator
45
{
56
MaskingResult Mask(string input, string mask);
67
}
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
using System.Text.RegularExpressions;
2-
3-
namespace Serilog.Enrichers.Sensitive
1+
namespace Serilog.Enrichers.Sensitive
42
{
5-
public class IbanMaskingOperator : IMaskingOperator
3+
public class IbanMaskingOperator : RegexMaskingOperator
64
{
7-
private static readonly Regex IbanReplaceRegex = new Regex("[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}", RegexOptions.Compiled);
5+
private const string IbanReplacePattern = "[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{4}[0-9]{7}([a-zA-Z0-9]?){0,16}";
86

9-
public MaskingResult Mask(string input, string mask)
10-
{
11-
return new MaskingResult
12-
{
13-
Match = true,
14-
Result = IbanReplaceRegex.Replace(input, mask)
15-
};
16-
}
7+
public IbanMaskingOperator() : base(IbanReplacePattern)
8+
{
9+
}
1710
}
1811
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Text.RegularExpressions;
3+
4+
namespace Serilog.Enrichers.Sensitive
5+
{
6+
public abstract class RegexMaskingOperator : IMaskingOperator
7+
{
8+
private readonly Regex _regex;
9+
10+
protected RegexMaskingOperator(string regexString) : this(regexString, RegexOptions.Compiled)
11+
{
12+
}
13+
14+
protected RegexMaskingOperator(string regexString, RegexOptions options)
15+
{
16+
_regex = new Regex(regexString ?? throw new ArgumentNullException(nameof(regexString)), options);
17+
if (string.IsNullOrWhiteSpace(regexString))
18+
{
19+
throw new ArgumentOutOfRangeException(nameof(regexString), "Regex pattern cannot be empty or whitespace.");
20+
}
21+
}
22+
23+
public MaskingResult Mask(string input, string mask)
24+
{
25+
var preprocessedInput = PreprocessInput(input);
26+
if (!ShouldMaskInput(preprocessedInput))
27+
{
28+
return MaskingResult.NoMatch;
29+
}
30+
31+
var maskedResult = _regex.Replace(preprocessedInput, PreprocessMask(mask));
32+
var result = new MaskingResult
33+
{
34+
Result = maskedResult,
35+
Match = maskedResult != input
36+
};
37+
38+
return result;
39+
}
40+
41+
protected virtual bool ShouldMaskInput(string input) => true;
42+
43+
protected virtual string PreprocessInput(string input) => input;
44+
45+
protected virtual string PreprocessMask(string mask) => mask;
46+
}
47+
}

src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ private string ReplaceSensitiveDataFromString(string input)
8484
public static IEnumerable<IMaskingOperator> DefaultOperators => new List<IMaskingOperator>
8585
{
8686
new EmailAddressMaskingOperator(),
87-
new IbanMaskingOperator()
87+
new IbanMaskingOperator(),
88+
new CreditCardMaskingOperator()
8889
};
8990

9091
}

0 commit comments

Comments
 (0)