Testland
Browse all skills & agents

pii-masking-pipeline-builder

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill pii-masking-pipeline-builder
View source

pii-masking-pipeline-builder

Overview

Authoring a masking pipeline requires three classifications per field (regulatory regime, operator, reversibility) and one global decision (pipeline ordering + audit hooks). This workflow produces a deployable YAML spec that downstream tools execute:

  • presidio-pii-detection runs the detector.
  • Faker substitution operators (references/faker-masking-operators.md) / synthea-healthcare-data supply substitute values.
  • The verification pass (below) audits the output for leaks.

When to use

  • Promoting a production-data snapshot to a staging environment.
  • Building a recurring refresh pipeline that masks nightly.
  • Establishing a per-table contract that PR-reviewers can audit.

Step 1 - Inventory the source

Enumerate every column / field in the source dataset. For each, record:

ColumnTypeSample valueCardinalityCross-table join?
users.emailstringalice@acme.comhighyes (joins events)
users.ssnstring123-45-6789highno
users.dobdate1985-03-14mediumno
users.zipstring02139lowno
users.countrystringUSvery lowno

A schema introspector can produce the first columns; cardinality and join graph need a quick analytical pass.

Step 2 - Classify each field

Look up each column in the cross-regime catalog (references/pii-categories.md) and record which regulatory regime(s) apply. Include linkable fields explicitly (NIST 800-122 §2.2).

ColumnGDPRCPRA SPINISTHIPAARisk
users.email-✓ #6direct
users.ssn✓ #7direct, high-sensitivity
users.doblinkable-linkable✓ #3linkable
users.ziplinkable-linkable✓ #2 (sub-state)linkable
users.country----non-PII

Any field marked direct OR linkable enters the masking scope. A field marked only "linkable" still gets masked because it identifies in combination with others (Sweeney 87% rule, see references/pii-categories.md).

Step 3 - Pick an operator per field

Match each field to a technique in the techniques catalog (references/masking-techniques.md). Decision tree:

  1. Must round-trip for authorised consumer? (e.g., payments) → Tokenisation (vault) or FPE.
  2. Must join across tables? → Deterministic substitution or salted hash with consistent salt per source value.
  3. Continuous numeric needing analytics? → Number variance.
  4. Categorical demographic for analytics? → Generalisation + l-diversity.
  5. Free text potentially containing PII? → Presidio detect → replace / redact.
  6. No analytical or operational use? → Nulling / redaction.
ColumnOperatorRationaleReversible?
users.emailFaker substitution (deterministic via hash-seed)Joins across tables; need referential integrityYes (via salt vault)
users.ssnTokenisation (vault)Strict regulator scope; round-trip needed for authYes (via vault)
users.dobGeneralisation to yearAnalytics needs age bracket, not exact DOBNo
users.zipTruncation to first 3 digitsHIPAA Safe Harbor #2 rule (>20k pop only)No
users.countryPass-throughNot PIIn/a

Step 4 - Pseudonymisation vs anonymisation gate

For each masked field, mark whether the result remains personal data under GDPR Art. 4(5):

  • Reversible techniques (deterministic substitution, tokenisation, encryption, salted hashing with retained salt) = pseudonymised → output is still personal data → masking pipeline output is still in GDPR scope.
  • Irreversible techniques (random substitution, generalisation, nulling, redaction) = anonymised → potentially out of GDPR scope (subject to Recital 26 reasonable-likelihood test).

Document the gate decision per dataset:

output_classification: pseudonymised  # GDPR scope retained
gdpr_lawful_basis: Article 6(1)(f) legitimate interests
retention: 90 days
access_control: only-dev-environment-team

vs.

output_classification: anonymised
gdpr_lawful_basis: out-of-scope per Recital 26
retention: indefinite
access_control: open

The author cannot claim "anonymised" if any reversible technique is in the pipeline.

Step 5 - Compose the pipeline

A standard order:

  1. Schema-aware mask - apply per-column operators from Step 3 (deterministic, fast, no NER needed).
  2. Free-text detect + mask - for any string column wider than ~50 characters, run presidio-pii-detection to catch embedded PII (e.g., a user-typed comment that contains an email).
  3. Audit hook - sample N rows of output and run the verification pass (below) before declaring the run complete.
  4. Manifest - emit a per-run manifest recording: pipeline version, source snapshot ID, row count in / out, operator versions, salt vault key version.

Step 6 - Emit the YAML spec

Recommended shape - consumable by a generic pipeline runner:

pipeline:
  name: users-staging-refresh
  source:
    type: postgres
    connection: $PROD_RO_DSN
    schema: public
    table: users
  classification:
    output: pseudonymised
    regimes: [gdpr, cpra, hipaa]
  fields:
    - column: email
      operator: deterministic_substitution
      provider: faker
      provider_method: internet.email
      seed_strategy: hash(salt + value)
      salt_ref: vault://masking/users.email
    - column: ssn
      operator: tokenisation
      vault: vault://masking/users.ssn
    - column: dob
      operator: generalisation
      params:
        granularity: year
    - column: zip
      operator: truncation
      params:
        keep_chars: 3
        from: start
    - column: country
      operator: passthrough
  free_text_columns:
    - notes
    - support_message
  free_text_detector:
    type: presidio
    language: en
    score_threshold: 0.45
    entities: [PERSON, EMAIL_ADDRESS, PHONE_NUMBER, US_SSN, CREDIT_CARD, IP_ADDRESS]
    on_detect: replace
  audit:
    sample_rows: 100
    fail_on_critic_block: true
  output:
    type: postgres
    connection: $STAGING_RW_DSN
    schema: public
    table: users
  manifest:
    write_to: s3://masking-manifests/${run_id}.json

Step 7 - Worked example

A SaaS app refreshes its staging from prod nightly. Source has 4M users with 22 columns, 3 of which are free-text. Synthesised spec:

pipeline:
  name: prod-to-staging-nightly
  source: { type: postgres, table: users }
  classification: { output: pseudonymised, regimes: [gdpr, cpra] }
  fields:
    - { column: user_id, operator: passthrough }                  # internal opaque ID
    - { column: email, operator: deterministic_substitution,
        provider: faker, provider_method: internet.email,
        seed_strategy: hash(salt + value), salt_ref: vault://prod/email }
    - { column: full_name, operator: substitution,
        provider: faker, provider_method: name }
    - { column: phone, operator: substitution,
        provider: faker, provider_method: phone_number }
    - { column: address_line1, operator: substitution,
        provider: faker, provider_method: address }
    - { column: country, operator: passthrough }
    - { column: language, operator: passthrough }
    - { column: created_at, operator: passthrough }
    - { column: last_login_at, operator: passthrough }
    - { column: signup_ip, operator: encryption,
        params: { algo: fpe-ff1 }, key_ref: vault://prod/ip-fpe }
    - { column: notes, operator: free_text_mask }
  free_text_detector:
    type: presidio
    language: en
    score_threshold: 0.5
    on_detect: replace
  audit: { sample_rows: 100, fail_on_critic_block: true }

Pipeline classification: pseudonymised (email is deterministic, IP is FPE-encrypted with key retained). The user explicitly accepts that this output remains in GDPR scope.

Verification pass - auditing the masked output

Every pipeline run ends with an adversarial leak audit that re-detects PII in the masked output and challenges the pipeline's "clean" claim. Audit checklist:

  1. Sample the output. N rows (default 1000) uniformly; 10 000+ for high-risk datasets: shuf -n 1000 masked-users.csv > sample.csv.
  2. Detect. Re-run presidio-pii-detection against the sample with the strictest entity set and a lower score_threshold than the pipeline used during masking (e.g. 0.4 vs 0.5+) - the audit should catch hits the pipeline filtered out as low-confidence. Scan every column, including declared-non-PII passthrough columns.
  3. Cross-reference. For each hit: was the column in the spec, and what operator ran? A hit in an unclassified or passthrough column is a leak unless the column is genuinely non-PII per references/pii-categories.md. Was the operator appropriate per references/masking-techniques.md? Did it silently fail (literal "NULL" string still detected)?
  4. Classify by regime. Map each leak to its regulator(s) via the cross-jurisdiction table; a leak counts against every regime listing it.
  5. Verdict.
BLOCK if any hit is:
  - A CPRA SPI / GDPR Art. 9 / HIPAA Safe Harbor identifier
  - A direct identifier in a column where the pipeline declared
    "anonymised" output
  - A hit in a column the pipeline didn't classify

PASS-WITH-CAVEATS if:
  - Only linkable (not direct) leaks remain
  - The pipeline output is declared "pseudonymised" (GDPR scope
    retained, so linkable hits are tolerable when access-controlled)

PASS if:
  - Zero hits, OR
  - Only false-positive hits that the analyst flags as
    Presidio-noise (e.g., a fake-shaped string that's actually a
    UUID)

The audit refuses to mark a run "pass" if any CPRA SPI / GDPR Art. 9 / HIPAA Safe Harbor identifier appears unmasked, if the spec lacks a manifest (no provenance = no audit trail), or on a "we'll fix it next time" promise - leaks block the promotion. Findings are suppressed only via an explicit per-row waiver. Re-audit on every pipeline-spec change; detection is heuristic (Presidio's recogniser ceiling), so custom PatternRecognizers cover in-house ID formats and full-dataset scans replace sampling for comprehensive audits.

Anti-patterns

Anti-patternWhy it failsFix
Per-column operator without referential checkJoins break after maskingGroup columns that share keys; apply deterministic operators consistently
Free-text columns skippedEmbedded PII (user-typed emails) leaksAlways run Presidio on any string column > ~50 chars
Claiming "anonymised" when any reversible op is in the pipelineFalse GDPR compliance claimAudit the pipeline; pseudonymised if any operator is reversible
No audit stepOperator failure or recogniser drift goes unnoticedAlways sample output and run the verification pass
Salt vault key shared across pipelinesSalt-rotation breaks every downstream pipeline at oncePer-pipeline salt; rotate independently
No manifestCannot reproduce a past run; auditors can't trace lineageAlways emit manifest with version IDs
Pipeline runs on prod-write connectionRisk of writing masked data back over prodStrict source = read-only DSN; output = staging-write DSN

Limitations

  • No automated regime mapping. The author must classify each field against the regimes (Step 2) - the tool doesn't infer it.
  • Pipeline runners vary. This skill emits a generic YAML; the team needs a runner (custom Python / dbt / Spark job / commercial tool) to execute it.
  • Free-text detection is heuristic. False positives + negatives are real (see presidio-pii-detection limitations).
  • Doesn't cover application-layer PII generation. A pipeline masks data at rest; the application might still write fresh PII to logs at runtime - pair with log-masking middleware.

References

Faker as a masking substitution operator

View source (opens in new window)

Faker as a masking substitution operator

Companion reference for pii-masking-pipeline-builder - using the Faker family to put believable stand-in values back into a dataset a masking step has just de-identified. Library mechanics for fixture-style fake data built from nothing (install, provider catalogue, locales, factories) live in the qa-test-data plugin's faker-data skill - this reference deliberately does not repeat them.

The axis this reference owns

A de-identification step has already run: direct identifiers are nulled, redacted, or replaced with a placeholder. The dataset is private but broken, because joins fail and validators reject empty fields. This reference covers putting usable values back and the privacy decisions that forces: keeping a shared identifier consistent so joins survive, deterministic versus random substitution, preserving a validated field shape, and proving the result is safe rather than merely realistic.

It deliberately does not cover the generic mechanics of a fake-data library: installation, the provider catalogue, per-locale tours, or factory patterns for building fixtures from nothing. Nor does it cover detecting PII or choosing between masking operators such as hashing, tokenisation, and nulling. Both happen before this step.

Minimum library surface

The worked example below uses only Faker.seed(n) (run reproducibility, a class method) and fake.unique.<method>() (injective values, raising UniquenessException once the value space is exhausted). The full mechanic table - instance vs class seeding, JS refDate, multiple-locale weighting, and Presidio's custom operator for driving substitution from an anonymiser - is in the "Library surface and seed-exposure detail" section at the end of this file.

Step 1 - Build the substitution map before substituting anything

The masking literature requires masked values to stay consistent across databases that share a data element, and to be repeatable (the same input always yields the same output) yet not reversible to the original (en.wikipedia.org/wiki/Data_masking (opens in new window)).

So do not substitute row by row while streaming. Collect the distinct real values first, build one map, apply it everywhere. Two properties must hold:

  • Injective. Two real people must never collapse onto one substitute, or a join over-matches and row counts silently inflate. fake.unique enforces this until the provider's value space runs out (faker.readthedocs.io (opens in new window)).
  • Complete. Cover every place the identifier appears: denormalised copies, free-text mentions in comment fields, filenames, exports already in the bundle. A column missed here is a column where the real value survives.

The map is the sensitive artifact. Retaining it where the recipient can reach it makes the output pseudonymised, not anonymised: GDPR Article 4(5) treats data attributable to a subject only via separately-kept additional information as pseudonymised (gdpr-info.eu (opens in new window)), and Recital 26 counts data re-attributable through such additional information as personal data (gdpr-info.eu (opens in new window)). Keeping the map is a legitimate choice. Calling the result anonymous afterwards is not.

Step 2 - What a seed guarantees, and what it does not

Does. Reproduce the same generator sequence on a later run, for the same library version. Faker warns that "as we keep updating datasets, results are not guaranteed to be consistent across patch versions" (faker.readthedocs.io (opens in new window)); the JavaScript port warns "you may get different values for the same seed" after an upgrade (fakerjs.dev (opens in new window)).

Does not. Act as a key. Python Faker draws from a random.Random (faker.readthedocs.io (opens in new window)), and the standard library warns that the module's generators are not for security use and points to the secrets module for cryptographic uses (docs.python.org (opens in new window)). A seed offers no resistance to anyone holding it.

The four seeding strategies (per-value derived seed, global seed plus a stored map, global seed with the map discarded, unseeded random) are compared by re-identification exposure in the seed-exposure matrix at the end of this file. The per-value derived seed is the pattern to avoid. It needs no stored map, and it fails for the reason unsalted hashing of a low-entropy field fails: the input space is enumerable, so the mapping is rebuildable. If a stored map is unacceptable, the answer is a keyed transform, not a derived seed.

Step 3 - Preserve the shape the downstream system validates

Check-digit values. Payment card numbers, and national identifiers including Canadian social insurance numbers, Israeli and South African ID numbers, and Swedish personal identity numbers, carry a Luhn ("mod 10") check digit specified in ISO/IEC 7812-1 (en.wikipedia.org/wiki/Luhn_algorithm (opens in new window)). Faker exposes credit_card_number(card_type=...) accepting 'amex', 'visa', 'mastercard', 'discover', 'diners', 'jcb', and 'maestro' among others, but does not state whether the output carries a valid check digit (faker.readthedocs.io (opens in new window)), so assert it rather than assume it:

def luhn_ok(number: str) -> bool:
    digits = [int(d) for d in number if d.isdigit()][::-1]
    return sum(d if i % 2 == 0 else sum(divmod(d * 2, 10))
               for i, d in enumerate(digits)) % 10 == 0

assert luhn_ok(fake.credit_card_number(card_type="visa"))

Luhn "was designed to protect against accidental errors, not malicious attacks" (en.wikipedia.org/wiki/Luhn_algorithm (opens in new window)), so passing it proves the field parses, not that the value is safe.

Locale-bound formats. Phone numbers and postal codes are validated against the row's country, so pin one locale per row from that country column instead of relying on multiple-locale mode's weighted random draw.

Format plus reversibility. If the receiving system needs the original back later and the shape must survive, substitution is the wrong operator: NIST SP 800-38G, "Recommendation for Block Cipher Modes of Operation: Methods for Format-Preserving Encryption", specifies FF1 and FF3 for that case (csrc.nist.gov (opens in new window)).

Step 4 - Realistic is not safe, so measure the result

Residual quasi-identifiers. Substitution removes only the direct identifiers actually substituted. Birth date, postal code, sex, and admission date usually stay because they carry the analytical value, and in combination they still identify people. NIST SP 800-188, "De-Identifying Government Datasets: Techniques and Governance" (September 2023), treats transforming quasi-identifiers and running re-identification studies as part of de-identification, not optional extras (csrc.nist.gov (opens in new window)). The exit criterion is therefore a measurement over the remaining quasi-identifier columns (smallest equivalence-class size, count of unique rows), against Recital 26's test of whether means are "reasonably likely to be used", accounting for "the costs of and the amount of time required for identification" (gdpr-info.eu (opens in new window)).

Accidental collisions. Generators draw from fixed data, described in the JavaScript port's guide as "lists of names, words etc" (fakerjs.dev (opens in new window)), so a generated value can coincide with a real person's, including one in the source file. Diff substitutes against source values and fail on any intersection.

Worked example - two joined files

customers.csv holds email; orders.csv holds billing_email and joins on it. A masking step already dropped ssn and full_name.

import csv
from faker import Faker

fake = Faker("en_US")
Faker.seed(0)  # run reproducibility only: not a secret, not derived from the data

def load(path):
    with open(path, newline="") as f:
        return list(csv.DictReader(f))

customers, orders = load("customers.csv"), load("orders.csv")
real = {c["email"] for c in customers} | {o["billing_email"] for o in orders}
email_map = {v: fake.unique.email() for v in sorted(real)}

assert len(set(email_map.values())) == len(email_map)   # injective
assert not (set(email_map.values()) & real)             # no collision with source

before = len({c["email"] for c in customers} & {o["billing_email"] for o in orders})
for c in customers:
    c["email"] = email_map[c["email"]]
for o in orders:
    o["billing_email"] = email_map[o["billing_email"]]
after = len({c["email"] for c in customers} & {o["billing_email"] for o in orders})

assert before == after, f"join key cardinality changed: {before} -> {after}"
print(f"substituted {len(email_map)} identifiers, join keys preserved: {after}")

Discard email_map, or store it under separate access control. Never ship it beside the substituted files.

Expected output shape

FileColumnBeforeAfter
customers.csvemailalice.tan@acme.examplexrogers@example.org
orders.csvbilling_emailalice.tan@acme.examplexrogers@example.org
customers.csvpostcodeSW1A 1AASW1A 1AA (quasi-identifier, unchanged)
substituted 12480 identifiers, join keys preserved: 9317
collision check vs source values: 0
residual quasi-identifiers not substituted: postcode, birth_date, sex

The third line is a handoff to a re-identification measurement: an open item, not a pass.

Anti-patterns

Anti-patternWhy it failsFix
Seeding the generator from the real value before each callThe mapping is rebuildable by anyone with the code and a candidate value listOne global seed plus an access-controlled map, or a keyed transform
Substituting row by row while streaming, with no mapThe same person gets a different substitute per occurrence and every join breaksCollect distinct values, build the map once, then apply
Calling the extract anonymous while retaining the mapThe map is the "additional information" of GDPR Article 4(5); the output is still personal dataDestroy the map, or label the output pseudonymised
Substituting a checksum-bearing field without asserting the check digitDownstream validators reject rows, so the environment looks broken rather than maskedAssert Luhn or the field's own check rule before writing
Multiple-locale mode on a dataset with a country columnLocale is a weighted random draw per call, so a French row gets a Japanese postcodeInstantiate one locale per row from that row's country
Declaring done once the direct identifiers look fakeBirth date plus postcode plus sex still identify peopleMeasure equivalence-class sizes over the remaining quasi-identifiers before release
Never diffing substitutes against source valuesFixed name lists mean a substitute can be a real person in the same fileFail the run on any intersection

Limitations

  • A seed is version-bound. Results are "not guaranteed to be consistent across patch versions" (faker.readthedocs.io (opens in new window)), so a rerun after an upgrade produces a different map and breaks joins against extracts already delivered. Pin the version beside the seed.
  • Substitution alone never reaches anonymity. Quasi-identifier transformation and the re-identification study are separate work (csrc.nist.gov (opens in new window)).
  • Injectivity has a ceiling. fake.unique raises UniquenessException once the value space is exhausted (faker.readthedocs.io (opens in new window)), so high-cardinality columns need a composed value (prefix plus counter).
  • No semantic coherence between substituted fields. A substituted address and phone number are drawn independently, so area code and region will not agree. Derive a dependent field from the substituted one instead.

Library surface and seed-exposure detail

MechanicBehaviourSource
Faker.seed(n)Class method; seeds the shared random.Random across all internal generators. Calling .seed() on an instance raises TypeErrorfaker.readthedocs.io (opens in new window)
fake.seed_instance(n)Creates and seeds a unique random.Random for one instancefaker.readthedocs.io (opens in new window)
fake.unique.<method>()Tracks values already returned; raises UniquenessException after repeated failures to find a new onefaker.readthedocs.io (opens in new window)
Multiple-locale modeThe proxy "randomly select[s] a generator using a distribution defined by the provided weights", so locale varies per callfaker.readthedocs.io (opens in new window)
faker.seed(123) (JS)Fixes the sequence; setting the seed again resets it. Date helpers also need refDate or faker.setDefaultRefDate()fakerjs.dev (opens in new window)

To drive substitution from an anonymiser, Microsoft Presidio exposes a custom operator taking a "lambda to execute on the PII data. The lambda return type must be a string", passed as OperatorConfig("replace", {"new_value": "BIP"})-style entries in the operators dict (presidio.dataprivacystack.org (opens in new window)).

Seed exposure matrix

The four seeding strategies referenced from Step 2 above, ranked by the re-identification exposure each one leaves:

MechanismJoins surviveRe-identification exposure
Seed derived per value, reseeding from the real value before each callYesHigh. Anyone with the code, the library version, and a candidate list of real values re-runs the derivation and rebuilds the map. The seed reproduces the substitute; it does not conceal the input
One global seed plus a stored mapYesEqual to the exposure of the map. Control access to the map, not the seed
One global seed, map discarded after the runYes, within the runLow from the seed alone: the substitute follows call order, not the input value, so the seed reconstructs nothing without the source data
Unseeded, random per occurrenceNoLow, but the dataset is unusable for anything relational

Data-masking techniques and privacy models

View source (opens in new window)

Data-masking techniques and privacy models

Companion reference for pii-masking-pipeline-builder (Step 3 operator selection). Also the rule book the verification pass draws from.

Overview

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.

When to use

  • Picking the right masking operator for a field (pii-categories.md (opens in new window) classified as PII).
  • Deciding whether output is pseudonymised (still in GDPR scope) or anonymised (out of GDPR scope).
  • Sizing a privacy model (k-anonymity / differential privacy) against utility loss.

How to use this reference

  1. Classify the field as direct identifier, quasi-identifier, or sensitive attribute (pii-categories.md (opens in new window)).
  2. Decide the reversibility need - must an authorised consumer recover the real value? If yes, pick a reversible operator (encryption, FPE, tokenisation, deterministic substitution); if no, pick an irreversible one from the seven-techniques catalog below.
  3. Preserve what the test needs - format, distribution, or referential integrity across tables narrows the operator (deterministic substitution / salted hashing keep joins; shuffling keeps a column's distribution).
  4. Confirm the scope outcome in the pseudonymisation-vs-anonymisation table - reversible output is still personal data under GDPR.
  5. Layer a dataset privacy model when quasi-identifiers survive per-field masking - k-anonymity through differential privacy, in privacy-models.md (opens in new window).
  6. Cross-check the anti-patterns before shipping the pipeline.

The seven canonical masking techniques

Drawing from the Wikipedia data-masking taxonomy (en.wikipedia.org/wiki/Data_masking (opens in new window)) and ISO/IEC 20889:2018 (cite by stable ID; standard text behind paywall):

1. Substitution

Replace the real value with an authentic-looking value from a lookup table - "John Smith" → "Maria Garcia."

2. Shuffling

Randomly rearrange values within a column - salaries column gets shuffled, each row keeps a real salary but no longer the right person's salary.

  • Reversibility: Irreversible.
  • Distribution: Preserved exactly (it's the same set of values, reordered).
  • Use for: Columns where the distribution matters for analytics but the per-row truth is sensitive (salary, performance score).
  • Risk: If rare values exist (1 person earns $5M), shuffling doesn't anonymise them - the value identifies its row position cluster.

3. Number / date variance

Apply a bounded random offset: salary ± 10 %, dates ± 120 days (Wikipedia data-masking page).

  • Reversibility: Irreversible without the per-row offset key.
  • Use for: Continuous numeric / temporal fields where approximate values are useful (analytics) but exact values are sensitive.
  • Risk: Bounded variance may leak the original value (date ± 120 days narrows to a year; salary ± 10 % narrows to a bracket).

4. Encryption

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.

5. Hashing

Apply a one-way hash (SHA-256 / SHA-512) with optional salt.

  • Reversibility: Irreversible (assuming the salt + hash are cryptographically sound and the input space isn't enumerable).
  • Determinism: Same input → same hash. Used as a deterministic pseudonym preserving referential integrity.
  • Risk: Low-entropy fields (SSN with known format) are enumerable under unsalted hashing - attacker pre-computes all 1 billion possible SSNs. Always salt + per-tenant key.
  • Tooling: Presidio hash operator with hash_type = "sha256" or "sha512" and salt parameter.

6. Nulling out / deletion

Replace the value with NULL or remove the column entirely.

  • Reversibility: Irreversible.
  • Use for: Fields with no analytical value to non-prod consumers (auth tokens, security questions, plaintext passwords).
  • Risk: Schema constraints (NOT NULL) may block the operation; pipeline must coordinate with schema.

7. Masking-out / character scrambling

Show partial value - credit card "**** **** **** 1234," email "j***@example.com."

  • Reversibility: Irreversible (unmasked characters can leak some info - last-4 of card identifies brand + issuer family).
  • Use for: Customer-facing displays where the user must recognise their own value; analytics that need partial info.
  • Tooling: Presidio mask operator with chars_to_mask, masking_char, from_end parameters.

Additional techniques

Tokenisation

Replace the real value with a token (random opaque string) and store the real-value → token map in a separate, access-controlled vault.

  • Reversibility: Reversible via the vault (authorised lookup).
  • Use for: Payment processing (PCI-DSS-driven), any field where the token must round-trip for authorised consumers without exposing the value to the consuming system.

Redaction

Remove the value entirely (no placeholder, no length signal).

  • Reversibility: Irreversible.
  • Use for: Free-text logs, screenshots, document exports where even the presence of a field is sensitive.
  • Tooling: Presidio redact operator (no parameters).

Synthetic substitution

Replace with a synthetically generated value preserving distribution / format (faker-masking-operators.md (opens in new window); synthea-healthcare-data for health records).

  • Reversibility: Irreversible.
  • Use for: Demo / training environments where realistic-looking but never-real data is required.

Microsoft Presidio anonymizer operators

Per presidio.dataprivacystack.org/anonymizer (opens in new window), the Presidio Anonymizer engine supports six built-in operators:

OperatorParametersReversibleMaps to canonical technique
replacenew_value (defaults to <entity_type>)No (random) / Yes (deterministic substitution)#1 Substitution
redact-NoRedaction
maskchars_to_mask, masking_char, from_endNo#7 Masking-out
hashhash_type (sha256 / sha512), saltNo (one-way)#5 Hashing
encryptkeyYes (with key)#4 Encryption
customlambdaDepends 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).

Reversible vs irreversible - pseudonymisation vs anonymisation

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/ (opens in new window)).

TechniquePseudonymisation?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 charsdepends
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.

Privacy models - NIST SP 800-188

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:

  • k-anonymity - every record indistinguishable from k-1 others on the quasi-identifiers (generalise / suppress / aggregate).
  • l-diversity - k-anonymity plus l well-represented sensitive values per equivalence class.
  • t-closeness - l-diversity plus a sensitive-attribute distribution within t of the overall distribution.
  • Differential privacy - a formal guarantee bounded by the privacy budget ε, achieved by noise injection on query outputs.

Full definitions, achievement methods, weaknesses, and ε / k guidance (with NIST + primary-source citations): privacy-models.md (opens in new window).

Picking a technique per field

Field characteristicRecommended techniquePrivacy model layer
Must round-trip for authorised consumer (payment processing)Tokenisation (vault) or FPEnone (reversible)
Must join across tables, opaque value OKDeterministic substitution / salted hashingk-anonymity on quasi-identifiers
Free-text PII inside a log lineRedaction or replace-with-<TYPE> (Presidio analyzer + anonymizer)-
Continuous numeric for analyticsNumber variancet-closeness if sensitive attribute
Categorical demographic (race, etc.) for analyticsGeneralisation + l-diversityl-diversity
Statistical query releaseDifferential privacy mechanismDP
Demo / training, no analytics utility neededSynthetic substitution (Faker / Synthea)n/a (no real data)

Worked example - masking a non-prod customers table

An analytics team needs a non-prod copy of a customers table. Walk each field through the steps in "How to use this reference":

FieldNeedOperatorScope outcome
customer_id (FK, joined across tables)Opaque but joinableDeterministic substitution / salted hashing (#1 / #5)Pseudonymised - reversible via key
full_nameNo analytics valueRandom substitution (Faker)Anonymised
emailSupport must recognise own valueMasking-out j***@example.com (#7)Partial - depends on revealed chars
national_id (SSN)No analytics value; enumerable formatNulling out (#6) - never unsalted hashingAnonymised
date_of_birthAge band usefulGeneralise to a band (age 47 → "40 - 50")Anonymised (k-anonymity input)
salaryDistribution usefulNumber variance ± 10 % (#3)Anonymised - t-closeness if sensitive
auth_tokenNo analytics valueNulling 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 (opens in new window).

Anti-patterns

Anti-patternWhy it failsFix
Unsalted hashing of SSNSSN format is enumerable (~10⁹); attacker rebuilds the mapping table in minutes.Salt + key per tenant; or tokenise via vault.
FPE for an analytics datasetFormat 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 claimGDPR 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 anonymityRe-identification probability is 50 % for the equivalence class.k ≥ 5 typical; k = 10+ for high-risk datasets.
Shuffling a rare-value columnOutliers identify themselves regardless of position.Combine shuffling with generalisation or suppression of outliers.
Number variance ± 1 % on salariesThe 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 controlsThe vault becomes the single point of failure.Strict access control + audit logging + separate key custody.
Differential privacy with ε = 100Useless budget; no privacy guarantee.ε ≤ 1 typical for strong privacy; ε ≤ 10 for relaxed cases.

Limitations

  • No single technique fits every field. Pipeline must apply per-field policy (pii-masking-pipeline-builder).
  • Re-identification research evolves. NIST 800-188 Annex documents known attacks; the techniques above are sound under 2024 attack models, not future ones.
  • Utility loss is real. Aggressive anonymisation (high k, low ε) makes the dataset less useful for analytics. Pipeline owner must trade off explicitly.
  • Tooling support varies. Presidio implements the Anonymizer operators above out of the box; k-anonymity / l-diversity / DP typically require additional libraries (ARX, OpenDP, IBM Differential Privacy Library) not part of Presidio.

References

PII categories - cross-regime catalog

View source (opens in new window)

PII categories - cross-regime catalog

Companion reference for pii-masking-pipeline-builder (Step 2 field classification). Also the scoping source for presidio-pii-detection recogniser sets.

Overview

This is the canonical category catalog that the masking pipeline and detectors reference for scope. It enumerates four regimes:

  • GDPR (EU General Data Protection Regulation, Regulation 2016/679) - definitive for EU personal data.
  • CCPA/CPRA (California Consumer Privacy Act + California Privacy Rights Act) - broadest US state law; many other states (VA, CO, CT, UT) follow its shape.
  • NIST SP 800-122 (US federal guide) - the federal agency-applicable definition; influential as a US-default model.
  • HIPAA (Health Insurance Portability and Accountability Act, 45 CFR § 164.514) - the Safe Harbor 18 identifiers for de-identification of protected health information (PHI).

This is a pure reference - no execution steps. Workflow skills consume it.

When to use

  • Authoring a masking rule and confirming which fields fall under which regulator's protection.
  • Reviewing a dataset to classify its PII risk level before allowing it into a non-production environment.
  • Scoping the recogniser set for a PII detector (presidio-pii-detection).
  • Onboarding a tester to the vocabulary used by leak-detection reviews.

Per-regime identifier catalogs

The full enumerations - GDPR Art. 4(1) identifiers and Art. 9 special categories with the Art. 4(5) pseudonymisation distinction, the CCPA/CPRA statutory categories and CPRA sensitive personal information, NIST SP 800-122 linked-vs-linkable and the six confidentiality-impact factors, and the HIPAA Safe Harbor 18 identifiers - are in regime-catalogs.md (opens in new window). The cross-jurisdiction map below is the fast scoping tool; consult the catalogs for the per-regime detail behind each column.

Cross-jurisdiction map

The fastest way to scope a masking pipeline is to enumerate fields present in the dataset and look up which regimes flag each:

FieldGDPR Art. 4(1)GDPR Art. 9CCPA/CPRACPRA SPINIST 800-122HIPAA Safe Harbor
Full name-✓ (A)-✓ (#1)
Email-✓ (A)-✓ (#6)
Phone-✓ (A)-✓ (#4)
SSN-✓ (A, B)✓ (#7)
Passport / driver's licence-✓ (A)✓ (#11)
IP address✓ (Recital 30)-✓ (A)-linkable✓ (#15)
Cookie / device ID-✓ (A)-linkable✓ (#13)
Birth datelinkable-✓ (A)-linkable✓ (#3 - months/days)
Precise geolocation-✓ (G)✓ (#2 - sub-state)
Race / ethnicity✓ (C)--
Religion✓ (C)--
Sexual orientation✓ (C)--
Health condition✓ (Art. 4(15))✓ (B)- (covered by PHI rules)
Genetic data✓ (Art. 4(13))✓ (B)--
Biometric (face, fingerprint)✓ (Art. 4(14))✓ (E)✓ (if uniquely identifying)✓ (#16, #17)
Account login + password-✓ (A)✓ (#10)
Credit-card / IBAN-✓ (A, D)✓ (#10)
Medical record number- (covered in B)✓ (B)✓ (health subset)✓ (#8)
Browsing history-✓ (F)-✓ (#14)
Purchase records-✓ (D)--
Inferred profile / score-✓ (K)-linkable-

"linkable" = field alone may not identify, but combined with other fields it does (NIST §2.2).

Common confusions

ConfusionReality
"PII = SSN, name, email."These are subsets. GDPR personal data includes online identifiers, location, biometrics, inferences. Use the full Art. 4(1) list.
"CCPA only covers consumers."CCPA "consumer" includes employees and job applicants under CPRA (Cal. Civ. Code § 1798.140(i)).
"HIPAA only covers hospitals."HIPAA covers covered entities (providers, plans, clearinghouses) and business associates. Business associates inherit HIPAA obligations via BAAs.
"IP address isn't personal data."GDPR Recital 30 lists IP addresses as online identifiers. CJEU Breyer (C-582/14) confirmed dynamic IPs are personal data when linkable.

Anti-patterns

Anti-patternWhy it failsFix
Single-list scopingOnly catches one regime's identifiers; leaks the others.Use the cross-jurisdiction map above as the union scope.
Treating PHI as "just sensitive PII"HIPAA Safe Harbor has 18 specific identifiers - birth date months, vehicle IDs, certificate numbers - that GDPR lists don't enumerate.Apply HIPAA Safe Harbor when the dataset is PHI.
Mapping CCPA to GDPR Art. 9 onlyCPRA SPI includes financial + government identifiers Art. 9 doesn't.Apply CPRA SPI as a separate scope layer.
Stopping at "direct identifiers"NIST §2.2 says linkable info is PII. Date-of-birth + ZIP + sex re-identifies most individuals.Include linkable fields in scope.
Pseudonymisation = anonymisationGDPR Art. 4(5) keeps pseudonymised data personal.Document which masking outputs are pseudonymised (in scope) vs anonymised (out of scope).
Ignoring inferred profilesCCPA category K covers inferences. A "risk score" derived from PII is itself PII.Treat inferred / derived fields the same as their sources.

Limitations

  • Statutes evolve. This catalog reflects GDPR (2016, in force 2018), CCPA (2018) as amended by CPRA (2020, in force 2023), NIST SP 800-122 (2010), HIPAA Privacy Rule (45 CFR Part 164, current). Re-fetch citations annually.
  • Jurisdiction is not exhaustive. This catalog covers four high-frequency regimes. Other regimes (LGPD Brazil, PIPEDA Canada, APPI Japan, PDPA Singapore, PIPL China) have similar but non-identical lists.
  • Sectoral additions exist. GLBA (US financial), FERPA (US education), COPPA (US children), state-specific laws (VA CDPA, CO CPA, etc.) add fields. When a dataset crosses sectors, consult the sector-specific list.
  • PII detection is heuristic. A detector (presidio-pii-detection) finds patterns that look like PII; it cannot guarantee category-completeness. Reviewer must spot-check.

References

Privacy models - NIST SP 800-188

View source (opens in new window)

Privacy models - NIST SP 800-188

Deep reference behind masking-techniques.md (opens in new window). Consult after per-field operators are chosen, when quasi-identifiers remain and the whole dataset's disclosure risk must be bounded.

NIST SP 800-188:2023 ("De-Identifying Government Datasets", csrc.nist.gov/pubs/sp/800/188/final (opens in new window)) formalises statistical privacy models layered above the per-field techniques.

k-anonymity

A dataset is k-anonymous if every record is indistinguishable from at least k - 1 other records when projected on the quasi-identifiers (Sweeney 2002, cited in NIST 800-188).

  • Achieve via: Generalisation (age 47 → "40 - 50"), suppression (drop the row), and aggregation.
  • Picks k: Typical values are k = 5, k = 10, k = 100 depending on dataset size + risk tolerance.
  • Weakness: Vulnerable to homogeneity attack - if all k records share the same sensitive value, k-anonymity doesn't protect it.

l-diversity

Strengthens k-anonymity by requiring at least l well-represented values of the sensitive attribute within each equivalence class (Machanavajjhala et al. 2007).

  • Achieve via: Suppression of records that would break l, or perturbation of sensitive values.
  • Weakness: Vulnerable to skewness / similarity attack - the l values may be semantically similar.

t-closeness

Strengthens l-diversity by requiring the distribution of the sensitive attribute in each equivalence class be close (within t, by Earth Mover's Distance) to the distribution in the overall dataset (Li et al. 2007).

  • Trade-off: Higher t = better utility, lower t = stronger privacy.

Differential privacy

A formal mathematical guarantee: the probability of any output changes by at most a multiplicative factor (e^ε) when a single record is added/removed. ε (epsilon) is the privacy budget - lower ε = stronger privacy.

  • Achieve via: Noise injection (Laplace / Gaussian mechanism) on query outputs, not on the raw dataset.
  • Trade-off: Utility-vs-budget. Apple, Google, US Census 2020 use differential privacy.
  • Cite: NIST SP 800-188:2023 §6; original Dwork 2006 "Calibrating noise to sensitivity."

Picking the model

  • k-anonymity - baseline for quasi-identifier sets; pick k >= 5 (k = 10+ for high-risk datasets).
  • l-diversity - add when a homogeneity attack on the sensitive attribute is plausible.
  • t-closeness - add when the sensitive attribute's distribution itself leaks (skewness / similarity).
  • Differential privacy - use for statistical query release, not raw-dataset export; keep ε <= 1 for strong privacy, ε <= 10 relaxed.

References

  • NIST SP 800-188:2023 "De-Identifying Government Datasets" - csrc.nist.gov/pubs/sp/800/188/final (opens in new window). Definitions of k-anonymity, l-diversity, t-closeness, differential privacy.
  • Sweeney 2002 (k-anonymity), Machanavajjhala et al. 2007 (l-diversity), Li et al. 2007 (t-closeness), Dwork 2006 "Calibrating noise to sensitivity" (differential privacy) - all cited in NIST 800-188.

Per-regime identifier catalogs

View source (opens in new window)

Per-regime identifier catalogs

Full per-regime enumerations behind pii-categories.md (opens in new window). The cross-jurisdiction map there is the fast scoping tool; this file holds the detail behind each column.

GDPR - personal data (Article 4(1))

Definition (Article 4(1)): "any information relating to an identified or identifiable natural person ('data subject')" (gdpr-info.eu/art-4-gdpr/ (opens in new window)).

The article enumerates identifiers that make a person identifiable:

Identifier classExamples
NameGiven name, surname, full name, online aliases linked to the person
Identification numberNational ID, passport, driver's licence, tax ID, employee ID
Location dataGPS coordinates, IP-derived city/region, cell-tower triangulation
Online identifierIP address, cookie ID, device fingerprint, advertising ID (per Recital 30)
Physical/physiological factorHeight, weight, eye colour, fingerprint, gait
Genetic factorDNA-derived information (further defined in Art. 4(13))
Mental factorDiagnosed mental-health conditions, IQ test results
Economic factorSalary, credit score, transaction history, account balances
Cultural factorLanguage, religion, ethnic background
Social factorMarital status, family relationships, social-network connections

Source: Article 4(1) GDPR (gdpr-info.eu/art-4-gdpr/ (opens in new window)).

GDPR Article 9 - special categories of personal data

Article 9(1) lists categories whose processing is prohibited by default unless one of the Article 9(2) exceptions applies:

  • Racial or ethnic origin
  • Political opinions
  • Religious or philosophical beliefs
  • Trade-union membership
  • Genetic data (defined in Art. 4(13))
  • Biometric data processed for unique identification (defined in Art. 4(14))
  • Data concerning health (defined in Art. 4(15))
  • Data concerning a natural person's sex life or sexual orientation

A masking pipeline for an EU dataset must apply at least the broader Art. 4(1) rules and stricter rules to any field falling under Art. 9 (special categories carry higher fines and must be either redacted or fully anonymised, not merely pseudonymised).

GDPR Article 4(5) - pseudonymisation vs anonymisation

"Pseudonymisation" (Art. 4(5)) keeps data attributable to a subject with additional information, kept separately. Pseudonymised data is still personal data under GDPR - it remains in scope.

Anonymised data (no longer linkable to a subject under any reasonably likely method, per Recital 26) falls out of GDPR scope. The masking pipeline must mark which output is which (masking-techniques.md (opens in new window) explains the techniques).

CCPA / CPRA - personal information

Definition (Cal. Civ. Code § 1798.140(v)(1), as amended by CPRA): "information that identifies, relates to, describes, is reasonably capable of being associated with, or could reasonably be linked, directly or indirectly, with a particular consumer or household" (oag.ca.gov/privacy/ccpa (opens in new window)).

Statutory categories enumerated in § 1798.140(v)(1)(A) - (K):

#CategoryExamples
AIdentifiersName, postal address, email, IP address, account name, SSN, driver's licence, passport
BCustomer recordsRecords covered by Cal. Civ. Code § 1798.80(e) - name, signature, education, employment, financial info, medical, health-insurance, with paper/electronic regardless of storage medium
CProtected classificationsRace, religion, gender, sexual orientation, age, national origin, disability, marital status (under California or federal law)
DCommercial informationPurchases, products considered, consuming history
EBiometric informationFingerprints, retina, hand prints, voice recordings, keystroke patterns
FInternet/network activityBrowsing history, search history, interaction with a website or app
GGeolocation dataPhysical location, movements, especially "precise geolocation" (CPRA refinement)
HSensory dataAudio, electronic, visual, thermal, olfactory recordings
IProfessional/employmentJob titles, salaries, employment records
JEducationEducation records as defined in 20 USC § 1232g (FERPA)
KInferencesProfile drawn from any of A - J to predict preferences, characteristics, predispositions, behaviour

CPRA - sensitive personal information (SPI)

CPRA added a subcategory of personal information requiring extra protection (Cal. Civ. Code § 1798.140(ae)):

  • Government identifiers - SSN, driver's licence, state ID, passport number
  • Account log-in + password / financial account / debit-card / credit-card number with security code
  • Precise geolocation (≤1,850 ft / 1,850 ft radius)
  • Racial / ethnic origin, religious / philosophical beliefs, union membership
  • Contents of mail, email, text messages (where the business isn't the intended recipient)
  • Genetic data
  • Biometric information processed to uniquely identify a consumer
  • Health information (collected by businesses, distinct from HIPAA PHI)
  • Sex life or sexual orientation

Citation: oag.ca.gov/privacy/ccpa "Sensitive Personal Information" (oag.ca.gov/privacy/ccpa (opens in new window)).

NIST SP 800-122 - PII

Definition (citing OMB Memorandum 07-16, reproduced in NIST SP 800-122 Section 2.1): "information which can be used to distinguish or trace an individual's identity, such as their name, social security number, biometric records, etc., alone, or when combined with other personal or identifying information which is linked or linkable to a specific individual, such as date and place of birth, mother's maiden name, etc."

Citation: NIST SP 800-122:2010 §2.1, fetched from csrc.nist.gov/pubs/sp/800/122/final (opens in new window).

Linked vs linkable

NIST 800-122 §2.2 introduces a crucial distinction:

  • Linked information is information about or related to an individual that is logically associated with other information about the individual.
  • Linkable information is information about or related to an individual for which there is a possibility of logical association with other information about the individual.

A masking pipeline must consider linkable fields (e.g., birth date alone isn't identifying, but date + zip + sex is - the Sweeney 87 % rule). The pipeline shouldn't only protect direct identifiers.

Confidentiality impact levels

NIST 800-122 §3 names six factors that drive the PII confidentiality impact level (low / moderate / high):

  1. Identifiability - how directly the PII identifies
  2. Quantity - how many individuals' data
  3. Data field sensitivity - what specific fields (SSN > name)
  4. Context of use - what the PII is used for
  5. Obligation to protect confidentiality - legal duty
  6. Access to and location of PII - where stored, who can access

Masking aggressiveness scales with impact level.

HIPAA Safe Harbor - 18 identifiers (45 CFR § 164.514(b)(2))

For health data (PHI), the HIPAA Privacy Rule defines two de-identification methods (Expert Determination, 45 CFR § 164.514(b)(1), and Safe Harbor, 45 CFR § 164.514(b)(2)). Safe Harbor requires removing all of these 18 identifiers (per HHS guidance, hhs.gov/hipaa/for-professionals/privacy/special-topics/de-identification (opens in new window)):

  1. Names
  2. All geographic subdivisions smaller than a state (street, city, county, precinct, ZIP - except first 3 digits of ZIP if population > 20,000)
  3. All elements of dates (except year) directly related to the individual, including birth, admission, discharge, death; all ages over 89 → "90 or older"
  4. Phone numbers
  5. Fax numbers
  6. Electronic mail addresses
  7. Social Security numbers
  8. Medical record numbers
  9. Health plan beneficiary numbers
  10. Account numbers
  11. Certificate / licence numbers
  12. Vehicle identifiers (incl. licence plate)
  13. Device identifiers and serial numbers
  14. Web URLs
  15. IP addresses
  16. Biometric identifiers (fingerprints, voiceprints)
  17. Full-face photos and comparable images
  18. Any other unique identifying number, characteristic, or code

A masking pipeline operating on health data must catch all 18; a detector configured only for GDPR's broader categories will miss HIPAA-required identifiers (e.g., medical record number is not explicit in GDPR Art. 4(1) - covered by "identification number" but a detector may not flag it without a HIPAA-specific recogniser).

Related skills

k-anonymity-verifier

Verifies that a masked dataset satisfies k-anonymity, l-diversity, and t-closeness by computing equivalence classes over chosen quasi-identifiers and reporting re-identification risk. Covers quasi-identifier selection heuristics, threshold guidance, pycanon API (k_anonymity / l_diversity / t_closeness / report), ARX Java API and GUI workflow, SmartNoise for differential-privacy comparison, and CI-gate integration. Distinct from pii-masking-pipeline-builder's masking-techniques catalog (which lists masking operators but defers k-anonymity measurement to dedicated tooling) and from presidio-pii-detection (which detects PII spans but offers no equivalence-class analysis). Use when you need to confirm whether a masked dataset meets a stated k, l, or t threshold before promoting it to a non-production environment.

presidio-pii-detection

Author and run Microsoft Presidio PII detection - wraps presidio-analyzer (PII detector) + presidio-anonymizer (replace/redact/mask/hash/encrypt operators) for scanning datasets, log streams, and free-text fields. Covers AnalyzerEngine + AnonymizerEngine setup, built-in recognizers (PERSON, EMAIL_ADDRESS, CREDIT_CARD, US_SSN, IBAN_CODE, country-specific IDs across US/UK/Spain/Italy/Poland/Singapore/Australia/India and more), custom PatternRecognizer authoring, score thresholds, and CI gating. Use when scanning *existing* data for PII (vs synthesising fresh fixtures with synthetic-pii-generator).

synthea-healthcare-data

Author and run Synthea (MITRE's open-source synthetic patient population simulator) to produce HIPAA-safe synthetic medical records for testing health IT systems. Covers Gradle build, population-size and state-specific generation, FHIR R4 / STU3 / DSTU2 / C-CDA / CSV / CPCDS output formats, disease-module customisation, and the lifecycle-simulation approach (birth-through-death patient journeys with realistic demographics). Use when testing FHIR servers, EHR integrations, claims processing, or any health IT system that needs realistic patient records without HIPAA exposure (distinct from the generic Faker family - qa-test-data faker-data for fixtures, the pii-masking-pipeline-builder faker-masking-operators reference for masking substitution; this is health-domain-specific).