cuonztech-privacy-chaos-gate 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,310 +1,394 @@
1
- # CuonzTech Privacy Chaos Gate (CPCG)
2
-
3
- CI-Gate und Auto-Fix für Consent-Tracker: findet Requests, die **vor** der Einwilligung
4
- firen — und liefert den Fix als Pull Request. Prüft auch die Gegenrichtung:
5
- läuft erlaubtes Tracking nach dem Akzeptieren korrekt an?
6
-
7
- > **Hinweis:** Technisches Hilfsmittel, **keine Rechtsberatung**, keine Garantie für
8
- > revDSG-/DSGVO-/TTDSG-Konformität. / Technical aid, **not legal advice**.
9
-
10
- ## Installieren
11
-
12
- Voraussetzung: Node.js 20 oder neuer.
13
-
14
- ```bash
15
- # 1. CLI installieren (Befehl: cpcg)
16
- npm install -g cuonztech-privacy-chaos-gate # oder im Projekt: npm install -D cuonztech-privacy-chaos-gate
17
-
18
- # 2. Prüf-Browser installieren (einmalig pro Rechner)
19
- npx playwright install chromium
20
-
21
- # 3. Ersten Scan starten (Scan und Konsolen-Gate brauchen keine Lizenz)
22
- cpcg --url https://staging.example.com
23
- ```
24
-
25
- Ohne Installation: `npx cuonztech-privacy-chaos-gate --url https://staging.example.com`
26
- (der Browser aus Schritt 2 muss trotzdem vorhanden sein; auf Linux-CI zusätzlich mit Systembibliotheken:
27
- `npx playwright install --with-deps chromium`). Fehlt der Browser, meldet das CLI das und beendet sich mit Exit 3.
28
-
29
- Das CLI hat keine Subcommands: alle Angaben sind Optionen von `cpcg`.
30
-
31
- ## Lizenz und Preise
32
-
33
- Das Paket ist proprietär und source-available (Datei `LICENSE`, ab 0.2.1; die Versionen 0.1.0 und 0.2.0 wurden unter MIT veröffentlicht und bleiben es).
34
- Online-Kauf → Code per Mail.
35
-
36
- ```bash
37
- export CPCG_LICENSE="CPCG-…" # Lizenzcode (Pilot/Einmal) oder Kunden-Key CPCG-K-… (Abo)
38
- cpcg --url https://staging.example.com --jurisdiction CH
39
- ```
40
-
41
- In CI die Lizenz als Secret setzen und per Umgebungsvariable `CPCG_LICENSE` übergeben, nicht als Argument.
42
-
43
- **Frei, ohne Lizenz** (auch kommerziell und intern, auch gegen Produktiv-URLs): Scan, Konsolen-Gate (Exit-Code, JSON-Report, neutraler Evidence-Report ohne Kundenbezug), lokales `--auto-fix` ohne Commit/PR. Ziele auf `localhost`, `127.0.0.1` und `::1` sind immer frei.
44
-
45
- **Nur mit gültiger Lizenz:**
46
-
47
- | Funktion | Option |
48
- |----------|--------|
49
- | PDF-Report sowie Kundenreport (HTML/Markdown mit `--client`) | `--report-pdf`, `--client` |
50
- | Auto-Fix als Commit oder Pull Request | `--git-commit`, `--create-pr` |
51
-
52
- Ohne Lizenz werden diese Funktionen mit einem Hinweis übersprungen; der Exit-Code bleibt der des Gates. Exit 2 gibt es nur bei einer **ungültigen, abgelaufenen oder gesperrten** Lizenz, nicht bei fehlender Lizenz und nicht bei einem nicht erreichbaren Lizenzserver.
53
-
54
- | Plan | Preis (netto, CHF) | Umfang |
55
- |------|--------------------|--------|
56
- | Pilot / Evidence | 890 einmalig | Kundenreport, Auto-Fix-PR, 1 Domain, 90 Tage |
57
- | Pro | 69 je Domain und Monat | Kundenreport, Auto-Fix-PR, GitHub Action und CLI-Lizenz |
58
- | Agency | 349 je Monat | Pro für bis zu 10 Domains |
59
-
60
- Die Domain-Anzahl beruht auf Vertrauen: das CLI zählt Domains nicht. Scans werden nicht gezählt. Monatlich kündbar. Kauf und Fragen: `support@cuonztech.ch`.
61
-
62
- ## Was das Tool prüft
63
-
64
- 15 Chaos-Szenarien in einem Lauf — jedes mit ehrlichem Status
65
- (**bestanden / auffällig / nicht prüfbar**). Ein Button, der nicht gefunden wird,
66
- wird nie zu einem grünen Häkchen.
67
-
68
- | # | Szenario | Frage |
69
- |---|----------|-------|
70
- | 1 | happy-path-denied | Bleibt Tracking nach «Ablehnen» aus? |
71
- | 2 | happy-path-accepted | Ist vor dem Klick Ruhe — und läuft erlaubtes Tracking danach an? |
72
- | 3 | fast-click | Navigation vor dem Banner: feuert dann schon etwas? |
73
- | 4 | state-manipulation | Tracking nach Storage-Wipe? |
74
- | 5 | network-drop | Tracker trotz blockiertem CMP? |
75
- | 6 | consent-revoked | Stoppt Tracking nach Widerruf? Welche IDs bleiben liegen? |
76
- | 7 | category-selective | Nur Statistik erlaubt — bleiben Marketing-Tracker aus? |
77
- | 8 | consent-persistence | Überlebt die Entscheidung Reload, Unterseite und Neustart? |
78
- | 9 | banner-ignored | Starten Tracker per Timer/Scroll ohne Klick? |
79
- | 10 | subpage-spa | Greift der Schutz auf Unterseiten und bei SPA-Navigation? |
80
- | 11 | cmp-failure | Tracking-Zeitfenster bei verzögerter/defekter CMP? |
81
- | 12 | embed-leaks | Kontaktieren YouTube/Maps/Social-Embeds vor Freigabe? |
82
- | 13 | consent-mode-v2 | Decken sich Consent-Mode-Zustände mit Cookies/Requests? |
83
- | 14 | duplicate-events | Doppelte page_view-Events (Plugin + GTM parallel)? |
84
- | 15 | mobile-usability | Sind Ablehnen/Einstellungen auf dem Handy bedienbar? |
85
-
86
- **Tracker (Auswahl):** Google Tag Manager, Google Analytics, Meta, TikTok, LinkedIn,
87
- Hotjar, Criteo, Marketo, HubSpot, Matomo.
88
-
89
- **CMPs (Auswahl):** OneTrust, Cookiebot, Usercentrics, Tealium, iubenda, etracker,
90
- JustRelate, Didomi, Axeptio, Sourcepoint, Consentmanager, Borlabs, Klaro.
91
-
92
- ## Output: der Prüfbericht
93
-
94
- Jeder Scan schreibt ein Evidence-Paket:
95
-
96
- ```
97
- reports/privacy-chaos-report.json LeakReport (maschinenlesbar), Pfad per --report-output
98
- reports/privacy-chaos-report-evidence.md Evidence-Report (Markdown); mit Lizenz und --client als Kundenreport
99
- reports/privacy-chaos-report-evidence.html Evidence-Report (HTML, CuonzTech-Brand); mit Lizenz und --client als Kundenreport
100
- reports/privacy-chaos-report-deluxe.pdf Deluxe-PDF mit Deckblatt (mit --report-pdf, nur mit Lizenz)
101
- reports/evidence/ Screenshots (portabel, relative Pfade)
102
- ```
103
-
104
- Die Dateinamen richten sich nach dem JSON-Pfad: `<name>.json` ergibt `<name>-evidence.md`, `<name>-evidence.html`
105
- und `<name>-deluxe.pdf` im selben Ordner, die Screenshots liegen in `evidence/` daneben. Standard ist
106
- `reports/privacy-chaos-report.json`; mit `--report-output reports/scan.json` heissen die Dateien `reports/scan-evidence.md` usw.
107
-
108
- Der Report führt **Befunde** (Regelverstösse), **Hinweise** und **beobachtete Requests**
109
- (Timelines) getrennt — und ordnet jedes Ergebnis mehreren Rechtsräumen zu
110
- (CH, EU, UK, US-CA, BR, ROW). Keine globale Compliance-Aussage.
111
-
112
- **Hinweise sind kein Blocker.** Rein funktionale Drittanbieter (etwa Google Fonts) erscheinen
113
- als Hinweis und blockieren das Gate nicht. Mit `--strict` blockieren auch Hinweise.
114
- Bekannte, bewusst akzeptierte Befunde lassen sich mit `--ignore` ausnehmen; sie stehen im Report weiterhin
115
- als «ignoriert».
116
-
117
- ## Flags
118
-
119
- ```bash
120
- cpcg --url https://staging.example.com \
121
- --report-pdf \
122
- --client "Firma AG" \
123
- --jurisdiction CH \
124
- --report-output reports/scan.json
125
- ```
126
-
127
- | Option | Wirkung |
128
- |--------|---------|
129
- | `--url <url>` | Ziel (Pflicht) |
130
- | `--license <code>` | Lizenzcode oder Kunden-Key; besser per Env `CPCG_LICENSE` |
131
- | `--jurisdiction <code>` | `CH`, `EU`, `UK`, `US-CA`, `BR` oder `ROW` |
132
- | `--report-output <pfad>` | JSON-Report (weitere Dateien daneben) |
133
- | `--report-pdf [boolean]` | Deluxe-PDF (Deckblatt + Evidence) — Lizenz erforderlich |
134
- | `--client <name>` | Kundenname für Kundenreport und Deckblatt — Lizenz erforderlich |
135
- | `--demo [boolean]` | Report als synthetische Testdaten kennzeichnen |
136
- | `--fail-on-leak [boolean]` | Exit 1 bei Leaks (CI-Gate, Standard `true`) |
137
- | `--fail-on-uncheckable [boolean]` | Exit 4, wenn Kernszenarien nicht prüfbar sind (Standard `true`); bei `false` Exit 0, das Ergebnis bleibt als unvollständig markiert |
138
- | `--strict [boolean]` | Auch Hinweise (z. B. Google Fonts) blockieren das Gate (Standard `false`) |
139
- | `--ignore <liste>` | Befunde ausnehmen: Tracker-Name oder Host-Muster, kommagetrennt oder mehrfach (`fonts.googleapis.com,*.hotjar.com`); im Report als «ignoriert». `*`/`**` und nackte TLDs (`com`, `ch`) werden **immer** abgelehnt, auch mit `--allow-broad-ignore` (zum Abschalten der Blockierung dient `--fail-on-leak false`); ganze Zonen (`*.com`, `.ch`) nur mit `--allow-broad-ignore`. Die Konsole nennt, wie viele Befunde ausgenommen wurden; ein Szenario, dessen Befunde alle ausgenommen sind, steht als «bestanden (Befunde ignoriert)» |
140
- | `--allow-broad-ignore [boolean]` | Ganze Zonen als `--ignore`-Muster (`*.com`, `*.co.uk`, `.ch`) bewusst zulassen; sie erscheinen in Konsole und Report als Warnung. `*` und nackte TLDs bleiben abgelehnt (Standard `false`) |
141
- | `--accept-selector <css>` | Eigener Selektor des Akzeptieren-Buttons, wenn die Erkennung ihn nicht findet (mehrfach oder kommagetrennt) |
142
- | `--reject-selector <css>` | Eigener Selektor des Ablehnen-Buttons |
143
- | `--settings-selector <css>` | Eigener Selektor des Einstellungen-Buttons |
144
- | `--banner-frame <teilstring>` | Teil der URL des iframes, in dem der Banner liegt (CMP im Frame); findet der Scan keinen solchen Frame, warnt er |
145
- | `--banner-wait <sekunden>` | Wartefenster auf einen spät erscheinenden Banner, 1 bis 60 (Standard 8; Umgebung `CPCG_BANNER_WAIT`). Mit Angabe wird das Fenster ausgeschöpft, ebenso bei `--accept-selector`/`--reject-selector`; die Schlussprüfung wartet zusätzlich eine Nachfrist |
146
- | `--max-subpages <n>` | Obergrenze der Unterseiten-Stichprobe (Szenario `subpage-spa`), 1 bis 40 (Standard 12; Umgebung `CPCG_MAX_SUBPAGES`); das Zeitbudget wächst moderat mit (45 s bis 12, 90 s bis 24, sonst 120 s) |
147
- | `--auto-fix [boolean]` | Patches erzeugen und im Quellverzeichnis anwenden (Standard `true` nur mit ausdrücklichem `--source-dir`) |
148
- | `--source-dir <dir>` | Quellcode für Auto-Fix; ein nicht vorhandenes Verzeichnis ist ein Fehler (Exit 3). Für `--git-commit`/`--create-pr` gilt das Git-Repository, in dem es liegt (nicht das Arbeitsverzeichnis) |
149
- | `--git-commit [boolean]` | Fix in den Git-Branch committen — Lizenz erforderlich |
150
- | `--create-pr [boolean]` | Fix als Pull Request öffnen (Token in Env `GITHUB_TOKEN`, ersatzweise `--github-token`) — Lizenz erforderlich; hat Vorrang vor `--git-commit` |
151
- | `--base-branch <branch>`, `--branch-prefix <prefix>` | Basisbranch und Branch-Präfix des Fix-PRs |
152
- | `--llm-remediation [boolean]` | Opt-in: Quellcode darf an den LLM-Anbieter gesendet werden (Standard `false`) |
153
- | `--llm-provider`, `--llm-model`, `--api-key` | Nur `openai` ist umgesetzt; andere Anbieter fallen auf die Regel-Engine zurück |
154
- | `--headless [boolean]`, `--timeout-ms <ms>` | Browser-Modus und Timeout je Szenario-Navigation |
155
-
156
- Alle Optionen und Standardwerte: `cpcg --help`.
157
-
158
- **Exit-Codes** (für CI): `0` ok · `1` Leaks gefunden (auch wenn ein Fix nur vorgeschlagen, aber nicht verifiziert ist) ·
159
- `2` Lizenz **ungültig, abgelaufen oder gesperrt** (eine fehlende Lizenz oder ein nicht erreichbarer Lizenzserver ist kein Exit 2) ·
160
- `3` Fehler (Ziel unerreichbar oder HTTP 4xx/5xx, Konfiguration, Browser fehlt, falsche Argumente) ·
161
- `4` unvollständig, Kernszenarien nicht prüfbar.
162
- Ein Lauf, der nichts abschliessend prüfen konnte, ist nie grün: `--fail-on-uncheckable` (Standard) macht daraus Exit 4.
163
-
164
- ### Unterseiten-Stichprobe
165
-
166
- Das Szenario `subpage-spa` prüft höchstens `--max-subpages` verlinkte Unterseiten (verschiedene erste Pfadsegmente zuerst, Impressum und Datenschutz zuletzt). Sind mehr Unterseiten verlinkt als geprüft wurden, steht das nicht nur im Report: Die Konsole meldet «Unterseiten: X von Y geprüft — nicht geprüfte Routen können Verstösse enthalten; --max-subpages erhöhen», `report.json` führt es unter `gate.sampling`, die Step-Summary und der Report-Kopf zeigen «PASS, aber Stichprobe», und `gate.incomplete` ist `true`. Der Exit-Code bleibt bei PASS `0` (nicht `4`).
167
-
168
- ### Banner nicht gefunden: Selektoren und Frames
169
-
170
- Findet die Erkennung den Banner oder einen Button nicht, wird das Szenario «nicht prüfbar» (nie grün). Dann helfen die Overrides:
171
-
172
- ```bash
173
- cpcg --url https://staging.example.com \
174
- --accept-selector "#meine-zustimmung" \
175
- --reject-selector "button[data-consent=reject]" \
176
- --settings-selector ".cookie-einstellungen" \
177
- --banner-frame "consent.mein-cmp.example"
178
- ```
179
-
180
- Erscheint der Banner erst spät, verlängert `--banner-wait <sekunden>` das Wartefenster (Standard 8 s). Der Scan behauptet «kein Banner» nur, wenn die Seite stabil lud und die Schlussprüfung das Fenster samt Nachfrist ohne Banner-Hinweis durchlief. Sonst steht eine eigene Meldung: «Banner erschien nach dem Wartefenster — mit --banner-wait erhöhen», «Banner vorhanden, aber nicht über Ablehnen/Akzeptieren bedienbar» (zum Beispiel nur ein Schliessen-Kreuz) oder «Seite instabil (Reload-Schleife) — Banner nicht auswertbar».
181
-
182
- ## Auto-Fix: Grenzen und Consent-Anbindung
183
-
184
- `--auto-fix` verschiebt Tracker-Skripte hinter die Einwilligung. Das Ergebnis ist ein Vorschlag, den die Review-Stufe prüft;
185
- ein nicht verifizierter Fix macht das Gate nicht grün.
186
-
187
- - **OneTrust und Cookiebot** werden automatisch gelesen (Kategorien Statistik und Marketing); ein Hook ist nicht nötig. OneTrust wird auch ohne CDN-Marker erkannt, etwa bei selbst gehostetem SDK (`Optanon…`-Symbole, `OptanonConsent`-Cookie).
188
- - **Eigene Banner** melden die Einwilligung an die eingefügte Runtime, zum Beispiel `window.cpcgGrantConsent("analytics")` (Statistik) oder `window.cpcgGrantConsent("marketing")`, ohne Argument alle Kategorien. Der Report nennt die passenden Hooks. Ohne dieses Signal startet kein Tracker; «Ablehnen» ruft nichts auf.
189
- - **Andere Consent-Plattformen** (zum Beispiel Usercentrics, Sourcepoint, Didomi, iubenda) sind **nicht automatisch behebbar**: der Auto-Fix kann ihre Kategorie-Einwilligung nicht sicher lesen und erzeugt keinen Patch, sondern eine Anleitung zur manuellen Behebung.
190
- - **Tracker, die nicht im Quelltext stehen** (über Tag Manager, Server, CMS oder Drittanbieter geladen) bleiben «manuell zu beheben».
191
- - **LLM-gestützte Patches** sind ein Opt-in (`--llm-remediation`), nur mit OpenAI umgesetzt, und senden den Quellcode an den Anbieter. Ein API-Key allein genügt nicht und sendet nichts.
192
- - **Commit und Pull Request** (`--git-commit`, `--create-pr`) brauchen eine Lizenz; das lokale Anwenden der Patches nicht. Git läuft im Repository, in dem `--source-dir` liegt; liegt es in keinem, bleibt der Fix lokal und der Lauf meldet das ausdrücklich.
193
- - **Teilfix:** Bleiben gemeldete Tracker offen, zählt das Gate nur diese als «ungepatcht»; Score und «Passed» des Review-Agenten gelten dann ausdrücklich nur für den gepatchten Teil.
194
-
195
- ## Demo-Portal
196
-
197
- Reproduzierbare Testseite mit fünf absichtlichen Consent-Leaks
198
- (Pre-Consent-Bootstrap, Timer, Widerruf, Embeds, doppelte Events):
199
-
200
- ```bash
201
- node demo-portal/server.mjs # http://127.0.0.1:8787
202
- npx tsx scripts/scan-demo-portal.mts # voller Scan + Report
203
- npx tsx scripts/make-deluxe-report.mts # Deluxe-PDF daraus
204
- ```
205
-
206
- Die Skripte in `scripts/` sind TypeScript: `node scripts/x.mts` scheitert an den Importen, deshalb `npx tsx`.
207
-
208
- Erwartet: Gate **BLOCKED**, Szenarien «auffällig» — exakt dieselben Zahlen bei jedem Lauf.
209
-
210
- ## GitHub Action
211
-
212
- ```yaml
213
- - uses: cuonztech/privacy-chaos-gate-action@v1
214
- id: cpcg
215
- with:
216
- staging-url: ${{ vars.STAGING_URL }}
217
- license: ${{ secrets.CPCG_LICENSE }}
218
- report-pdf: true
219
- client-name: "Firma AG"
220
- ```
221
-
222
- Die Action installiert das veröffentlichte Paket `cuonztech-privacy-chaos-gate` in der Version, die in `action.yml`
223
- fest eingetragen ist (`npm install --ignore-scripts`, ohne Secrets in der Umgebung dieses Schritts), samt dem passenden
224
- Chromium (`playwright install --with-deps chromium`). Kennt die installierte CLI eine angeforderte Option nicht, gibt die Action
225
- eine Warnung aus, statt sie still wegzulassen.
226
-
227
- Scan und Konsolen-Gate laufen ohne Lizenz. `report-pdf`, `client-name`, `git-commit` und `create-pr` brauchen eine gültige Lizenz;
228
- ohne sie werden sie mit Hinweis übersprungen.
229
-
230
- | Input | Standard | Bedeutung |
231
- |-------|----------|-----------|
232
- | `staging-url` | Pflicht | Zu prüfende URL |
233
- | `license` | leer | Lizenzcode oder Kunden-Key; wird nur per Umgebungsvariable `CPCG_LICENSE` übergeben |
234
- | `fail-on-leak` | `true` | Build blockieren (Exit 1), wenn Leaks bleiben |
235
- | `fail-on-uncheckable` | `true` | Build blockieren (Exit 4), wenn Kernszenarien nicht prüfbar sind |
236
- | `strict` | `false` | Auch Hinweise (z. B. Google Fonts) blockieren das Gate |
237
- | `ignore` | leer | Befunde ausnehmen (Tracker-Name oder Host-Muster, eine Angabe je Zeile oder kommagetrennt) |
238
- | `accept-selector`, `reject-selector`, `settings-selector` | leer | Eigene CSS-Selektoren der Banner-Buttons (eine Angabe je Zeile) |
239
- | `banner-frame` | leer | Teil der URL des iframes mit dem Banner |
240
- | `max-subpages` | leer (CLI: 12) | Höchstzahl verlinkter Unterseiten, die auf Tracker geprüft werden (max. 40; mehr dauert länger) |
241
- | `banner-wait` | leer (CLI-Standard) | Sekunden, die auf ein spät erscheinendes Banner gewartet wird (1–60) |
242
- | `auto-fix` | `false` | Patches auf dem Runner erzeugen und anwenden; ein Gate ändert Dateien nicht stillschweigend |
243
- | `create-pr` | `false` | Fix als Pull Request öffnen (braucht `auto-fix: true`, `github-token` und eine Lizenz) |
244
- | `github-token` | leer | Token für `create-pr` (`contents: write`, `pull-requests: write`) |
245
- | `git-commit` | `false` | Fix in den ausgecheckten Branch committen (braucht `auto-fix: true` und eine Lizenz) |
246
- | `target-source-dir` | `.` | Quellcode-Verzeichnis für Auto-Fix |
247
- | `report-output` | `reports/privacy-chaos-report.json` | Pfad des JSON-Reports |
248
- | `report-pdf`, `client-name` | `false`, leer | Deluxe-PDF und Kundenname (Lizenz erforderlich) |
249
- | `jurisdiction` | `CH` | Rechtsraum |
250
- | `llm-remediation`, `api-key`, `llm-provider`, `llm-model` | `false`, leer, `openai`, `gpt-4o` | Optional: LLM-gestützte Patches. Nur OpenAI ist umgesetzt; der Quellcode wird dann an den Anbieter gesendet; ohne `llm-remediation: true` wirkt der Key nicht |
251
-
252
- Beispiel mit Pull Request als Ergebnis:
253
-
254
- ```yaml
255
- permissions:
256
- contents: write
257
- pull-requests: write
258
- steps:
259
- - uses: actions/checkout@v4
260
- - uses: cuonztech/privacy-chaos-gate-action@v1
261
- with:
262
- staging-url: ${{ vars.STAGING_URL }}
263
- license: ${{ secrets.CPCG_LICENSE }}
264
- auto-fix: true
265
- create-pr: true
266
- github-token: ${{ secrets.GITHUB_TOKEN }}
267
- ```
268
-
269
- Outputs: `leaks-found`, `leaks-count`, `patch-applied`, `report-path` sowie `gate_state`, `incomplete`, `uncheckable_count`,
270
- `leak_requests` und `exit_code` (geschrieben vom CLI nach `GITHUB_OUTPUT`; leer, wenn die installierte CLI sie nicht kennt).
271
- Beispiel: `if: steps.cpcg.outputs.gate_state == 'blocked'`.
272
-
273
- ## Jurisdiktion
274
-
275
- `--jurisdiction CH|EU|UK|US-CA|BR|ROW` — gleicher Befund, unterschiedliche Einordnung.
276
-
277
- > **Wichtig:** Das Gate selbst prüft in jedem Rechtsraum nach der strengen **Opt-in-Linie (EU)**: Jeder nicht-essenzielle
278
- > Tracker vor einer Einwilligung blockiert (Exit 1). Das Profil ändert nur Schweregrad und Begleittext im Bericht, nicht das
279
- > Ergebnis. Für eine reine Schweizer Seite mit Opt-out-Modell ist das Gate damit strenger als das Recht; ein «BLOCKED» ist
280
- > dort nicht automatisch ein Rechtsverstoss. Eine Bewertung nach Rechtsraum (CH und US-CA als Opt-out) ist für 0.3 geplant.
281
-
282
-
283
- - **EU:** Opt-In-Pflicht für nicht-essenzielle Tracker. Ein beobachteter Request ist ein
284
- schwerwiegender Hinweis — die Einstufung als Verstoß erfordert die Prüfung von Zweck,
285
- Technologie und Consent-Zustand. Consent Mode ist eine technische Implementierung,
286
- keine Rechtsgrundlage.
287
- - **CH:** Risiko nach revDSG-Praxis; der EDÖB verlangt insbesondere bei personalisierter
288
- Werbung eine Einwilligung.
289
- - **US-CA:** Opt-out-Modell. Ohne Prüfung von Sale/Sharing-Opt-out und GPC ist die
290
- Bewertung unvollständig.
291
- - **ROW:** Nicht bewertet — länderspezifische Prüfung erforderlich.
292
-
293
- ## Entwicklung
294
-
295
- ```bash
296
- pnpm install
297
- pnpm test # Vitest-Suite (inkl. PHP-Lizenzdienst, falls Docker läuft; action.yml-Pin und Optionsweitergabe)
298
- pnpm build
299
- pnpm typecheck:all # src + tests + scripts
300
- npx tsx scripts/e2e-cli-deluxe.mts # E2E: echte CLI gegen Demo-Portal
301
- ```
302
-
303
- Der Lizenzdienst liegt in `server/license-api/` (eigene README mit Deploy-Checkliste und Betrieb)
304
- und gehört nicht zum veröffentlichten Paket.
305
-
306
- Secrets (Stripe, Lizenz-Secret) nur über Env/KeePass — nie im Repo.
307
-
308
- ---
309
-
310
- © CuonzTech · Carlo Cuonz · [cuonztech.ch](https://cuonztech.ch) · Lizenz: siehe [LICENSE](LICENSE)
1
+ # CuonzTech Privacy Chaos Gate (CPCG)
2
+
3
+ CI-Gate für Consent-Tracker: stresst den Consent-Banner mit 16 Chaos-Szenarien, findet Requests vor der Einwilligung
4
+ und nach dem Ablehnen, bewertet sie je Rechtsraum und schlägt den Fix vor: als Pull Request für OneTrust und Cookiebot,
5
+ sonst als Anleitung. Der Report zeigt auch die Gegenrichtung (welche Tracker nach dem Akzeptieren laden); beim
6
+ Re-Scan der Vorschau (`--verify-url`) ist sie die Gegenprobe für «Fix verifiziert».
7
+
8
+ > **Hinweis:** Technisches Hilfsmittel, **keine Rechtsberatung**, keine Garantie für
9
+ > revDSG-/DSGVO-/TTDSG-Konformität. / Technical aid, **not legal advice**.
10
+
11
+ ## Installieren
12
+
13
+ Voraussetzung: Node.js 20 oder neuer.
14
+
15
+ ```bash
16
+ # 1. CLI installieren (Befehl: cpcg)
17
+ npm install -g cuonztech-privacy-chaos-gate # oder im Projekt: npm install -D cuonztech-privacy-chaos-gate
18
+
19
+ # 2. Prüf-Browser installieren (einmalig pro Rechner)
20
+ npx playwright install chromium
21
+
22
+ # 3. Ersten Scan starten (Scan und Konsolen-Gate brauchen keine Lizenz)
23
+ cpcg --url https://staging.example.com
24
+ ```
25
+
26
+ Ohne Installation: `npx cuonztech-privacy-chaos-gate --url https://staging.example.com`
27
+ (der Browser aus Schritt 2 muss trotzdem vorhanden sein; auf Linux-CI zusätzlich mit Systembibliotheken:
28
+ `npx playwright install --with-deps chromium`). Fehlt der Browser, meldet das CLI das und beendet sich mit Exit 3.
29
+
30
+ Das CLI hat keine Subcommands: alle Angaben sind Optionen von `cpcg`.
31
+
32
+ ## Lizenz und Preise
33
+
34
+ Das Paket ist proprietär und source-available (Datei `LICENSE`, ab 0.2.1; die Versionen 0.1.0 und 0.2.0 wurden unter MIT veröffentlicht und bleiben es).
35
+ Online-Kauf → Code per Mail.
36
+
37
+ ```bash
38
+ export CPCG_LICENSE="CPCG-…" # Lizenzcode (Pilot/Einmal) oder Kunden-Key CPCG-K-… (Abo)
39
+ cpcg --url https://staging.example.com --jurisdiction CH
40
+ ```
41
+
42
+ In CI die Lizenz als Secret setzen und per Umgebungsvariable `CPCG_LICENSE` übergeben, nicht als Argument.
43
+
44
+ **Frei, ohne Lizenz** (auch kommerziell und intern, auch gegen Produktiv-URLs): Scan, Konsolen-Gate (Exit-Code, JSON-Report, neutraler Evidence-Report ohne Kundenbezug), lokales `--auto-fix` ohne Commit/PR. Ziele auf `localhost`, `127.0.0.1` und `::1` sind immer frei.
45
+
46
+ **Nur mit gültiger Lizenz:**
47
+
48
+ | Funktion | Option |
49
+ |----------|--------|
50
+ | PDF-Report sowie Kundenreport (HTML/Markdown mit `--client`) | `--report-pdf`, `--client` |
51
+ | Auto-Fix als Commit oder Pull Request | `--git-commit`, `--create-pr` |
52
+
53
+ Ohne Lizenz werden diese Funktionen mit einem Hinweis übersprungen; der Exit-Code bleibt der des Gates. Exit 2 gibt es nur bei einer **ungültigen, abgelaufenen oder gesperrten** Lizenz, nicht bei fehlender Lizenz und nicht bei einem nicht erreichbaren Lizenzserver.
54
+
55
+ | Plan | Preis (netto, CHF) | Umfang |
56
+ |------|--------------------|--------|
57
+ | Pilot / Evidence | 890 einmalig | Kundenreport, Auto-Fix-PR, 1 Domain, 90 Tage |
58
+ | Pro | 69 je Domain und Monat | Kundenreport, Auto-Fix-PR (bei OneTrust/Cookiebot; sonst Anleitung), Scan und Gate frei |
59
+ | Agency | 349 je Monat | Pro für bis zu 10 Domains |
60
+
61
+ Die Domain-Anzahl beruht auf Vertrauen: das CLI zählt Domains nicht. Scans werden nicht gezählt. Monatlich kündbar. Kauf und Fragen: `support@cuonztech.ch`.
62
+
63
+ ## Was das Tool prüft
64
+
65
+ 16 Chaos-Szenarien in einem Lauf — jedes mit ehrlichem Status
66
+ (**bestanden / auffällig / nicht prüfbar**). Ein Button, der nicht gefunden wird,
67
+ wird nie zu einem grünen Häkchen.
68
+
69
+ | # | Szenario | Frage |
70
+ |---|----------|-------|
71
+ | 1 | happy-path-denied | Bleibt Tracking nach «Ablehnen» aus? |
72
+ | 2 | happy-path-accepted | Ist vor dem Klick Ruhe — und läuft erlaubtes Tracking danach an? |
73
+ | 3 | fast-click | Rennen: Link vor dem Banner geklickt, und Ablehnen sofort beim Erscheinen (ohne auf ein fertiges CMP zu warten, bei Bedarf doppelt): Geht die Ablehnung verloren, feuern danach Tracker, auch nach Neuladen? |
74
+ | 4 | state-manipulation | Tracking nach Storage-Wipe? Fragt die Seite danach erneut? Wird eine gezielt beschädigte Ablehnung als Zustimmung gelesen? |
75
+ | 5 | network-drop | Tracker trotz blockiertem CMP? Ein Banner, das schon ohne JavaScript im HTML steht (kein CMP-Skript, das ausfallen könnte), gilt als «nicht anwendbar»; ein per Skript gebautes Banner eines unbekannten Anbieters bleibt «nicht prüfbar». |
76
+ | 6 | consent-revoked | Stoppt Tracking nach Widerruf? Welche IDs bleiben liegen? |
77
+ | 7 | category-selective | Nur Statistik erlaubt — bleiben Marketing-Tracker aus? |
78
+ | 8 | consent-persistence | Überlebt die Ablehnung Reload, bis zu 3 Unterseiten und Neustart? |
79
+ | 9 | banner-ignored | Starten Tracker per Timer/Scroll ohne Klick? |
80
+ | 10 | subpage-spa | Greift der Schutz auf Unterseiten und bei SPA-Navigation? |
81
+ | 11 | cmp-failure | Tracking-Zeitfenster bei verzögerter/defekter CMP? Bei einem Banner ohne CMP-Skript im HTML «nicht anwendbar» (wie network-drop). |
82
+ | 12 | embed-leaks | Kontaktieren YouTube/Maps/Social-Embeds vor Freigabe? |
83
+ | 13 | consent-mode-v2 | Decken sich Consent-Mode-Zustände mit Cookies/Requests? Welche Signale (`gcs`/`gcd`) senden die Google-Tags vor und nach der Entscheidung: «erteilt» trotz Ablehnen, Standard «erteilt», cookielose Pings? |
84
+ | 14 | duplicate-events | Doppelte page_view-Events (Plugin + GTM parallel)? |
85
+ | 15 | mobile-usability | Sind Ablehnen/Einstellungen auf dem Handy bedienbar? |
86
+ | 16 | gpc-signal | Beachtet die Seite das Opt-out-Signal Global Privacy Control (`Sec-GPC: 1`) vor jeder Entscheidung? Vergleich mit einem Lauf ohne GPC. Werbe-Tracker trotz GPC blockieren nur nach US-CA, sonst Hinweis. |
87
+
88
+ **Tracker:** rund 245 Anbieter über 252 Tracker-Hosts, 27 präzise Host-/Pfad-Muster und 1209 Cookie-Muster
89
+ (u. a. Google Analytics und Tag Manager, Meta, TikTok, LinkedIn, Adobe, Piano, Hotjar, Criteo, The Trade Desk,
90
+ Taboola, Awin, HubSpot, Matomo), dazu CNAME-Cloaking (eigene Subdomain, die per DNS auf einen Tracker zeigt).
91
+ Fremde Hosts ohne Zuordnung listet der Report ohne Bewertung. Umfang, Quellen, Regeln gegen Fehlalarme und Messung:
92
+ `docs/TRACKER-ABDECKUNG.md`.
93
+
94
+ **CMPs:** OneTrust, Cookiebot, Usercentrics, Didomi, iubenda, Axeptio, Sourcepoint, Tealium, etracker, JustRelate,
95
+ Consentmanager, Borlabs, Klaro, CookieYes, Complianz, Cookie Script, CookieFirst, Termly, TrustArc, Osano, die
96
+ WordPress-Banner Cookie Notice, CookieLawInfo und Moove GDPR sowie cookieconsent (Open Source); erkannt zusätzlich
97
+ Quantcast Choice, CCM19 und Real Cookie Banner. Andere Banner bedient die Textklassifikation (de/en/fr/it).
98
+ Details und Messung: `docs/CMP-ABDECKUNG.md`.
99
+
100
+ ## Output: der Prüfbericht
101
+
102
+ Jeder Scan schreibt ein Evidence-Paket:
103
+
104
+ ```
105
+ reports/privacy-chaos-report.json LeakReport (maschinenlesbar), Pfad per --report-output
106
+ reports/privacy-chaos-report-evidence.md Evidence-Report (Markdown); mit Lizenz und --client als Kundenreport
107
+ reports/privacy-chaos-report-evidence.html Evidence-Report (HTML, CuonzTech-Brand); mit Lizenz und --client als Kundenreport
108
+ reports/privacy-chaos-report-deluxe.pdf Deluxe-PDF mit Deckblatt (mit --report-pdf, nur mit Lizenz)
109
+ reports/evidence/ Screenshots (portabel, relative Pfade)
110
+ ```
111
+
112
+ Die Dateinamen richten sich nach dem JSON-Pfad: `<name>.json` ergibt `<name>-evidence.md`, `<name>-evidence.html`
113
+ und `<name>-deluxe.pdf` im selben Ordner, die Screenshots liegen in `evidence/` daneben. Standard ist
114
+ `reports/privacy-chaos-report.json`; mit `--report-output reports/scan.json` heissen die Dateien `reports/scan-evidence.md` usw.
115
+
116
+ Der Report führt **Befunde** (Regelverstösse), **Hinweise** und **beobachtete Requests**
117
+ (Timelines) getrennt — und ordnet jedes Ergebnis mehreren Rechtsräumen zu
118
+ (CH, EU, UK, US-CA, BR, ROW). Keine globale Compliance-Aussage.
119
+
120
+ **Hinweise sind kein Blocker.** Rein funktionale Drittanbieter (etwa Google Fonts) und cookielose
121
+ Reichweitenmessung (etwa Plausible, Fathom) erscheinen als Hinweis und blockieren das Gate nicht. Ebenso Befunde, die nach dem gewählten Rechtsraum nur Hinweis sind
122
+ (zum Beispiel Tracker vor der Einwilligung mit `--jurisdiction CH`, Opt-out-Modell): Sie stehen mit ihrer Einordnung
123
+ in der Liste «Hinweise», nicht im Befundregister. Ein Szenario, das nur wegen solcher Hinweise auffiel, gilt als
124
+ «bestanden nach CH (opt-out)». Mit `--strict` blockieren auch Hinweise.
125
+
126
+ **Nicht prüfbar heisst: mit Grund.** Der Report führt je nicht prüfbarem Szenario den Grund in einer eigenen Liste
127
+ «Warum nicht prüfbar» (auch in der Konsole, in der Step-Summary und in `report.json` unter `gate.uncheckableReasons`).
128
+ Das Deckblatt nennt nur Prüfthemen, deren Szenarien alle tatsächlich geprüft wurden, und den Rechtsraum.
129
+
130
+ **Bot-Schutz.** Zeigt die Zielseite dem Browser eine Challenge- oder Sperrseite (etwa Cloudflare, DataDome, Akamai,
131
+ AWS WAF, Imperva, PerimeterX) oder ein Rate-Limit (HTTP 429), sind die betroffenen Szenarien nicht prüfbar. Report-Kopf,
132
+ Konsole und Step-Summary nennen das gesondert, `report.json` unter `botProtection`. Das ist kein Befund über Consent.
133
+ Skripte dieser Anbieter auf einer normal geladenen Seite (etwa Cloudflare Turnstile im Formular) lösen nichts aus.
134
+ Ein Lauf, in dem der Bot-Schutz die Kernszenarien verhindert, endet immer mit Exit 4, auch mit `--fail-on-uncheckable false`.
135
+ Abhilfe: Scan-Rechner bzw. CI-Runner beim Bot-Schutz freigeben oder eine Staging-URL ohne Bot-Schutz scannen.
136
+
137
+ **Code-Lösungen sind Muster.** Werte in `{{…}}` (etwa `{{GA4_MESS_ID}}`) sind Platzhalter, die Legende darunter sagt,
138
+ was hingehört.
139
+ Bekannte, bewusst akzeptierte Befunde lassen sich mit `--ignore` ausnehmen; sie stehen im Report weiterhin
140
+ als «ignoriert».
141
+
142
+ ## Flags
143
+
144
+ ```bash
145
+ cpcg --url https://staging.example.com \
146
+ --report-pdf \
147
+ --client "Firma AG" \
148
+ --jurisdiction CH \
149
+ --report-output reports/scan.json
150
+ ```
151
+
152
+ | Option | Wirkung |
153
+ |--------|---------|
154
+ | `--url <url>` | Ziel (Pflicht) |
155
+ | `--license <code>` | Lizenzcode oder Kunden-Key; besser per Env `CPCG_LICENSE` |
156
+ | `--jurisdiction <code>` | `CH`, `EU`, `UK`, `US-CA`, `BR` oder `ROW` |
157
+ | `--report-output <pfad>` | JSON-Report (weitere Dateien daneben) |
158
+ | `--report-pdf [boolean]` | Deluxe-PDF (Deckblatt + Evidence) — Lizenz erforderlich |
159
+ | `--client <name>` | Kundenname für Kundenreport und Deckblatt — Lizenz erforderlich |
160
+ | `--demo [boolean]` | Report als synthetische Testdaten kennzeichnen |
161
+ | `--fail-on-leak [boolean]` | Exit 1 bei Leaks (CI-Gate, Standard `true`) |
162
+ | `--fail-on-uncheckable [boolean]` | Exit 4, wenn Kernszenarien nicht prüfbar sind (Standard `true`); bei `false` Exit 0, das Ergebnis bleibt als unvollständig markiert |
163
+ | `--strict [boolean]` | Auch Hinweise (z. B. Google Fonts) blockieren das Gate (Standard `false`) |
164
+ | `--ignore <liste>` | Befunde ausnehmen: Tracker-Name oder Host-Muster, kommagetrennt oder mehrfach (`fonts.googleapis.com,*.hotjar.com`); im Report als «ignoriert». `*`/`**` und nackte TLDs (`com`, `ch`) werden **immer** abgelehnt, auch mit `--allow-broad-ignore` (zum Abschalten der Blockierung dient `--fail-on-leak false`); ganze Zonen (`*.com`, `.ch`) nur mit `--allow-broad-ignore`. Die Konsole nennt, wie viele Befunde ausgenommen wurden; ein Szenario, dessen Befunde alle ausgenommen sind, steht als «bestanden (Befunde ignoriert)» |
165
+ | `--allow-broad-ignore [boolean]` | Ganze Zonen als `--ignore`-Muster (`*.com`, `*.co.uk`, `.ch`) bewusst zulassen; sie erscheinen in Konsole und Report als Warnung. `*` und nackte TLDs bleiben abgelehnt (Standard `false`) |
166
+ | `--accept-selector <css>` | Eigener Selektor des Akzeptieren-Buttons, wenn die Erkennung ihn nicht findet (mehrfach oder kommagetrennt) |
167
+ | `--reject-selector <css>` | Eigener Selektor des Ablehnen-Buttons |
168
+ | `--settings-selector <css>` | Eigener Selektor des Einstellungen-Buttons |
169
+ | `--banner-frame <teilstring>` | Teil der URL des iframes, in dem der Banner liegt (CMP im Frame); findet der Scan keinen solchen Frame, warnt er |
170
+ | `--banner-wait <sekunden>` | Wartefenster auf einen spät erscheinenden Banner, 1 bis 60 (Standard 8; Umgebung `CPCG_BANNER_WAIT`). Mit Angabe wird das Fenster ausgeschöpft, ebenso bei `--accept-selector`/`--reject-selector`; die Schlussprüfung wartet zusätzlich eine Nachfrist |
171
+ | `--max-subpages <n>` | Obergrenze der Unterseiten-Stichprobe (Szenario `subpage-spa`), 1 bis 40 (Standard 12; Umgebung `CPCG_MAX_SUBPAGES`); das Zeitbudget wächst moderat mit (45 s bis 12, 90 s bis 24, sonst 120 s) |
172
+ | `--sitemap` | Sitemap-Tiefenlauf: `subpage-spa` prüft Unterseiten aus `robots.txt`/`sitemap.xml` (auch Index und gzip) zusammen mit den Links der Startseite, reihum nach Pfadbereich, bis `--deep-budget` verbraucht ist; `--max-subpages` gilt dann nicht (Umgebung `CPCG_SITEMAP`) |
173
+ | `--deep-budget <s>` | Zeitbudget des Tiefenlaufs für die Unterseiten, 30 bis 3600 s (Standard 300; Umgebung `CPCG_DEEP_BUDGET`) |
174
+ | `--deep-state <pfad>` | Zustandsdatei des Tiefenlaufs: der nächste Lauf besucht nur die noch offenen Unterseiten; sind alle besucht, beginnt eine neue Runde (Umgebung `CPCG_DEEP_STATE`) |
175
+ | `--baseline <pfad>` | Report (JSON) eines früheren Laufs: der Bericht nennt neue, weggefallene und unveränderte Befunde (je Regel, Tracker und Host) und geänderte Szenario-Status; warnt bei anderer Ziel-URL oder Scan-Konfiguration (Umgebung `CPCG_BASELINE`) |
176
+ | `--har` | HAR-Mitschnitt je Browser-Kontext ohne Inhalte nach `evidence/har/<Lauf>/` (Adressen, Header, Zeiten; Umgebung `CPCG_HAR`). Achtung: Die Dateien enthalten Cookies und `Set-Cookie`-Header der Zielseite und der Drittanbieter; in öffentlichen Repos nicht als Artefakt hochladen |
177
+ | `--verify-url <url>` | Geschlossener Kreis: hat die Ziel-URL Befunde, prüft CPCG zusätzlich diese URL (z. B. die Vorschau des Fix-PRs). «Fix verifiziert» (Zustand `remediated`, Exit 0) nur, wenn dort nichts leakt, die Kernszenarien und alle Szenarien mit Befund geprüft wurden und kein Bot-Schutz greift; sonst bleibt das Gate BLOCKED mit Grund (Umgebung `CPCG_VERIFY_URL`) |
178
+ | `--verify-wait <s>` | So lange auf die Erreichbarkeit der Verify-URL warten, 0 bis 1800 s (Standard 0; Umgebung `CPCG_VERIFY_WAIT`) |
179
+ | `--target-header <"Name: Wert">` | Freigabe-Header nur für die geprüfte Website (mehrfach; Umgebung `CPCG_TARGET_HEADERS`, eine Zeile je Header): z. B. eine WAF-Ausnahme gegen Bot-Schutz auf der eigenen Seite oder `x-vercel-protection-bypass: …` für eine geschützte Vorschau. Geht nur an die Origins des Ziels bzw. der Verify-URL, nie an Drittanbieter und nie hinter eine Weiterleitung auf eine fremde Origin (auch nicht an eine Domain, auf die die Seite später weiterleitet); der Wert erscheint in keinem Report und keiner HAR-Datei. Nebenwirkung: Der Scanner holt Dokumente der eigenen Origin dann selbst; der HTTP-Cache des Browsers ist aus, und die Chrome-Sperre für Anfragen ins lokale Netz ist abgeschaltet, damit Staging-Seiten im internen Netz normal laden |
180
+ | `--auto-fix [boolean]` | Patches erzeugen und im Quellverzeichnis anwenden (Standard `true` nur mit ausdrücklichem `--source-dir`) |
181
+ | `--source-dir <dir>` | Quellcode für Auto-Fix; ein nicht vorhandenes Verzeichnis ist ein Fehler (Exit 3). Für `--git-commit`/`--create-pr` gilt das Git-Repository, in dem es liegt (nicht das Arbeitsverzeichnis) |
182
+ | `--git-commit [boolean]` | Fix in den Git-Branch committen — Lizenz erforderlich |
183
+ | `--create-pr [boolean]` | Fix als Pull Request öffnen (Token in Env `GITHUB_TOKEN`, ersatzweise `--github-token`) — Lizenz erforderlich; hat Vorrang vor `--git-commit` |
184
+ | `--base-branch <branch>`, `--branch-prefix <prefix>` | Basisbranch und Branch-Präfix des Fix-PRs |
185
+ | `--llm-remediation [boolean]` | Opt-in: Quellcode darf an den LLM-Anbieter gesendet werden (Standard `false`) |
186
+ | `--allow-custom-banner-fix [boolean]` | Opt-in: Auto-Fix darf auch bei eigenem Banner Skripte ändern. Standard `false`: dann nur Anleitung (`cpcgGrantConsent()`), kein Patch |
187
+ | `--llm-provider`, `--llm-model`, `--api-key` | Nur `openai` ist umgesetzt; andere Anbieter fallen auf die Regel-Engine zurück |
188
+ | `--headless [boolean]`, `--timeout-ms <ms>` | Browser-Modus und Timeout je Szenario-Navigation |
189
+
190
+ Alle Optionen und Standardwerte: `cpcg --help`.
191
+
192
+ **Exit-Codes** (für CI): `0` ok · `1` Leaks gefunden (auch wenn ein Fix nur vorgeschlagen, aber nicht verifiziert ist) ·
193
+ `2` Lizenz **ungültig, abgelaufen oder gesperrt** (eine fehlende Lizenz oder ein nicht erreichbarer Lizenzserver ist kein Exit 2) ·
194
+ `3` Fehler (Ziel unerreichbar oder HTTP 4xx/5xx ausser 403/429, Konfiguration, Browser fehlt, falsche Argumente; bei 403/429 versucht der Browser-Scan es trotzdem, siehe Bot-Schutz) ·
195
+ `4` unvollständig, Kernszenarien nicht prüfbar.
196
+ Ein Lauf, der nichts abschliessend prüfen konnte, ist nie grün: `--fail-on-uncheckable` (Standard) macht daraus Exit 4.
197
+
198
+ ### Unterseiten-Stichprobe
199
+
200
+ Das Szenario `subpage-spa` prüft höchstens `--max-subpages` verlinkte Unterseiten (verschiedene erste Pfadsegmente zuerst, Impressum und Datenschutz zuletzt). Sind mehr Unterseiten verlinkt als geprüft wurden, steht das nicht nur im Report: Die Konsole meldet «Unterseiten: X von Y geprüft — nicht geprüfte Routen können Verstösse enthalten; --max-subpages erhöhen», `report.json` führt es unter `gate.sampling`, die Step-Summary und der Report-Kopf zeigen «PASS, aber Stichprobe», und `gate.incomplete` ist `true`. Der Exit-Code bleibt bei PASS `0` (nicht `4`).
201
+
202
+ Mit `--sitemap` meldet das Gate den Fortschritt der Runde: «Unterseiten (Sitemap-Tiefenlauf): 120 von 300 besucht, über 3 Läufe». Gezählt werden besuchte Kandidaten (auch Fehlerseiten); die Befunde eines Laufs stehen in dessen eigenem Report. Gelesen werden höchstens 25 Sitemap-Dateien, 5000 Adressen und 10 MB je Datei, nur im Zielbereich (Weiterleitungen nach aussen werden nicht verfolgt). Im CI die Zustandsdatei mit `actions/cache` zwischen den Läufen aufbewahren.
203
+
204
+ **Prüfbarkeit des Berichts:** Jeder Report enthält unter `provenance` die CPCG-Version, den Browser und die Scan-Konfiguration ohne Geheimnisse (Lizenz, Tokens, API-Keys, lokale Pfade) samt SHA-256 (`configHash`): gleicher Hash heisst gleiche Prüfung. Neben dem Report liegt `<name>-manifest.sha256` mit den Hashes aller Artefakte (Report, Markdown, HTML, PDF, Screenshots, HAR); im Report-Ordner prüft `sha256sum -c <name>-manifest.sha256`, dass nichts verändert wurde. `<name>-manifest.json` enthält dieselben Angaben mit Konfigurations-Hash und Version.
205
+
206
+ ### Banner nicht gefunden: Selektoren und Frames
207
+
208
+ Findet die Erkennung den Banner oder einen Button nicht, wird das Szenario «nicht prüfbar» (nie grün). Dann helfen die Overrides:
209
+
210
+ ```bash
211
+ cpcg --url https://staging.example.com \
212
+ --accept-selector "#meine-zustimmung" \
213
+ --reject-selector "button[data-consent=reject]" \
214
+ --settings-selector ".cookie-einstellungen" \
215
+ --banner-frame "consent.mein-cmp.example"
216
+ ```
217
+
218
+ Erscheint der Banner erst spät, verlängert `--banner-wait <sekunden>` das Wartefenster (Standard 8 s). Der Scan behauptet «kein Banner» nur, wenn die Seite stabil lud und die Schlussprüfung das Fenster samt Nachfrist ohne Banner-Hinweis durchlief. Sonst steht eine eigene Meldung: «Banner erschien nach dem Wartefenster — mit --banner-wait erhöhen», «Banner vorhanden, aber nicht über Ablehnen/Akzeptieren bedienbar» (zum Beispiel nur ein Schliessen-Kreuz) oder «Seite instabil (Reload-Schleife) — Banner nicht auswertbar».
219
+
220
+ ## Auto-Fix: Grenzen und Consent-Anbindung
221
+
222
+ `--auto-fix` verschiebt Tracker-Skripte hinter die Einwilligung. Das Ergebnis ist ein Vorschlag, den die Review-Stufe prüft;
223
+ ein nicht verifizierter Fix macht das Gate nicht grün.
224
+
225
+ - **OneTrust und Cookiebot** werden automatisch gelesen (Kategorien Statistik und Marketing); ein Hook ist nicht nötig. OneTrust wird auch ohne CDN-Marker erkannt, etwa bei selbst gehostetem SDK (`Optanon…`-Symbole, `OptanonConsent`-Cookie).
226
+ - **Eigene Banner** ändert der Auto-Fix nicht. Er gibt nur eine Anleitung aus: Der Banner ruft nach jeder Zustimmung `window.cpcgGrantConsent("analytics")` (Statistik) bzw. `window.cpcgGrantConsent("marketing")` auf, ohne Argument alle Kategorien. Erst mit `--allow-custom-banner-fix` fügt der Fix die Runtime ein. Ohne dieses Signal startet kein Tracker.
227
+ - **Andere Consent-Plattformen** (zum Beispiel Usercentrics, Sourcepoint, Didomi, iubenda) sind **nicht automatisch behebbar**: der Auto-Fix kann ihre Kategorie-Einwilligung nicht sicher lesen und erzeugt keinen Patch, sondern eine Anleitung zur manuellen Behebung.
228
+ - **Tracker, die nicht im Quelltext stehen** (über Tag Manager, Server, CMS oder Drittanbieter geladen) bleiben «manuell zu beheben».
229
+ - **LLM-gestützte Patches** sind ein Opt-in (`--llm-remediation`), nur mit OpenAI umgesetzt, und senden den Quellcode an den Anbieter. Ein API-Key allein genügt nicht und sendet nichts.
230
+ - **Commit und Pull Request** (`--git-commit`, `--create-pr`) brauchen eine Lizenz; das lokale Anwenden der Patches nicht. Git läuft im Repository, in dem `--source-dir` liegt; liegt es in keinem, bleibt der Fix lokal und der Lauf meldet das ausdrücklich.
231
+ - **Teilfix:** Bleiben gemeldete Tracker offen, zählt das Gate nur diese als «ungepatcht»; Score und «Passed» des Review-Agenten gelten dann ausdrücklich nur für den gepatchten Teil.
232
+
233
+ ## Demo-Portal
234
+
235
+ Reproduzierbare Testseite mit fünf absichtlichen Consent-Leaks
236
+ (Pre-Consent-Bootstrap, Timer, Widerruf, Embeds, doppelte Events):
237
+
238
+ ```bash
239
+ node demo-portal/server.mjs # http://127.0.0.1:8787
240
+ npx tsx scripts/scan-demo-portal.mts # voller Scan + Report
241
+ npx tsx scripts/make-deluxe-report.mts # Deluxe-PDF daraus
242
+ ```
243
+
244
+ Die Skripte in `scripts/` sind TypeScript: `node scripts/x.mts` scheitert an den Importen, deshalb `npx tsx`.
245
+
246
+ Erwartet: Gate **BLOCKED**, Szenarien «auffällig» — exakt dieselben Zahlen bei jedem Lauf.
247
+
248
+ ## GitHub Action
249
+
250
+ ```yaml
251
+ - uses: cuonztech/privacy-chaos-gate-action@v1
252
+ id: cpcg
253
+ with:
254
+ staging-url: ${{ vars.STAGING_URL }}
255
+ license: ${{ secrets.CPCG_LICENSE }}
256
+ report-pdf: true
257
+ client-name: "Firma AG"
258
+ ```
259
+
260
+ Die Action installiert das veröffentlichte Paket `cuonztech-privacy-chaos-gate` in der Version, die in `action.yml`
261
+ fest eingetragen ist (`npm install --ignore-scripts`, ohne Secrets in der Umgebung dieses Schritts), samt dem passenden
262
+ Chromium (`playwright install --with-deps chromium`). Kennt die installierte CLI eine angeforderte Option nicht, gibt die Action
263
+ eine Warnung aus, statt sie still wegzulassen.
264
+
265
+ Scan und Konsolen-Gate laufen ohne Lizenz. `report-pdf`, `client-name`, `git-commit` und `create-pr` brauchen eine gültige Lizenz;
266
+ ohne sie werden sie mit Hinweis übersprungen.
267
+
268
+ | Input | Standard | Bedeutung |
269
+ |-------|----------|-----------|
270
+ | `staging-url` | Pflicht | Zu prüfende URL |
271
+ | `license` | leer | Lizenzcode oder Kunden-Key; wird nur per Umgebungsvariable `CPCG_LICENSE` übergeben |
272
+ | `fail-on-leak` | `true` | Build blockieren (Exit 1), wenn Leaks bleiben |
273
+ | `fail-on-uncheckable` | `true` | Build blockieren (Exit 4), wenn Kernszenarien nicht prüfbar sind |
274
+ | `strict` | `false` | Auch Hinweise (z. B. Google Fonts) blockieren das Gate |
275
+ | `ignore` | leer | Befunde ausnehmen (Tracker-Name oder Host-Muster, eine Angabe je Zeile oder kommagetrennt) |
276
+ | `accept-selector`, `reject-selector`, `settings-selector` | leer | Eigene CSS-Selektoren der Banner-Buttons (eine Angabe je Zeile) |
277
+ | `banner-frame` | leer | Teil der URL des iframes mit dem Banner |
278
+ | `max-subpages` | leer (CLI: 12) | Höchstzahl verlinkter Unterseiten, die auf Tracker geprüft werden (max. 40; mehr dauert länger) |
279
+ | `sitemap` | `false` | Sitemap-Tiefenlauf (siehe `--sitemap`) |
280
+ | `deep-budget` | leer (CLI: 300) | Zeitbudget des Tiefenlaufs in Sekunden (30 bis 3600) |
281
+ | `deep-state` | leer | Zustandsdatei der Wiederaufnahme (mit `actions/cache` aufbewahren) |
282
+ | `baseline-report` | leer | Report-JSON eines früheren Laufs für den Vergleich (z. B. per `actions/cache` oder `download-artifact` bereitstellen) |
283
+ | `har` | `false` | HAR-Mitschnitt ohne Inhalte nach `evidence/har/` |
284
+ | `verify-url` | leer | Vorschau-URL des Fixes für den geschlossenen Kreis (siehe `--verify-url`) |
285
+ | `verify-wait` | leer (CLI: 0) | Sekunden Wartezeit, bis die Verify-URL erreichbar ist (0 bis 1800) |
286
+ | `target-headers` | leer | Freigabe-Header für die eigene Website, eine Zeile `Name: Wert` je Header, aus Secrets (z. B. `${{ secrets.CPCG_TARGET_HEADERS }}`); nur per Umgebung übergeben, nie als Argument |
287
+ | `banner-wait` | leer (CLI-Standard) | Sekunden, die auf ein spät erscheinendes Banner gewartet wird (1–60) |
288
+ | `auto-fix` | `false` | Patches auf dem Runner erzeugen und anwenden; ein Gate ändert Dateien nicht stillschweigend |
289
+ | `create-pr` | `false` | Fix als Pull Request öffnen (braucht `auto-fix: true`, `github-token` und eine Lizenz) |
290
+ | `github-token` | leer | Token für `create-pr` (`contents: write`, `pull-requests: write`) |
291
+ | `git-commit` | `false` | Fix in den ausgecheckten Branch committen (braucht `auto-fix: true` und eine Lizenz) |
292
+ | `target-source-dir` | `.` | Quellcode-Verzeichnis für Auto-Fix |
293
+ | `report-output` | `reports/privacy-chaos-report.json` | Pfad des JSON-Reports |
294
+ | `report-pdf`, `client-name` | `false`, leer | Deluxe-PDF und Kundenname (Lizenz erforderlich) |
295
+ | `jurisdiction` | `CH,EU` | Rechtsräume, deren Regeln das Gate entscheiden (kommagetrennt, strengster gewinnt; siehe «Jurisdiktion») |
296
+ | `llm-remediation`, `api-key`, `llm-provider`, `llm-model` | `false`, leer, `openai`, `gpt-4o` | Optional: LLM-gestützte Patches. Nur OpenAI ist umgesetzt; der Quellcode wird dann an den Anbieter gesendet; ohne `llm-remediation: true` wirkt der Key nicht |
297
+
298
+ Beispiel mit Pull Request als Ergebnis:
299
+
300
+ ```yaml
301
+ permissions:
302
+ contents: write
303
+ pull-requests: write
304
+ steps:
305
+ - uses: actions/checkout@v4
306
+ - uses: cuonztech/privacy-chaos-gate-action@v1
307
+ with:
308
+ staging-url: ${{ vars.STAGING_URL }}
309
+ license: ${{ secrets.CPCG_LICENSE }}
310
+ auto-fix: true
311
+ create-pr: true
312
+ github-token: ${{ secrets.GITHUB_TOKEN }}
313
+ ```
314
+
315
+ Outputs: `leaks-found`, `leaks-count`, `patch-applied`, `report-path` sowie `gate_state`, `incomplete`, `uncheckable_count`,
316
+ `leak_requests`, `verified` (`true`/`false` nach einem Re-Scan, sonst leer) und `exit_code` (geschrieben vom CLI nach `GITHUB_OUTPUT`; leer, wenn die installierte CLI sie nicht kennt).
317
+ Beispiel: `if: steps.cpcg.outputs.gate_state == 'blocked'`.
318
+
319
+ Geschlossener Kreis: Nach dem Deployment der Vorschau des Fix-PRs prüft ein zweiter Workflow die Vorschau. Hat die Ziel-URL
320
+ (Produktion) noch Befunde und ist die Vorschau sauber und vollständig geprüft, meldet das Gate «PASS (Fix auf der Verify-URL
321
+ … verifiziert; die Ziel-URL leakt, bis der Fix deployed ist)» und `verified=true`. Leakt die Vorschau weiter, sind dort
322
+ Szenarien mit Befund nicht prüfbar oder ist sie nicht erreichbar, bleibt das Gate BLOCKED mit Grund (`verified=false`).
323
+
324
+ ```yaml
325
+ # Vercel meldet fertige Builds als repository_dispatch (Workflow-Datei muss auf dem Standard-Branch liegen).
326
+ # Im Vercel-Projekt muss die Meldung eingeschaltet sein (gitProviderOptions.disableRepositoryDispatchEvents = false).
327
+ # Andere Hoster, die GitHub-Deployments anlegen: «on: deployment_status» und deployment_status.target_url.
328
+ on:
329
+ repository_dispatch:
330
+ types: [vercel.deployment.success]
331
+ jobs:
332
+ verify:
333
+ if: github.event.client_payload.environment != 'production'
334
+ runs-on: ubuntu-24.04
335
+ steps:
336
+ - uses: cuonztech/privacy-chaos-gate-action@v1
337
+ id: cpcg
338
+ with:
339
+ staging-url: ${{ vars.PRODUCTION_URL }}
340
+ verify-url: ${{ github.event.client_payload.url }}
341
+ verify-wait: 120
342
+ ```
343
+
344
+ «Verifiziert» verlangt ausserdem die Gegenprobe: Die Tracker aus den Befunden müssen auf der Vorschau nach dem Akzeptieren laden
345
+ (eine Vorschau ganz ohne Tracking, etwa ohne Mess-ID, belegt keinen Fix), und Unterseiten mit Befund werden auf der Vorschau gezielt
346
+ erneut besucht. Ein Szenario, das auf der Vorschau nur an einer Grenze der Seite scheitert, die schon auf der Produktion galt
347
+ (eingebettetes Banner ohne CMP-Skript, keine verlinkte Unterseite), zählt als nachgeprüft: die Befunde stammen dann aus den Teilen,
348
+ die der Re-Scan wiederholt hat. Eine neue Grenze auf der Vorschau zählt nicht. Exit 0 bezieht sich dabei nur auf die Vorschau: Die Produktion leakt, bis der Fix deployed ist. `verify-url` deshalb
349
+ nur im Workflow der Vorschau setzen, nicht dauerhaft im Produktions-Gate.
350
+
351
+ Ist die Vorschau per Zugangsschutz gesperrt (z. B. Vercel Deployment Protection), gibt `target-headers` den Freigabe-Header mit
352
+ (bei Vercel «Protection Bypass for Automation»: `x-vercel-protection-bypass: <Secret>`). Ohne Freigabe sieht der Scanner nur die Anmeldeseite;
353
+ die Szenarien sind dann nicht prüfbar und der Fix gilt nicht als verifiziert. Die Vorschau muss für den Runner erreichbar sein.
354
+
355
+ ## Jurisdiktion
356
+
357
+ `--jurisdiction` nimmt einen oder mehrere Rechtsräume, kommagetrennt (`CH | EU | UK | US-CA | BR | ROW`, Standard `CH,EU`,
358
+ in der Action ebenso). Der Rechtsraum entscheidet, was das Gate blockiert; gewählt mehrere, gewinnt der strengste. Was nach
359
+ einem Rechtsraum nicht blockiert, steht als Hinweis mit Begründung im Report. `--strict` lässt auch Hinweise blockieren.
360
+
361
+ | Regel (Befund) | EU, UK, BR (opt-in) | CH, US-CA (opt-out) | ROW |
362
+ |---|---|---|---|
363
+ | Tracker vor der Einwilligung (`pre-consent-tracking`), auch ohne gespeicherte Entscheidung nach gelöschtem Speicher (`tracking-without-consent`) | blockiert (funktionale Drittanbieter und cookielose Reichweitenmessung: Hinweis) | Hinweis | Hinweis |
364
+ | Tracker nach Ablehnen, auch nach beschädigter gespeicherter Ablehnung oder verlorenem Klick-Rennen mit registrierter Ablehnung (`tracking-after-deny`) | blockiert | blockiert | Hinweis |
365
+ | Keine Ablehnen-Möglichkeit (`no-reject-option`) | blockiert | blockiert | Hinweis |
366
+ | Werbe-Tracker trotz Global Privacy Control (`gpc-ignored`) | Hinweis | US-CA: blockiert (nur Werbung); CH: Hinweis | Hinweis |
367
+ | Nach Ablehnen meldet der IAB-TCF-String Zustimmung zu Zwecken (`tcf-consent-mismatch`) | blockiert | blockiert | Hinweis |
368
+ | Tracker-Request nach Ablehnen, dessen IAB-TCF-String die Ablehnung trägt (weder Zustimmung noch berechtigtes Interesse) | Hinweis (mit `--strict` blockierend) | Hinweis | Hinweis |
369
+ | Tracker-Request nach Ablehnen, dessen IAB-TCF-String berechtigtes Interesse beansprucht (Anbieter darf ohne Einwilligung verarbeiten) | blockiert (`tracking-after-deny`, mit Angabe) | blockiert | Hinweis |
370
+ | Übrige Regeln (Widerruf, abgelehnte Kategorie, Consent-Mode-Widerspruch, doppelte Events, Embeds) | blockiert | blockiert | Hinweis |
371
+
372
+ Einordnung, keine Rechtsberatung: Die Opt-out-Linie für CH (Information und wirksame Ablehnen-Möglichkeit; für
373
+ personalisierte Werbung und Profiling ist auch dort meist eine Einwilligung nötig) und US-CA sowie die Wertung von GPC
374
+ sind offene Fragen an die Anwaltsprüfung (`docs/LICENSE-PRUEFHINWEISE.md`, Punkte 13 und 16). Für Seiten mit Publikum
375
+ aus der EU gehört `EU` in die Liste.
376
+
377
+ ## Entwicklung
378
+
379
+ ```bash
380
+ pnpm install
381
+ pnpm test # Vitest-Suite (inkl. PHP-Lizenzdienst, falls Docker läuft; action.yml-Pin und Optionsweitergabe)
382
+ pnpm build
383
+ pnpm typecheck:all # src + tests + scripts
384
+ npx tsx scripts/e2e-cli-deluxe.mts # E2E: echte CLI gegen Demo-Portal
385
+ ```
386
+
387
+ Der Lizenzdienst liegt in `server/license-api/` (eigene README mit Deploy-Checkliste und Betrieb)
388
+ und gehört nicht zum veröffentlichten Paket.
389
+
390
+ Secrets (Stripe, Lizenz-Secret) nur über Env/KeePass — nie im Repo.
391
+
392
+ ---
393
+
394
+ © CuonzTech · Carlo Cuonz · [cuonztech.ch](https://cuonztech.ch) · Lizenz: siehe [LICENSE](LICENSE)