< Summary

Information
Class: AmbientServices.AmbientLogSensitiveFieldFilters
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/AmbientLogSensitiveFieldFilters.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 116
Line coverage: 100%
Branch coverage
80%
Covered branches: 8
Total branches: 10
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
RegisterFieldNameFilter(...)100%11100%
RegisterFieldNameFilter(...)100%11100%
ShouldMaskFieldName(...)83.33%66100%
MaskValueIfSensitive(...)100%22100%
GetRegisteredFilters()100%11100%
.ctor(...)100%11100%
Dispose()50%22100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/AmbientLogSensitiveFieldFilters.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Linq;
 5using System.Text.RegularExpressions;
 6using System.Threading;
 7
 8namespace AmbientServices;
 9
 10/// <summary>
 11/// Registry of per-caller regex filters used to mask sensitive field names in structured log output.
 12/// Multiple unrelated assemblies may each register filters; all active filters are applied when rendering log data.
 13/// </summary>
 14/// <remarks>
 15/// <pitch>Process-wide scrubbing of secrets from structured logs: register a field-name pattern ("password", "token", …
 16/// <pledge>
 17/// Filters match field names, never values, and a match replaces the value with a fixed mask string.  Registrations are
 18/// Registration, disposal, and mask checks are thread-safe and take effect immediately; each check consults the set of 
 19/// </pledge>
 20/// <plan>A <see cref="ConcurrentDictionary{TKey, TValue}"/> of <see cref="Regex"/>es keyed by an <see cref="Interlocked
 21/// <priority>
 22/// 1. Masking a field that did not need it over letting one through: filters match field <em>names</em> rather than val
 23/// 2. Independent registrations over a single coordinated list: each assembly registers its own patterns and disposes o
 24/// 3. Immediate effect over cached filter sets: every check consults the filters registered at that instant, so protect
 25/// </priority>
 26/// </remarks>
 27public static class AmbientLogSensitiveFieldFilters
 28{
 29    /// <summary>
 30    /// The string substituted for values whose field names match a registered sensitive-field pattern.
 31    /// </summary>
 32    public const string MaskedValue = "***";
 33
 34    private static int _nextRegistrationId;
 235    private static readonly ConcurrentDictionary<int, Regex> _filters = new();
 36
 37    /// <summary>
 38    /// Registers a regex that matches sensitive log field names (property or dictionary keys).
 39    /// </summary>
 40    /// <remarks>
 41    /// Unregister by disposing the returned <see cref="IDisposable"/> (for example with <c>using</c> or by calling <see
 42    /// Each caller should keep and dispose its own registration; disposing one registration does not affect filters reg
 43    /// </remarks>
 44    /// <param name="fieldNamePattern">A regex matched against field names (not values).</param>
 45    /// <returns>An <see cref="IDisposable"/> that removes this filter when disposed.</returns>
 46    public static IDisposable RegisterFieldNameFilter(Regex fieldNamePattern)
 47    {
 48#if NET5_0_OR_GREATER
 249        ArgumentNullException.ThrowIfNull(fieldNamePattern);
 50#else
 51        if (fieldNamePattern is null) throw new ArgumentNullException(nameof(fieldNamePattern));
 52#endif
 253        int id = Interlocked.Increment(ref _nextRegistrationId);
 254        _filters[id] = fieldNamePattern;
 255        return new Registration(id);
 56    }
 57
 58    /// <summary>
 59    /// Registers a regex that matches sensitive log field names (property or dictionary keys). Dispose the returned ins
 60    /// </summary>
 61    /// <param name="pattern">The regex pattern string.</param>
 62    /// <param name="options">Regex options. Defaults to <see cref="RegexOptions.IgnoreCase"/> | <see cref="RegexOptions
 63    /// <returns>An <see cref="IDisposable"/> that removes this filter when disposed.</returns>
 64    public static IDisposable RegisterFieldNameFilter(string pattern, RegexOptions options = RegexOptions.IgnoreCase | R
 65    {
 66#if NET5_0_OR_GREATER
 267        ArgumentNullException.ThrowIfNull(pattern);
 68#else
 69        if (pattern is null) throw new ArgumentNullException(nameof(pattern));
 70#endif
 271        return RegisterFieldNameFilter(new Regex(pattern, options));
 72    }
 73
 74    /// <summary>
 75    /// Returns whether <paramref name="fieldName"/> matches any currently registered sensitive-field filter.
 76    /// </summary>
 77    public static bool ShouldMaskFieldName(string fieldName)
 78    {
 279        if (string.IsNullOrEmpty(fieldName)) return false;
 280        foreach (Regex filter in _filters.Values)
 81        {
 282            if (filter.IsMatch(fieldName)) return true;
 83        }
 284        return false;
 285    }
 86
 87    /// <summary>
 88    /// Masks <paramref name="value"/> when <paramref name="fieldName"/> matches a registered filter; otherwise returns 
 89    /// </summary>
 90    public static object? MaskValueIfSensitive(string fieldName, object? value)
 91    {
 292        return ShouldMaskFieldName(fieldName) ? MaskedValue : value;
 93    }
 94
 95    /// <summary>
 96    /// Returns a snapshot of the currently registered filters (for diagnostics and tests).
 97    /// </summary>
 98    public static IReadOnlyCollection<Regex> GetRegisteredFilters()
 99    {
 2100        return _filters.Values.ToArray();
 101    }
 102
 103    private sealed class Registration : IDisposable
 104    {
 105        private readonly int _id;
 106        private int _disposed;
 107
 2108        public Registration(int id) => _id = id;
 109
 110        public void Dispose()
 111        {
 2112            if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
 2113            _ = _filters.TryRemove(_id, out _);
 2114        }
 115    }
 116}