# Committed seeds and fixtures contain no real-looking PII

- **Pattern:** `ab-002638` (`soc2-readiness.data-lifecycle.no-real-pii-in-committed-fixtures`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-07-03
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002638
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002638)

## Why it matters

SOC 2 C1.1 requires identifying and protecting confidential information, and a git repository is the leakiest place personal data can live: it is cloned to every laptop, mirrored to CI runners, shared with contractors, and immortal in history even after deletion. Real customer rows exported into a seed file "just to have realistic test data" are a confidentiality breach an auditor will flag on day one, and the pattern is common in AI-built projects because coding assistants happily paste a production query result into `prisma/seed.ts` when asked for sample data. Unlike a leaked key, leaked PII cannot be rotated: once an email address, SSN, or card number is in history, the exposure is permanent. This check evidences SOC 2 C1.1 by verifying that committed data files use synthetic, reserved-domain, or documented-test values only.

## Severity rationale

Medium because committed PII is a real confidentiality exposure with permanent git-history persistence, but exploitation requires repo access and the blast radius is bounded by how much data was committed, unlike a live credential.

## Remediation

Replace real values in seeds and fixtures with synthetic data: use `@faker-js/faker` configured to emit reserved documentation domains (`faker.internet.email({ provider: 'example.com' })`), the 555-01XX fictional phone range, and processor-published test card numbers (Stripe's `4242424242424242`). Purge already-committed real values from git history with `git filter-repo` (history persistence is the actual exposure, not just the current tree), then add a secret/PII scanning step to CI so regressions are caught at commit time.

## Detection

- **ID:** `no-real-pii-in-committed-fixtures`
- **Severity:** `medium`
- **What to look for:** Enumerate committed data files: `prisma/seed.ts`, `seeds/`, `fixtures/`, `__fixtures__/`, `test/` and `tests/` data files (`.json`, `.csv`, `.sql`, `.ts` seed scripts), `supabase/seed.sql`, `cypress/fixtures/`, `db/seeds/`. Within them, apply ONLY these deterministic detectors, precision over recall: (a) email addresses whose domain is NOT a reserved documentation domain — reserved domains pass: `example.com`, `example.org`, `example.net`, any `*.test`, `*.invalid`, `*.localhost`, `*.example` (RFC 2606/6761); real deliverable domains fail, including the project's own domain (internal staff emails are real PII); (b) SSN-format strings (`\d{3}-\d{2}-\d{4}`) unless the area number is `000`, `666`, or `900`-`999` (invalid/never-issued ranges); (c) 13-19 digit numbers that pass the Luhn check and are NOT processor-published test PANs (allow Stripe and major-processor documented test numbers: `4242424242424242`, `4000...` series, `4111111111111111`, `5555555555554444`, `378282246310005`, `6011111111111117`); (d) phone numbers ONLY when in labeled fields (`phone:`, `tel:`, `mobile:`) and outside the `555-01XX` fictional range. Names alone NEVER fail: a `"John Smith"` in a seed is not evidence of real PII. Count all data files examined, and for each detector hit quote a REDACTED excerpt showing shape only — never the full value.
- **Pass criteria:** Data files exist and every detector above comes back clean — values are faker-generated, reserved-domain, invalid-range, or documented test numbers — or data files contain no person-shaped fields at all. Report the count of data files scanned and their paths even on pass. Do NOT pass on a comment or README claiming the data is fake — only the detector rules above decide, and a deliverable-domain email is a fail regardless of intent annotations.
- **Fail criteria:** Any detector (a)-(d) fires on a committed data file. Volume matters for the detail string, not the verdict: one real email fails the same as five hundred rows, because git history makes any committed value permanent.
- **Skip (N/A) when:** The repository has no seed, fixture, or committed test-data files at all. Quote: `"No seed/fixture/test-data files found (checked prisma/seed.*, seeds/, fixtures/, cypress/fixtures/, supabase/seed.sql, test data files)"`.
- **Before evaluating, quote:** The list of data files examined, and for each finding the file path plus a REDACTED excerpt showing shape only (e.g. `j***@gmail.com`, `4539-…-…03 (Luhn-valid, not in the documented test-PAN set)`). Never reproduce a full email, SSN, or PAN in the report or telemetry.
- **Report even on pass:** `"<N> data files scanned (paths); all person-shaped values synthetic: reserved domains / 555-01XX phones / documented test PANs"`.
- **Detail on fail:** `"prisma/seed.ts: 12 email addresses at deliverable domains (gmail.com, outlook.com) and 3 labeled phone fields outside 555-01XX — values redacted; git history retains them even if replaced"` or `"cypress/fixtures/users.json: 2 Luhn-valid card numbers not in the documented test-PAN set"`.
- **Remediation:** Regenerate seeds with `@faker-js/faker` pinned to reserved domains (`faker.internet.email({ provider: 'example.com' })`), fictional phone ranges, and Stripe test PANs; purge history with `git filter-repo` since replacement alone leaves the exposure in every clone; then wire a CI scanning gate so it cannot recur. The `secret-scanning-in-ci` check in this audit covers that gate.

## External references

- soc2 C1.1
- iso-27001 A.8.33

Taxons: privacy-consent, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-002638
