Skip to content

Commit

Permalink
Merge branch 'master' into feature/pathmask
Browse files Browse the repository at this point in the history
  • Loading branch information
sandermvanvliet authored Apr 14, 2024
2 parents 9326948 + dfcfa19 commit 1f9bf0b
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 8 deletions.
File renamed without changes.
4 changes: 4 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog for Serilog.Enrichers.Sensitive

## 1.7.3

- Add support for masking property values that contain a `Uri`, reported by @yadanilov19

## 1.7.2

- FIx issue where Microsoft.Extensions.Configuration.Binder version 7.0.0 or up was required for JSON configuration to work. [#25](https://github.com/serilog-contrib/Serilog.Enrichers.Sensitive/issues/25)
Expand Down
11 changes: 11 additions & 0 deletions src/Serilog.Enrichers.Sensitive/MaskOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Serilog.Enrichers.Sensitive;

public class MaskOptions
{
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;

}
35 changes: 35 additions & 0 deletions src/Serilog.Enrichers.Sensitive/MaskPropertyCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections.Generic;

namespace Serilog.Enrichers.Sensitive;

public class MaskPropertyCollection : List<MaskProperty>
{
private readonly Dictionary<string, MaskOptions> _properties = new();

public void Add(string propertyName)
{
_properties.Add(propertyName.ToLower(), MaskOptions.Default);
}

public void Add(string propertyName, MaskOptions maskOptions)
{
_properties.Add(propertyName.ToLower(), maskOptions);
}

public bool TryGetProperty(string propertyName, out MaskOptions options)
{
return _properties.TryGetValue(propertyName.ToLower(), out options);
}

public static MaskPropertyCollection From(IEnumerable<string> enricherOptionsMaskProperties)
{
var collection = new MaskPropertyCollection();

foreach (var x in enricherOptionsMaskProperties)
{
collection.Add(x, MaskOptions.Default);
}

return collection;
}
}
130 changes: 127 additions & 3 deletions src/Serilog.Enrichers.Sensitive/SensitiveDataEnricher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ internal class SensitiveDataEnricher : ILogEventEnricher
{
private readonly MaskingMode _maskingMode;
public const string DefaultMaskValue = "***MASKED***";
private const string DefaultMaskPad = "***";

private static readonly MessageTemplateParser Parser = new();
private readonly FieldInfo _messageTemplateBackingField;
private readonly List<IMaskingOperator> _maskingOperators;
private readonly string _maskValue;
private readonly List<string> _maskProperties;
private readonly MaskPropertyCollection _maskProperties;
private readonly List<string> _excludeProperties;

public SensitiveDataEnricher(SensitiveDataEnricherOptions options)
Expand Down Expand Up @@ -113,9 +114,22 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
return (false, null);
}

if (_maskProperties.Contains(property.Key, StringComparer.InvariantCultureIgnoreCase))
if(_maskProperties.TryGetProperty(property.Key, out var options))
{
return (true, new ScalarValue(_maskValue));
if (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(MaskWithUriOptions(_maskValue, uriMaskOptions, uriValue)));
case ScalarValue { Value: Uri uriValue }:
return (true, new ScalarValue(MaskWithOptions(_maskValue, options, uriValue.ToString())));
}
}

switch (property.Value)
Expand All @@ -131,6 +145,21 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)

return (false, null);
}
// System.Uri is a built-in scalar type in Serilog:
// https://github.com/serilog/serilog/blob/dev/src/Serilog/Capturing/PropertyValueConverter.cs#L23
// which is why this needs special handling and isn't
// caught by the string value above.
case ScalarValue { Value: Uri uriValue }:
{
var (wasMasked, maskedValue) = ReplaceSensitiveDataFromString(uriValue.ToString());

if (wasMasked)
{
return (true, new ScalarValue(new Uri(maskedValue)));
}

return (false, null);
}
case SequenceValue sequenceValue:
var resultElements = new List<LogEventPropertyValue>();
var anyElementMasked = false;
Expand Down Expand Up @@ -198,6 +227,101 @@ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
}
}

private string MaskWithUriOptions(string maskValue, UriMaskOptions options, Uri uri)
{
var scheme = options.ShowScheme
? uri.Scheme
: DefaultMaskPad;

var host = options.ShowHost
? uri.Host
: DefaultMaskPad;

var path = options.ShowPath
? uri.AbsolutePath
: "/" + DefaultMaskPad;

var queryString = !string.IsNullOrEmpty(uri.Query)
? options.ShowQueryString
? uri.Query
: "?" + DefaultMaskPad
: "";

return $"{scheme}://{host}{path}{queryString}";
}

private string MaskWithOptions(string maskValue, MaskOptions options, string input)
{
if (options is { ShowFirst: >= 0, ShowLast: < 0 })
{
var start = options.ShowFirst;
if (start >= input.Length)
{
start = 1;
}

var first = input.Substring(0, start);
if (options.PreserveLength)
{
return first.PadRight(input.Length, '*');
}

return first + DefaultMaskPad;
}

if (options is { ShowFirst: < 0, ShowLast: >= 0 })
{
var end = input.Length - options.ShowLast;
if (end <= 0)
{
end = input.Length - 1;
}

var last = input.Substring(end);

if (options.PreserveLength)
{
return last.PadLeft(input.Length, '*');
}

return DefaultMaskPad + last;
}

if (options is { ShowFirst: >= 0, ShowLast: >= 0 })
{
if (options.ShowFirst + options.ShowLast >= input.Length)
{
if (input.Length > 3)
{
return input[0] + DefaultMaskPad + input.Last();
}

if (input.Length == 3)
{
return input[0] + DefaultMaskPad;
}

if (input.Length <= 2)
{
return maskValue;
}
}

var start = options.ShowFirst;
var end = input.Length - options.ShowLast;
var pad = input.Length - (input.Length - end);

if (options.PreserveLength)
{
return input.Substring(0, start).PadRight(pad, '*') + input.Substring(end);
}

return input.Substring(0, start) + DefaultMaskPad + input.Substring(end);
}

return maskValue;
}

private (bool, string) ReplaceSensitiveDataFromString(string input)
{
var isMasked = false;
Expand Down
21 changes: 18 additions & 3 deletions src/Serilog.Enrichers.Sensitive/SensitiveDataEnricherOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public SensitiveDataEnricherOptions(
Mode = mode;
MaskValue = maskValue;
MaskingOperators = maskingOperators == null ? new List<IMaskingOperator>() : ResolveMaskingOperators(maskingOperators);
MaskProperties = maskProperties?.ToList() ?? new List<string>();
MaskProperties = maskProperties == null ? new MaskPropertyCollection() : MaskPropertyCollection.From(maskProperties);
ExcludeProperties = excludeProperties?.ToList() ?? new List<string>();
}

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

/// <remarks>
/// 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.
Expand Down Expand Up @@ -130,4 +130,19 @@ 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;
}
}
9 changes: 9 additions & 0 deletions src/Serilog.Enrichers.Sensitive/UriMaskOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Serilog.Enrichers.Sensitive;

public class UriMaskOptions : MaskOptions
{
public bool ShowScheme { get; set; } = true;
public bool ShowHost { get; set; } = true;
public bool ShowPath { get; set; } = false;
public bool ShowQueryString { get; set; } = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp3.1;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net6.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>

<IsPackable>false</IsPackable>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Serilog.Core;
using Serilog.Sinks.InMemory;
using System;
using System.Text.RegularExpressions;
using Serilog.Sinks.InMemory.Assertions;
using Xunit;

namespace Serilog.Enrichers.Sensitive.Tests.Unit
{
public class WhenMaskingLogEventWithNonStringScalarValue
{
private readonly InMemorySink _inMemorySnk;
private readonly Logger _logger;

[Fact]
public void GivenPropertyValueIsUri_ValueIsMasked()
{
_logger.Information("Message {Prop}", new Uri("https://tempuri.org?someparam=SENSITIVE"));

_inMemorySnk
.Snapshot()
.Should()
.HaveMessage("Message {Prop}")
.Appearing()
.Once()
.WithProperty("Prop")
.WithValue(new Uri("https://tempuri.org?***MASKED***"));
}

[Fact]
public void GivenPropertyValueIsTypeWithToStringOverride_ValueIsMasked()
{
_logger.Information("Message {Prop}", new TypeWithToStringOverride("my SECRET message"));

_inMemorySnk
.Snapshot()
.Should()
.HaveMessage("Message {Prop}")
.Appearing()
.Once()
.WithProperty("Prop")
.WithValue("my ***MASKED*** message");
}

public WhenMaskingLogEventWithNonStringScalarValue()
{
_inMemorySnk = new InMemorySink();

_logger = new LoggerConfiguration()
.Enrich.WithSensitiveDataMasking(options =>
{
options.MaskingOperators.Add(new UriMaskingOperator());
options.MaskingOperators.Add(new TestRegexMaskingOperator());
})
.WriteTo.Sink(_inMemorySnk)
.CreateLogger();
}
}

public class TestRegexMaskingOperator : RegexMaskingOperator
{
public TestRegexMaskingOperator() : base("SECRET", RegexOptions.Compiled)
{
}
}

public class TypeWithToStringOverride
{
private readonly string _value;

public TypeWithToStringOverride(string value)
{
_value = value;
}

public override string ToString()
{
return _value;
}
}

public class UriMaskingOperator : RegexMaskingOperator
{
private const string SomePattern =
"someparam=.*?(.(?:&|$))";

public UriMaskingOperator() : base(SomePattern, RegexOptions.IgnoreCase | RegexOptions.Compiled)
{
}

protected override string PreprocessInput(string input)
{
return input;
}
}
}
Loading

0 comments on commit 1f9bf0b

Please sign in to comment.