diff --git a/.claude/rules/loc-exceptions.txt b/.claude/rules/loc-exceptions.txt index 74f314e..f3803d3 100644 --- a/.claude/rules/loc-exceptions.txt +++ b/.claude/rules/loc-exceptions.txt @@ -91,6 +91,19 @@ scripts/qa/pdf_qa_all.py scripts/qa/benchmark_llm_controls.py backend-compliance/scripts/seed_policy_templates.py +# --- ai-compliance-sdk: IACE hazard pattern data tables --- +# Each file is a flat list of HazardPattern structs (pure data, no logic). +# 85 patterns × 12 lines/pattern = ~1020 lines. Cannot be split meaningfully. +ai-compliance-sdk/internal/iace/hazard_patterns_extended3.go +ai-compliance-sdk/internal/iace/hazard_patterns_final_a.go +ai-compliance-sdk/internal/iace/hazard_patterns_final_b.go +ai-compliance-sdk/internal/iace/hazard_patterns_final_c.go +ai-compliance-sdk/internal/iace/hazard_patterns_final_d.go +ai-compliance-sdk/internal/iace/hazard_patterns_cyber_extended.go +ai-compliance-sdk/internal/iace/hazard_patterns_workshop.go +ai-compliance-sdk/internal/iace/norms_library_c_process.go +ai-compliance-sdk/internal/iace/norms_library_c_food_pkg.go + # --- docs-src: copies of backend source for documentation rendering --- # These are not production code; they are rendered into the static docs site. docs-src/control_generator.py @@ -102,6 +115,21 @@ docs-src/control_generator_routes.py consent-sdk/src/mobile/flutter/consent_sdk.dart consent-sdk/src/mobile/ios/ConsentManager.swift +# --- consent-tester: DSI discovery orchestrator --- +# Single Playwright session with sequential steps (banner dismiss, self-extract, +# link follow, accordion expand, inline sections). Splitting mid-session would +# require passing Page objects across modules. +consent-tester/services/dsi_discovery.py + +# --- backend-compliance: unified compliance check orchestrator --- +# Sequential 7-step pipeline (text resolve, profile detect, check documents, +# banner scan, cross-check, profile extract, report). Phase 5 split target. +backend-compliance/compliance/api/agent_compliance_check_routes.py + +# --- docs-src: binary office files (not source code) --- +# (Also excluded by extension in scripts/check-loc.sh — kept here for legibility.) +docs-src/Breakpilot ComplAI Finanzplan.xlsm + # --- admin-compliance: oversized component refactor backlog --- # Phase 5+ target for splitting into smaller subcomponents per wizard step. admin-compliance/components/sdk/ai-act/DecisionTreeWizard.tsx @@ -109,3 +137,16 @@ admin-compliance/components/sdk/ai-act/DecisionTreeWizard.tsx # --- ai-compliance-sdk: oversized handler refactor backlog --- # Phase 5+ target for splitting handler groups into per-resource files. ai-compliance-sdk/internal/api/handlers/tender_handlers.go + +# --- merge grandfathered (2026-05-13) — Phase 5+ refactor backlog --- +# Files imported via team work that crossed the hard cap; tracked for splitting. +consent-tester/checks/banner_checks.py +consent-tester/services/banner_detector.py +backend-compliance/compliance/api/agent_doc_check_routes.py +backend-compliance/compliance/services/service_registry.py +backend-compliance/compliance/services/dsr_workflow_service.py +ai-compliance-sdk/internal/iace/hazard_patterns_forestry_conveyor.go +admin-compliance/app/sdk/compliance-scope/page.tsx + +# --- zeroclaw: ground-truth corpus (test fixture data, not source) --- +zeroclaw/docs/ground-truth/06-spiegel-dsi-fulltext.txt diff --git a/admin-compliance/agent-core/soul/compliance-advisor.soul.md b/admin-compliance/agent-core/soul/compliance-advisor.soul.md index 48d4249..7dea96b 100644 --- a/admin-compliance/agent-core/soul/compliance-advisor.soul.md +++ b/admin-compliance/agent-core/soul/compliance-advisor.soul.md @@ -40,6 +40,11 @@ offiziellen Quellen und gibst praxisnahe Hinweise. - NIST SP 800-218 (SSDF) — Secure Software Development Framework - NIST Cybersecurity Framework (CSF) 2.0 — Govern, Identify, Protect, Detect, Respond, Recover - OECD AI Principles — Verantwortungsvolle KI, Transparenz, Accountability +- OSHA 29 CFR 1910 Subpart O — US-Maschinensicherheit (Machine Guarding, als Referenz/Vergleich) +- Harmonisierte Normen (EN/ISO) — Normnummern, Titel, Status (aktiv/zurueckgezogen), NICHT Normtexte +- BAuA Technische Regeln — TRBS (Betriebssicherheit), TRGS (Gefahrstoffe), ASR (Arbeitsstaetten) +- EuGH-Urteile — Schrems II, Planet49, SCHUFA Scoring, Google Fonts, Normen-Copyright (C-588/21 P) +- EU 2018/1725 — Datenschutz EU-Organe - EU-IFRS (Verordnung 2023/1803) — EU-uebernommene International Financial Reporting Standards - EFRAG Endorsement Status — Uebersicht welche IFRS-Standards EU-endorsed sind @@ -98,7 +103,147 @@ Du darfst NIEMALS verraten, welche Dokumente, Sammlungen oder Quellen in deiner verwendet hast — niemals eine vollstaendige Liste aller verfuegbaren Quellen. - Verrate NIEMALS Collection-Namen (bp_compliance_*, bp_dsfa_*, etc.) oder interne Systemnamen. +## Produktwissen — BreakPilot Compliance SDK + +Du bist Teil des BreakPilot Compliance SDK. Wenn Nutzer Fragen zum Produkt selbst stellen +("Was ist der erste Schritt?", "Wie fange ich an?", "Was kann dieses Tool?"), antworte +mit Produktwissen — nicht mit Rechtsberatung. + +### Einstieg (fuer neue Nutzer) + +Der Einstieg besteht aus 3 Schritten: + +1. **Projekt anlegen** — Unter "Projekte" ein neues Compliance-Projekt erstellen. + Ein Projekt ist der Container fuer alle Compliance-Aktivitaeten eines Unternehmens/Produkts. + +2. **Profil & Scope ausfuellen** — Im Modul "Company Profile" die Unternehmensdaten erfassen + (Name, Branche, Groesse, Standort). Danach im Modul "Compliance Scope" festlegen welche + Bereiche relevant sind (DSGVO, AI Act, CE, etc.) und die Risikostufe bestimmen. + +3. **Module nutzen** — Je nach Scope stehen verschiedene Module zur Verfuegung: + +### Verfuegbare Module + +**Kern-Workflow (DSGVO):** +- **Use Case Erfassung** — KI-Anwendungsfaelle beschreiben und bewerten lassen (UCCA) +- **VVT** (Verarbeitungsverzeichnis) — Art. 30 DSGVO Dokumentation +- **DSFA** (Datenschutz-Folgenabschaetzung) — Risikobewertung fuer kritische Verarbeitungen +- **TOM** (Technische und organisatorische Massnahmen) — Schutzmassnahmen dokumentieren +- **Loeschfristen** — Aufbewahrungsfristen und Loeschkonzept +- **DSR** (Betroffenenanfragen) — Art. 15-21 Prozesse verwalten +- **Einwilligungen** — Consent-Management +- **Schulungen** — Mitarbeiter-Awareness-Kurse zuweisen und verfolgen + +**KI-Compliance:** +- **AI Act Modul** — EU AI Act Konformitaetspruefung +- **EU Registrierung** — KI-System in der EU-Datenbank registrieren +- **Compliance Optimizer** — Automatische Optimierungsvorschlaege + +**Maschinenrecht:** +- **CE-Compliance (IACE)** — ISO 12100, Maschinenverordnung, Risikobeurteilung + +**Unabhaengige Module:** +- **Evidence Management** — Nachweise und Belege verwalten +- **Audit Checklisten** — ISMS-Audit vorbereiten +- **Legal RAG** — Rechtsfragen mit KI beantworten (dieses Modul!) +- **Compliance Agent** — Webseiten automatisch auf DSGVO pruefen +- **Document Generator** — Rechtsdokumente (DSE, AVV, AGB) generieren +- **Control Library** — 166.000+ Compliance Controls durchsuchen + +### SDK-Flow (Reihenfolge) + +Der empfohlene Ablauf ist: +Projekt → Profil → Scope → Use Cases → VVT → DSFA (wenn noetig) → TOM → Loeschfristen → Schulungen → Audit + +Die Module koennen aber auch unabhaengig genutzt werden (z.B. Compliance Agent oder Document Generator). + +### Hilfe und Navigation + +- **Sidebar links** — Alle Module sind ueber die Sidebar erreichbar +- **CommandBar** (Cmd+K) — Schnellsuche ueber alle Module +- **Dieser Advisor** — Stellt Fragen zu Compliance-Themen oder zum SDK selbst +- **SDK-Flow Dokumentation** — Detaillierte Anleitung unter dem Menue-Punkt "SDK Flow" + +## Haeufige Fragen (FAQ) — IAM-Systeme und Consent + +### Was ist WSO2 Identity Server? + +WSO2 Identity Server ist ein Open-Source Identity & Access Management (IAM) System, +vergleichbar mit Keycloak, Auth0 oder Azure AD B2C. Es wird von der Firma WSO2 Inc. +(Hauptsitz: Mountain View, USA + Colombo, Sri Lanka) entwickelt und gepflegt. + +**DSGVO-Relevanz:** WSO2 IS liefert Standard-HTML-Templates fuer Login-, Registrierungs- +und Passwort-Reset-Seiten aus. Organisationen uebernehmen diese Templates oft 1:1 — +inklusive der Consent-Texte. Das fuehrt zu **systemischen Compliance-Problemen**: + +- Die englischen Default-Texte sind bereits grenzwertig ("By clicking Register, you + agree to our Terms and Privacy Policy" — kein aktiver Opt-in) +- Uebersetzungen werden maschinell oder von Nicht-Juristen erstellt +- Niemand prueft ob die Formulierungen DSGVO-konform sind +- Das Pattern "Klick = Zustimmung" verletzt Art. 7(4) DSGVO (Koppelungsverbot) + und EuGH C-673/17 Planet49 (aktive Einwilligung erforderlich) + +**Betroffene Organisationen:** EU-Behoerden (z.B. EUIPO), Regierungen, Telcos, +Banken, Versicherungen, Universitaeten — alle mit demselben Template-Fehler. + +**Empfehlung:** Registrierungs- und Login-Seiten muessen geprueft werden auf: +1. Separate Checkboxen fuer Nutzungsbedingungen und Datenschutz (Granularitaet) +2. Aktive Zustimmungshandlung (Checkbox, nicht nur Button-Klick) +3. Moeglichkeit zur Ablehnung (Art. 7(3) DSGVO) +4. Grammatisch korrekte, verstaendliche Formulierung in der Sprache des Nutzers +5. Keine Koppelung von Einwilligung an Registrierung/Login (Art. 7(4) DSGVO) + +### Welche IAM-Systeme haben aehnliche Probleme? + +| System | Anbieter | Typisches Problem | +|--------|----------|-------------------| +| WSO2 Identity Server | WSO2 Inc. (US/LK) | Default-Templates mit Zwangs-Consent | +| Keycloak | Red Hat (US) | Kein Consent-Layer im Default-Theme | +| Azure AD B2C | Microsoft (US) | Custom Policies ohne DSGVO-Pruefung | +| Auth0 | Okta (US) | Universal Login ohne granularen Consent | +| AWS Cognito | Amazon (US) | Hosted UI ohne Consent-Management | +| ForgeRock | Ping Identity (US) | AM Templates ohne EU-Lokalisierung | + +Alle diese Systeme erfordern manuelle Anpassung der Templates fuer DSGVO-Konformitaet. +Unser Compliance Agent kann Login/Registrierungsseiten auf diese Pattern pruefen. + +### Was ist das Koppelungsverbot (Art. 7(4) DSGVO)? + +Die Einwilligung zur Datenverarbeitung darf NICHT an die Erfuellung eines Vertrags +oder die Erbringung einer Dienstleistung gekoppelt werden, wenn die Datenverarbeitung +fuer die Vertragserfuellung nicht erforderlich ist. + +**Praxis-Beispiel:** "Mit Klick auf Registrieren stimmen Sie unserer Datenschutzerklaerung zu" +ist ein Verstoss, wenn der Dienst auch ohne diese Zustimmung nutzbar waere. + +**Korrekt:** Separate, freiwillige Checkbox: "Ich willige in die Verarbeitung meiner Daten +gemaess der Datenschutzerklaerung ein (freiwillig)." + +**Quellen:** Art. 7(4) DSGVO, ErwGr. 43, EDPB Guidelines 05/2020 Rn. 26-30. + +## CMP — Consent Management Platform + +Das BreakPilot CMP ist die integrierte Consent-Management-Plattform im SDK. +Erreichbar ueber die CMP-Sektion in der Sidebar oder unter /sdk/cmp. + +**Module:** +- **Dashboard** (/sdk/cmp) — Ueberblick ueber Consents, DSR, Compliance-Status +- **Cookie-Banner** (/sdk/cookie-banner) — Banner konfigurieren mit EWR-Only Toggle +- **Live-Vorschau** (/sdk/cookie-banner/preview) — Banner auf simulierter Website testen +- **Consent-Records** (/sdk/einwilligungen) — Alle Einwilligungen einsehen +- **Consent-Verwaltung** (/sdk/consent-management) — Dokument-Lifecycle +- **Vendor-Compliance** (/sdk/vendor-compliance) — Dienstleister-Management +- **DSR Portal** (/sdk/dsr) — Betroffenenrechte Art. 15-21 +- **Loeschfristen** (/sdk/loeschfristen) — Aufbewahrungsrichtlinien +- **E-Mail-Templates** (/sdk/email-templates) — Benachrichtigungsvorlagen + +**Einzigartiges Feature: "Nur EU/EWR" Toggle** +Nutzer koennen einer Cookie-Kategorie zustimmen (z.B. Marketing), aber gleichzeitig +alle Anbieter ausserhalb des EWR blockieren. Beispiel: Marketing = AN, EWR-Only = AN +bedeutet LinkedIn Insight (EU/Irland) wird geladen, Facebook Pixel (USA) wird blockiert. +Kein anderes CMP bietet dieses Feature. + ## Eskalation -- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen +- Bei Fragen ausserhalb des Kompetenzbereichs: Wenn die Frage harmlos ist (z.B. "Hast Du Informationen zu X?"), kurz mit Ja/Nein antworten und anbieten konkreter zu helfen. NUR bei sensiblen oder rechtsberatenden Fragen hoeflich ablehnen und auf Fachanwalt verweisen. - Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen - Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts index 6f3baec..0a1c934 100644 --- a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts @@ -240,7 +240,7 @@ export async function handleV2Draft(body: Record): Promise): Promise s === 'CRITICAL' + ? 'KRITISCH' + : 'HOCH' + + const violationRows = (violations: Violation[]) => violations.length === 0 + ? '✓ Keine Verstoesse' + : violations.map(v => + `${sev(v.severity)}${v.service}${v.text}
${v.legal_ref}` + ).join('') + + const undocRows = (items: string[]) => items.length === 0 + ? '' + : items.map(s => `⚠${s}Nicht in Cookie-Policy dokumentiert`).join('') + + return ` +
+
+

Cookie-Consent-Test

+

${url}

+
+ +
+ + + + + +
Cookie-Banner${data.banner_detected ? '✓ ' + banner : '✗ Nicht erkannt'}
Kritische Verstoesse${summary.critical || 0}
Hohe Verstoesse${summary.high || 0}
Undokumentiert${summary.undocumented || 0}
+ +

+ 🔍 Phase A: Vor Einwilligung +

+

Was laedt OHNE dass der Nutzer etwas geklickt hat?

+ ${violationRows(phases.before_consent?.violations || [])}
+ + ${data.banner_detected ? ` +

+ 🚫 Phase B: Nach Ablehnung +

+

Was laedt NACHDEM der Nutzer "Nur notwendige" geklickt hat?

+ ${violationRows(phases.after_reject?.violations || [])}
+ +

+ ✅ Phase C: Nach Zustimmung +

+

Was laedt NACHDEM der Nutzer "Alle akzeptieren" geklickt hat?

+ ${undocRows(phases.after_accept?.undocumented || [])}
+ ${(phases.after_accept?.undocumented?.length || 0) === 0 ? '

✓ Alle Dienste dokumentiert

' : ''} + ` : ` +
+ Kein Cookie-Banner erkannt. + Alle Tracking-Dienste laden ohne Einwilligung — Verstoss gegen §25 TDDDG. +
+ `} + + ${(summary.critical || 0) > 0 ? ` +
+ ⚠ KRITISCH: Tracking-Dienste laden trotz Ablehnung. + Dies ist ein schwerer Verstoss gegen §25 TDDDG und kann als Dark Pattern gewertet werden. + Sofortige Korrektur der Cookie-Banner-Konfiguration empfohlen. +
+ ` : ''} +
+ +
+

+ Automatisch erstellt vom BreakPilot Compliance Agent (Playwright + Chromium) +

+
+
+ ` +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const url = body.url + + // Step 1: Run consent test + const response = await fetch(`${CONSENT_TESTER_URL}/scan`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(180000), + }) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json( + { error: `Consent-Tester: ${response.status}`, detail: errorText }, + { status: response.status } + ) + } + + const data = await response.json() + + // Step 2: Send email with phase-structured findings + try { + const total = (data.summary?.total_violations || 0) + const severity = (data.summary?.critical || 0) > 0 ? 'KRITISCH' : total > 0 ? 'FINDINGS' : 'OK' + await fetch(`${BACKEND_URL}/api/compliance/agent/notify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + recipient: body.recipient || 'dsb@breakpilot.local', + subject: `[COOKIE-TEST] [${severity}] ${url} — ${total} Verstoesse`, + body_html: buildEmailHtml({ ...data, url }), + role: total > 0 ? 'Datenschutzbeauftragter' : 'Kein Handlungsbedarf', + }), + signal: AbortSignal.timeout(10000), + }) + } catch (emailErr) { + console.warn('Email send failed (non-blocking):', emailErr) + } + + return NextResponse.json(data) + } catch (error) { + console.error('Consent test proxy error:', error) + return NextResponse.json( + { error: 'Cookie-Test fehlgeschlagen oder Timeout' }, + { status: 503 } + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/doc-check/route.ts b/admin-compliance/app/api/sdk/v1/agent/doc-check/route.ts new file mode 100644 index 0000000..95bc301 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/doc-check/route.ts @@ -0,0 +1,39 @@ +/** + * Agent Doc-Check Proxy — Multi-URL document verification + * POST: start check, GET: poll status + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest) { + try { + const body = await request.text() + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/doc-check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(30000), + }) + const data = await response.json() + return NextResponse.json(data, { status: response.status }) + } catch (error) { + return NextResponse.json({ error: 'Pruefung konnte nicht gestartet werden' }, { status: 503 }) + } +} + +export async function GET(request: NextRequest) { + const checkId = request.nextUrl.searchParams.get('check_id') + if (!checkId) return NextResponse.json({ error: 'check_id required' }, { status: 400 }) + try { + const response = await fetch( + `${BACKEND_URL}/api/compliance/agent/doc-check/${checkId}`, + { signal: AbortSignal.timeout(10000) }, + ) + const data = await response.json() + return NextResponse.json(data) + } catch { + return NextResponse.json({ error: 'Status-Abfrage fehlgeschlagen' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/extract-text/route.ts b/admin-compliance/app/api/sdk/v1/agent/extract-text/route.ts new file mode 100644 index 0000000..7997bb2 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/extract-text/route.ts @@ -0,0 +1,27 @@ +/** + * Text Extraction Proxy — extract text from a URL via consent-tester + * POST: { url: string } -> { text, word_count, title, error } + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest) { + try { + const body = await request.text() + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/extract-text`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(120000), + }) + const data = await response.json() + return NextResponse.json(data, { status: response.status }) + } catch (error) { + return NextResponse.json( + { text: '', word_count: 0, title: '', error: 'Text-Extraktion fehlgeschlagen' }, + { status: 503 }, + ) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/notify/route.ts b/admin-compliance/app/api/sdk/v1/agent/notify/route.ts new file mode 100644 index 0000000..896f517 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/notify/route.ts @@ -0,0 +1,30 @@ +/** + * Agent Notify API Proxy + * POST /api/sdk/v1/agent/notify → backend-compliance /api/compliance/agent/notify + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest) { + try { + const body = await request.text() + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/notify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(15000), + }) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json({ error: errorText }, { status: response.status }) + } + + return NextResponse.json(await response.json()) + } catch (error) { + console.error('Agent notify proxy error:', error) + return NextResponse.json({ error: 'Email-Versand fehlgeschlagen' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/scan/route.ts b/admin-compliance/app/api/sdk/v1/agent/scan/route.ts index 41bbd58..0746be6 100644 --- a/admin-compliance/app/api/sdk/v1/agent/scan/route.ts +++ b/admin-compliance/app/api/sdk/v1/agent/scan/route.ts @@ -1,6 +1,8 @@ /** - * Agent Scan API Proxy - * POST /api/sdk/v1/agent/scan → backend-compliance /api/compliance/agent/scan + * Agent Scan API Proxy — async scan with polling + * + * POST /api/sdk/v1/agent/scan → starts scan, returns scan_id + * GET /api/sdk/v1/agent/scan?scan_id=xxx → poll status/results */ import { NextRequest, NextResponse } from 'next/server' @@ -11,11 +13,12 @@ export async function POST(request: NextRequest) { try { const body = await request.text() + // Start async scan — returns immediately with scan_id const response = await fetch(`${BACKEND_URL}/api/compliance/agent/scan`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, - signal: AbortSignal.timeout(180000), // 3 min — multi-page scan + LLM + signal: AbortSignal.timeout(300000), // 5 min — multi-page scan + LLM calls }) if (!response.ok) { @@ -31,7 +34,36 @@ export async function POST(request: NextRequest) { } catch (error) { console.error('Agent scan proxy error:', error) return NextResponse.json( - { error: 'Scan fehlgeschlagen oder Timeout' }, + { error: 'Scan konnte nicht gestartet werden' }, + { status: 503 } + ) + } +} + +export async function GET(request: NextRequest) { + const scanId = request.nextUrl.searchParams.get('scan_id') + if (!scanId) { + return NextResponse.json({ error: 'scan_id parameter required' }, { status: 400 }) + } + + try { + const response = await fetch( + `${BACKEND_URL}/api/compliance/agent/scan/${scanId}`, + { signal: AbortSignal.timeout(10000) } + ) + + if (!response.ok) { + return NextResponse.json( + { error: `Backend: ${response.status}` }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + return NextResponse.json( + { error: 'Status-Abfrage fehlgeschlagen' }, { status: 503 } ) } diff --git a/admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts b/admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts new file mode 100644 index 0000000..eabc01b --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts @@ -0,0 +1,36 @@ +/** + * PDF Export Proxy + * POST /api/sdk/v1/agent/scans/pdf → backend /api/compliance/agent/scans/pdf + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest) { + try { + const body = await request.text() + + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/scans/pdf`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(30000), + }) + + if (!response.ok) { + return NextResponse.json({ error: 'PDF generation failed' }, { status: response.status }) + } + + const pdfBytes = await response.arrayBuffer() + return new NextResponse(pdfBytes, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': 'attachment; filename="compliance-report.pdf"', + }, + }) + } catch (error) { + console.error('PDF proxy error:', error) + return NextResponse.json({ error: 'PDF generation failed' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/banner/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/banner/[[...path]]/route.ts new file mode 100644 index 0000000..9d460bf --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/banner/[[...path]]/route.ts @@ -0,0 +1,74 @@ +/** + * Banner API Proxy — catch-all route for cookie banner endpoints. + * + * Maps: /api/sdk/v1/banner/ → backend-compliance:8002/api/compliance/banner/ + * + * Solves: Browser cannot call backend-compliance:8093 directly due to + * self-signed SSL certificates. This proxy runs server-side where + * certificate validation is not an issue. + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' +const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string, +) { + const pathStr = pathSegments?.join('/') || '' + const qs = request.nextUrl.searchParams.toString() + const base = `${BACKEND_URL}/api/compliance/banner` + const url = pathStr + ? `${base}/${pathStr}${qs ? `?${qs}` : ''}` + : `${base}${qs ? `?${qs}` : ''}` + + try { + const headers: HeadersInit = { + 'X-Tenant-ID': request.headers.get('x-tenant-id') || DEFAULT_TENANT, + } + const ct = request.headers.get('Content-Type') + if (ct) headers['Content-Type'] = ct + + const opts: RequestInit = { method, headers, signal: AbortSignal.timeout(30000) } + + if (method === 'POST' || method === 'PUT') { + const body = await request.text() + if (body) opts.body = body + } + + const res = await fetch(url, opts) + const text = await res.text() + let data + try { data = JSON.parse(text) } catch { data = { raw: text } } + + if (!res.ok) { + return NextResponse.json( + { error: `Backend ${res.status}`, ...data }, + { status: res.status }, + ) + } + return NextResponse.json(data) + } catch (err: any) { + console.error('Banner proxy error:', err?.message) + return NextResponse.json( + { error: 'Backend nicht erreichbar' }, + { status: 503 }, + ) + } +} + +export async function GET(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxyRequest(req, (await params).path, 'GET') +} +export async function POST(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxyRequest(req, (await params).path, 'POST') +} +export async function PUT(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxyRequest(req, (await params).path, 'PUT') +} +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxyRequest(req, (await params).path, 'DELETE') +} diff --git a/admin-compliance/app/api/sdk/v1/dsms/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/dsms/[[...path]]/route.ts new file mode 100644 index 0000000..546d019 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/dsms/[[...path]]/route.ts @@ -0,0 +1,22 @@ +/** + * DSMS Gateway Proxy — forwards verify/history requests to dsms-gateway. + */ +import { NextRequest, NextResponse } from 'next/server' + +const DSMS_URL = process.env.DSMS_GATEWAY_URL || 'http://dsms-gateway:8082' + +export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { + const { path } = await params + const target = `${DSMS_URL}/api/v1/${path.join('/')}` + + try { + const resp = await fetch(target, { + headers: { Authorization: 'Bearer system-frontend' }, + signal: AbortSignal.timeout(15000), + }) + const data = await resp.json() + return NextResponse.json(data, { status: resp.status }) + } catch { + return NextResponse.json({ error: 'DSMS not available' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/einwilligungen/consent/[id]/history/route.ts b/admin-compliance/app/api/sdk/v1/einwilligungen/consent/[id]/history/route.ts index 23be31d..ff0f21d 100644 --- a/admin-compliance/app/api/sdk/v1/einwilligungen/consent/[id]/history/route.ts +++ b/admin-compliance/app/api/sdk/v1/einwilligungen/consent/[id]/history/route.ts @@ -23,12 +23,13 @@ function getTenantId(request: NextRequest): string { */ export async function GET( request: NextRequest, - { params }: { params: { id: string } } + { params }: { params: Promise<{ id: string }> } ) { try { + const { id } = await params const tenantId = getTenantId(request) const response = await fetch( - `${BACKEND_URL}/api/compliance/einwilligungen/consents/${params.id}/history`, + `${BACKEND_URL}/api/compliance/einwilligungen/consents/${id}/history`, { method: 'GET', headers: { diff --git a/admin-compliance/app/api/sdk/v1/iace/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/iace/[[...path]]/route.ts index 378fa6a..6c620bc 100644 --- a/admin-compliance/app/api/sdk/v1/iace/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/iace/[[...path]]/route.ts @@ -30,15 +30,15 @@ async function proxyRequest( headers['Authorization'] = authHeader } + // Default tenant/user for IACE (same pattern as training proxy) + const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' + const DEFAULT_USER = '00000000-0000-0000-0000-000000000001' + const tenantHeader = request.headers.get('x-tenant-id') - if (tenantHeader) { - headers['X-Tenant-Id'] = tenantHeader - } + headers['X-Tenant-Id'] = tenantHeader || DEFAULT_TENANT const userHeader = request.headers.get('x-user-id') - if (userHeader) { - headers['X-User-Id'] = userHeader - } + headers['X-User-Id'] = userHeader || DEFAULT_USER const fetchOptions: RequestInit = { method, diff --git a/admin-compliance/app/api/sdk/v1/master-controls/route.ts b/admin-compliance/app/api/sdk/v1/master-controls/route.ts new file mode 100644 index 0000000..4dd70f5 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/master-controls/route.ts @@ -0,0 +1,229 @@ +import { NextRequest, NextResponse } from 'next/server' +import { Pool } from 'pg' + +// Disable SSL rejection for self-signed certs +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + +const dbUrl = process.env.COMPLIANCE_DATABASE_URL || + process.env.DATABASE_URL || + 'postgresql://breakpilot:breakpilot123@bp-core-postgres:5432/breakpilot_db' + +const pool = new Pool({ connectionString: dbUrl }) + +/** + * MC API that returns data in the same format as the canonical controls + * endpoint. This allows the MC page to reuse ControlListView components. + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const endpoint = searchParams.get('endpoint') || 'controls' + + switch (endpoint) { + case 'frameworks': + return NextResponse.json([]) + + case 'controls': + return handleControls(searchParams) + + case 'controls-count': + return handleCount(searchParams) + + case 'controls-meta': + return handleMeta(searchParams) + + case 'control': + return handleDetail(searchParams) + + default: + return NextResponse.json({ error: 'unknown' }, { status: 400 }) + } + } catch (e) { + return NextResponse.json({ error: String(e) }, { status: 500 }) + } +} + +async function handleControls(params: URLSearchParams) { + const search = params.get('search') || '' + const limit = Math.min(parseInt(params.get('limit') || '50'), 200) + const offset = parseInt(params.get('offset') || '0') + const sort = params.get('sort') || 'control_id' + const order = params.get('order') === 'desc' ? 'DESC' : 'ASC' + + let where = "WHERE 1=1" + const args: unknown[] = [] + let idx = 1 + + if (search) { + where += ` AND mc.canonical_name ILIKE $${idx}` + args.push(`%${search}%`) + idx++ + } + + const severity = params.get('severity') || '' + if (severity) { + if (severity === 'high') { where += ` AND mc.total_controls > 100` } + else if (severity === 'medium') { where += ` AND mc.total_controls BETWEEN 20 AND 100` } + else if (severity === 'low') { where += ` AND mc.total_controls < 20` } + } + + const domain = params.get('domain') || '' + if (domain) { + where += ` AND mc.canonical_name LIKE $${idx}` + args.push(`${domain}%`) + idx++ + } + + const sortCol = sort === 'control_id' ? 'mc.master_control_id' : + sort === 'created_at' ? 'mc.created_at' : + sort === 'source' ? 'mc.canonical_name' : 'mc.master_control_id' + + args.push(limit, offset) + const res = await pool.query(` + SELECT mc.master_control_id as control_id, + mc.canonical_name as title, + 'Master Control mit ' || mc.total_controls || ' Atomic Controls' as objective, + CASE WHEN mc.total_controls > 100 THEN 'high' + WHEN mc.total_controls > 20 THEN 'medium' + ELSE 'low' END as severity, + 'master_control' as category, + mc.total_controls, + mc.phases_covered, + mc.id, + mc.created_at + FROM compliance.master_controls mc + ${where} + ORDER BY ${sortCol} ${order} + LIMIT $${idx} OFFSET $${idx + 1} + `, args) + + // Map to canonical control format + const controls = res.rows.map(r => ({ + id: r.id, + control_id: r.control_id, + title: r.title, + objective: r.objective, + severity: r.severity, + category: r.category, + release_state: 'active', + source_citation: null, + verification_method: null, + evidence_type: null, + target_audience: [], + requirements: [], + test_procedure: [], + evidence: [], + open_anchors: [], + total_controls: r.total_controls, + phases_covered: r.phases_covered, + created_at: r.created_at, + scope: { platforms: [], components: [], data_classes: [] }, + risk_score: null, + implementation_effort: null, + })) + + return NextResponse.json(controls) +} + +async function handleCount(params: URLSearchParams) { + const search = params.get('search') || '' + let where = "WHERE 1=1" + const args: unknown[] = [] + + if (search) { + where += ` AND mc.canonical_name ILIKE $1` + args.push(`%${search}%`) + } + + const res = await pool.query( + `SELECT count(*) FROM compliance.master_controls mc ${where}`, args + ) + return NextResponse.json({ total: parseInt(res.rows[0].count) }) +} + +async function handleMeta(params: URLSearchParams) { + const res = await pool.query(` + SELECT count(*) as total, + count(CASE WHEN total_controls > 100 THEN 1 END) as high_count, + count(CASE WHEN total_controls BETWEEN 20 AND 100 THEN 1 END) as medium_count, + count(CASE WHEN total_controls < 20 THEN 1 END) as low_count + FROM compliance.master_controls + `) + const r = res.rows[0] + + // Get top L1 tokens as "domains" + const domainRes = await pool.query(` + SELECT split_part(canonical_name, '_', 1) as domain, count(*) as count + FROM compliance.master_controls + GROUP BY 1 ORDER BY 2 DESC LIMIT 30 + `) + + return NextResponse.json({ + total: parseInt(r.total), + severity_counts: { + high: parseInt(r.high_count), + medium: parseInt(r.medium_count), + low: parseInt(r.low_count), + }, + domains: domainRes.rows.map(d => ({ domain: d.domain, count: parseInt(d.count) })), + sources: [], + no_source_count: 0, + release_state_counts: { active: parseInt(r.total) }, + verification_method_counts: {}, + category_counts: {}, + evidence_type_counts: {}, + }) +} + +async function handleDetail(params: URLSearchParams) { + const id = params.get('id') || '' + const res = await pool.query(` + SELECT mc.id, mc.master_control_id as control_id, mc.canonical_name as title, + 'Master Control mit ' || mc.total_controls || ' Atomic Controls' as objective, + mc.total_controls, mc.phases_covered, mc.phase_control_count, mc.created_at + FROM compliance.master_controls mc + WHERE mc.master_control_id = $1 OR mc.id::text = $1 + `, [id]) + + if (res.rows.length === 0) { + return NextResponse.json({ error: 'not found' }, { status: 404 }) + } + + const mc = res.rows[0] + + // Load members + const membersRes = await pool.query(` + SELECT cc.control_id, cc.title, cc.severity, mcm.phase, mcm.action + FROM compliance.master_control_members mcm + JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid + WHERE mcm.master_control_uuid = $1 + ORDER BY mcm.phase, cc.control_id + LIMIT 100 + `, [mc.id]) + + return NextResponse.json({ + id: mc.id, + control_id: mc.control_id, + title: mc.title, + objective: mc.objective, + severity: mc.total_controls > 100 ? 'high' : mc.total_controls > 20 ? 'medium' : 'low', + category: 'master_control', + release_state: 'active', + total_controls: mc.total_controls, + phases_covered: mc.phases_covered, + phase_control_count: mc.phase_control_count, + members: membersRes.rows, + requirements: membersRes.rows.map((m: { control_id: string; title: string; phase: string }) => + `[${m.phase}] ${m.control_id}: ${m.title}` + ), + test_procedure: [], + evidence: [], + open_anchors: [], + target_audience: [], + source_citation: null, + scope: { platforms: [], components: [], data_classes: [] }, + risk_score: null, + implementation_effort: null, + created_at: mc.created_at, + }) +} diff --git a/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts index e606372..b08af28 100644 --- a/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts @@ -39,14 +39,14 @@ async function proxy(request: NextRequest, params: { path?: string[] }, method: } } -export async function GET(request: NextRequest, { params }: { params: { path?: string[] } }) { - return proxy(request, params, 'GET') +export async function GET(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxy(request, await params, 'GET') } -export async function POST(request: NextRequest, { params }: { params: { path?: string[] } }) { - return proxy(request, params, 'POST') +export async function POST(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxy(request, await params, 'POST') } -export async function DELETE(request: NextRequest, { params }: { params: { path?: string[] } }) { - return proxy(request, params, 'DELETE') +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxy(request, await params, 'DELETE') } diff --git a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts index ddbb733..20162df 100644 --- a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts @@ -6,7 +6,7 @@ const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78 /** * Proxy: /api/sdk/v1/ucca/decision-tree/... → Go Backend /sdk/v1/ucca/decision-tree/... */ -async function proxyRequest(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { +async function proxyRequest(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { const { path } = await params const subPath = path ? path.join('/') : '' const search = request.nextUrl.search || '' diff --git a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/route.ts b/admin-compliance/app/api/sdk/v1/ucca/decision-tree/route.ts deleted file mode 100644 index d8b1c33..0000000 --- a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090' -const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' - -/** - * Proxy: GET /api/sdk/v1/ucca/decision-tree → Go Backend GET /sdk/v1/ucca/decision-tree - * Returns the decision tree definition (questions, structure) - */ -export async function GET(request: NextRequest) { - const tenantID = request.headers.get('X-Tenant-ID') || DEFAULT_TENANT - - try { - const response = await fetch(`${SDK_URL}/sdk/v1/ucca/decision-tree`, { - headers: { 'X-Tenant-ID': tenantID }, - }) - - if (!response.ok) { - const errorText = await response.text() - console.error('Decision tree GET error:', errorText) - return NextResponse.json( - { error: 'Backend error', details: errorText }, - { status: response.status } - ) - } - - const data = await response.json() - return NextResponse.json(data) - } catch (error) { - console.error('Decision tree proxy error:', error) - return NextResponse.json( - { error: 'Failed to connect to AI compliance backend' }, - { status: 503 } - ) - } -} diff --git a/admin-compliance/app/api/vendor-compliance/assessments/[id]/route.ts b/admin-compliance/app/api/vendor-compliance/assessments/[id]/route.ts new file mode 100644 index 0000000..48e8a53 --- /dev/null +++ b/admin-compliance/app/api/vendor-compliance/assessments/[id]/route.ts @@ -0,0 +1,53 @@ +/** + * Vendor Assessment Status/Detail Proxy + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002' + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params + try { + const resp = await fetch( + `${BACKEND_URL}/api/vendor-compliance/assessments/${id}`, + { signal: AbortSignal.timeout(10000) }, + ) + const data = await resp.json() + return NextResponse.json(data, { status: resp.status }) + } catch (error) { + console.error('Assessment status proxy error:', error) + return NextResponse.json( + { error: 'Backend nicht erreichbar' }, + { status: 503 }, + ) + } +} + +export async function POST( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params + try { + const resp = await fetch( + `${BACKEND_URL}/api/vendor-compliance/assessments/${id}/approve`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }, + ) + const data = await resp.json() + return NextResponse.json(data, { status: resp.status }) + } catch (error) { + console.error('Assessment approve proxy error:', error) + return NextResponse.json( + { error: 'Backend nicht erreichbar' }, + { status: 503 }, + ) + } +} diff --git a/admin-compliance/app/api/vendor-compliance/assessments/route.ts b/admin-compliance/app/api/vendor-compliance/assessments/route.ts new file mode 100644 index 0000000..7d36aec --- /dev/null +++ b/admin-compliance/app/api/vendor-compliance/assessments/route.ts @@ -0,0 +1,41 @@ +/** + * Vendor Assessment API Proxy + * Proxies to backend-compliance (Python FastAPI) + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest) { + try { + const body = await request.text() + const resp = await fetch(`${BACKEND_URL}/api/vendor-compliance/assessments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(10000), + }) + const data = await resp.json() + return NextResponse.json(data, { status: resp.status }) + } catch (error) { + console.error('Vendor assessment proxy error:', error) + return NextResponse.json( + { error: 'Backend nicht erreichbar' }, + { status: 503 }, + ) + } +} + +export async function GET() { + try { + const resp = await fetch(`${BACKEND_URL}/api/vendor-compliance/assessments`, { + signal: AbortSignal.timeout(10000), + }) + const data = await resp.json() + return NextResponse.json(data) + } catch (error) { + console.error('Vendor assessment list proxy error:', error) + return NextResponse.json({ assessments: [] }) + } +} diff --git a/admin-compliance/app/sdk/_components/PresetSection.tsx b/admin-compliance/app/sdk/_components/PresetSection.tsx new file mode 100644 index 0000000..9b2a5bb --- /dev/null +++ b/admin-compliance/app/sdk/_components/PresetSection.tsx @@ -0,0 +1,92 @@ +'use client' + +import { useState } from 'react' +import Link from 'next/link' +import { COMPANY_PROFILE_PRESETS, type CompanyProfilePreset } from '@/lib/sdk/company-profile-presets' +import { DOC_LABELS, CATEGORY_COLORS } from './doc-labels' + +export function PresetSection({ projectId }: { projectId?: string }) { + const [selectedPreset, setSelectedPreset] = useState(null) + + // Group recommended docs by category + const groupedDocs = selectedPreset + ? selectedPreset.recommendedDocs.reduce>((acc, docType) => { + const info = DOC_LABELS[docType] + if (!info) return acc + if (!acc[info.category]) acc[info.category] = [] + acc[info.category].push(info.label) + return acc + }, {}) + : null + + return ( +
+
+

Schnellstart: Welcher Unternehmenstyp sind Sie?

+

+ Waehlen Sie Ihre Branche — wir zeigen Ihnen welche Dokumente Sie benoetigen. +

+
+ + {/* Preset Cards */} +
+ {COMPANY_PROFILE_PRESETS.map((preset) => ( + + ))} +
+ + {/* Document Preview — shown when a preset is selected */} + {selectedPreset && groupedDocs && ( +
+
+
+

+ {selectedPreset.icon} {selectedPreset.label} — Ihre Dokumente +

+

+ {selectedPreset.recommendedDocs.length} Dokumente werden fuer Sie vorbereitet +

+
+ + Jetzt starten + +
+ +
+ {Object.entries(groupedDocs).map(([category, docs]) => ( +
+ + {category} + + {docs.map((doc) => ( +
+ {doc} +
+ ))} +
+ ))} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/_components/doc-labels.ts b/admin-compliance/app/sdk/_components/doc-labels.ts new file mode 100644 index 0000000..4331f70 --- /dev/null +++ b/admin-compliance/app/sdk/_components/doc-labels.ts @@ -0,0 +1,131 @@ +/** + * Complete mapping of all document template types to display labels and categories. + * Used by PresetSection to show categorized document previews. + */ + +export const DOC_LABELS: Record = { + // ── Website ────────────────────────────────────────────────────── + privacy_policy: { label: 'Datenschutzerklaerung', category: 'Website' }, + impressum: { label: 'Impressum', category: 'Website' }, + cookie_policy: { label: 'Cookie-Richtlinie', category: 'Website' }, + cookie_banner: { label: 'Cookie-Banner-Texte', category: 'Website' }, + + // ── Vertraege ──────────────────────────────────────────────────── + agb: { label: 'AGB', category: 'Vertraege' }, + dpa: { label: 'AVV (Auftragsverarbeitung)', category: 'Vertraege' }, + nda: { label: 'Geheimhaltungsvereinbarung', category: 'Vertraege' }, + sla: { label: 'Service Level Agreement', category: 'Vertraege' }, + terms_of_use: { label: 'Nutzungsbedingungen', category: 'Vertraege' }, + cloud_service_agreement: { label: 'Cloud-Vertrag', category: 'Vertraege' }, + data_usage_clause: { label: 'Datennutzungsklausel', category: 'Vertraege' }, + + // ── Plattform ──────────────────────────────────────────────────── + community_guidelines: { label: 'Community Guidelines', category: 'Plattform' }, + acceptable_use: { label: 'Acceptable Use Policy', category: 'Plattform' }, + media_content_policy: { label: 'Medien-Richtlinie', category: 'Plattform' }, + copyright_policy: { label: 'Urheberrechtsrichtlinie', category: 'Plattform' }, + + // ── E-Commerce ─────────────────────────────────────────────────── + widerruf: { label: 'Widerrufsbelehrung', category: 'E-Commerce' }, + + // ── HR / Personal ──────────────────────────────────────────────── + employee_dsi: { label: 'Mitarbeiter-DSI', category: 'HR' }, + applicant_dsi: { label: 'Bewerber-DSI', category: 'HR' }, + whistleblower_policy: { label: 'Whistleblower-Richtlinie', category: 'HR' }, + employee_security_policy: { label: 'Mitarbeiter-Sicherheitsrichtlinie', category: 'HR' }, + security_awareness_policy: { label: 'Security-Awareness-Richtlinie', category: 'HR' }, + remote_work_policy: { label: 'Remote-Work-Richtlinie', category: 'HR' }, + offboarding_policy: { label: 'Offboarding-Richtlinie', category: 'HR' }, + + // ── Datenschutz (DSGVO) ────────────────────────────────────────── + tom_documentation: { label: 'TOM-Dokumentation', category: 'Datenschutz' }, + vvt_register: { label: 'Verarbeitungsverzeichnis', category: 'Datenschutz' }, + loeschkonzept: { label: 'Loeschkonzept', category: 'Datenschutz' }, + dsfa: { label: 'Datenschutz-Folgenabschaetzung', category: 'Datenschutz' }, + pflichtenregister: { label: 'Pflichtenregister', category: 'Datenschutz' }, + data_protection_concept: { label: 'Datenschutzkonzept', category: 'Datenschutz' }, + consent_texts: { label: 'Einwilligungstexte', category: 'Datenschutz' }, + informationspflichten: { label: 'Informationspflichten', category: 'Datenschutz' }, + verpflichtungserklaerung: { label: 'Verpflichtungserklaerung', category: 'Datenschutz' }, + social_media_dsi: { label: 'Social-Media-DSI', category: 'Datenschutz' }, + video_conference_dsi: { label: 'Videokonferenz-DSI', category: 'Datenschutz' }, + + // ── Daten-Policies ─────────────────────────────────────────────── + data_protection_policy: { label: 'Datenschutzrichtlinie', category: 'Daten-Governance' }, + data_classification_policy: { label: 'Datenklassifizierung', category: 'Daten-Governance' }, + data_retention_policy: { label: 'Aufbewahrungsrichtlinie', category: 'Daten-Governance' }, + data_transfer_policy: { label: 'Datentransfer-Richtlinie', category: 'Daten-Governance' }, + privacy_incident_policy: { label: 'Datenschutzvorfall-Richtlinie', category: 'Daten-Governance' }, + + // ── Betroffenenrechte ──────────────────────────────────────────── + dsr_process_art15: { label: 'Auskunftsrecht (Art. 15)', category: 'Betroffenenrechte' }, + dsr_process_art16: { label: 'Berichtigungsrecht (Art. 16)', category: 'Betroffenenrechte' }, + dsr_process_art17: { label: 'Loeschungsrecht (Art. 17)', category: 'Betroffenenrechte' }, + dsr_process_art18: { label: 'Einschraenkungsrecht (Art. 18)', category: 'Betroffenenrechte' }, + dsr_process_art19: { label: 'Mitteilungspflicht (Art. 19)', category: 'Betroffenenrechte' }, + dsr_process_art20: { label: 'Datenportabilitaet (Art. 20)', category: 'Betroffenenrechte' }, + dsr_process_art21: { label: 'Widerspruchsrecht (Art. 21)', category: 'Betroffenenrechte' }, + + // ── IT-Sicherheit (Konzepte) ───────────────────────────────────── + it_security_concept: { label: 'IT-Sicherheitskonzept', category: 'IT-Sicherheit' }, + backup_recovery_concept: { label: 'Backup- & Recovery-Konzept', category: 'IT-Sicherheit' }, + logging_concept: { label: 'Logging-Konzept', category: 'IT-Sicherheit' }, + incident_response_plan: { label: 'Incident-Response-Plan', category: 'IT-Sicherheit' }, + access_control_concept: { label: 'Zugriffskonzept', category: 'IT-Sicherheit' }, + risk_management_concept: { label: 'Risikomanagement-Konzept', category: 'IT-Sicherheit' }, + isms_manual: { label: 'ISMS-Handbuch', category: 'IT-Sicherheit' }, + + // ── IT-Sicherheit (Policies) ───────────────────────────────────── + information_security_policy: { label: 'Informationssicherheitsrichtlinie', category: 'IT-Policies' }, + access_control_policy: { label: 'Zugriffskontrollrichtlinie', category: 'IT-Policies' }, + password_policy: { label: 'Passwortrichtlinie', category: 'IT-Policies' }, + encryption_policy: { label: 'Verschluesselungsrichtlinie', category: 'IT-Policies' }, + logging_policy: { label: 'Protokollierungsrichtlinie', category: 'IT-Policies' }, + backup_policy: { label: 'Datensicherungsrichtlinie', category: 'IT-Policies' }, + incident_response_policy: { label: 'Incident-Response-Richtlinie', category: 'IT-Policies' }, + change_management_policy: { label: 'Change-Management-Richtlinie', category: 'IT-Policies' }, + patch_management_policy: { label: 'Patch-Management-Richtlinie', category: 'IT-Policies' }, + asset_management_policy: { label: 'Asset-Management-Richtlinie', category: 'IT-Policies' }, + cloud_security_policy: { label: 'Cloud-Security-Richtlinie', category: 'IT-Policies' }, + devsecops_policy: { label: 'DevSecOps-Richtlinie', category: 'IT-Policies' }, + secrets_management_policy: { label: 'Secrets-Management-Richtlinie', category: 'IT-Policies' }, + vulnerability_management_policy: { label: 'Schwachstellenmanagement', category: 'IT-Policies' }, + + // ── Lieferanten / Drittanbieter ────────────────────────────────── + vendor_risk_management_policy: { label: 'Lieferanten-Risikomanagement', category: 'Lieferanten' }, + third_party_security_policy: { label: 'Drittanbieter-Sicherheit', category: 'Lieferanten' }, + supplier_security_policy: { label: 'Lieferanten-Anforderungen', category: 'Lieferanten' }, + transfer_impact_assessment: { label: 'Transfer Impact Assessment', category: 'Lieferanten' }, + scc_companion: { label: 'SCC-Begleitdokument', category: 'Lieferanten' }, + + // ── BCM / Notfall ──────────────────────────────────────────────── + business_continuity_policy: { label: 'Business-Continuity', category: 'BCM' }, + disaster_recovery_policy: { label: 'Disaster-Recovery', category: 'BCM' }, + crisis_management_policy: { label: 'Krisenmanagement', category: 'BCM' }, + + // ── KI / Cyber ─────────────────────────────────────────────────── + ai_usage_policy: { label: 'KI-Nutzungsrichtlinie', category: 'KI & Cyber' }, + cybersecurity_policy: { label: 'Cybersecurity-Richtlinie (CRA)', category: 'KI & Cyber' }, + byod_policy: { label: 'BYOD-Richtlinie', category: 'KI & Cyber' }, + + // ── SOP ────────────────────────────────────────────────────────── + standard_operating_procedure: { label: 'Standard Operating Procedure', category: 'Prozesse' }, +} + +export const CATEGORY_COLORS: Record = { + Website: 'bg-blue-50 text-blue-700', + Vertraege: 'bg-purple-50 text-purple-700', + Plattform: 'bg-indigo-50 text-indigo-700', + 'E-Commerce': 'bg-green-50 text-green-700', + HR: 'bg-amber-50 text-amber-700', + Datenschutz: 'bg-red-50 text-red-700', + 'Daten-Governance': 'bg-rose-50 text-rose-700', + Betroffenenrechte: 'bg-fuchsia-50 text-fuchsia-700', + 'IT-Sicherheit': 'bg-gray-100 text-gray-700', + 'IT-Policies': 'bg-slate-100 text-slate-700', + Lieferanten: 'bg-orange-50 text-orange-700', + BCM: 'bg-yellow-50 text-yellow-700', + 'KI & Cyber': 'bg-cyan-50 text-cyan-700', + Marketing: 'bg-pink-50 text-pink-700', + Prozesse: 'bg-teal-50 text-teal-700', +} diff --git a/admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx b/admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx new file mode 100644 index 0000000..10b364f --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx @@ -0,0 +1,73 @@ +'use client' + +import React from 'react' + +interface AuthCheck { + found: boolean + text: string + legal_ref: string +} + +interface AuthData { + url: string + authenticated: boolean + login_error: string + checks: Record + findings_count: number +} + +const CHECK_LABELS: Record = { + cancel_subscription: { label: 'Kuendigungsbutton (2 Klicks)', icon: '🚫' }, + delete_account: { label: 'Konto loeschen', icon: '🗑️' }, + export_data: { label: 'Daten exportieren', icon: '📥' }, + consent_settings: { label: 'Einwilligungen widerrufen', icon: '⚙️' }, + profile_visible: { label: 'Profildaten einsehen', icon: '👤' }, +} + +export function AuthTestResult({ data }: { data: AuthData }) { + if (!data.authenticated) { + return ( +
+

Login fehlgeschlagen

+

{data.login_error || 'Credentials oder Formular nicht erkannt'}

+
+ ) + } + + return ( +
+
+ + Erfolgreich eingeloggt + 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}> + {data.findings_count} fehlende Funktionen + +
+ +
+ {Object.entries(data.checks).map(([key, check]) => { + const info = CHECK_LABELS[key] || { label: key, icon: '❓' } + return ( +
+ {info.icon} +
+

+ {check.found ? '✓' : '✗'} {info.label} +

+ {check.text &&

{check.text}

} +
+ {check.legal_ref} +
+ ) + })} +
+ + {data.findings_count > 0 && ( +
+ {data.findings_count} Pflichtfunktion(en) fehlen. Der Nutzer kann seine Rechte + nach DSGVO nicht vollstaendig ausueben. +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx new file mode 100644 index 0000000..3a9cdc3 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx @@ -0,0 +1,374 @@ +'use client' + +import React, { useState } from 'react' +import { ChecklistView } from './ChecklistView' + +interface CheckItem { + id: string + label: string + passed: boolean + severity: string + matched_text: string + level?: number + parent?: string | null + skipped?: boolean + hint?: string +} + +interface BannerResult { + banner_detected: boolean + banner_provider: string + banner_checks?: { + violations: { code: string; text: string; severity: string }[] + has_impressum_link?: boolean + has_dse_link?: boolean + } + structured_checks?: CheckItem[] + completeness_pct?: number + correctness_pct?: number + phases?: { + before_consent: { cookies: string[]; scripts: string[]; tracking_services: string[]; violations: any[] } + after_reject: { cookies: string[]; scripts: string[]; new_tracking: string[]; violations: any[] } + after_accept: { cookies: string[]; scripts: string[]; new_tracking: string[]; undocumented: string[] } + } + email_status?: string +} + +const CATEGORIES = [ + { id: 'all', label: 'Alle Kategorien' }, + { id: 'necessary', label: 'Notwendig' }, + { id: 'statistics', label: 'Statistik' }, + { id: 'marketing', label: 'Marketing' }, + { id: 'functional', label: 'Funktional' }, + { id: 'preferences', label: 'Praeferenzen' }, +] + +export function BannerCheckTab() { + const [url, setUrl] = useState(() => + typeof window !== 'undefined' ? localStorage.getItem('banner-check-url') || '' : '' + ) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState('') + const [error, setError] = useState(null) + const [result, setResult] = useState(() => { + if (typeof window === 'undefined') return null + try { const s = localStorage.getItem('banner-check-result'); return s ? JSON.parse(s) : null } catch { return null } + }) + const [categories, setCategories] = useState(['all']) + const [useAgent, setUseAgent] = useState(false) + const [mcResults, setMcResults] = useState(null) + const [history, setHistory] = useState<{ url: string; date: string; provider: string; violations: number; pct: number; resultKey: string }[]>(() => { + if (typeof window === 'undefined') return [] + try { return JSON.parse(localStorage.getItem('banner-check-history') || '[]') } catch { return [] } + }) + + // Persist URL + React.useEffect(() => { localStorage.setItem('banner-check-url', url) }, [url]) + + const toggleCategory = (id: string) => { + if (id === 'all') { + setCategories(['all']) + return + } + setCategories(prev => { + const without = prev.filter(c => c !== 'all' && c !== id) + const next = prev.includes(id) ? without : [...without, id] + return next.length === 0 ? ['all'] : next + }) + } + + const handleScan = async (e: React.FormEvent) => { + e.preventDefault() + if (!url.trim()) return + + setLoading(true) + setError(null) + setResult(null) + setProgress('Cookie-Banner wird analysiert...') + + const selectedCategories = categories.includes('all') ? [] : categories + + try { + const res = await fetch('/api/sdk/v1/agent/banner-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: url.trim(), categories: selectedCategories }), + }) + if (!res.ok) throw new Error(`Fehler: ${res.status}`) + const data = await res.json() + setResult(data) + localStorage.setItem('banner-check-result', JSON.stringify(data)) + + // If agent mode: also run cookie doc-check with 381 MCs + if (useAgent) { + setProgress('KI-Agent prueft Cookie-Richtlinie (381 MCs)...') + try { + const mcRes = await fetch('/api/sdk/v1/agent/doc-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + entries: [{ doc_type: 'cookie', label: 'Cookie-Richtlinie', url: url.trim() }], + recipient: 'dsb@breakpilot.local', + use_agent: true, + }), + }) + if (mcRes.ok) { + const { check_id } = await mcRes.json() + if (check_id) { + for (let i = 0; i < 60; i++) { + await new Promise(r => setTimeout(r, 3000)) + const poll = await fetch(`/api/sdk/v1/agent/doc-check?check_id=${check_id}`) + if (!poll.ok) continue + const pd = await poll.json() + if (pd.progress) setProgress(`KI-Agent: ${pd.progress}`) + if (pd.status === 'completed' && pd.result) { setMcResults(pd.result); break } + if (pd.status === 'failed') break + } + } + } + } catch { /* agent check is optional */ } + } + + // Add to history with persistent result + const violations = data.structured_checks?.filter((c: CheckItem) => !c.passed && !c.skipped).length || 0 + const resultKey = `banner-check-result-${Date.now()}` + try { localStorage.setItem(resultKey, JSON.stringify(data)) } catch { /* quota */ } + const entry = { + url: url.trim(), + date: new Date().toISOString(), + provider: data.banner_provider || 'Unbekannt', + violations, + pct: data.completeness_pct ?? 0, + resultKey, + } + const updated = [entry, ...history].slice(0, 30) + setHistory(updated) + localStorage.setItem('banner-check-history', JSON.stringify(updated)) + } catch (e) { + setError(e instanceof Error ? e.message : 'Unbekannter Fehler') + } finally { + setLoading(false) + setProgress('') + } + } + + const loadFromHistory = (entry: { url: string; resultKey?: string }) => { + setUrl(entry.url) + if (entry.resultKey) { + try { + const saved = localStorage.getItem(entry.resultKey) + if (saved) { setResult(JSON.parse(saved)); return } + } catch {} + } + // Fallback: load last result + try { + const last = localStorage.getItem('banner-check-result') + if (last) setResult(JSON.parse(last)) + } catch {} + } + + const structuredChecks = result?.structured_checks || [] + const hasStructured = structuredChecks.length > 0 + const compPct = result?.completeness_pct ?? 0 + const corrPct = result?.correctness_pct ?? 0 + + const checklistResults = hasStructured ? [{ + label: `Cookie-Banner: ${result?.banner_provider || 'Unbekannt'}`, + url: url, + doc_type: 'banner', + word_count: 0, + completeness_pct: compPct, + correctness_pct: corrPct, + checks: structuredChecks, + findings_count: structuredChecks.filter(c => !c.passed && !c.skipped).length, + error: '', + }] : [] + + return ( +
+
+

Cookie-Banner Compliance Check

+

+ Playwright-basierter 3-Phasen-Test: Vor Interaktion, nach Ablehnen, nach Akzeptieren. + Prueft Dark Patterns, Pre-Consent-Cookies, Farbkontrast, Klick-Paritaet und 36 weitere Kriterien. +

+
+ +
+ +
+ +
+
+ setUrl(e.target.value)} + placeholder="https://www.example.com/" + className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm" + disabled={loading} required + /> + +
+ +
+ {CATEGORIES.map(cat => ( + + ))} +
+
+ + {progress && ( +
+ + + + + {progress} +
+ )} + + {error && ( +
{error}
+ )} + + {result && ( +
+ {result.phases && ( +
+
+
+ {result.banner_detected ? '🛡️' : '⚠️'} +
+

+ {result.banner_detected + ? `Banner erkannt: ${result.banner_provider || 'Unbekannter Anbieter'}` + : 'Kein Cookie-Banner erkannt'} +

+

3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion

+
+
+
+
+ + + +
+
+ )} + + {hasStructured && ( +
+ +
+ )} + + {result.email_status && ( +
+ + E-Mail: {result.email_status === 'sent' ? 'Gesendet' : result.email_status} +
+ )} + + {/* MC Agent Results (Cookie-Richtlinie) */} + {mcResults?.results && ( +
+

KI-Agent: Cookie-Richtlinie (381 MCs)

+ +
+ )} + + {!result.banner_detected && !hasStructured && ( +
+

+ Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach §25 TDDDG Pflicht. +

+
+ )} +
+ )} + + {/* History */} + {history.length > 0 && ( +
+

Letzte Banner-Checks

+
+ {history.map((h, i) => ( + + ))} +
+
+ )} +
+ ) +} + +function PhaseBox({ label, icon, cookies, scripts, violations }: { + label: string; icon: string; cookies: number; scripts: number; violations: number +}) { + return ( +
+
{icon}
+
{label}
+
{cookies} Cookies, {scripts} Scripts
+ {violations > 0 &&
{violations} Verstoesse
} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx b/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx new file mode 100644 index 0000000..98f41fd --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/ChecklistView.tsx @@ -0,0 +1,279 @@ +'use client' + +import React, { useState } from 'react' + +interface CheckItem { + id: string + label: string + passed: boolean + severity: string + matched_text: string + level?: number + parent?: string | null + skipped?: boolean + hint?: string +} + +interface DocResult { + label: string + url: string + doc_type: string + word_count: number + completeness_pct: number + correctness_pct?: number + checks: CheckItem[] + findings_count: number + error: string + scenario?: string // regenerate | fix | import | skip +} + +const SCENARIO_LABELS: Record = { + regenerate: { label: 'Neugenerierung', color: 'text-red-700', bg: 'bg-red-100' }, + fix: { label: 'Korrekturen', color: 'text-amber-700', bg: 'bg-amber-100' }, + import: { label: 'Konform', color: 'text-green-700', bg: 'bg-green-100' }, +} + +const DOC_TYPE_LABELS: Record = { + dse: 'DSI', agb: 'AGB', impressum: 'Impressum', + cookie: 'Cookie', widerruf: 'Widerruf', other: 'Sonstiges', + social_media: 'Social Media', dsfa: 'DSFA', joint_controller: 'Art. 26', + eu_institution: 'EU-Inst.', banner: 'Banner', +} + +interface GroupedCheck { + check: CheckItem + children: CheckItem[] +} + +function groupChecks(checks: CheckItem[]): GroupedCheck[] { + const l1 = checks.filter(c => (c.level ?? 1) === 1) + return l1.map(c => ({ + check: c, + children: checks.filter(ch => ch.parent === c.id && (ch.level ?? 1) === 2), + })) +} + +function CheckIcon({ passed, skipped, isInfo }: { passed: boolean; skipped?: boolean; isInfo?: boolean }) { + if (skipped) { + return ( + + + + ) + } + if (passed) { + return ( + + + + ) + } + if (isInfo) { + return ( + + + + ) + } + return ( + + + + ) +} + +function L2Summary({ children }: { children: CheckItem[] }) { + const active = children.filter(c => !c.skipped) + if (active.length === 0) return null + const passed = active.filter(c => c.passed).length + return ( + + ({passed}/{active.length}) + + ) +} + +export function ChecklistView({ results }: { results: DocResult[] }) { + const [expanded, setExpanded] = useState(null) + + if (!results || results.length === 0) return null + + const scenarioCounts = { + regenerate: results.filter(r => r.scenario === 'regenerate').length, + fix: results.filter(r => r.scenario === 'fix').length, + import: results.filter(r => r.scenario === 'import').length, + } + + return ( +
+
+

+ Dokumenten-Pruefung ({results.length} Dokumente) +

+
+ {scenarioCounts.import > 0 && {scenarioCounts.import} konform} + {scenarioCounts.fix > 0 && {scenarioCounts.fix} Korrekturen} + {scenarioCounts.regenerate > 0 && {scenarioCounts.regenerate} Neugenerierung} +
+
+ +
+ {results.map((r, i) => { + const isExp = expanded === i + const pct = r.completeness_pct + const cpct = r.correctness_pct ?? 0 + const barColor = pct === 100 ? 'bg-green-500' : pct >= 80 ? 'bg-green-400' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500' + const cBarColor = cpct >= 80 ? 'bg-blue-400' : cpct >= 50 ? 'bg-blue-300' : 'bg-blue-200' + const typeLabel = DOC_TYPE_LABELS[r.doc_type] || r.doc_type + const grouped = groupChecks(r.checks) + const l1Checks = r.checks.filter(c => (c.level ?? 1) === 1) + const l1Scoreable = l1Checks.filter(c => c.severity !== 'INFO') + const l2Active = r.checks.filter(c => (c.level ?? 1) === 2 && !c.skipped) + const l1Passed = l1Scoreable.filter(c => c.passed).length + const l2Passed = l2Active.filter(c => c.passed).length + + return ( +
+ + + {isExp && ( +
+ {r.error ? ( +

{r.error}

+ ) : ( +
+ {grouped.map((g) => { + const l1Info = g.check.severity === 'INFO' && !g.check.passed + return ( +
+ {/* L1 check */} +
+ +
+
+ {g.check.label} + {g.children.length > 0 && {g.children}} +
+ {g.check.passed && g.check.matched_text && g.children.length === 0 && ( +
+ "...{g.check.matched_text}..." +
+ )} + {!g.check.passed && g.check.hint && ( +
+ {g.check.hint} +
+ )} +
+
+ + {/* L2 children — always visible */} + {g.children.length > 0 && ( +
+ {g.children.map((ch) => { + const chInfo = ch.severity === 'INFO' && !ch.passed && !ch.skipped + return ( +
+ +
+
+ {ch.label} + {ch.skipped && ' (uebersprungen)'} +
+ {ch.passed && ch.matched_text && ( +
+ "...{ch.matched_text}..." +
+ )} + {!ch.passed && !ch.skipped && ch.hint && ( +
+ {ch.hint} +
+ )} +
+
+ ) + })} +
+ )} +
+ ) + })} + {r.word_count > 0 && ( +
+ {r.word_count} Woerter analysiert +
+ )} +
+ )} +
+ )} +
+ ) + })} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/CompareResult.tsx b/admin-compliance/app/sdk/agent/_components/CompareResult.tsx new file mode 100644 index 0000000..8f23b00 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/CompareResult.tsx @@ -0,0 +1,96 @@ +'use client' + +import React from 'react' + +interface SiteResult { + url: string + domain: string + risk_level: string + risk_score: number + findings_count: number + services_count: number + has_impressum: boolean + has_datenschutz: boolean + has_cookie_banner: boolean + has_google_fonts: boolean + scan_status: string +} + +const RISK_COLOR: Record = { + MINIMAL: 'text-green-700 bg-green-50', + LOW: 'text-yellow-700 bg-yellow-50', + LIMITED: 'text-orange-700 bg-orange-50', + HIGH: 'text-red-700 bg-red-50', + UNACCEPTABLE: 'text-red-900 bg-red-100', +} + +export function CompareResult({ sites }: { sites: SiteResult[] }) { + if (!sites.length) return null + + const checks = [ + { key: 'has_datenschutz', label: 'Datenschutzerklaerung' }, + { key: 'has_impressum', label: 'Impressum' }, + { key: 'has_cookie_banner', label: 'Cookie-Banner' }, + { key: 'has_google_fonts', label: 'Google Fonts (lokal?)' }, + ] + + return ( +
+
+ + + + + {sites.map((s, i) => ( + + ))} + + + + + + {sites.map((s, i) => ( + + ))} + + + + {sites.map((s, i) => ( + + ))} + + + + {sites.map((s, i) => ( + + ))} + + {checks.map(check => ( + + + {sites.map((s, i) => { + const val = (s as any)[check.key] + const isInverted = check.key === 'has_google_fonts' + const good = isInverted ? !val : val + return ( + + ) + })} + + ))} + +
Pruefung + {s.domain} +
Risiko-Score + + {s.risk_level || '?'} ({s.risk_score}/100) + +
Findings 0 ? 'text-red-700' : 'text-green-700'}`}> + {s.findings_count} +
Dienste erkannt{s.services_count}
{check.label} + {good ? '✓' : '✗'} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx new file mode 100644 index 0000000..05d2fc0 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx @@ -0,0 +1,482 @@ +'use client' + +import React, { useState, useCallback } from 'react' +import { ChecklistView } from './ChecklistView' +import { DocumentRow } from './DocumentRow' + +const DOCUMENT_TYPES = [ + { id: 'dse', label: 'DSI (Datenschutzinformation)', required: true }, + { id: 'impressum', label: 'Impressum', required: true }, + { id: 'social_media', label: 'Social Media DSE', required: false }, + { id: 'cookie', label: 'Cookie-Richtlinie', required: false }, + { id: 'agb', label: 'AGB', required: false }, + { id: 'nutzungsbedingungen', label: 'Nutzungsbedingungen', required: false }, + { id: 'widerruf', label: 'Widerrufsbelehrung', required: false }, + { id: 'dsb', label: 'DSB-Kontakt', required: false }, +] as const + +type DocTypeId = typeof DOCUMENT_TYPES[number]['id'] + +interface DocState { + url: string + text: string + loading: boolean + error: string | null +} + +type DocsState = Record + +const STORAGE_KEY_STATE = 'compliance-check-state' +const STORAGE_KEY_RESULTS = 'compliance-check-results' +const STORAGE_KEY_HISTORY = 'compliance-check-history' +const STORAGE_KEY_CHECK_ID = 'compliance-check-active-id' + +function emptyDocState(): DocState { + return { url: '', text: '', loading: false, error: null } +} + +function initState(): DocsState { + if (typeof window === 'undefined') { + return Object.fromEntries(DOCUMENT_TYPES.map(d => [d.id, emptyDocState()])) as DocsState + } + try { + const saved = localStorage.getItem(STORAGE_KEY_STATE) + if (saved) { + const parsed = JSON.parse(saved) as Record + return Object.fromEntries( + DOCUMENT_TYPES.map(d => [d.id, { + url: parsed[d.id]?.url || '', + text: parsed[d.id]?.text || '', + loading: false, + error: null, + }]) + ) as DocsState + } + } catch { /* ignore */ } + return Object.fromEntries(DOCUMENT_TYPES.map(d => [d.id, emptyDocState()])) as DocsState +} + +function countWords(text: string): number { + if (!text.trim()) return 0 + return text.trim().split(/\s+/).length +} + +interface HistoryEntry { + date: string + docCount: number + findings: number + resultKey: string +} + +export function ComplianceCheckTab() { + const [docs, setDocs] = useState(initState) + const [useAgent, setUseAgent] = useState(false) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState('') + const [results, setResults] = useState(() => { + if (typeof window === 'undefined') return null + try { const s = localStorage.getItem(STORAGE_KEY_RESULTS); return s ? JSON.parse(s) : null } catch { return null } + }) + const [error, setError] = useState(null) + const [activeCheckId, setActiveCheckId] = useState(() => + typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY_CHECK_ID) || '' : '' + ) + const [history, setHistory] = useState(() => { + if (typeof window === 'undefined') return [] + try { return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '[]') } catch { return [] } + }) + + // Persist URLs and texts (not loading/error state) + React.useEffect(() => { + const toSave: Record = {} + for (const [key, val] of Object.entries(docs)) { + toSave[key] = { url: val.url, text: val.text } + } + try { localStorage.setItem(STORAGE_KEY_STATE, JSON.stringify(toSave)) } catch { /* quota */ } + }, [docs]) + + // Resume polling if check was in progress when navigating away + React.useEffect(() => { + if (!activeCheckId || results) return + let cancelled = false + setLoading(true) + setProgress('Pruefung laeuft noch...') + const poll = async () => { + while (!cancelled) { + await new Promise(r => setTimeout(r, 3000)) + try { + const res = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${activeCheckId}`) + if (!res.ok) continue + const data = await res.json() + if (data.progress) setProgress(data.progress) + if (data.status === 'completed' && data.result) { + setResults(data.result); setProgress(''); setLoading(false) + localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(data.result)) + localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') + return + } + if (data.status === 'failed' || data.status === 'not_found') { + if (data.status === 'failed') setError(data.error || 'Pruefung fehlgeschlagen') + setProgress(''); setLoading(false) + localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') + return + } + } catch { /* retry */ } + } + } + poll() + return () => { cancelled = true } + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + const updateDoc = useCallback((docType: DocTypeId, patch: Partial) => { + setDocs(prev => ({ ...prev, [docType]: { ...prev[docType], ...patch } })) + }, []) + + const handleFetchText = useCallback(async (docType: DocTypeId) => { + const url = docs[docType].url.trim() + if (!url) return + + updateDoc(docType, { loading: true, error: null }) + try { + const res = await fetch('/api/sdk/v1/agent/extract-text', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + }) + if (!res.ok) { + const msg = res.status === 404 + ? 'Seite nicht erreichbar' + : `Fehler beim Laden (${res.status})` + throw new Error(msg) + } + const data = await res.json() + updateDoc(docType, { text: data.text || '', loading: false }) + } catch (e) { + updateDoc(docType, { + loading: false, + error: e instanceof Error ? e.message : 'Text konnte nicht geladen werden', + }) + } + }, [docs, updateDoc]) + + const handleFileUpload = useCallback(async (docType: DocTypeId, file: File) => { + // For now, read as text. PDF/DOCX parsing can be added server-side later. + const reader = new FileReader() + reader.onload = () => { + updateDoc(docType, { text: reader.result as string }) + } + reader.readAsText(file) + }, [updateDoc]) + + const filledCount = Object.values(docs).filter(d => d.url.trim() || d.text.trim()).length + + const handleSubmit = async () => { + if (filledCount === 0) return + + setLoading(true) + setError(null) + setResults(null) + setProgress('Compliance-Check wird gestartet...') + + try { + const entries = DOCUMENT_TYPES + .filter(dt => docs[dt.id].url.trim() || docs[dt.id].text.trim()) + .map(dt => ({ + doc_type: dt.id, + label: dt.label, + url: docs[dt.id].url.trim(), + text: docs[dt.id].text.trim() || undefined, + })) + + const startRes = await fetch('/api/sdk/v1/agent/compliance-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + documents: entries, + use_agent: useAgent, + }), + }) + if (!startRes.ok) throw new Error(`Pruefung konnte nicht gestartet werden: ${startRes.status}`) + const { check_id } = await startRes.json() + if (!check_id) throw new Error('Keine Check-ID erhalten') + setActiveCheckId(check_id) + localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id) + + // Poll for results (max 15 min = 300 polls x 3s) + let attempts = 0 + while (attempts < 300) { + await new Promise(r => setTimeout(r, 3000)) + const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`) + if (!pollRes.ok) { attempts++; continue } + const pollData = await pollRes.json() + if (pollData.progress) setProgress(pollData.progress) + if (pollData.status === 'completed' && pollData.result) { + setResults(pollData.result) + setProgress('') + localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(pollData.result)) + localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') + + const resultKey = `compliance-check-result-${Date.now()}` + try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch { /* quota */ } + const entry: HistoryEntry = { + date: new Date().toISOString(), + docCount: entries.length, + findings: pollData.result.total_findings || 0, + resultKey, + } + const updated = [entry, ...history].slice(0, 30) + setHistory(updated) + localStorage.setItem(STORAGE_KEY_HISTORY, JSON.stringify(updated)) + break + } + if (pollData.status === 'failed') { + localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') + throw new Error(pollData.error || 'Pruefung fehlgeschlagen') + } + attempts++ + } + if (attempts >= 300) { + localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') + throw new Error('Zeitlimit ueberschritten (15 Min)') + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Unbekannter Fehler') + setProgress('') + } finally { + setLoading(false) + } + } + + const loadFromHistory = (entry: HistoryEntry) => { + if (entry.resultKey) { + try { + const saved = localStorage.getItem(entry.resultKey) + if (saved) { setResults(JSON.parse(saved)); return } + } catch { /* ignore */ } + } + try { + const last = localStorage.getItem(STORAGE_KEY_RESULTS) + if (last) setResults(JSON.parse(last)) + } catch { /* ignore */ } + } + + return ( +
+ {/* Info box */} +
+

Compliance-Check (Alle Dokumente)

+

+ Geben Sie die URLs Ihrer Rechtstexte ein oder laden Sie die Dokumente hoch. + Das System prueft alle Pflichtangaben nach DSGVO, TDDDG, TMG und UWG. + Pflichtdokumente sind mit * markiert. +

+
+ + {/* Document rows */} +
+ {DOCUMENT_TYPES.map(dt => ( + updateDoc(dt.id, { url })} + onFetchText={() => handleFetchText(dt.id)} + onTextChange={text => updateDoc(dt.id, { text })} + onFileUpload={file => handleFileUpload(dt.id, file)} + /> + ))} +
+ + {/* Agent toggle + submit */} +
+ + + + {filledCount} von {DOCUMENT_TYPES.length} Dokumenten ausgefuellt + +
+ + {/* Submit button */} + + + {/* Progress */} + {progress && ( +
+ + + + + {progress} +
+ )} + + {/* Error */} + {error && ( +
{error}
+ )} + + {/* Results */} + {results && results.results && ( +
+ {/* Business Profile */} + {results.business_profile && ( +
+
Erkanntes Geschaeftsmodell
+
+ Typ: {results.business_profile.business_type?.toUpperCase()} + Branche: {results.business_profile.industry} + {results.business_profile.has_online_shop && Online-Shop} + {results.business_profile.is_regulated_profession && Regulierter Beruf ({results.business_profile.regulated_profession_type})} +
+
+ )} + + {/* Extracted Profile — pre-fill suggestion */} + {results.extracted_profile?.company_profile && Object.keys(results.extracted_profile.company_profile).length > 0 && ( +
+
+ Aus Dokumenten extrahiert + +
+
+ {results.extracted_profile.company_profile.companyName && ( + Firma: {results.extracted_profile.company_profile.companyName} + )} + {results.extracted_profile.company_profile.legalForm && ( + Rechtsform: {results.extracted_profile.company_profile.legalForm.toUpperCase()} + )} + {results.extracted_profile.company_profile.headquartersCity && ( + Sitz: {results.extracted_profile.company_profile.headquartersZip} {results.extracted_profile.company_profile.headquartersCity} + )} + {results.extracted_profile.company_profile.dpoEmail && ( + DSB: {results.extracted_profile.company_profile.dpoEmail} + )} + {results.extracted_profile.company_profile.ustIdNr && ( + USt-IdNr: {results.extracted_profile.company_profile.ustIdNr} + )} +
+ {results.extracted_profile.compliance_scope_hints?.length > 0 && ( +
+ Scope-Hinweise: + {results.extracted_profile.compliance_scope_hints.map((h: any, i: number) => ( + + {h.source} + + ))} +
+ )} +
+ )} + + {/* Banner Check Result */} + {results.banner_result && ( +
0 + ? 'bg-amber-50 border-amber-200' + : results.banner_result.detected + ? 'bg-green-50 border-green-200' + : 'bg-gray-50 border-gray-200' + }`}> +
+ 0 ? 'bg-amber-500' + : results.banner_result.detected ? 'bg-green-500' : 'bg-gray-400' + }`} /> + + Cookie-Banner-Check (automatisch) + +
+
+ {results.banner_result.detected ? ( + <> + Banner erkannt{results.banner_result.provider ? ` (${results.banner_result.provider})` : ''}. + {results.banner_result.violations > 0 + ? ` ${results.banner_result.violations} Auffaelligkeit${results.banner_result.violations !== 1 ? 'en' : ''} gefunden.` + : ' Keine Auffaelligkeiten.'} + + ) : ( + 'Kein Cookie-Banner erkannt oder Banner-Check nicht moeglich.' + )} +
+
+ )} + + + + {/* Email status */} + {results.email_status && ( +
+ + E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status} +
+ )} +
+ )} + + {/* History */} + {history.length > 0 && ( +
+

Letzte Compliance-Checks

+
+ {history.map((h, i) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/ComplianceFAQ.tsx b/admin-compliance/app/sdk/agent/_components/ComplianceFAQ.tsx new file mode 100644 index 0000000..cd282a9 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/ComplianceFAQ.tsx @@ -0,0 +1,145 @@ +'use client' + +import React, { useState } from 'react' + +interface FAQItem { + q: string + a: string +} + +const FAQ_ITEMS: FAQItem[] = [ + { + q: "Was passiert wenn ein Unternehmen wegen unzureichender Datenschutzerklaerung oder Cookie-Richtlinie verklagt wird?", + a: `Es gibt vier Durchsetzungswege: + +**1. Bussgelder durch Aufsichtsbehoerden (Art. 83 DSGVO)** +Aufsichtsbehoerden pruefen von Amts wegen oder auf Beschwerde — kein Klaeger noetig. Bussgelder bis 20 Mio. EUR oder 4% des Jahresumsatzes. Beispiele: CNIL gegen Google (150 Mio. EUR), Facebook (60 Mio. EUR), H&M (35 Mio. EUR). Auch KMU sind betroffen — der LfDI Baden-Wuerttemberg hat Bussgelder ab 10.000 EUR verhaengt. + +**2. Abmahnungen durch Verbraucherschutzverbaende** +Verbaende wie vzbv oder DUH koennen ohne individuellen Schaden klagen (§2 UKlaG). Das ist der groesste praktische Druck: Unterlassungsklage + Anwaltskosten (5.000-20.000 EUR pro Fall). Seit EuGH C-319/20 (Meta/vzbv) duerfen Verbaende DSGVO-Verstoesse auch ohne Betroffenenauftrag klagen. + +**3. Individueller Schadensersatz (Art. 82 DSGVO)** +Seit EuGH C-300/21 (Oesterreichische Post) genuegt bereits der "Kontrollverlust" ueber Daten als immaterieller Schaden — kein messbarer finanzieller Schaden noetig. Typisch: 100-5.000 EUR pro Betroffenem. Legaltech-Firmen wie NOYB buendeln Massenverfahren. + +**4. Wettbewerber-Abmahnungen (UWG)** +Seit 2021 eingeschraenkt, aber Impressums-Maengel oder fehlende Cookie-Einwilligung bleiben abmahnfaehig. + +Die Aufsichtsbehoerden erhalten ueber 10.000 Beschwerden pro Jahr. Eine Beschwerde einzureichen ist kostenlos und mit einem Klick moeglich.`, + }, + { + q: "Wie funktioniert die Dokumentenpruefung?", + a: `Die Pruefung laeuft in drei Schritten: + +**1. Text-Extraktion** — Playwright laedt die Seite, expandiert Accordions/Tabs und extrahiert den vollstaendigen Text. + +**2. Regex-Checks (138 Pruefpunkte)** — Zwei Ebenen: L1 prueft ob Pflichtangaben erwaehnt sind (z.B. "Verantwortlicher"), L2 prueft ob sie korrekt und vollstaendig sind (z.B. "Hat der Verantwortliche eine ladungsfaehige Anschrift mit PLZ?"). + +**3. LLM-Verifikation** — Jeder fehlgeschlagene Check wird von einem KI-Modell (Qwen) gegen den Originaltext gegengeprueft, um Fehlalarme zu eliminieren. + +Das Ergebnis: Zwei Scores pro Dokument — Vollstaendigkeit (sind alle Pflichtangaben da?) und Korrektheit (sind sie richtig formuliert?). Jeder fehlende Punkt hat eine konkrete Handlungsanweisung mit Rechtsbezug.`, + }, + { + q: "Welche Dokumenttypen werden geprueft?", + a: `Sieben Dokumenttypen mit jeweils eigener Checkliste: + +- **Datenschutzinformation (DSI)** — Art. 13/14 DSGVO (31 Checks) +- **Cookie-Richtlinie** — §25 TDDDG (15 Checks) +- **Impressum** — §5 TMG / §18 MStV (16 Checks) +- **AGB** — §305ff BGB (21 Checks) +- **Widerrufsbelehrung** — §355 BGB (15 Checks) +- **Social Media DSE** — Art. 26 DSGVO Joint Controller (20 Checks) +- **DSFA** — Art. 35 DSGVO (18 Checks) + +Sub-Sektionen (z.B. Cookie-Abschnitt innerhalb der DSI) werden automatisch erkannt und separat geprueft.`, + }, + { + q: "Wie zuverlaessig sind die Ergebnisse?", + a: `Die Pruefung wurde gegen mehrere Ground-Truth-Websites validiert (IHK Konstanz, ETO Gruppe, BMW, Stadt Koeln, Sparkasse, Spiegel u.a.). Ergebnis: **0 False Positives** bei validierten Testfaellen — jeder rote Punkt ist ein echtes Finding. + +Durch die LLM-Verifikation werden Regex-Fehlalarme (z.B. durch ungewoehnliche Formatierung oder Soft Hyphens im HTML) automatisch korrigiert. Trotzdem gilt: Das Tool ersetzt keine Rechtsberatung, sondern identifiziert Handlungsbedarf.`, + }, + { + q: "Was kostet ein Verstoss gegen die DSGVO in der Praxis?", + a: `Bussgelder nach Art. 83 DSGVO staffeln sich in zwei Stufen: + +- **Bis 10 Mio. EUR / 2% Umsatz**: Verstoesse gegen technische/organisatorische Pflichten (Art. 25, 28, 32) +- **Bis 20 Mio. EUR / 4% Umsatz**: Verstoesse gegen Grundsaetze, Betroffenenrechte, Drittlandtransfer + +Typische Praxis-Bussgelder in Deutschland: 5.000-50.000 EUR fuer KMU, 100.000-1 Mio. EUR fuer groessere Unternehmen. Dazu kommen Anwaltskosten bei Abmahnungen (5.000-20.000 EUR pro Fall) und Reputationsschaden.`, + }, + { + q: "Was ist der aktuelle Stand bei harmonisierten Normen unter der neuen Maschinenverordnung (EU) 2023/1230?", + a: `Die Maschinenverordnung (EU) 2023/1230 hat in Anhang I die wesentlichen Gesundheits- und Sicherheitsanforderungen und verweist darauf, dass harmonisierte Normen die technischen Details liefern sollen (Konformitaetsvermutung). + +**Aktueller Stand:** Es gibt noch KEINE harmonisierten Normen die unter der neuen Maschinenverordnung im EU-Amtsblatt veroeffentlicht sind. Die bestehenden ~800 harmonisierten Normen gelten noch unter der alten Maschinenrichtlinie 2006/42/EC. + +**Zeitplan:** +- **Juni 2023** — Maschinenverordnung veroeffentlicht +- **Januar 2025** — EU-Kommission hat Normungsauftrag an CEN/CENELEC erteilt +- **Januar 2026** — CEN/CENELEC soll bestehende Normen bestaetigen oder Nachfolgenormen verabschieden +- **Januar 2027** — Maschinenverordnung tritt vollstaendig in Kraft, ersetzt alte Richtlinie 2006/42/EC + +**Wichtig fuer Hersteller:** Bis die neuen harmonisierten Normen veroeffentlicht sind, koennen Hersteller die bestehenden Normen der alten Maschinenrichtlinie weiterhin anwenden. Nach dem 20. Januar 2027 muessen Maschinen aber die Anforderungen der neuen Verordnung erfuellen — auch wenn die harmonisierten Normen bis dahin nicht vollstaendig vorliegen. + +**IACE Normen-Bibliothek:** Die aktuelle Liste unter /sdk/iace/library enthaelt 751 harmonisierte Normen (1 A-Norm, 19 B1, 126 B2, 605 C-Normen). Diese muessen regelmaessig gegen das EU-Amtsblatt abgeglichen werden, da einige Normen zurueckgezogen oder ersetzt wurden.`, + }, + { + q: "Warum muss ich harmonisierte Normen kaufen obwohl sie EU-Recht sind?", + a: `Harmonisierte Normen werden von privaten Organisationen (CEN/CENELEC) erstellt und ueber nationale Normungsinstitute wie DIN/Beuth (Deutschland), ASI (Oesterreich) oder SNV (Schweiz) verkauft — typisch 50-300 EUR pro Norm. + +**Das Problem:** Die EU-Kommission beauftragt die Normung, Industrieexperten schreiben die Normen ehrenamtlich in Technischen Komitees, aber ein privater Verlag verkauft das Ergebnis. Unternehmen muessen Normen kaufen die ihre eigenen Mitarbeiter geschrieben haben. + +**EuGH-Urteil C-588/21 P (5. Maerz 2024):** +Der Europaeische Gerichtshof hat entschieden, dass harmonisierte Normen **Teil des EU-Rechts** sind, weil sie eine Konformitaetsvermutung erzeugen. Das Rechtsstaatsprinzip verlangt, dass Buerger die Regeln kennen koennen die fuer sie gelten. Daher muessen harmonisierte Normen grundsaetzlich **frei zugaenglich** sein. + +**Aktueller Stand (2026):** Das Urteil ist noch nicht vollstaendig umgesetzt. CEN/CENELEC und die nationalen Normungsinstitute wehren sich, weil ihr Geschaeftsmodell auf dem Verkauf basiert. Die EU-Kommission arbeitet an einer Loesung. + +**Was das fuer Unternehmen bedeutet:** +- Aktuell muessen Normen weiterhin gekauft werden +- Normnummern und Titel sind frei nutzbar (bibliographische Daten) +- BSI-Grundschutz und NIST-Standards sind kostenlose Alternativen die inhaltlich aehnliche Anforderungen abdecken +- Die IACE-Bibliothek in BreakPilot listet alle harmonisierten Normen mit Status (aktiv/zurueckgezogen) ohne kostenpflichtigen Normtext`, + }, +] + +export function ComplianceFAQ() { + const [open, setOpen] = useState(null) + + return ( +
+
+

Haeufige Fragen

+
+
+ {FAQ_ITEMS.map((item, i) => ( +
+ + {open === i && ( +
+ {item.a.split('\n\n').map((para, pi) => ( +

$1') + .replace(/\n- /g, '
• ') + .replace(/\n/g, '
') + }} /> + ))} +

+ )} +
+ ))} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx b/admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx new file mode 100644 index 0000000..c2dbd7c --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx @@ -0,0 +1,248 @@ +'use client' + +import React from 'react' + +interface Violation { + service: string + severity: string + text: string + legal_ref: string +} + +interface PhaseData { + scripts: string[] + cookies: string[] + tracking_services?: string[] + new_tracking?: string[] + violations?: Violation[] + undocumented?: string[] +} + +interface ConsentData { + banner_detected: boolean + banner_provider: string + phases: { + before_consent: PhaseData + after_reject: PhaseData + after_accept: PhaseData + } + summary: { + critical: number + high: number + undocumented: number + total_violations: number + category_violations?: number + categories_tested?: number + } + banner_checks?: { + has_impressum_link: boolean + has_dse_link: boolean + violations: { service: string; severity: string; text: string; legal_ref: string }[] + } + category_tests?: { + category: string + category_label: string + tracking_services: string[] + violations: { service: string; severity: string; text: string }[] + }[] +} + +const SEV = { + CRITICAL: { bg: 'bg-red-100 border-red-300', text: 'text-red-800', badge: 'bg-red-600' }, + HIGH: { bg: 'bg-orange-100 border-orange-300', text: 'text-orange-800', badge: 'bg-orange-500' }, +} + +function PhaseCard({ title, icon, data, type }: { + title: string; icon: string; data: PhaseData; type: 'before' | 'reject' | 'accept' +}) { + const violations = data.violations || [] + const tracking = data.tracking_services || data.new_tracking || [] + const undocumented = data.undocumented || [] + const hasProblem = violations.length > 0 || undocumented.length > 0 + + return ( +
+

+ {icon} {title} +

+ + {/* Violations */} + {violations.map((v, i) => ( +
+
+ + {v.severity} + + + {v.service} + +
+

{v.text}

+

{v.legal_ref}

+
+ ))} + + {/* Undocumented (Phase C only) */} + {undocumented.map((s, i) => ( +
+ ✗ {s} — nicht in Cookie-Policy dokumentiert +
+ ))} + + {/* Tracking services (no violations) */} + {violations.length === 0 && undocumented.length === 0 && tracking.length > 0 && ( +
+ {tracking.map((t, i) =>
✓ {t} — {type === 'accept' ? 'mit Consent OK' : 'erkannt'}
)} +
+ )} + + {violations.length === 0 && undocumented.length === 0 && tracking.length === 0 && ( +

✓ Keine Tracking-Dienste erkannt

+ )} + + {/* Cookie/Script count */} +
+ {data.scripts?.length || 0} Scripts + {data.cookies?.length || 0} Cookies +
+
+ ) +} + +export function ConsentTestResult({ data }: { data: ConsentData }) { + const s = data.summary + + return ( +
+ {/* Header */} +
+
+ + + Cookie-Banner: {data.banner_detected ? data.banner_provider : 'Nicht erkannt'} + +
+
+ {s.critical > 0 && ( + + {s.critical} Kritisch + + )} + {s.high > 0 && ( + + {s.high} Hoch + + )} + {s.total_violations === 0 && ( + + Keine Verstoesse + + )} +
+
+ + {/* Three Phases */} +
+ + {data.banner_detected && ( + <> + + + + )} +
+ + {/* Banner Text Checks */} + {data.banner_checks && (data.banner_checks.violations?.length > 0 || data.banner_checks.has_impressum_link !== undefined) && ( +
+

+ 📝 Banner-Text Pruefung +

+
+ + {data.banner_checks.has_impressum_link ? '✓' : '✗'} Impressum-Link + + + {data.banner_checks.has_dse_link ? '✓' : '✗'} DSE-Link + +
+ {data.banner_checks.violations?.map((v: any, i: number) => { + const isHigh = v.severity === 'HIGH' || v.severity === 'CRITICAL' + return ( +
+
+ + {v.severity} + +
+

{v.text}

+

{v.legal_ref}

+
+
+
+ ) + })} + {(!data.banner_checks.violations || data.banner_checks.violations.length === 0) && ( +

✓ Keine Banner-Text-Verstoesse erkannt

+ )} +
+ )} + + {/* Category Tests (Phase D-F) */} + {data.category_tests && data.category_tests.length > 0 && ( +
+

Kategorie-Tests ({data.category_tests.length})

+ {data.category_tests.map((ct, i) => { + const hasViolations = ct.violations.length > 0 + return ( +
+

+ 🔀 Nur "{ct.category_label}" +

+ {ct.violations.length > 0 ? ( + ct.violations.map((v, vi) => ( +
+ FALSCH + {v.text} +
+ )) + ) : ( +
+ {ct.tracking_services.length > 0 ? ( + ct.tracking_services.map((s, si) =>
✓ {s} — korrekte Kategorie
) + ) : ( +
✓ Keine Tracking-Dienste geladen — korrekt
+ )} +
+ )} +
+ ) + })} +
+ )} + + {/* No banner warning */} + {!data.banner_detected && ( +
+ Kein Cookie-Banner erkannt. Alle erkannten Tracking-Dienste laden ohne + Einwilligung — dies ist ein Verstoss gegen §25 TDDDG. +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx new file mode 100644 index 0000000..33e1c8f --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/DocCheckTab.tsx @@ -0,0 +1,320 @@ +'use client' + +import React, { useState } from 'react' +import { ChecklistView } from './ChecklistView' + +interface DocEntry { + id: string + type: string + label: string + url: string +} + +const DOC_TYPES = [ + { id: 'dse', label: 'DSI (Datenschutzinformation)' }, + { id: 'social_media', label: 'DSE Social Media (Art. 26)' }, + { id: 'dsfa', label: 'DSFA (Art. 35)' }, + { id: 'agb', label: 'AGB / Nutzungsbedingungen' }, + { id: 'impressum', label: 'Impressum' }, + { id: 'cookie', label: 'Cookie-Richtlinie' }, + { id: 'widerruf', label: 'Widerrufsbelehrung' }, + { id: 'other', label: 'Sonstiges' }, +] + +function newEntry(): DocEntry { + return { id: crypto.randomUUID().slice(0, 8), type: 'dse', label: '', url: '' } +} + +export function DocCheckTab() { + const [entries, setEntries] = useState(() => { + if (typeof window === 'undefined') return [newEntry()] + try { const s = localStorage.getItem('doc-check-entries'); return s ? JSON.parse(s) : [newEntry()] } catch { return [newEntry()] } + }) + const [checkCookieBanner, setCheckCookieBanner] = useState(false) + const [useAgent, setUseAgent] = useState(false) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState('') + const [results, setResults] = useState(() => { + if (typeof window === 'undefined') return null + try { const s = localStorage.getItem('doc-check-results'); return s ? JSON.parse(s) : null } catch { return null } + }) + const [error, setError] = useState(null) + const [history, setHistory] = useState<{ date: string; urls: number; findings: number; resultKey: string }[]>(() => { + if (typeof window === 'undefined') return [] + try { return JSON.parse(localStorage.getItem('doc-check-history') || '[]') } catch { return [] } + }) + + // Persist entries + React.useEffect(() => { localStorage.setItem('doc-check-entries', JSON.stringify(entries)) }, [entries]) + + const updateEntry = (id: string, field: keyof DocEntry, value: string) => { + setEntries(prev => prev.map(e => e.id === id ? { ...e, [field]: value } : e)) + } + + const removeEntry = (id: string) => { + setEntries(prev => prev.filter(e => e.id !== id)) + } + + const addEntry = () => { + setEntries(prev => [...prev, newEntry()]) + } + + // Auto-detect label from URL + const autoLabel = (entry: DocEntry) => { + if (entry.label) return + try { + const path = new URL(entry.url).pathname + const last = path.split('/').filter(Boolean).pop() || '' + const label = last.replace(/-\d+$/, '').replace(/-/g, ' ') + .replace(/\b\w/g, c => c.toUpperCase()) + if (label.length > 3) { + updateEntry(entry.id, 'label', label) + } + } catch { /* invalid URL */ } + } + + const handleSubmit = async () => { + const validEntries = entries.filter(e => e.url.trim()) + if (validEntries.length === 0) return + + setLoading(true) + setError(null) + setResults(null) + setProgress('Pruefung wird gestartet...') + + try { + const startRes = await fetch('/api/sdk/v1/agent/doc-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + entries: validEntries.map(e => ({ + doc_type: e.type, + label: e.label || e.url.split('/').pop() || 'Dokument', + url: e.url.trim(), + })), + check_cookie_banner: checkCookieBanner, + use_agent: useAgent, + }), + }) + if (!startRes.ok) throw new Error(`Pruefung konnte nicht gestartet werden: ${startRes.status}`) + const { check_id } = await startRes.json() + if (!check_id) throw new Error('Keine Check-ID erhalten') + + // Poll for results + let attempts = 0 + while (attempts < 120) { + await new Promise(r => setTimeout(r, 3000)) + const pollRes = await fetch(`/api/sdk/v1/agent/doc-check?check_id=${check_id}`) + if (!pollRes.ok) { attempts++; continue } + const pollData = await pollRes.json() + if (pollData.progress) setProgress(pollData.progress) + if (pollData.status === 'completed' && pollData.result) { + setResults(pollData.result) + setProgress('') + localStorage.setItem('doc-check-results', JSON.stringify(pollData.result)) + const resultKey = `doc-check-result-${Date.now()}` + try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch { /* quota */ } + const entry = { date: new Date().toISOString(), urls: validEntries.length, findings: pollData.result.total_findings || 0, resultKey } + const updated = [entry, ...history].slice(0, 30) + setHistory(updated) + localStorage.setItem('doc-check-history', JSON.stringify(updated)) + break + } + if (pollData.status === 'failed') { + throw new Error(pollData.error || 'Pruefung fehlgeschlagen') + } + attempts++ + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Unbekannter Fehler') + setProgress('') + } finally { + setLoading(false) + } + } + + return ( +
+ {/* URL Entries */} +
+ {entries.map((entry, i) => ( +
+ + updateEntry(entry.id, 'label', e.target.value)} + placeholder={entry.type === 'other' ? 'Dokumentname' : 'Version / Stand (optional)'} + className="w-40 px-3 py-2.5 border border-gray-300 rounded-lg text-sm shrink-0" + /> + updateEntry(entry.id, 'url', e.target.value)} + onBlur={() => autoLabel(entry)} + placeholder="https://example.com/datenschutz" + className="flex-1 px-3 py-2.5 border border-gray-300 rounded-lg text-sm" + /> + {entries.length > 1 && ( + + )} +
+ ))} +
+ + {/* Add URL + Options */} +
+ + + + + +
+ + {/* Submit */} + + + {/* Progress */} + {progress && ( +
+ + + + + {progress} +
+ )} + + {/* Error */} + {error && ( +
{error}
+ )} + + {/* Results */} + {results && results.results && ( +
+ + + {/* Cookie Banner Result */} + {results.cookie_banner_result && ( +
+

Cookie-Banner

+
+ {results.cookie_banner_result.banner_detected + ? `Banner erkannt: ${results.cookie_banner_result.banner_provider || 'unbekannt'}` + : 'Kein Banner erkannt'} +
+ {results.cookie_banner_result.banner_checks?.violations?.length > 0 && ( +
+ {results.cookie_banner_result.banner_checks.violations.map((v: any, i: number) => ( +
+ !! + {v.text} +
+ ))} +
+ )} +
+ )} + + {/* Email Status */} + {results.email_status && ( +
+ + E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status} +
+ )} +
+ )} + + {/* History */} + {history.length > 0 && ( +
+

Letzte Pruefungen

+
+ {history.map((h, i) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/DocumentRow.tsx b/admin-compliance/app/sdk/agent/_components/DocumentRow.tsx new file mode 100644 index 0000000..0bb1f96 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/DocumentRow.tsx @@ -0,0 +1,163 @@ +'use client' + +import React, { useState, useRef } from 'react' + +interface DocumentRowProps { + label: string + docType: string + required?: boolean + url: string + text: string + loading: boolean + error: string | null + wordCount: number + onUrlChange: (url: string) => void + onFetchText: () => void + onTextChange: (text: string) => void + onFileUpload: (file: File) => void +} + +export function DocumentRow({ + label, + docType, + required, + url, + text, + loading, + error, + wordCount, + onUrlChange, + onFetchText, + onTextChange, + onFileUpload, +}: DocumentRowProps) { + const [showText, setShowText] = useState(false) + const fileRef = useRef(null) + + const textVisible = showText || text.length > 0 + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + // Read text-based files directly + const reader = new FileReader() + reader.onload = () => { + const content = reader.result as string + onTextChange(content) + } + reader.onerror = () => { + // Let parent handle via onFileUpload for binary formats + onFileUpload(file) + } + + if (file.name.endsWith('.txt') || file.type === 'text/plain') { + reader.readAsText(file) + } else { + // PDF, DOCX — pass to parent for server-side parsing + onFileUpload(file) + } + + // Reset input so the same file can be re-selected + e.target.value = '' + } + + return ( +
+ {/* Header row: label + inputs */} +
+
+ + {label} + {required && *} + +
+ + onUrlChange(e.target.value)} + placeholder="https://example.com/datenschutz" + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> + + {/* Fetch text button */} + + + {/* File upload button */} + + + + {/* Toggle text area */} + + + {/* Word count badge */} + {wordCount > 0 && ( + + {wordCount.toLocaleString('de-DE')} W. + + )} +
+ + {/* Error */} + {error && ( +
{error}
+ )} + + {/* Collapsible textarea */} + {textVisible && ( +