# ASVS V14.7 — Data Minimisation Review — Pre-Pilot

**Audit-Datum:** 2026-05-23
**Auditor:** operator (KAR-536)
**Scope:** Alle Tabellen mit personenbeziehbaren Daten (PII) im BMW-Pilot-Scope.
**Referenz:** OWASP ASVS v5.0.0 Verification Requirement V14.7.1; DSGVO Art. 5(1)(c) "Datenminimierung".
**Status:** Pass mit dokumentierten Follow-Ups. Keine Code-Aenderungen erforderlich vor BMW-Pilot.

---

## 1. Reviewed Tables (PII-tragend)

35 `public.*` Tabellen insgesamt. Davon 6 mit PII oder personenbezogenen Daten:

| Tabelle | PII-Felder | Zweck | Bewertung |
|---|---|---|---|
| `consultants` (MO-23) | first_name, last_name, display_name, auth_user_id | Berater-Stammdaten fuer Projekt-Assignment | **PASS** — minimal |
| `fachbereich_users` | email, full_name, department, phone | BMW-Fachbereich-User fuer Intake-Form-Vorbefuellung | **CAVEAT** — `phone` siehe F1 |
| `intake_submission_tokens` | email | Public-Intake-Token-Email-Binding | **PASS** — single-purpose |
| `intakes` | (kein direct-PII; Foreign-Keys zu fachbereich_users) | Intake-Aufnahmen | **PASS** |
| `oee_orphan_reminders` | email_to | Notification fuer verwaiste OEE-Records | **PASS** — operational |
| `project_members` | email | Mitglieder-Liste fuer Workstream-Notifications | **CAVEAT** — siehe F2 |

**Out-of-scope** (V1 BMW-Pilot ist Stand-alone, kein Multi-Tenant — `project_kadi_v2_tenant_later.md`):
- `public.consultants` aus `supabase/migrations/supabase-migration-platform-v2.sql` (Control-Plane Variant mit bio/avatar_url/region/skill_tags) — diese Variant ist fuer Multi-Tenant V2, kommt erst nach BMW-Pilot live. Audit-Schluss: vor V2-Rollout separates Review.

---

## 2. Findings

### F1 — `fachbereich_users.phone` nicht im Code verwendet

**Was:** Spalte `phone text` in `fachbereich_users` Tabelle existiert (Migration `supabase/migrations/supabase-migration-intake-v1-schema.sql:131`).

**Verwendung:** Code-Grep (`grep -rn "phone" app components lib --include='*.ts' --include='*.tsx'`) findet `phone` nur in `lib/logger.ts` als PII-Redaction-Marker. **Kein Read-Pfad, kein Write-Pfad in der App.**

**Risiko:** Spalte ist datenschutzrechtlich ueber-collected — falls je Telefonnummern eingegeben wuerden, lagen sie ohne Zweck im System.

**Aktueller Schaden:** Vermutlich Null oder sehr gering — keine UI nimmt das Feld auf. Verifizieren via Production-Query:
```sql
SELECT count(*) FROM fachbereich_users WHERE phone IS NOT NULL AND phone <> '';
```
(operator kann das in der Operations-Phase ausfuehren, vor BMW-Pilot-Cutover.)

**Empfehlung:**
- **Option A (preferred):** Migration `supabase-migration-v14.7-drop-phone.sql` schreibt `ALTER TABLE fachbereich_users DROP COLUMN phone`.
- **Option B:** Falls Phone fuer kuenftige UI gewollt: Backlog-Issue fuer "Phone-UI-Adoption + Consent" anlegen, sonst Drop.

**Aufwand:** XS (Migration + Rollback). Empfehlung: Option A vor BMW-Pilot.

### F2 — `project_members.email` Redundanz pruefen

**Was:** `project_members.email text` (Migration `supabase/migrations/supabase-migration-pmo-phase-1.sql`).

**Hintergrund:** `project_members` referenziert Mitglieder eines PMO-Workstreams. Email ist optional.

**Risiko:** Wenn `email` redundant zu `consultants.email` oder `fachbereich_users.email` per `auth_user_id`-Join verfuegbar ist, sollte sie nicht doppelt gespeichert werden (Stale-Data-Risiko + Duplikat).

**Aktueller Stand:** Spalte existiert, Nutzung im Code muss verifiziert werden:
```bash
grep -rn "project_members.*email\|project_members\\.email" app components lib --include='*.ts' --include='*.tsx'
```

**Empfehlung:**
- Falls *nicht* gelesen: in F1-Migration mit-droppen.
- Falls *gelesen*: pruefen ob Lookup auf `consultants.email` via `auth_user_id`-Join moeglich ist; dann Spalte als deprecated markieren und Migration in V2-Refactor.

**Aufwand:** S (Code-Lesepfad-Audit + Migration). Soft-Block fuer BMW-Pilot — kann post-Pilot.

### F3 — `consultants.display_name` kann PII enthalten

**Was:** `consultants.display_name text NOT NULL` (Migration `MO-23/planning-tables.sql`).

**Hintergrund:** Wird im UI angezeigt, ueblich `"Vor- Nachname"`.

**Risiko:** Niedrig — display_name ist gewollt, kein Over-Collection. Aber: bei "Right-to-Erasure" (DSGVO Art. 17) muss display_name beim Deletion-Flow mit-anonymisiert werden.

**Stand:** KAR-529 (Art-17-Endpoint) marked consultants als deletion-target via `is_active=false` und PII-Scrubbing. **PASS, aber im DSGVO-Runbook explizit listen.**

### F4 — `intake_field_values.value_json` kann freie PII enthalten

**Was:** Intake-Form-Felder werden generisch in `intake_field_values.value_json jsonb` gespeichert.

**Risiko:** Free-Form-Felder erlauben User-Eingabe beliebiger Inhalte — Telefonnummern, Email-Adressen, sogar IBAN. Da die Felder generisch sind, kann der Code nicht "wissen" was drinsteht.

**Mitigation:**
- Field-Definitions (`intake_field_definitions`) markieren `field_type` (text, select, etc.) und `is_required`. Sensible Felder sollten in einem Sinn-Audit explizit als `pii=true` markiert werden.
- DSGVO-Loeschung via KAR-529 Endpoint erfasst auch `intake_field_values` (Cascade ueber `intake_id`).

**Empfehlung:** Field-Definitions-Audit *separat*: Welche Felder sind tatsaechlich notwendig fuer Lieferantenbefaehigung? Bei Felder die seit 90 Tagen `0 Submissions` haben → in Inactive-Liste, fuer V2 droppen.

**Aufwand:** M — eigenes Backlog-Issue (operator erstellt KAR im Anschluss).

---

## 3. Retention Review

ASVS V14.7 deckt auch *Aufbewahrungsdauer* ab.

| Datenklasse | Aktuelle Retention | Bewertung |
|---|---|---|
| `intake_submission_tokens` | Permanent (auch nach `used_at`) | **REVIEW** — TTL fuer used Tokens nicht definiert. Empfehlung: nach 90 Tagen droppen (Audit-Spur via Daily-Log). |
| `oee_orphan_reminders` | Permanent | **REVIEW** — sent reminders 30 Tage retainen, danach drop. |
| `audit_log` (KAR-538 file-events) | Permanent | **PASS** — Audit-Logs muessen retainen (Compliance-Anforderung). |
| `auth.users` (Supabase) | Permanent bis User-Deletion | **PASS via KAR-529** — Art-17-Loeschung mit Anonymization. |
| `intakes` + `intake_field_values` | Permanent | **REVIEW** — Aufnahme-Dokumente werden im Pilot bewusst behalten, post-Pilot Retention-Policy festlegen. |

**Empfehlung:** Retention-Cronjob `supabase-migration-v14.7-retention-cleanup.sql` mit pg_cron-Schedule, der einmal pro Woche:
- `DELETE FROM intake_submission_tokens WHERE used_at < now() - interval '90 days'`
- `DELETE FROM oee_orphan_reminders WHERE sent_at < now() - interval '30 days'`

**Aufwand:** S (Migration + pg_cron-Setup). **Soft-Block fuer BMW-Pilot — kann in Operations-Sprint folgen.**

---

## 4. Right-to-Know / Right-to-Portability

ASVS V14.7 verlangt explizit "right to know what data is collected".

**Stand:**
- DSGVO-Art-15-Endpoint (Auskunft) ist via KAR-529 implementiert: `POST /api/gdpr/export` liefert JSON-Export aller User-bezogenen Daten.
- DSGVO-Art-17-Endpoint (Loeschung) ist via KAR-529 implementiert: `POST /api/gdpr/delete` setzt `is_active=false` und scrubbt PII.

**Gap:** Es gibt noch *kein* User-facing UI fuer den Self-Service-Export. Stand 2026-05-23 ist Auskunft per Mail an Datenschutz → Admin triggert Endpoint manuell.

**Empfehlung:** Self-Service-Privacy-Page (`/account/privacy`) mit "Daten exportieren" und "Konto loeschen" Buttons. **Post-Pilot Roadmap.**

---

## 5. ASVS-Mapping Update

Patch fuer `docs/security/asvs-l2.md` Zeile 234:

```
| V14.7.1 data minimisation reviewed | Pass (with caveat) | Audit in docs/audits/asvs-v14.7-data-minimisation.md (KAR-536). 35 Tabellen reviewed, 6 mit PII. Follow-Ups: F1 phone-Drop, F2 project_members.email-Audit, F3 Retention-Cronjob. |
```

---

## 6. Open Follow-Ups (Linear-Issues empfohlen)

| ID | Title | Prio | Aufwand |
|---|---|---|---|
| V14.7-F1 | Drop `fachbereich_users.phone` Migration | 🟠 High (vor BMW-Pilot) | XS |
| V14.7-F2 | `project_members.email` Verwendung pruefen + ggf. droppen | 🟡 Medium (post-Pilot) | S |
| V14.7-F3 | Retention-Cronjob fuer used tokens + orphan reminders | 🟡 Medium (Operations-Sprint) | S |
| V14.7-F4 | Intake-Field-Definitions Sinn-Audit (welche Felder sind noch genutzt?) | 🟡 Medium | M |
| V14.7-F5 | Self-Service `/account/privacy` UI fuer Export + Deletion | 🔵 Low (post-Pilot) | M |

operator erstellt diese als KAR-Sub-Issues im Anschluss.

---

## 7. Verification

```bash
# Re-run PII-column scan
grep -E "^\\s+(email|phone|telefon|first_name|last_name|full_name|address)" supabase/migrations/supabase-migration-*.sql supabase/bootstrap/supabase-schema.sql

# Re-check unused-column candidates
grep -rn "phone" app components lib --include="*.ts" --include="*.tsx"

# Production-Query (operator operations):
# SELECT count(*) FROM fachbereich_users WHERE phone IS NOT NULL AND phone <> '';
```

---

**Verwandte Audits:**
- `docs/security/asvs-l2.md` — Master ASVS-Mapping
- `docs/audits/asvs-v3.10-sensitive-data-in-url.md` — KAR-540
- `docs/audits/rls-cross-user-audit.md` — KAR-525
- `app/api/gdpr/export/route.ts` + `app/api/gdpr/delete/route.ts` — KAR-529 (Art-15 + Art-17)
