feat(infra): JsonSanitizer + AuditOptions binding (UDT-010 B3)
Adds the metadata sanitization layer per #REQ-AUD-5:
SIGCM2.Infrastructure/Audit/JsonSanitizer.cs (static class):
- Sanitize(object?, IReadOnlyCollection<string>) -> string?
- Serializes via System.Text.Json + JsonNode recursive traversal.
- Strips blacklisted keys at every nesting level (objects + arrays).
- Case-insensitive match (ToLowerInvariant on both sides).
- Null input -> null output (never throws).
- Output is always valid JSON (ISJSON=1 compatible — satisfies AuditEvent CHECK).
SIGCM2.Application/Audit/AuditOptions.cs:
- Documented the IConfiguration array-binding quirk: config is ADDITIVE
(append at higher indices), not REPLACE. Intentional for security — defaults
like 'password'/'token'/'cvv' must not be silently dropped.
SIGCM2.Infrastructure/DependencyInjection.cs:
- services.Configure<AuditOptions>(configuration.GetSection(AuditOptions.SectionName))
wired in AddInfrastructure().
Tests (Strict TDD, RED -> GREEN):
- JsonSanitizerTests (10): null/empty-blacklist/flat/nested/arrays/case-insensitive/
primitives/round-trip-valid-json/string-as-value/default-keys-effective.
- AuditOptionsBindingTests (2): defaults when section absent + additive override.
One test needed adjustment during GREEN: 'AlreadySerializedJsonString' originally
asserted against an encoding-specific literal; rewrote to use JsonDocument
round-trip (validates behavior without coupling to encoder quirks).
Suite: 348/348 Application.Tests + 130/130 Api.Tests = 478/478 passing.
Refs: sdd/udt-010-auditoria-trazabilidad/{spec#REQ-AUD-5, design#D-5, tasks#B3}
This commit is contained in:
63
src/api/SIGCM2.Infrastructure/Audit/JsonSanitizer.cs
Normal file
63
src/api/SIGCM2.Infrastructure/Audit/JsonSanitizer.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace SIGCM2.Infrastructure.Audit;
|
||||
|
||||
/// UDT-010 (#REQ-AUD-5): serializes a metadata object to JSON and strips keys in the blacklist
|
||||
/// at every nesting level. Case-insensitive match. Null input → null output (never throws).
|
||||
/// Produces valid JSON consumable by AuditEvent.Metadata (ISJSON=1 constraint).
|
||||
public static class JsonSanitizer
|
||||
{
|
||||
/// Serializes <paramref name="obj"/> to JSON and removes any property whose key matches
|
||||
/// (case-insensitively) an entry in <paramref name="blacklist"/>. Recursive into nested
|
||||
/// objects and arrays. Returns null if the input is null.
|
||||
public static string? Sanitize(object? obj, IReadOnlyCollection<string> blacklist)
|
||||
{
|
||||
if (obj is null) return null;
|
||||
|
||||
var node = JsonSerializer.SerializeToNode(obj);
|
||||
if (node is null) return null;
|
||||
|
||||
if (blacklist.Count > 0)
|
||||
{
|
||||
var blacklistLower = blacklist
|
||||
.Select(k => k.ToLowerInvariant())
|
||||
.ToHashSet();
|
||||
Strip(node, blacklistLower);
|
||||
}
|
||||
|
||||
return node.ToJsonString();
|
||||
}
|
||||
|
||||
private static void Strip(JsonNode node, HashSet<string> blacklistLower)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case JsonObject obj:
|
||||
var keysToRemove = obj
|
||||
.Where(kvp => blacklistLower.Contains(kvp.Key.ToLowerInvariant()))
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
obj.Remove(key);
|
||||
|
||||
foreach (var kvp in obj.ToList())
|
||||
{
|
||||
if (kvp.Value is not null)
|
||||
Strip(kvp.Value, blacklistLower);
|
||||
}
|
||||
break;
|
||||
|
||||
case JsonArray arr:
|
||||
foreach (var item in arr)
|
||||
{
|
||||
if (item is not null)
|
||||
Strip(item, blacklistLower);
|
||||
}
|
||||
break;
|
||||
|
||||
// JsonValue: scalar, nothing to strip
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using SIGCM2.Application.Abstractions;
|
||||
using SIGCM2.Application.Abstractions.Persistence;
|
||||
using SIGCM2.Application.Abstractions.Security;
|
||||
using SIGCM2.Application.Audit;
|
||||
using SIGCM2.Application.Auth;
|
||||
using SIGCM2.Infrastructure.Http;
|
||||
using SIGCM2.Infrastructure.Messaging;
|
||||
@@ -68,6 +69,9 @@ public static class DependencyInjection
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddScoped<IClientContext, ClientContext>();
|
||||
|
||||
// UDT-010: Audit options (SanitizedKeys blacklist) — overridable via appsettings "Audit".
|
||||
services.Configure<AuditOptions>(configuration.GetSection(AuditOptions.SectionName));
|
||||
|
||||
// Dispatcher
|
||||
services.AddScoped<IDispatcher, Dispatcher>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user