Masking strategies¶
A strategy is how a sensitive value gets transformed. All built-in
strategies are deterministic: the RNG is seeded from
sha256(masking.seed + value), so the same input always maps to the same
output — across columns, tables, databases and runs. (The
seed map then makes that durable even when inputs to the
computation change.)
Catalogue¶
| Strategy | Output | Notes |
|---|---|---|
fake_name |
Mary Johnson → Susan Scott |
consistent first+last from bundled dictionaries |
fake_first_name / fake_last_name |
single name part | |
fake_city |
Austin → Tucson |
US city list bundled; register your own for other locales |
fake_email |
jane@corp.com → karen.lopez316@example.invalid |
RFC-reserved .invalid TLD — can never actually deliver |
fake_email_keep_domain |
jane@corp.com → karen.lopez316@corp.com |
keeps the (possibly identifying) domain — explicit opt-in |
fake_uuid |
valid, deterministic v4 UUID | case and {} braces preserved; uuid.UUID in → uuid.UUID out |
fake_ip |
203.0.113.7 → 141.66.203.9 |
valid octets (1–254); IPv6 keeps grouping/case |
fake_credit_card |
digits random, separators kept | Luhn-valid so checksum-validating consumers keep working |
fake_date |
±30–730-day deterministic shift | always a real calendar date, same representation in/out |
format_random |
Ab3-9z → Qf7-2k |
same length + character classes; typed values stay typed (below) |
shuffle |
characters permuted in place | separators keep positions; typed values dispatch like format_random |
redact |
Ada-99 → ***-** |
keeps length/separators |
null / blank |
SQL NULL / '' |
for fields that shouldn't survive at all |
Typed values stay typed — and valid¶
Values arrive from the driver as Python objects, not strings. format_random
and shuffle route them to type-preserving logic:
| Input type | Behavior |
|---|---|
int |
digits randomized, digit count kept, no leading zero, sign kept |
float / Decimal |
digits randomized in place — still parses (1e-05 keeps its e) |
date / datetime |
delegated to fake_date (a real date, same type) |
uuid.UUID |
delegated to fake_uuid (a uuid.UUID) |
bool |
passed through — one bit has no shape to hide |
This is what keeps a masked DATE column from receiving "8342-73-51" and a
masked INTEGER column from receiving a string.
Which strategy applies to a column?¶
First match wins:
flowchart TD
A["Sensitive column<br/>(detected rule, e.g. email)"] --> B{"1. column_strategies<br/>(your per-column call)"}
B -- yes --> Z[use it]
B -- no --> C{"2. rule_strategies<br/>(your per-rule mapping)"}
C -- yes --> Z
C -- no --> D{"3. built-in default<br/>for the rule"}
D -- yes --> Z
D -- no --> E["4. masking.default_strategy"]
Built-in rule defaults: email→fake_email, full_name→fake_name,
first_name/last_name/city → their fakes, uuid→fake_uuid,
ip_address→fake_ip, credit_card→fake_credit_card,
date/date_of_birth→fake_date, phone/ssn/zip_code→format_random,
address→redact.
Free text is your call¶
Long text fields (notes, comments, support transcripts) can contain
anything. Detection flags the column at best — the safe treatments are
blunt ones, chosen explicitly:
masking:
column_strategies:
notes: blank # wipe it
# notes: redact # keep length, hide content
# notes: format_random # scramble it
Custom strategies and dictionaries¶
from dbmask.masking.rules import register_strategy
from dbmask.masking.dictionaries import register_dictionary
from dbmask.masking.format import seeded_rng
register_dictionary("countries", ["France", "Japan", "Brazil"])
def strat_fixed_suffix(value, ctx):
if value is None:
return None # masking never invents data
rng = seeded_rng(str(value), ctx.seed)
return f"user-{rng.randint(1000, 9999)}"
register_strategy("fixed_suffix", strat_fixed_suffix)
Rules for a well-behaved strategy: deterministic (derive randomness from
seeded_rng(value, ctx.seed)), None stays None, and output should be
valid for the column's type. dbmask strategies lists everything registered.