< Summary

Information
Class: Intentum.Core.Behavior.BehaviorSpaceSanitization
Assembly: Intentum.Core
File(s): /home/runner/work/Intentum/Intentum/src/Intentum.Core/Behavior/BehaviorSpaceSanitization.cs
Line coverage
98%
Covered lines: 69
Uncovered lines: 1
Coverable lines: 70
Total lines: 128
Line coverage: 98.5%
Branch coverage
90%
Covered branches: 38
Total branches: 42
Branch coverage: 90.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%11100%
Sanitize(...)100%88100%
RedactMetadata(...)100%1414100%
MaskPii(...)100%44100%
MaskEmail(...)50%44100%
MaskPhone(...)50%22100%
CopyMetadata(...)100%66100%
Mask(...)75%4488.88%

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>
 17public sealed record SanitizationOptions(
 18    bool MaskActor = true,
 19    bool MaskAction = true,
 20    IReadOnlyList<string>? MetadataKeysToRedact = null,
 21    bool UseHash = true,
 22    string Placeholder = "***",
 23    bool MaskEmails = false,
 24    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{
 132    private static readonly Regex EmailRegex = new(@"[\w.+-]+@[\w-]+\.[\w.]+", RegexOptions.Compiled, TimeSpan.FromSecon
 133    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)
 739    {
 740        ArgumentNullException.ThrowIfNull(space);
 741        options ??= new SanitizationOptions();
 742        var sanitized = new BehaviorSpace();
 43
 3744        foreach (var evt in space.Events)
 845        {
 846            var actor = options.MaskActor ? Mask(evt.Actor, options) : evt.Actor;
 847            var action = options.MaskAction ? Mask(evt.Action, options) : evt.Action;
 848            var meta = RedactMetadata(evt.Metadata, options.MetadataKeysToRedact, options);
 849            sanitized.Observe(new BehaviorEvent(actor, action, evt.OccurredAt, meta));
 850        }
 51
 752        CopyMetadata(sanitized, space.Metadata, options.MetadataKeysToRedact);
 753        return sanitized;
 754    }
 55
 56    private static IReadOnlyDictionary<string, object>? RedactMetadata(
 57        IReadOnlyDictionary<string, object>? meta,
 58        IReadOnlyList<string>? keysToRedact,
 59        SanitizationOptions options)
 860    {
 861        if (meta == null)
 462            return meta;
 463        var dict = new Dictionary<string, object>(meta);
 464        if (keysToRedact != null)
 165        {
 566            foreach (var key in keysToRedact.Where(dict.ContainsKey))
 167                dict[key] = "[redacted]";
 168        }
 469        if (options.MaskEmails || options.MaskPhoneNumbers)
 270        {
 1071            foreach (var key in dict.Keys.ToList())
 272            {
 273                if (dict[key] is string value)
 274                    dict[key] = MaskPii(value, options);
 275            }
 276        }
 477        return dict;
 878    }
 79
 80    private static string MaskPii(string value, SanitizationOptions options)
 281    {
 282        if (options.MaskEmails)
 283            value = EmailRegex.Replace(value, m => MaskEmail(m.Value));
 284        if (options.MaskPhoneNumbers)
 285            value = PhoneRegex.Replace(value, m => MaskPhone(m.Value));
 286        return value;
 287    }
 88
 89    private static string MaskEmail(string email)
 190    {
 191        var parts = email.Split('@');
 192        if (parts.Length != 2) return "***";
 193        var maskedLocal = parts[0].Length > 0 ? parts[0][0].ToString() : "***";
 194        return $"{maskedLocal}@***.***";
 195    }
 96
 97    private static string MaskPhone(string phone)
 198    {
 199        if (phone.Length < 4) return "***";
 1100        return phone[..2] + "***" + phone[^2..];
 1101    }
 102
 103    private static void CopyMetadata(
 104        BehaviorSpace sanitized,
 105        IReadOnlyDictionary<string, object> metadata,
 106        IReadOnlyList<string>? keysToRedact)
 7107    {
 25108        foreach (var kv in metadata)
 2109        {
 2110            var value = keysToRedact != null && keysToRedact.Contains(kv.Key, StringComparer.OrdinalIgnoreCase)
 2111                ? "[redacted]"
 2112                : kv.Value;
 2113            sanitized.SetMetadata(kv.Key, value);
 2114        }
 7115    }
 116
 117    private static string Mask(string value, SanitizationOptions options)
 8118    {
 8119        if (string.IsNullOrEmpty(value))
 0120            return value;
 8121        if (options.UseHash)
 6122        {
 6123            var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(value));
 6124            return Convert.ToHexString(bytes)[..Math.Min(16, bytes.Length * 2)];
 125        }
 2126        return options.Placeholder;
 8127    }
 128}