Merge feat/zeroclaw-compliance-agent into main

Brings all compliance doc-check features:
- 162 regex checks + 1874 Master Controls
- LLM-agnostic agent with tool calling
- Banner check (46 checks, 30 CMPs, stealth, Shadow DOM)
- Impressum check (24 checks)
- Deep consent verification (DataLayer, GCM, TCF)
- CMP E2E tests (39 tests)
- HTML email reports, FAQ, persistent history

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-11 11:44:20 +02:00
175 changed files with 20063 additions and 1283 deletions
@@ -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<string, AuthCheck>
findings_count: number
}
const CHECK_LABELS: Record<string, { label: string; icon: string }> = {
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 (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm font-medium text-red-800">Login fehlgeschlagen</p>
<p className="text-xs text-red-600 mt-1">{data.login_error || 'Credentials oder Formular nicht erkannt'}</p>
</div>
)
}
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-green-500" />
<span className="text-sm font-medium text-gray-900">Erfolgreich eingeloggt</span>
<span className={`ml-auto text-xs px-2 py-1 rounded font-medium ${data.findings_count > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
{data.findings_count} fehlende Funktionen
</span>
</div>
<div className="space-y-2">
{Object.entries(data.checks).map(([key, check]) => {
const info = CHECK_LABELS[key] || { label: key, icon: '❓' }
return (
<div key={key} className={`flex items-center gap-3 p-3 rounded-lg border ${check.found ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<span className="text-lg">{info.icon}</span>
<div className="flex-1">
<p className={`text-sm font-medium ${check.found ? 'text-green-800' : 'text-red-800'}`}>
{check.found ? '✓' : '✗'} {info.label}
</p>
{check.text && <p className="text-xs text-gray-500 mt-0.5">{check.text}</p>}
</div>
<span className="text-[10px] text-gray-400">{check.legal_ref}</span>
</div>
)
})}
</div>
{data.findings_count > 0 && (
<div className="bg-red-50 border-l-4 border-red-500 p-3 text-xs text-red-700">
<strong>{data.findings_count} Pflichtfunktion(en) fehlen.</strong> Der Nutzer kann seine Rechte
nach DSGVO nicht vollstaendig ausueben.
</div>
)}
</div>
)
}
@@ -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<string, string> = {
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 (
<div className="space-y-4">
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50">
<th className="text-left px-3 py-2 text-xs font-medium text-gray-500 w-44">Pruefung</th>
{sites.map((s, i) => (
<th key={i} className="text-center px-3 py-2 text-xs font-medium text-gray-700">
{s.domain}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tr>
<td className="px-3 py-2 text-gray-600">Risiko-Score</td>
{sites.map((s, i) => (
<td key={i} className="px-3 py-2 text-center">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${RISK_COLOR[s.risk_level] || 'text-gray-600 bg-gray-50'}`}>
{s.risk_level || '?'} ({s.risk_score}/100)
</span>
</td>
))}
</tr>
<tr>
<td className="px-3 py-2 text-gray-600">Findings</td>
{sites.map((s, i) => (
<td key={i} className={`px-3 py-2 text-center font-medium ${s.findings_count > 0 ? 'text-red-700' : 'text-green-700'}`}>
{s.findings_count}
</td>
))}
</tr>
<tr>
<td className="px-3 py-2 text-gray-600">Dienste erkannt</td>
{sites.map((s, i) => (
<td key={i} className="px-3 py-2 text-center text-gray-700">{s.services_count}</td>
))}
</tr>
{checks.map(check => (
<tr key={check.key}>
<td className="px-3 py-2 text-gray-600">{check.label}</td>
{sites.map((s, i) => {
const val = (s as any)[check.key]
const isInverted = check.key === 'has_google_fonts'
const good = isInverted ? !val : val
return (
<td key={i} className={`px-3 py-2 text-center font-medium ${good ? 'text-green-600' : 'text-red-600'}`}>
{good ? '✓' : '✗'}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
@@ -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 (
<div className={`border rounded-lg p-4 ${hasProblem ? 'border-red-200 bg-red-50' : 'border-green-200 bg-green-50'}`}>
<h4 className="text-sm font-semibold text-gray-900 mb-2 flex items-center gap-2">
<span>{icon}</span> {title}
</h4>
{/* Violations */}
{violations.map((v, i) => (
<div key={i} className={`mb-2 p-2 rounded border ${SEV[v.severity as keyof typeof SEV]?.bg || SEV.HIGH.bg}`}>
<div className="flex items-center gap-2">
<span className={`text-[10px] px-1.5 py-0.5 rounded text-white ${SEV[v.severity as keyof typeof SEV]?.badge || SEV.HIGH.badge}`}>
{v.severity}
</span>
<span className={`text-xs font-medium ${SEV[v.severity as keyof typeof SEV]?.text || SEV.HIGH.text}`}>
{v.service}
</span>
</div>
<p className="text-xs text-gray-700 mt-1">{v.text}</p>
<p className="text-[10px] text-gray-500 mt-0.5">{v.legal_ref}</p>
</div>
))}
{/* Undocumented (Phase C only) */}
{undocumented.map((s, i) => (
<div key={i} className="mb-2 p-2 rounded border border-yellow-300 bg-yellow-50">
<span className="text-xs text-yellow-800"> {s} nicht in Cookie-Policy dokumentiert</span>
</div>
))}
{/* Tracking services (no violations) */}
{violations.length === 0 && undocumented.length === 0 && tracking.length > 0 && (
<div className="text-xs text-green-700">
{tracking.map((t, i) => <div key={i}> {t} {type === 'accept' ? 'mit Consent OK' : 'erkannt'}</div>)}
</div>
)}
{violations.length === 0 && undocumented.length === 0 && tracking.length === 0 && (
<p className="text-xs text-green-700"> Keine Tracking-Dienste erkannt</p>
)}
{/* Cookie/Script count */}
<div className="flex gap-3 mt-2 text-[10px] text-gray-400">
<span>{data.scripts?.length || 0} Scripts</span>
<span>{data.cookies?.length || 0} Cookies</span>
</div>
</div>
)
}
export function ConsentTestResult({ data }: { data: ConsentData }) {
const s = data.summary
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`w-3 h-3 rounded-full ${data.banner_detected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm font-medium text-gray-900">
Cookie-Banner: {data.banner_detected ? data.banner_provider : 'Nicht erkannt'}
</span>
</div>
<div className="flex gap-2">
{s.critical > 0 && (
<span className="text-xs px-2 py-1 rounded bg-red-600 text-white font-medium">
{s.critical} Kritisch
</span>
)}
{s.high > 0 && (
<span className="text-xs px-2 py-1 rounded bg-orange-500 text-white font-medium">
{s.high} Hoch
</span>
)}
{s.total_violations === 0 && (
<span className="text-xs px-2 py-1 rounded bg-green-500 text-white font-medium">
Keine Verstoesse
</span>
)}
</div>
</div>
{/* Three Phases */}
<div className="space-y-3">
<PhaseCard
title="Phase A: Vor Einwilligung"
icon="🔍"
data={data.phases.before_consent}
type="before"
/>
{data.banner_detected && (
<>
<PhaseCard
title="Phase B: Nach Ablehnung"
icon="🚫"
data={data.phases.after_reject}
type="reject"
/>
<PhaseCard
title="Phase C: Nach Zustimmung"
icon="✅"
data={data.phases.after_accept}
type="accept"
/>
</>
)}
</div>
{/* Banner Text Checks */}
{data.banner_checks && (data.banner_checks.violations?.length > 0 || data.banner_checks.has_impressum_link !== undefined) && (
<div className="border rounded-lg p-4 border-gray-200 bg-gray-50">
<h4 className="text-sm font-semibold text-gray-900 mb-3 flex items-center gap-2">
<span>📝</span> Banner-Text Pruefung
</h4>
<div className="flex gap-3 mb-3 text-xs">
<span className={data.banner_checks.has_impressum_link ? 'text-green-600' : 'text-red-600'}>
{data.banner_checks.has_impressum_link ? '✓' : '✗'} Impressum-Link
</span>
<span className={data.banner_checks.has_dse_link ? 'text-green-600' : 'text-red-600'}>
{data.banner_checks.has_dse_link ? '✓' : '✗'} DSE-Link
</span>
</div>
{data.banner_checks.violations?.map((v: any, i: number) => {
const isHigh = v.severity === 'HIGH' || v.severity === 'CRITICAL'
return (
<div key={i} className={`mb-2 p-2 rounded border ${isHigh ? 'border-red-300 bg-red-50' : 'border-yellow-300 bg-yellow-50'}`}>
<div className="flex items-start gap-2">
<span className={`text-[10px] px-1.5 py-0.5 rounded text-white ${isHigh ? 'bg-red-600' : 'bg-yellow-600'}`}>
{v.severity}
</span>
<div>
<p className="text-xs text-gray-800">{v.text}</p>
<p className="text-[10px] text-gray-500 mt-0.5">{v.legal_ref}</p>
</div>
</div>
</div>
)
})}
{(!data.banner_checks.violations || data.banner_checks.violations.length === 0) && (
<p className="text-xs text-green-700"> Keine Banner-Text-Verstoesse erkannt</p>
)}
</div>
)}
{/* Category Tests (Phase D-F) */}
{data.category_tests && data.category_tests.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-900 mt-2">Kategorie-Tests ({data.category_tests.length})</h4>
{data.category_tests.map((ct, i) => {
const hasViolations = ct.violations.length > 0
return (
<div key={i} className={`border rounded-lg p-4 ${hasViolations ? 'border-red-200 bg-red-50' : 'border-green-200 bg-green-50'}`}>
<h4 className="text-sm font-semibold text-gray-900 mb-2 flex items-center gap-2">
<span>🔀</span> Nur &quot;{ct.category_label}&quot;
</h4>
{ct.violations.length > 0 ? (
ct.violations.map((v, vi) => (
<div key={vi} className="mb-2 p-2 rounded border border-red-300 bg-red-100">
<span className="text-xs font-bold text-red-800 px-1.5 py-0.5 rounded bg-red-200">FALSCH</span>
<span className="text-xs text-red-700 ml-2">{v.text}</span>
</div>
))
) : (
<div className="text-xs text-green-700">
{ct.tracking_services.length > 0 ? (
ct.tracking_services.map((s, si) => <div key={si}> {s} korrekte Kategorie</div>)
) : (
<div> Keine Tracking-Dienste geladen korrekt</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
{/* No banner warning */}
{!data.banner_detected && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-xs text-red-700">
<strong>Kein Cookie-Banner erkannt.</strong> Alle erkannten Tracking-Dienste laden ohne
Einwilligung dies ist ein Verstoss gegen §25 TDDDG.
</div>
)}
</div>
)
}
@@ -1,6 +1,7 @@
'use client'
import React, { useState } from 'react'
import { TextReference } from './TextReference'
interface ServiceInfo {
name: string
@@ -14,11 +15,27 @@ interface ServiceInfo {
status: string
}
interface TextRef {
found: boolean
source_url: string
document_type: string
section_heading: string
section_number: string
parent_section: string
paragraph_index: number
original_text: string
issue: string
correction_type: string
correction_text: string
insert_after: string
}
interface ScanFinding {
code: string
severity: string
text: string
correction: string
<<<<<<< HEAD
doc_title: string
}
@@ -30,6 +47,9 @@ interface DiscoveredDocument {
word_count: number
completeness_pct: number
findings_count: number
=======
text_reference: TextRef | null
>>>>>>> feat/zeroclaw-compliance-agent
}
interface ScanData {
@@ -249,7 +269,12 @@ export function ScanResult({ data }: { data: ScanData }) {
</span>
<p className="text-sm text-gray-800 flex-1">{f.text}</p>
</div>
{f.correction && (
{/* Text Reference (original text + position + correction) */}
{f.text_reference && (
<TextReference ref={f.text_reference} correction={f.correction} />
)}
{/* Fallback: correction without text reference */}
{!f.text_reference && f.correction && (
<div className="mt-2">
<button onClick={() => setExpandedCorrection(isExp ? null : corrKey)}
className="text-xs text-purple-600 hover:text-purple-800 font-medium">
@@ -272,6 +297,7 @@ export function ScanResult({ data }: { data: ScanData }) {
</div>
</div>
)}
<<<<<<< HEAD
{/* Email Status */}
{data.email_status && (
@@ -280,6 +306,37 @@ export function ScanResult({ data }: { data: ScanData }) {
E-Mail: {data.email_status === 'sent' ? 'Gesendet' : data.email_status}
</div>
)}
=======
{/* PDF Export Button */}
<div className="pt-4 border-t flex gap-3">
<button
onClick={async () => {
try {
const res = await fetch('/api/sdk/v1/agent/scans/pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: '', scan_type: 'scan', analysis_mode: 'post_launch', result: data }),
})
if (res.ok) {
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'compliance-report.pdf'
a.click()
URL.revokeObjectURL(url)
}
} catch (e) { console.error('PDF export failed:', e) }
}}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
PDF herunterladen
</button>
</div>
>>>>>>> feat/zeroclaw-compliance-agent
</div>
)
}
@@ -0,0 +1,108 @@
'use client'
import React, { useState } from 'react'
interface TextRef {
found: boolean
source_url: string
document_type: string
section_heading: string
section_number: string
parent_section: string
paragraph_index: number
original_text: string
issue: string
correction_type: string
correction_text: string
insert_after: string
}
const ISSUE_LABELS: Record<string, { label: string; color: string }> = {
missing: { label: 'Fehlt in der DSE', color: 'text-red-700 bg-red-50' },
incomplete: { label: 'Unvollstaendig', color: 'text-yellow-700 bg-yellow-50' },
incorrect: { label: 'Fehlerhaft', color: 'text-orange-700 bg-orange-50' },
}
const CORRECTION_LABELS: Record<string, string> = {
insert: 'Neuen Abschnitt einfuegen',
append: 'Am Ende des Absatzes ergaenzen',
replace: 'Absatz ersetzen',
}
export function TextReference({ ref, correction }: { ref: TextRef; correction?: string }) {
const [showCorrection, setShowCorrection] = useState(false)
const issue = ISSUE_LABELS[ref.issue] || null
const correctionText = correction || ref.correction_text
return (
<div className="mt-3 space-y-2 text-sm">
{/* Original Text Block */}
<div>
<p className="text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
<span>📄</span> Originaltextblock:
</p>
<div className={`rounded-lg p-3 border ${ref.found ? 'bg-gray-50 border-gray-200' : 'bg-red-50 border-red-200'}`}>
{ref.found ? (
<p className="text-gray-700 text-xs whitespace-pre-wrap">{ref.original_text || '(Textinhalt konnte nicht extrahiert werden)'}</p>
) : (
<p className="text-red-600 text-xs italic">Nicht vorhanden Eintrag fehlt in der {ref.document_type}.</p>
)}
</div>
</div>
{/* Position */}
<div>
<p className="text-xs font-medium text-gray-500 mb-1 flex items-center gap-1">
<span>📍</span> Position:
</p>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-2 text-xs text-blue-800">
{ref.found ? (
<>
<span className="font-semibold">{ref.section_heading || 'Abschnitt unbekannt'}</span>
{ref.section_number && <span className="text-blue-600 ml-1">(Nr. {ref.section_number})</span>}
{ref.parent_section && <span className="text-blue-500 ml-1">in: {ref.parent_section}</span>}
{ref.paragraph_index > 0 && <span className="text-blue-500 ml-1">| Absatz {ref.paragraph_index}</span>}
</>
) : ref.insert_after ? (
<span><strong>{CORRECTION_LABELS[ref.correction_type] || 'Einfuegen'}</strong> nach Abschnitt &quot;{ref.insert_after}&quot;</span>
) : (
<span>Neuen Abschnitt in der {ref.document_type} anlegen</span>
)}
{ref.source_url && (
<div className="text-blue-400 mt-1 truncate">in: {ref.source_url}</div>
)}
</div>
</div>
{/* Correction */}
{correctionText && (
<div>
<button
onClick={() => setShowCorrection(!showCorrection)}
className="text-xs text-purple-600 hover:text-purple-800 font-medium flex items-center gap-1"
>
<span>{showCorrection ? '▼' : '▶'}</span>
<span></span> Korrekturvorschlag {showCorrection ? 'ausblenden' : 'anzeigen'}
</button>
{showCorrection && (
<div className="mt-2 bg-white border border-purple-200 rounded-lg p-3 relative">
{issue && (
<span className={`text-[10px] px-2 py-0.5 rounded-full font-medium mb-2 inline-block ${issue.color}`}>
{CORRECTION_LABELS[ref.correction_type] || issue.label}
</span>
)}
<pre className="text-xs text-gray-700 whitespace-pre-wrap font-sans mt-1">{correctionText}</pre>
<button
onClick={() => navigator.clipboard.writeText(correctionText)}
className="absolute top-2 right-2 text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded transition-colors"
title="In Zwischenablage kopieren"
>
📋 Kopieren
</button>
</div>
)}
</div>
)}
</div>
)
}