# Phase C: Sensitive Data Redaction — Design Document > **Status:** Approved — ready for implementation planning > **Spec:** `docs/plans/2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md` section C1 > **UI Decision:** Simple toggle (Option 1) > **Branch:** `feat/export-phase-c` ## Overview Server-side regex redaction with a simple checkbox toggle in the export preview modal. No rich editor — keeps the existing textarea. User sees a summary of what was masked and can manually edit the result. --- ## Backend ### New File: `backend/app/services/redaction_service.py` **`apply_redaction(session) -> tuple[Session, RedactionSummary]`** - Deep-copies the session (original ORM object never mutated) - Walks `decisions` list and `custom_steps`, applies regex replacements to all string fields: `answer`, `notes`, `command_output`, `content`, `action_performed` - Also redacts top-level session fields: `scratchpad`, `outcome_notes`, `next_steps` - Returns the sanitized copy and a summary of what was found **`RedactionSummary` dataclass:** ```python @dataclass class RedactionSummary: ips: int = 0 emails: int = 0 tokens: int = 0 unc_paths: int = 0 ``` ### Regex Patterns (conservative — false positives > false negatives) | Pattern | Regex | Replacement | |---------|-------|-------------| | IPv4 | `\b(?:\d{1,3}\.){3}\d{1,3}\b` | `[IP REDACTED]` | | IPv6 | `\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b` | `[IP REDACTED]` | | Email | `\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z\|a-z]{2,}\b` | `[EMAIL REDACTED]` | | Bearer tokens | `Bearer\s+[A-Za-z0-9._-]+` | `[TOKEN REDACTED]` | | API key patterns | Long hex/base64 strings (32+ chars) | `[TOKEN REDACTED]` | | UNC paths | `\\\\[\w.-]+\\[\w$.-]+` | `[UNC PATH REDACTED]` | Hostname redaction is **not** included — MSP tickets legitimately reference hostnames. ### Schema Change: `backend/app/schemas/session.py` Add to `SessionExport`: ```python redaction_mode: Literal["none", "mask"] = "none" ``` ### Integration Point: `backend/app/api/endpoints/sessions.py` Insert at ~line 297 (after session fetch, before format branching): ```python redaction_summary = None if export_options.redaction_mode == "mask": session, redaction_summary = apply_redaction(session) ``` Export generators receive `redaction_summary` and append a footer when present: ``` --- Redacted: 3 IPs, 2 emails, 1 token --- ``` ### Response The redaction summary is returned via an `X-Redaction-Summary` response header (JSON-encoded) to avoid changing the existing content-based response body. ### No Migration Needed All changes are runtime — no database schema changes. --- ## Frontend ### `ExportPreviewModal.tsx` New props: - `redactionEnabled?: boolean` - `onToggleRedaction?: (enabled: boolean) => void` - `redactionSummary?: { ips: number; emails: number; tokens: number; unc_paths: number } | null` Add a "Mask Sensitive Data" checkbox next to the existing "Include Summary" checkbox, using the same visual pattern: ```tsx ``` When `redactionSummary` has matches, show an info line below the toggles in `text-blue-400`: ``` Masked: 3 IPs, 2 emails, 1 token ``` If redaction is on but nothing was found: `"No sensitive data detected"` in `text-white/40`. ### `SessionDetailPage.tsx` - Add `redactionMode` state (`'none' | 'mask'`) - Wire into export options object - Pass toggle callback to `ExportPreviewModal` - Same pattern as existing `includeSummary` state ### `types/session.ts` Add to `SessionExport` type: ```typescript redaction_mode?: 'none' | 'mask' ``` --- ## Testing ### Backend: `backend/tests/test_psa_export.py` — `TestPhaseC` class - Test redaction of each pattern type individually (IP, email, bearer token, API key, UNC path) - Test `redaction_mode="none"` leaves content untouched - Test original session object is not mutated (deep copy verification) - Test redaction summary counts are accurate - Test redaction across all text fields (`notes`, `command_output`, `answer`, `scratchpad`, `outcome_notes`, `next_steps`) - Test edge cases: empty strings, no matches, overlapping patterns ### Frontend `npm run build` validates types. No new component tests needed for a checkbox toggle. --- ## Files to Create/Modify | Action | File | |--------|------| | Create | `backend/app/services/redaction_service.py` | | Modify | `backend/app/schemas/session.py` | | Modify | `backend/app/api/endpoints/sessions.py` | | Modify | `frontend/src/types/session.ts` | | Modify | `frontend/src/components/session/ExportPreviewModal.tsx` | | Modify | `frontend/src/pages/SessionDetailPage.tsx` | | Extend | `backend/tests/test_psa_export.py` |