Skip to content

MaskProperties configuration support #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog for Serilog.Enrichers.Sensitive

## 2.0.0

This release introduces support for configuring `MaskProperties` via JSON. As the version number indicates this is a breaking change as the C# equivalent had to be changed to match.
See the [README](README.md) for more details on configuring specific property masking options.

## 1.7.4

- Add masking options for properties [#29](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/29)
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project>
<PropertyGroup>
<Version>1.7.4.0</Version>
<Version>2.0.0.0</Version>
<Authors>Sander van Vliet, Huibert Jan Nieuwkamer, Scott Toberman</Authors>
<Company>Codenizer BV</Company>
<Copyright>2023 Sander van Vliet</Copyright>
<Copyright>2025 Sander van Vliet</Copyright>
</PropertyGroup>
</Project>
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,66 @@ An example config file:
```

> **Warning:** Contrary to what you might expect, for JSON configuration `Operators` should be used instead of `MaskingOperators`.

### Masking specific properties

You can configure specific options for property masking via the `MaskProperties` property via code as well as JSON.

From code:

```csharp
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskProperties.Add(MaskProperty.WithDefaults("Email"));
})
.WriteTo.Sink(inMemorySink)
.CreateLogger();
```

Here we're using `MaskProperty.WithDefaults()` to indicate we just want to mask the `Email` property. You can specify more options like so:

```csharp
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskProperties.Add(new MaskProperty
{
Name = "Email",
Options = new MaskOptions {
ShowFirst = 3
}
});
})
.WriteTo.Sink(inMemorySink)
.CreateLogger();
```

Via JSON configuration (for example `appsettings.json`) you can follow a similar approach:

```json
{
"Serilog": {
"Using": [
"Serilog.Enrichers.Sensitive"
],
"Enrich": [
{
"Name": "WithSensitiveDataMasking",
"Args": {
"options": {
"MaskProperties": [
{
"Name": "someproperty",
"Options": {
"ShowFirst": 3
}
}
]
}
}
}
]
}
}
```
53 changes: 50 additions & 3 deletions src/Serilog.Enrichers.Sensitive/MaskOptions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
namespace Serilog.Enrichers.Sensitive;
using System;

public class MaskOptions
namespace Serilog.Enrichers.Sensitive;

public class MaskOptions : IEquatable<MaskOptions>
{
public static readonly MaskOptions Default= new();
public static readonly MaskOptions Default = new();
public const int NotSet = -1;
public int ShowFirst { get; set; } = NotSet;
public int ShowLast { get; set; } = NotSet;
public bool PreserveLength { get; set; } = true;

public bool Equals(MaskOptions? other)
{
if (other is null)
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

return ShowFirst == other.ShowFirst && ShowLast == other.ShowLast && PreserveLength == other.PreserveLength;
}

public override bool Equals(object? obj)
{
if (obj is null)
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

if (obj.GetType() != GetType())
{
return false;
}

return Equals((MaskOptions)obj);
}

public override int GetHashCode()
{
unchecked
{
var hashCode = ShowFirst;
hashCode = (hashCode * 397) ^ ShowLast;
hashCode = (hashCode * 397) ^ PreserveLength.GetHashCode();
return hashCode;
}
}
}
12 changes: 12 additions & 0 deletions src/Serilog.Enrichers.Sensitive/MaskProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Serilog.Enrichers.Sensitive;

public class MaskProperty
{
public string Name { get; set; }
public MaskOptions Options { get; set; } = new();

public static MaskProperty WithDefaults(string propertyName)
{
return new MaskProperty { Name = propertyName };
}
}
35 changes: 0 additions & 35 deletions src/Serilog.Enrichers.Sensitive/MaskPropertyCollection.cs

This file was deleted.

15 changes: 8 additions & 7 deletions src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal class SensitiveDataEnricher : ILogEventEnricher
private readonly FieldInfo _messageTemplateBackingField;
private readonly List<IMaskingOperator> _maskingOperators;
private readonly string _maskValue;
private readonly MaskPropertyCollection _maskProperties;
private readonly List<MaskProperty> _maskProperties;
private readonly List<string> _excludeProperties;

public SensitiveDataEnricher(SensitiveDataEnricherOptions options)
Expand All @@ -33,7 +33,7 @@ public SensitiveDataEnricher(
MaskingMode.Globally,
DefaultMaskValue,
DefaultOperators.Select(o => o.GetType().AssemblyQualifiedName),
new List<string>(),
_maskProperties,
new List<string>());

if (options != null)
Expand Down Expand Up @@ -114,21 +114,22 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
return (false, null);
}

if(_maskProperties.TryGetProperty(property.Key, out var options))
var matchingProperty = _maskProperties.SingleOrDefault(p => p.Name.Equals(property.Key, StringComparison.OrdinalIgnoreCase));
if(matchingProperty != null)
{
if (options == MaskOptions.Default)
if (matchingProperty.Options == MaskOptions.Default)
{
return (true, new ScalarValue(_maskValue));
}

switch (property.Value)
{
case ScalarValue { Value: string stringValue }:
return (true, new ScalarValue(MaskWithOptions(_maskValue, options, stringValue)));
case ScalarValue { Value: Uri uriValue } when options is UriMaskOptions uriMaskOptions:
return (true, new ScalarValue(MaskWithOptions(_maskValue, matchingProperty.Options, stringValue)));
case ScalarValue { Value: Uri uriValue } when matchingProperty.Options is UriMaskOptions uriMaskOptions:
return (true, new ScalarValue(MaskWithUriOptions(_maskValue, uriMaskOptions, uriValue)));
case ScalarValue { Value: Uri uriValue }:
return (true, new ScalarValue(MaskWithOptions(_maskValue, options, uriValue.ToString())));
return (true, new ScalarValue(MaskWithOptions(_maskValue, matchingProperty.Options, uriValue.ToString())));
}
}

Expand Down
21 changes: 3 additions & 18 deletions src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ public SensitiveDataEnricherOptions(
MaskingMode mode = MaskingMode.Globally,
string maskValue = SensitiveDataEnricher.DefaultMaskValue,
IEnumerable<string>? maskingOperators = null,
IEnumerable<string>? maskProperties = null,
List<MaskProperty>? maskProperties = null,
IEnumerable<string>? excludeProperties = null,
// ReSharper disable once UnusedParameter.Local as this only exists to support JSON configuration, see the Operators property below
IEnumerable<string>? operators = null)
{
Mode = mode;
MaskValue = maskValue;
MaskingOperators = maskingOperators == null ? new List<IMaskingOperator>() : ResolveMaskingOperators(maskingOperators);
MaskProperties = maskProperties == null ? new MaskPropertyCollection() : MaskPropertyCollection.From(maskProperties);
MaskProperties = maskProperties == null ? new List<MaskProperty>() : maskProperties.ToList();
ExcludeProperties = excludeProperties?.ToList() ?? new List<string>();
}

Expand Down Expand Up @@ -90,7 +90,7 @@ private static List<IMaskingOperator> ResolveMaskingOperators(IEnumerable<string
/// The list of properties that should always be masked regardless of whether they match the pattern of any of the masking operators
/// </summary>
/// <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>
public MaskPropertyCollection MaskProperties { get; set; } = new();
public List<MaskProperty> MaskProperties { get; set; } = new();
/// <summary>
/// The list of properties that should never be masked
/// </summary>
Expand Down Expand Up @@ -130,19 +130,4 @@ public void Apply(SensitiveDataEnricherOptions other)
other.Operators = Operators;
}
}

public class MaskProperty
{
public MaskProperty()
{
}

public MaskProperty(string propertyName)
{
Name = propertyName;
}

public string Name { get; set; }
public MaskOptions Options { get; set; } = MaskOptions.Default;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ public void ReproCaseIssue25()
.WithProperty("secret")
.WithValue("**SECRET**");
}

[Fact]
public void GivenMaskPropertyWithSpecificOptions_OptionsAreApplied()
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("enricher-config.json")
.Build();

var inMemorySink = new InMemorySink();

var logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.WriteTo.Sink(inMemorySink)
.CreateLogger();

logger.Information("A test message {propwithoptions}", "1234567890");

inMemorySink
.Should()
.HaveMessage("A test message {propwithoptions}")
.Appearing().Once()
.WithProperty("propwithoptions")
.WithValue("123*******");
}
}

public class MyTestMaskingOperator : IMaskingOperator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public WhenMaskingDestructuredObject()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskingOperators = new List<IMaskingOperator> { new EmailAddressMaskingOperator() };
options.MaskProperties.Add("SensitiveProperty");
options.MaskProperties.Add(MaskProperty.WithDefaults("SensitiveProperty"));
})
.CreateLogger();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void GivenLogMessageHasSpecificProperty_PropertyValueIsMasked()
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskProperties.Add("Email");
options.MaskProperties.Add(MaskProperty.WithDefaults("Email"));
})
.WriteTo.Sink(inMemorySink)
.CreateLogger();
Expand All @@ -38,7 +38,7 @@ public void GivenLogMessageHasSpecificPropertyAndLogMessageHasPropertyButLowerCa
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskProperties.Add("Email");
options.MaskProperties.Add(MaskProperty.WithDefaults("Email"));
})
.WriteTo.Sink(inMemorySink)
.CreateLogger();
Expand Down Expand Up @@ -86,7 +86,7 @@ public void GivenLogMessageHasSpecificPropertyAndPropertyIsExcludedAndAlsoInclud
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskProperties.Add("Email");
options.MaskProperties.Add(MaskProperty.WithDefaults("Email"));
options.ExcludeProperties.Add("Email");
})
.WriteTo.Sink(inMemorySink)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void GivenMaskOptionsWithInputShorterThanNumberOfCharactersThatShouldBeSh
var inMemorySink = new InMemorySink();
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(
options => options.MaskProperties.Add("Prop", new MaskOptions{ ShowFirst = showFirst, ShowLast = showLast, PreserveLength = preserveLength}))
options => options.MaskProperties.Add(new MaskProperty { Name = "Prop", Options = new MaskOptions{ ShowFirst = showFirst, ShowLast = showLast, PreserveLength = preserveLength}}))
.WriteTo.Sink(inMemorySink)
.CreateLogger();

Expand All @@ -52,13 +52,13 @@ public void GivenUriMaskOptions(bool showScheme, bool showHost, bool showPath, b
var inMemorySink = new InMemorySink();
var logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(
options => options.MaskProperties.Add("Prop", new UriMaskOptions
options => options.MaskProperties.Add(new MaskProperty { Name ="Prop", Options = new UriMaskOptions
{
ShowScheme = showScheme,
ShowHost = showHost,
ShowPath = showPath,
ShowQueryString = showQuery
}))
}}))
.WriteTo.Sink(inMemorySink)
.CreateLogger();

Expand Down
Loading