< Summary

Information
Class: Intentum.Core.Behavior.SanitizationOptions
Assembly: Intentum.Core
File(s): /home/runner/work/Intentum/Intentum/src/Intentum.Core/Behavior/BehaviorSpaceSanitization.cs
Line coverage
100%
Covered lines: 8
Uncovered lines: 0
Coverable lines: 8
Total lines: 128
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/Intentum/Intentum/src/Intentum.Core/Behavior/BehaviorSpaceSanitization.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using System.Text.RegularExpressions;
 4
 5namespace Intentum.Core.Behavior;
 6
 7/// <summary>
 8/// Options for sanitizing a behavior space (actor/action/metadata masking) for GDPR/CCPA or privacy.
 9/// </summary>
 10/// <param name="MaskActor">When true, replace actor with a hash or placeholder.</param>
 11/// <param name="MaskAction">When true, replace action with a hash or placeholder.</param>
 12/// <param name="MetadataKeysToRedact">Metadata keys to redact (value replaced with "[redacted]"). When null, no metadat
 13/// <param name="UseHash">When true, use SHA256 hash (truncated) for masked values; when false, use Placeholder.</param>
 14/// <param name="Placeholder">Placeholder string when UseHash is false (e.g. "***").</param>
 15/// <param name="MaskEmails">When true, detect and mask email addresses in metadata values.</param>
 16/// <param name="MaskPhoneNumbers">When true, detect and mask phone numbers in metadata values.</param>
 717public sealed record SanitizationOptions(
 818    bool MaskActor = true,
 819    bool MaskAction = true,
 1520    IReadOnlyList<string>? MetadataKeysToRedact = null,
 821    bool UseHash = true,
 222    string Placeholder = "***",
 623    bool MaskEmails = false,
 1224    bool MaskPhoneNumbers = false);
 25
 26/// <summary>
 27/// Extension methods for sanitizing (anonymizing/masking) a behavior space before vector production or inference.
 28/// Use for GDPR/CCPA compliance: mask PII in actor/action/metadata before sending to external models or storage.
 29/// </summary>
 30public static class BehaviorSpaceSanitization
 31{
 32    private static readonly Regex EmailRegex = new(@"[\w.+-]+@[\w-]+\.[\w.]+", RegexOptions.Compiled, TimeSpan.FromSecon
 33    private static readonly Regex PhoneRegex = new(@"[\+]?[\d\s\-\(\)]{10,}", RegexOptions.Compiled, TimeSpan.FromSecond
 34    /// <summary>
 35    /// Returns a new behavior space with actor/action/metadata sanitized according to options.
 36    /// Original space is not modified.
 37    /// </summary>
 38    public static BehaviorSpace Sanitize(this BehaviorSpace space, SanitizationOptions? options = null)
 39    {
 40        ArgumentNullException.ThrowIfNull(space);
 41        options ??= new SanitizationOptions();
 42        var sanitized = new BehaviorSpace();
 43
 44        foreach (var evt in space.Events)
 45        {
 46            var actor = options.MaskActor ? Mask(evt.Actor, options) : evt.Actor;
 47            var action = options.MaskAction ? Mask(evt.Action, options) : evt.Action;
 48            var meta = RedactMetadata(evt.Metadata, options.MetadataKeysToRedact, options);
 49            sanitized.Observe(new BehaviorEvent(actor, action, evt.OccurredAt, meta));
 50        }
 51
 52        CopyMetadata(sanitized, space.Metadata, options.MetadataKeysToRedact);
 53        return sanitized;
 54    }
 55
 56    private static IReadOnlyDictionary<string, object>? RedactMetadata(
 57        IReadOnlyDictionary<string, object>? meta,
 58        IReadOnlyList<string>? keysToRedact,
 59        SanitizationOptions options)
 60    {
 61        if (meta == null)
 62            return meta;
 63        var dict = new Dictionary<string, object>(meta);
 64        if (keysToRedact != null)
 65        {
 66            foreach (var key in keysToRedact.Where(dict.ContainsKey))
 67                dict[key] = "[redacted]";
 68        }
 69        if (options.MaskEmails || options.MaskPhoneNumbers)
 70        {
 71            foreach (var key in dict.Keys.ToList())
 72            {
 73                if (dict[key] is string value)
 74                    dict[key] = MaskPii(value, options);
 75            }
 76        }
 77        return dict;
 78    }
 79
 80    private static string MaskPii(string value, SanitizationOptions options)
 81    {
 82        if (options.MaskEmails)
 83            value = EmailRegex.Replace(value, m => MaskEmail(m.Value));
 84        if (options.MaskPhoneNumbers)
 85            value = PhoneRegex.Replace(value, m => MaskPhone(m.Value));
 86        return value;
 87    }
 88
 89    private static string MaskEmail(string email)
 90    {
 91        var parts = email.Split('@');
 92        if (parts.Length != 2) return "***";
 93        var maskedLocal = parts[0].Length > 0 ? parts[0][0].ToString() : "***";
 94        return $"{maskedLocal}@***.***";
 95    }
 96
 97    private static string MaskPhone(string phone)
 98    {
 99        if (phone.Length < 4) return "***";
 100        return phone[..2] + "***" + phone[^2..];
 101    }
 102
 103    private static void CopyMetadata(
 104        BehaviorSpace sanitized,
 105        IReadOnlyDictionary<string, object> metadata,
 106        IReadOnlyList<string>? keysToRedact)
 107    {
 108        foreach (var kv in metadata)
 109        {
 110            var value = keysToRedact != null && keysToRedact.Contains(kv.Key, StringComparer.OrdinalIgnoreCase)
 111                ? "[redacted]"
 112                : kv.Value;
 113            sanitized.SetMetadata(kv.Key, value);
 114        }
 115    }
 116
 117    private static string Mask(string value, SanitizationOptions options)
 118    {
 119        if (string.IsNullOrEmpty(value))
 120            return value;
 121        if (options.UseHash)
 122        {
 123            var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value));
 124            return Convert.ToHexString(bytes)[..Math.Min(16, bytes.Length * 2)];
 125        }
 126        return options.Placeholder;
 127    }
 128}