diff --git a/transformation-config/skills/pii-bouncer/config.yaml b/transformation-config/skills/pii-bouncer/config.yaml new file mode 100644 index 00000000..2e520d3d --- /dev/null +++ b/transformation-config/skills/pii-bouncer/config.yaml @@ -0,0 +1,12 @@ +type: docs-only +template: description.md +description: Mask PII in session replay — add the ph-no-capture class to sensitive elements and configure recording mask options +tags: [best-practices, session-replay, privacy, pii] +shared_docs: + - https://posthog.com/docs/session-replay/privacy.md + - https://posthog.com/docs/libraries/js/config.md +variants: + - id: all + display_name: PII Bouncer + tags: [session-replay, privacy, pii] + docs_urls: [] diff --git a/transformation-config/skills/pii-bouncer/description.md b/transformation-config/skills/pii-bouncer/description.md new file mode 100644 index 00000000..b9317d09 --- /dev/null +++ b/transformation-config/skills/pii-bouncer/description.md @@ -0,0 +1,150 @@ +# PII Bouncer + +This skill hardens a frontend project against **PII leaking into session +replay and autocapture**. It edits project source: it adds the +`ph-no-capture` class to sensitive elements and tightens the masking options +on `posthog.init`. The only file it *creates* is the final report. + +This is the prevention layer that complements PostHog's other PII defenses: +the security scanner stops PII being **sent as event properties**; this skill +stops PII being **recorded in the DOM** (replay) or **read by autocapture**. + +## What posthog-js already does (don't undo it) + +Read the bundled reference docs — `references/privacy.md` and +`references/config.md` — for the authoritative API and defaults. The two facts +that shape this skill: + +- **`maskAllInputs` defaults to `true`.** Every `` value is already + masked in replay out of the box. Your job is **not** to "turn on input + masking" — it's to (a) confirm nobody disabled it, and (b) cover the + surfaces defaults miss. +- **General text is NOT masked by default.** Sensitive content rendered as + text (an SSN on a profile page, an order total, an account balance) is + recorded verbatim unless you mask it. This is the main gap to close. + +So the value this skill adds is: masking sensitive **text**, blocking +sensitive **non-input elements**, and verifying the input default is intact — +not restating defaults. + +## The two mechanisms + +1. **`ph-no-capture` (CSS class)** — add it to an element and that element is + replaced by a same-size placeholder block in replay, **and** autocapture is + disabled for it. Use for discrete sensitive elements (a card-number field, + a div showing an SSN, a billing summary). This is a class, not an + attribute — in JSX use `className="ph-no-capture"`, everywhere else + `class="ph-no-capture"`. **Merge** it into any existing class list; never + clobber existing classes. + - Do **not** confuse this with the `data-ph-no-capture` *attribute*, which + is a separate, autocapture-only opt-out. For replay masking, use the + **class**. + +2. **Init mask config** — project-wide masking, set wherever PostHog is + initialised: the `session_recording` options on `posthog.init(token, + { session_recording: { … } })`, **or** — in React projects that may have no + literal `init` call — the `options={{ session_recording: { … } }}` prop on + ``. The high-value option is `maskTextSelector` (e.g. `"*"` + to mask all text, or a scoped selector for sensitive regions). Confirm + `maskAllInputs` is not set to `false`. Use the exact option names and + defaults from `references/config.md` — do not guess them. + +## Workflow + +Emit a `[STATUS]` line (see below) as you enter each phase. Before phase 1, +read `references/privacy.md` and `references/config.md` — they are the source +of truth for the masking option names and defaults. + +1. **Confirm prerequisites.** If `posthog-js` is not a dependency anywhere, + emit `[ABORT] no-posthog-js`. Find where PostHog is initialised — a + `posthog.init(...)` call **or** a React `` (which is + configured via an `options={{…}}` prop and often has no literal `init` + call). Only if **neither** exists, emit `[ABORT] no-init-call`. Enumerate + frontend templates (`.jsx` / `.tsx` / `.vue` / `.svelte` / `.astro` / + `.html`); if none exist, emit `[ABORT] no-frontend-templates`. +2. **Scan templates** for sensitive elements using the heuristic below. +3. **Mask elements.** Add the `ph-no-capture` class (framework-correct + attribute) to each sensitive element. Minimal diffs — touch only the + class attribute, merge don't clobber, skip anything already marked. +4. **Tighten init.** In the `session_recording` options, confirm + `maskAllInputs` is not `false`, and add `maskTextSelector` covering + sensitive text (prefer a scoped selector; use `"*"` when sensitive text is + widespread). Do not duplicate an option that is already set. +5. **Write the report** to `./posthog-pii-bouncer-report.md`. + +## Sensitive-element heuristic + +Conservative bias: **when a signal matches, mask it.** Over-masking only +over-protects; under-masking leaks PII. Treat an element as sensitive when +any of these hold: + +| Signal | Matches | +|---|---| +| `type` | `password`, `email`, `tel` | +| `autocomplete` | `current-password`, `new-password`, `cc-number`, `cc-csc`, `cc-exp`, `email`, `tel`, `one-time-code`, `ssn` | +| `name` / `id` | matches `/\b(pass(word)?\|pwd\|card\|cc[-_]?(num\|number)\|cvv\|cvc\|csc\|ssn\|sin\|nin\|pin\|dob\|birth\|tax\|passport\|iban\|account[-_]?(num\|number))\b/i` — word-bounded so `pass` doesn't match `passenger`, `pin` doesn't match `spinner`, etc. | +| label / `aria-label` | text contains "password", "credit card", "card number", "CVV/CVC", "SSN", "social security", "date of birth", "bank account", "routing" | +| placeholder | matches the same terms as label | +| rendered text | an element whose static text obviously prints PII (SSN, full card number, bank account) — mask via `ph-no-capture` or a scoped `maskTextSelector` | + +When unsure whether a field is sensitive, mask it. + +## Idempotency + +This skill must be safe to run repeatedly. Before editing, check whether an +element already has `ph-no-capture` (or sits inside a blocked ancestor) and +whether the init already sets the option — if so, skip it. A second run must +produce **zero** changes. + +## Live activity — `[STATUS]` + +The wizard's "Working on …" banner reads `[STATUS]` lines you emit in plain +text. Emit one as you enter each phase, e.g.: + +``` +[STATUS] Scanning templates for sensitive inputs +[STATUS] Masking 4 sensitive elements +[STATUS] Tightening session_recording config +[STATUS] Writing report +``` + +## Abort statuses + +Emit these `[ABORT]` lines verbatim when the condition holds, then stop. The +wizard catches them and renders the outro — do not halt yourself otherwise. + +- `[ABORT] no-posthog-js` — `posthog-js` is not installed anywhere. +- `[ABORT] no-init-call` — no `posthog.init(...)` call found. +- `[ABORT] no-frontend-templates` — no `.jsx` / `.tsx` / `.vue` / `.svelte` / + `.astro` / `.html` files found. + +These strings are a contract with the wizard, which routes each one to a +specific outro. Keep them in sync with the consuming side — see (in the +PostHog/wizard repo) `src/lib/programs/pii-bouncer/abort-cases.ts`. + +## Report + +Write `./posthog-pii-bouncer-report.md` covering: + +- **Elements masked** — for each, the `file:line`, the element, and which + heuristic signal matched. +- **Init changes** — the exact `session_recording` options added or confirmed. +- **Reviewed but skipped** — elements you considered and deliberately left + alone, with a one-line rationale (so a human can spot a wrong call). +- **Manual follow-ups** — anything needing human judgment (e.g. a dynamic + list that may render PII, a third-party iframe replay can't reach). + +## Key principles + +- **Minimal diffs.** Touch only the class attribute and the init options. + Never reformat surrounding code. +- **Merge, don't clobber.** Append `ph-no-capture` to existing class lists. +- **Evidence in the report.** Every masked element cites `file:line` and the + signal that matched. +- **Authoritative API.** Use the bundled `references/privacy.md` and + `references/config.md` for exact option names and defaults; never invent + option names. + +## Framework guidelines + +{commandments}