24
loading...
This website collects cookies to deliver better user experience
FilterAttribute
class to the ASP.NET action invocation pipeline:// Startup.cs
protected override void ConfigureMvc(MvcOptions options)
{
options.Filters.Add<ApplicationInsightsActionFilterAttribute>();
}
// ApplicationInsightsActionFilterAttribute.cs
public class ApplicationInsightsActionFilterAttribute : ActionFilterAttribute
{
internal const string REQUEST_TELEMETRY_KEY = "Request-Body";
internal const string RESPONSE_TELEMETRY_KEY = "Response-Body";
private readonly ILogger<ApplicationInsightsActionFilterAttribute> _logger;
public ApplicationInsightsActionFilterAttribute(
ILogger<ApplicationInsightsActionFilterAttribute> logger)
{
_logger = logger;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.HttpContext is null)
{
await next();
return;
}
try
{
string fromBodyParameter = context.ActionDescriptor.Parameters
.Where(item => item.BindingInfo.BindingSource == BindingSource.Body)
.FirstOrDefault()
?.Name.ToUpperInvariant();
if (string.IsNullOrWhiteSpace(fromBodyParameter))
return;
KeyValuePair<string, object> arguments = context.ActionArguments
.First(x => x.Key.ToUpperInvariant() == fromBodyParameter);
string argument = SerializeDataUsingObfuscator(arguments.Value);
RequestTelemetry request = context.HttpContext.Features.Get<RequestTelemetry>();
request.Properties.Add(REQUEST_TELEMETRY_KEY, argument);
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error executing the response logger task");
}
finally
{
await next();
}
}
public override async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context?.HttpContext is null)
{
await next();
return;
}
try
{
if (context.Result is ObjectResult result &&
result.Value != null)
{
string serializedObj = SerializeDataUsingObfuscator(result.Value);
RequestTelemetry request = context.HttpContext.Features.Get<RequestTelemetry>();
request.Properties.Add(RESPONSE_TELEMETRY_KEY, serializedObj);
};
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error executing the response logger task");
}
finally
{
await next();
}
}
private static string SerializeDataUsingObfuscator(object value) =>
JsonConvert.SerializeObject(value,
new JsonSerializerSettings
{
ContractResolver = new ObfuscatorContractResolver()
});
}
OnActionExecutionAsync
and OnResultExecutionAsync
. The first one is fired before the incoming request triggers the controller action; the latter is fired when the controller action returns. Let's focus on OnActionExecutionAsync
for a moment: firstly it gets the controller action parameters that have a body, if any - you can change that code to get the query strings, paths, or whatever you'd like - and then calls the method SerializeDataUsingObfuscator
passing the CLR object that represents the request body. To better explain this concept, consider the following API:public async Task<ActionResult> MyWonderfulApi([FromBody] RequestDto dto)
{
// code
return Ok();
}
SerializeDataUsingObfuscator
method is of type RequestDto
and it contains the incoming data.ContractResolver
implementation, called ObfuscatorContractResolver
. This class serializes the request body following our obfuscation rules - that we are going to define in a minute.Sensitive
. Moreover, this attribute allows some customization to obfuscate properties using different patterns - ie. truncate the string, show the first/last N chars, etc. Here's the definition and an usage example:// WonderfulDto.cs
public class WonderfulDto
{
[Sensitive(ObfuscationType.MaskChar)]
[JsonProperty("pin")]
public string Pin { get; set; }
}
// SensitiveAttribute.cs using the Factory pattern to separate responsibilities
[AttributeUsage(
AttributeTargets.Property
, AllowMultiple = false)]
public class SensitiveAttribute : Attribute
{
private SensitiveBase Sensitive { get; }
public int? TruncateAfter { get; }
public SensitiveAttribute(
ObfuscationType obfuscationType,
int? truncateAfter)
{
TruncateAfter = truncateAfter;
Sensitive = GetSensitive(obfuscationType);
}
public SensitiveAttribute(
ObfuscationType obfuscationType)
{
TruncateAfter = null;
Sensitive = GetSensitive(obfuscationType);
}
private SensitiveBase GetSensitive(ObfuscationType obfuscationType)
{
return obfuscationType switch
{
ObfuscationType.HeadVisible => new SensitiveHeadVisible(TruncateAfter),
ObfuscationType.TailVisible => new SensitiveTailVisible(),
ObfuscationType.MaskChar => new SensitiveMaskChar(TruncateAfter),
_ => throw new NotImplementedException($"None type {nameof(obfuscationType)}: {obfuscationType}")
};
}
internal string Obfuscate(string strValue)
{
return Sensitive.Obfuscate(strValue);
}
internal string Obfuscate(Guid guid) => Sensitive.Obfuscate(guid);
internal string Obfuscate(DateTime dateTime) => Sensitive.Obfuscate(dateTime);
internal string Obfuscate(Enum @enum) => Sensitive.Obfuscate(@enum);
internal virtual string ObfuscateDefault(object value) => Sensitive.ObfuscateDefault(value);
}
ObfuscatorContractResolver
:// ObfuscatorContractResolver.cs
public class ObfuscatorContractResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
IList<JsonProperty> baseProperties = base.CreateProperties(type, memberSerialization);
var sensitiveData = new Dictionary<string, SensitiveAttribute>();
foreach (PropertyInfo p in type.GetProperties())
{
var customAttributes = p.GetCustomAttributes(false);
var jsonPropertyAttribute = customAttributes
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
if (jsonPropertyAttribute is null)
continue;
var sensitiveAttribute = customAttributes
.OfType<SensitiveAttribute>()
.FirstOrDefault();
if (sensitiveAttribute is null)
continue;
var propertyName = jsonPropertyAttribute.PropertyName.ToUpperInvariant();
sensitiveData.Add(propertyName, sensitiveAttribute);
}
if (!sensitiveData.Any())
return baseProperties;
var processedProperties = new List<JsonProperty>();
foreach (JsonProperty baseProperty in baseProperties)
{
if (sensitiveData.TryGetValue(baseProperty.PropertyName.ToUpperInvariant(), out SensitiveAttribute sensitiveAttribute))
{
baseProperty.PropertyType = typeof(string);
baseProperty.ValueProvider = new ObfuscatorValueProvider(baseProperty.ValueProvider, sensitiveAttribute);
}
processedProperties.Add(baseProperty);
}
return processedProperties;
}
}
// ObfuscatorValueProvider.cs
internal class ObfuscatorValueProvider : IValueProvider
{
private readonly IValueProvider _valueProvider;
private readonly SensitiveAttribute _sensitiveAttribute;
public ObfuscatorValueProvider(
IValueProvider valueProvider,
SensitiveAttribute sensitiveAttribute)
{
_valueProvider = valueProvider;
_sensitiveAttribute = sensitiveAttribute;
}
public object GetValue(object target)
{
var originalValue = _valueProvider.GetValue(target);
var result = originalValue switch
{
null => null,
string strValue => _sensitiveAttribute.Obfuscate(strValue),
Guid guid => _sensitiveAttribute.Obfuscate(guid),
Enum @enum => _sensitiveAttribute.Obfuscate(@enum),
DateTime dateTime => _sensitiveAttribute.Obfuscate(dateTime),
_ => _sensitiveAttribute.ObfuscateDefault(originalValue),
};
return result;
}
public void SetValue(object target, object value)
{
// we don't care
}
}
Sensitive
attribute definition over DTOs properties, JSON.NET understands if and how a given property should be serialized. The result will be sent to ApplicationInsights.OnResultExecutionAsync
implementation: if code runs smooth and no exception is thrown, the response body serialized using the obfuscation rules is attached to the RequestTelemetry
object.OnResultExecutionAsync
will not be fired. This happens because the flow runs out the normal ASP.NET action invocation pipeline.public void Configure(IApplicationBuilder app)
{
app.UseExceptionHandler(async (context) =>
{
RequestTelemetry request = context.Features.Get<RequestTelemetry>();
string serializedObj = JsonConvert.SerializeObject(error,
new JsonSerializerSettings
{
ContractResolver = new ObfuscatorContractResolver(),
});
request.Properties.Add(ApplicationInsightsActionFilterAttribute.RESPONSE_TELEMETRY_KEY, serializedObj);
}));
}
SensitiveHeadVisible
, SensitiveMaskChar
and SensitiveTailVisible
classes that implement some code to apply different obfuscation rules - such as hiding the value with a specific char, showing the string tail, and so on. You are free to create your preferred ones.ApplicationInsightsActionFilterAttribute
class, we did the following changes:// OnActionExecutionAsync
var loggerTask = Task.Run(() =>
{
string fromBodyParameter = context.ActionDescriptor.Parameters
// same code as before
});
await next();
try
{
if (loggerTask != null)
{
await loggerTask;
}
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error executing the response logger task");
}
// OnResultExecutionAsync
var loggerTask = Task.Run(() =>
{
if (context.Result is ObjectResult result &&
// same code as before
});
await next();
try
{
if (loggerTask != null)
{
await loggerTask;
}
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error executing the response logger task");
}
loggerTask
is already completed when awaited, so the impact is almost nil.ContractResolver
, you can obfuscate properties everywhere in your code! For example, we used the same approach for the log generated by our HttpClient
instances.