diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/HttpRequestExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/HttpRequestExtensions.cs deleted file mode 100644 index c99194925..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/HttpRequestExtensions.cs +++ /dev/null @@ -1,53 +0,0 @@ -#nullable disable -using Microsoft.AspNetCore.Http.Extensions; -using TeachingRecordSystem.Api.Infrastructure.Logging; - -namespace TeachingRecordSystem.Api; - -public static class HttpRequestExtensions -{ - public static string GetScrubbedRequestUrl(this HttpRequest httpRequest) - { - if (httpRequest is null) - { - throw new ArgumentNullException(nameof(httpRequest)); - } - - var httpContext = httpRequest.HttpContext; - var requestUrl = httpRequest.GetEncodedUrl(); - - var redactedUrlParameters = httpContext.GetEndpoint()?.Metadata?.GetMetadata(); - - return redactedUrlParameters?.ScrubUrl(requestUrl) ?? requestUrl; - } - - public static string GetScrubbedRequestPathAndQuery(this HttpRequest httpRequest) - { - if (httpRequest is null) - { - throw new ArgumentNullException(nameof(httpRequest)); - } - - var httpContext = httpRequest.HttpContext; - var pathAndQuery = $"{httpContext.Request.Path}{httpContext.Request.QueryString}"; - - var redactedUrlParameters = httpContext.GetEndpoint()?.Metadata?.GetMetadata(); - - return redactedUrlParameters?.ScrubUrl(pathAndQuery) ?? pathAndQuery; - } - - public static string GetScrubbedQueryString(this HttpRequest httpRequest) - { - if (httpRequest is null) - { - throw new ArgumentNullException(nameof(httpRequest)); - } - - var httpContext = httpRequest.HttpContext; - var queryString = httpContext.Request.QueryString.ToString(); - - var redactedUrlParameters = httpContext.GetEndpoint()?.Metadata?.GetMetadata(); - - return redactedUrlParameters?.ScrubQueryString(queryString) ?? queryString; - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/ApplicationInsights/RedactedUrlTelemetryProcessor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/ApplicationInsights/RedactedUrlTelemetryProcessor.cs deleted file mode 100644 index 3858da777..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/ApplicationInsights/RedactedUrlTelemetryProcessor.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.ApplicationInsights.Extensibility; - -namespace TeachingRecordSystem.Api.Infrastructure.ApplicationInsights; - -public class RedactedUrlTelemetryProcessor : ITelemetryProcessor -{ - private readonly ITelemetryProcessor _next; - private readonly IHttpContextAccessor _httpContextAccessor; - - public RedactedUrlTelemetryProcessor(ITelemetryProcessor next, IHttpContextAccessor httpContextAccessor) - { - _next = next; - _httpContextAccessor = httpContextAccessor; - } - - public void Process(ITelemetry item) - { - if (item is RequestTelemetry requestTelemetry) - { - requestTelemetry.Url = new Uri(_httpContextAccessor.HttpContext!.Request.GetScrubbedRequestUrl()); - } - - _next.Process(item); - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RedactQueryParamAttribute.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RedactQueryParamAttribute.cs deleted file mode 100644 index 52dc7e20c..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RedactQueryParamAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ApplicationModels; - -namespace TeachingRecordSystem.Api.Infrastructure.Logging; - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public sealed class RedactQueryParamAttribute : Attribute, IActionModelConvention -{ - public RedactQueryParamAttribute(string queryParameterName) - { - QueryParameterName = queryParameterName ?? throw new ArgumentNullException(nameof(queryParameterName)); - } - - public string QueryParameterName { get; } - - public void Apply(ActionModel action) - { - foreach (var selector in action.Selectors) - { - var endpointMetadata = selector.EndpointMetadata; - - var redactedInfo = endpointMetadata.OfType().SingleOrDefault(); - if (redactedInfo is null) - { - redactedInfo = new(); - endpointMetadata.Add(redactedInfo); - } - - redactedInfo.QueryParameters.Add(QueryParameterName); - } - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RedactedUrlParameters.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RedactedUrlParameters.cs deleted file mode 100644 index b1241a9ce..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RedactedUrlParameters.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.WebUtilities; - -namespace TeachingRecordSystem.Api.Infrastructure.Logging; - -internal sealed class RedactedUrlParameters -{ - public RedactedUrlParameters() - { - QueryParameters = new HashSet(StringComparer.OrdinalIgnoreCase); - } - - public ISet QueryParameters { get; } - - public string ScrubUrl(string url) - { - if (url is null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (!url.Contains("?")) - { - return url; - } - - var path = url.Split('?')[0]; - var queryString = url.Split('?')[1]; - - var scrubbedQueryString = ScrubQueryString(queryString); - return $"{path}{scrubbedQueryString}"; - } - - public string ScrubQueryString(string queryString) - { - if (string.IsNullOrEmpty(queryString)) - { - return string.Empty; - } - - var query = QueryHelpers.ParseQuery(queryString); - - foreach (var key in query.Keys.ToArray()) - { - if (QueryParameters.Contains(key)) - { - query[key] = "**REDACTED**"; - } - } - - var scrubbedQueryString = new QueryBuilder(query).ToString(); - return scrubbedQueryString; - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RemoveRedactedUrlParametersEnricher.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RemoveRedactedUrlParametersEnricher.cs deleted file mode 100644 index 23a7d14f8..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RemoveRedactedUrlParametersEnricher.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Serilog.Core; -using Serilog.Events; - -namespace TeachingRecordSystem.Api.Infrastructure.Logging; - -public class RemoveRedactedUrlParametersEnricher : ILogEventEnricher -{ - private readonly HttpContext _httpContext; - - public RemoveRedactedUrlParametersEnricher(HttpContext httpContext) - { - _httpContext = httpContext; - } - - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - const string requestPathPropertyName = "RequestPath"; - - if (logEvent.Properties.ContainsKey(requestPathPropertyName)) - { - var scrubbedRequestPath = _httpContext.Request.GetScrubbedRequestPathAndQuery(); - logEvent.AddOrUpdateProperty(new LogEventProperty(requestPathPropertyName, new ScalarValue(scrubbedRequestPath))); - } - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RemoveRedactedUrlParametersEventProcessor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RemoveRedactedUrlParametersEventProcessor.cs deleted file mode 100644 index ad3eaabf1..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/RemoveRedactedUrlParametersEventProcessor.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Sentry.Extensibility; - -namespace TeachingRecordSystem.Api.Infrastructure.Logging; - -public class RemoveRedactedUrlParametersEventProcessor : ISentryEventProcessor -{ - private readonly IHttpContextAccessor _httpContextAccessor; - - public RemoveRedactedUrlParametersEventProcessor(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - public SentryEvent Process(SentryEvent @event) - { - var httpContext = _httpContextAccessor.HttpContext ?? throw new System.Exception("No HttpContext."); - @event.Request.QueryString = httpContext.Request.GetScrubbedQueryString(); - return @event; - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs index 77f81984d..80bd0a4e3 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/Program.cs @@ -12,7 +12,6 @@ using TeachingRecordSystem.Api.Infrastructure.ApplicationModel; using TeachingRecordSystem.Api.Infrastructure.Filters; using TeachingRecordSystem.Api.Infrastructure.Json; -using TeachingRecordSystem.Api.Infrastructure.Logging; using TeachingRecordSystem.Api.Infrastructure.Mapping; using TeachingRecordSystem.Api.Infrastructure.Middleware; using TeachingRecordSystem.Api.Infrastructure.ModelBinding; @@ -28,6 +27,7 @@ using TeachingRecordSystem.Core.Services.NameSynonyms; using TeachingRecordSystem.Core.Services.TrnGenerationApi; using TeachingRecordSystem.ServiceDefaults; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; [assembly: ApiController] namespace TeachingRecordSystem.Api; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/TeachingRecordSystem.Api.csproj b/TeachingRecordSystem/src/TeachingRecordSystem.Api/TeachingRecordSystem.Api.csproj index fb1dfd46e..cc1b22691 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/TeachingRecordSystem.Api.csproj +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/TeachingRecordSystem.Api.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -10,13 +10,10 @@ - - - diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V1/Controllers/TeachersController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V1/Controllers/TeachersController.cs index 297444692..9f415e92d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V1/Controllers/TeachersController.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V1/Controllers/TeachersController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -using TeachingRecordSystem.Api.Infrastructure.Logging; using TeachingRecordSystem.Api.Infrastructure.Security; using TeachingRecordSystem.Api.V1.Requests; using TeachingRecordSystem.Api.V1.Responses; @@ -28,7 +27,6 @@ public TeachersController(IMediator mediator) [ProducesResponseType(typeof(GetTeacherResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] - [RedactQueryParam("birthdate"), RedactQueryParam("nino")] public async Task GetTeacher([FromRoute] GetTeacherRequest request) { var response = await _mediator.Send(request); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/IttOutcomeController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/IttOutcomeController.cs index e0275a567..3073aaf40 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/IttOutcomeController.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/IttOutcomeController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -using TeachingRecordSystem.Api.Infrastructure.Logging; using TeachingRecordSystem.Api.Infrastructure.Security; using TeachingRecordSystem.Api.V2.Requests; using TeachingRecordSystem.Api.V2.Responses; @@ -30,7 +29,6 @@ public IttOutcomeController(IMediator mediator) [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)] [MapError(10001, statusCode: StatusCodes.Status404NotFound)] [MapError(10002, statusCode: StatusCodes.Status409Conflict)] - [RedactQueryParam("birthdate")] public async Task SetIttOutcome([FromBody] SetIttOutcomeRequest request) { var response = await _mediator.Send(request); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/TeachersController.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/TeachersController.cs index 63dbe11cc..972bc8b4a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/TeachersController.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Api/V2/Controllers/TeachersController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; -using TeachingRecordSystem.Api.Infrastructure.Logging; using TeachingRecordSystem.Api.Infrastructure.Security; using TeachingRecordSystem.Api.V2.Requests; using TeachingRecordSystem.Api.V2.Responses; @@ -53,7 +52,6 @@ public async Task GetTeacher([FromRoute] GetTeacherRequest reques [ProducesResponseType(StatusCodes.Status204NoContent)] [MapError(10001, statusCode: StatusCodes.Status404NotFound)] [MapError(10002, statusCode: StatusCodes.Status409Conflict)] - [RedactQueryParam("birthdate")] [Authorize(Policy = AuthorizationPolicies.UpdatePerson)] public async Task Update([FromBody] UpdateTeacherRequest request) { diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Logging/WebApplicationBuilderExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Logging/WebApplicationBuilderExtensions.cs deleted file mode 100644 index 57212a3e2..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Infrastructure/Logging/WebApplicationBuilderExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Serilog; - -namespace TeachingRecordSystem.AuthorizeAccess.Infrastructure.Logging; - -public static class WebApplicationBuilderExtensions -{ - public static WebApplicationBuilder ConfigureLogging(this WebApplicationBuilder builder) - { - if (builder.Environment.IsProduction()) - { - builder.WebHost.UseSentry(dsn: builder.Configuration.GetRequiredValue("Sentry:Dsn")); - } - - builder.Services.AddApplicationInsightsTelemetry(); - - // We want all logging to go through Serilog so that our filters are always applied - builder.Logging.ClearProviders(); - - builder.Host.UseSerilog((ctx, services, config) => config.ConfigureSerilog(ctx.HostingEnvironment, ctx.Configuration, services)); - - return builder; - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs index e8aea82cf..9671f2167 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/Program.cs @@ -16,7 +16,6 @@ using TeachingRecordSystem.AuthorizeAccess; using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Filters; using TeachingRecordSystem.AuthorizeAccess.Infrastructure.FormFlow; -using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Logging; using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Middleware; using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Oidc; using TeachingRecordSystem.AuthorizeAccess.Infrastructure.Security; @@ -28,6 +27,7 @@ using TeachingRecordSystem.Core.Services.Files; using TeachingRecordSystem.Core.Services.PersonMatching; using TeachingRecordSystem.ServiceDefaults; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; using TeachingRecordSystem.SupportUi.Infrastructure.FormFlow; using TeachingRecordSystem.UiCommon.Filters; using TeachingRecordSystem.UiCommon.FormFlow; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj index f90f85281..64c7dea76 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/TeachingRecordSystem.AuthorizeAccess.csproj @@ -14,10 +14,7 @@ - - - diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/appsettings.json b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/appsettings.json index 130b8d51d..a3e445ec3 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/appsettings.json +++ b/TeachingRecordSystem/src/TeachingRecordSystem.AuthorizeAccess/appsettings.json @@ -1,10 +1,7 @@ { "Serilog": { "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft.AspNetCore": "Warning" - } + "Default": "Information" }, "Enrich": [ "FromLogContext" ], "Using": [ "Serilog.Expressions" ] diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Extensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Extensions.cs index 6d016196d..1dd5a65e5 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Extensions.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Extensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting; using Prometheus; using TeachingRecordSystem.Core.Infrastructure.Configuration; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; namespace TeachingRecordSystem.ServiceDefaults; @@ -24,6 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults( builder.Services.AddHealthChecks().AddNpgSql(builder.Configuration.GetPostgresConnectionString()); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + builder.Services.AddSingleton(); if (builder.Environment.IsProduction()) { diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/ApplicationInsights/RedactedUrlTelemetryProcessor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/ApplicationInsights/RedactedUrlTelemetryProcessor.cs new file mode 100644 index 000000000..2b1978034 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/ApplicationInsights/RedactedUrlTelemetryProcessor.cs @@ -0,0 +1,19 @@ +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; + +namespace TeachingRecordSystem.ServiceDefaults.Infrastructure.ApplicationInsights; + +public class RedactedUrlTelemetryProcessor(ITelemetryProcessor next, UrlRedactor urlRedactor) : ITelemetryProcessor +{ + public void Process(ITelemetry item) + { + if (item is RequestTelemetry requestTelemetry) + { + requestTelemetry.Url = new Uri(urlRedactor.GetScrubbedRequestUrl()); + } + + next.Process(item); + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RedactParametersAttribute.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RedactParametersAttribute.cs new file mode 100644 index 000000000..0f3de1fa8 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RedactParametersAttribute.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; + +namespace TeachingRecordSystem; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public sealed class RedactParametersAttribute(params string[] parameterNames) : Attribute, IActionModelConvention, IPageApplicationModelConvention +{ + public string[] ParameterNames { get; } = parameterNames; + + void IActionModelConvention.Apply(ActionModel action) + { + foreach (var selector in action.Selectors) + { + UpdateEndpointMetadata(selector.EndpointMetadata); + } + } + + void IPageApplicationModelConvention.Apply(PageApplicationModel model) => UpdateEndpointMetadata(model.EndpointMetadata); + + private void UpdateEndpointMetadata(IList endpointMetadata) + { + var metadata = endpointMetadata.OfType().SingleOrDefault(); + if (metadata is null) + { + metadata = new(); + endpointMetadata.Add(metadata); + } + + foreach (var parameterName in ParameterNames) + { + metadata.ParameterNames.Add(parameterName); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RedactedParametersMetadata.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RedactedParametersMetadata.cs new file mode 100644 index 000000000..b2cc5c047 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RedactedParametersMetadata.cs @@ -0,0 +1,6 @@ +namespace TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; + +public class RedactedParametersMetadata +{ + public ICollection ParameterNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase); +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RemoveRedactedUrlParametersEnricher.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RemoveRedactedUrlParametersEnricher.cs new file mode 100644 index 000000000..530094dde --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RemoveRedactedUrlParametersEnricher.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http; +using Serilog.Core; +using Serilog.Events; + +namespace TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; + +public class RemoveRedactedUrlParametersEnricher(IHttpContextAccessor httpContextAccessor, UrlRedactor urlRedactor) : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + var httpContext = httpContextAccessor.HttpContext; + + if (httpContext is null) + { + return; + } + + foreach (var (key, value) in logEvent.Properties) + { + if (value is ScalarValue scalarValue && scalarValue.Value is string stringValue) + { + var newValue = stringValue; + + var requestPath = httpContext.Request.Path; + if (stringValue.Contains(requestPath)) + { + var scrubbedRequestPath = urlRedactor.GetScrubbedRequestPath(); + newValue = stringValue.Replace(requestPath, scrubbedRequestPath); + } + + var queryString = httpContext.Request.QueryString.ToString(); + if (queryString is not "" && stringValue.Contains(queryString.TrimStart('?'))) + { + var scrubbedQueryString = urlRedactor.GetScrubbedQueryString(); + newValue = newValue.Replace(queryString.TrimStart('?'), scrubbedQueryString.TrimStart('?')); + } + + if (newValue != stringValue) + { + logEvent.AddOrUpdateProperty(new LogEventProperty(key, new ScalarValue(newValue))); + } + } + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RemoveRedactedUrlParametersEventProcessor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RemoveRedactedUrlParametersEventProcessor.cs new file mode 100644 index 000000000..db4731d3b --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/RemoveRedactedUrlParametersEventProcessor.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Sentry.Extensibility; + +namespace TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; + +public class RemoveRedactedUrlParametersEventProcessor(IHttpContextAccessor httpContextAccessor, UrlRedactor urlRedactor) : ISentryEventProcessor +{ + public SentryEvent Process(SentryEvent @event) + { + var httpContext = httpContextAccessor.HttpContext; + + if (httpContext is not null) + { + @event.Request.QueryString = urlRedactor.GetScrubbedQueryString(); + @event.Request.Url = urlRedactor.GetScrubbedRequestUrl(); + } + + return @event; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/UrlRedactor.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/UrlRedactor.cs new file mode 100644 index 000000000..267946a25 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/UrlRedactor.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.AspNetCore.WebUtilities; + +namespace TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; + +public class UrlRedactor(IHttpContextAccessor httpContextAccessor) +{ + private const string RedactedContent = "***"; + + private static readonly HashSet _knownPiiFieldNames = new( + [ + "trn", + "birthdate", + "dateofbirth", + "firstname", + "middlename", + "lastname", + "previousfirstname", + "previousmiddlename", + "previouslastname", + "slugid", + "email", + "emailaddress", + "nationalinsurancenumber", + "nino", + "teacherid", + ], + StringComparer.OrdinalIgnoreCase); + + public string GetScrubbedRequestUrl() + { + var httpContext = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No HttpContext."); + var endpoint = httpContext.GetEndpoint(); + + var path = httpContext.Request.Path; + var queryString = httpContext.Request.QueryString.ToString(); + + var request = httpContext.Request; + + return UriHelper.BuildAbsolute( + request.Scheme, + request.Host, + request.PathBase, + RedactPiiFromUrlPath(path, endpoint, httpContext), + new QueryString(RedactPiiFromUrlQueryString(queryString, endpoint))); + } + + public string GetScrubbedRequestPath() + { + var httpContext = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No HttpContext."); + var endpoint = httpContext.GetEndpoint(); + + var path = httpContext.Request.Path; + return RedactPiiFromUrlPath(path, endpoint, httpContext); + } + + public string GetScrubbedRequestPathAndQuery() + { + var httpContext = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No HttpContext."); + var endpoint = httpContext.GetEndpoint(); + + var path = httpContext.Request.Path; + var queryString = httpContext.Request.QueryString.ToString(); + return $"{RedactPiiFromUrlPath(path, endpoint, httpContext)}{RedactPiiFromUrlQueryString(queryString, endpoint)}"; + } + + public string GetScrubbedQueryString() + { + var httpContext = httpContextAccessor.HttpContext ?? throw new InvalidOperationException("No HttpContext."); + var endpoint = httpContext.GetEndpoint(); + + var queryString = httpContext.Request.QueryString.ToString(); + return RedactPiiFromUrlQueryString(queryString, endpoint); + } + + private static RedactedParametersMetadata GetRedactedParametersMetadata(Endpoint? endpoint) => + endpoint?.Metadata.GetMetadata() ?? new(); + + private static string RedactPiiFromUrlPath(string path, Endpoint? endpoint, HttpContext httpContext) + { + if (endpoint is null || endpoint is not RouteEndpoint routeEndpoint) + { + return path; + } + + var endpointMetadata = GetRedactedParametersMetadata(endpoint); + + foreach (var parameter in routeEndpoint.RoutePattern.Parameters) + { + if (parameter is RoutePatternParameterPart routePatternParameterPart && + (_knownPiiFieldNames.Contains(routePatternParameterPart.Name) || endpointMetadata.ParameterNames.Contains(routePatternParameterPart.Name))) + { + var routeValue = httpContext.GetRouteValue(routePatternParameterPart.Name)!.ToString()!; + path = path.Replace(routeValue, RedactedContent); + } + } + + return path; + } + + private static string RedactPiiFromUrlQueryString(string queryString, Endpoint? endpoint) + { + var endpointMetadata = GetRedactedParametersMetadata(endpoint); + + var query = QueryHelpers.ParseQuery(queryString); + + foreach (var key in query.Keys.ToArray()) + { + if (_knownPiiFieldNames.Contains(key) || endpointMetadata.ParameterNames.Contains(key) == true) + { + query[key] = RedactedContent; + } + } + + var scrubbedQueryString = new QueryBuilder(query).ToString(); + return scrubbedQueryString; + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/WebApplicationBuilderExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/WebApplicationBuilderExtensions.cs similarity index 53% rename from TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/WebApplicationBuilderExtensions.cs rename to TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/WebApplicationBuilderExtensions.cs index a9dd07289..7b8edcae3 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Api/Infrastructure/Logging/WebApplicationBuilderExtensions.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/Infrastructure/Logging/WebApplicationBuilderExtensions.cs @@ -1,8 +1,13 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Sentry.Extensibility; using Serilog; -using TeachingRecordSystem.Api.Infrastructure.ApplicationInsights; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.ApplicationInsights; -namespace TeachingRecordSystem.Api.Infrastructure.Logging; +namespace TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; public static class WebApplicationBuilderExtensions { @@ -18,10 +23,17 @@ public static WebApplicationBuilder ConfigureLogging(this WebApplicationBuilder builder.Services.AddApplicationInsightsTelemetry() .AddApplicationInsightsTelemetryProcessor(); + builder.Services.AddTransient(); + // We want all logging to go through Serilog so that our filters are always applied builder.Logging.ClearProviders(); - builder.Host.UseSerilog((ctx, services, config) => config.ConfigureSerilog(ctx.HostingEnvironment, ctx.Configuration, services)); + builder.Host.UseSerilog((ctx, services, config) => + { + config.ConfigureSerilog(ctx.HostingEnvironment, ctx.Configuration, services); + + config.Enrich.With(services.GetRequiredService()); + }); return builder; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/TeachingRecordSystem.ServiceDefaults.csproj b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/TeachingRecordSystem.ServiceDefaults.csproj index a48ef7ffa..fddb128b7 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/TeachingRecordSystem.ServiceDefaults.csproj +++ b/TeachingRecordSystem/src/TeachingRecordSystem.ServiceDefaults/TeachingRecordSystem.ServiceDefaults.csproj @@ -13,8 +13,11 @@ + + + diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Infrastructure/Logging/WebApplicationBuilderExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Infrastructure/Logging/WebApplicationBuilderExtensions.cs deleted file mode 100644 index 7290c60e8..000000000 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Infrastructure/Logging/WebApplicationBuilderExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Serilog; - -namespace TeachingRecordSystem.SupportUi.Infrastructure.Logging; - -public static class WebApplicationBuilderExtensions -{ - public static WebApplicationBuilder ConfigureLogging(this WebApplicationBuilder builder) - { - if (builder.Environment.IsProduction()) - { - builder.WebHost.UseSentry(dsn: builder.Configuration.GetRequiredValue("Sentry:Dsn")); - } - - builder.Services.AddApplicationInsightsTelemetry(); - - // We want all logging to go through Serilog so that our filters are always applied - builder.Logging.ClearProviders(); - - builder.Host.UseSerilog((ctx, services, config) => config.ConfigureSerilog(ctx.HostingEnvironment, ctx.Configuration, services)); - - return builder; - } -} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs index f50fc0ca7..0117ab4ea 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/Index.cshtml.cs @@ -8,6 +8,7 @@ namespace TeachingRecordSystem.SupportUi.Pages.Persons; +[RedactParameters("Search")] public partial class IndexModel : PageModel { private const int MaxSearchResultCount = 500; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs index 4ad345ffd..a15e379c2 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Program.cs @@ -17,11 +17,11 @@ using TeachingRecordSystem.Core.Services.Files; using TeachingRecordSystem.Core.Services.PersonMatching; using TeachingRecordSystem.ServiceDefaults; +using TeachingRecordSystem.ServiceDefaults.Infrastructure.Logging; using TeachingRecordSystem.SupportUi; using TeachingRecordSystem.SupportUi.Infrastructure; using TeachingRecordSystem.SupportUi.Infrastructure.Filters; using TeachingRecordSystem.SupportUi.Infrastructure.FormFlow; -using TeachingRecordSystem.SupportUi.Infrastructure.Logging; using TeachingRecordSystem.SupportUi.Infrastructure.ModelBinding; using TeachingRecordSystem.SupportUi.Infrastructure.Redis; using TeachingRecordSystem.SupportUi.Infrastructure.Security; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TeachingRecordSystem.SupportUi.csproj b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TeachingRecordSystem.SupportUi.csproj index a24ec6e2f..9c5bca1b3 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TeachingRecordSystem.SupportUi.csproj +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/TeachingRecordSystem.SupportUi.csproj @@ -13,13 +13,10 @@ - - - diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/appsettings.json b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/appsettings.json index f69f51c7f..3f5d6c419 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/appsettings.json +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/appsettings.json @@ -1,10 +1,7 @@ { "Serilog": { "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft.AspNetCore": "Warning" - } + "Default": "Information" }, "Enrich": [ "FromLogContext" ], "Using": [ "Serilog.Expressions" ]