namespace Serilog.Enrichers.Sensitive.Demo
using System.Threading.Tasks;
static void Main(string[] args)
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(MaskingMode.InArea, new IMaskingOperator[]
new IbanMaskingOperator()
using (logger.EnterSensitiveArea())
logger.Information("Bank NL02RABO0123456789 is invalid Iban, but NL91ABNA0417164300 is valid, and NL91ABNA0417164301 invalid");
logger.Information("Bank NL02ABNA0123456789 is valid Iban, but NL00ABNA0417164300 is invalid, and NL91ABNA0417164300 valid");
namespace Serilog.Enrichers.Sensitive
using System.Text.RegularExpressions;
public class IbanMaskingOperator : RegexMaskingOperator
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}";
IIbanValidator validator = new IbanValidator();
public IbanMaskingOperator() : base(IbanReplacePattern)
protected override MatchEvaluator MatchEvaluator(Regex regex, string mask)
if (validator.Validate(value).IsValid)
return regex.Replace(match.Value, PreprocessMask(mask));
namespace Serilog.Enrichers.Sensitive
using System.Text.RegularExpressions;
public struct MaskingResult
public bool Match { get; set; }
public string Result { get; set; }
public static MaskingResult NoMatch => new MaskingResult { Match = false };
public interface IMaskingOperator
MaskingResult Mask(string input, string mask);
public abstract class RegexMaskingOperator : IMaskingOperator
private readonly Regex _regex;
protected RegexMaskingOperator(string regexString) : this(regexString, RegexOptions.Compiled)
protected RegexMaskingOperator(string regexString, RegexOptions options)
_regex = new Regex(regexString ?? throw new ArgumentNullException(nameof(regexString)), options);
if (string.IsNullOrWhiteSpace(regexString))
throw new ArgumentOutOfRangeException(nameof(regexString), "Regex pattern cannot be empty or whitespace.");
public MaskingResult Mask(string input, string mask)
var preprocessedInput = PreprocessInput(input);
if (!ShouldMaskInput(preprocessedInput))
return MaskingResult.NoMatch;
var maskedResult = _regex.Replace(preprocessedInput, MatchEvaluator(_regex, mask));
var result = new MaskingResult
Match = maskedResult != input
protected virtual MatchEvaluator MatchEvaluator(Regex regex, string mask)
return (Match match) => { return regex.Replace(match.Value, PreprocessMask(mask)); };
protected virtual bool ShouldMaskInput(string input) => true;
protected virtual string PreprocessInput(string input) => input;
protected virtual string PreprocessMask(string mask) => mask;
namespace Serilog.Enrichers.Sensitive
using System.Collections.Generic;
using Serilog.Configuration;
public static class ExtensionMethods
public static SensitiveArea EnterSensitiveArea(this ILogger logger)
var sensitiveArea = new SensitiveArea();
SensitiveArea.Instance = sensitiveArea;
public static LoggerConfiguration WithSensitiveDataMasking(this LoggerEnrichmentConfiguration loggerConfiguration)
return loggerConfiguration.WithSensitiveDataMasking(MaskingMode.Globally, SensitiveDataEnricher.DefaultOperators);
public static LoggerConfiguration WithSensitiveDataMaskingInArea(this LoggerEnrichmentConfiguration loggerConfiguration)
return loggerConfiguration.WithSensitiveDataMasking(MaskingMode.InArea, SensitiveDataEnricher.DefaultOperators);
public static LoggerConfiguration WithSensitiveDataMasking(
this LoggerEnrichmentConfiguration loggerConfiguration, MaskingMode mode,
IEnumerable<IMaskingOperator> operators)
return loggerConfiguration
.With(new SensitiveDataEnricher(mode, operators));
public class SensitiveArea : IDisposable
private static readonly AsyncLocal<SensitiveArea> InstanceLocal = new AsyncLocal<SensitiveArea>();
public static SensitiveArea Instance
get => InstanceLocal.Value;
set => InstanceLocal.Value = value;
internal class SensitiveDataEnricher : ILogEventEnricher
private readonly MaskingMode _maskingMode;
private const string MaskValue = "***MASKED***";
private static readonly MessageTemplateParser Parser = new MessageTemplateParser();
private readonly FieldInfo _messageTemplateBackingField;
private readonly List<IMaskingOperator> _maskingOperators;
public SensitiveDataEnricher(MaskingMode maskingMode, IEnumerable<IMaskingOperator> maskingOperators)
_maskingMode = maskingMode;
var fields = typeof(LogEvent).GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
_messageTemplateBackingField = fields.SingleOrDefault(f => f.Name.Contains("<MessageTemplate>"));
_maskingOperators = maskingOperators.ToList();
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
if (_maskingMode == MaskingMode.Globally || SensitiveArea.Instance != null)
var messageTemplateText = ReplaceSensitiveDataFromString(logEvent.MessageTemplate.Text);
_messageTemplateBackingField.SetValue(logEvent, Parser.Parse(messageTemplateText));
foreach (var property in logEvent.Properties.ToList())
if (property.Value is ScalarValue scalar && scalar.Value is string stringValue)
logEvent.AddOrUpdateProperty(
new ScalarValue(ReplaceSensitiveDataFromString(stringValue))));
private string ReplaceSensitiveDataFromString(string input)
foreach (var maskingOperator in _maskingOperators)
var maskResult = maskingOperator.Mask(input, MaskValue);
input = maskResult.Result;
public static IEnumerable<IMaskingOperator> DefaultOperators => new List<IMaskingOperator>
new IbanMaskingOperator(),