# 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
<label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
  <input type="checkbox" checked={redactionEnabled} onChange={...} />
  Mask Sensitive Data
</label>
```

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` |
