Build-an-X workflow that owns the full detect → mask → verify pipeline for PII in test data. Walks the author through (1) classifying each field against the cross-regime PII catalog (GDPR / CCPA-CPRA / NIST SP 800-122 / HIPAA, in references/pii-categories.md), (2) picking a masking operator from the techniques catalog (seven canonical operators + Presidio operators + privacy models, in references/masking-techniques.md), (3) deciding pseudonymisation (reversible, in GDPR scope) vs anonymisation (irreversible, out of scope), (4) ordering the pipeline (detect → operator → audit) and emitting a deployable YAML config for Presidio + Faker + Synthea wrappers (Faker-as-masking-operator detail in references/faker-masking-operators.md), and (5) running the adversarial verification pass that re-detects PII in the masked output and blocks promotion on a leak. Use when non-production environments need masked production data - from field classification through runnable masking config to the leak audit.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Companion reference for pii-masking-pipeline-builder (Step 3 operator
selection). Also the rule book the verification pass draws from.
Masking is the act of transforming a real value into a substitute that breaks the link to the original subject while preserving testable properties (format, distribution, referential integrity). Which technique is correct depends on three things: whether the result must be reversible, whether the field is referentially shared across tables, and what privacy model the dataset must satisfy.
This is the pure reference that the pipeline builder and leak-detection audits draw from to choose operators per field.
Drawing from the Wikipedia data-masking taxonomy (en.wikipedia.org/wiki/Data_masking) and ISO/IEC 20889:2018 (cite by stable ID; standard text behind paywall):
Replace the real value with an authentic-looking value from a lookup table - "John Smith" → "Maria Garcia."
hash(real_id) → fake_id keeps joins intact across tables).replace operator
(presidio.dataprivacystack.org/anonymizer),
Faker library generators
(faker-masking-operators.md).Randomly rearrange values within a column - salaries column gets shuffled, each row keeps a real salary but no longer the right person's salary.
Apply a bounded random offset: salary ± 10 %, dates ± 120 days (Wikipedia data-masking page).
Apply a cryptographic algorithm with a key. Two sub-variants:
General encryption (AES-256-GCM, etc.) - output is opaque ciphertext; reversible only with the key. Use for fields that must round-trip back to plaintext for authorised consumers.
Format-preserving encryption (FPE) (FF1 / FF3 per NIST SP 800-38G) - output has the same format as input (16-digit card → 16-digit ciphertext). Use when legacy systems validate format.
Reversibility: Reversible (key required).
Use for: PII that must round-trip for authorised business logic; legacy-format requirements.
Apply a one-way hash (SHA-256 / SHA-512) with optional salt.
hash operator with hash_type =
"sha256" or "sha512" and salt parameter.Replace the value with NULL or remove the column entirely.
Show partial value - credit card "**** **** **** 1234," email "j***@example.com."
mask operator with chars_to_mask,
masking_char, from_end parameters.Replace the real value with a token (random opaque string) and store the real-value → token map in a separate, access-controlled vault.
Remove the value entirely (no placeholder, no length signal).
redact operator (no parameters).Replace with a synthetically generated value preserving
distribution / format
(faker-masking-operators.md;
synthea-healthcare-data
for health records).
Per presidio.dataprivacystack.org/anonymizer, the Presidio Anonymizer engine supports six built-in operators:
| Operator | Parameters | Reversible | Maps to canonical technique |
|---|---|---|---|
replace | new_value (defaults to <entity_type>) | No (random) / Yes (deterministic substitution) | #1 Substitution |
redact | - | No | Redaction |
mask | chars_to_mask, masking_char, from_end | No | #7 Masking-out |
hash | hash_type (sha256 / sha512), salt | No (one-way) | #5 Hashing |
encrypt | key | Yes (with key) | #4 Encryption |
custom | lambda | Depends on lambda | (caller-defined) |
Invocation: engine.anonymize(text=, analyzer_results=, operators={"PERSON": OperatorConfig("replace", {"new_value": "BIP"})}).
OperatorConfig constructor signature: OperatorConfig(operator_name, params={}) (Presidio docs).
GDPR Art. 4(5) defines pseudonymisation as "processing of personal data in such a manner that the personal data can no longer be attributed to a specific data subject without the use of additional information, provided that such additional information is kept separately" (gdpr-info.eu/art-4-gdpr/).
| Technique | Pseudonymisation? | Anonymisation? |
|---|---|---|
| Deterministic substitution (same input → same output) | ✓ | - |
| Random substitution | - | ✓ |
| Shuffling | - | ✓ (when distribution-only) |
| Number / date variance | - | ✓ if variance ≥ identifying granularity |
| General encryption (key kept) | ✓ | - |
| FPE (key kept) | ✓ | - |
| Salted hashing (salt kept separately) | ✓ | - |
| Unsalted hashing of low-entropy field | ✗ (re-identifiable by enumeration) | ✗ |
| Nulling | - | ✓ |
| Masking-out (partial) | depends on revealed chars | depends |
| Tokenisation (vault kept) | ✓ | - |
| Tokenisation + vault destroyed | - | ✓ |
| Redaction | - | ✓ |
| Synthetic substitution | - | ✓ |
Implication: A "masking pipeline" output that uses reversible techniques is still personal data under GDPR - it remains in scope. Only fully irreversible output is out of GDPR scope per Recital 26.
NIST SP 800-188:2023 formalises statistical privacy models that sit above the per-field operators - pick one for the whole dataset's disclosure risk once quasi-identifiers remain after masking:
Full definitions, achievement methods, weaknesses, and ε / k guidance (with NIST + primary-source citations): privacy-models.md.
| Field characteristic | Recommended technique | Privacy model layer |
|---|---|---|
| Must round-trip for authorised consumer (payment processing) | Tokenisation (vault) or FPE | none (reversible) |
| Must join across tables, opaque value OK | Deterministic substitution / salted hashing | k-anonymity on quasi-identifiers |
| Free-text PII inside a log line | Redaction or replace-with-<TYPE> (Presidio analyzer + anonymizer) | - |
| Continuous numeric for analytics | Number variance | t-closeness if sensitive attribute |
| Categorical demographic (race, etc.) for analytics | Generalisation + l-diversity | l-diversity |
| Statistical query release | Differential privacy mechanism | DP |
| Demo / training, no analytics utility needed | Synthetic substitution (Faker / Synthea) | n/a (no real data) |
customers tableAn analytics team needs a non-prod copy of a customers table. Walk each
field through the steps in "How to use this reference":
| Field | Need | Operator | Scope outcome |
|---|---|---|---|
customer_id (FK, joined across tables) | Opaque but joinable | Deterministic substitution / salted hashing (#1 / #5) | Pseudonymised - reversible via key |
full_name | No analytics value | Random substitution (Faker) | Anonymised |
email | Support must recognise own value | Masking-out j***@example.com (#7) | Partial - depends on revealed chars |
national_id (SSN) | No analytics value; enumerable format | Nulling out (#6) - never unsalted hashing | Anonymised |
date_of_birth | Age band useful | Generalise to a band (age 47 → "40 - 50") | Anonymised (k-anonymity input) |
salary | Distribution useful | Number variance ± 10 % (#3) | Anonymised - t-closeness if sensitive |
auth_token | No analytics value | Nulling out / deletion (#6) | Anonymised |
Resulting scope: because customer_id uses a reversible deterministic
map, the output is pseudonymised - still personal data under GDPR
Recital 26. To move the dataset out of scope, destroy the substitution key
so customer_id can no longer be re-linked. The remaining quasi-identifiers
(date_of_birth band, salary bracket) then need a dataset privacy model -
see privacy-models.md.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Unsalted hashing of SSN | SSN format is enumerable (~10⁹); attacker rebuilds the mapping table in minutes. | Salt + key per tenant; or tokenise via vault. |
| FPE for an analytics dataset | Format preservation lets a join attack with another dataset recover identity. | Use random substitution for analytics datasets that don't need format round-trip. |
| "GDPR-compliant" pseudonymisation claim | GDPR pseudonymised data is still personal data - Article 4(5) is explicit. | Either mark output pseudonymised (in scope) or fully anonymise (out of scope). |
| k = 2 anonymity | Re-identification probability is 50 % for the equivalence class. | k ≥ 5 typical; k = 10+ for high-risk datasets. |
| Shuffling a rare-value column | Outliers identify themselves regardless of position. | Combine shuffling with generalisation or suppression of outliers. |
| Number variance ± 1 % on salaries | The variance is smaller than the precision needed to identify; effectively no masking. | Variance must exceed the identifying granularity - ± 10 % minimum for salary. |
| Tokenisation without vault access controls | The vault becomes the single point of failure. | Strict access control + audit logging + separate key custody. |
| Differential privacy with ε = 100 | Useless budget; no privacy guarantee. | ε ≤ 1 typical for strong privacy; ε ≤ 10 for relaxed cases. |
pii-masking-pipeline-builder).presidio-pii-detection.