Skip to content
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
27 changes: 17 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,27 @@ After installing the Nuget package in your project, you need to take the followi
- **SubstituteText**: If set, the entire property value will be override with this text. Note that using this setting will ignore all other settings.
- **Mask**: Set to a character to use it when masking the property's value. By default, the character `*` is used.

> PS.: These customization fields only work for fields with a `string` base type.

2. Call the `JsonMask.MaskSensitiveData()` function, passing in your object instance as a parameter.

## Support

This library supports masking of `string` fields only, although it also supports `List<string>`/`IEnumerable<string>` and `Dictionary<string, string>`. Nested class properties are also masked, independently of depth.

| Property Type | Support |
|:---: |:---: |
| string | ✅ |
| List\<T>, where T is a class or string | ✅ |
| IEnumerable\<T>, where T is a class or string | ✅ |
| Dictionary<string, string> | ✅ |
| Any other collection type, such as Array, ArrayList\<T>, etc | ❌ |
| Any other base type different from string | ❌ |
### Base Types
This library supports masking of the following base types:
- `string` fields, which are masked following the rules detailed in the [Usage](#usage) section.
- Other types such as `bool`, `(s)byte`, `(u)short`, `(u)int`, `(u)long`, `float`, `double`, `decimal`, `char`, `Datetime`, `DatetimeOffset`, and `Guid`, which are set to their respective default values when having the `[SensitiveData]` attribute.
- Any other base types are currently NOT supported.

### Collections
The library also supports the masking of some collections, such as:
- `List<T>`, where T is a class or `string`.
- `IEnumerable<T>`, where T is a class or `string`.
- `Dictionary<string, string>`.
- Any other collection type, such as `Array`, `ArrayList<T>`, etc., is NOT supported.

### Nested class fields
Nested class properties are also masked, independently of depth.

## Examples

Expand Down
66 changes: 56 additions & 10 deletions src/JsonDataMasking.Test/JsonMaskTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,6 @@ public void MaskSensitiveData_UsesCustomMask_WhenHasAttributeWithCustomMaskChara
Assert.Equal(expectedMask, maskedCustomerData.CreditCardSecurityCode);
}

[Fact]
public void MaskSensitiveData_ThrowsNotSupportedException_WhenPropertyWithNotSupportedTypeHasAttribute()
{
// Arrange
var creditCard = new CreditCardMock { SecurityCode = 999 };

// Act and Assert
Assert.Throws<NotSupportedException>(() => JsonMask.MaskSensitiveData(creditCard));
}

[Fact]
public void MaskSensitiveData_MasksUsingDefaultSize_WhenHasAttributeWithInvalidShowFirstAndLastRange()
{
Expand Down Expand Up @@ -373,5 +363,61 @@ public void MaskSensitiveData_ThrowsNotSupportedException_WhenPropertyIsArray()
// Act and assert
Assert.Throws<NotSupportedException>(() => JsonMask.MaskSensitiveData(passcodes));
}

[Fact]
public void MaskSensitiveData_MasksOtherBaseTypes_WhenTheyHaveAttribute()
{
// Arrange
var baseTypes = new BasicTypesSensitiveMock();

// Act
var maskedBaseTypes = JsonMask.MaskSensitiveData(baseTypes);

// Assert
Assert.False(maskedBaseTypes.Bool);
Assert.Equal(0, maskedBaseTypes.Byte);
Assert.Equal(0, maskedBaseTypes.Sbyte);
Assert.Equal(0, maskedBaseTypes.Short);
Assert.Equal(0, maskedBaseTypes.Ushort);
Assert.Equal(0, maskedBaseTypes.Int);
Assert.Equal(0u, maskedBaseTypes.Uint);
Assert.Equal(0, maskedBaseTypes.Long);
Assert.Equal(0u, maskedBaseTypes.Ulong);
Assert.Equal(0, maskedBaseTypes.Float);
Assert.Equal(0, maskedBaseTypes.Double);
Assert.Equal(0, maskedBaseTypes.Decimal);
Assert.Equal('\0', maskedBaseTypes.Char);
Assert.Equal(new DateTime(), maskedBaseTypes.DateTime);
Assert.Equal(new DateTimeOffset(), maskedBaseTypes.DateTimeOffset);
Assert.Equal(Guid.Empty, maskedBaseTypes.Guid);
}

[Fact]
public void MaskSensitiveData_DoesNotMaskOtherBaseTypes_WhenWithoutSensitiveAttribute()
{
// Arrange
var nonSensitiveBaseTypes = new BasicTypesMock();

// Act
var maskedBaseTypes = JsonMask.MaskSensitiveData(nonSensitiveBaseTypes);

// Assert
Assert.True(maskedBaseTypes.Bool);
Assert.Equal(1, maskedBaseTypes.Byte);
Assert.Equal(1, maskedBaseTypes.Sbyte);
Assert.Equal(1, maskedBaseTypes.Short);
Assert.Equal(1, maskedBaseTypes.Ushort);
Assert.Equal(1, maskedBaseTypes.Int);
Assert.Equal(1u, maskedBaseTypes.Uint);
Assert.Equal(1, maskedBaseTypes.Long);
Assert.Equal(1u, maskedBaseTypes.Ulong);
Assert.Equal(1, maskedBaseTypes.Float);
Assert.Equal(1, maskedBaseTypes.Double);
Assert.Equal(1, maskedBaseTypes.Decimal);
Assert.Equal('1', maskedBaseTypes.Char);
Assert.Equal(new DateTime().AddYears(1), maskedBaseTypes.DateTime);
Assert.Equal(new DateTimeOffset().AddYears(1), maskedBaseTypes.DateTimeOffset);
Assert.Equal(Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), maskedBaseTypes.Guid);
}
}
}
24 changes: 24 additions & 0 deletions src/JsonDataMasking.Test/MockData/BasicTypesMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace JsonDataMasking.Test.MockData
{
public class BasicTypesMock
{
public bool Bool { get; set; } = true;
public byte Byte { get; set; } = 1;
public sbyte Sbyte { get; set; } = 1;
public short Short { get; set; } = 1;
public ushort Ushort { get; set; } = 1;
public int Int { get; set; } = 1;
public uint Uint { get; set; } = 1;
public long Long { get; set; } = 1;
public ulong Ulong { get; set; } = 1;
public float Float { get; set; } = 1;
public double Double { get; set; } = 1;
public decimal Decimal { get; set; } = 1;
public char Char { get; set; } = '1';
public DateTime DateTime { get; set; } = new DateTime().AddYears(1);
public DateTimeOffset DateTimeOffset { get; set; } = new DateTimeOffset().AddYears(1);
public Guid Guid { get; set; } = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff");
}
}
41 changes: 41 additions & 0 deletions src/JsonDataMasking.Test/MockData/BasicTypesSensitiveMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using JsonDataMasking.Attributes;
using System;

namespace JsonDataMasking.Test.MockData
{
public class BasicTypesSensitiveMock
{
[SensitiveData]
public bool Bool { get; set; } = true;
[SensitiveData]
public byte Byte { get; set; } = 1;
[SensitiveData]
public sbyte Sbyte { get; set; } = 1;
[SensitiveData]
public short Short { get; set; } = 1;
[SensitiveData]
public ushort Ushort { get; set; } = 1;
[SensitiveData]
public int Int { get; set; } = 1;
[SensitiveData]
public uint Uint { get; set; } = 1;
[SensitiveData]
public long Long { get; set; } = 1;
[SensitiveData]
public ulong Ulong { get; set; } = 1;
[SensitiveData]
public float Float { get; set; } = 1;
[SensitiveData]
public double Double { get; set; } = 1;
[SensitiveData]
public decimal Decimal { get; set; } = 1;
[SensitiveData]
public char Char { get; set; } = '1';
[SensitiveData]
public DateTime DateTime { get; set; } = new DateTime().AddYears(1);
[SensitiveData]
public DateTimeOffset DateTimeOffset { get; set; } = new DateTimeOffset().AddYears(1);
[SensitiveData]
public Guid Guid { get; set; } = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff");
}
}
48 changes: 35 additions & 13 deletions src/JsonDataMasking/Masks/JsonMask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace JsonDataMasking.Masks
{
public static class JsonMask

Check warning on line 12 in src/JsonDataMasking/Masks/JsonMask.cs

View workflow job for this annotation

GitHub Actions / Build

Missing XML comment for publicly visible type or member 'JsonMask'
{
/// <summary>
/// Default size of the mask, used if <c>PreserveLength</c> is set to <c>false</c>.
Expand All @@ -18,7 +18,7 @@

#region Masking
/// <summary>
/// Mask values of <c>string</c> and some <c>string collections</c> type class properties that have the <c>[SensitiveData]</c> attribute.
/// Mask values of primitive types and some <c>string collections</c> type class properties that have the <c>[SensitiveData]</c> attribute.
/// Properties with <c>null</c> values or that don't have the attribute remain unchanged.
/// </summary>
/// <typeparam name="T"></typeparam>
Expand Down Expand Up @@ -61,16 +61,44 @@
{
MaskDictionaryProperty(data, property);
}
else if (propertyAttribute != null && IsSupportedBaseType(property.PropertyType))
else if (propertyAttribute != null)
{
var maskedPropertyValue = GetMaskedPropertyValue(propertyValue?.ToString(), propertyAttribute);
var maskedPropertyValue = GetMaskedPropertyValue(property.PropertyType, propertyValue, propertyAttribute);
property.SetValue(data, maskedPropertyValue);
}
}
return data;
}

private static string? GetMaskedPropertyValue(string? currentPropertyValue, SensitiveDataAttribute attribute)
private static readonly Dictionary<Type, object?> MaskDefaults = new Dictionary<Type, object?>()
{
[typeof(bool)] = default(bool),
[typeof(byte)] = default(byte),
[typeof(sbyte)] = default(sbyte),
[typeof(short)] = default(short),
[typeof(ushort)] = default(ushort),
[typeof(int)] = default(int),
[typeof(uint)] = default(uint),
[typeof(long)] = default(long),
[typeof(ulong)] = default(ulong),
[typeof(float)] = default(float),
[typeof(double)] = default(double),
[typeof(decimal)] = default(decimal),
[typeof(char)] = default(char),
[typeof(DateTime)] = default(DateTime),
[typeof(DateTimeOffset)] = default(DateTimeOffset),
[typeof(Guid)] = default(Guid)
};

private static object? GetMaskedPropertyValue(Type type, object? currentPropertyValue, SensitiveDataAttribute attribute)
=> type switch
{
_ when type == typeof(string) => GetMaskedPropertyValueString(currentPropertyValue?.ToString(), attribute),
_ when MaskDefaults.TryGetValue(type, out var defaultValue) => defaultValue,
_ => throw new NotSupportedException($"Masking of type {type.Name} is not supported")
};

private static string? GetMaskedPropertyValueString(string? currentPropertyValue, SensitiveDataAttribute attribute)
{
if (string.IsNullOrWhiteSpace(currentPropertyValue)) return currentPropertyValue;

Expand Down Expand Up @@ -114,8 +142,8 @@
object? maskedCollectionValue = null;
if (IsClassReferenceType(collectionType))
maskedCollectionValue = MaskPropertiesWithSensitiveDataAttribute(value);
else if (propertyAttribute != null && IsSupportedBaseType(collectionType))
maskedCollectionValue = GetMaskedPropertyValue(value?.ToString(), propertyAttribute);
else if (propertyAttribute != null)
maskedCollectionValue = GetMaskedPropertyValue(collectionType, value, propertyAttribute);

if (maskedCollectionValue != null)
maskedCollection.Add(maskedCollectionValue);
Expand All @@ -138,7 +166,7 @@

foreach (var pair in collection)
{
var maskedCollectionValue = GetMaskedPropertyValue(pair.Value, propertyAttribute);
var maskedCollectionValue = GetMaskedPropertyValueString(pair.Value, propertyAttribute);
maskedCollection.Add(pair.Key, maskedCollectionValue);
}
property.SetValue(data, maskedCollection);
Expand All @@ -160,12 +188,6 @@
private static bool IsPropertyTypeEqualsToAnonymousType(PropertyInfo property) =>
property.ReflectedType.AssemblyQualifiedName.Contains("AnonymousType");

private static bool IsSupportedBaseType(Type type) => type switch
{
Type _ when type == typeof(string) => true,
_ => throw new NotSupportedException("Masking of non-string base types is not supported")
};

private static bool AreFirstAndLastParametersInValidRange(int propertySize, SensitiveDataAttribute attribute) =>
attribute.ShowFirst <= propertySize && attribute.ShowLast <= propertySize && (attribute.ShowFirst + attribute.ShowLast) <= propertySize;

Expand Down
Loading