cadrumo.core.redaction.rules module¶
Redaction-rule registry and the redact() helper family.
The core.classification.policies.RedactionRule shape lives in
core.classification so the SensitivityClass policy
table can reference rule names without a circular import. This module ships:
a small in-memory registry of default
core.classification.policies.RedactionRuleinstances keyed by name (NIF, URL, OAuth bearer token, opaque bearer token);redact(), the flat-string helper that applies a tuple of rules in declared order;redact_structured(), the recursive variant that walks dict / list / tuple containers and redacts every string leaf in place;redact_for_log(), the convenience wrapper for log lines and exception messages;redact_for_cli_output()andredact_structured_for_cli_output(), the public CLI success-output profile for rendered text and JSON-shaped payloads;default_rules_for()anddefault_rules_for_class(), the resolvers that turn rule names stored on acore.classification.policies.ClassificationPolicyinto the underlyingcore.classification.policies.RedactionRuleinstances.
The redaction strategies, defined in
core.classification.policies.RedactionStrategy, are:
SHA256_PREFIXReplace the matched span with
sha256:<first-8-hex>of its SHA-256 digest. Used for the personal identity shapes (NIF / NIE), which are matched on shape alone.SHA256_PREFIX_IF_IDENTITYAs
SHA256_PREFIX, but only when the matched span is a real Spanish tax identity. Used for the CIF shape, whose letter-led form collides with ordinary document references; the control character, checked by the host-bound admission gate, is what tells the two apart.SHA256_PREFIX_IF_IBANAs
SHA256_PREFIX, but only when the matched span passes the ISO 13616 mod-97 check. Used for bank accounts, by an operator decision that deliberately reaches past this module’s stated tax-identity must-handle list. A BOE citation is the standing negative control: it must keep passing through untouched, and a pattern that starts eating one is too wide.HOST_ONLYFor URL-shaped values, retain only
<scheme>://<host>; everything else (path, query, fragment) is dropped.FINGERPRINTBearer- / token-shaped values rewrite to
token:sha256:<first-8-hex>.ELLIPSISReplace the matched span with three ASCII full stops.
- ALWAYS_REDACT_KEY_TERMS: frozenset[str]¶
Key-name fragments every key-name redaction predicate in this tree must treat as sensitive, regardless of the domain the predicate otherwise scopes to.
This module owns the SHAPE-based redaction rules (
redact_for_log(),default_rules_for_class()); it also owns this minimal NAME-based base because a key-name predicate is a second, complementary layer over shape-based redaction wherever one exists (seecadrumo.core.logging,cadrumo.application.live.remote_state_outcomesandcadrumo.application.user_profile), and a base that lives in only one of those consuming sites is a base only until someone edits the other one.passwordsits here rather than in any one consumer because it is a generic credential name with no domain scope. It was previously declared by the profile overview alone, so the two other predicates — which redact a NIF, a bearer token and a PKCS#12 blob — did not treat a key namedpasswordas sensitive at all. That asymmetry is the exact failure the paragraph below describes, and it survived because nothing enumerated the third consumer.Each consuming predicate composes its own set as
ALWAYS_REDACT_KEY_TERMS | {domain-specific-additions}— never redeclares an overlapping term independently. A term wrongly added here costs an extra scrubbed log line; a term silently missing from one composing site can leak a NIF the other site already knew to redact.
- carries_redaction_placeholder(value)[source]¶
Return whether value contains a hashing strategy’s placeholder anywhere.
Walks the same JSON shapes
redact_structured()walks – strings, dict keys and values, list and tuple elements – and answers whether any string holds thesha256:<hex>form theSHA256_PREFIXfamily andFINGERPRINTwrite (the latter istoken:in front of the former).It exists for payloads that must never have been redacted because they stand in for a live value: a replayed placeholder is not the value it replaced, and a consumer that treats it as one reads a hash as a tax identifier. Re-applying the rules cannot answer this question, because a placeholder is not itself shaped like anything a rule matches.
- default_rules_for(policy)[source]¶
Resolve the rule references on a policy to concrete rule instances.
A name the registry cannot resolve is REFUSED, not skipped. Skipping was the previous behaviour and it made this fail open: a typo in a policy’s rule tuple dropped that arm of the policy, and the only evidence was sensitive data arriving unredacted somewhere nobody was looking. A confidentiality boundary has to fail the other way, so an unresolvable name now stops the caller instead of quietly narrowing what gets redacted.
The skip was documented as deliberate — room for per-domain policies to name custom rules registered by other modules. That extension point has no user and no mechanism: every policy is built in the default table in
core.classification, this function’s only caller isdefault_rules_for_class(), and the registry is a frozen mapping with nothing to register through. It was therefore paying a fail-open confidentiality risk for a flexibility nothing used. Should per-domain rules ever be wanted, they need a real registration path, and this refusal is what would make its absence obvious rather than silent.- Parameters:
policy (
ClassificationPolicy) – Acore.classification.policies.ClassificationPolicywhoseredaction_rulesfield carries rule names.- Return type:
- Returns:
A tuple of
core.classification.policies.RedactionRuleinstances in the order they were declared on the policy.- Raises:
RedactionError – If the policy names a rule the registry does not declare.
- default_rules_for_class(sensitivity)[source]¶
Resolve the default rule set for a sensitivity class.
Convenience wrapper that goes through
cadrumo.core.classification._default_policy_forand thendefault_rules_for()so callers do not need to know about the policy table.- Parameters:
sensitivity (
SensitivityClass) – Thecore.classification.policies.SensitivityClasswhose default rules should apply.- Returns:
the policy table and the rule registry are both frozen mappings of frozen records, and every redacted string resolved this afresh.
- Return type:
Ordered tuple of rules for that class. Cached per class
- redact(value, *, rules)[source]¶
Apply
rulesto a flat string in declared order.- Parameters:
value (
str) – The candidate string. Non-string inputs raiseTypeError; consumers must stringify upstream.rules (
tuple[RedactionRule,...]) – Ordered tuple ofcore.classification.policies.RedactionRuleinstances. Each rule’s pattern is compiled withre.MULTILINEand its strategy is applied to every match.
- Return type:
- Returns:
The redacted string.
- Raises:
RedactionError – When
valueis not astr.
- redact_structured(value, *, rules)[source]¶
Recursively apply
rulesto every string value inside a structure.Walks dicts, lists, and tuples; redacts every string reached as a dict value or a list/tuple element. Non-string non-container values pass through unchanged. The container shape is preserved (dict stays dict, list stays list, tuple stays tuple). The resulting object is a fresh copy at every container level — the input is never mutated.
Dict KEYS are redacted with the same rules as values. They were not, until a measurement found a NIF or IBAN written as a key surviving the observability sink in cleartext while the value beside it was hashed. Nothing exercised that: no payload model on this path declares a
dict[str, ...]field today, so the gap was latent rather than live, and one innocently-added field would have opened it with nothing to catch the change.Redacting keys is safe here because the diagnostic rules HASH rather than substitute a fixed placeholder, so two distinct keys stay distinct. Where a rule does collapse several inputs onto one placeholder,
_unique_mapping_key()suffixes the duplicate instead of overwriting it — a redacted log may not silently lose an entry to a key collision.Sets are still not walked, and that one needs no guard: every production caller passes
model_dump(mode="json"), which serialises a set to a LIST before it ever arrives here, and lists are walked. A set only reaches this function from a hand-built mapping, which no caller constructs.This is the load-bearing primitive for nested audit payloads: submission audit events and run-trace records are nested dicts, and a flat
redact()call would not reach the NIF nested under e.g.event["payload"]["taxpayer"]["nif"].- Parameters:
- Return type:
- Returns:
A redacted copy of
valuewith the same nested shape.
- normalise_redaction_key(key)[source]¶
Fold a payload key to the snake-case token the sensitivity sets are written in.
Every sensitivity decision in this package and its consumers is a membership test against a frozenset of lower-snake-case key names, so the key has to be folded to that shape first.
Noneand a key made entirely of separators both fold to"", which matches no name in any of those sets – the sets hold only non-empty tokens – so the empty answer is “not classified” rather than a value a caller must branch on.Folding is
str.casefold(), notstr.lower(): casefold is the more aggressive of the two, so more spellings collapse onto the ASCII token a sensitivity set names. For a redaction classifier that direction is the safe one – it can only make a key match a sensitive name it would otherwise have missed, never the reverse.
- redact_for_log(text)[source]¶
Redact a string against the AUDIT-class rule set for log/error use.
Convenience wrapper for call sites that construct exception messages or log lines containing operator-controlled PII (NIF / NIE / CIF, OAuth tokens, session URLs). Raised exceptions interpolate user-controlled identifiers into
exc.args[0]; the standard logging filter covers theloggingpath but notstr(exc)flowing through Typer’s default error renderer, JSON envelopes, or observability sinks that capture exception text without going through the filter. Redact at the construction site so the secret is never in the exception’s message field to begin with.The AUDIT rule set is the right default for exception text: it redacts NIF (sha256-prefix), URL host-only, and bearer-token fingerprints. The
core.classification.policies.SensitivityClassidentity and diagnostic classes are named for at-rest identity data and observability sinks respectively;AUDITis the canonical class for the log/error path.
- redact_validation_context(context)[source]¶
Remove raw validation values while preserving safe diagnostic structure.
- redact_for_cli_output(text, *, reveal_identifiers=False)[source]¶
Redact a rendered operator-facing CLI output line.
The CLI public-output profile composes the AUDIT rule set used by logs/errors with additional profile, bucket, and secure-object key handling. It deliberately keeps display labels untouched and targets machine identifiers, storage lookup values, URL paths, bearer tokens, and tax identities that should not be emitted as success output.
- Parameters:
- Return type:
- Returns:
Redacted CLI-safe text.
- Raises:
RedactionError – When
textis not astr.
- redact_structured_for_cli_output(value, *, reveal_identifiers=False)[source]¶
Recursively redact a JSON-shaped value for public CLI output.
Unlike
redact_structured(), this helper is key-aware so values under canonical profile, bucket, and secure-object key fields become stable placeholders before JSON serialization. Container shape is preserved and the input object is never mutated.- Parameters:
value (
object) – JSON-shaped payload to prepare for CLI success output.reveal_identifiers (
bool) – WhenTrue, opaque profile and bucket identifier surfaces are emitted raw (the operator opt-out for multi-client disambiguation). Tax identities, tokens, URLs, and secure-object keys stay redacted regardless.
- Return type:
- Returns:
A redacted copy with the same nested shape.