Initial commit: breakpilot-lehrer - Lehrer KI Platform

Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions
+364
View File
@@ -0,0 +1,364 @@
/**
* Constants for Companion Module
* Phase colors, defaults, and configuration
*/
import { PhaseId, PhaseDurations, Phase, TeacherSettings } from './types'
// ============================================================================
// Phase Colors (Didactic Color Psychology)
// ============================================================================
export const PHASE_COLORS: Record<PhaseId, { hex: string; tailwind: string; gradient: string }> = {
einstieg: {
hex: '#4A90E2',
tailwind: 'bg-blue-500',
gradient: 'from-blue-500 to-blue-600',
},
erarbeitung: {
hex: '#F5A623',
tailwind: 'bg-orange-500',
gradient: 'from-orange-500 to-orange-600',
},
sicherung: {
hex: '#7ED321',
tailwind: 'bg-green-500',
gradient: 'from-green-500 to-green-600',
},
transfer: {
hex: '#9013FE',
tailwind: 'bg-purple-600',
gradient: 'from-purple-600 to-purple-700',
},
reflexion: {
hex: '#6B7280',
tailwind: 'bg-gray-500',
gradient: 'from-gray-500 to-gray-600',
},
}
// ============================================================================
// Phase Definitions
// ============================================================================
export const PHASE_SHORT_NAMES: Record<PhaseId, string> = {
einstieg: 'E',
erarbeitung: 'A',
sicherung: 'S',
transfer: 'T',
reflexion: 'R',
}
export const PHASE_DISPLAY_NAMES: Record<PhaseId, string> = {
einstieg: 'Einstieg',
erarbeitung: 'Erarbeitung',
sicherung: 'Sicherung',
transfer: 'Transfer',
reflexion: 'Reflexion',
}
export const PHASE_DESCRIPTIONS: Record<PhaseId, string> = {
einstieg: 'Motivation, Kontext setzen, Vorwissen aktivieren',
erarbeitung: 'Hauptinhalt, aktives Lernen, neue Konzepte',
sicherung: 'Konsolidierung, Zusammenfassung, Uebungen',
transfer: 'Anwendung, neue Kontexte, kreative Aufgaben',
reflexion: 'Rueckblick, Selbsteinschaetzung, Ausblick',
}
export const PHASE_ORDER: PhaseId[] = [
'einstieg',
'erarbeitung',
'sicherung',
'transfer',
'reflexion',
]
// ============================================================================
// Default Durations (in minutes)
// ============================================================================
export const DEFAULT_PHASE_DURATIONS: PhaseDurations = {
einstieg: 8,
erarbeitung: 20,
sicherung: 10,
transfer: 7,
reflexion: 5,
}
export const DEFAULT_LESSON_LENGTH = 45 // minutes (German standard)
export const EXTENDED_LESSON_LENGTH = 50 // minutes (with buffer)
// ============================================================================
// Timer Thresholds (in seconds)
// ============================================================================
export const TIMER_WARNING_THRESHOLD = 5 * 60 // 5 minutes = warning (yellow)
export const TIMER_CRITICAL_THRESHOLD = 2 * 60 // 2 minutes = critical (red)
// ============================================================================
// SVG Pie Timer Constants
// ============================================================================
export const PIE_TIMER_RADIUS = 42
export const PIE_TIMER_CIRCUMFERENCE = 2 * Math.PI * PIE_TIMER_RADIUS // ~263.89
export const PIE_TIMER_STROKE_WIDTH = 8
export const PIE_TIMER_SIZE = 120 // viewBox size
// ============================================================================
// Timer Color Classes
// ============================================================================
export const TIMER_COLOR_CLASSES = {
plenty: 'text-green-500 stroke-green-500',
warning: 'text-amber-500 stroke-amber-500',
critical: 'text-red-500 stroke-red-500',
overtime: 'text-red-600 stroke-red-600 animate-pulse',
}
export const TIMER_BG_COLORS = {
plenty: 'bg-green-500/10',
warning: 'bg-amber-500/10',
critical: 'bg-red-500/10',
overtime: 'bg-red-600/20',
}
// ============================================================================
// Keyboard Shortcuts
// ============================================================================
export const KEYBOARD_SHORTCUTS = {
PAUSE_RESUME: ' ', // Spacebar
EXTEND_5MIN: 'e',
NEXT_PHASE: 'n',
CLOSE_MODAL: 'Escape',
SHOW_HELP: '?',
} as const
export const KEYBOARD_SHORTCUT_DESCRIPTIONS: Record<string, string> = {
' ': 'Pause/Fortsetzen',
'e': '+5 Minuten',
'n': 'Naechste Phase',
'Escape': 'Modal schliessen',
'?': 'Hilfe anzeigen',
}
// ============================================================================
// Default Settings
// ============================================================================
export const DEFAULT_TEACHER_SETTINGS: TeacherSettings = {
defaultPhaseDurations: DEFAULT_PHASE_DURATIONS,
preferredLessonLength: DEFAULT_LESSON_LENGTH,
autoAdvancePhases: true,
soundNotifications: true,
showKeyboardShortcuts: true,
highContrastMode: false,
onboardingCompleted: false,
}
// ============================================================================
// System Templates
// ============================================================================
export const SYSTEM_TEMPLATES = [
{
templateId: 'standard-45',
name: 'Standard (45 Min)',
description: 'Klassische Unterrichtsstunde',
durations: DEFAULT_PHASE_DURATIONS,
isSystemTemplate: true,
},
{
templateId: 'double-90',
name: 'Doppelstunde (90 Min)',
description: 'Fuer laengere Arbeitsphasen',
durations: {
einstieg: 10,
erarbeitung: 45,
sicherung: 15,
transfer: 12,
reflexion: 8,
},
isSystemTemplate: true,
},
{
templateId: 'math-focused',
name: 'Mathematik-fokussiert',
description: 'Lange Erarbeitung und Sicherung',
durations: {
einstieg: 5,
erarbeitung: 25,
sicherung: 10,
transfer: 5,
reflexion: 5,
},
isSystemTemplate: true,
},
{
templateId: 'language-practice',
name: 'Sprachpraxis',
description: 'Betont kommunikative Phasen',
durations: {
einstieg: 10,
erarbeitung: 15,
sicherung: 8,
transfer: 10,
reflexion: 7,
},
isSystemTemplate: true,
},
]
// ============================================================================
// Suggestion Icons (Lucide icon names)
// ============================================================================
export const SUGGESTION_ICONS = {
grading: 'ClipboardCheck',
homework: 'BookOpen',
planning: 'Calendar',
meeting: 'Users',
deadline: 'Clock',
material: 'FileText',
communication: 'MessageSquare',
default: 'Lightbulb',
}
// ============================================================================
// Priority Colors
// ============================================================================
export const PRIORITY_COLORS = {
urgent: {
bg: 'bg-red-100',
text: 'text-red-700',
border: 'border-red-200',
dot: 'bg-red-500',
},
high: {
bg: 'bg-orange-100',
text: 'text-orange-700',
border: 'border-orange-200',
dot: 'bg-orange-500',
},
medium: {
bg: 'bg-yellow-100',
text: 'text-yellow-700',
border: 'border-yellow-200',
dot: 'bg-yellow-500',
},
low: {
bg: 'bg-slate-100',
text: 'text-slate-700',
border: 'border-slate-200',
dot: 'bg-slate-400',
},
}
// ============================================================================
// Event Type Icons & Colors
// ============================================================================
export const EVENT_TYPE_CONFIG = {
exam: {
icon: 'FileQuestion',
color: 'text-red-600',
bg: 'bg-red-50',
},
parent_meeting: {
icon: 'Users',
color: 'text-blue-600',
bg: 'bg-blue-50',
},
deadline: {
icon: 'Clock',
color: 'text-amber-600',
bg: 'bg-amber-50',
},
other: {
icon: 'Calendar',
color: 'text-slate-600',
bg: 'bg-slate-50',
},
}
// ============================================================================
// Storage Keys
// ============================================================================
export const STORAGE_KEYS = {
SETTINGS: 'companion_settings',
CURRENT_SESSION: 'companion_current_session',
ONBOARDING_STATE: 'companion_onboarding',
CUSTOM_TEMPLATES: 'companion_custom_templates',
LAST_MODE: 'companion_last_mode',
}
// ============================================================================
// API Endpoints (relative to backend)
// ============================================================================
export const API_ENDPOINTS = {
DASHBOARD: '/api/state/dashboard',
LESSON_START: '/api/classroom/sessions',
LESSON_UPDATE: '/api/classroom/sessions', // + /{id}
TEMPLATES: '/api/classroom/templates',
SETTINGS: '/api/teacher/settings',
FEEDBACK: '/api/feedback',
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Create default phases array from durations
*/
export function createDefaultPhases(durations: PhaseDurations = DEFAULT_PHASE_DURATIONS): Phase[] {
return PHASE_ORDER.map((phaseId, index) => ({
id: phaseId,
shortName: PHASE_SHORT_NAMES[phaseId],
displayName: PHASE_DISPLAY_NAMES[phaseId],
duration: durations[phaseId],
status: index === 0 ? 'active' : 'planned',
color: PHASE_COLORS[phaseId].hex,
}))
}
/**
* Calculate total duration from phase durations
*/
export function calculateTotalDuration(durations: PhaseDurations): number {
return Object.values(durations).reduce((sum, d) => sum + d, 0)
}
/**
* Get timer color status based on remaining time
*/
export function getTimerColorStatus(
remainingSeconds: number,
isOvertime: boolean
): 'plenty' | 'warning' | 'critical' | 'overtime' {
if (isOvertime) return 'overtime'
if (remainingSeconds <= TIMER_CRITICAL_THRESHOLD) return 'critical'
if (remainingSeconds <= TIMER_WARNING_THRESHOLD) return 'warning'
return 'plenty'
}
/**
* Format seconds as MM:SS
*/
export function formatTime(seconds: number): string {
const absSeconds = Math.abs(seconds)
const mins = Math.floor(absSeconds / 60)
const secs = absSeconds % 60
const sign = seconds < 0 ? '-' : ''
return `${sign}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
/**
* Format minutes as "X Min"
*/
export function formatMinutes(minutes: number): string {
return `${minutes} Min`
}
+2
View File
@@ -0,0 +1,2 @@
export * from './types'
export * from './constants'
+329
View File
@@ -0,0 +1,329 @@
/**
* TypeScript Types for Companion Module
* Migration from Flask companion.py/companion_js.py
*/
// ============================================================================
// Phase System
// ============================================================================
export type PhaseId = 'einstieg' | 'erarbeitung' | 'sicherung' | 'transfer' | 'reflexion'
export interface Phase {
id: PhaseId
shortName: string // E, A, S, T, R
displayName: string
duration: number // minutes
status: 'planned' | 'active' | 'completed'
actualTime?: number // seconds (actual time spent)
color: string // hex color
}
export interface PhaseContext {
currentPhase: PhaseId
phaseDisplayName: string
}
// ============================================================================
// Dashboard / Companion Mode
// ============================================================================
export interface CompanionStats {
classesCount: number
studentsCount: number
learningUnitsCreated: number
gradesEntered: number
}
export interface Progress {
percentage: number
completed: number
total: number
}
export type SuggestionPriority = 'urgent' | 'high' | 'medium' | 'low'
export interface Suggestion {
id: string
title: string
description: string
priority: SuggestionPriority
icon: string // lucide icon name
actionTarget: string // navigation path
estimatedTime: number // minutes
}
export type EventType = 'exam' | 'parent_meeting' | 'deadline' | 'other'
export interface UpcomingEvent {
id: string
title: string
date: string // ISO date string
type: EventType
inDays: number
}
export interface CompanionData {
context: PhaseContext
stats: CompanionStats
phases: Phase[]
progress: Progress
suggestions: Suggestion[]
upcomingEvents: UpcomingEvent[]
}
// ============================================================================
// Lesson Mode
// ============================================================================
export type LessonStatus =
| 'not_started'
| 'in_progress'
| 'paused'
| 'completed'
| 'overtime'
export interface LessonPhase {
phase: PhaseId
duration: number // planned duration in minutes
status: 'planned' | 'active' | 'completed' | 'skipped'
actualTime: number // actual time spent in seconds
startedAt?: string // ISO timestamp
completedAt?: string // ISO timestamp
}
export interface Homework {
id: string
title: string
description?: string
dueDate: string // ISO date
attachments?: string[]
completed?: boolean
}
export interface Material {
id: string
title: string
type: 'document' | 'video' | 'presentation' | 'link' | 'other'
url?: string
fileName?: string
}
export interface LessonReflection {
rating: number // 1-5 stars
notes: string
nextSteps: string
savedAt?: string
}
export interface LessonSession {
sessionId: string
classId: string
className: string
subject: string
topic?: string
startTime: string // ISO timestamp
endTime?: string // ISO timestamp
phases: LessonPhase[]
totalPlannedDuration: number // minutes
currentPhaseIndex: number
elapsedTime: number // seconds
isPaused: boolean
pausedAt?: string
pauseDuration: number // total pause time in seconds
overtimeMinutes: number
status: LessonStatus
homeworkList: Homework[]
materials: Material[]
reflection?: LessonReflection
}
// ============================================================================
// Lesson Templates
// ============================================================================
export interface PhaseDurations {
einstieg: number
erarbeitung: number
sicherung: number
transfer: number
reflexion: number
}
export interface LessonTemplate {
templateId: string
name: string
description?: string
subject?: string
durations: PhaseDurations
isSystemTemplate: boolean
createdBy?: string
createdAt?: string
}
// ============================================================================
// Settings
// ============================================================================
export interface TeacherSettings {
defaultPhaseDurations: PhaseDurations
preferredLessonLength: number // minutes (default 45)
autoAdvancePhases: boolean
soundNotifications: boolean
showKeyboardShortcuts: boolean
highContrastMode: boolean
onboardingCompleted: boolean
selectedTemplateId?: string
}
// ============================================================================
// Timer State
// ============================================================================
export type TimerColorStatus = 'plenty' | 'warning' | 'critical' | 'overtime'
export interface TimerState {
isRunning: boolean
isPaused: boolean
elapsedSeconds: number
remainingSeconds: number
totalSeconds: number
progress: number // 0-1
colorStatus: TimerColorStatus
currentPhase: LessonPhase | null
}
// ============================================================================
// Forms
// ============================================================================
export interface LessonStartFormData {
classId: string
subject: string
topic?: string
templateId?: string
customDurations?: PhaseDurations
}
export interface Class {
id: string
name: string
grade: string
studentCount: number
}
// ============================================================================
// Feedback
// ============================================================================
export type FeedbackType = 'bug' | 'feature' | 'feedback'
export interface FeedbackSubmission {
type: FeedbackType
title: string
description: string
screenshot?: string // base64
sessionId?: string
metadata?: Record<string, unknown>
}
// ============================================================================
// Onboarding
// ============================================================================
export interface OnboardingStep {
step: number
title: string
description: string
completed: boolean
}
export interface OnboardingState {
currentStep: number
totalSteps: number
steps: OnboardingStep[]
selectedState?: string // Bundesland
selectedSchoolType?: string
completed: boolean
}
// ============================================================================
// WebSocket Messages
// ============================================================================
export type WSMessageType =
| 'phase_update'
| 'timer_tick'
| 'overtime_warning'
| 'pause_toggle'
| 'session_end'
| 'sync_request'
export interface WSMessage {
type: WSMessageType
payload: {
sessionId: string
phase?: number
elapsed?: number
isPaused?: boolean
overtimeMinutes?: number
[key: string]: unknown
}
timestamp: string
}
// ============================================================================
// API Responses
// ============================================================================
export interface APIResponse<T> {
success: boolean
data?: T
error?: string
message?: string
}
export interface DashboardResponse extends APIResponse<CompanionData> {}
export interface LessonResponse extends APIResponse<LessonSession> {}
export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
export interface SettingsResponse extends APIResponse<TeacherSettings> {}
// ============================================================================
// Component Props
// ============================================================================
export type CompanionMode = 'companion' | 'lesson' | 'classic'
export interface ModeToggleProps {
currentMode: CompanionMode
onModeChange: (mode: CompanionMode) => void
}
export interface PhaseTimelineProps {
phases: Phase[]
currentPhaseIndex: number
onPhaseClick?: (index: number) => void
}
export interface VisualPieTimerProps {
progress: number // 0-1
remainingSeconds: number
totalSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
currentPhaseName: string
phaseColor: string
}
export interface QuickActionsBarProps {
onExtend: (minutes: number) => void
onPause: () => void
onResume: () => void
onSkip: () => void
isPaused: boolean
isLastPhase: boolean
disabled?: boolean
}
+60
View File
@@ -0,0 +1,60 @@
/**
* Website Content Type Definitions
*
* Types for website content (no server-side imports)
*/
export interface HeroContent {
badge: string
title: string
titleHighlight1: string
titleHighlight2: string
subtitle: string
ctaPrimary: string
ctaSecondary: string
ctaHint: string
}
export interface FeatureContent {
id: string
icon: string
title: string
description: string
}
export interface FAQItem {
question: string
answer: string[]
}
export interface PricingPlan {
id: string
name: string
description: string
price: number
currency: string
interval: string
popular?: boolean
features: {
tasks: string
taskDescription: string
included: string[]
}
}
export interface WebsiteContent {
hero: HeroContent
features: FeatureContent[]
faq: FAQItem[]
pricing: PricingPlan[]
trust: {
item1: { value: string; label: string }
item2: { value: string; label: string }
item3: { value: string; label: string }
}
testimonial: {
quote: string
author: string
role: string
}
}
+284
View File
@@ -0,0 +1,284 @@
/**
* Website Content Management (Server-only)
*
* Loads website texts from JSON files.
* Admin can edit texts via /development/content
*
* IMPORTANT: This file may only be used in Server Components!
* For Client Components use @/lib/content-types
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, accessSync, constants } from 'fs'
import { join, dirname } from 'path'
// Re-export types from content-types for backward compatibility
export type {
HeroContent,
FeatureContent,
FAQItem,
PricingPlan,
WebsiteContent,
} from './content-types'
import type { WebsiteContent } from './content-types'
// Content directory - use environment variable or relative path
function getContentDir(): string {
// Check environment variable first
if (process.env.CONTENT_DIR) {
return process.env.CONTENT_DIR
}
// Try various possible paths
const possiblePaths = [
join(process.cwd(), 'content'), // Standard: CWD/content
join(process.cwd(), 'admin-v2', 'content'), // If CWD is project root
'/app/content', // Docker container
join(dirname(__filename), '..', 'content'), // Relative to this file
]
// Check if any path exists and is writable
for (const path of possiblePaths) {
try {
if (existsSync(path)) {
accessSync(path, constants.W_OK)
return path
}
} catch {
// Path not writable, try next
}
}
// Fallback: Create in CWD
const fallbackPath = join(process.cwd(), 'content')
try {
mkdirSync(fallbackPath, { recursive: true, mode: 0o755 })
console.log(`[Content] Created content directory at: ${fallbackPath}`)
} catch (err) {
console.error(`[Content] Failed to create content directory: ${err}`)
}
return fallbackPath
}
const CONTENT_DIR = getContentDir()
// Default Content
const defaultContent: WebsiteContent = {
hero: {
badge: 'Entwickelt fuer deutsche Lehrkraefte',
title: 'Korrigieren Sie',
titleHighlight1: 'schneller',
titleHighlight2: 'besser',
subtitle: 'BreakPilot unterstuetzt Lehrkraefte mit intelligenter KI bei der Bewertung von Aufgaben. Sparen Sie bis zu 50% Ihrer Korrekturzeit und geben Sie besseres Feedback.',
ctaPrimary: '7 Tage kostenlos testen',
ctaSecondary: 'Mehr erfahren',
ctaHint: 'Keine Kreditkarte fuer den Start erforderlich',
},
features: [
{
id: 'ai-correction',
icon: '✍️',
title: 'KI-gestuetzte Korrektur',
description: 'Intelligente Analyse von Schuelerantworten mit Verbesserungsvorschlaegen und automatischer Bewertung nach Ihren Kriterien.',
},
{
id: 'templates',
icon: '📋',
title: 'Dokumentvorlagen',
description: 'Erstellen und verwalten Sie Ihre eigenen Arbeitsblatt-Vorlagen. Wiederverwendbar fuer verschiedene Klassen und Jahrgaenge.',
},
{
id: 'analytics',
icon: '📊',
title: 'Fortschrittsanalyse',
description: 'Verfolgen Sie die Entwicklung Ihrer Schueler ueber Zeit. Erkennen Sie Staerken und Schwaechen fruehzeitig.',
},
{
id: 'gdpr',
icon: '🔒',
title: 'DSGVO-konform',
description: 'Hosting in Deutschland, volle Datenschutzkonformitaet. Ihre Daten und die Ihrer Schueler sind sicher.',
},
{
id: 'team',
icon: '👥',
title: 'Team-Funktionen',
description: 'Arbeiten Sie im Fachbereich zusammen. Teilen Sie Vorlagen, Bewertungskriterien und Best Practices.',
},
{
id: 'mobile',
icon: '📱',
title: 'Ueberall verfuegbar',
description: 'Browserbasiert und responsive. Funktioniert auf Desktop, Tablet und Smartphone - ohne Installation.',
},
],
faq: [
{
question: 'Was ist bei Breakpilot eine „Aufgabe"?',
answer: [
'Eine Aufgabe ist ein abgeschlossener Arbeitsauftrag, den du mit Breakpilot erledigst.',
'Typische Beispiele:',
'• eine Klassenarbeit korrigieren (egal wie viele Seiten)',
'• mehrere Klassenarbeiten in einer Serie korrigieren',
'• einen Elternbrief erstellen',
'Wichtig: Die Anzahl der Seiten, Dateien oder Uploads spielt dabei keine Rolle.',
],
},
{
question: 'Kann ich Breakpilot kostenlos testen?',
answer: [
'Ja.',
'• Du kannst Breakpilot 7 Tage kostenlos testen',
'• Dafuer ist eine Kreditkarte erforderlich',
'• Wenn du innerhalb der Testphase kuendigst, entstehen keine Kosten',
],
},
{
question: 'Werden meine Daten fuer KI-Training verwendet?',
answer: [
'Nein.',
'• Deine Inhalte werden nicht fuer das Training oeffentlicher KI-Modelle genutzt',
'• Die Verarbeitung erfolgt DSGVO-konform',
'• Deine Daten bleiben unter deiner Kontrolle',
],
},
{
question: 'Kann ich meinen Tarif jederzeit aendern oder kuendigen?',
answer: [
'Ja.',
'• Upgrades sind jederzeit moeglich',
'• Downgrades greifen zum naechsten Abrechnungszeitraum',
'• Kuendigungen sind jederzeit moeglich',
],
},
],
pricing: [
{
id: 'basic',
name: 'Basic',
description: 'Perfekt fuer den Einstieg',
price: 9.90,
currency: 'EUR',
interval: 'Monat',
features: {
tasks: '30 Aufgaben',
taskDescription: 'pro Monat',
included: [
'KI-gestuetzte Korrektur',
'Basis-Dokumentvorlagen',
'E-Mail Support',
],
},
},
{
id: 'standard',
name: 'Standard',
description: 'Fuer regelmaessige Nutzer',
price: 19.90,
currency: 'EUR',
interval: 'Monat',
popular: true,
features: {
tasks: '100 Aufgaben',
taskDescription: 'pro Monat',
included: [
'Alles aus Basic',
'Eigene Vorlagen erstellen',
'Batch-Verarbeitung',
'Bis zu 3 Teammitglieder',
'Prioritaets-Support',
],
},
},
{
id: 'premium',
name: 'Premium',
description: 'Sorglos-Tarif fuer Vielnutzer',
price: 39.90,
currency: 'EUR',
interval: 'Monat',
features: {
tasks: 'Unbegrenzt',
taskDescription: 'Fair Use',
included: [
'Alles aus Standard',
'Unbegrenzte Aufgaben (Fair Use)',
'Bis zu 10 Teammitglieder',
'Admin-Panel & Audit-Log',
'API-Zugang',
'Eigenes Branding',
'Dedizierter Support',
],
},
},
],
trust: {
item1: { value: 'DSGVO', label: 'Konform & sicher' },
item2: { value: '7 Tage', label: 'Kostenlos testen' },
item3: { value: '100%', label: 'Made in Germany' },
},
testimonial: {
quote: 'BreakPilot hat meine Korrekturzeit halbiert. Ich habe endlich wieder Zeit fuer das Wesentliche: meine Schueler.',
author: 'Maria S.',
role: 'Deutschlehrerin, Gymnasium',
},
}
/**
* Loads content from JSON file or returns default
*/
export function getContent(): WebsiteContent {
const contentPath = join(CONTENT_DIR, 'website.json')
try {
if (existsSync(contentPath)) {
const fileContent = readFileSync(contentPath, 'utf-8')
return JSON.parse(fileContent) as WebsiteContent
}
} catch (error) {
console.error('Error loading content:', error)
}
return defaultContent
}
/**
* Saves content to JSON file
* @returns Object with success status and optional error message
*/
export function saveContent(content: WebsiteContent): { success: boolean; error?: string } {
const contentPath = join(CONTENT_DIR, 'website.json')
try {
// Ensure directory exists
if (!existsSync(CONTENT_DIR)) {
console.log(`[Content] Creating directory: ${CONTENT_DIR}`)
mkdirSync(CONTENT_DIR, { recursive: true, mode: 0o755 })
}
// Check write permissions
try {
accessSync(CONTENT_DIR, constants.W_OK)
} catch {
const error = `Directory not writable: ${CONTENT_DIR}`
console.error(`[Content] ${error}`)
return { success: false, error }
}
// Write file
writeFileSync(contentPath, JSON.stringify(content, null, 2), 'utf-8')
console.log(`[Content] Saved successfully to: ${contentPath}`)
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error(`[Content] Error saving: ${errorMessage}`)
return { success: false, error: errorMessage }
}
}
/**
* Returns default content (for reset)
*/
export function getDefaultContent(): WebsiteContent {
return defaultContent
}
@@ -0,0 +1,164 @@
/**
* Extended TypeScript types for Abitur-Archiv
* Builds upon abitur-docs-types.ts with additional search and integration features
*/
import { AbiturDokument, AbiturDocsFilter } from './abitur-docs-types'
// Theme suggestion for autocomplete search
export interface ThemaSuggestion {
label: string // "Gedichtanalyse Romantik"
count: number // 12 Dokumente
aufgabentyp: string // "gedichtanalyse"
zeitraum?: string // "Romantik"
kategorie?: string // Category for grouping
}
// Extended filter with theme search
export interface AbiturArchivFilter extends AbiturDocsFilter {
thema?: string // Semantic theme search query
aufgabentyp?: string // Specific task type filter
}
// Similar document result from RAG
export interface SimilarDocument {
id: string
dateiname: string
similarity_score: number
fach: string
jahr: number
niveau: 'eA' | 'gA'
typ: 'aufgabe' | 'erwartungshorizont'
aufgaben_nummer: string
}
// Extended document with similar documents
export interface AbiturDokumentExtended extends AbiturDokument {
similar_documents?: SimilarDocument[]
themes?: string[] // Extracted themes from content
}
// Response for archive listing
export interface AbiturArchivResponse {
documents: AbiturDokumentExtended[]
total: number
page: number
limit: number
total_pages: number
themes?: ThemaSuggestion[] // Available themes for current filter
}
// Response for theme suggestions
export interface ThemaSuggestResponse {
suggestions: ThemaSuggestion[]
query: string
}
// Response for similar documents
export interface SimilarDocsResponse {
document_id: string
similar: SimilarDocument[]
}
// Klausur creation from archive
export interface KlausurFromArchivRequest {
archiv_dokument_id: string
aufgabentyp: string
title?: string
}
export interface KlausurFromArchivResponse {
klausur_id: string
eh_id?: string
success: boolean
message?: string
}
// View mode for document display
export type ViewMode = 'grid' | 'list'
// Sort options
export type SortField = 'jahr' | 'fach' | 'datum' | 'dateiname'
export type SortDirection = 'asc' | 'desc'
export interface SortConfig {
field: SortField
direction: SortDirection
}
// Predefined theme categories
export const THEME_CATEGORIES = {
textanalyse: {
label: 'Textanalyse',
subcategories: ['pragmatisch', 'literarisch', 'sachtext', 'rede', 'kommentar']
},
gedichtanalyse: {
label: 'Gedichtanalyse',
subcategories: ['romantik', 'expressionismus', 'barock', 'klassik', 'moderne']
},
dramenanalyse: {
label: 'Dramenanalyse',
subcategories: ['klassisch', 'modern', 'episches_theater']
},
prosaanalyse: {
label: 'Prosaanalyse',
subcategories: ['roman', 'kurzgeschichte', 'novelle', 'erzaehlung']
},
eroerterung: {
label: 'Eroerterung',
subcategories: ['textgebunden', 'materialgestuetzt', 'frei']
},
sprachreflexion: {
label: 'Sprachreflexion',
subcategories: ['sprachwandel', 'sprachkritik', 'kommunikation']
}
} as const
// Popular theme suggestions (static fallback)
export const POPULAR_THEMES: ThemaSuggestion[] = [
{ label: 'Textanalyse', count: 45, aufgabentyp: 'textanalyse_pragmatisch', kategorie: 'Analyse' },
{ label: 'Gedichtanalyse', count: 38, aufgabentyp: 'gedichtanalyse', kategorie: 'Analyse' },
{ label: 'Eroerterung', count: 32, aufgabentyp: 'eroerterung_textgebunden', kategorie: 'Argumentation' },
{ label: 'Dramenanalyse', count: 28, aufgabentyp: 'dramenanalyse', kategorie: 'Analyse' },
{ label: 'Prosaanalyse', count: 25, aufgabentyp: 'prosaanalyse', kategorie: 'Analyse' },
]
// Quick action types for document cards
export type QuickAction = 'preview' | 'download' | 'add_to_klausur' | 'view_similar'
// Fullscreen viewer state
export interface ViewerState {
isFullscreen: boolean
zoom: number
currentPage: number
totalPages: number
}
// Default viewer state
export const DEFAULT_VIEWER_STATE: ViewerState = {
isFullscreen: false,
zoom: 100,
currentPage: 1,
totalPages: 1
}
// Zoom levels
export const ZOOM_LEVELS = [25, 50, 75, 100, 125, 150, 175, 200] as const
export const MIN_ZOOM = 25
export const MAX_ZOOM = 200
export const ZOOM_STEP = 25
// Helper functions
export function getThemeLabel(aufgabentyp: string): string {
const entries = Object.entries(THEME_CATEGORIES)
for (const [key, value] of entries) {
if (aufgabentyp.startsWith(key)) {
return value.label
}
}
return aufgabentyp
}
export function formatSimilarityScore(score: number): string {
return `${Math.round(score * 100)}%`
}
@@ -0,0 +1,84 @@
/**
* TypeScript types for Abitur Documents (NiBiS, etc.)
*/
export interface AbiturDokument {
id: string
dateiname: string
original_dateiname: string
bundesland: string
fach: string
jahr: number
niveau: 'eA' | 'gA' // erhöhtes/grundlegendes Anforderungsniveau
typ: 'aufgabe' | 'erwartungshorizont'
aufgaben_nummer: string // I, II, III
status: 'pending' | 'indexed' | 'error'
confidence: number
file_path: string
file_size: number
indexed: boolean
vector_ids: string[]
created_at: string
updated_at: string
}
export interface AbiturDocsResponse {
documents: AbiturDokument[]
total: number
page: number
limit: number
total_pages: number
}
export interface AbiturDocsFilter {
fach?: string
jahr?: number
bundesland?: string
niveau?: 'eA' | 'gA'
typ?: 'aufgabe' | 'erwartungshorizont'
page?: number
limit?: number
}
// Available filter options
export const FAECHER = [
{ id: 'deutsch', label: 'Deutsch' },
{ id: 'mathematik', label: 'Mathematik' },
{ id: 'englisch', label: 'Englisch' },
{ id: 'biologie', label: 'Biologie' },
{ id: 'physik', label: 'Physik' },
{ id: 'chemie', label: 'Chemie' },
{ id: 'geschichte', label: 'Geschichte' },
] as const
export const JAHRE = [2025, 2024, 2023, 2022, 2021, 2020] as const
export const BUNDESLAENDER = [
{ id: 'niedersachsen', label: 'Niedersachsen' },
{ id: 'bayern', label: 'Bayern' },
{ id: 'nrw', label: 'Nordrhein-Westfalen' },
{ id: 'bw', label: 'Baden-Württemberg' },
] as const
export const NIVEAUS = [
{ id: 'eA', label: 'Erhöhtes Anforderungsniveau (eA)' },
{ id: 'gA', label: 'Grundlegendes Anforderungsniveau (gA)' },
] as const
export const TYPEN = [
{ id: 'aufgabe', label: 'Aufgabe' },
{ id: 'erwartungshorizont', label: 'Erwartungshorizont' },
] as const
// Helper functions
export function formatFileSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
export function formatDocumentTitle(doc: AbiturDokument): string {
const fachLabel = FAECHER.find(f => f.id === doc.fach)?.label || doc.fach
const typLabel = doc.typ === 'erwartungshorizont' ? 'EWH' : 'Aufgabe'
return `${fachLabel} ${doc.jahr} - ${doc.niveau} ${doc.aufgaben_nummer} (${typLabel})`
}
+613
View File
@@ -0,0 +1,613 @@
/**
* Module Registry - Track all backend modules and their frontend connections
*
* This registry ensures no backend modules get lost during migration.
* Each module entry defines:
* - Backend service and endpoints
* - Frontend pages that use it
* - Connection status (connected, partial, not connected)
*/
export interface BackendModule {
id: string
name: string
description: string
category: 'compliance' | 'ai' | 'infrastructure' | 'education' | 'communication' | 'development'
backend: {
service: string // e.g. 'consent-service', 'python-backend', 'klausur-service'
port: number
basePath: string
endpoints: {
path: string
method: string
description: string
}[]
}
frontend: {
adminV2Page?: string // New admin-v2 page path
oldAdminPage?: string // Old admin page path (for reference)
status: 'connected' | 'partial' | 'not-connected' | 'deprecated'
}
dependencies?: string[] // IDs of other modules this depends on
priority: 'critical' | 'high' | 'medium' | 'low'
notes?: string
}
export const MODULE_REGISTRY: BackendModule[] = [
// ===========================================
// COMPLIANCE MODULES
// ===========================================
{
id: 'consent-documents',
name: 'Consent Dokumente',
description: 'Verwaltung rechtlicher Dokumente (AGB, Datenschutz, etc.)',
category: 'compliance',
backend: {
service: 'consent-service',
port: 8081,
basePath: '/api/consent/admin',
endpoints: [
{ path: '/documents', method: 'GET', description: 'Liste aller Dokumente' },
{ path: '/documents', method: 'POST', description: 'Dokument erstellen' },
{ path: '/documents/{id}', method: 'GET', description: 'Dokument Details' },
{ path: '/documents/{id}', method: 'PUT', description: 'Dokument aktualisieren' },
{ path: '/documents/{id}', method: 'DELETE', description: 'Dokument loeschen' },
]
},
frontend: {
adminV2Page: '/sdk/consent-management',
oldAdminPage: '/admin/consent',
status: 'connected'
},
priority: 'critical'
},
{
id: 'consent-versions',
name: 'Dokument-Versionierung',
description: 'Versionsverwaltung und Freigabe-Workflow fuer rechtliche Dokumente',
category: 'compliance',
backend: {
service: 'consent-service',
port: 8081,
basePath: '/api/consent/admin',
endpoints: [
{ path: '/documents/{id}/versions', method: 'GET', description: 'Versionen eines Dokuments' },
{ path: '/versions', method: 'POST', description: 'Neue Version erstellen' },
{ path: '/versions/{id}', method: 'PUT', description: 'Version aktualisieren' },
{ path: '/versions/{id}', method: 'DELETE', description: 'Version loeschen' },
{ path: '/versions/{id}/submit-review', method: 'POST', description: 'Zur Pruefung einreichen' },
{ path: '/versions/{id}/approve', method: 'POST', description: 'Version genehmigen' },
{ path: '/versions/{id}/reject', method: 'POST', description: 'Version ablehnen' },
{ path: '/versions/{id}/publish', method: 'POST', description: 'Version veroeffentlichen' },
{ path: '/versions/{id}/approval-history', method: 'GET', description: 'Genehmigungsverlauf' },
{ path: '/versions/upload-word', method: 'POST', description: 'Word-Dokument importieren' },
]
},
frontend: {
adminV2Page: '/sdk/workflow',
oldAdminPage: '/admin/consent (Versions Tab)',
status: 'connected'
},
dependencies: ['consent-documents'],
priority: 'critical'
},
{
id: 'consent-user',
name: 'Nutzer-Einwilligungen',
description: 'Tracking von Nutzer-Einwilligungen fuer DSGVO-Compliance',
category: 'compliance',
backend: {
service: 'consent-service',
port: 8081,
basePath: '/api/consent',
endpoints: [
{ path: '/status', method: 'GET', description: 'Einwilligungsstatus pruefen' },
{ path: '/give', method: 'POST', description: 'Einwilligung erteilen' },
{ path: '/withdraw', method: 'POST', description: 'Einwilligung widerrufen' },
{ path: '/history', method: 'GET', description: 'Einwilligungshistorie' },
]
},
frontend: {
adminV2Page: '/sdk/einwilligungen',
oldAdminPage: '/admin/consent (Users Tab)',
status: 'connected',
},
priority: 'critical',
},
{
id: 'dsr-requests',
name: 'Datenschutzanfragen (DSR)',
description: 'DSGVO Art. 15-21 Anfragen verwalten',
category: 'compliance',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/dsr',
endpoints: [
{ path: '/requests', method: 'GET', description: 'Alle DSR-Anfragen' },
{ path: '/requests', method: 'POST', description: 'Neue Anfrage erstellen' },
{ path: '/requests/{id}', method: 'GET', description: 'Anfrage-Details' },
{ path: '/requests/{id}/process', method: 'POST', description: 'Anfrage bearbeiten' },
{ path: '/requests/{id}/export', method: 'GET', description: 'Daten exportieren' },
]
},
frontend: {
adminV2Page: '/sdk/dsr',
oldAdminPage: '/admin/dsr',
status: 'connected'
},
priority: 'high',
},
{
id: 'dsms',
name: 'Datenschutz-Management-System',
description: 'Zentrales DSMS fuer Dokumentation und Compliance',
category: 'compliance',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/dsms',
endpoints: [
{ path: '/documents', method: 'GET', description: 'DSMS-Dokumente' },
{ path: '/processes', method: 'GET', description: 'Verarbeitungsverzeichnis' },
{ path: '/toms', method: 'GET', description: 'TOM-Katalog' },
{ path: '/audits', method: 'GET', description: 'Audit-Historie' },
]
},
frontend: {
adminV2Page: '/sdk/dsms',
oldAdminPage: '/admin/dsms',
status: 'connected'
},
priority: 'medium'
},
{
id: 'cookie-categories',
name: 'Cookie-Kategorien',
description: 'Verwaltung von Cookie-Kategorien fuer Consent Banner',
category: 'compliance',
backend: {
service: 'consent-service',
port: 8081,
basePath: '/api/consent/admin',
endpoints: [
{ path: '/cookies/categories', method: 'GET', description: 'Alle Cookie-Kategorien' },
{ path: '/cookies/categories', method: 'POST', description: 'Kategorie erstellen' },
{ path: '/cookies/categories/{id}', method: 'PUT', description: 'Kategorie aktualisieren' },
{ path: '/cookies/categories/{id}', method: 'DELETE', description: 'Kategorie loeschen' },
]
},
frontend: {
adminV2Page: undefined,
oldAdminPage: '/admin/consent (Cookies Tab)',
status: 'not-connected'
},
priority: 'medium',
notes: 'Cookie-Kategorien Tab im alten Admin vorhanden'
},
// ===========================================
// AI MODULES
// ===========================================
{
id: 'ai-agents',
name: 'AI Agents',
description: 'Multi-Agent System Verwaltung und Monitoring',
category: 'ai',
backend: {
service: 'voice-service',
port: 8088,
basePath: '/api/v1/agents',
endpoints: [
{ path: '/sessions', method: 'GET', description: 'Agent-Sessions' },
{ path: '/statistics', method: 'GET', description: 'Agent-Statistiken' },
{ path: '/{agentId}', method: 'GET', description: 'Agent-Details' },
{ path: '/{agentId}/soul', method: 'GET', description: 'SOUL-Konfiguration' },
]
},
frontend: {
adminV2Page: '/ai/agents',
oldAdminPage: undefined,
status: 'connected'
},
priority: 'high',
notes: 'Neues Multi-Agent System'
},
{
id: 'ai-quality',
name: 'AI Quality (BQAS)',
description: 'KI-Qualitaetssicherung und Evaluierung',
category: 'ai',
backend: {
service: 'voice-service',
port: 8088,
basePath: '/api/bqas',
endpoints: [
{ path: '/evaluate', method: 'POST', description: 'Antwort evaluieren' },
{ path: '/metrics', method: 'GET', description: 'Qualitaetsmetriken' },
]
},
frontend: {
adminV2Page: '/ai/quality',
oldAdminPage: '/admin/quality',
status: 'connected'
},
priority: 'high'
},
{
id: 'llm-compare',
name: 'LLM Vergleich',
description: 'Vergleich verschiedener KI-Modelle und Provider',
category: 'ai',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/llm',
endpoints: [
{ path: '/providers', method: 'GET', description: 'Verfuegbare Provider' },
{ path: '/compare', method: 'POST', description: 'Modelle vergleichen' },
{ path: '/benchmark', method: 'POST', description: 'Benchmark ausfuehren' },
]
},
frontend: {
adminV2Page: '/ai/llm-compare',
oldAdminPage: '/admin/llm-compare',
status: 'connected'
},
priority: 'medium'
},
{
id: 'magic-help',
name: 'Magic Help (TrOCR)',
description: 'Handschrifterkennung mit TrOCR und LoRA Fine-Tuning',
category: 'ai',
backend: {
service: 'klausur-service',
port: 8086,
basePath: '/api/klausur/trocr',
endpoints: [
{ path: '/status', method: 'GET', description: 'TrOCR Status' },
{ path: '/extract', method: 'POST', description: 'Text aus Bild extrahieren' },
{ path: '/training/examples', method: 'GET', description: 'Trainingsbeispiele' },
{ path: '/training/add', method: 'POST', description: 'Trainingsbeispiel hinzufuegen' },
{ path: '/training/fine-tune', method: 'POST', description: 'Fine-Tuning starten' },
]
},
frontend: {
adminV2Page: '/ai/magic-help',
oldAdminPage: '/admin/magic-help',
status: 'connected'
},
priority: 'medium',
notes: 'Lokale Handschrifterkennung mit Privacy-by-Design'
},
{
id: 'klausur-korrektur',
name: 'Klausur-Korrektur',
description: 'KI-gestuetzte Abitur-Korrektur mit EH-Vorschlaegen',
category: 'ai',
backend: {
service: 'klausur-service',
port: 8086,
basePath: '/api/v1',
endpoints: [
{ path: '/klausuren', method: 'GET', description: 'Alle Klausuren' },
{ path: '/klausuren', method: 'POST', description: 'Klausur erstellen' },
{ path: '/klausuren/{id}/students', method: 'GET', description: 'Studentenarbeiten' },
{ path: '/students/{id}/annotations', method: 'GET', description: 'Anmerkungen' },
{ path: '/students/{id}/gutachten/generate', method: 'POST', description: 'Gutachten generieren' },
]
},
frontend: {
adminV2Page: '/ai/klausur-korrektur',
oldAdminPage: '/admin/klausur-korrektur',
status: 'not-connected'
},
priority: 'high',
notes: 'Komplexes Modul mit eigenem Backend-Service'
},
{
id: 'ocr-labeling',
name: 'OCR-Labeling',
description: 'Handschrift-Training und Label-Verwaltung',
category: 'ai',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/ocr',
endpoints: [
{ path: '/samples', method: 'GET', description: 'Training-Samples' },
{ path: '/labels', method: 'GET', description: 'Label-Kategorien' },
{ path: '/train', method: 'POST', description: 'Training starten' },
]
},
frontend: {
adminV2Page: '/ai/ocr-labeling',
oldAdminPage: '/admin/ocr-labeling',
status: 'not-connected'
},
priority: 'medium'
},
{
id: 'rag-management',
name: 'RAG & Daten',
description: 'Retrieval Augmented Generation und Training Data',
category: 'ai',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/rag',
endpoints: [
{ path: '/documents', method: 'GET', description: 'RAG-Dokumente' },
{ path: '/collections', method: 'GET', description: 'Vector-Collections' },
{ path: '/query', method: 'POST', description: 'RAG-Abfrage' },
]
},
frontend: {
adminV2Page: '/ai/rag',
oldAdminPage: '/admin/rag',
status: 'connected'
},
priority: 'medium'
},
// ===========================================
// INFRASTRUCTURE MODULES
// ===========================================
{
id: 'gpu-infrastructure',
name: 'GPU Infrastruktur',
description: 'vast.ai GPU-Management und Monitoring',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/gpu',
endpoints: [
{ path: '/instances', method: 'GET', description: 'GPU-Instanzen' },
{ path: '/instances', method: 'POST', description: 'Instanz erstellen' },
{ path: '/usage', method: 'GET', description: 'Nutzungsstatistiken' },
]
},
frontend: {
adminV2Page: '/infrastructure/gpu',
oldAdminPage: '/admin/gpu',
status: 'connected'
},
priority: 'medium'
},
{
id: 'security-dashboard',
name: 'Security Dashboard',
description: 'DevSecOps Dashboard und Vulnerability Scans',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/security',
endpoints: [
{ path: '/scans', method: 'GET', description: 'Security-Scans' },
{ path: '/vulnerabilities', method: 'GET', description: 'Schwachstellen' },
{ path: '/compliance', method: 'GET', description: 'Compliance-Status' },
]
},
frontend: {
adminV2Page: '/infrastructure/security',
oldAdminPage: '/admin/security',
status: 'connected'
},
priority: 'high'
},
{
id: 'sbom',
name: 'SBOM',
description: 'Software Bill of Materials',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/sbom',
endpoints: [
{ path: '/components', method: 'GET', description: 'Komponenten-Liste' },
{ path: '/licenses', method: 'GET', description: 'Lizenz-Uebersicht' },
{ path: '/export', method: 'GET', description: 'SBOM exportieren' },
]
},
frontend: {
adminV2Page: '/infrastructure/sbom',
oldAdminPage: '/admin/sbom',
status: 'connected'
},
priority: 'medium'
},
{
id: 'middleware',
name: 'Middleware Manager',
description: 'Verwaltung und Monitoring der Backend-Middleware',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/middleware',
endpoints: [
{ path: '/status', method: 'GET', description: 'Middleware-Status' },
{ path: '/config', method: 'GET', description: 'Konfiguration' },
]
},
frontend: {
adminV2Page: '/infrastructure/middleware',
oldAdminPage: '/admin/middleware',
status: 'connected'
},
priority: 'medium'
},
{
id: 'ci-cd',
name: 'CI/CD Pipeline',
description: 'Build-Pipeline und Deployment-Management',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/builds',
endpoints: [
{ path: '/pipelines', method: 'GET', description: 'Pipeline-Status' },
{ path: '/builds', method: 'GET', description: 'Build-Historie' },
]
},
frontend: {
adminV2Page: '/infrastructure/ci-cd',
oldAdminPage: '/admin/builds',
status: 'connected'
},
priority: 'medium'
},
// ===========================================
// EDUCATION MODULES
// ===========================================
{
id: 'edu-search',
name: 'Bildungssuche',
description: 'Suche nach Bildungsinhalten und Ressourcen',
category: 'education',
backend: {
service: 'edu-search-service',
port: 8089,
basePath: '/api/edu',
endpoints: [
{ path: '/search', method: 'GET', description: 'Bildungssuche' },
{ path: '/resources', method: 'GET', description: 'Ressourcen' },
]
},
frontend: {
adminV2Page: '/education/edu-search',
oldAdminPage: '/admin/edu-search',
status: 'connected'
},
priority: 'medium'
},
// ===========================================
// COMMUNICATION MODULES
// ===========================================
{
id: 'alerts',
name: 'Alerts & Benachrichtigungen',
description: 'System-Benachrichtigungen und Alerts',
category: 'communication',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/alerts',
endpoints: [
{ path: '/notifications', method: 'GET', description: 'Benachrichtigungen' },
{ path: '/alerts', method: 'GET', description: 'Aktive Alerts' },
]
},
frontend: {
adminV2Page: '/communication/alerts',
oldAdminPage: '/admin/alerts',
status: 'connected'
},
priority: 'medium'
},
{
id: 'unified-inbox',
name: 'Unified Inbox',
description: 'E-Mail-Konten und KI-Analyse',
category: 'communication',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/mail',
endpoints: [
{ path: '/accounts', method: 'GET', description: 'E-Mail-Konten' },
{ path: '/messages', method: 'GET', description: 'Nachrichten' },
{ path: '/analyze', method: 'POST', description: 'KI-Analyse' },
]
},
frontend: {
adminV2Page: '/communication/mail',
oldAdminPage: '/admin/mail',
status: 'connected'
},
priority: 'low'
},
// ===========================================
// DEVELOPMENT MODULES
// ===========================================
{
id: 'voice-service',
name: 'Voice Service',
description: 'Voice-First Interface',
category: 'development',
backend: {
service: 'voice-service',
port: 8088,
basePath: '/api/voice',
endpoints: [
{ path: '/transcribe', method: 'POST', description: 'Sprache transkribieren' },
{ path: '/synthesize', method: 'POST', description: 'Text zu Sprache' },
]
},
frontend: {
adminV2Page: '/development/voice',
oldAdminPage: '/admin/voice',
status: 'not-connected'
},
priority: 'low'
},
]
// Helper functions
export function getModulesByCategory(category: BackendModule['category']): BackendModule[] {
return MODULE_REGISTRY.filter(m => m.category === category)
}
export function getConnectedModules(): BackendModule[] {
return MODULE_REGISTRY.filter(m => m.frontend.status === 'connected')
}
export function getNotConnectedModules(): BackendModule[] {
return MODULE_REGISTRY.filter(m => m.frontend.status === 'not-connected')
}
export function getPartialModules(): BackendModule[] {
return MODULE_REGISTRY.filter(m => m.frontend.status === 'partial')
}
export function getModuleStats() {
const total = MODULE_REGISTRY.length
const connected = MODULE_REGISTRY.filter(m => m.frontend.status === 'connected').length
const partial = MODULE_REGISTRY.filter(m => m.frontend.status === 'partial').length
const notConnected = MODULE_REGISTRY.filter(m => m.frontend.status === 'not-connected').length
const deprecated = MODULE_REGISTRY.filter(m => m.frontend.status === 'deprecated').length
return {
total,
connected,
partial,
notConnected,
deprecated,
percentComplete: Math.round((connected / total) * 100)
}
}
export function getCategoryStats(category: BackendModule['category']) {
const modules = getModulesByCategory(category)
const total = modules.length
const connected = modules.filter(m => m.frontend.status === 'connected').length
const partial = modules.filter(m => m.frontend.status === 'partial').length
const notConnected = modules.filter(m => m.frontend.status === 'not-connected').length
return {
total,
connected,
partial,
notConnected,
percentComplete: total > 0 ? Math.round((connected / total) * 100) : 0
}
}
+448
View File
@@ -0,0 +1,448 @@
/**
* Navigation Structure for Admin-Lehrer
*
* Main categories with color-coded modules.
* SDK/Compliance categories removed - this is the Lehrer-focused admin panel.
*/
export type CategoryId = 'ai' | 'infrastructure' | 'education' | 'communication' | 'development' | 'website'
export interface NavModule {
id: string
name: string
href: string
description: string
purpose: string
audience: string[]
gdprArticles?: string[]
oldAdminPath?: string // Reference to old admin for migration
subgroup?: string // Optional subgroup for visual grouping in sidebar
}
export interface NavCategory {
id: CategoryId
name: string
icon: string
color: string
colorClass: string
description: string
modules: NavModule[]
}
export const navigation: NavCategory[] = [
// =========================================================================
// KI & Automatisierung
// =========================================================================
{
id: 'ai',
name: 'KI & Automatisierung',
icon: 'brain',
color: '#14b8a6', // Teal
colorClass: 'ai',
description: 'LLM, OCR, RAG & Machine Learning',
modules: [
// -----------------------------------------------------------------------
// KI-Daten-Pipeline: Magic Help -> OCR -> Indexierung -> Suche
// -----------------------------------------------------------------------
{
id: 'magic-help',
name: 'Magic Help (TrOCR)',
href: '/ai/magic-help',
description: 'TrOCR Testing & Fine-Tuning',
purpose: 'Testen und verbessern Sie die TrOCR-Handschrifterkennung. Laden Sie Bilder hoch, um die OCR-Qualitaet zu pruefen, und trainieren Sie das Modell mit LoRA Fine-Tuning. Bidirektionaler Austausch mit OCR-Labeling.',
audience: ['Entwickler', 'Administratoren', 'QA'],
oldAdminPath: '/admin/magic-help',
subgroup: 'KI-Daten-Pipeline',
},
{
id: 'ocr-labeling',
name: 'OCR-Labeling',
href: '/ai/ocr-labeling',
description: 'Handschrift-Training & Labels',
purpose: 'Labeln Sie Handschrift-Samples fuer das Training von TrOCR-Modellen. Erstellen Sie Ground Truth Daten, die zur RAG Pipeline exportiert werden koennen.',
audience: ['Entwickler', 'Data Scientists', 'QA'],
oldAdminPath: '/admin/ocr-labeling',
subgroup: 'KI-Daten-Pipeline',
},
{
id: 'rag-pipeline',
name: 'RAG Pipeline',
href: '/ai/rag-pipeline',
description: 'Dokument-Indexierung',
purpose: 'RAG-Pipeline fuer Bildungsdokumente: NiBiS Erwartungshorizonte, Schulordnungen, Custom EH. OCR, Chunking und Vektor-Indexierung in Qdrant.',
audience: ['Entwickler', 'Data Scientists', 'Bildungs-Admins'],
oldAdminPath: '/admin/training',
subgroup: 'KI-Daten-Pipeline',
},
{
id: 'rag',
name: 'Daten & RAG',
href: '/ai/rag',
description: 'Vektor-Suche & Collections',
purpose: 'Verwalten und durchsuchen Sie indexierte Dokumente. Zeigt Status aller Qdrant Collections und ermoeglicht semantische Suche.',
audience: ['Entwickler', 'Data Scientists', 'Compliance Officer'],
oldAdminPath: '/admin/rag',
subgroup: 'KI-Daten-Pipeline',
},
// -----------------------------------------------------------------------
// KI-Werkzeuge: Standalone-Tools fuer Entwicklung & QA
// -----------------------------------------------------------------------
{
id: 'llm-compare',
name: 'LLM Vergleich',
href: '/ai/llm-compare',
description: 'KI-Provider Vergleich',
purpose: 'Vergleichen Sie verschiedene LLM-Anbieter (Ollama, OpenAI, Anthropic) hinsichtlich Qualitaet, Geschwindigkeit und Kosten. Standalone-Werkzeug fuer Modell-Evaluation.',
audience: ['Entwickler', 'Data Scientists'],
oldAdminPath: '/admin/llm-compare',
subgroup: 'KI-Werkzeuge',
},
{
id: 'ocr-compare',
name: 'OCR Vergleich',
href: '/ai/ocr-compare',
description: 'OCR-Methoden & Vokabel-Extraktion',
purpose: 'Vergleichen Sie verschiedene OCR-Methoden (lokales LLM, Vision LLM, PaddleOCR, Tesseract, Anthropic) fuer Vokabel-Extraktion. Grid-Overlay, Block-Review und LLM-Vergleich.',
audience: ['Entwickler', 'Data Scientists', 'Lehrer'],
subgroup: 'KI-Werkzeuge',
},
{
id: 'test-quality',
name: 'Test Quality (BQAS)',
href: '/ai/test-quality',
description: 'Golden Suite, RAG & Synthetic Tests',
purpose: 'BQAS Dashboard mit Golden Suite (97 Referenz-Tests), RAG/Korrektur Tests und Synthetic Test Generierung. Ueberwacht die Qualitaet der KI-Ausgaben.',
audience: ['Entwickler', 'Data Scientists', 'QA'],
oldAdminPath: '/admin/quality',
subgroup: 'KI-Werkzeuge',
},
{
id: 'gpu',
name: 'GPU Infrastruktur',
href: '/ai/gpu',
description: 'vast.ai GPU Management',
purpose: 'Verwalten Sie GPU-Instanzen auf vast.ai fuer ML-Training und Inferenz.',
audience: ['DevOps', 'Entwickler'],
oldAdminPath: '/admin/gpu',
subgroup: 'KI-Werkzeuge',
},
// -----------------------------------------------------------------------
// KI-Anwendungen: Endnutzer-orientierte KI-Module
// -----------------------------------------------------------------------
{
id: 'agents',
name: 'Agent Management',
href: '/ai/agents',
description: 'Multi-Agent System & SOUL-Editor',
purpose: 'Verwaltung des Multi-Agent-Systems. Bearbeiten Sie Agent-Persoenlichkeiten (SOUL-Files), ueberwachen Sie Sessions und analysieren Sie Agent-Statistiken. Architektur-Dokumentation fuer Entwickler.',
audience: ['Entwickler', 'Lehrer', 'Admins'],
subgroup: 'KI-Anwendungen',
},
],
},
// =========================================================================
// Infrastruktur & DevOps
// =========================================================================
{
id: 'infrastructure',
name: 'Infrastruktur & DevOps',
icon: 'server',
color: '#f97316', // Orange
colorClass: 'infrastructure',
description: 'GPU, Security, CI/CD & Monitoring',
modules: [
{
id: 'ci-cd',
name: 'CI/CD',
href: '/infrastructure/ci-cd',
description: 'Pipelines, Deployments & Container',
purpose: 'CI/CD Dashboard mit Gitea Actions Pipelines, Deployment-Status und Container-Management.',
audience: ['DevOps', 'Entwickler'],
subgroup: 'DevOps Pipeline',
},
{
id: 'tests',
name: 'Test Dashboard',
href: '/infrastructure/tests',
description: 'Test-Suites, Coverage & CI/CD',
purpose: 'Zentrales Dashboard fuer alle 280+ Tests. Unit (Go, Python), Integration, E2E (Playwright) und BQAS Quality Tests. Aggregiert Tests aus allen Services ohne physische Migration.',
audience: ['Entwickler', 'QA', 'DevOps'],
subgroup: 'DevOps Pipeline',
},
{
id: 'sbom',
name: 'SBOM',
href: '/infrastructure/sbom',
description: 'Software Bill of Materials',
purpose: 'Verwalten Sie alle Software-Abhaengigkeiten und deren Lizenzen.',
audience: ['DevOps', 'Compliance'],
oldAdminPath: '/admin/sbom',
subgroup: 'DevOps Pipeline',
},
{
id: 'security',
name: 'Security',
href: '/infrastructure/security',
description: 'DevSecOps Dashboard & Scans',
purpose: 'Security-Scans, Vulnerability-Reports und OWASP-Compliance.',
audience: ['DevOps', 'Security'],
oldAdminPath: '/admin/security',
subgroup: 'DevOps Pipeline',
},
{
id: 'middleware',
name: 'Middleware',
href: '/infrastructure/middleware',
description: 'Middleware Stack & API Gateway',
purpose: 'Ueberwachen und testen Sie den Middleware-Stack und API Gateway.',
audience: ['DevOps'],
oldAdminPath: '/admin/middleware',
subgroup: 'Infrastructure',
},
],
},
// =========================================================================
// Bildung & Schule
// =========================================================================
{
id: 'education',
name: 'Bildung & Schule',
icon: 'graduation',
color: '#3b82f6', // Blue
colorClass: 'education',
description: 'Bildungsquellen & Lehrplaene',
modules: [
{
id: 'edu-search',
name: 'Education Search',
href: '/education/edu-search',
description: 'Bildungsquellen & Crawler',
purpose: 'Verwalten Sie Bildungsquellen und konfigurieren Sie Crawler fuer neue Inhalte.',
audience: ['Content Manager'],
oldAdminPath: '/admin/edu-search',
},
{
id: 'zeugnisse-crawler',
name: 'Zeugnisse-Crawler',
href: '/education/zeugnisse-crawler',
description: 'Zeugnis-Daten',
purpose: 'Verwalten Sie gecrawlte Zeugnis-Strukturen und -Formate.',
audience: ['Entwickler'],
oldAdminPath: '/admin/zeugnisse-crawler',
},
{
id: 'abitur-archiv',
name: 'Abitur-Archiv',
href: '/education/abitur-archiv',
description: 'Zentralabitur-Materialien 2021-2025',
purpose: 'Durchsuchen und filtern Sie Abitur-Aufgaben und Erwartungshorizonte. Themensuche mit semantischer Suche via RAG. Integration mit Klausur-Korrektur fuer schnelle Vorlagen-Nutzung.',
audience: ['Lehrer', 'Entwickler'],
},
{
id: 'klausur-korrektur',
name: 'Klausur-Korrektur',
href: '/education/klausur-korrektur',
description: 'Abitur-Korrektur mit KI',
purpose: 'KI-gestuetzte Korrektur von Abitur- und Vorabitur-Klausuren. Nutzt die RAG-Pipeline fuer Erwartungshorizont-Vorschlaege.',
audience: ['Lehrer', 'Entwickler'],
oldAdminPath: '/admin/klausur-korrektur',
},
],
},
// =========================================================================
// Kommunikation & Alerts
// =========================================================================
{
id: 'communication',
name: 'Kommunikation & Alerts',
icon: 'mail',
color: '#22c55e', // Green
colorClass: 'communication',
description: 'Matrix, E-Mail & Benachrichtigungen',
modules: [
{
id: 'video-chat',
name: 'Video & Chat',
href: '/communication/video-chat',
description: 'Matrix & Jitsi Monitoring',
purpose: 'Dashboard fuer Matrix Synapse (E2EE Messaging) und Jitsi Meet (Videokonferenzen). Ueberwachen Sie Service-Status, aktive Meetings, Traffic und SysEleven Ressourcenplanung.',
audience: ['Admins', 'DevOps', 'Support'],
oldAdminPath: '/admin/communication',
},
{
id: 'matrix',
name: 'Voice Service',
href: '/communication/matrix',
description: 'Voice-First Interface & Architektur',
purpose: 'Konfigurieren und testen Sie den Voice-Service (PersonaPlex-7B, TaskOrchestrator). Dokumentation der Voice-First Architektur mit DSGVO-Compliance.',
audience: ['Entwickler', 'Admins'],
oldAdminPath: '/admin/voice',
},
{
id: 'mail',
name: 'Unified Inbox',
href: '/communication/mail',
description: 'E-Mail-Konten & KI-Analyse',
purpose: 'Verwalten Sie E-Mail-Konten und nutzen Sie KI zur Kategorisierung.',
audience: ['Support', 'Admins'],
oldAdminPath: '/admin/mail',
},
{
id: 'alerts',
name: 'Alerts Monitoring',
href: '/communication/alerts',
description: 'Google Alerts & Feed-Ueberwachung',
purpose: 'Ueberwachen Sie Google Alerts und RSS-Feeds fuer relevante Neuigkeiten.',
audience: ['Marketing', 'Admins'],
oldAdminPath: '/admin/alerts',
},
],
},
// =========================================================================
// Entwicklung & Produkte
// =========================================================================
{
id: 'development',
name: 'Entwicklung & Produkte',
icon: 'code',
color: '#64748b', // Slate
colorClass: 'development',
description: 'Workflow, Game, Docs & Brandbook',
modules: [
{
id: 'workflow',
name: 'Dev Workflow',
href: '/development/workflow',
description: 'Git, CI/CD & Team-Regeln',
purpose: 'Entwicklungs-Workflow mit Git, CI/CD Pipeline und Team-Konventionen. Pflichtlektuere fuer alle Entwickler.',
audience: ['Entwickler', 'DevOps'],
},
{
id: 'docs',
name: 'Developer Docs',
href: '/development/docs',
description: 'API & Architektur',
purpose: 'Durchsuchen Sie die API-Dokumentation und Architektur-Diagramme.',
audience: ['Entwickler'],
oldAdminPath: '/admin/docs',
},
{
id: 'brandbook',
name: 'Brandbook',
href: '/development/brandbook',
description: 'Corporate Design',
purpose: 'Referenz fuer Logos, Farben, Typografie und Design-Richtlinien.',
audience: ['Designer', 'Marketing'],
oldAdminPath: '/admin/brandbook',
},
{
id: 'screen-flow',
name: 'Screen Flow',
href: '/development/screen-flow',
description: 'UI Screen-Verbindungen',
purpose: 'Visualisieren Sie die Navigation und Screen-Verbindungen der App.',
audience: ['Designer', 'Entwickler'],
oldAdminPath: '/admin/screen-flow',
},
],
},
// =========================================================================
// Website
// =========================================================================
{
id: 'website',
name: 'Website',
icon: 'globe',
color: '#0ea5e9', // Sky-500
colorClass: 'website',
description: 'Website Content & Management',
modules: [
{
id: 'uebersetzungen',
name: 'Uebersetzungen',
href: '/website/uebersetzungen',
description: 'Website Content & Sprachen',
purpose: 'Verwalten Sie Website-Inhalte und Uebersetzungen.',
audience: ['Content Manager'],
oldAdminPath: '/admin/content',
},
{
id: 'manager',
name: 'Website Manager',
href: '/website/manager',
description: 'CMS Dashboard',
purpose: 'Visuelles CMS-Dashboard fuer die BreakPilot Website. Alle Sektionen bearbeiten mit Live-Preview.',
audience: ['Content Manager', 'Entwickler'],
},
],
},
]
// Meta modules (always visible)
export const metaModules: NavModule[] = [
{
id: 'dashboard',
name: 'Dashboard',
href: '/dashboard',
description: 'Uebersicht & Statistiken',
purpose: 'Zentrale Uebersicht ueber alle Systeme mit wichtigen Kennzahlen.',
audience: ['Alle'],
oldAdminPath: '/admin',
},
{
id: 'architecture',
name: 'Architektur',
href: '/architecture',
description: 'Backend-Module & Datenfluss',
purpose: 'Uebersicht aller Backend-Module und deren Verbindung zum Frontend. Essentiell fuer Migration und Audit.',
audience: ['Entwickler', 'DevOps', 'Auditoren', 'Manager'],
},
{
id: 'onboarding',
name: 'Onboarding',
href: '/onboarding',
description: 'Lern-Wizards',
purpose: 'Gefuehrte Tutorials fuer neue Benutzer.',
audience: ['Alle'],
oldAdminPath: '/admin/onboarding',
},
{
id: 'backlog',
name: 'Production Backlog',
href: '/backlog',
description: 'Go-Live Checkliste',
purpose: 'Verfolgen Sie den Fortschritt zum Production-Launch.',
audience: ['Entwickler', 'Manager'],
oldAdminPath: '/admin/backlog',
},
{
id: 'rbac',
name: 'RBAC',
href: '/rbac',
description: 'Rollen & Berechtigungen',
purpose: 'Verwalten Sie Benutzerrollen und Zugriffsrechte.',
audience: ['Admins', 'DSB'],
oldAdminPath: '/admin/rbac',
},
]
// Helper function to get category by ID
export function getCategoryById(id: CategoryId): NavCategory | undefined {
return navigation.find(cat => cat.id === id)
}
// Helper function to get module by href
export function getModuleByHref(href: string): { category: NavCategory; module: NavModule } | undefined {
for (const category of navigation) {
const module = category.modules.find(m => m.href === href)
if (module) {
return { category, module }
}
}
return undefined
}
// Helper function to get all modules flat
export function getAllModules(): NavModule[] {
return [...navigation.flatMap(cat => cat.modules), ...metaModules]
}
+85
View File
@@ -0,0 +1,85 @@
/**
* Role-based Access System for Admin-Lehrer
*
* Roles determine which categories and modules are visible.
* SDK-specific roles (auditor, dsb) removed for Lehrer frontend.
*/
import { CategoryId } from './navigation'
export type RoleId = 'developer' | 'manager'
export interface Role {
id: RoleId
name: string
description: string
icon: string
visibleCategories: CategoryId[]
color: string
}
export const roles: Role[] = [
{
id: 'developer',
name: 'Entwickler',
description: 'Voller Zugriff auf alle Bereiche',
icon: 'code',
visibleCategories: ['ai', 'infrastructure', 'education', 'communication', 'development', 'website'],
color: 'bg-primary-100 border-primary-300 text-primary-700',
},
{
id: 'manager',
name: 'Manager',
description: 'Executive Uebersicht',
icon: 'chart',
visibleCategories: ['communication', 'website'],
color: 'bg-blue-100 border-blue-300 text-blue-700',
},
]
// Storage key for localStorage
const ROLE_STORAGE_KEY = 'admin-lehrer-selected-role'
// Get role by ID
export function getRoleById(id: RoleId): Role | undefined {
return roles.find(role => role.id === id)
}
// Check if category is visible for a role
export function isCategoryVisibleForRole(categoryId: CategoryId, roleId: RoleId): boolean {
const role = getRoleById(roleId)
return role ? role.visibleCategories.includes(categoryId) : false
}
// Get stored role from localStorage (client-side only)
export function getStoredRole(): RoleId | null {
if (typeof window === 'undefined') return null
const stored = localStorage.getItem(ROLE_STORAGE_KEY)
if (stored && roles.some(r => r.id === stored)) {
return stored as RoleId
}
return null
}
// Store role in localStorage
export function storeRole(roleId: RoleId): void {
if (typeof window === 'undefined') return
localStorage.setItem(ROLE_STORAGE_KEY, roleId)
}
// Clear stored role
export function clearStoredRole(): void {
if (typeof window === 'undefined') return
localStorage.removeItem(ROLE_STORAGE_KEY)
}
// Check if this is a first-time visitor (no role stored)
export function isFirstTimeVisitor(): boolean {
return getStoredRole() === null
}
// Get visible categories for a role
export function getVisibleCategoriesForRole(roleId: RoleId): CategoryId[] {
const role = getRoleById(roleId)
return role ? role.visibleCategories : []
}
@@ -0,0 +1,324 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { exportToPDF, exportToZIP, downloadExport } from '../export'
import type { SDKState } from '../types'
// Mock jsPDF as a class
vi.mock('jspdf', () => {
return {
default: class MockJsPDF {
internal = {
pageSize: { getWidth: () => 210, getHeight: () => 297 },
}
setFillColor = vi.fn().mockReturnThis()
setDrawColor = vi.fn().mockReturnThis()
setTextColor = vi.fn().mockReturnThis()
setFontSize = vi.fn().mockReturnThis()
setFont = vi.fn().mockReturnThis()
setLineWidth = vi.fn().mockReturnThis()
text = vi.fn().mockReturnThis()
line = vi.fn().mockReturnThis()
rect = vi.fn().mockReturnThis()
roundedRect = vi.fn().mockReturnThis()
circle = vi.fn().mockReturnThis()
addPage = vi.fn().mockReturnThis()
setPage = vi.fn().mockReturnThis()
getNumberOfPages = vi.fn(() => 5)
splitTextToSize = vi.fn((text: string) => [text])
output = vi.fn(() => new Blob(['mock-pdf'], { type: 'application/pdf' }))
},
}
})
// Mock JSZip as a class
vi.mock('jszip', () => {
return {
default: class MockJSZip {
private mockFolder = {
file: vi.fn().mockReturnThis(),
folder: vi.fn(() => this.mockFolder),
}
folder = vi.fn(() => this.mockFolder)
generateAsync = vi.fn(() => Promise.resolve(new Blob(['mock-zip'], { type: 'application/zip' })))
},
}
})
const createMockState = (overrides: Partial<SDKState> = {}): SDKState => ({
version: '1.0.0',
lastModified: new Date('2024-01-15'),
tenantId: 'test-tenant',
userId: 'test-user',
subscription: 'PROFESSIONAL',
currentPhase: 1,
currentStep: 'use-case-workshop',
completedSteps: ['use-case-workshop', 'screening'],
checkpoints: {
'CP-UC': {
checkpointId: 'CP-UC',
passed: true,
validatedAt: new Date(),
validatedBy: 'SYSTEM',
errors: [],
warnings: [],
},
},
useCases: [
{
id: 'uc-1',
name: 'Test Use Case',
description: 'A test use case for testing',
category: 'Marketing',
stepsCompleted: 3,
steps: [
{ id: 's1', name: 'Step 1', completed: true, data: {} },
{ id: 's2', name: 'Step 2', completed: true, data: {} },
{ id: 's3', name: 'Step 3', completed: true, data: {} },
],
assessmentResult: null,
createdAt: new Date(),
updatedAt: new Date(),
},
],
activeUseCase: 'uc-1',
screening: null,
modules: [],
requirements: [],
controls: [
{
id: 'ctrl-1',
name: 'Test Control',
description: 'A test control',
type: 'TECHNICAL',
category: 'Access Control',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: [],
owner: 'Test Owner',
dueDate: null,
},
],
evidence: [],
checklist: [],
risks: [
{
id: 'risk-1',
title: 'Test Risk',
description: 'A test risk',
category: 'Security',
likelihood: 3,
impact: 4,
severity: 'HIGH',
inherentRiskScore: 12,
residualRiskScore: 6,
status: 'MITIGATED',
mitigation: [
{
id: 'mit-1',
description: 'Test mitigation',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 80,
controlId: 'ctrl-1',
},
],
owner: 'Risk Owner',
relatedControls: ['ctrl-1'],
relatedRequirements: [],
},
],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
sbom: null,
securityIssues: [],
securityBacklog: [],
commandBarHistory: [],
recentSearches: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
...overrides,
})
describe('exportToPDF', () => {
it('should return a Blob', async () => {
const state = createMockState()
const result = await exportToPDF(state)
expect(result).toBeInstanceOf(Blob)
})
it('should create a PDF with the correct type', async () => {
const state = createMockState()
const result = await exportToPDF(state)
expect(result.type).toBe('application/pdf')
})
it('should handle empty state', async () => {
const emptyState = createMockState({
useCases: [],
risks: [],
controls: [],
completedSteps: [],
})
const result = await exportToPDF(emptyState)
expect(result).toBeInstanceOf(Blob)
})
it('should handle state with multiple risks of different severities', async () => {
const state = createMockState({
risks: [
{
id: 'risk-1',
title: 'Critical Risk',
description: 'Critical',
category: 'Security',
likelihood: 5,
impact: 5,
severity: 'CRITICAL',
inherentRiskScore: 25,
residualRiskScore: 15,
status: 'IDENTIFIED',
mitigation: [],
owner: null,
relatedControls: [],
relatedRequirements: [],
},
{
id: 'risk-2',
title: 'Low Risk',
description: 'Low',
category: 'Operational',
likelihood: 1,
impact: 1,
severity: 'LOW',
inherentRiskScore: 1,
residualRiskScore: 1,
status: 'ACCEPTED',
mitigation: [],
owner: null,
relatedControls: [],
relatedRequirements: [],
},
],
})
const result = await exportToPDF(state)
expect(result).toBeInstanceOf(Blob)
})
})
describe('exportToZIP', () => {
it('should return a Blob', async () => {
const state = createMockState()
const result = await exportToZIP(state)
expect(result).toBeInstanceOf(Blob)
})
it('should create a ZIP with the correct type', async () => {
const state = createMockState()
const result = await exportToZIP(state)
expect(result.type).toBe('application/zip')
})
it('should handle empty state', async () => {
const emptyState = createMockState({
useCases: [],
risks: [],
controls: [],
completedSteps: [],
})
const result = await exportToZIP(emptyState)
expect(result).toBeInstanceOf(Blob)
})
it('should respect includeEvidence option', async () => {
const state = createMockState()
const result = await exportToZIP(state, { includeEvidence: false })
expect(result).toBeInstanceOf(Blob)
})
it('should respect includeDocuments option', async () => {
const state = createMockState()
const result = await exportToZIP(state, { includeDocuments: false })
expect(result).toBeInstanceOf(Blob)
})
})
describe('downloadExport', () => {
let mockCreateElement: ReturnType<typeof vi.spyOn>
let mockAppendChild: ReturnType<typeof vi.spyOn>
let mockRemoveChild: ReturnType<typeof vi.spyOn>
let mockLink: { href: string; download: string; click: ReturnType<typeof vi.fn> }
beforeEach(() => {
mockLink = {
href: '',
download: '',
click: vi.fn(),
}
mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as unknown as HTMLElement)
mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockLink as unknown as HTMLElement)
mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockLink as unknown as HTMLElement)
})
it('should download JSON format', async () => {
const state = createMockState()
await downloadExport(state, 'json')
expect(mockLink.download).toContain('.json')
expect(mockLink.click).toHaveBeenCalled()
})
it('should download PDF format', async () => {
const state = createMockState()
await downloadExport(state, 'pdf')
expect(mockLink.download).toContain('.pdf')
expect(mockLink.click).toHaveBeenCalled()
})
it('should download ZIP format', async () => {
const state = createMockState()
await downloadExport(state, 'zip')
expect(mockLink.download).toContain('.zip')
expect(mockLink.click).toHaveBeenCalled()
})
it('should include date in filename', async () => {
const state = createMockState()
await downloadExport(state, 'json')
// Check that filename contains a date pattern
expect(mockLink.download).toMatch(/ai-compliance-sdk-\d{4}-\d{2}-\d{2}\.json/)
})
it('should throw error for unknown format', async () => {
const state = createMockState()
await expect(downloadExport(state, 'unknown' as any)).rejects.toThrow('Unknown export format')
})
})
@@ -0,0 +1,250 @@
import { describe, it, expect } from 'vitest'
import {
SDK_STEPS,
getStepById,
getStepByUrl,
getNextStep,
getPreviousStep,
getCompletionPercentage,
getPhaseCompletionPercentage,
type SDKState,
} from '../types'
describe('SDK_STEPS', () => {
it('should have steps defined for both phases', () => {
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
expect(phase1Steps.length).toBeGreaterThan(0)
expect(phase2Steps.length).toBeGreaterThan(0)
})
it('should have unique IDs for all steps', () => {
const ids = SDK_STEPS.map(s => s.id)
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(ids.length)
})
it('should have unique URLs for all steps', () => {
const urls = SDK_STEPS.map(s => s.url)
const uniqueUrls = new Set(urls)
expect(uniqueUrls.size).toBe(urls.length)
})
it('should have checkpoint IDs for all steps', () => {
SDK_STEPS.forEach(step => {
expect(step.checkpointId).toBeDefined()
expect(step.checkpointId.length).toBeGreaterThan(0)
})
})
})
describe('getStepById', () => {
it('should return the correct step for a valid ID', () => {
const step = getStepById('use-case-workshop')
expect(step).toBeDefined()
expect(step?.name).toBe('Use Case Workshop')
})
it('should return undefined for an invalid ID', () => {
const step = getStepById('invalid-step-id')
expect(step).toBeUndefined()
})
it('should find steps in Phase 2', () => {
const step = getStepById('dsfa')
expect(step).toBeDefined()
expect(step?.phase).toBe(2)
})
})
describe('getStepByUrl', () => {
it('should return the correct step for a valid URL', () => {
const step = getStepByUrl('/sdk/advisory-board')
expect(step).toBeDefined()
expect(step?.id).toBe('use-case-workshop')
})
it('should return undefined for an invalid URL', () => {
const step = getStepByUrl('/invalid/url')
expect(step).toBeUndefined()
})
it('should find Phase 2 steps by URL', () => {
const step = getStepByUrl('/sdk/dsfa')
expect(step).toBeDefined()
expect(step?.id).toBe('dsfa')
})
})
describe('getNextStep', () => {
it('should return the next step in sequence', () => {
const nextStep = getNextStep('use-case-workshop')
expect(nextStep).toBeDefined()
expect(nextStep?.id).toBe('screening')
})
it('should return undefined for the last step', () => {
const lastStep = SDK_STEPS[SDK_STEPS.length - 1]
const nextStep = getNextStep(lastStep.id)
expect(nextStep).toBeUndefined()
})
it('should handle transition between phases', () => {
const lastPhase1Step = SDK_STEPS.filter(s => s.phase === 1).pop()
expect(lastPhase1Step).toBeDefined()
const nextStep = getNextStep(lastPhase1Step!.id)
expect(nextStep?.phase).toBe(2)
})
})
describe('getPreviousStep', () => {
it('should return the previous step in sequence', () => {
const prevStep = getPreviousStep('screening')
expect(prevStep).toBeDefined()
expect(prevStep?.id).toBe('use-case-workshop')
})
it('should return undefined for the first step', () => {
const prevStep = getPreviousStep('use-case-workshop')
expect(prevStep).toBeUndefined()
})
})
describe('getCompletionPercentage', () => {
const createMockState = (completedSteps: string[]): SDKState => ({
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'test',
subscription: 'PROFESSIONAL',
currentPhase: 1,
currentStep: 'use-case-workshop',
completedSteps,
checkpoints: {},
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
sbom: null,
securityIssues: [],
securityBacklog: [],
commandBarHistory: [],
recentSearches: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
})
it('should return 0 for no completed steps', () => {
const state = createMockState([])
const percentage = getCompletionPercentage(state)
expect(percentage).toBe(0)
})
it('should return 100 for all completed steps', () => {
const allStepIds = SDK_STEPS.map(s => s.id)
const state = createMockState(allStepIds)
const percentage = getCompletionPercentage(state)
expect(percentage).toBe(100)
})
it('should calculate correct percentage for partial completion', () => {
const halfSteps = SDK_STEPS.slice(0, Math.floor(SDK_STEPS.length / 2)).map(s => s.id)
const state = createMockState(halfSteps)
const percentage = getCompletionPercentage(state)
expect(percentage).toBeGreaterThan(40)
expect(percentage).toBeLessThan(60)
})
})
describe('getPhaseCompletionPercentage', () => {
const createMockState = (completedSteps: string[]): SDKState => ({
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'test',
subscription: 'PROFESSIONAL',
currentPhase: 1,
currentStep: 'use-case-workshop',
completedSteps,
checkpoints: {},
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
sbom: null,
securityIssues: [],
securityBacklog: [],
commandBarHistory: [],
recentSearches: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
})
it('should return 0 for Phase 1 with no completed steps', () => {
const state = createMockState([])
const percentage = getPhaseCompletionPercentage(state, 1)
expect(percentage).toBe(0)
})
it('should return 100 for Phase 1 when all Phase 1 steps are complete', () => {
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1).map(s => s.id)
const state = createMockState(phase1Steps)
const percentage = getPhaseCompletionPercentage(state, 1)
expect(percentage).toBe(100)
})
it('should not count Phase 2 steps in Phase 1 percentage', () => {
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2).map(s => s.id)
const state = createMockState(phase2Steps)
const percentage = getPhaseCompletionPercentage(state, 1)
expect(percentage).toBe(0)
})
})
+471
View File
@@ -0,0 +1,471 @@
/**
* SDK API Client
*
* Centralized API client for SDK state management with error handling,
* retry logic, and optimistic locking support.
*/
import { SDKState, CheckpointStatus } from './types'
// =============================================================================
// TYPES
// =============================================================================
export interface APIResponse<T> {
success: boolean
data?: T
error?: string
version?: number
lastModified?: string
}
export interface StateResponse {
tenantId: string
state: SDKState
version: number
lastModified: string
}
export interface SaveStateRequest {
tenantId: string
state: SDKState
version?: number // For optimistic locking
}
export interface CheckpointValidationResult {
checkpointId: string
passed: boolean
errors: Array<{
ruleId: string
field: string
message: string
severity: 'ERROR' | 'WARNING' | 'INFO'
}>
warnings: Array<{
ruleId: string
field: string
message: string
severity: 'ERROR' | 'WARNING' | 'INFO'
}>
validatedAt: string
validatedBy: string
}
export interface APIError extends Error {
status?: number
code?: string
retryable: boolean
}
// =============================================================================
// CONFIGURATION
// =============================================================================
const DEFAULT_BASE_URL = '/api/sdk/v1'
const DEFAULT_TIMEOUT = 30000 // 30 seconds
const MAX_RETRIES = 3
const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
// =============================================================================
// API CLIENT
// =============================================================================
export class SDKApiClient {
private baseUrl: string
private tenantId: string
private timeout: number
private abortControllers: Map<string, AbortController> = new Map()
constructor(options: {
baseUrl?: string
tenantId: string
timeout?: number
}) {
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL
this.tenantId = options.tenantId
this.timeout = options.timeout || DEFAULT_TIMEOUT
}
// ---------------------------------------------------------------------------
// Private Methods
// ---------------------------------------------------------------------------
private createError(message: string, status?: number, retryable = false): APIError {
const error = new Error(message) as APIError
error.status = status
error.retryable = retryable
return error
}
private async fetchWithTimeout(
url: string,
options: RequestInit,
requestId: string
): Promise<Response> {
const controller = new AbortController()
this.abortControllers.set(requestId, controller)
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
})
return response
} finally {
clearTimeout(timeoutId)
this.abortControllers.delete(requestId)
}
}
private async fetchWithRetry<T>(
url: string,
options: RequestInit,
retries = MAX_RETRIES
): Promise<T> {
const requestId = `${Date.now()}-${Math.random()}`
let lastError: Error | null = null
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await this.fetchWithTimeout(url, options, requestId)
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep the HTTP status message
}
// Don't retry client errors (4xx) except for 429 (rate limit)
const retryable = response.status >= 500 || response.status === 429
if (!retryable || attempt === retries) {
throw this.createError(errorMessage, response.status, retryable)
}
} else {
const data = await response.json()
return data as T
}
} catch (error) {
lastError = error as Error
if (error instanceof Error && error.name === 'AbortError') {
throw this.createError('Request timeout', 408, true)
}
// Check if it's a retryable error
const apiError = error as APIError
if (!apiError.retryable || attempt === retries) {
throw error
}
}
// Wait before retrying (exponential backoff)
if (attempt < retries) {
await this.sleep(RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1])
}
}
throw lastError || this.createError('Unknown error', 500, false)
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
// ---------------------------------------------------------------------------
// Public Methods - State Management
// ---------------------------------------------------------------------------
/**
* Load SDK state for the current tenant
*/
async getState(): Promise<StateResponse | null> {
try {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)
if (response.success && response.data) {
return response.data
}
return null
} catch (error) {
const apiError = error as APIError
// 404 means no state exists yet - that's okay
if (apiError.status === 404) {
return null
}
throw error
}
}
/**
* Save SDK state for the current tenant
* Supports optimistic locking via version parameter
*/
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
`${this.baseUrl}/state`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(version !== undefined && { 'If-Match': String(version) }),
},
body: JSON.stringify({
tenantId: this.tenantId,
state,
version,
}),
}
)
if (!response.success) {
throw this.createError(response.error || 'Failed to save state', 500, true)
}
return response.data!
}
/**
* Delete SDK state for the current tenant
*/
async deleteState(): Promise<void> {
await this.fetchWithRetry<APIResponse<void>>(
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
}
)
}
// ---------------------------------------------------------------------------
// Public Methods - Checkpoint Validation
// ---------------------------------------------------------------------------
/**
* Validate a specific checkpoint
*/
async validateCheckpoint(
checkpointId: string,
data?: unknown
): Promise<CheckpointValidationResult> {
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
`${this.baseUrl}/checkpoints/validate`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId: this.tenantId,
checkpointId,
data,
}),
}
)
if (!response.success || !response.data) {
throw this.createError(response.error || 'Checkpoint validation failed', 500, true)
}
return response.data
}
/**
* Get all checkpoint statuses
*/
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
`${this.baseUrl}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)
return response.data || {}
}
// ---------------------------------------------------------------------------
// Public Methods - Flow Navigation
// ---------------------------------------------------------------------------
/**
* Get current flow state
*/
async getFlowState(): Promise<{
currentStep: string
currentPhase: 1 | 2
completedSteps: string[]
suggestions: Array<{ stepId: string; reason: string }>
}> {
const response = await this.fetchWithRetry<APIResponse<{
currentStep: string
currentPhase: 1 | 2
completedSteps: string[]
suggestions: Array<{ stepId: string; reason: string }>
}>>(
`${this.baseUrl}/flow?tenantId=${encodeURIComponent(this.tenantId)}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)
if (!response.data) {
throw this.createError('Failed to get flow state', 500, true)
}
return response.data
}
/**
* Navigate to next/previous step
*/
async navigateFlow(direction: 'next' | 'previous'): Promise<{
stepId: string
phase: 1 | 2
}> {
const response = await this.fetchWithRetry<APIResponse<{
stepId: string
phase: 1 | 2
}>>(
`${this.baseUrl}/flow`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId: this.tenantId,
direction,
}),
}
)
if (!response.data) {
throw this.createError('Failed to navigate flow', 500, true)
}
return response.data
}
// ---------------------------------------------------------------------------
// Public Methods - Export
// ---------------------------------------------------------------------------
/**
* Export SDK state in various formats
*/
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> {
const response = await this.fetchWithTimeout(
`${this.baseUrl}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
{
method: 'GET',
headers: {
'Accept': format === 'json' ? 'application/json' : format === 'pdf' ? 'application/pdf' : 'application/zip',
},
},
`export-${Date.now()}`
)
if (!response.ok) {
throw this.createError(`Export failed: ${response.statusText}`, response.status, true)
}
return response.blob()
}
// ---------------------------------------------------------------------------
// Public Methods - Utility
// ---------------------------------------------------------------------------
/**
* Cancel all pending requests
*/
cancelAllRequests(): void {
this.abortControllers.forEach(controller => controller.abort())
this.abortControllers.clear()
}
/**
* Update tenant ID (useful when switching contexts)
*/
setTenantId(tenantId: string): void {
this.tenantId = tenantId
}
/**
* Get current tenant ID
*/
getTenantId(): string {
return this.tenantId
}
/**
* Health check
*/
async healthCheck(): Promise<boolean> {
try {
const response = await this.fetchWithTimeout(
`${this.baseUrl}/health`,
{ method: 'GET' },
`health-${Date.now()}`
)
return response.ok
} catch {
return false
}
}
}
// =============================================================================
// SINGLETON FACTORY
// =============================================================================
let clientInstance: SDKApiClient | null = null
export function getSDKApiClient(tenantId?: string): SDKApiClient {
if (!clientInstance && !tenantId) {
throw new Error('SDKApiClient not initialized. Provide tenantId on first call.')
}
if (!clientInstance && tenantId) {
clientInstance = new SDKApiClient({ tenantId })
}
if (tenantId && clientInstance && clientInstance.getTenantId() !== tenantId) {
clientInstance.setTenantId(tenantId)
}
return clientInstance!
}
export function resetSDKApiClient(): void {
if (clientInstance) {
clientInstance.cancelAllRequests()
}
clientInstance = null
}
@@ -0,0 +1,665 @@
/**
* SDK Catalog Manager - Central Registry
*
* Maps all SDK catalogs to a unified interface for browsing, searching, and CRUD.
*/
import type {
CatalogId,
CatalogMeta,
CatalogModule,
CatalogEntry,
CatalogStats,
CatalogOverviewStats,
CustomCatalogEntry,
CustomCatalogs,
} from './types'
// =============================================================================
// CATALOG DATA IMPORTS
// =============================================================================
import { RISK_CATALOG } from '../dsfa/risk-catalog'
import { MITIGATION_LIBRARY } from '../dsfa/mitigation-library'
import { AI_RISK_CATALOG } from '../dsfa/ai-risk-catalog'
import { AI_MITIGATION_LIBRARY } from '../dsfa/ai-mitigation-library'
import { PROHIBITED_AI_PRACTICES } from '../dsfa/prohibited-ai-practices'
import { EU_BASE_FRAMEWORKS, NATIONAL_FRAMEWORKS } from '../dsfa/eu-legal-frameworks'
import { GDPR_ENFORCEMENT_CASES } from '../dsfa/gdpr-enforcement-cases'
import { WP248_CRITERIA, SDM_GOALS, DSFA_AUTHORITY_RESOURCES } from '../dsfa/types'
import { VVT_BASELINE_CATALOG } from '../vvt-baseline-catalog'
import { BASELINE_TEMPLATES } from '../loeschfristen-baseline-catalog'
import { VENDOR_TEMPLATES, COUNTRY_RISK_PROFILES } from '../vendor-compliance/catalog/vendor-templates'
import { LEGAL_BASIS_INFO, STANDARD_RETENTION_PERIODS } from '../vendor-compliance/catalog/legal-basis'
// =============================================================================
// HELPER: Resolve localized text fields
// =============================================================================
function resolveField(value: unknown): string {
if (value === null || value === undefined) return ''
if (typeof value === 'string') return value
if (typeof value === 'number') return String(value)
if (typeof value === 'object' && 'de' in (value as Record<string, unknown>)) {
return String((value as Record<string, string>).de || '')
}
return String(value)
}
// =============================================================================
// SDM_GOALS as entries array (it's a Record, not an array)
// =============================================================================
const SDM_GOALS_ENTRIES = Object.entries(SDM_GOALS).map(([key, val]) => ({
id: key,
name: val.name,
description: val.description,
article: val.article,
}))
// =============================================================================
// CATALOG REGISTRY
// =============================================================================
export const CATALOG_REGISTRY: Record<CatalogId, CatalogMeta> = {
'dsfa-risks': {
id: 'dsfa-risks',
name: 'DSFA Risikokatalog',
description: 'Standardrisiken fuer Datenschutz-Folgenabschaetzungen',
module: 'dsfa',
icon: 'ShieldAlert',
systemCount: RISK_CATALOG.length,
allowCustom: true,
idField: 'id',
nameField: 'title',
descriptionField: 'description',
categoryField: 'category',
fields: [
{ key: 'id', label: 'Risiko-ID', type: 'text', required: true, placeholder: 'R-XXX-01' },
{ key: 'title', label: 'Titel', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'category', label: 'Kategorie', type: 'select', required: true, options: [
{ value: 'confidentiality', label: 'Vertraulichkeit' },
{ value: 'integrity', label: 'Integritaet' },
{ value: 'availability', label: 'Verfuegbarkeit' },
{ value: 'rights_freedoms', label: 'Rechte & Freiheiten' },
]},
{ key: 'typicalLikelihood', label: 'Typische Eintrittswahrscheinlichkeit', type: 'select', required: false, options: [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
]},
{ key: 'typicalImpact', label: 'Typische Auswirkung', type: 'select', required: false, options: [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
]},
],
searchableFields: ['id', 'title', 'description', 'category'],
},
'dsfa-mitigations': {
id: 'dsfa-mitigations',
name: 'DSFA Massnahmenbibliothek',
description: 'Technische und organisatorische Massnahmen fuer DSFAs',
module: 'dsfa',
icon: 'Shield',
systemCount: MITIGATION_LIBRARY.length,
allowCustom: true,
idField: 'id',
nameField: 'title',
descriptionField: 'description',
categoryField: 'type',
fields: [
{ key: 'id', label: 'Massnahmen-ID', type: 'text', required: true, placeholder: 'M-XXX-01' },
{ key: 'title', label: 'Titel', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'type', label: 'Typ', type: 'select', required: true, options: [
{ value: 'technical', label: 'Technisch' },
{ value: 'organizational', label: 'Organisatorisch' },
{ value: 'legal', label: 'Rechtlich' },
]},
{ key: 'effectiveness', label: 'Wirksamkeit', type: 'select', required: false, options: [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
]},
{ key: 'legalBasis', label: 'Rechtsgrundlage', type: 'text', required: false },
],
searchableFields: ['id', 'title', 'description', 'type', 'legalBasis'],
},
'ai-risks': {
id: 'ai-risks',
name: 'KI-Risikokatalog',
description: 'Spezifische Risiken fuer KI-Systeme',
module: 'ai_act',
icon: 'Bot',
systemCount: AI_RISK_CATALOG.length,
allowCustom: true,
idField: 'id',
nameField: 'title',
descriptionField: 'description',
categoryField: 'category',
fields: [
{ key: 'id', label: 'Risiko-ID', type: 'text', required: true, placeholder: 'R-AI-XXX-01' },
{ key: 'title', label: 'Titel', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'category', label: 'Kategorie', type: 'select', required: true, options: [
{ value: 'confidentiality', label: 'Vertraulichkeit' },
{ value: 'integrity', label: 'Integritaet' },
{ value: 'availability', label: 'Verfuegbarkeit' },
{ value: 'rights_freedoms', label: 'Rechte & Freiheiten' },
]},
{ key: 'typicalLikelihood', label: 'Eintrittswahrscheinlichkeit', type: 'select', required: false, options: [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
]},
{ key: 'typicalImpact', label: 'Auswirkung', type: 'select', required: false, options: [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
]},
],
searchableFields: ['id', 'title', 'description', 'category'],
},
'ai-mitigations': {
id: 'ai-mitigations',
name: 'KI-Massnahmenbibliothek',
description: 'Massnahmen fuer KI-spezifische Risiken',
module: 'ai_act',
icon: 'ShieldCheck',
systemCount: AI_MITIGATION_LIBRARY.length,
allowCustom: true,
idField: 'id',
nameField: 'title',
descriptionField: 'description',
categoryField: 'type',
fields: [
{ key: 'id', label: 'Massnahmen-ID', type: 'text', required: true },
{ key: 'title', label: 'Titel', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'type', label: 'Typ', type: 'select', required: true, options: [
{ value: 'technical', label: 'Technisch' },
{ value: 'organizational', label: 'Organisatorisch' },
{ value: 'legal', label: 'Rechtlich' },
]},
{ key: 'effectiveness', label: 'Wirksamkeit', type: 'select', required: false, options: [
{ value: 'low', label: 'Niedrig' },
{ value: 'medium', label: 'Mittel' },
{ value: 'high', label: 'Hoch' },
]},
],
searchableFields: ['id', 'title', 'description', 'type'],
},
'prohibited-ai-practices': {
id: 'prohibited-ai-practices',
name: 'Verbotene KI-Praktiken',
description: 'Absolut und bedingt verbotene KI-Anwendungen nach AI Act',
module: 'ai_act',
icon: 'Ban',
systemCount: PROHIBITED_AI_PRACTICES.length,
allowCustom: false,
idField: 'id',
nameField: 'title',
descriptionField: 'description',
categoryField: 'severity',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'title', label: 'Titel', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'severity', label: 'Schwere', type: 'select', required: true, options: [
{ value: 'absolute', label: 'Absolutes Verbot' },
{ value: 'conditional', label: 'Bedingtes Verbot' },
]},
{ key: 'legalBasis', label: 'Rechtsgrundlage', type: 'text', required: false },
],
searchableFields: ['id', 'title', 'description', 'severity', 'legalBasis'],
},
'vvt-templates': {
id: 'vvt-templates',
name: 'VVT Baseline-Vorlagen',
description: 'Vorlagen fuer Verarbeitungstaetigkeiten',
module: 'vvt',
icon: 'FileText',
systemCount: VVT_BASELINE_CATALOG.length,
allowCustom: true,
idField: 'templateId',
nameField: 'name',
descriptionField: 'description',
categoryField: 'businessFunction',
fields: [
{ key: 'templateId', label: 'Vorlagen-ID', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'businessFunction', label: 'Geschaeftsbereich', type: 'select', required: true, options: [
{ value: 'hr', label: 'Personal' },
{ value: 'finance', label: 'Finanzen' },
{ value: 'sales', label: 'Vertrieb' },
{ value: 'marketing', label: 'Marketing' },
{ value: 'support', label: 'Support' },
{ value: 'it', label: 'IT' },
{ value: 'other', label: 'Sonstige' },
]},
{ key: 'protectionLevel', label: 'Schutzniveau', type: 'select', required: false, options: [
{ value: 'LOW', label: 'Niedrig' },
{ value: 'MEDIUM', label: 'Mittel' },
{ value: 'HIGH', label: 'Hoch' },
]},
],
searchableFields: ['templateId', 'name', 'description', 'businessFunction'],
},
'loeschfristen-templates': {
id: 'loeschfristen-templates',
name: 'Loeschfristen-Vorlagen',
description: 'Baseline-Vorlagen fuer Aufbewahrungsfristen',
module: 'vvt',
icon: 'Clock',
systemCount: BASELINE_TEMPLATES.length,
allowCustom: true,
idField: 'templateId',
nameField: 'dataObjectName',
descriptionField: 'description',
categoryField: 'retentionDriver',
fields: [
{ key: 'templateId', label: 'Vorlagen-ID', type: 'text', required: true },
{ key: 'dataObjectName', label: 'Datenobjekt', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'retentionDuration', label: 'Aufbewahrungsdauer', type: 'number', required: true, min: 0 },
{ key: 'retentionUnit', label: 'Einheit', type: 'select', required: true, options: [
{ value: 'days', label: 'Tage' },
{ value: 'months', label: 'Monate' },
{ value: 'years', label: 'Jahre' },
]},
{ key: 'deletionMethod', label: 'Loeschmethode', type: 'text', required: false },
{ key: 'responsibleRole', label: 'Verantwortlich', type: 'text', required: false },
],
searchableFields: ['templateId', 'dataObjectName', 'description', 'retentionDriver'],
},
'vendor-templates': {
id: 'vendor-templates',
name: 'AV-Vorlagen',
description: 'Vorlagen fuer Auftragsverarbeitungsvertraege',
module: 'vendor',
icon: 'Building2',
systemCount: VENDOR_TEMPLATES.length,
allowCustom: true,
idField: 'id',
nameField: 'name',
descriptionField: 'description',
categoryField: 'serviceCategory',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'serviceCategory', label: 'Kategorie', type: 'text', required: true },
],
searchableFields: ['id', 'name', 'description', 'serviceCategory'],
},
'country-risk-profiles': {
id: 'country-risk-profiles',
name: 'Laenderrisikoprofile',
description: 'Datenschutz-Risikobewertung nach Laendern',
module: 'vendor',
icon: 'Globe',
systemCount: COUNTRY_RISK_PROFILES.length,
allowCustom: false,
idField: 'code',
nameField: 'name',
categoryField: 'riskLevel',
fields: [
{ key: 'code', label: 'Laendercode', type: 'text', required: true },
{ key: 'name', label: 'Land', type: 'text', required: true },
{ key: 'riskLevel', label: 'Risikostufe', type: 'select', required: true, options: [
{ value: 'LOW', label: 'Niedrig' },
{ value: 'MEDIUM', label: 'Mittel' },
{ value: 'HIGH', label: 'Hoch' },
{ value: 'VERY_HIGH', label: 'Sehr hoch' },
]},
{ key: 'isEU', label: 'EU-Mitglied', type: 'boolean', required: false },
{ key: 'isEEA', label: 'EWR-Mitglied', type: 'boolean', required: false },
{ key: 'hasAdequacyDecision', label: 'Angemessenheitsbeschluss', type: 'boolean', required: false },
],
searchableFields: ['code', 'name', 'riskLevel'],
},
'legal-bases': {
id: 'legal-bases',
name: 'Rechtsgrundlagen',
description: 'DSGVO Art. 6 und Art. 9 Rechtsgrundlagen',
module: 'reference',
icon: 'Scale',
systemCount: LEGAL_BASIS_INFO.length,
allowCustom: false,
idField: 'type',
nameField: 'name',
descriptionField: 'description',
categoryField: 'article',
fields: [
{ key: 'type', label: 'Typ', type: 'text', required: true },
{ key: 'article', label: 'Artikel', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'isSpecialCategory', label: 'Besondere Kategorie (Art. 9)', type: 'boolean', required: false },
],
searchableFields: ['type', 'article', 'name', 'description'],
},
'retention-periods': {
id: 'retention-periods',
name: 'Aufbewahrungsfristen',
description: 'Gesetzliche Standard-Aufbewahrungsfristen',
module: 'reference',
icon: 'Timer',
systemCount: STANDARD_RETENTION_PERIODS.length,
allowCustom: false,
idField: 'id',
nameField: 'name',
descriptionField: 'description',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'legalBasis', label: 'Rechtsgrundlage', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
],
searchableFields: ['id', 'name', 'legalBasis', 'description'],
},
'eu-legal-frameworks': {
id: 'eu-legal-frameworks',
name: 'EU-Rechtsrahmen',
description: 'EU-weite Datenschutzgesetze und -verordnungen',
module: 'reference',
icon: 'Landmark',
systemCount: EU_BASE_FRAMEWORKS.length,
allowCustom: false,
idField: 'id',
nameField: 'name',
descriptionField: 'description',
categoryField: 'type',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'fullName', label: 'Vollstaendiger Name', type: 'text', required: true },
{ key: 'type', label: 'Typ', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
],
searchableFields: ['id', 'name', 'fullName', 'description', 'type'],
},
'national-legal-frameworks': {
id: 'national-legal-frameworks',
name: 'Nationale Rechtsrahmen',
description: 'Nationale Datenschutzgesetze der EU/EWR-Staaten',
module: 'reference',
icon: 'Flag',
systemCount: NATIONAL_FRAMEWORKS.length,
allowCustom: false,
idField: 'id',
nameField: 'name',
descriptionField: 'description',
categoryField: 'countryCode',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'countryCode', label: 'Land', type: 'text', required: true },
{ key: 'type', label: 'Typ', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
],
searchableFields: ['id', 'name', 'countryCode', 'description', 'type'],
},
'gdpr-enforcement-cases': {
id: 'gdpr-enforcement-cases',
name: 'DSGVO-Bussgeldentscheidungen',
description: 'Relevante Bussgeldentscheidungen als Referenz',
module: 'reference',
icon: 'Gavel',
systemCount: GDPR_ENFORCEMENT_CASES.length,
allowCustom: false,
idField: 'id',
nameField: 'company',
descriptionField: 'description',
categoryField: 'country',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'company', label: 'Unternehmen', type: 'text', required: true },
{ key: 'country', label: 'Land', type: 'text', required: true },
{ key: 'year', label: 'Jahr', type: 'number', required: true },
{ key: 'fineOriginal', label: 'Bussgeld (EUR)', type: 'number', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
],
searchableFields: ['id', 'company', 'country', 'description'],
},
'wp248-criteria': {
id: 'wp248-criteria',
name: 'WP248 Kriterien',
description: 'Kriterien zur DSFA-Pflichtpruefung nach WP248',
module: 'dsfa',
icon: 'ClipboardCheck',
systemCount: WP248_CRITERIA.length,
allowCustom: false,
idField: 'code',
nameField: 'title',
descriptionField: 'description',
fields: [
{ key: 'code', label: 'Code', type: 'text', required: true },
{ key: 'title', label: 'Titel', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'gdprRef', label: 'DSGVO-Referenz', type: 'text', required: false },
],
searchableFields: ['code', 'title', 'description', 'gdprRef'],
},
'sdm-goals': {
id: 'sdm-goals',
name: 'SDM Gewaehrleistungsziele',
description: 'Standard-Datenschutzmodell Gewaehrleistungsziele',
module: 'dsfa',
icon: 'Target',
systemCount: SDM_GOALS_ENTRIES.length,
allowCustom: false,
idField: 'id',
nameField: 'name',
descriptionField: 'description',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'name', label: 'Name', type: 'text', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea', required: true },
{ key: 'article', label: 'DSGVO-Artikel', type: 'text', required: false },
],
searchableFields: ['id', 'name', 'description', 'article'],
},
'dsfa-authority-resources': {
id: 'dsfa-authority-resources',
name: 'Aufsichtsbehoerden-Ressourcen',
description: 'DSFA-Ressourcen der deutschen Aufsichtsbehoerden',
module: 'dsfa',
icon: 'Building',
systemCount: DSFA_AUTHORITY_RESOURCES.length,
allowCustom: false,
idField: 'id',
nameField: 'shortName',
descriptionField: 'name',
categoryField: 'state',
fields: [
{ key: 'id', label: 'ID', type: 'text', required: true },
{ key: 'shortName', label: 'Kurzname', type: 'text', required: true },
{ key: 'name', label: 'Voller Name', type: 'text', required: true },
{ key: 'state', label: 'Bundesland', type: 'text', required: true },
],
searchableFields: ['id', 'shortName', 'name', 'state'],
},
}
// =============================================================================
// SYSTEM ENTRIES MAP
// =============================================================================
const SYSTEM_ENTRIES_MAP: Record<CatalogId, Record<string, unknown>[]> = {
'dsfa-risks': RISK_CATALOG as unknown as Record<string, unknown>[],
'dsfa-mitigations': MITIGATION_LIBRARY as unknown as Record<string, unknown>[],
'ai-risks': AI_RISK_CATALOG as unknown as Record<string, unknown>[],
'ai-mitigations': AI_MITIGATION_LIBRARY as unknown as Record<string, unknown>[],
'prohibited-ai-practices': PROHIBITED_AI_PRACTICES as unknown as Record<string, unknown>[],
'vvt-templates': VVT_BASELINE_CATALOG as unknown as Record<string, unknown>[],
'loeschfristen-templates': BASELINE_TEMPLATES as unknown as Record<string, unknown>[],
'vendor-templates': VENDOR_TEMPLATES as unknown as Record<string, unknown>[],
'country-risk-profiles': COUNTRY_RISK_PROFILES as unknown as Record<string, unknown>[],
'legal-bases': LEGAL_BASIS_INFO as unknown as Record<string, unknown>[],
'retention-periods': STANDARD_RETENTION_PERIODS as unknown as Record<string, unknown>[],
'eu-legal-frameworks': EU_BASE_FRAMEWORKS as unknown as Record<string, unknown>[],
'national-legal-frameworks': NATIONAL_FRAMEWORKS as unknown as Record<string, unknown>[],
'gdpr-enforcement-cases': GDPR_ENFORCEMENT_CASES as unknown as Record<string, unknown>[],
'wp248-criteria': WP248_CRITERIA as unknown as Record<string, unknown>[],
'sdm-goals': SDM_GOALS_ENTRIES as unknown as Record<string, unknown>[],
'dsfa-authority-resources': DSFA_AUTHORITY_RESOURCES as unknown as Record<string, unknown>[],
}
// =============================================================================
// ENTRY CONVERTERS
// =============================================================================
function systemEntryToCatalogEntry(
catalogId: CatalogId,
data: Record<string, unknown>,
): CatalogEntry {
const meta = CATALOG_REGISTRY[catalogId]
const idValue = resolveField(data[meta.idField])
const nameValue = resolveField(data[meta.nameField])
const descValue = meta.descriptionField ? resolveField(data[meta.descriptionField]) : undefined
const catValue = meta.categoryField ? resolveField(data[meta.categoryField]) : undefined
return {
id: idValue || crypto.randomUUID(),
catalogId,
source: 'system',
data,
displayName: nameValue || idValue || '(Unbenannt)',
displayDescription: descValue,
category: catValue,
}
}
function customEntryToCatalogEntry(
catalogId: CatalogId,
entry: CustomCatalogEntry,
): CatalogEntry {
const meta = CATALOG_REGISTRY[catalogId]
const nameValue = resolveField(entry.data[meta.nameField])
const descValue = meta.descriptionField ? resolveField(entry.data[meta.descriptionField]) : undefined
const catValue = meta.categoryField ? resolveField(entry.data[meta.categoryField]) : undefined
return {
id: entry.id,
catalogId,
source: 'custom',
data: entry.data,
displayName: nameValue || '(Unbenannt)',
displayDescription: descValue,
category: catValue,
}
}
// =============================================================================
// PUBLIC API
// =============================================================================
export function getSystemEntries(catalogId: CatalogId): CatalogEntry[] {
const raw = SYSTEM_ENTRIES_MAP[catalogId] || []
return raw.map(data => systemEntryToCatalogEntry(catalogId, data))
}
export function getAllEntries(
catalogId: CatalogId,
customEntries: CustomCatalogEntry[] = [],
): CatalogEntry[] {
const system = getSystemEntries(catalogId)
const custom = customEntries.map(e => customEntryToCatalogEntry(catalogId, e))
return [...system, ...custom]
}
export function getCatalogsByModule(module: CatalogModule): CatalogMeta[] {
return Object.values(CATALOG_REGISTRY).filter(c => c.module === module)
}
export function getCatalogStats(
catalogId: CatalogId,
customEntries: CustomCatalogEntry[] = [],
): CatalogStats {
const meta = CATALOG_REGISTRY[catalogId]
return {
catalogId,
systemCount: meta.systemCount,
customCount: customEntries.length,
totalCount: meta.systemCount + customEntries.length,
}
}
export function getOverviewStats(customCatalogs: CustomCatalogs): CatalogOverviewStats {
const modules: CatalogModule[] = ['dsfa', 'vvt', 'vendor', 'ai_act', 'reference']
const byModule = {} as Record<CatalogModule, { catalogs: number; entries: number }>
let totalSystemEntries = 0
let totalCustomEntries = 0
for (const mod of modules) {
const cats = getCatalogsByModule(mod)
let entries = 0
for (const cat of cats) {
const customCount = customCatalogs[cat.id]?.length ?? 0
entries += cat.systemCount + customCount
totalSystemEntries += cat.systemCount
totalCustomEntries += customCount
}
byModule[mod] = { catalogs: cats.length, entries }
}
return {
totalCatalogs: Object.keys(CATALOG_REGISTRY).length,
totalSystemEntries,
totalCustomEntries,
totalEntries: totalSystemEntries + totalCustomEntries,
byModule,
}
}
export function searchCatalog(
catalogId: CatalogId,
query: string,
customEntries: CustomCatalogEntry[] = [],
): CatalogEntry[] {
const allEntries = getAllEntries(catalogId, customEntries)
const meta = CATALOG_REGISTRY[catalogId]
const q = query.toLowerCase().trim()
if (!q) return allEntries
return allEntries
.map(entry => {
let score = 0
for (const field of meta.searchableFields) {
const value = resolveField(entry.data[field]).toLowerCase()
if (value.includes(q)) {
score += value.startsWith(q) ? 10 : 5
}
}
// Also search display name
if (entry.displayName.toLowerCase().includes(q)) {
score += 15
}
return { entry, score }
})
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score)
.map(r => r.entry)
}
@@ -0,0 +1,118 @@
/**
* SDK Catalog Manager - Type Definitions
*/
// All catalog IDs in the system
export type CatalogId =
| 'dsfa-risks'
| 'dsfa-mitigations'
| 'ai-risks'
| 'ai-mitigations'
| 'prohibited-ai-practices'
| 'vvt-templates'
| 'loeschfristen-templates'
| 'vendor-templates'
| 'country-risk-profiles'
| 'legal-bases'
| 'retention-periods'
| 'eu-legal-frameworks'
| 'national-legal-frameworks'
| 'gdpr-enforcement-cases'
| 'wp248-criteria'
| 'sdm-goals'
| 'dsfa-authority-resources'
// Module grouping
export type CatalogModule = 'dsfa' | 'vvt' | 'vendor' | 'ai_act' | 'reference'
// Field types for dynamic forms
export type CatalogFieldType = 'text' | 'textarea' | 'number' | 'select' | 'multiselect' | 'boolean' | 'tags'
export interface CatalogFieldSchema {
key: string
label: string
type: CatalogFieldType
required: boolean
placeholder?: string
description?: string
options?: { value: string; label: string }[]
helpText?: string
min?: number
max?: number
step?: number
}
export interface CatalogMeta {
id: CatalogId
name: string
description: string
module: CatalogModule
icon: string // lucide icon name
systemCount: number
allowCustom: boolean
idField: string // which field is the unique ID (e.g. 'id', 'templateId', 'code')
nameField: string // which field is the display name (e.g. 'title', 'name', 'dataObjectName')
descriptionField?: string // which field holds description
categoryField?: string // optional grouping field
fields: CatalogFieldSchema[]
searchableFields: string[]
}
// A custom catalog entry added by the user
export interface CustomCatalogEntry {
id: string // Generated UUID
catalogId: CatalogId
data: Record<string, unknown>
createdAt: string // ISO date
updatedAt: string // ISO date
createdBy?: string
}
// All custom entries, keyed by CatalogId
export type CustomCatalogs = Partial<Record<CatalogId, CustomCatalogEntry[]>>
// Combined view entry (system or custom)
export interface CatalogEntry {
id: string
catalogId: CatalogId
source: 'system' | 'custom'
data: Record<string, unknown>
displayName: string
displayDescription?: string
category?: string
}
// Stats for a single catalog
export interface CatalogStats {
catalogId: CatalogId
systemCount: number
customCount: number
totalCount: number
}
// Stats for all catalogs
export interface CatalogOverviewStats {
totalCatalogs: number
totalSystemEntries: number
totalCustomEntries: number
totalEntries: number
byModule: Record<CatalogModule, { catalogs: number; entries: number }>
}
// Module labels
export const CATALOG_MODULE_LABELS: Record<CatalogModule, string> = {
dsfa: 'DSFA & Risiken',
vvt: 'VVT & Loeschfristen',
vendor: 'Auftragsverarbeitung',
ai_act: 'AI Act',
reference: 'Referenzdaten',
}
// Module icons (lucide names)
export const CATALOG_MODULE_ICONS: Record<CatalogModule, string> = {
dsfa: 'ShieldAlert',
vvt: 'FileText',
vendor: 'Building2',
ai_act: 'Bot',
reference: 'BookOpen',
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,722 @@
import type { ScopeProfilingAnswer, ComplianceDepthLevel, ScopeDocumentType } from './compliance-scope-types'
export interface GoldenTest {
id: string
name: string
description: string
answers: ScopeProfilingAnswer[]
expectedLevel: ComplianceDepthLevel | null // null for prefill tests
expectedMinDocuments?: ScopeDocumentType[]
expectedHardTriggerIds?: string[]
expectedDsfaRequired?: boolean
tags: string[]
}
export const GOLDEN_TESTS: GoldenTest[] = [
// GT-01: 2-Person Freelancer, nur B2B, DE-Hosting → L1
{
id: 'GT-01',
name: '2-Person Freelancer B2B',
description: 'Kleinstes Setup ohne besondere Risiken',
answers: [
{ questionId: 'org_employee_count', value: '2' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'consulting' },
{ questionId: 'data_health', value: false },
{ questionId: 'data_genetic', value: false },
{ questionId: 'data_biometric', value: false },
{ questionId: 'data_racial_ethnic', value: false },
{ questionId: 'data_political_opinion', value: false },
{ questionId: 'data_religious', value: false },
{ questionId: 'data_union_membership', value: false },
{ questionId: 'data_sexual_orientation', value: false },
{ questionId: 'data_criminal', value: false },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
{ questionId: 'process_has_incident_plan', value: true },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<100' },
],
expectedLevel: 'L1',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
expectedHardTriggerIds: [],
expectedDsfaRequired: false,
tags: ['baseline', 'freelancer', 'b2b'],
},
// GT-02: Solo IT-Berater → L1
{
id: 'GT-02',
name: 'Solo IT-Berater',
description: 'Einzelperson, minimale Datenverarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'it_services' },
{ questionId: 'data_health', value: false },
{ questionId: 'data_genetic', value: false },
{ questionId: 'data_biometric', value: false },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['baseline', 'solo', 'minimal'],
},
// GT-03: 5-Person Agentur, Website, kein Tracking → L1
{
id: 'GT-03',
name: '5-Person Agentur ohne Tracking',
description: 'Kleine Agentur, einfache Website ohne Analytics',
answers: [
{ questionId: 'org_employee_count', value: '5' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'marketing' },
{ questionId: 'tech_has_website', value: true },
{ questionId: 'tech_has_tracking', value: false },
{ questionId: 'data_volume', value: '1000-10000' },
{ questionId: 'org_customer_count', value: '100-1000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L1',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
tags: ['baseline', 'agency', 'simple'],
},
// GT-04: 30-Person SaaS B2B, EU-Cloud → L2 (scale trigger)
{
id: 'GT-04',
name: '30-Person SaaS B2B',
description: 'Scale-Trigger durch Mitarbeiterzahl',
answers: [
{ questionId: 'org_employee_count', value: '30' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'software' },
{ questionId: 'tech_has_cloud', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: false },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER'],
tags: ['scale', 'saas', 'growth'],
},
// GT-05: 50-Person Handel B2C, Webshop → L2 (B2C+Webshop)
{
id: 'GT-05',
name: '50-Person E-Commerce B2C',
description: 'B2C mit Webshop erhöht Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '50' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_has_webshop', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'org_customer_count', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L2',
expectedHardTriggerIds: ['HT-H01'],
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER', 'EINWILLIGUNG'],
tags: ['b2c', 'webshop', 'retail'],
},
// GT-06: 80-Person Dienstleister, Cloud → L2 (scale)
{
id: 'GT-06',
name: '80-Person Dienstleister',
description: 'Größerer Betrieb mit Cloud-Services',
answers: [
{ questionId: 'org_employee_count', value: '80' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'professional_services' },
{ questionId: 'tech_has_cloud', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'AVV'],
tags: ['scale', 'services'],
},
// GT-07: 20-Person Startup mit GA4 Tracking → L2 (tracking)
{
id: 'GT-07',
name: 'Startup mit Google Analytics',
description: 'Tracking-Tools erhöhen Compliance-Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '20' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'technology' },
{ questionId: 'tech_has_website', value: true },
{ questionId: 'tech_has_tracking', value: true },
{ questionId: 'tech_tracking_tools', value: 'google_analytics' },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNG'],
tags: ['tracking', 'analytics', 'startup'],
},
// GT-08: Kita-App (Minderjaehrige) → L3 (HT-B01)
{
id: 'GT-08',
name: 'Kita-App für Eltern',
description: 'Datenverarbeitung von Minderjährigen unter 16',
answers: [
{ questionId: 'org_employee_count', value: '15' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'data_volume', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-B01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNG', 'AVV'],
tags: ['hard-trigger', 'minors', 'education'],
},
// GT-09: Krankenhaus-Software → L3 (HT-A01)
{
id: 'GT-09',
name: 'Krankenhaus-Verwaltungssoftware',
description: 'Gesundheitsdaten Art. 9 DSGVO',
answers: [
{ questionId: 'org_employee_count', value: '200' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10-50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'health', 'art9'],
},
// GT-10: HR-Scoring-Plattform → L3 (HT-C01)
{
id: 'GT-10',
name: 'HR-Scoring für Bewerbungen',
description: 'Automatisierte Entscheidungen im HR-Bereich',
answers: [
{ questionId: 'org_employee_count', value: '40' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'hr_tech' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'profiling' },
{ questionId: 'tech_adm_impact', value: 'employment' },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-C01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'adm', 'profiling'],
},
// GT-11: Fintech Kreditscoring → L3 (HT-H05 + C01)
{
id: 'GT-11',
name: 'Fintech Kreditscoring',
description: 'Finanzsektor mit automatisierten Entscheidungen',
answers: [
{ questionId: 'org_employee_count', value: '120' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'finance' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'scoring' },
{ questionId: 'tech_adm_impact', value: 'credit' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H05', 'HT-C01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'finance', 'scoring'],
},
// GT-12: Bildungsplattform Minderjaehrige → L3 (HT-B01)
{
id: 'GT-12',
name: 'Online-Lernplattform für Schüler',
description: 'Bildungssektor mit minderjährigen Nutzern',
answers: [
{ questionId: 'org_employee_count', value: '35' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'tech_has_tracking', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-B01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'education', 'minors'],
},
// GT-13: Datenbroker → L3 (HT-H02)
{
id: 'GT-13',
name: 'Datenbroker / Adresshandel',
description: 'Geschäftsmodell basiert auf Datenhandel',
answers: [
{ questionId: 'org_employee_count', value: '25' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'data_broker' },
{ questionId: 'data_is_core_business', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '100-1000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'data-broker'],
},
// GT-14: Video + ADM → L3 (HT-D05)
{
id: 'GT-14',
name: 'Videoüberwachung mit Gesichtserkennung',
description: 'Biometrische Daten mit automatisierter Verarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '60' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'security' },
{ questionId: 'data_biometric', value: true },
{ questionId: 'tech_has_video_surveillance', value: true },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-D05'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'biometric', 'video'],
},
// GT-15: 500-MA Konzern ohne Zert → L3 (HT-G04)
{
id: 'GT-15',
name: 'Großunternehmen ohne Zertifizierung',
description: 'Scale-Trigger durch Unternehmensgröße',
answers: [
{ questionId: 'org_employee_count', value: '500' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'manufacturing' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '>100000' },
{ questionId: 'cert_has_iso27001', value: false },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-G04'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'scale', 'enterprise'],
},
// GT-16: ISO 27001 Anbieter → L4 (HT-F01)
{
id: 'GT-16',
name: 'ISO 27001 zertifizierter Cloud-Provider',
description: 'Zertifizierung erfordert höchste Compliance',
answers: [
{ questionId: 'org_employee_count', value: '150' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27001', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F01'],
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV', 'CERT_ISO27001'],
tags: ['hard-trigger', 'certification', 'iso'],
},
// GT-17: TISAX Automobilzulieferer → L4 (HT-F04)
{
id: 'GT-17',
name: 'TISAX-zertifizierter Automobilzulieferer',
description: 'Automotive-Branche mit TISAX-Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '300' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'automotive' },
{ questionId: 'cert_has_tisax', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10-50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F04'],
tags: ['hard-trigger', 'certification', 'tisax'],
},
// GT-18: ISO 27701 Cloud-Provider → L4 (HT-F02)
{
id: 'GT-18',
name: 'ISO 27701 Privacy-zertifiziert',
description: 'Privacy-spezifische Zertifizierung',
answers: [
{ questionId: 'org_employee_count', value: '200' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27701', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F02'],
tags: ['hard-trigger', 'certification', 'privacy'],
},
// GT-19: Grosskonzern + Art.9 + >1M DS → L4 (HT-G05)
{
id: 'GT-19',
name: 'Konzern mit sensiblen Massendaten',
description: 'Kombination aus Scale und Art. 9 Daten',
answers: [
{ questionId: 'org_employee_count', value: '2000' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'insurance' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '>100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-G05'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'scale', 'art9'],
},
// GT-20: Nur B2C Webshop → L2 (HT-H01)
{
id: 'GT-20',
name: 'Reiner B2C Webshop',
description: 'B2C-Trigger ohne weitere Risiken',
answers: [
{ questionId: 'org_employee_count', value: '12' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_has_webshop', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L2',
expectedHardTriggerIds: ['HT-H01'],
tags: ['b2c', 'webshop'],
},
// GT-21: Keine Daten, keine MA → L1
{
id: 'GT-21',
name: 'Minimale Datenverarbeitung',
description: 'Absolute Baseline ohne Risiken',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'consulting' },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'tech_has_website', value: false },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['baseline', 'minimal'],
},
// GT-22: Alle Art.9 Kategorien → L3 (HT-A09)
{
id: 'GT-22',
name: 'Alle Art. 9 Kategorien',
description: 'Multiple sensible Datenkategorien',
answers: [
{ questionId: 'org_employee_count', value: '50' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'research' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_genetic', value: true },
{ questionId: 'data_biometric', value: true },
{ questionId: 'data_racial_ethnic', value: true },
{ questionId: 'data_political_opinion', value: true },
{ questionId: 'data_religious', value: true },
{ questionId: 'data_union_membership', value: true },
{ questionId: 'data_sexual_orientation', value: true },
{ questionId: 'data_criminal', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A09'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'art9', 'multiple-categories'],
},
// GT-23: Drittland + Art.9 → L3 (HT-E04)
{
id: 'GT-23',
name: 'Drittlandtransfer mit Art. 9 Daten',
description: 'Kombination aus Drittland und sensiblen Daten',
answers: [
{ questionId: 'org_employee_count', value: '45' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'us' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'tech_has_third_country_transfer', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-E04'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'third-country', 'art9'],
},
// GT-24: Minderjaehrige + Art.9 → L4 (HT-B02)
{
id: 'GT-24',
name: 'Minderjährige mit Gesundheitsdaten',
description: 'Kombination aus vulnerabler Gruppe und Art. 9',
answers: [
{ questionId: 'org_employee_count', value: '30' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-B02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'minors', 'health', 'combined-risk'],
},
// GT-25: KI autonome Entscheidungen → L3 (HT-C02)
{
id: 'GT-25',
name: 'KI mit autonomen Entscheidungen',
description: 'AI Act relevante autonome Systeme',
answers: [
{ questionId: 'org_employee_count', value: '70' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'ai_services' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'autonomous_decision' },
{ questionId: 'tech_has_ai', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-C02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'ai', 'adm'],
},
// GT-26: Multiple Zertifizierungen → L4 (HT-F01-05)
{
id: 'GT-26',
name: 'Multiple Zertifizierungen',
description: 'Mehrere Zertifizierungen kombiniert',
answers: [
{ questionId: 'org_employee_count', value: '250' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27001', value: true },
{ questionId: 'cert_has_iso27701', value: true },
{ questionId: 'cert_has_soc2', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F01', 'HT-F02', 'HT-F03'],
tags: ['hard-trigger', 'certification', 'multiple'],
},
// GT-27: Oeffentlicher Sektor + Gesundheit → L3 (HT-H07 + A01)
{
id: 'GT-27',
name: 'Öffentlicher Sektor mit Gesundheitsdaten',
description: 'Behörde mit Art. 9 Datenverarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '120' },
{ questionId: 'org_business_model', value: 'b2g' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'public_sector' },
{ questionId: 'org_is_public_sector', value: true },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H07', 'HT-A01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'public-sector', 'health'],
},
// GT-28: Bildung + KI + Minderjaehrige → L4 (HT-B03)
{
id: 'GT-28',
name: 'EdTech mit KI für Minderjährige',
description: 'Triple-Risiko: Bildung, KI, vulnerable Gruppe',
answers: [
{ questionId: 'org_employee_count', value: '55' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'tech_has_ai', value: true },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-B03'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'education', 'ai', 'minors', 'triple-risk'],
},
// GT-29: Freelancer mit 1 Art.9 → L3 (hard trigger override despite low score)
{
id: 'GT-29',
name: 'Freelancer mit Gesundheitsdaten',
description: 'Hard Trigger überschreibt niedrige Score-Bewertung',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'override', 'art9', 'freelancer'],
},
// GT-30: Enterprise, alle Prozesse vorhanden → L3 (good process maturity)
{
id: 'GT-30',
name: 'Enterprise mit reifer Prozesslandschaft',
description: 'Große Organisation mit allen Compliance-Prozessen',
answers: [
{ questionId: 'org_employee_count', value: '450' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'manufacturing' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
{ questionId: 'process_has_incident_plan', value: true },
{ questionId: 'process_has_dsb', value: true },
{ questionId: 'process_has_training', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-G04'],
tags: ['enterprise', 'mature', 'all-processes'],
},
// GT-31: SMB, nur 1 Block beantwortet → L1 (graceful degradation)
{
id: 'GT-31',
name: 'Unvollständige Profilerstellung',
description: 'Test für graceful degradation bei unvollständigen Antworten',
answers: [
{ questionId: 'org_employee_count', value: '8' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'org_industry', value: 'consulting' },
// Nur Block 1 (Organization) beantwortet, Rest fehlt
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['incomplete', 'degradation', 'edge-case'],
},
// GT-32: CompanyProfile Prefill Konsistenz → null (prefill test, no expected level)
{
id: 'GT-32',
name: 'CompanyProfile Prefill Test',
description: 'Prüft ob CompanyProfile-Daten korrekt in ScopeProfile übernommen werden',
answers: [
{ questionId: 'org_employee_count', value: '25' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_hosting_location', value: 'eu' },
// Diese Werte sollten mit CompanyProfile-Prefill übereinstimmen
],
expectedLevel: null,
tags: ['prefill', 'integration', 'consistency'],
},
]
@@ -0,0 +1,821 @@
import type {
ScopeQuestionBlock,
ScopeQuestionBlockId,
ScopeProfilingQuestion,
ScopeProfilingAnswer,
ComplianceScopeState,
} from './compliance-scope-types'
import type { CompanyProfile } from './types'
/**
* Block 1: Organisation & Reife
*/
const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
id: 'organisation',
title: 'Organisation & Reife',
description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen',
order: 1,
questions: [
{
id: 'org_employee_count',
type: 'number',
question: 'Wie viele Mitarbeiter hat Ihre Organisation?',
helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)',
required: true,
scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
mapsToCompanyProfile: 'employeeCount',
},
{
id: 'org_customer_count',
type: 'single',
question: 'Wie viele Kunden/Nutzer betreuen Sie?',
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
required: true,
options: [
{ value: '<100', label: 'Weniger als 100' },
{ value: '100-1000', label: '100 bis 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000+', label: 'Mehr als 100.000' },
],
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
},
{
id: 'org_annual_revenue',
type: 'single',
question: 'Wie hoch ist Ihr jährlicher Umsatz?',
helpText: 'Wählen Sie die zutreffende Umsatzklasse',
required: true,
options: [
{ value: '<2Mio', label: 'Unter 2 Mio. EUR' },
{ value: '2-10Mio', label: '2 bis 10 Mio. EUR' },
{ value: '10-50Mio', label: '10 bis 50 Mio. EUR' },
{ value: '>50Mio', label: 'Über 50 Mio. EUR' },
],
scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
mapsToCompanyProfile: 'annualRevenue',
},
{
id: 'org_cert_target',
type: 'multi',
question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf',
required: false,
options: [
{ value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
{ value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' },
{ value: 'TISAX', label: 'TISAX (Automotive)' },
{ value: 'SOC2', label: 'SOC 2 (US-Standard)' },
{ value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' },
{ value: 'Keine', label: 'Keine Zertifizierung geplant' },
],
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
},
{
id: 'org_industry',
type: 'single',
question: 'In welcher Branche sind Sie tätig?',
helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen',
required: true,
options: [
{ value: 'it_software', label: 'IT & Software' },
{ value: 'healthcare', label: 'Gesundheitswesen' },
{ value: 'education', label: 'Bildung & Forschung' },
{ value: 'finance', label: 'Finanzdienstleistungen' },
{ value: 'retail', label: 'Einzelhandel & E-Commerce' },
{ value: 'manufacturing', label: 'Produktion & Fertigung' },
{ value: 'consulting', label: 'Beratung & Dienstleistungen' },
{ value: 'public', label: 'Öffentliche Verwaltung' },
{ value: 'other', label: 'Sonstige' },
],
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
mapsToCompanyProfile: 'industry',
mapsToVVTQuestion: 'org_industry',
mapsToLFQuestion: 'org-branche',
},
{
id: 'org_business_model',
type: 'single',
question: 'Was ist Ihr primäres Geschäftsmodell?',
helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen',
required: true,
options: [
{ value: 'b2b', label: 'B2B (Business-to-Business)' },
{ value: 'b2c', label: 'B2C (Business-to-Consumer)' },
{ value: 'both', label: 'B2B und B2C gemischt' },
{ value: 'b2g', label: 'B2G (Business-to-Government)' },
],
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
mapsToCompanyProfile: 'businessModel',
mapsToVVTQuestion: 'org_b2b_b2c',
mapsToLFQuestion: 'org-geschaeftsmodell',
},
{
id: 'org_has_dsb',
type: 'boolean',
question: 'Haben Sie einen Datenschutzbeauftragten bestellt?',
helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht',
required: true,
scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
},
],
}
/**
* Block 2: Daten & Betroffene
*/
const BLOCK_2_DATA: ScopeQuestionBlock = {
id: 'data',
title: 'Daten & Betroffene',
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
order: 2,
questions: [
{
id: 'data_minors',
type: 'boolean',
question: 'Verarbeiten Sie Daten von Minderjährigen?',
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
required: true,
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
mapsToVVTQuestion: 'data_minors',
},
{
id: 'data_art9',
type: 'multi',
question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
required: true,
options: [
{ value: 'gesundheit', label: 'Gesundheitsdaten' },
{ value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
{ value: 'genetik', label: 'Genetische Daten' },
{ value: 'politisch', label: 'Politische Meinungen' },
{ value: 'religion', label: 'Religiöse/weltanschauliche Überzeugungen' },
{ value: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit' },
{ value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
{ value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
{ value: 'ethnisch', label: 'Ethnische Herkunft' },
],
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
mapsToVVTQuestion: 'data_health',
},
{
id: 'data_hr',
type: 'boolean',
question: 'Verarbeiten Sie Personaldaten (HR)?',
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
required: true,
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
mapsToVVTQuestion: 'dept_hr',
mapsToLFQuestion: 'data-hr',
},
{
id: 'data_communication',
type: 'boolean',
question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
required: true,
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
},
{
id: 'data_financial',
type: 'boolean',
question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
mapsToVVTQuestion: 'dept_finance',
mapsToLFQuestion: 'data-buchhaltung',
},
{
id: 'data_volume',
type: 'single',
question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?',
helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen',
required: true,
options: [
{ value: '<1000', label: 'Unter 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000-1000000', label: '100.000 bis 1 Mio.' },
{ value: '>1000000', label: 'Über 1 Mio.' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
],
}
/**
* Block 3: Verarbeitung & Zweck
*/
const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
id: 'processing',
title: 'Verarbeitung & Zweck',
description: 'Wie und wofür werden personenbezogene Daten verarbeitet?',
order: 3,
questions: [
{
id: 'proc_tracking',
type: 'boolean',
question: 'Setzen Sie Tracking oder Profiling ein?',
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'proc_adm_scoring',
type: 'boolean',
question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
},
{
id: 'proc_ai_usage',
type: 'multi',
question: 'Setzen Sie KI-Systeme ein?',
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
required: true,
options: [
{ value: 'keine', label: 'Keine KI im Einsatz' },
{ value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
{ value: 'scoring', label: 'Scoring/Risikobewertung' },
{ value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
{ value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
{ value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
],
scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
},
{
id: 'proc_data_combination',
type: 'boolean',
question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
helpText: 'Data Matching, Anreicherung aus externen Quellen',
required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
},
{
id: 'proc_employee_monitoring',
type: 'boolean',
question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
},
{
id: 'proc_video_surveillance',
type: 'boolean',
question: 'Setzen Sie Videoüberwachung ein?',
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
required: true,
scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
mapsToVVTQuestion: 'special_video_surveillance',
mapsToLFQuestion: 'data-video',
},
],
}
/**
* Block 4: Technik/Hosting/Transfers
*/
const BLOCK_4_TECH: ScopeQuestionBlock = {
id: 'tech',
title: 'Technik, Hosting & Transfers',
description: 'Technische Infrastruktur und Datenübermittlung',
order: 4,
questions: [
{
id: 'tech_hosting_location',
type: 'single',
question: 'Wo werden Ihre Daten primär gehostet?',
helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
required: true,
options: [
{ value: 'de', label: 'Deutschland' },
{ value: 'eu', label: 'EU (ohne Deutschland)' },
{ value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
{ value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
{ value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
},
{
id: 'tech_subprocessors',
type: 'boolean',
question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. erfordert AVV nach Art. 28 DSGVO',
required: true,
scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
},
{
id: 'tech_third_country',
type: 'boolean',
question: 'Übermitteln Sie Daten in Drittländer?',
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
mapsToVVTQuestion: 'transfer_cloud_us',
},
{
id: 'tech_encryption_rest',
type: 'boolean',
question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
required: true,
scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
},
{
id: 'tech_encryption_transit',
type: 'boolean',
question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
helpText: 'TLS/SSL für alle Verbindungen',
required: true,
scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
},
{
id: 'tech_cloud_providers',
type: 'multi',
question: 'Welche Cloud-Anbieter nutzen Sie?',
helpText: 'Mehrfachauswahl möglich',
required: false,
options: [
{ value: 'aws', label: 'Amazon Web Services (AWS)' },
{ value: 'azure', label: 'Microsoft Azure' },
{ value: 'gcp', label: 'Google Cloud Platform (GCP)' },
{ value: 'hetzner', label: 'Hetzner' },
{ value: 'ionos', label: 'IONOS' },
{ value: 'ovh', label: 'OVH' },
{ value: 'andere', label: 'Andere Anbieter' },
{ value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
],
scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
},
],
}
/**
* Block 5: Rechte & Prozesse
*/
const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
id: 'processes',
title: 'Rechte & Prozesse',
description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
order: 5,
questions: [
{
id: 'proc_dsar_process',
type: 'boolean',
question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. Art. 15-22 DSGVO',
required: true,
scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
},
{
id: 'proc_deletion_concept',
type: 'boolean',
question: 'Haben Sie ein Löschkonzept?',
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
},
{
id: 'proc_incident_response',
type: 'boolean',
question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
},
{
id: 'proc_regular_audits',
type: 'boolean',
question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
helpText: 'Interne oder externe Prüfungen mindestens jährlich',
required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
},
{
id: 'proc_training',
type: 'boolean',
question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
required: true,
scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
},
],
}
/**
* Block 6: Produktkontext
*/
const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
id: 'product',
title: 'Produktkontext',
description: 'Spezifische Merkmale Ihrer Produkte und Services',
order: 6,
questions: [
{
id: 'prod_type',
type: 'multi',
question: 'Welche Art von Produkten/Services bieten Sie an?',
helpText: 'Mehrfachauswahl möglich',
required: true,
options: [
{ value: 'webapp', label: 'Web-Anwendung' },
{ value: 'mobile', label: 'Mobile App (iOS/Android)' },
{ value: 'saas', label: 'SaaS-Plattform' },
{ value: 'onpremise', label: 'On-Premise Software' },
{ value: 'api', label: 'API/Schnittstellen' },
{ value: 'iot', label: 'IoT/Hardware' },
{ value: 'beratung', label: 'Beratungsleistungen' },
{ value: 'handel', label: 'Handel/Vertrieb' },
],
scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
},
{
id: 'prod_cookies_consent',
type: 'boolean',
question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
},
{
id: 'prod_webshop',
type: 'boolean',
question: 'Betreiben Sie einen Online-Shop?',
helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'prod_api_external',
type: 'boolean',
question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
},
{
id: 'prod_data_broker',
type: 'boolean',
question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
required: true,
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
},
],
}
/**
* All question blocks in order
*/
export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
BLOCK_1_ORGANISATION,
BLOCK_2_DATA,
BLOCK_3_PROCESSING,
BLOCK_4_TECH,
BLOCK_5_PROCESSES,
BLOCK_6_PRODUCT,
]
/**
* Prefill scope answers from CompanyProfile
*/
export function prefillFromCompanyProfile(
profile: CompanyProfile
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// employeeCount
if (profile.employeeCount != null) {
answers.push({
questionId: 'org_employee_count',
value: profile.employeeCount,
})
}
// annualRevenue
if (profile.annualRevenue) {
answers.push({
questionId: 'org_annual_revenue',
value: profile.annualRevenue,
})
}
// industry
if (profile.industry) {
answers.push({
questionId: 'org_industry',
value: profile.industry,
})
}
// businessModel
if (profile.businessModel) {
answers.push({
questionId: 'org_business_model',
value: profile.businessModel,
})
}
// dpoName -> org_has_dsb
if (profile.dpoName && profile.dpoName.trim() !== '') {
answers.push({
questionId: 'org_has_dsb',
value: true,
})
}
// usesAI -> proc_ai_usage
if (profile.usesAI === true) {
// We don't know which specific AI type, so just mark as "generativ" as a default
answers.push({
questionId: 'proc_ai_usage',
value: ['generativ'],
})
} else if (profile.usesAI === false) {
answers.push({
questionId: 'proc_ai_usage',
value: ['keine'],
})
}
// offerings -> prod_type mapping
if (profile.offerings && profile.offerings.length > 0) {
const prodTypes: string[] = []
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
prodTypes.push('webapp')
}
if (
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
) {
prodTypes.push('mobile')
}
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
prodTypes.push('saas')
}
if (
offeringsLower.some(
(o) => o.includes('onpremise') || o.includes('on-premise')
)
) {
prodTypes.push('onpremise')
}
if (offeringsLower.some((o) => o.includes('api'))) {
prodTypes.push('api')
}
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
prodTypes.push('iot')
}
if (
offeringsLower.some(
(o) => o.includes('beratung') || o.includes('consulting')
)
) {
prodTypes.push('beratung')
}
if (
offeringsLower.some(
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
)
) {
prodTypes.push('handel')
}
if (prodTypes.length > 0) {
answers.push({
questionId: 'prod_type',
value: prodTypes,
})
}
}
return answers
}
/**
* Prefill scope answers from VVT profiling answers
*/
export function prefillFromVVTAnswers(
vvtAnswers: Record<string, unknown>
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// Build reverse mapping: VVT question -> Scope question
const reverseMap: Record<string, string> = {}
for (const block of SCOPE_QUESTION_BLOCKS) {
for (const q of block.questions) {
if (q.mapsToVVTQuestion) {
reverseMap[q.mapsToVVTQuestion] = q.id
}
}
}
// Map VVT answers to scope answers
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
const scopeQuestionId = reverseMap[vvtQuestionId]
if (scopeQuestionId) {
answers.push({
questionId: scopeQuestionId,
value: vvtValue,
})
}
}
return answers
}
/**
* Prefill scope answers from Loeschfristen profiling answers
*/
export function prefillFromLoeschfristenAnswers(
lfAnswers: Array<{ questionId: string; value: unknown }>
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// Build reverse mapping: LF question -> Scope question
const reverseMap: Record<string, string> = {}
for (const block of SCOPE_QUESTION_BLOCKS) {
for (const q of block.questions) {
if (q.mapsToLFQuestion) {
reverseMap[q.mapsToLFQuestion] = q.id
}
}
}
// Map LF answers to scope answers
for (const lfAnswer of lfAnswers) {
const scopeQuestionId = reverseMap[lfAnswer.questionId]
if (scopeQuestionId) {
answers.push({
questionId: scopeQuestionId,
value: lfAnswer.value,
})
}
}
return answers
}
/**
* Export scope answers in VVT format
*/
export function exportToVVTAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, unknown> {
const vvtAnswers: Record<string, unknown> = {}
for (const answer of scopeAnswers) {
// Find the question
let question: ScopeProfilingQuestion | undefined
for (const block of SCOPE_QUESTION_BLOCKS) {
question = block.questions.find((q) => q.id === answer.questionId)
if (question) break
}
if (question?.mapsToVVTQuestion) {
vvtAnswers[question.mapsToVVTQuestion] = answer.value
}
}
return vvtAnswers
}
/**
* Export scope answers in Loeschfristen format
*/
export function exportToLoeschfristenAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Array<{ questionId: string; value: unknown }> {
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
for (const answer of scopeAnswers) {
// Find the question
let question: ScopeProfilingQuestion | undefined
for (const block of SCOPE_QUESTION_BLOCKS) {
question = block.questions.find((q) => q.id === answer.questionId)
if (question) break
}
if (question?.mapsToLFQuestion) {
lfAnswers.push({
questionId: question.mapsToLFQuestion,
value: answer.value,
})
}
}
return lfAnswers
}
/**
* Export scope answers for TOM generator
*/
export function exportToTOMProfile(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, unknown> {
const tomProfile: Record<string, unknown> = {}
// Get answer values
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
// Map relevant scope answers to TOM profile fields
tomProfile.industry = getVal('org_industry')
tomProfile.employeeCount = getVal('org_employee_count')
tomProfile.hasDataMinors = getVal('data_minors')
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
? (getVal('data_art9') as string[]).length > 0
: false
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
? !(getVal('proc_ai_usage') as string[]).includes('keine')
: false
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
tomProfile.hasTraining = getVal('proc_training')
return tomProfile
}
/**
* Check if a block is complete (all required questions answered)
*/
export function isBlockComplete(
answers: ScopeProfilingAnswer[],
blockId: ScopeQuestionBlockId
): boolean {
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
if (!block) return false
const requiredQuestions = block.questions.filter((q) => q.required)
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
}
/**
* Get progress for a specific block (0-100)
*/
export function getBlockProgress(
answers: ScopeProfilingAnswer[],
blockId: ScopeQuestionBlockId
): number {
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
if (!block) return 0
const requiredQuestions = block.questions.filter((q) => q.required)
if (requiredQuestions.length === 0) return 100
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
const answeredCount = requiredQuestions.filter((q) =>
answeredQuestionIds.has(q.id)
).length
return Math.round((answeredCount / requiredQuestions.length) * 100)
}
/**
* Get total progress across all blocks (0-100)
*/
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
let totalRequired = 0
let totalAnswered = 0
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
for (const block of SCOPE_QUESTION_BLOCKS) {
const requiredQuestions = block.questions.filter((q) => q.required)
totalRequired += requiredQuestions.length
totalAnswered += requiredQuestions.filter((q) =>
answeredQuestionIds.has(q.id)
).length
}
if (totalRequired === 0) return 100
return Math.round((totalAnswered / totalRequired) * 100)
}
/**
* Get answer value for a specific question
*/
export function getAnswerValue(
answers: ScopeProfilingAnswer[],
questionId: string
): unknown {
const answer = answers.find((a) => a.questionId === questionId)
return answer?.value
}
/**
* Get all questions as a flat array
*/
export function getAllQuestions(): ScopeProfilingQuestion[] {
return SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions)
}
File diff suppressed because it is too large Load Diff
+121
View File
@@ -0,0 +1,121 @@
'use client'
import React, { createContext, useContext, useReducer, useMemo } from 'react'
import type { CustomCatalogEntry, CustomCatalogs, CatalogId } from './catalog-manager/types'
// =============================================================================
// SIMPLIFIED STATE (Admin-Lehrer only needs catalog-manager state)
// =============================================================================
export interface SDKState {
customCatalogs: CustomCatalogs
}
export type SDKAction =
| { type: 'ADD_CUSTOM_CATALOG_ENTRY'; payload: CustomCatalogEntry }
| { type: 'UPDATE_CUSTOM_CATALOG_ENTRY'; payload: { catalogId: CatalogId; entryId: string; data: Record<string, unknown> } }
| { type: 'DELETE_CUSTOM_CATALOG_ENTRY'; payload: { catalogId: CatalogId; entryId: string } }
| { type: 'SET_STATE'; payload: Partial<SDKState> }
export const initialState: SDKState = {
customCatalogs: {},
}
// =============================================================================
// REDUCER
// =============================================================================
function sdkReducer(state: SDKState, action: SDKAction): SDKState {
switch (action.type) {
case 'ADD_CUSTOM_CATALOG_ENTRY': {
const entry = action.payload
const existing = state.customCatalogs[entry.catalogId] || []
return {
...state,
customCatalogs: {
...state.customCatalogs,
[entry.catalogId]: [...existing, entry],
},
}
}
case 'UPDATE_CUSTOM_CATALOG_ENTRY': {
const { catalogId, entryId, data } = action.payload
const existing = state.customCatalogs[catalogId] || []
return {
...state,
customCatalogs: {
...state.customCatalogs,
[catalogId]: existing.map(e =>
e.id === entryId
? { ...e, data, updatedAt: new Date().toISOString() }
: e
),
},
}
}
case 'DELETE_CUSTOM_CATALOG_ENTRY': {
const { catalogId, entryId } = action.payload
const existing = state.customCatalogs[catalogId] || []
return {
...state,
customCatalogs: {
...state.customCatalogs,
[catalogId]: existing.filter(e => e.id !== entryId),
},
}
}
case 'SET_STATE': {
return { ...state, ...action.payload }
}
default:
return state
}
}
// =============================================================================
// CONTEXT
// =============================================================================
interface SDKContextValue {
state: SDKState
dispatch: React.Dispatch<SDKAction>
}
const SDKContext = createContext<SDKContextValue | null>(null)
// =============================================================================
// PROVIDER
// =============================================================================
interface SDKProviderProps {
children: React.ReactNode
}
export function SDKProvider({ children }: SDKProviderProps) {
const [state, dispatch] = useReducer(sdkReducer, initialState)
const value: SDKContextValue = useMemo(() => ({
state,
dispatch,
}), [state, dispatch])
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>
}
// =============================================================================
// HOOK
// =============================================================================
export function useSDK(): SDKContextValue {
const context = useContext(SDKContext)
if (!context) {
throw new Error('useSDK must be used within SDKProvider')
}
return context
}
export { SDKContext }
+210
View File
@@ -0,0 +1,210 @@
/**
* Demo Controls for AI Compliance SDK
*/
import { Control } from '../types'
export const DEMO_CONTROLS: Control[] = [
// Zugangskontrolle
{
id: 'demo-ctrl-1',
name: 'Multi-Faktor-Authentifizierung',
description: 'Alle Systemzugriffe erfordern mindestens zwei unabhängige Authentifizierungsfaktoren (Wissen + Besitz).',
type: 'TECHNICAL',
category: 'Zugangskontrolle',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-1'],
owner: 'IT-Sicherheit',
dueDate: null,
},
{
id: 'demo-ctrl-2',
name: 'Rollenbasiertes Berechtigungskonzept',
description: 'Zugriffsrechte werden nach dem Least-Privilege-Prinzip anhand definierter Rollen vergeben und regelmäßig überprüft.',
type: 'ORGANIZATIONAL',
category: 'Zugangskontrolle',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-2'],
owner: 'IT-Sicherheit',
dueDate: null,
},
// Verfügbarkeit
{
id: 'demo-ctrl-3',
name: 'Automatisiertes Backup-System',
description: 'Tägliche inkrementelle Backups und wöchentliche Vollbackups aller kritischen Daten mit Verschlüsselung.',
type: 'TECHNICAL',
category: 'Verfügbarkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-3'],
owner: 'IT-Betrieb',
dueDate: null,
},
{
id: 'demo-ctrl-4',
name: 'Georedundante Datenspeicherung',
description: 'Kritische Daten werden synchron in zwei geographisch getrennten Rechenzentren gespeichert.',
type: 'TECHNICAL',
category: 'Verfügbarkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-4'],
owner: 'IT-Betrieb',
dueDate: null,
},
// KI-Fairness
{
id: 'demo-ctrl-5',
name: 'Bias-Monitoring',
description: 'Kontinuierliche Überwachung der KI-Modelle auf systematische Verzerrungen anhand definierter Fairness-Metriken.',
type: 'TECHNICAL',
category: 'KI-Governance',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-5'],
owner: 'Data Science Lead',
dueDate: null,
},
{
id: 'demo-ctrl-6',
name: 'Human-in-the-Loop',
description: 'Kritische automatisierte Entscheidungen werden vor Umsetzung durch qualifizierte Mitarbeiter überprüft.',
type: 'ORGANIZATIONAL',
category: 'KI-Governance',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-6'],
owner: 'Fachbereich HR',
dueDate: null,
},
// Transparenz
{
id: 'demo-ctrl-7',
name: 'Explainable AI Komponenten',
description: 'Einsatz von SHAP/LIME zur Erklärung von KI-Entscheidungen für nachvollziehbare Begründungen.',
type: 'TECHNICAL',
category: 'Transparenz',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-7'],
owner: 'Data Science Lead',
dueDate: null,
},
{
id: 'demo-ctrl-8',
name: 'Verständliche Datenschutzinformationen',
description: 'Betroffene erhalten klare, verständliche Informationen über die Verarbeitung ihrer Daten gemäß Art. 13-14 DSGVO.',
type: 'ORGANIZATIONAL',
category: 'Transparenz',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-8'],
owner: 'DSB',
dueDate: null,
},
// Datensparsamkeit
{
id: 'demo-ctrl-9',
name: 'Zweckbindungskontrollen',
description: 'Technische Maßnahmen stellen sicher, dass Daten nur für definierte Zwecke verarbeitet werden.',
type: 'TECHNICAL',
category: 'Datensparsamkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-9'],
owner: 'IT-Sicherheit',
dueDate: null,
},
{
id: 'demo-ctrl-10',
name: 'Anonymisierungs-Pipeline',
description: 'Automatisierte Anonymisierung von Daten für Analysen, wo keine Personenbezug erforderlich ist.',
type: 'TECHNICAL',
category: 'Datensparsamkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-10'],
owner: 'Data Engineering',
dueDate: null,
},
// KI-Sicherheit
{
id: 'demo-ctrl-11',
name: 'Input-Validierung',
description: 'Strenge Validierung aller Eingabedaten zur Verhinderung von Adversarial Attacks auf KI-Modelle.',
type: 'TECHNICAL',
category: 'KI-Sicherheit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-11'],
owner: 'Data Science Lead',
dueDate: null,
},
{
id: 'demo-ctrl-12',
name: 'Model Performance Monitoring',
description: 'Kontinuierliche Überwachung der Modell-Performance mit automatischen Alerts bei Abweichungen.',
type: 'TECHNICAL',
category: 'KI-Sicherheit',
implementationStatus: 'PARTIAL',
effectiveness: 'MEDIUM',
evidence: [],
owner: 'Data Science Lead',
dueDate: new Date('2026-03-31'),
},
// Datenlebenszyklus
{
id: 'demo-ctrl-13',
name: 'Automatisierte Löschroutinen',
description: 'Technische Umsetzung der Aufbewahrungsfristen mit automatischer Löschung nach Fristablauf.',
type: 'TECHNICAL',
category: 'Datenlebenszyklus',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-13'],
owner: 'IT-Betrieb',
dueDate: null,
},
{
id: 'demo-ctrl-14',
name: 'Löschprotokoll-Review',
description: 'Quartalsweise Überprüfung der Löschprotokolle durch den DSB.',
type: 'ORGANIZATIONAL',
category: 'Datenlebenszyklus',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-14'],
owner: 'DSB',
dueDate: null,
},
// Audit
{
id: 'demo-ctrl-15',
name: 'Umfassendes Audit-Logging',
description: 'Alle sicherheitsrelevanten Ereignisse werden manipulationssicher protokolliert und 10 Jahre aufbewahrt.',
type: 'TECHNICAL',
category: 'Audit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-15'],
owner: 'IT-Sicherheit',
dueDate: null,
},
]
export function getDemoControls(): Control[] {
return DEMO_CONTROLS.map(ctrl => ({
...ctrl,
dueDate: ctrl.dueDate ? new Date(ctrl.dueDate) : null,
}))
}
+224
View File
@@ -0,0 +1,224 @@
/**
* Demo DSFA for AI Compliance SDK
*/
import { DSFA, DSFASection, DSFAApproval } from '../types'
export const DEMO_DSFA: DSFA = {
id: 'demo-dsfa-1',
status: 'IN_REVIEW',
version: 2,
sections: [
{
id: 'dsfa-sec-1',
title: 'Systematische Beschreibung der Verarbeitungsvorgänge',
content: `## 1. Verarbeitungsbeschreibung
### 1.1 Gegenstand der Verarbeitung
Die geplante KI-gestützte Kundenanalyse verarbeitet personenbezogene Daten von Kunden und Interessenten zur Optimierung von Marketingmaßnahmen und Personalisierung von Angeboten.
### 1.2 Verarbeitungszwecke
- Kundensegmentierung basierend auf Kaufverhalten
- Churn-Prediction zur Kundenbindung
- Personalisierte Produktempfehlungen
- Optimierung von Marketing-Kampagnen
### 1.3 Kategorien personenbezogener Daten
- **Stammdaten**: Name, Adresse, E-Mail, Telefon
- **Transaktionsdaten**: Käufe, Bestellungen, Retouren
- **Nutzungsdaten**: Clickstreams, Seitenaufrufe, Verweildauer
- **Demographische Daten**: Alter, Geschlecht, PLZ-Region
### 1.4 Kategorien betroffener Personen
- Bestandskunden (ca. 250.000 aktive Kunden)
- Registrierte Interessenten (ca. 100.000)
- Newsletter-Abonnenten (ca. 180.000)
### 1.5 Rechtsgrundlage
**Primär**: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
**Sekundär**: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweiterte Profiling-Maßnahmen)
Das berechtigte Interesse liegt in der Verbesserung des Kundenerlebnisses und der Effizienzsteigerung des Marketings.`,
status: 'COMPLETED',
order: 1,
},
{
id: 'dsfa-sec-2',
title: 'Bewertung der Notwendigkeit und Verhältnismäßigkeit',
content: `## 2. Notwendigkeit und Verhältnismäßigkeit
### 2.1 Notwendigkeit der Verarbeitung
Die Verarbeitung ist notwendig, um:
- Kunden individuell relevante Angebote zu unterbreiten
- Abwanderungsgefährdete Kunden frühzeitig zu identifizieren
- Marketing-Budget effizienter einzusetzen
- Wettbewerbsfähigkeit zu erhalten
### 2.2 Verhältnismäßigkeitsprüfung
**Alternative Methoden geprüft:**
1. **Manuelle Analyse**: Nicht praktikabel bei 250.000+ Kunden
2. **Regelbasierte Systeme**: Zu ungenau, führt zu höherem Datenverbrauch
3. **Aggregierte Analysen**: Keine ausreichende Personalisierung möglich
**Ergebnis**: Die KI-gestützte Analyse stellt die mildeste effektive Maßnahme dar.
### 2.3 Datensparsamkeit
- Nur für den Zweck notwendige Daten werden verarbeitet
- Sensitive Kategorien (Art. 9 DSGVO) werden ausgeschlossen
- Automatische Löschung nach definierten Fristen
### 2.4 Interessenabwägung
| Interesse des Verantwortlichen | Interesse der Betroffenen |
|-------------------------------|---------------------------|
| Effizientes Marketing | Privatsphäre |
| Kundenbindung | Keine unerwünschte Profilbildung |
| Umsatzsteigerung | Transparenz über Verarbeitung |
**Ausgleichende Maßnahmen:**
- Umfassende Informationen nach Art. 13/14 DSGVO
- Einfacher Opt-out für Profiling
- Human-Review bei kritischen Entscheidungen`,
status: 'COMPLETED',
order: 2,
},
{
id: 'dsfa-sec-3',
title: 'Risikobewertung',
content: `## 3. Risiken für Rechte und Freiheiten
### 3.1 Identifizierte Risiken
| # | Risiko | Eintritt | Schwere | Gesamt |
|---|--------|----------|---------|--------|
| R1 | Unbefugter Zugriff auf Profildaten | Mittel | Hoch | HOCH |
| R2 | Diskriminierende Entscheidungen durch Bias | Mittel | Hoch | HOCH |
| R3 | Unzulässige Profilbildung | Mittel | Mittel | MITTEL |
| R4 | Fehlende Nachvollziehbarkeit | Hoch | Mittel | MITTEL |
| R5 | Übermäßige Datensammlung | Niedrig | Mittel | NIEDRIG |
### 3.2 Detailanalyse kritischer Risiken
**R1 - Unbefugter Zugriff**
- Quelle: Externe Angreifer, Insider-Bedrohung
- Auswirkung: Identitätsdiebstahl, Reputationsschaden
- Betroffene: Alle Kunden
**R2 - Diskriminierende Entscheidungen**
- Quelle: Historische Verzerrungen in Trainingsdaten
- Auswirkung: Benachteiligung bestimmter Gruppen
- Betroffene: Potentiell alle, besonders geschützte Gruppen`,
status: 'COMPLETED',
order: 3,
},
{
id: 'dsfa-sec-4',
title: 'Maßnahmen zur Risikominderung',
content: `## 4. Abhilfemaßnahmen
### 4.1 Technische Maßnahmen
| Maßnahme | Risiko | Status | Wirksamkeit |
|----------|--------|--------|-------------|
| Multi-Faktor-Authentifizierung | R1 | ✅ Umgesetzt | Hoch |
| Verschlüsselung (AES-256) | R1 | ✅ Umgesetzt | Hoch |
| Bias-Monitoring | R2 | ✅ Umgesetzt | Mittel |
| Explainable AI | R4 | ✅ Umgesetzt | Mittel |
| Zweckbindungskontrollen | R3 | ✅ Umgesetzt | Hoch |
| Audit-Logging | R1, R4 | ✅ Umgesetzt | Hoch |
### 4.2 Organisatorische Maßnahmen
| Maßnahme | Risiko | Status | Wirksamkeit |
|----------|--------|--------|-------------|
| Rollenbasierte Zugriffskontrolle | R1 | ✅ Umgesetzt | Hoch |
| Human-in-the-Loop | R2 | ✅ Umgesetzt | Hoch |
| Datenschutz-Schulungen | R1, R3 | ✅ Umgesetzt | Mittel |
| Regelmäßige Audits | Alle | ⏳ Geplant | Hoch |
### 4.3 Restrisikobewertung
Nach Implementierung aller Maßnahmen:
- **R1**: HOCH → MITTEL (akzeptabel)
- **R2**: HOCH → MITTEL (akzeptabel)
- **R3**: MITTEL → NIEDRIG (akzeptabel)
- **R4**: MITTEL → NIEDRIG (akzeptabel)
- **R5**: NIEDRIG → NIEDRIG (akzeptabel)`,
status: 'COMPLETED',
order: 4,
},
{
id: 'dsfa-sec-5',
title: 'Stellungnahme des Datenschutzbeauftragten',
content: `## 5. Stellungnahme DSB
### 5.1 Bewertung
Der Datenschutzbeauftragte hat die DSFA geprüft und kommt zu folgender Einschätzung:
**Positiv:**
- Umfassende Risikoanalyse durchgeführt
- Technische Schutzmaßnahmen dem Stand der Technik entsprechend
- Transparenzpflichten angemessen berücksichtigt
- Interessenabwägung nachvollziehbar dokumentiert
**Verbesserungspotenzial:**
- Regelmäßige Überprüfung der Bias-Metriken sollte quartalsweise erfolgen
- Informationen für Betroffene könnten noch verständlicher formuliert werden
- Löschkonzept sollte um automatische Überprüfungsmechanismen ergänzt werden
### 5.2 Empfehlung
Der DSB empfiehlt die **Genehmigung** der Verarbeitungstätigkeit unter der Voraussetzung, dass:
1. Die identifizierten Verbesserungsmaßnahmen innerhalb von 3 Monaten umgesetzt werden
2. Eine jährliche Überprüfung der DSFA erfolgt
3. Bei wesentlichen Änderungen eine Aktualisierung vorgenommen wird
---
*Datum: 2026-01-28*
*Unterschrift: [DSB]*`,
status: 'COMPLETED',
order: 5,
},
],
approvals: [
{
id: 'dsfa-appr-1',
approver: 'Dr. Thomas Schmidt',
role: 'Datenschutzbeauftragter',
status: 'APPROVED',
comment: 'Unter den genannten Voraussetzungen genehmigt.',
approvedAt: new Date('2026-01-28'),
},
{
id: 'dsfa-appr-2',
approver: 'Maria Weber',
role: 'CISO',
status: 'APPROVED',
comment: 'Technische Maßnahmen sind angemessen.',
approvedAt: new Date('2026-01-29'),
},
{
id: 'dsfa-appr-3',
approver: 'Michael Bauer',
role: 'Geschäftsführung',
status: 'PENDING',
comment: null,
approvedAt: null,
},
],
createdAt: new Date('2026-01-15'),
updatedAt: new Date('2026-02-01'),
}
export function getDemoDSFA(): DSFA {
return {
...DEMO_DSFA,
approvals: DEMO_DSFA.approvals.map(a => ({
...a,
approvedAt: a.approvedAt ? new Date(a.approvedAt) : null,
})),
createdAt: new Date(DEMO_DSFA.createdAt),
updatedAt: new Date(DEMO_DSFA.updatedAt),
}
}
+556
View File
@@ -0,0 +1,556 @@
/**
* Demo Data Seeding for AI Compliance SDK
*
* IMPORTANT: Demo data is NOT hardcoded in the frontend.
* This module provides seed data that gets stored via the API,
* exactly like real customer data would be stored.
*
* The seedDemoData() function writes data through the API,
* and the data is then loaded from the database like any other data.
*/
import { SDKState } from '../types'
import { getSDKApiClient } from '../api-client'
// Seed data imports (these are templates, not runtime data)
import { getDemoUseCases, DEMO_USE_CASES } from './use-cases'
import { getDemoRisks, DEMO_RISKS } from './risks'
import { getDemoControls, DEMO_CONTROLS } from './controls'
import { getDemoDSFA, DEMO_DSFA } from './dsfa'
import { getDemoTOMs, DEMO_TOMS } from './toms'
import { getDemoProcessingActivities, getDemoRetentionPolicies, DEMO_PROCESSING_ACTIVITIES, DEMO_RETENTION_POLICIES } from './vvt'
// Re-export for direct access to seed templates (for testing/development)
export {
getDemoUseCases,
getDemoRisks,
getDemoControls,
getDemoDSFA,
getDemoTOMs,
getDemoProcessingActivities,
getDemoRetentionPolicies,
// Raw data exports
DEMO_USE_CASES,
DEMO_RISKS,
DEMO_CONTROLS,
DEMO_DSFA,
DEMO_TOMS,
DEMO_PROCESSING_ACTIVITIES,
DEMO_RETENTION_POLICIES,
}
/**
* Generate a complete demo state object
* This is used as seed data for the API, not as runtime data
*/
export function generateDemoState(tenantId: string, userId: string): Partial<SDKState> {
const now = new Date()
return {
// Metadata
version: '1.0.0',
lastModified: now,
// Tenant & User
tenantId,
userId,
subscription: 'PROFESSIONAL',
// Customer Type
customerType: 'new',
// Company Profile (Demo: TechStart GmbH - SaaS-Startup aus Berlin)
companyProfile: {
companyName: 'TechStart GmbH',
legalForm: 'gmbh',
industry: 'Technologie / IT',
foundedYear: 2022,
businessModel: 'B2B_B2C',
offerings: ['app_web', 'software_saas', 'services_consulting'],
companySize: 'small',
employeeCount: '10-49',
annualRevenue: '2-10 Mio',
headquartersCountry: 'DE',
headquartersCity: 'Berlin',
hasInternationalLocations: false,
internationalCountries: [],
targetMarkets: ['germany_only', 'dach'],
primaryJurisdiction: 'DE',
isDataController: true,
isDataProcessor: true,
usesAI: true,
aiUseCases: ['KI-gestützte Kundenberatung', 'Automatisierte Dokumentenanalyse'],
dpoName: 'Max Mustermann',
dpoEmail: 'dsb@techstart.de',
legalContactName: null,
legalContactEmail: null,
isComplete: true,
completedAt: new Date('2026-01-14'),
},
// Progress - showing a realistic partially completed workflow
currentPhase: 2,
currentStep: 'tom',
completedSteps: [
'company-profile',
'use-case-assessment',
'screening',
'modules',
'requirements',
'controls',
'evidence',
'audit-checklist',
'risks',
'ai-act',
'obligations',
'dsfa',
],
checkpoints: {
'CP-PROF': { checkpointId: 'CP-PROF', passed: true, validatedAt: new Date('2026-01-14'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-UC': { checkpointId: 'CP-UC', passed: true, validatedAt: new Date('2026-01-15'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-SCAN': { checkpointId: 'CP-SCAN', passed: true, validatedAt: new Date('2026-01-16'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-MOD': { checkpointId: 'CP-MOD', passed: true, validatedAt: new Date('2026-01-17'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-REQ': { checkpointId: 'CP-REQ', passed: true, validatedAt: new Date('2026-01-18'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-CTRL': { checkpointId: 'CP-CTRL', passed: true, validatedAt: new Date('2026-01-19'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-EVI': { checkpointId: 'CP-EVI', passed: true, validatedAt: new Date('2026-01-20'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-CHK': { checkpointId: 'CP-CHK', passed: true, validatedAt: new Date('2026-01-21'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-RISK': { checkpointId: 'CP-RISK', passed: true, validatedAt: new Date('2026-01-22'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-AI': { checkpointId: 'CP-AI', passed: true, validatedAt: new Date('2026-01-25'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-OBL': { checkpointId: 'CP-OBL', passed: true, validatedAt: new Date('2026-01-27'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-DSFA': { checkpointId: 'CP-DSFA', passed: true, validatedAt: new Date('2026-01-30'), validatedBy: 'DSB', errors: [], warnings: [] },
},
// Phase 1 Data
useCases: getDemoUseCases(),
activeUseCase: 'demo-uc-1',
screening: {
id: 'demo-scan-1',
status: 'COMPLETED',
startedAt: new Date('2026-01-16T09:00:00'),
completedAt: new Date('2026-01-16T09:15:00'),
sbom: {
format: 'CycloneDX',
version: '1.4',
components: [
{
name: 'tensorflow',
version: '2.15.0',
type: 'library',
purl: 'pkg:pypi/tensorflow@2.15.0',
licenses: ['Apache-2.0'],
vulnerabilities: [],
},
{
name: 'scikit-learn',
version: '1.4.0',
type: 'library',
purl: 'pkg:pypi/scikit-learn@1.4.0',
licenses: ['BSD-3-Clause'],
vulnerabilities: [],
},
{
name: 'pandas',
version: '2.2.0',
type: 'library',
purl: 'pkg:pypi/pandas@2.2.0',
licenses: ['BSD-3-Clause'],
vulnerabilities: [],
},
],
dependencies: [],
generatedAt: new Date('2026-01-16T09:10:00'),
},
securityScan: {
totalIssues: 3,
critical: 0,
high: 1,
medium: 1,
low: 1,
issues: [
{
id: 'sec-issue-1',
severity: 'HIGH',
title: 'Outdated cryptography library',
description: 'The cryptography library version 41.0.0 has known vulnerabilities',
cve: 'CVE-2024-1234',
cvss: 7.5,
affectedComponent: 'cryptography',
remediation: 'Upgrade to cryptography >= 42.0.0',
status: 'RESOLVED',
},
{
id: 'sec-issue-2',
severity: 'MEDIUM',
title: 'Insecure default configuration',
description: 'Debug mode enabled in production configuration',
cve: null,
cvss: 5.3,
affectedComponent: 'app-config',
remediation: 'Set DEBUG=false in production',
status: 'RESOLVED',
},
{
id: 'sec-issue-3',
severity: 'LOW',
title: 'Missing security headers',
description: 'X-Content-Type-Options header not set',
cve: null,
cvss: 3.1,
affectedComponent: 'web-server',
remediation: 'Add security headers middleware',
status: 'RESOLVED',
},
],
},
error: null,
},
modules: [
{
id: 'demo-mod-1',
name: 'Kundendaten-Modul',
description: 'Verarbeitung von Kundendaten für Marketing und Analyse',
regulations: ['DSGVO', 'TTDSG'],
criticality: 'HIGH',
processesPersonalData: true,
hasAIComponents: true,
},
{
id: 'demo-mod-2',
name: 'HR-Modul',
description: 'Bewerbermanagement und Personalverwaltung',
regulations: ['DSGVO', 'AGG', 'AI Act'],
criticality: 'HIGH',
processesPersonalData: true,
hasAIComponents: true,
},
{
id: 'demo-mod-3',
name: 'Support-Modul',
description: 'Kundenservice und Chatbot-System',
regulations: ['DSGVO', 'AI Act'],
criticality: 'MEDIUM',
processesPersonalData: true,
hasAIComponents: true,
},
],
requirements: [
{
id: 'demo-req-1',
regulation: 'DSGVO',
article: 'Art. 5',
title: 'Grundsätze der Verarbeitung',
description: 'Einhaltung der Grundsätze für die Verarbeitung personenbezogener Daten',
criticality: 'CRITICAL',
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-1', 'demo-ctrl-2', 'demo-ctrl-9'],
},
{
id: 'demo-req-2',
regulation: 'DSGVO',
article: 'Art. 32',
title: 'Sicherheit der Verarbeitung',
description: 'Geeignete technische und organisatorische Maßnahmen',
criticality: 'CRITICAL',
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-1', 'demo-ctrl-3', 'demo-ctrl-4'],
},
{
id: 'demo-req-3',
regulation: 'DSGVO',
article: 'Art. 25',
title: 'Datenschutz durch Technikgestaltung',
description: 'Privacy by Design und Privacy by Default',
criticality: 'HIGH',
applicableModules: ['demo-mod-1', 'demo-mod-2'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-9', 'demo-ctrl-10'],
},
{
id: 'demo-req-4',
regulation: 'AI Act',
article: 'Art. 13',
title: 'Transparenz',
description: 'Transparenzanforderungen für KI-Systeme',
criticality: 'HIGH',
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-7', 'demo-ctrl-8'],
},
{
id: 'demo-req-5',
regulation: 'AI Act',
article: 'Art. 9',
title: 'Risikomanagement',
description: 'Risikomanagementsystem für Hochrisiko-KI',
criticality: 'HIGH',
applicableModules: ['demo-mod-2'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-5', 'demo-ctrl-6', 'demo-ctrl-11', 'demo-ctrl-12'],
},
],
controls: getDemoControls(),
evidence: [
{
id: 'demo-evi-1',
controlId: 'demo-ctrl-1',
type: 'SCREENSHOT',
name: 'MFA-Konfiguration Azure AD',
description: 'Screenshot der MFA-Einstellungen im Azure AD Admin Portal',
fileUrl: null,
validFrom: new Date('2026-01-01'),
validUntil: new Date('2027-01-01'),
uploadedBy: 'IT-Security',
uploadedAt: new Date('2026-01-10'),
},
{
id: 'demo-evi-2',
controlId: 'demo-ctrl-2',
type: 'DOCUMENT',
name: 'Berechtigungskonzept v2.1',
description: 'Dokumentiertes Berechtigungskonzept mit Rollenmatrix',
fileUrl: null,
validFrom: new Date('2026-01-01'),
validUntil: null,
uploadedBy: 'IT-Security',
uploadedAt: new Date('2026-01-05'),
},
{
id: 'demo-evi-5',
controlId: 'demo-ctrl-5',
type: 'AUDIT_REPORT',
name: 'Bias-Audit Q1/2026',
description: 'Externer Audit-Bericht zur Fairness des KI-Modells',
fileUrl: null,
validFrom: new Date('2026-01-15'),
validUntil: new Date('2026-04-15'),
uploadedBy: 'Data Science Lead',
uploadedAt: new Date('2026-01-20'),
},
],
checklist: [
{
id: 'demo-chk-1',
requirementId: 'demo-req-1',
title: 'Rechtmäßigkeit der Verarbeitung geprüft',
description: 'Dokumentierte Prüfung der Rechtsgrundlagen',
status: 'PASSED',
notes: 'Geprüft durch DSB',
verifiedBy: 'DSB',
verifiedAt: new Date('2026-01-20'),
},
{
id: 'demo-chk-2',
requirementId: 'demo-req-2',
title: 'TOMs dokumentiert und umgesetzt',
description: 'Technische und organisatorische Maßnahmen',
status: 'PASSED',
notes: 'Alle TOMs implementiert',
verifiedBy: 'CISO',
verifiedAt: new Date('2026-01-21'),
},
],
risks: getDemoRisks(),
// Phase 2 Data
aiActClassification: {
riskCategory: 'HIGH',
systemType: 'Beschäftigungsbezogenes KI-System (Art. 6 Abs. 2 AI Act)',
obligations: [
{
id: 'demo-ai-obl-1',
article: 'Art. 9',
title: 'Risikomanagementsystem',
description: 'Einrichtung eines KI-Risikomanagementsystems',
deadline: new Date('2026-08-01'),
status: 'IN_PROGRESS',
},
{
id: 'demo-ai-obl-2',
article: 'Art. 10',
title: 'Daten-Governance',
description: 'Anforderungen an Trainingsdaten',
deadline: new Date('2026-08-01'),
status: 'COMPLETED',
},
{
id: 'demo-ai-obl-3',
article: 'Art. 13',
title: 'Transparenz',
description: 'Dokumentation für Nutzer',
deadline: new Date('2026-08-01'),
status: 'COMPLETED',
},
],
assessmentDate: new Date('2026-01-25'),
assessedBy: 'Compliance Team',
justification: 'Das System fällt unter Art. 6 Abs. 2 lit. a AI Act (Einstellung und Auswahl von Personen).',
},
obligations: [
{
id: 'demo-obl-1',
regulation: 'DSGVO',
article: 'Art. 30',
title: 'Verarbeitungsverzeichnis',
description: 'Führung eines Verzeichnisses der Verarbeitungstätigkeiten',
deadline: null,
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
status: 'COMPLETED',
responsible: 'DSB',
},
{
id: 'demo-obl-2',
regulation: 'DSGVO',
article: 'Art. 35',
title: 'Datenschutz-Folgenabschätzung',
description: 'Durchführung einer DSFA für Hochrisiko-Verarbeitungen',
deadline: null,
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
status: 'COMPLETED',
responsible: 'DSB',
},
{
id: 'demo-obl-3',
regulation: 'AI Act',
article: 'Art. 49',
title: 'CE-Kennzeichnung',
description: 'CE-Kennzeichnung für Hochrisiko-KI-Systeme',
deadline: new Date('2026-08-01'),
penalty: 'Bis zu 35 Mio. EUR oder 7% des Jahresumsatzes',
status: 'PENDING',
responsible: 'Compliance',
},
],
dsfa: getDemoDSFA(),
toms: getDemoTOMs(),
retentionPolicies: getDemoRetentionPolicies(),
vvt: getDemoProcessingActivities(),
// Documents, Cookie Banner, etc. - partially filled
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
// Security
sbom: null,
securityIssues: [],
securityBacklog: [],
// UI State
commandBarHistory: [],
recentSearches: ['DSGVO Art. 5', 'Bias-Monitoring', 'TOM Verschlüsselung'],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
}
}
/**
* Seed demo data into the database via API
* This ensures demo data is stored exactly like real customer data
*/
export async function seedDemoData(
tenantId: string = 'demo-tenant',
userId: string = 'demo-user',
apiBaseUrl?: string
): Promise<{ success: boolean; message: string }> {
try {
const apiClient = getSDKApiClient(tenantId)
// Generate the demo state
const demoState = generateDemoState(tenantId, userId) as SDKState
// Save via the same API that real data uses
await apiClient.saveState(demoState)
return {
success: true,
message: `Demo data successfully seeded for tenant ${tenantId}`,
}
} catch (error) {
console.error('Failed to seed demo data:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error during seeding',
}
}
}
/**
* Check if demo data exists for a tenant
*/
export async function hasDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
try {
const apiClient = getSDKApiClient(tenantId)
const response = await apiClient.getState()
// Check if we have any use cases (indicating data exists)
return response !== null && response.state && Array.isArray(response.state.useCases) && response.state.useCases.length > 0
} catch {
return false
}
}
/**
* Clear demo data for a tenant
*/
export async function clearDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
try {
const apiClient = getSDKApiClient(tenantId)
await apiClient.deleteState()
return true
} catch {
return false
}
}
/**
* Seed demo data via direct API call (for use outside of React context)
* This is useful for server-side seeding or CLI tools
*/
export async function seedDemoDataDirect(
baseUrl: string,
tenantId: string = 'demo-tenant',
userId: string = 'demo-user'
): Promise<{ success: boolean; message: string }> {
try {
const demoState = generateDemoState(tenantId, userId)
const response = await fetch(`${baseUrl}/api/sdk/v1/state`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId,
userId,
state: demoState,
}),
})
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(error.message || `HTTP ${response.status}`)
}
return {
success: true,
message: `Demo data successfully seeded for tenant ${tenantId}`,
}
} catch (error) {
console.error('Failed to seed demo data:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error during seeding',
}
}
}
+268
View File
@@ -0,0 +1,268 @@
/**
* Demo Risks for AI Compliance SDK
*/
import { Risk, RiskMitigation } from '../types'
export const DEMO_RISKS: Risk[] = [
{
id: 'demo-risk-1',
title: 'Unbefugter Zugriff auf personenbezogene Daten',
description: 'Risiko des unbefugten Zugriffs auf Kundendaten durch externe Angreifer oder interne Mitarbeiter ohne entsprechende Berechtigung.',
category: 'Datensicherheit',
likelihood: 3,
impact: 5,
severity: 'CRITICAL',
inherentRiskScore: 15,
residualRiskScore: 6,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-1a',
description: 'Implementierung von Multi-Faktor-Authentifizierung für alle Systemzugriffe',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-1',
},
{
id: 'demo-mit-1b',
description: 'Rollenbasiertes Zugriffskonzept mit Least-Privilege-Prinzip',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-2',
},
],
owner: 'CISO',
relatedControls: ['demo-ctrl-1', 'demo-ctrl-2'],
relatedRequirements: ['demo-req-1', 'demo-req-2'],
},
{
id: 'demo-risk-2',
title: 'KI-Bias bei automatisierten Entscheidungen',
description: 'Das KI-System könnte systematische Verzerrungen aufweisen, die zu diskriminierenden Entscheidungen führen, insbesondere bei der Bewerbungsvorauswahl.',
category: 'KI-Ethik',
likelihood: 4,
impact: 4,
severity: 'HIGH',
inherentRiskScore: 16,
residualRiskScore: 8,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-2a',
description: 'Regelmäßiges Bias-Monitoring mit Fairness-Metriken',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-5',
},
{
id: 'demo-mit-2b',
description: 'Human-in-the-Loop bei kritischen Entscheidungen',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 25,
controlId: 'demo-ctrl-6',
},
],
owner: 'Data Science Lead',
relatedControls: ['demo-ctrl-5', 'demo-ctrl-6'],
relatedRequirements: ['demo-req-5', 'demo-req-6'],
},
{
id: 'demo-risk-3',
title: 'Datenverlust durch Systemausfall',
description: 'Verlust von Kundendaten und KI-Modellen durch Hardware-Defekte, Softwarefehler oder Naturkatastrophen.',
category: 'Verfügbarkeit',
likelihood: 2,
impact: 5,
severity: 'HIGH',
inherentRiskScore: 10,
residualRiskScore: 3,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-3a',
description: 'Tägliche inkrementelle und wöchentliche Vollbackups',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-3',
},
{
id: 'demo-mit-3b',
description: 'Georedundante Datenspeicherung in zwei Rechenzentren',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 35,
controlId: 'demo-ctrl-4',
},
],
owner: 'IT-Leiter',
relatedControls: ['demo-ctrl-3', 'demo-ctrl-4'],
relatedRequirements: ['demo-req-3'],
},
{
id: 'demo-risk-4',
title: 'Unzureichende Transparenz bei KI-Entscheidungen',
description: 'Betroffene verstehen nicht, wie KI-Entscheidungen zustande kommen, was zu Beschwerden und regulatorischen Problemen führen kann.',
category: 'Transparenz',
likelihood: 4,
impact: 3,
severity: 'MEDIUM',
inherentRiskScore: 12,
residualRiskScore: 4,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-4a',
description: 'Explainable AI Komponenten für nachvollziehbare Entscheidungen',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-7',
},
{
id: 'demo-mit-4b',
description: 'Verständliche Informationen für Betroffene gem. Art. 13-14 DSGVO',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-8',
},
],
owner: 'DSB',
relatedControls: ['demo-ctrl-7', 'demo-ctrl-8'],
relatedRequirements: ['demo-req-4'],
},
{
id: 'demo-risk-5',
title: 'Unerlaubte Profilbildung',
description: 'Durch die Zusammenführung verschiedener Datenquellen könnte eine unzulässige umfassende Profilbildung von Personen entstehen.',
category: 'Datenschutz',
likelihood: 3,
impact: 4,
severity: 'HIGH',
inherentRiskScore: 12,
residualRiskScore: 6,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-5a',
description: 'Strenge Zweckbindung der Datenverarbeitung',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 25,
controlId: 'demo-ctrl-9',
},
{
id: 'demo-mit-5b',
description: 'Datensparsamkeit durch Aggregation und Anonymisierung',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-10',
},
],
owner: 'DSB',
relatedControls: ['demo-ctrl-9', 'demo-ctrl-10'],
relatedRequirements: ['demo-req-7', 'demo-req-8'],
},
{
id: 'demo-risk-6',
title: 'Mangelnde Modell-Robustheit',
description: 'KI-Modelle könnten durch Adversarial Attacks oder veränderte Inputdaten manipuliert werden und falsche Ergebnisse liefern.',
category: 'KI-Sicherheit',
likelihood: 2,
impact: 4,
severity: 'MEDIUM',
inherentRiskScore: 8,
residualRiskScore: 4,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-6a',
description: 'Input-Validierung und Anomalie-Erkennung',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-11',
},
{
id: 'demo-mit-6b',
description: 'Regelmäßige Modell-Retraining und Performance-Monitoring',
type: 'MITIGATE',
status: 'IN_PROGRESS',
effectiveness: 20,
controlId: 'demo-ctrl-12',
},
],
owner: 'Data Science Lead',
relatedControls: ['demo-ctrl-11', 'demo-ctrl-12'],
relatedRequirements: ['demo-req-9'],
},
{
id: 'demo-risk-7',
title: 'Verstoß gegen Aufbewahrungsfristen',
description: 'Daten werden länger als zulässig gespeichert oder zu früh gelöscht, was zu Compliance-Verstößen führt.',
category: 'Datenschutz',
likelihood: 3,
impact: 3,
severity: 'MEDIUM',
inherentRiskScore: 9,
residualRiskScore: 3,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-7a',
description: 'Automatisierte Löschroutinen mit Retention-Policy-Enforcement',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-13',
},
{
id: 'demo-mit-7b',
description: 'Quartalsmäßige Überprüfung der Löschprotokolle',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 25,
controlId: 'demo-ctrl-14',
},
],
owner: 'DSB',
relatedControls: ['demo-ctrl-13', 'demo-ctrl-14'],
relatedRequirements: ['demo-req-10'],
},
{
id: 'demo-risk-8',
title: 'Fehlende Nachvollziehbarkeit im Audit',
description: 'Bei Prüfungen können Verarbeitungsvorgänge nicht lückenlos nachvollzogen werden.',
category: 'Compliance',
likelihood: 2,
impact: 3,
severity: 'MEDIUM',
inherentRiskScore: 6,
residualRiskScore: 2,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-8a',
description: 'Umfassendes Audit-Logging aller Verarbeitungsvorgänge',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 50,
controlId: 'demo-ctrl-15',
},
],
owner: 'IT-Leiter',
relatedControls: ['demo-ctrl-15'],
relatedRequirements: ['demo-req-11'],
},
]
export function getDemoRisks(): Risk[] {
return DEMO_RISKS
}
+296
View File
@@ -0,0 +1,296 @@
/**
* Demo TOMs (Technical & Organizational Measures) for AI Compliance SDK
* These are seed data structures - actual data is stored in database
*/
import { TOM } from '../types'
export const DEMO_TOMS: TOM[] = [
// Zugangskontrolle
{
id: 'demo-tom-1',
category: 'Zugangskontrolle',
name: 'Physische Zutrittskontrolle',
description: 'Elektronische Zugangskontrollsysteme mit personenbezogenen Zutrittskarten für alle Serverräume und Rechenzentren. Protokollierung aller Zutritte.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Facility Management',
implementationDate: new Date('2025-06-01'),
reviewDate: new Date('2026-06-01'),
evidence: ['demo-evi-tom-1'],
},
{
id: 'demo-tom-2',
category: 'Zugangskontrolle',
name: 'Besuchermanagement',
description: 'Registrierung aller Besucher mit Identitätsprüfung, Ausgabe von Besucherausweisen und permanente Begleitung in sicherheitsrelevanten Bereichen.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'MEDIUM',
responsiblePerson: 'Empfang/Security',
implementationDate: new Date('2025-03-15'),
reviewDate: new Date('2026-03-15'),
evidence: ['demo-evi-tom-2'],
},
// Zugriffskontrolle
{
id: 'demo-tom-3',
category: 'Zugriffskontrolle',
name: 'Identity & Access Management (IAM)',
description: 'Zentrales IAM-System mit automatischer Provisionierung, Deprovisionierung und regelmäßiger Rezertifizierung aller Benutzerkonten.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-3'],
},
{
id: 'demo-tom-4',
category: 'Zugriffskontrolle',
name: 'Privileged Access Management (PAM)',
description: 'Spezielles Management für administrative Zugänge mit Session-Recording, automatischer Passwortrotation und Just-in-Time-Berechtigungen.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-04-01'),
reviewDate: new Date('2026-04-01'),
evidence: ['demo-evi-tom-4'],
},
{
id: 'demo-tom-5',
category: 'Zugriffskontrolle',
name: 'Berechtigungskonzept-Review',
description: 'Halbjährliche Überprüfung aller Berechtigungen durch die jeweiligen Fachbereichsleiter mit dokumentierter Rezertifizierung.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Fachbereichsleiter',
implementationDate: new Date('2025-02-01'),
reviewDate: new Date('2026-02-01'),
evidence: ['demo-evi-tom-5'],
},
// Verschlüsselung
{
id: 'demo-tom-6',
category: 'Verschlüsselung',
name: 'Datenverschlüsselung at Rest',
description: 'AES-256 Verschlüsselung aller personenbezogenen Daten in Datenbanken und Dateisystemen. Key Management über HSM.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-15'),
reviewDate: new Date('2026-01-15'),
evidence: ['demo-evi-tom-6'],
},
{
id: 'demo-tom-7',
category: 'Verschlüsselung',
name: 'Transportverschlüsselung',
description: 'TLS 1.3 für alle externen Verbindungen, mTLS für interne Service-Kommunikation. Regelmäßige Überprüfung der Cipher Suites.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-7'],
},
// Pseudonymisierung
{
id: 'demo-tom-8',
category: 'Pseudonymisierung',
name: 'Pseudonymisierungs-Pipeline',
description: 'Automatisierte Pseudonymisierung von Daten vor der Verarbeitung in Analytics-Systemen. Reversible Zuordnung nur durch autorisierten Prozess.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Data Engineering',
implementationDate: new Date('2025-05-01'),
reviewDate: new Date('2026-05-01'),
evidence: ['demo-evi-tom-8'],
},
// Integrität
{
id: 'demo-tom-9',
category: 'Integrität',
name: 'Datenintegritätsprüfung',
description: 'Checksummen-Validierung bei allen Datentransfers, Hash-Verifikation gespeicherter Daten, automatische Alerts bei Abweichungen.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-03-01'),
reviewDate: new Date('2026-03-01'),
evidence: ['demo-evi-tom-9'],
},
{
id: 'demo-tom-10',
category: 'Integrität',
name: 'Change Management',
description: 'Dokumentierter Change-Prozess mit Vier-Augen-Prinzip für alle Änderungen an produktiven Systemen. CAB-Freigabe für kritische Changes.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Leitung',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-10'],
},
// Verfügbarkeit
{
id: 'demo-tom-11',
category: 'Verfügbarkeit',
name: 'Disaster Recovery Plan',
description: 'Dokumentierter und getesteter DR-Plan mit RTO <4h und RPO <1h. Jährliche DR-Tests mit Dokumentation.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Leitung',
implementationDate: new Date('2025-02-01'),
reviewDate: new Date('2026-02-01'),
evidence: ['demo-evi-tom-11'],
},
{
id: 'demo-tom-12',
category: 'Verfügbarkeit',
name: 'High Availability Cluster',
description: 'Aktiv-Aktiv-Cluster für alle kritischen Systeme mit automatischem Failover. 99,9% Verfügbarkeits-SLA.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-12'],
},
// Belastbarkeit
{
id: 'demo-tom-13',
category: 'Belastbarkeit',
name: 'Load Balancing & Auto-Scaling',
description: 'Dynamische Skalierung basierend auf Last-Metriken. Load Balancer mit Health Checks und automatischer Traffic-Umleitung.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-04-01'),
reviewDate: new Date('2026-04-01'),
evidence: ['demo-evi-tom-13'],
},
{
id: 'demo-tom-14',
category: 'Belastbarkeit',
name: 'DDoS-Schutz',
description: 'Cloudbasierter DDoS-Schutz mit automatischer Traffic-Filterung. Kapazität für 10x Normal-Traffic.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-14'],
},
// Wiederherstellbarkeit
{
id: 'demo-tom-15',
category: 'Wiederherstellbarkeit',
name: 'Backup-Strategie',
description: '3-2-1 Backup-Strategie: 3 Kopien, 2 verschiedene Medien, 1 Offsite. Tägliche inkrementelle, wöchentliche Vollbackups.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-15'],
},
{
id: 'demo-tom-16',
category: 'Wiederherstellbarkeit',
name: 'Restore-Tests',
description: 'Monatliche Restore-Tests mit zufällig ausgewählten Daten. Dokumentation der Recovery-Zeit und Vollständigkeit.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-02-01'),
reviewDate: new Date('2026-02-01'),
evidence: ['demo-evi-tom-16'],
},
// Überprüfung & Bewertung
{
id: 'demo-tom-17',
category: 'Überprüfung & Bewertung',
name: 'Penetration Tests',
description: 'Jährliche externe Penetration Tests durch zertifizierte Dienstleister. Zusätzliche Tests nach größeren Änderungen.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-03-01'),
reviewDate: new Date('2026-03-01'),
evidence: ['demo-evi-tom-17'],
},
{
id: 'demo-tom-18',
category: 'Überprüfung & Bewertung',
name: 'Security Awareness Training',
description: 'Verpflichtendes Security-Training für alle Mitarbeiter bei Einstellung und jährlich. Phishing-Simulationen quartalsweise.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'MEDIUM',
responsiblePerson: 'HR / IT-Sicherheit',
implementationDate: new Date('2025-01-15'),
reviewDate: new Date('2026-01-15'),
evidence: ['demo-evi-tom-18'],
},
// KI-spezifische TOMs
{
id: 'demo-tom-19',
category: 'KI-Governance',
name: 'Model Governance Framework',
description: 'Dokumentierter Prozess für Entwicklung, Test, Deployment und Monitoring von KI-Modellen. Model Cards für alle produktiven Modelle.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Data Science Lead',
implementationDate: new Date('2025-06-01'),
reviewDate: new Date('2026-06-01'),
evidence: ['demo-evi-tom-19'],
},
{
id: 'demo-tom-20',
category: 'KI-Governance',
name: 'Bias Detection & Monitoring',
description: 'Automatisiertes Monitoring der Modell-Outputs auf Bias. Alerting bei signifikanten Abweichungen von Fairness-Metriken.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Data Science Lead',
implementationDate: new Date('2025-07-01'),
reviewDate: new Date('2026-07-01'),
evidence: ['demo-evi-tom-20'],
},
]
export function getDemoTOMs(): TOM[] {
return DEMO_TOMS.map(tom => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
}))
}
@@ -0,0 +1,85 @@
/**
* Demo Use Cases for AI Compliance SDK
*/
import { UseCaseAssessment, AssessmentResult } from '../types'
export const DEMO_USE_CASES: UseCaseAssessment[] = [
{
id: 'demo-uc-1',
name: 'KI-gestützte Kundenanalyse',
description: 'Analyse von Kundenverhalten und Präferenzen mittels Machine Learning zur Personalisierung von Angeboten und Verbesserung des Customer Lifetime Value. Das System verarbeitet Transaktionsdaten, Clickstreams und demographische Informationen.',
category: 'Marketing',
stepsCompleted: 5,
steps: [
{ id: 'uc1-step-1', name: 'Grunddaten', completed: true, data: { type: 'customer-analytics', department: 'Marketing' } },
{ id: 'uc1-step-2', name: 'Datenquellen', completed: true, data: { sources: ['CRM', 'Webshop', 'Newsletter'] } },
{ id: 'uc1-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['Clustering', 'Recommender', 'Churn-Prediction'] } },
{ id: 'uc1-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
{ id: 'uc1-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
],
assessmentResult: {
riskLevel: 'HIGH',
applicableRegulations: ['DSGVO', 'AI Act', 'TTDSG'],
recommendedControls: ['Einwilligungsmanagement', 'Profilbildungstransparenz', 'Opt-out-Mechanismus'],
dsfaRequired: true,
aiActClassification: 'LIMITED',
},
createdAt: new Date('2026-01-15'),
updatedAt: new Date('2026-02-01'),
},
{
id: 'demo-uc-2',
name: 'Automatisierte Bewerbungsvorauswahl',
description: 'KI-System zur Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse, Qualifikationsabgleich und Erfahrungsbewertung. Ziel ist die Effizienzsteigerung im Recruiting-Prozess bei gleichzeitiger Gewährleistung von Fairness.',
category: 'HR',
stepsCompleted: 5,
steps: [
{ id: 'uc2-step-1', name: 'Grunddaten', completed: true, data: { type: 'hr-screening', department: 'Personal' } },
{ id: 'uc2-step-2', name: 'Datenquellen', completed: true, data: { sources: ['Bewerbungsportal', 'LinkedIn', 'XING'] } },
{ id: 'uc2-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['NLP', 'Matching', 'Scoring'] } },
{ id: 'uc2-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Bewerber'] } },
{ id: 'uc2-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
],
assessmentResult: {
riskLevel: 'HIGH',
applicableRegulations: ['DSGVO', 'AI Act', 'AGG'],
recommendedControls: ['Bias-Monitoring', 'Human-in-the-Loop', 'Transparenzpflichten'],
dsfaRequired: true,
aiActClassification: 'HIGH',
},
createdAt: new Date('2026-01-20'),
updatedAt: new Date('2026-02-02'),
},
{
id: 'demo-uc-3',
name: 'Chatbot für Kundenservice',
description: 'Konversationeller KI-Assistent für die automatisierte Beantwortung von Kundenanfragen im First-Level-Support. Basiert auf Large Language Models mit firmeneigenem Wissen.',
category: 'Kundenservice',
stepsCompleted: 5,
steps: [
{ id: 'uc3-step-1', name: 'Grunddaten', completed: true, data: { type: 'chatbot', department: 'Support' } },
{ id: 'uc3-step-2', name: 'Datenquellen', completed: true, data: { sources: ['FAQ', 'Wissensdatenbank', 'Ticketsystem'] } },
{ id: 'uc3-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['LLM', 'RAG', 'Intent-Classification'] } },
{ id: 'uc3-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
{ id: 'uc3-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'MEDIUM' } },
],
assessmentResult: {
riskLevel: 'MEDIUM',
applicableRegulations: ['DSGVO', 'AI Act'],
recommendedControls: ['KI-Kennzeichnung', 'Übergabe an Menschen', 'Datensparsamkeit'],
dsfaRequired: false,
aiActClassification: 'LIMITED',
},
createdAt: new Date('2026-01-25'),
updatedAt: new Date('2026-02-03'),
},
]
export function getDemoUseCases(): UseCaseAssessment[] {
return DEMO_USE_CASES.map(uc => ({
...uc,
createdAt: new Date(uc.createdAt),
updatedAt: new Date(uc.updatedAt),
}))
}
+316
View File
@@ -0,0 +1,316 @@
/**
* Demo VVT (Verarbeitungsverzeichnis / Processing Activities Register) for AI Compliance SDK
* Art. 30 DSGVO - These are seed data structures - actual data is stored in database
*/
import { ProcessingActivity, RetentionPolicy } from '../types'
export const DEMO_PROCESSING_ACTIVITIES: ProcessingActivity[] = [
{
id: 'demo-pa-1',
name: 'KI-gestützte Kundenanalyse',
purpose: 'Analyse von Kundenverhalten und Präferenzen zur Personalisierung von Angeboten, Churn-Prediction und Marketing-Optimierung',
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse) / Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweitertes Profiling)',
dataCategories: [
'Stammdaten (Name, Adresse, E-Mail, Telefon)',
'Transaktionsdaten (Käufe, Bestellungen, Retouren)',
'Nutzungsdaten (Clickstreams, Seitenaufrufe, Verweildauer)',
'Demographische Daten (Alter, Geschlecht, PLZ-Region)',
],
dataSubjects: [
'Bestandskunden (ca. 250.000 aktive)',
'Registrierte Interessenten (ca. 100.000)',
'Newsletter-Abonnenten (ca. 180.000)',
],
recipients: [
'Interne Fachabteilungen (Marketing, Vertrieb)',
'E-Mail-Marketing-Dienstleister (AV-Vertrag vorhanden)',
'Cloud-Infrastruktur-Anbieter (AV-Vertrag vorhanden)',
],
thirdCountryTransfers: false,
retentionPeriod: '3 Jahre nach letzter Aktivität, danach Anonymisierung',
technicalMeasures: [
'AES-256 Verschlüsselung',
'Pseudonymisierung',
'Zugriffskontrolle mit MFA',
'Audit-Logging',
],
organizationalMeasures: [
'Rollenbasiertes Berechtigungskonzept',
'Verpflichtung auf Datengeheimnis',
'Regelmäßige Datenschutzschulungen',
'Dokumentierte Prozesse',
],
},
{
id: 'demo-pa-2',
name: 'Automatisierte Bewerbungsvorauswahl',
purpose: 'KI-gestützte Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse und Qualifikationsabgleich zur Effizienzsteigerung im Recruiting',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (vorvertragliche Maßnahmen) / § 26 BDSG (Beschäftigungsverhältnis)',
dataCategories: [
'Bewerberdaten (Name, Kontakt, Geburtsdatum)',
'Qualifikationen (Ausbildung, Berufserfahrung, Zertifikate)',
'Lebenslaufdaten (Werdegang, Fähigkeiten)',
'Bewerbungsschreiben',
],
dataSubjects: [
'Bewerber auf offene Stellen',
'Initiativbewerber',
],
recipients: [
'HR-Abteilung',
'Fachabteilungsleiter (nur finale Kandidaten)',
'Betriebsrat (Einsichtnahme möglich)',
],
thirdCountryTransfers: false,
retentionPeriod: '6 Monate nach Abschluss des Bewerbungsverfahrens (bei Ablehnung), länger nur mit Einwilligung für Talentpool',
technicalMeasures: [
'Verschlüsselte Speicherung',
'Zugangsbeschränkung auf HR',
'Automatische Löschroutinen',
'Bias-Monitoring',
],
organizationalMeasures: [
'Human-in-the-Loop für finale Entscheidungen',
'Dokumentierte KI-Entscheidungskriterien',
'Transparente Information an Bewerber',
'Regelmäßige Fairness-Audits',
],
},
{
id: 'demo-pa-3',
name: 'Kundenservice-Chatbot',
purpose: 'Automatisierte Beantwortung von Kundenanfragen im First-Level-Support mittels KI-gestütztem Dialogsystem',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) / Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)',
dataCategories: [
'Kundenstammdaten (zur Identifikation)',
'Kommunikationsinhalte (Chat-Verläufe)',
'Technische Daten (Session-ID, Zeitstempel)',
'Serviceanfragen und deren Lösungen',
],
dataSubjects: [
'Kunden mit aktiven Verträgen',
'Interessenten mit Anfragen',
],
recipients: [
'Kundenservice-Team (bei Eskalation)',
'Cloud-Anbieter (Hosting, AV-Vertrag)',
],
thirdCountryTransfers: false,
retentionPeriod: '2 Jahre für Chat-Verläufe, danach Anonymisierung für Training',
technicalMeasures: [
'TLS-Verschlüsselung',
'Keine Speicherung sensitiver Daten im Chat',
'Automatische PII-Erkennung und Maskierung',
],
organizationalMeasures: [
'Klare KI-Kennzeichnung gegenüber Kunden',
'Jederzeit Übergabe an Menschen möglich',
'Schulung des Eskalations-Teams',
],
},
{
id: 'demo-pa-4',
name: 'Mitarbeiterverwaltung',
purpose: 'Verwaltung von Personalstammdaten, Gehaltsabrechnung, Zeiterfassung und Personalentwicklung',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Arbeitsvertrag) / § 26 BDSG (Beschäftigungsverhältnis) / gesetzliche Pflichten (Steuer, SV)',
dataCategories: [
'Personalstammdaten (Name, Adresse, Geburtsdatum, SV-Nr.)',
'Vertragsdaten (Arbeitsvertrag, Gehalt, Arbeitszeit)',
'Zeiterfassungsdaten',
'Leistungsbeurteilungen',
'Bankverbindung',
],
dataSubjects: [
'Aktive Mitarbeiter',
'Ehemalige Mitarbeiter (Archiv)',
],
recipients: [
'HR-Abteilung',
'Lohnbuchhaltung / Steuerberater',
'Sozialversicherungsträger',
'Finanzamt',
],
thirdCountryTransfers: false,
retentionPeriod: '10 Jahre nach Ausscheiden (steuerliche Aufbewahrungspflichten)',
technicalMeasures: [
'Verschlüsselte Speicherung',
'Strenge Zugriffskontrolle',
'Getrennte Systeme für verschiedene Datenkategorien',
],
organizationalMeasures: [
'Need-to-know-Prinzip',
'Dokumentierte Prozesse',
'Betriebsvereinbarung zur Datenverarbeitung',
],
},
{
id: 'demo-pa-5',
name: 'Website-Analyse und Marketing',
purpose: 'Analyse des Nutzerverhaltens auf der Website zur Optimierung der User Experience und für personalisierte Marketing-Maßnahmen',
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung via Cookie-Banner)',
dataCategories: [
'Pseudonymisierte Nutzungsdaten',
'Cookie-IDs und Tracking-Identifier',
'Geräteinformationen',
'Interaktionsdaten (Klicks, Scrollverhalten)',
],
dataSubjects: [
'Website-Besucher (nur mit Einwilligung)',
],
recipients: [
'Marketing-Team',
'Analytics-Anbieter (AV-Vertrag)',
'Advertising-Partner (nur mit erweiterter Einwilligung)',
],
thirdCountryTransfers: true,
retentionPeriod: '13 Monate für Analytics-Daten, Cookie-Laufzeit max. 12 Monate',
technicalMeasures: [
'IP-Anonymisierung',
'Secure Cookies',
'Consent-Management-System',
],
organizationalMeasures: [
'Transparente Cookie-Richtlinie',
'Einfacher Widerruf möglich',
'Regelmäßige Cookie-Audits',
],
},
{
id: 'demo-pa-6',
name: 'Videoüberwachung',
purpose: 'Schutz von Eigentum und Personen, Prävention und Aufklärung von Straftaten in Geschäftsräumen',
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an Sicherheit)',
dataCategories: [
'Videoaufnahmen',
'Zeitstempel',
'Aufnahmeort',
],
dataSubjects: [
'Mitarbeiter in überwachten Bereichen',
'Besucher und Kunden',
'Lieferanten',
],
recipients: [
'Sicherheitspersonal',
'Geschäftsleitung (bei Vorfällen)',
'Strafverfolgungsbehörden (auf Anforderung)',
],
thirdCountryTransfers: false,
retentionPeriod: '72 Stunden, bei Vorfällen bis zur Abschluss der Untersuchung',
technicalMeasures: [
'Verschlüsselte Speicherung',
'Automatische Löschung nach Fristablauf',
'Eingeschränkter Zugriff',
],
organizationalMeasures: [
'Beschilderung der überwachten Bereiche',
'Betriebsvereinbarung mit Betriebsrat',
'Dokumentiertes Einsichtsprotokoll',
],
},
]
export const DEMO_RETENTION_POLICIES: RetentionPolicy[] = [
{
id: 'demo-ret-1',
dataCategory: 'Kundenstammdaten',
description: 'Grundlegende Daten zur Kundenidentifikation (Name, Adresse, Kontaktdaten)',
legalBasis: 'Handels- und steuerrechtliche Aufbewahrungspflichten (§ 257 HGB, § 147 AO)',
retentionPeriod: '10 Jahre nach Vertragsende',
deletionMethod: 'Sichere Löschung mit Protokollierung, bei Papier: Aktenvernichtung DIN 66399',
exceptions: [
'Laufende Rechtsstreitigkeiten',
'Offene Forderungen',
],
},
{
id: 'demo-ret-2',
dataCategory: 'Transaktionsdaten',
description: 'Bestellungen, Rechnungen, Zahlungen, Lieferungen',
legalBasis: '§ 257 HGB, § 147 AO (handels- und steuerrechtliche Aufbewahrung)',
retentionPeriod: '10 Jahre ab Ende des Geschäftsjahres',
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
exceptions: [
'Garantiefälle (bis Ende der Garantiezeit)',
'Prüfungen durch Finanzbehörden',
],
},
{
id: 'demo-ret-3',
dataCategory: 'Bewerberdaten',
description: 'Lebenslauf, Anschreiben, Zeugnisse, Korrespondenz',
legalBasis: 'AGG (Diskriminierungsschutz) / § 26 BDSG',
retentionPeriod: '6 Monate nach Abschluss des Verfahrens',
deletionMethod: 'Sichere Löschung, bei Papier: Aktenvernichtung',
exceptions: [
'Aufnahme in Talentpool (mit Einwilligung): 2 Jahre',
'Diskriminierungsklagen: bis Abschluss',
],
},
{
id: 'demo-ret-4',
dataCategory: 'Personalakten',
description: 'Arbeitsverträge, Gehaltsabrechnungen, Beurteilungen, Abmahnungen',
legalBasis: '§ 257 HGB, § 147 AO, Sozialversicherungsrecht',
retentionPeriod: '10 Jahre nach Ausscheiden (teilweise 30 Jahre für Rentenansprüche)',
deletionMethod: 'Sichere Löschung mit Dokumentation',
exceptions: [
'Arbeitsrechtliche Streitigkeiten',
'Rentenversicherungsnachweise (lebenslang empfohlen)',
],
},
{
id: 'demo-ret-5',
dataCategory: 'Marketing-Profile',
description: 'Analysedaten, Segmentierungen, Präferenzen, Kaufhistorie',
legalBasis: 'Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)',
retentionPeriod: '3 Jahre nach letzter Aktivität, dann Anonymisierung',
deletionMethod: 'Pseudonymisierung → Anonymisierung → Löschung',
exceptions: [
'Widerruf der Einwilligung (sofortige Löschung)',
],
},
{
id: 'demo-ret-6',
dataCategory: 'Videoaufnahmen',
description: 'Aufnahmen der Sicherheitskameras',
legalBasis: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO)',
retentionPeriod: '72 Stunden',
deletionMethod: 'Automatisches Überschreiben',
exceptions: [
'Sicherheitsvorfälle (bis Abschluss der Untersuchung)',
'Anforderung durch Strafverfolgungsbehörden',
],
},
{
id: 'demo-ret-7',
dataCategory: 'KI-Trainingsdaten',
description: 'Anonymisierte Datensätze für Modell-Training',
legalBasis: 'Berechtigtes Interesse / ursprüngliche Zweckbindung (bei Kompatibilität)',
retentionPeriod: 'Solange Modell aktiv, danach Löschung mit Modell-Archivierung',
deletionMethod: 'Sichere Löschung bei Modell-Retirement',
exceptions: [
'Audit-Trail für Modell-Herkunft (anonymisierte Metadaten)',
],
},
{
id: 'demo-ret-8',
dataCategory: 'Audit-Logs',
description: 'Protokolle von Datenzugriffen und Systemereignissen',
legalBasis: 'Nachweispflichten DSGVO, Compliance-Anforderungen',
retentionPeriod: '10 Jahre',
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
exceptions: [
'Laufende Untersuchungen oder Audits',
],
},
]
export function getDemoProcessingActivities(): ProcessingActivity[] {
return DEMO_PROCESSING_ACTIVITIES
}
export function getDemoRetentionPolicies(): RetentionPolicy[] {
return DEMO_RETENTION_POLICIES
}
@@ -0,0 +1,548 @@
/**
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
* in den Dokumentengenerator.
*
* Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf
* den vom Benutzer ausgewählten Datenpunkten.
*/
import {
DataPoint,
DataPointCategory,
LegalBasis,
RetentionPeriod,
RiskLevel,
CATEGORY_METADATA,
LEGAL_BASIS_INFO,
RETENTION_PERIOD_INFO,
RISK_LEVEL_STYLING,
LocalizedText,
SupportedLanguage
} from '@/lib/sdk/einwilligungen/types'
// =============================================================================
// TYPES
// =============================================================================
/**
* Sprach-Option für alle Helper-Funktionen
*/
export type Language = SupportedLanguage
/**
* Generierte Platzhalter-Map für den Dokumentengenerator
*/
export interface DataPointPlaceholders {
'[DATENPUNKTE_COUNT]': string
'[DATENPUNKTE_LIST]': string
'[DATENPUNKTE_TABLE]': string
'[VERARBEITUNGSZWECKE]': string
'[RECHTSGRUNDLAGEN]': string
'[SPEICHERFRISTEN]': string
'[EMPFAENGER]': string
'[BESONDERE_KATEGORIEN]': string
'[DRITTLAND_TRANSFERS]': string
'[RISIKO_ZUSAMMENFASSUNG]': string
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Extrahiert Text aus LocalizedText basierend auf Sprache
*/
function getText(text: LocalizedText, lang: Language): string {
return text[lang] || text.de
}
/**
* Generiert eine Markdown-Tabelle der Datenpunkte
*
* @param dataPoints - Liste der ausgewählten Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Markdown-Tabelle als String
*/
export function generateDataPointsTable(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
if (dataPoints.length === 0) {
return lang === 'de'
? '*Keine Datenpunkte ausgewählt.*'
: '*No data points selected.*'
}
const header = lang === 'de'
? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
: '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
const separator = '|------------|-----------|-------|-----------------|---------------|'
const rows = dataPoints.map(dp => {
const category = CATEGORY_METADATA[dp.category]
const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
const name = getText(dp.name, lang)
const categoryName = getText(category.name, lang)
const purpose = getText(dp.purpose, lang)
const legalBasisName = getText(legalBasis.name, lang)
const retentionLabel = getText(retention.label, lang)
// Truncate long texts for table readability
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
}).join('\n')
return `${header}\n${separator}\n${rows}`
}
/**
* Gruppiert Datenpunkte nach Speicherfrist
*
* @param dataPoints - Liste der Datenpunkte
* @returns Record mit Speicherfrist als Key und Datenpunkten als Value
*/
export function groupByRetention(
dataPoints: DataPoint[]
): Record<RetentionPeriod, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.retentionPeriod
if (!acc[key]) {
acc[key] = []
}
acc[key].push(dp)
return acc
}, {} as Record<RetentionPeriod, DataPoint[]>)
}
/**
* Gruppiert Datenpunkte nach Kategorie
*
* @param dataPoints - Liste der Datenpunkte
* @returns Record mit Kategorie als Key und Datenpunkten als Value
*/
export function groupByCategory(
dataPoints: DataPoint[]
): Record<DataPointCategory, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.category
if (!acc[key]) {
acc[key] = []
}
acc[key].push(dp)
return acc
}, {} as Record<DataPointCategory, DataPoint[]>)
}
/**
* Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO)
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten)
*/
export function generateSpecialCategorySection(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const special = dataPoints.filter(dp => dp.isSpecialCategory)
if (special.length === 0) {
return ''
}
if (lang === 'de') {
const items = special.map(dp =>
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
).join('\n')
return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
${items}
Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.`
} else {
const items = special.map(dp =>
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
).join('\n')
return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
We process the following special categories of personal data:
${items}
Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
}
}
/**
* Generiert Liste aller eindeutigen Verarbeitungszwecke
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Kommaseparierte Liste der Zwecke
*/
export function generatePurposesList(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const purposes = new Set<string>()
dataPoints.forEach(dp => {
purposes.add(getText(dp.purpose, lang))
})
return [...purposes].join(', ')
}
/**
* Generiert Liste aller verwendeten Rechtsgrundlagen
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Formatierte Liste der Rechtsgrundlagen
*/
export function generateLegalBasisList(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const bases = new Set<LegalBasis>()
dataPoints.forEach(dp => {
bases.add(dp.legalBasis)
})
return [...bases].map(basis => {
const info = LEGAL_BASIS_INFO[basis]
return `${info.article} (${getText(info.name, lang)})`
}).join(', ')
}
/**
* Generiert Liste aller Speicherfristen gruppiert
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien
*/
export function generateRetentionList(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const grouped = groupByRetention(dataPoints)
const entries: string[] = []
for (const [period, points] of Object.entries(grouped)) {
const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
}
return entries.join('; ')
}
/**
* Generiert Liste aller Empfänger/Drittparteien
*
* @param dataPoints - Liste der Datenpunkte
* @returns Kommaseparierte Liste der Empfänger
*/
export function generateRecipientsList(dataPoints: DataPoint[]): string {
const recipients = new Set<string>()
dataPoints.forEach(dp => {
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
})
if (recipients.size === 0) {
return ''
}
return [...recipients].join(', ')
}
/**
* Generiert Abschnitt für Drittland-Übermittlungen
*
* @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true
* @param lang - Sprache für die Ausgabe
* @returns Markdown-Abschnitt als String
*/
export function generateThirdCountrySection(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
// Note: We assume dataPoints have been filtered for thirdCountryTransfer
// The actual flag would need to be added to the DataPoint interface
// For now, we check if any thirdPartyRecipients suggest third country
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
const thirdCountryPoints = dataPoints.filter(dp =>
dp.thirdPartyRecipients?.some(r =>
thirdCountryIndicators.some(indicator =>
r.toLowerCase().includes(indicator.toLowerCase())
)
)
)
if (thirdCountryPoints.length === 0) {
return ''
}
const recipients = new Set<string>()
thirdCountryPoints.forEach(dp => {
dp.thirdPartyRecipients?.forEach(r => {
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
recipients.add(r)
}
})
})
if (lang === 'de') {
return `## Übermittlung in Drittländer
Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR):
${[...recipients].map(r => `- ${r}`).join('\n')}
Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
} else {
return `## Transfers to Third Countries
We transfer personal data to the following recipients in third countries (outside the EU/EEA):
${[...recipients].map(r => `- ${r}`).join('\n')}
The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
}
}
/**
* Generiert Risiko-Zusammenfassung
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Formatierte Risiko-Zusammenfassung
*/
export function generateRiskSummary(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const riskCounts: Record<RiskLevel, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0
}
dataPoints.forEach(dp => {
riskCounts[dp.riskLevel]++
})
const parts = Object.entries(riskCounts)
.filter(([, count]) => count > 0)
.map(([level, count]) => {
const styling = RISK_LEVEL_STYLING[level as RiskLevel]
return `${count} ${getText(styling.label, lang).toLowerCase()}`
})
return parts.join(', ')
}
/**
* Generiert alle Platzhalter für den Dokumentengenerator
*
* @param dataPoints - Liste der ausgewählten Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Objekt mit allen Platzhaltern
*/
export function generateAllPlaceholders(
dataPoints: DataPoint[],
lang: Language = 'de'
): DataPointPlaceholders {
return {
'[DATENPUNKTE_COUNT]': String(dataPoints.length),
'[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
'[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
'[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
'[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
'[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
'[EMPFAENGER]': generateRecipientsList(dataPoints),
'[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
'[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
'[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
}
}
// =============================================================================
// VALIDATION HELPERS
// =============================================================================
/**
* Validierungswarnung für den Dokumentengenerator
*/
export interface ValidationWarning {
type: 'error' | 'warning' | 'info'
code: string
message: string
suggestion: string
affectedDataPoints?: DataPoint[]
}
/**
* Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns ValidationWarning oder null
*/
export function checkSpecialCategoriesWarning(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning | null {
const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
if (specialCategories.length === 0) {
return null
}
const hasSection = lang === 'de'
? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
: documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
if (!hasSection) {
return {
type: 'error',
code: 'MISSING_ART9_SECTION',
message: lang === 'de'
? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.`
: `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
suggestion: lang === 'de'
? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
: 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
affectedDataPoints: specialCategories
}
}
return null
}
/**
* Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns ValidationWarning oder null
*/
export function checkThirdCountryWarning(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning | null {
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
const thirdCountryPoints = dataPoints.filter(dp =>
dp.thirdPartyRecipients?.some(r =>
thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
)
)
if (thirdCountryPoints.length === 0) {
return null
}
const hasSCCMention = lang === 'de'
? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
: documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
if (!hasSCCMention) {
return {
type: 'warning',
code: 'MISSING_SCC_SECTION',
message: lang === 'de'
? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.`
: `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
suggestion: lang === 'de'
? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
: 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
affectedDataPoints: thirdCountryPoints
}
}
return null
}
/**
* Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns ValidationWarning oder null
*/
export function checkExplicitConsentWarning(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning | null {
const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
if (explicitConsentPoints.length === 0) {
return null
}
const hasConsentSection = lang === 'de'
? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
: documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
if (!hasConsentSection) {
return {
type: 'warning',
code: 'MISSING_CONSENT_SECTION',
message: lang === 'de'
? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
: `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
suggestion: lang === 'de'
? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
: 'Add a section about the right to withdraw consent.',
affectedDataPoints: explicitConsentPoints
}
}
return null
}
/**
* Führt alle Validierungsprüfungen durch
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns Array aller Warnungen
*/
export function validateDocument(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning[] {
const warnings: ValidationWarning[] = []
const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
if (specialCatWarning) warnings.push(specialCatWarning)
const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
if (thirdCountryWarning) warnings.push(thirdCountryWarning)
const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
if (consentWarning) warnings.push(consentWarning)
return warnings
}
@@ -0,0 +1,8 @@
/**
* Document Generator Library
*
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
* in den Dokumentengenerator.
*/
export * from './datapoint-helpers'
@@ -0,0 +1,224 @@
import { ConstraintEnforcer } from '../constraint-enforcer'
import type { ScopeDecision } from '../../compliance-scope-types'
describe('ConstraintEnforcer', () => {
const enforcer = new ConstraintEnforcer()
// Helper: minimal valid ScopeDecision
function makeDecision(overrides: Partial<ScopeDecision> = {}): ScopeDecision {
return {
id: 'test-decision',
determinedLevel: 'L2',
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
triggeredHardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '3h', triggeredBy: [] },
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
],
riskFlags: [],
gaps: [],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as ScopeDecision
}
describe('check - no decision', () => {
it('should allow basic documents (vvt, tom, dsi) without decision', () => {
const result = enforcer.check('vvt', null)
expect(result.allowed).toBe(true)
expect(result.adjustments.length).toBeGreaterThan(0)
expect(result.checkedRules).toContain('RULE-NO-DECISION')
})
it('should allow tom without decision', () => {
const result = enforcer.check('tom', null)
expect(result.allowed).toBe(true)
})
it('should allow dsi without decision', () => {
const result = enforcer.check('dsi', null)
expect(result.allowed).toBe(true)
})
it('should block non-basic documents without decision', () => {
const result = enforcer.check('dsfa', null)
expect(result.allowed).toBe(false)
expect(result.violations.length).toBeGreaterThan(0)
})
it('should block av_vertrag without decision', () => {
const result = enforcer.check('av_vertrag', null)
expect(result.allowed).toBe(false)
})
})
describe('check - RULE-DOC-REQUIRED', () => {
it('should allow required documents', () => {
const decision = makeDecision()
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
})
it('should warn but allow optional documents', () => {
const decision = makeDecision({
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
],
})
const result = enforcer.check('dsfa', decision)
expect(result.allowed).toBe(true) // Only warns, does not block
expect(result.adjustments.some(a => a.includes('nicht als Pflicht'))).toBe(true)
})
})
describe('check - RULE-DEPTH-MATCH', () => {
it('should block when requested depth exceeds determined level', () => {
const decision = makeDecision({ determinedLevel: 'L2' })
const result = enforcer.check('vvt', decision, 'L4')
expect(result.allowed).toBe(false)
expect(result.violations.some(v => v.includes('ueberschreitet'))).toBe(true)
})
it('should allow when requested depth matches level', () => {
const decision = makeDecision({ determinedLevel: 'L2' })
const result = enforcer.check('vvt', decision, 'L2')
expect(result.allowed).toBe(true)
})
it('should adjust when requested depth is below level', () => {
const decision = makeDecision({ determinedLevel: 'L3' })
const result = enforcer.check('vvt', decision, 'L1')
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('angehoben'))).toBe(true)
})
it('should allow without requested depth level', () => {
const decision = makeDecision({ determinedLevel: 'L3' })
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
})
})
describe('check - RULE-DSFA-ENFORCEMENT', () => {
it('should note when DSFA is not required but requested', () => {
const decision = makeDecision({ determinedLevel: 'L2' })
const result = enforcer.check('dsfa', decision)
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('nicht verpflichtend'))).toBe(true)
})
it('should allow DSFA when hard triggers require it', () => {
const decision = makeDecision({
determinedLevel: 'L3',
triggeredHardTriggers: [{
rule: {
id: 'HT-ART9',
label: 'Art. 9 Daten',
description: '',
conditionField: '',
conditionOperator: 'EQUALS' as const,
conditionValue: null,
minimumLevel: 'L3',
mandatoryDocuments: ['dsfa'],
dsfaRequired: true,
legalReference: 'Art. 35 DSGVO',
},
matchedValue: null,
explanation: 'Art. 9 Daten verarbeitet',
}],
})
const result = enforcer.check('dsfa', decision)
expect(result.allowed).toBe(true)
})
it('should warn about DSFA when drafting non-DSFA but DSFA is required', () => {
const decision = makeDecision({
determinedLevel: 'L3',
triggeredHardTriggers: [{
rule: {
id: 'HT-ART9',
label: 'Art. 9 Daten',
description: '',
conditionField: '',
conditionOperator: 'EQUALS' as const,
conditionValue: null,
minimumLevel: 'L3',
mandatoryDocuments: ['dsfa'],
dsfaRequired: true,
legalReference: 'Art. 35 DSGVO',
},
matchedValue: null,
explanation: '',
}],
requiredDocuments: [
{ documentType: 'dsfa', label: 'DSFA', required: true, depth: 'Vollstaendig', detailItems: [], estimatedEffort: '8h', triggeredBy: [] },
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
],
})
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('DSFA') && a.includes('verpflichtend'))).toBe(true)
})
})
describe('check - RULE-RISK-FLAGS', () => {
it('should note critical risk flags', () => {
const decision = makeDecision({
riskFlags: [
{ id: 'rf-1', severity: 'CRITICAL', title: 'Offene Art. 9 Verarbeitung', description: '', recommendation: 'DSFA durchfuehren' },
{ id: 'rf-2', severity: 'HIGH', title: 'Fehlende Verschluesselung', description: '', recommendation: 'TOM erstellen' },
{ id: 'rf-3', severity: 'LOW', title: 'Dokumentation unvollstaendig', description: '', recommendation: '' },
],
})
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('2 kritische/hohe Risiko-Flags'))).toBe(true)
})
it('should not flag when no risk flags present', () => {
const decision = makeDecision({ riskFlags: [] })
const result = enforcer.check('vvt', decision)
expect(result.adjustments.every(a => !a.includes('Risiko-Flags'))).toBe(true)
})
})
describe('check - checkedRules tracking', () => {
it('should track all checked rules', () => {
const decision = makeDecision()
const result = enforcer.check('vvt', decision)
expect(result.checkedRules).toContain('RULE-DOC-REQUIRED')
expect(result.checkedRules).toContain('RULE-DEPTH-MATCH')
expect(result.checkedRules).toContain('RULE-DSFA-ENFORCEMENT')
expect(result.checkedRules).toContain('RULE-RISK-FLAGS')
expect(result.checkedRules).toContain('RULE-HARD-TRIGGER-CONSISTENCY')
})
})
describe('checkFromContext', () => {
it('should reconstruct decision from DraftContext and check', () => {
const context = {
decisions: {
level: 'L2' as const,
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
hardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt' as const, depth: 'Standard', detailItems: [] },
],
},
companyProfile: { name: 'Test GmbH', industry: 'IT', employeeCount: 50, businessModel: 'SaaS', isPublicSector: false },
constraints: {
depthRequirements: { required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h' },
riskFlags: [],
boundaries: [],
},
}
const result = enforcer.checkFromContext('vvt', context)
expect(result.allowed).toBe(true)
expect(result.checkedRules.length).toBeGreaterThan(0)
})
})
})
@@ -0,0 +1,153 @@
import { IntentClassifier } from '../intent-classifier'
describe('IntentClassifier', () => {
const classifier = new IntentClassifier()
describe('classify - Draft mode', () => {
it.each([
['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'],
['Generiere eine TOM-Dokumentation', 'draft'],
['Schreibe eine Datenschutzerklaerung', 'draft'],
['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'],
['Create a DSFA document', 'draft'],
['Draft a privacy policy for us', 'draft'],
['Neues VVT anlegen', 'draft'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.7)
})
})
describe('classify - Validate mode', () => {
it.each([
['Pruefe die Konsistenz meiner Dokumente', 'validate'],
['Ist mein VVT korrekt?', 'validate'],
['Validiere die TOM gegen das VVT', 'validate'],
['Check die Vollstaendigkeit', 'validate'],
['Stimmt das mit der DSFA ueberein?', 'validate'],
['Cross-Check VVT und TOM', 'validate'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.7)
})
})
describe('classify - Ask mode', () => {
it.each([
['Was fehlt noch in meinem Profil?', 'ask'],
['Zeige mir die Luecken', 'ask'],
['Welche Dokumente fehlen noch?', 'ask'],
['Was ist der naechste Schritt?', 'ask'],
['Welche Informationen brauche ich noch?', 'ask'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.6)
})
})
describe('classify - Explain mode (fallback)', () => {
it.each([
['Was ist DSGVO?', 'explain'],
['Erklaere mir Art. 30', 'explain'],
['Hallo', 'explain'],
['Danke fuer die Hilfe', 'explain'],
])('"%s" should classify as %s (fallback)', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
})
})
describe('classify - confidence thresholds', () => {
it('should have high confidence for clear draft intents', () => {
const result = classifier.classify('Erstelle ein neues VVT')
expect(result.confidence).toBeGreaterThanOrEqual(0.85)
})
it('should have lower confidence for ambiguous inputs', () => {
const result = classifier.classify('Hallo')
expect(result.confidence).toBeLessThan(0.6)
})
it('should boost confidence with document type detection', () => {
const withDoc = classifier.classify('Erstelle VVT')
const withoutDoc = classifier.classify('Erstelle etwas')
expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence)
})
it('should boost confidence with multiple pattern matches', () => {
const single = classifier.classify('Erstelle Dokument')
const multi = classifier.classify('Erstelle und generiere ein neues Dokument')
expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence)
})
})
describe('detectDocumentType', () => {
it.each([
['VVT erstellen', 'vvt'],
['Verarbeitungsverzeichnis', 'vvt'],
['Art. 30 Dokumentation', 'vvt'],
['TOM definieren', 'tom'],
['technisch organisatorische Massnahmen', 'tom'],
['Art. 32 Massnahmen', 'tom'],
['DSFA durchfuehren', 'dsfa'],
['Datenschutz-Folgenabschaetzung', 'dsfa'],
['Art. 35 Pruefung', 'dsfa'],
['DPIA erstellen', 'dsfa'],
['Datenschutzerklaerung', 'dsi'],
['Privacy Policy', 'dsi'],
['Art. 13 Information', 'dsi'],
['Loeschfristen definieren', 'lf'],
['Loeschkonzept erstellen', 'lf'],
['Retention Policy', 'lf'],
['Auftragsverarbeitung', 'av_vertrag'],
['AVV erstellen', 'av_vertrag'],
['Art. 28 Vertrag', 'av_vertrag'],
['Einwilligung einholen', 'einwilligung'],
['Consent Management', 'einwilligung'],
['Cookie Banner', 'einwilligung'],
])('"%s" should detect document type %s', (input, expectedType) => {
const result = classifier.detectDocumentType(input)
expect(result).toBe(expectedType)
})
it('should return undefined for unrecognized types', () => {
expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined()
expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined()
})
})
describe('classify - Umlaut handling', () => {
it('should handle German umlauts correctly', () => {
// With actual umlauts (ä, ö, ü)
const result1 = classifier.classify('Prüfe die Vollständigkeit')
expect(result1.mode).toBe('validate')
// With ae/oe/ue substitution
const result2 = classifier.classify('Pruefe die Vollstaendigkeit')
expect(result2.mode).toBe('validate')
})
it('should handle ß correctly', () => {
const result = classifier.classify('Schließe Lücken')
// Should still detect via normalized patterns
expect(result).toBeDefined()
})
})
describe('classify - combined mode + document type', () => {
it('should detect both mode and document type', () => {
const result = classifier.classify('Erstelle ein VVT fuer unsere Firma')
expect(result.mode).toBe('draft')
expect(result.detectedDocumentType).toBe('vvt')
})
it('should detect validate + document type', () => {
const result = classifier.classify('Pruefe mein TOM auf Konsistenz')
expect(result.mode).toBe('validate')
expect(result.detectedDocumentType).toBe('tom')
})
})
})
@@ -0,0 +1,311 @@
import { StateProjector } from '../state-projector'
import type { SDKState } from '../../types'
describe('StateProjector', () => {
const projector = new StateProjector()
// Helper: minimal SDKState
function makeState(overrides: Partial<SDKState> = {}): SDKState {
return {
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'user1',
subscription: 'PROFESSIONAL',
customerType: null,
companyProfile: null,
complianceScope: null,
currentPhase: 1,
currentStep: 'company-profile',
completedSteps: [],
checkpoints: {},
importedDocuments: [],
gapAnalysis: null,
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
...overrides,
} as SDKState
}
function makeDecisionState(level: string = 'L2'): SDKState {
return makeState({
companyProfile: {
companyName: 'Test GmbH',
industry: 'IT-Dienstleistung',
employeeCount: 50,
businessModel: 'SaaS',
isPublicSector: false,
} as any,
complianceScope: {
decision: {
id: 'dec-1',
determinedLevel: level,
scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 },
triggeredHardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] },
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] },
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
],
riskFlags: [
{ id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' },
],
gaps: [
{ id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] },
],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
answers: [],
} as any,
vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[],
toms: [],
retentionPolicies: [],
})
}
describe('projectForDraft', () => {
it('should return a DraftContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result).toHaveProperty('decisions')
expect(result).toHaveProperty('companyProfile')
expect(result).toHaveProperty('constraints')
expect(result.decisions.level).toBe('L2')
})
it('should project company profile', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.companyProfile.name).toBe('Test GmbH')
expect(result.companyProfile.industry).toBe('IT-Dienstleistung')
expect(result.companyProfile.employeeCount).toBe(50)
})
it('should provide defaults when no company profile', () => {
const state = makeState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.companyProfile.name).toBe('Unbekannt')
expect(result.companyProfile.industry).toBe('Unbekannt')
expect(result.companyProfile.employeeCount).toBe(0)
})
it('should extract constraints and depth requirements', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.constraints.depthRequirements).toBeDefined()
expect(result.constraints.boundaries.length).toBeGreaterThan(0)
})
it('should extract risk flags', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.constraints.riskFlags.length).toBe(1)
expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung')
})
it('should include existing document data when available', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.existingDocumentData).toBeDefined()
expect((result.existingDocumentData as any).totalCount).toBe(1)
})
it('should return undefined existingDocumentData when none exists', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'tom')
expect(result.existingDocumentData).toBeUndefined()
})
it('should filter required documents', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.decisions.requiredDocuments.length).toBe(3)
expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true)
})
it('should handle empty state gracefully', () => {
const state = makeState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.decisions.level).toBe('L1')
expect(result.decisions.hardTriggers).toEqual([])
expect(result.decisions.requiredDocuments).toEqual([])
})
})
describe('projectForAsk', () => {
it('should return a GapContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
expect(result).toHaveProperty('unansweredQuestions')
expect(result).toHaveProperty('gaps')
expect(result).toHaveProperty('missingDocuments')
})
it('should identify missing documents', () => {
const state = makeDecisionState()
// vvt exists, tom and lf are missing
const result = projector.projectForAsk(state)
expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true)
expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true)
})
it('should not list existing documents as missing', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
// vvt exists in state
expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false)
})
it('should include gaps from scope decision', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
expect(result.gaps.length).toBe(1)
expect(result.gaps[0].title).toBe('TOM fehlt')
})
it('should handle empty state', () => {
const state = makeState()
const result = projector.projectForAsk(state)
expect(result.gaps).toEqual([])
expect(result.missingDocuments).toEqual([])
})
})
describe('projectForValidate', () => {
it('should return a ValidationContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result).toHaveProperty('documents')
expect(result).toHaveProperty('crossReferences')
expect(result).toHaveProperty('scopeLevel')
expect(result).toHaveProperty('depthRequirements')
})
it('should include all requested document types', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.documents.length).toBe(2)
expect(result.documents.map(d => d.type)).toContain('vvt')
expect(result.documents.map(d => d.type)).toContain('tom')
})
it('should include cross-references', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result.crossReferences).toHaveProperty('vvtCategories')
expect(result.crossReferences).toHaveProperty('tomControls')
expect(result.crossReferences).toHaveProperty('retentionCategories')
expect(result.crossReferences.vvtCategories.length).toBe(1)
expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung')
})
it('should include scope level', () => {
const state = makeDecisionState('L3')
const result = projector.projectForValidate(state, ['vvt'])
expect(result.scopeLevel).toBe('L3')
})
it('should include depth requirements per document type', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.depthRequirements).toHaveProperty('vvt')
expect(result.depthRequirements).toHaveProperty('tom')
})
it('should summarize documents', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.documents[0].contentSummary).toContain('1')
expect(result.documents[1].contentSummary).toContain('Keine TOM')
})
it('should handle empty state', () => {
const state = makeState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result.scopeLevel).toBe('L1')
expect(result.crossReferences.vvtCategories).toEqual([])
expect(result.crossReferences.tomControls).toEqual([])
})
})
describe('token budget estimation', () => {
it('projectForDraft should produce compact output', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
const json = JSON.stringify(result)
// Rough token estimation: ~4 chars per token
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500
})
it('projectForAsk should produce very compact output', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
const json = JSON.stringify(result)
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600
})
it('projectForValidate should stay within budget', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
const json = JSON.stringify(result)
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000
})
})
})
@@ -0,0 +1,221 @@
/**
* Constraint Enforcer - Hard Gate vor jedem Draft
*
* Stellt sicher, dass die Drafting Engine NIEMALS die deterministische
* Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang:
*
* 1. Ist der Dokumenttyp in requiredDocuments?
* 2. Passt die Draft-Tiefe zum Level?
* 3. Ist eine DSFA erforderlich (Hard Trigger)?
* 4. Werden Risiko-Flags beruecksichtigt?
*/
import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types'
import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types'
import type { ConstraintCheckResult, DraftContext } from './types'
export class ConstraintEnforcer {
/**
* Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist.
* Dies ist ein HARD GATE - bei Violation wird der Draft blockiert.
*/
check(
documentType: ScopeDocumentType,
decision: ScopeDecision | null,
requestedDepthLevel?: ComplianceDepthLevel
): ConstraintCheckResult {
const violations: string[] = []
const adjustments: string[] = []
const checkedRules: string[] = []
// Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben
if (!decision) {
checkedRules.push('RULE-NO-DECISION')
if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') {
violations.push(
'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.'
)
} else {
adjustments.push(
'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.'
)
}
return {
allowed: violations.length === 0,
violations,
adjustments,
checkedRules,
}
}
const level = decision.determinedLevel
const levelNumeric = getDepthLevelNumeric(level)
// -----------------------------------------------------------------------
// Rule 1: Dokumenttyp in requiredDocuments?
// -----------------------------------------------------------------------
checkedRules.push('RULE-DOC-REQUIRED')
const isRequired = decision.requiredDocuments.some(
d => d.documentType === documentType && d.required
)
const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
if (!isRequired && scopeReq && !scopeReq.required) {
// Nicht blockieren, aber warnen
adjustments.push(
`Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` +
`Entwurf ist moeglich, aber optional.`
)
}
// -----------------------------------------------------------------------
// Rule 2: Draft-Tiefe passt zum Level?
// -----------------------------------------------------------------------
checkedRules.push('RULE-DEPTH-MATCH')
if (requestedDepthLevel) {
const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel)
if (requestedNumeric > levelNumeric) {
violations.push(
`Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` +
`Die Scope-Engine hat Level ${level} festgelegt. ` +
`Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.`
)
} else if (requestedNumeric < levelNumeric) {
adjustments.push(
`Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` +
`Draft wird auf Level ${level} angehoben.`
)
}
}
// -----------------------------------------------------------------------
// Rule 3: DSFA-Enforcement
// -----------------------------------------------------------------------
checkedRules.push('RULE-DSFA-ENFORCEMENT')
if (documentType === 'dsfa') {
const dsfaRequired = decision.triggeredHardTriggers.some(
t => t.rule.dsfaRequired
)
if (!dsfaRequired && level !== 'L4') {
adjustments.push(
'DSFA ist laut Scope-Engine nicht verpflichtend. ' +
'Entwurf wird als freiwillige Massnahme gekennzeichnet.'
)
}
}
// Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen
if (documentType !== 'dsfa') {
const dsfaRequired = decision.triggeredHardTriggers.some(
t => t.rule.dsfaRequired
)
const dsfaInRequired = decision.requiredDocuments.some(
d => d.documentType === 'dsfa' && d.required
)
if (dsfaRequired && dsfaInRequired) {
// Nur ein Hinweis, kein Block
adjustments.push(
'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' +
'Bitte sicherstellen, dass auch eine DSFA erstellt wird.'
)
}
}
// -----------------------------------------------------------------------
// Rule 4: Risiko-Flags beruecksichtigt?
// -----------------------------------------------------------------------
checkedRules.push('RULE-RISK-FLAGS')
const criticalRisks = decision.riskFlags.filter(
f => f.severity === 'CRITICAL' || f.severity === 'HIGH'
)
if (criticalRisks.length > 0) {
adjustments.push(
`${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` +
`Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}`
)
}
// -----------------------------------------------------------------------
// Rule 5: Hard-Trigger Consistency
// -----------------------------------------------------------------------
checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY')
for (const trigger of decision.triggeredHardTriggers) {
const mandatoryDocs = trigger.rule.mandatoryDocuments
if (mandatoryDocs.includes(documentType)) {
// Gut - wir erstellen ein mandatory document
} else {
// Pruefen ob die mandatory documents des Triggers vorhanden sind
// (nur Hinweis, kein Block)
}
}
return {
allowed: violations.length === 0,
violations,
adjustments,
checkedRules,
}
}
/**
* Convenience: Prueft aus einem DraftContext heraus.
*/
checkFromContext(
documentType: ScopeDocumentType,
context: DraftContext
): ConstraintCheckResult {
// Reconstruct a minimal ScopeDecision from context
const pseudoDecision: ScopeDecision = {
id: 'projected',
determinedLevel: context.decisions.level,
scores: context.decisions.scores,
triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({
rule: {
id: t.id,
label: t.label,
description: '',
conditionField: '',
conditionOperator: 'EQUALS' as const,
conditionValue: null,
minimumLevel: context.decisions.level,
mandatoryDocuments: [],
dsfaRequired: false,
legalReference: t.legalReference,
},
matchedValue: null,
explanation: '',
})),
requiredDocuments: context.decisions.requiredDocuments.map(d => ({
documentType: d.documentType,
label: d.documentType,
required: true,
depth: d.depth,
detailItems: d.detailItems,
estimatedEffort: '',
triggeredBy: [],
})),
riskFlags: context.constraints.riskFlags.map(f => ({
id: `rf-${f.title}`,
severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL',
title: f.title,
description: '',
recommendation: f.recommendation,
})),
gaps: [],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
return this.check(documentType, pseudoDecision)
}
}
/** Singleton-Instanz */
export const constraintEnforcer = new ConstraintEnforcer()
@@ -0,0 +1,241 @@
/**
* Intent Classifier - Leichtgewichtiger Pattern-Matcher
*
* Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call.
* Deutsche und englische Muster werden unterstuetzt.
*
* Confidence-Schwellen:
* - >0.8: Hohe Sicherheit, automatisch anwenden
* - 0.6-0.8: Mittel, Nutzer kann bestaetigen
* - <0.6: Fallback zu 'explain'
*/
import type { AgentMode, IntentClassification } from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
// ============================================================================
// Pattern Definitions
// ============================================================================
interface ModePattern {
mode: AgentMode
patterns: RegExp[]
/** Base-Confidence wenn ein Pattern matched */
baseConfidence: number
}
const MODE_PATTERNS: ModePattern[] = [
{
mode: 'draft',
baseConfidence: 0.85,
patterns: [
/\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i,
/\b(draft|create|generate|write|compose)\b/i,
/\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i,
/\b(vorlage|template)\s+(erstell|generier)/i,
/\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i,
],
},
{
mode: 'validate',
baseConfidence: 0.80,
patterns: [
/\b(pruef|validier|check|kontrollier|ueberpruef)\b/i,
/\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i,
/\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i,
/\b(validate|verify|check|review)\b/i,
/\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i,
/\bcross[\s-]?check\b/i,
/\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i,
],
},
{
mode: 'ask',
baseConfidence: 0.75,
patterns: [
/\bwas\s+fehlt\b/i,
/\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i,
/\b(unvollstaendig|unfertig|offen)\b/i,
/\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i,
/\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i,
/\bworan\s+(muss|soll)\b/i,
],
},
]
/** Dokumenttyp-Erkennung */
const DOCUMENT_TYPE_PATTERNS: Array<{
type: ScopeDocumentType
patterns: RegExp[]
}> = [
{
type: 'vvt',
patterns: [
/\bv{1,2}t\b/i,
/\bverarbeitungsverzeichnis\b/i,
/\bverarbeitungstaetigkeit/i,
/\bprocessing\s+activit/i,
/\bart\.?\s*30\b/i,
],
},
{
type: 'tom',
patterns: [
/\btom\b/i,
/\btechnisch.*organisatorisch.*massnahm/i,
/\bart\.?\s*32\b/i,
/\bsicherheitsmassnahm/i,
],
},
{
type: 'dsfa',
patterns: [
/\bdsfa\b/i,
/\bdatenschutz[\s-]?folgenabschaetzung\b/i,
/\bdpia\b/i,
/\bart\.?\s*35\b/i,
/\bimpact\s+assessment\b/i,
],
},
{
type: 'dsi',
patterns: [
/\bdatenschutzerklaerung\b/i,
/\bprivacy\s+policy\b/i,
/\bdsi\b/i,
/\bart\.?\s*13\b/i,
/\bart\.?\s*14\b/i,
],
},
{
type: 'lf',
patterns: [
/\bloeschfrist/i,
/\bloeschkonzept/i,
/\bretention/i,
/\baufbewahr/i,
],
},
{
type: 'av_vertrag',
patterns: [
/\bavv?\b/i,
/\bauftragsverarbeit/i,
/\bdata\s+processing\s+agreement/i,
/\bart\.?\s*28\b/i,
],
},
{
type: 'betroffenenrechte',
patterns: [
/\bbetroffenenrecht/i,
/\bdata\s+subject\s+right/i,
/\bart\.?\s*15\b/i,
/\bauskunft/i,
],
},
{
type: 'einwilligung',
patterns: [
/\beinwillig/i,
/\bconsent/i,
/\bcookie/i,
],
},
]
// ============================================================================
// Classifier
// ============================================================================
export class IntentClassifier {
/**
* Klassifiziert die Nutzerabsicht anhand des Inputs.
*
* @param input - Die Nutzer-Nachricht
* @returns IntentClassification mit Mode, Confidence, Patterns
*/
classify(input: string): IntentClassification {
const normalized = this.normalize(input)
let bestMatch: IntentClassification = {
mode: 'explain',
confidence: 0.3,
matchedPatterns: [],
}
for (const modePattern of MODE_PATTERNS) {
const matched: string[] = []
for (const pattern of modePattern.patterns) {
if (pattern.test(normalized)) {
matched.push(pattern.source)
}
}
if (matched.length > 0) {
// Mehr Matches = hoehere Confidence (bis zum Maximum)
const matchBonus = Math.min(matched.length - 1, 2) * 0.05
const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99)
if (confidence > bestMatch.confidence) {
bestMatch = {
mode: modePattern.mode,
confidence,
matchedPatterns: matched,
}
}
}
}
// Dokumenttyp erkennen
const detectedDocType = this.detectDocumentType(normalized)
if (detectedDocType) {
bestMatch.detectedDocumentType = detectedDocType
// Dokumenttyp-Erkennung erhoeht Confidence leicht
bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99)
}
// Fallback: Bei Confidence <0.6 immer 'explain'
if (bestMatch.confidence < 0.6) {
bestMatch.mode = 'explain'
}
return bestMatch
}
/**
* Erkennt den Dokumenttyp aus dem Input.
*/
detectDocumentType(input: string): ScopeDocumentType | undefined {
const normalized = this.normalize(input)
for (const docPattern of DOCUMENT_TYPE_PATTERNS) {
for (const pattern of docPattern.patterns) {
if (pattern.test(normalized)) {
return docPattern.type
}
}
}
return undefined
}
/**
* Normalisiert den Input fuer Pattern-Matching.
* Ersetzt Umlaute, entfernt Sonderzeichen.
*/
private normalize(input: string): string {
return input
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/Ä/g, 'Ae')
.replace(/Ö/g, 'Oe')
.replace(/Ü/g, 'Ue')
}
}
/** Singleton-Instanz */
export const intentClassifier = new IntentClassifier()
@@ -0,0 +1,49 @@
/**
* Gap Analysis Prompt - Lueckenanalyse und gezielte Fragen
*/
import type { GapContext } from '../types'
export interface GapAnalysisInput {
context: GapContext
instructions?: string
}
export function buildGapAnalysisPrompt(input: GapAnalysisInput): string {
const { context, instructions } = input
return `## Aufgabe: Compliance-Lueckenanalyse
### Identifizierte Luecken:
${context.gaps.length > 0
? context.gaps.map(g => `- [${g.severity}] ${g.title}: ${g.description}`).join('\n')
: '- Keine Luecken identifiziert'}
### Fehlende Pflichtdokumente:
${context.missingDocuments.length > 0
? context.missingDocuments.map(d => `- ${d.label} (Tiefe: ${d.depth}, Aufwand: ${d.estimatedEffort})`).join('\n')
: '- Alle Pflichtdokumente vorhanden'}
### Unbeantwortete Fragen:
${context.unansweredQuestions.length > 0
? context.unansweredQuestions.map(q => `- [${q.blockId}] ${q.question}`).join('\n')
: '- Alle Fragen beantwortet'}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Aufgabe:
Analysiere den Stand und stelle EINE gezielte Frage, die die wichtigste Luecke adressiert.
Priorisiere nach:
1. Fehlende Pflichtdokumente
2. Kritische Luecken (HIGH/CRITICAL severity)
3. Unbeantwortete Pflichtfragen
4. Mittlere Luecken
### Antwort-Format:
Antworte in dieser Struktur:
1. **Statusuebersicht**: Kurze Zusammenfassung des Compliance-Stands (2-3 Saetze)
2. **Wichtigste Luecke**: Was fehlt am dringendsten?
3. **Gezielte Frage**: Eine konkrete Frage an den Nutzer
4. **Warum wichtig**: Warum muss diese Luecke geschlossen werden?
5. **Empfohlener naechster Schritt**: Link/Verweis zum SDK-Modul`
}
@@ -0,0 +1,91 @@
/**
* DSFA Draft Prompt - Datenschutz-Folgenabschaetzung (Art. 35 DSGVO)
*/
import type { DraftContext } from '../types'
export interface DSFADraftInput {
context: DraftContext
processingDescription?: string
instructions?: string
}
export function buildDSFADraftPrompt(input: DSFADraftInput): string {
const { context, processingDescription, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
const hardTriggers = context.decisions.hardTriggers
return `## Aufgabe: DSFA entwerfen (Art. 35 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Hard Triggers (Gruende fuer DSFA-Pflicht):
${hardTriggers.length > 0
? hardTriggers.map(t => `- ${t.id}: ${t.label} (${t.legalReference})`).join('\n')
: '- Keine Hard Triggers (DSFA auf Wunsch)'}
### Erforderliche Inhalte:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${processingDescription ? `### Beschreibung der Verarbeitung: ${processingDescription}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "beschreibung",
"title": "Systematische Beschreibung der Verarbeitung",
"content": "...",
"schemaField": "processingDescription"
},
{
"id": "notwendigkeit",
"title": "Notwendigkeit und Verhaeltnismaessigkeit",
"content": "...",
"schemaField": "necessityAssessment"
},
{
"id": "risikobewertung",
"title": "Bewertung der Risiken fuer die Rechte und Freiheiten",
"content": "...",
"schemaField": "riskAssessment"
},
{
"id": "massnahmen",
"title": "Massnahmen zur Eindaemmung der Risiken",
"content": "...",
"schemaField": "mitigationMeasures"
},
{
"id": "stellungnahme_dsb",
"title": "Stellungnahme des Datenschutzbeauftragten",
"content": "...",
"schemaField": "dpoOpinion"
},
{
"id": "standpunkt_betroffene",
"title": "Standpunkt der betroffenen Personen",
"content": "...",
"schemaField": "dataSubjectView"
},
{
"id": "ergebnis",
"title": "Ergebnis und Empfehlung",
"content": "...",
"schemaField": "conclusion"
}
]
}
Halte die Tiefe exakt auf Level ${level}.
Nutze WP248-Kriterien als Leitfaden fuer die Risikobewertung.`
}
@@ -0,0 +1,78 @@
/**
* Loeschfristen Draft Prompt - Loeschkonzept
*/
import type { DraftContext } from '../types'
export interface LoeschfristenDraftInput {
context: DraftContext
instructions?: string
}
export function buildLoeschfristenDraftPrompt(input: LoeschfristenDraftInput): string {
const { context, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: Loeschkonzept / Loeschfristen entwerfen
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${context.existingDocumentData ? `### Bestehende Loeschfristen: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "grundsaetze",
"title": "Grundsaetze der Datenlöschung",
"content": "...",
"schemaField": "principles"
},
{
"id": "kategorien",
"title": "Datenkategorien und Loeschfristen",
"content": "Tabellarische Uebersicht...",
"schemaField": "retentionSchedule"
},
{
"id": "gesetzliche_fristen",
"title": "Gesetzliche Aufbewahrungsfristen",
"content": "HGB, AO, weitere...",
"schemaField": "legalRetention"
},
{
"id": "loeschprozess",
"title": "Technischer Loeschprozess",
"content": "...",
"schemaField": "deletionProcess"
},
{
"id": "verantwortlichkeiten",
"title": "Verantwortlichkeiten",
"content": "...",
"schemaField": "responsibilities"
},
{
"id": "ausnahmen",
"title": "Ausnahmen und Sonderfaelle",
"content": "...",
"schemaField": "exceptions"
}
]
}
Halte die Tiefe exakt auf Level ${level}.
Beruecksichtige branchenspezifische Aufbewahrungsfristen fuer ${context.companyProfile.industry}.`
}
@@ -0,0 +1,102 @@
/**
* Privacy Policy Draft Prompt - Datenschutzerklaerung (Art. 13/14 DSGVO)
*/
import type { DraftContext } from '../types'
export interface PrivacyPolicyDraftInput {
context: DraftContext
websiteUrl?: string
instructions?: string
}
export function buildPrivacyPolicyDraftPrompt(input: PrivacyPolicyDraftInput): string {
const { context, websiteUrl, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: Datenschutzerklaerung entwerfen (Art. 13/14 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : ''}
${websiteUrl ? `- Website: ${websiteUrl}` : ''}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "verantwortlicher",
"title": "Verantwortlicher",
"content": "...",
"schemaField": "controller"
},
{
"id": "dsb",
"title": "Datenschutzbeauftragter",
"content": "...",
"schemaField": "dpo"
},
{
"id": "verarbeitungen",
"title": "Verarbeitungstaetigkeiten und Zwecke",
"content": "...",
"schemaField": "processingPurposes"
},
{
"id": "rechtsgrundlagen",
"title": "Rechtsgrundlagen der Verarbeitung",
"content": "...",
"schemaField": "legalBases"
},
{
"id": "empfaenger",
"title": "Empfaenger und Datenweitergabe",
"content": "...",
"schemaField": "recipients"
},
{
"id": "drittland",
"title": "Uebermittlung in Drittlaender",
"content": "...",
"schemaField": "thirdCountryTransfers"
},
{
"id": "speicherdauer",
"title": "Speicherdauer",
"content": "...",
"schemaField": "retentionPeriods"
},
{
"id": "betroffenenrechte",
"title": "Ihre Rechte als betroffene Person",
"content": "...",
"schemaField": "dataSubjectRights"
},
{
"id": "cookies",
"title": "Cookies und Tracking",
"content": "...",
"schemaField": "cookies"
},
{
"id": "aenderungen",
"title": "Aenderungen dieser Datenschutzerklaerung",
"content": "...",
"schemaField": "changes"
}
]
}
Halte die Tiefe exakt auf Level ${level}.`
}
@@ -0,0 +1,99 @@
/**
* TOM Draft Prompt - Technische und Organisatorische Massnahmen (Art. 32 DSGVO)
*/
import type { DraftContext } from '../types'
export interface TOMDraftInput {
context: DraftContext
focusArea?: string
instructions?: string
}
export function buildTOMDraftPrompt(input: TOMDraftInput): string {
const { context, focusArea, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: TOM-Dokument entwerfen (Art. 32 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte fuer Level ${level}:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
### Constraints
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}`).join('\n')}` : ''}
${focusArea ? `### Fokusbereich: ${focusArea}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
${context.existingDocumentData ? `### Bestehende TOM: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "zutrittskontrolle",
"title": "Zutrittskontrolle",
"content": "Massnahmen die unbefugten Zutritt zu Datenverarbeitungsanlagen verhindern...",
"schemaField": "accessControl"
},
{
"id": "zugangskontrolle",
"title": "Zugangskontrolle",
"content": "Massnahmen gegen unbefugte Systemnutzung...",
"schemaField": "systemAccessControl"
},
{
"id": "zugriffskontrolle",
"title": "Zugriffskontrolle",
"content": "Massnahmen zur Sicherstellung berechtigter Datenzugriffe...",
"schemaField": "dataAccessControl"
},
{
"id": "weitergabekontrolle",
"title": "Weitergabekontrolle / Uebertragungssicherheit",
"content": "Massnahmen bei Datenuebertragung und -transport...",
"schemaField": "transferControl"
},
{
"id": "eingabekontrolle",
"title": "Eingabekontrolle",
"content": "Nachvollziehbarkeit von Dateneingaben...",
"schemaField": "inputControl"
},
{
"id": "auftragskontrolle",
"title": "Auftragskontrolle",
"content": "Massnahmen zur weisungsgemaessen Auftragsverarbeitung...",
"schemaField": "orderControl"
},
{
"id": "verfuegbarkeitskontrolle",
"title": "Verfuegbarkeitskontrolle",
"content": "Schutz gegen Datenverlust...",
"schemaField": "availabilityControl"
},
{
"id": "trennungsgebot",
"title": "Trennungsgebot",
"content": "Getrennte Verarbeitung fuer verschiedene Zwecke...",
"schemaField": "separationControl"
}
]
}
Fuelle fehlende Informationen mit [PLATZHALTER: ...].
Halte die Tiefe exakt auf Level ${level}.`
}
@@ -0,0 +1,109 @@
/**
* VVT Draft Prompt - Verarbeitungsverzeichnis (Art. 30 DSGVO)
*/
import type { DraftContext } from '../types'
export interface VVTDraftInput {
context: DraftContext
activityName?: string
activityPurpose?: string
instructions?: string
}
export function buildVVTDraftPrompt(input: VVTDraftInput): string {
const { context, activityName, activityPurpose, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: VVT-Eintrag entwerfen (Art. 30 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
- Geschaeftsmodell: ${context.companyProfile.businessModel}
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : '- DSB: Nicht benannt'}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte fuer Level ${level}:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
### Constraints
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}: ${f.recommendation}`).join('\n')}` : ''}
${activityName ? `### Gewuenschte Verarbeitungstaetigkeit: ${activityName}` : ''}
${activityPurpose ? `### Zweck: ${activityPurpose}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
${context.existingDocumentData ? `### Bestehende VVT-Eintraege: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "bezeichnung",
"title": "Bezeichnung der Verarbeitungstaetigkeit",
"content": "...",
"schemaField": "name"
},
{
"id": "verantwortlicher",
"title": "Verantwortlicher",
"content": "...",
"schemaField": "controller"
},
{
"id": "zweck",
"title": "Zweck der Verarbeitung",
"content": "...",
"schemaField": "purpose"
},
{
"id": "rechtsgrundlage",
"title": "Rechtsgrundlage",
"content": "...",
"schemaField": "legalBasis"
},
{
"id": "betroffene",
"title": "Kategorien betroffener Personen",
"content": "...",
"schemaField": "dataSubjects"
},
{
"id": "datenkategorien",
"title": "Kategorien personenbezogener Daten",
"content": "...",
"schemaField": "dataCategories"
},
{
"id": "empfaenger",
"title": "Empfaenger",
"content": "...",
"schemaField": "recipients"
},
{
"id": "speicherdauer",
"title": "Speicherdauer / Loeschfristen",
"content": "...",
"schemaField": "retentionPeriod"
},
{
"id": "tom_referenz",
"title": "TOM-Referenz",
"content": "...",
"schemaField": "tomReference"
}
]
}
Fuelle fehlende Informationen mit [PLATZHALTER: Beschreibung was hier eingetragen werden muss].
Halte die Tiefe exakt auf Level ${level} (${context.constraints.depthRequirements.depth}).`
}
@@ -0,0 +1,11 @@
/**
* Drafting Engine Prompts - Re-Exports
*/
export { buildVVTDraftPrompt, type VVTDraftInput } from './draft-vvt'
export { buildTOMDraftPrompt, type TOMDraftInput } from './draft-tom'
export { buildDSFADraftPrompt, type DSFADraftInput } from './draft-dsfa'
export { buildPrivacyPolicyDraftPrompt, type PrivacyPolicyDraftInput } from './draft-privacy-policy'
export { buildLoeschfristenDraftPrompt, type LoeschfristenDraftInput } from './draft-loeschfristen'
export { buildCrossCheckPrompt, type CrossCheckInput } from './validate-cross-check'
export { buildGapAnalysisPrompt, type GapAnalysisInput } from './ask-gap-analysis'
@@ -0,0 +1,66 @@
/**
* Cross-Document Validation Prompt
*/
import type { ValidationContext } from '../types'
export interface CrossCheckInput {
context: ValidationContext
focusDocuments?: string[]
instructions?: string
}
export function buildCrossCheckPrompt(input: CrossCheckInput): string {
const { context, focusDocuments, instructions } = input
return `## Aufgabe: Cross-Dokument-Konsistenzpruefung
### Scope-Level: ${context.scopeLevel}
### Vorhandene Dokumente:
${context.documents.map(d => `- ${d.type}: ${d.contentSummary}`).join('\n')}
### Cross-Referenzen:
- VVT-Kategorien: ${context.crossReferences.vvtCategories.join(', ') || 'Keine'}
- DSFA-Risiken: ${context.crossReferences.dsfaRisks.join(', ') || 'Keine'}
- TOM-Controls: ${context.crossReferences.tomControls.join(', ') || 'Keine'}
- Loeschfristen-Kategorien: ${context.crossReferences.retentionCategories.join(', ') || 'Keine'}
### Tiefenpruefung pro Dokument:
${context.documents.map(d => {
const req = context.depthRequirements[d.type]
return req ? `- ${d.type}: Erforderlich=${req.required}, Tiefe=${req.depth}` : `- ${d.type}: Keine Requirements`
}).join('\n')}
${focusDocuments ? `### Fokus auf: ${focusDocuments.join(', ')}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Pruefkriterien:
1. Jede VVT-Taetigkeit muss einen TOM-Verweis haben
2. Jede VVT-Kategorie muss eine Loeschfrist haben
3. Bei DSFA-pflichtigen Verarbeitungen muss eine DSFA existieren
4. TOM-Massnahmen muessen zum Risikoprofil passen
5. Loeschfristen duerfen gesetzliche Minima nicht unterschreiten
6. Dokument-Tiefe muss Level ${context.scopeLevel} entsprechen
### Antwort-Format
Antworte als JSON:
{
"passed": true/false,
"errors": [
{
"id": "ERR-001",
"severity": "error",
"category": "scope_violation|inconsistency|missing_content|depth_mismatch|cross_reference",
"title": "...",
"description": "...",
"documentType": "vvt|tom|dsfa|...",
"crossReferenceType": "...",
"legalReference": "Art. ... DSGVO",
"suggestion": "..."
}
],
"warnings": [...],
"suggestions": [...]
}`
}
@@ -0,0 +1,337 @@
/**
* State Projector - Token-budgetierte Projektion des SDK-State
*
* Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die
* relevanten Slices fuer den jeweiligen Agent-Modus.
*
* Token-Budgets:
* - Draft: ~1500 Tokens
* - Ask: ~600 Tokens
* - Validate: ~2000 Tokens
*/
import type { SDKState, CompanyProfile } from '../types'
import type {
ComplianceScopeState,
ScopeDecision,
ScopeDocumentType,
ScopeGap,
RequiredDocument,
RiskFlag,
DOCUMENT_SCOPE_MATRIX,
DocumentDepthRequirement,
} from '../compliance-scope-types'
import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types'
import type {
DraftContext,
GapContext,
ValidationContext,
} from './types'
// ============================================================================
// State Projector
// ============================================================================
export class StateProjector {
/**
* Projiziert den SDKState fuer Draft-Operationen.
* Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints.
*
* ~1500 Tokens
*/
projectForDraft(
state: SDKState,
documentType: ScopeDocumentType
): DraftContext {
const decision = state.complianceScope?.decision ?? null
const level = decision?.determinedLevel ?? 'L1'
const depthReq = DOC_MATRIX[documentType]?.[level] ?? {
required: false,
depth: 'Basis',
detailItems: [],
estimatedEffort: 'N/A',
}
return {
decisions: {
level,
scores: decision?.scores ?? {
risk_score: 0,
complexity_score: 0,
assurance_need: 0,
composite_score: 0,
},
hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({
id: t.rule.id,
label: t.rule.label,
legalReference: t.rule.legalReference,
})),
requiredDocuments: (decision?.requiredDocuments ?? [])
.filter(d => d.required)
.map(d => ({
documentType: d.documentType,
depth: d.depth,
detailItems: d.detailItems,
})),
},
companyProfile: this.projectCompanyProfile(state.companyProfile),
constraints: {
depthRequirements: depthReq,
riskFlags: (decision?.riskFlags ?? []).map(f => ({
severity: f.severity,
title: f.title,
recommendation: f.recommendation,
})),
boundaries: this.deriveBoundaries(decision, documentType),
},
existingDocumentData: this.extractExistingDocumentData(state, documentType),
}
}
/**
* Projiziert den SDKState fuer Ask-Operationen.
* Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente.
*
* ~600 Tokens
*/
projectForAsk(state: SDKState): GapContext {
const decision = state.complianceScope?.decision ?? null
// Fehlende Pflichtdokumente ermitteln
const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required)
const existingDocTypes = this.getExistingDocumentTypes(state)
const missingDocuments = requiredDocs
.filter(d => !existingDocTypes.includes(d.documentType))
.map(d => ({
documentType: d.documentType,
label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType,
depth: d.depth,
estimatedEffort: d.estimatedEffort,
}))
// Gaps aus der Scope-Decision
const gaps = (decision?.gaps ?? []).map(g => ({
id: g.id,
severity: g.severity,
title: g.title,
description: g.description,
relatedDocuments: g.relatedDocuments,
}))
// Unbeantwortete Fragen (aus dem Scope-Profiling)
const answers = state.complianceScope?.answers ?? []
const answeredIds = new Set(answers.map(a => a.questionId))
return {
unansweredQuestions: [], // Populated dynamically from question catalog
gaps,
missingDocuments,
}
}
/**
* Projiziert den SDKState fuer Validate-Operationen.
* Fokus: Cross-Dokument-Konsistenz, Scope-Compliance.
*
* ~2000 Tokens
*/
projectForValidate(
state: SDKState,
documentTypes: ScopeDocumentType[]
): ValidationContext {
const decision = state.complianceScope?.decision ?? null
const level = decision?.determinedLevel ?? 'L1'
// Dokument-Zusammenfassungen sammeln
const documents = documentTypes.map(type => ({
type,
contentSummary: this.summarizeDocument(state, type),
structuredData: this.extractExistingDocumentData(state, type),
}))
// Cross-Referenzen extrahieren
const crossReferences = {
vvtCategories: (state.vvt ?? []).map(v =>
typeof v === 'object' && v !== null && 'name' in v ? String((v as Record<string, unknown>).name) : ''
).filter(Boolean),
dsfaRisks: state.dsfa
? ['DSFA vorhanden']
: [],
tomControls: (state.toms ?? []).map(t =>
typeof t === 'object' && t !== null && 'name' in t ? String((t as Record<string, unknown>).name) : ''
).filter(Boolean),
retentionCategories: (state.retentionPolicies ?? []).map(p =>
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
).filter(Boolean),
}
// Depth-Requirements fuer alle angefragten Typen
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
for (const type of documentTypes) {
depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? {
required: false,
depth: 'Basis',
detailItems: [],
estimatedEffort: 'N/A',
}
}
return {
documents,
crossReferences,
scopeLevel: level,
depthRequirements: depthRequirements as Record<ScopeDocumentType, DocumentDepthRequirement>,
}
}
// ==========================================================================
// Private Helpers
// ==========================================================================
private projectCompanyProfile(
profile: CompanyProfile | null
): DraftContext['companyProfile'] {
if (!profile) {
return {
name: 'Unbekannt',
industry: 'Unbekannt',
employeeCount: 0,
businessModel: 'Unbekannt',
isPublicSector: false,
}
}
return {
name: profile.companyName ?? profile.name ?? 'Unbekannt',
industry: profile.industry ?? 'Unbekannt',
employeeCount: typeof profile.employeeCount === 'number'
? profile.employeeCount
: parseInt(String(profile.employeeCount ?? '0'), 10) || 0,
businessModel: profile.businessModel ?? 'Unbekannt',
isPublicSector: profile.isPublicSector ?? false,
...(profile.dataProtectionOfficer ? {
dataProtectionOfficer: {
name: profile.dataProtectionOfficer.name ?? '',
email: profile.dataProtectionOfficer.email ?? '',
},
} : {}),
}
}
/**
* Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf.
*/
private deriveBoundaries(
decision: ScopeDecision | null,
documentType: ScopeDocumentType
): string[] {
const boundaries: string[] = []
const level = decision?.determinedLevel ?? 'L1'
// Grundregel: Scope-Engine ist autoritativ
boundaries.push(
`Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})`
)
// DSFA-Boundary
if (documentType === 'dsfa') {
const dsfaRequired = decision?.triggeredHardTriggers?.some(
t => t.rule.dsfaRequired
) ?? false
if (!dsfaRequired && level !== 'L4') {
boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.')
}
}
// Dokument nicht in requiredDocuments?
const isRequired = decision?.requiredDocuments?.some(
d => d.documentType === documentType && d.required
) ?? false
if (!isRequired) {
boundaries.push(
`Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.`
)
}
return boundaries
}
/**
* Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State.
*/
private extractExistingDocumentData(
state: SDKState,
documentType: ScopeDocumentType
): Record<string, unknown> | undefined {
switch (documentType) {
case 'vvt':
return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined
case 'tom':
return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined
case 'lf':
return state.retentionPolicies?.length
? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length }
: undefined
case 'dsfa':
return state.dsfa ? { assessment: state.dsfa } : undefined
case 'dsi':
return state.documents?.length
? { entries: state.documents.slice(0, 3), totalCount: state.documents.length }
: undefined
case 'einwilligung':
return state.consents?.length
? { entries: state.consents.slice(0, 5), totalCount: state.consents.length }
: undefined
default:
return undefined
}
}
/**
* Ermittelt welche Dokumenttypen bereits im State vorhanden sind.
*/
private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] {
const types: ScopeDocumentType[] = []
if (state.vvt?.length) types.push('vvt')
if (state.toms?.length) types.push('tom')
if (state.retentionPolicies?.length) types.push('lf')
if (state.dsfa) types.push('dsfa')
if (state.documents?.length) types.push('dsi')
if (state.consents?.length) types.push('einwilligung')
if (state.cookieBanner) types.push('einwilligung')
return types
}
/**
* Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung.
*/
private summarizeDocument(
state: SDKState,
documentType: ScopeDocumentType
): string {
switch (documentType) {
case 'vvt':
return state.vvt?.length
? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst`
: 'Keine VVT-Eintraege vorhanden'
case 'tom':
return state.toms?.length
? `${state.toms.length} TOM-Massnahmen definiert`
: 'Keine TOM-Massnahmen vorhanden'
case 'lf':
return state.retentionPolicies?.length
? `${state.retentionPolicies.length} Loeschfristen definiert`
: 'Keine Loeschfristen vorhanden'
case 'dsfa':
return state.dsfa
? 'DSFA vorhanden'
: 'Keine DSFA vorhanden'
default:
return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}`
}
}
}
/** Singleton-Instanz */
export const stateProjector = new StateProjector()
@@ -0,0 +1,279 @@
/**
* Drafting Engine - Type Definitions
*
* Typen fuer die 4 Agent-Rollen: Explain, Ask, Draft, Validate
* Die Drafting Engine erweitert den Compliance Advisor um aktive Dokumententwurfs-
* und Validierungsfaehigkeiten, stets unter Beachtung der deterministischen Scope-Engine.
*/
import type {
ComplianceDepthLevel,
ComplianceScores,
ScopeDecision,
ScopeDocumentType,
ScopeGap,
RequiredDocument,
RiskFlag,
DocumentDepthRequirement,
ScopeProfilingQuestion,
} from '../compliance-scope-types'
import type { CompanyProfile } from '../types'
// ============================================================================
// Agent Mode
// ============================================================================
/** Die 4 Agent-Rollen */
export type AgentMode = 'explain' | 'ask' | 'draft' | 'validate'
/** Confidence-Score fuer Intent-Erkennung */
export interface IntentClassification {
mode: AgentMode
confidence: number
matchedPatterns: string[]
/** Falls Draft oder Validate: erkannter Dokumenttyp */
detectedDocumentType?: ScopeDocumentType
}
// ============================================================================
// Draft Context (fuer Draft-Mode)
// ============================================================================
/** Projizierter State fuer Draft-Operationen (~1500 Tokens) */
export interface DraftContext {
/** Scope-Entscheidung (Level, Scores, Hard Triggers) */
decisions: {
level: ComplianceDepthLevel
scores: ComplianceScores
hardTriggers: Array<{ id: string; label: string; legalReference: string }>
requiredDocuments: Array<{
documentType: ScopeDocumentType
depth: string
detailItems: string[]
}>
}
/** Firmenprofil-Auszug */
companyProfile: {
name: string
industry: string
employeeCount: number
businessModel: string
isPublicSector: boolean
dataProtectionOfficer?: { name: string; email: string }
}
/** Constraints aus der Scope-Engine */
constraints: {
depthRequirements: DocumentDepthRequirement
riskFlags: Array<{ severity: string; title: string; recommendation: string }>
boundaries: string[]
}
/** Optional: bestehende Dokumentdaten aus dem SDK-State */
existingDocumentData?: Record<string, unknown>
}
// ============================================================================
// Gap Context (fuer Ask-Mode)
// ============================================================================
/** Projizierter State fuer Ask-Operationen (~600 Tokens) */
export interface GapContext {
/** Noch unbeantwortete Fragen aus dem Scope-Profiling */
unansweredQuestions: Array<{
id: string
question: string
type: string
blockId: string
}>
/** Identifizierte Luecken */
gaps: Array<{
id: string
severity: string
title: string
description: string
relatedDocuments: ScopeDocumentType[]
}>
/** Fehlende Pflichtdokumente */
missingDocuments: Array<{
documentType: ScopeDocumentType
label: string
depth: string
estimatedEffort: string
}>
}
// ============================================================================
// Validation Context (fuer Validate-Mode)
// ============================================================================
/** Projizierter State fuer Validate-Operationen (~2000 Tokens) */
export interface ValidationContext {
/** Zu validierende Dokumente */
documents: Array<{
type: ScopeDocumentType
/** Zusammenfassung/Auszug des Inhalts */
contentSummary: string
/** Strukturierte Daten falls vorhanden */
structuredData?: Record<string, unknown>
}>
/** Cross-Referenzen zwischen Dokumenten */
crossReferences: {
/** VVT Kategorien (Verarbeitungstaetigkeiten) */
vvtCategories: string[]
/** DSFA Risiken */
dsfaRisks: string[]
/** TOM Controls */
tomControls: string[]
/** Loeschfristen-Kategorien */
retentionCategories: string[]
}
/** Scope-Level fuer Tiefenpruefung */
scopeLevel: ComplianceDepthLevel
/** Relevante Depth-Requirements */
depthRequirements: Record<ScopeDocumentType, DocumentDepthRequirement>
}
// ============================================================================
// Validation Result
// ============================================================================
export type ValidationSeverity = 'error' | 'warning' | 'suggestion'
export interface ValidationFinding {
id: string
severity: ValidationSeverity
category: 'scope_violation' | 'inconsistency' | 'missing_content' | 'depth_mismatch' | 'cross_reference'
title: string
description: string
/** Betroffenes Dokument */
documentType: ScopeDocumentType
/** Optional: Referenz zu anderem Dokument */
crossReferenceType?: ScopeDocumentType
/** Rechtsgrundlage falls relevant */
legalReference?: string
/** Vorschlag zur Behebung */
suggestion?: string
/** Kann automatisch uebernommen werden */
autoFixable?: boolean
}
export interface ValidationResult {
passed: boolean
timestamp: string
scopeLevel: ComplianceDepthLevel
errors: ValidationFinding[]
warnings: ValidationFinding[]
suggestions: ValidationFinding[]
}
// ============================================================================
// Draft Session
// ============================================================================
export interface DraftRevision {
id: string
content: string
sections: DraftSection[]
createdAt: string
instruction?: string
}
export interface DraftSection {
id: string
title: string
content: string
/** Mapping zum Dokumentschema (z.B. VVT-Feld) */
schemaField?: string
}
export interface DraftSession {
id: string
mode: AgentMode
documentType: ScopeDocumentType
/** Aktueller Draft-Inhalt */
currentDraft: DraftRevision | null
/** Alle bisherigen Revisionen */
revisions: DraftRevision[]
/** Validierungszustand */
validationState: ValidationResult | null
/** Constraint-Check Ergebnis */
constraintCheck: ConstraintCheckResult | null
createdAt: string
updatedAt: string
}
// ============================================================================
// Constraint Check (Hard Gate)
// ============================================================================
export interface ConstraintCheckResult {
/** Darf der Draft erstellt werden? */
allowed: boolean
/** Verletzungen die den Draft blockieren */
violations: string[]
/** Anpassungen die vorgenommen werden sollten */
adjustments: string[]
/** Gepruefte Regeln */
checkedRules: string[]
}
// ============================================================================
// Chat / API Types
// ============================================================================
export interface DraftingChatMessage {
role: 'user' | 'assistant' | 'system'
content: string
/** Metadata fuer Agent-Nachrichten */
metadata?: {
mode: AgentMode
documentType?: ScopeDocumentType
hasDraft?: boolean
hasValidation?: boolean
}
}
export interface DraftingChatRequest {
message: string
history: DraftingChatMessage[]
sdkStateProjection: DraftContext | GapContext | ValidationContext
mode?: AgentMode
documentType?: ScopeDocumentType
}
export interface DraftRequest {
documentType: ScopeDocumentType
draftContext: DraftContext
instructions?: string
existingDraft?: DraftRevision
}
export interface DraftResponse {
draft: DraftRevision
constraintCheck: ConstraintCheckResult
tokensUsed: number
}
export interface ValidateRequest {
documentType: ScopeDocumentType
draftContent: string
validationContext: ValidationContext
}
// ============================================================================
// Feature Flag
// ============================================================================
export interface DraftingEngineConfig {
/** Feature-Flag: Drafting Engine aktiviert */
enableDraftingEngine: boolean
/** Verfuegbare Modi (fuer schrittweises Rollout) */
enabledModes: AgentMode[]
/** Max Token-Budget fuer State-Projection */
maxProjectionTokens: number
}
export const DEFAULT_DRAFTING_ENGINE_CONFIG: DraftingEngineConfig = {
enableDraftingEngine: false,
enabledModes: ['explain'],
maxProjectionTokens: 4096,
}
@@ -0,0 +1,343 @@
'use client'
/**
* useDraftingEngine - React Hook fuer die Drafting Engine
*
* Managed: currentMode, activeDocumentType, draftSessions, validationState
* Handled: State-Projection, API-Calls, Streaming
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
*/
import { useState, useCallback, useRef } from 'react'
import { useSDK } from '../context'
import { stateProjector } from './state-projector'
import { intentClassifier } from './intent-classifier'
import { constraintEnforcer } from './constraint-enforcer'
import type {
AgentMode,
DraftSession,
DraftRevision,
DraftingChatMessage,
ValidationResult,
ConstraintCheckResult,
DraftContext,
GapContext,
ValidationContext,
} from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
export interface DraftingEngineState {
currentMode: AgentMode
activeDocumentType: ScopeDocumentType | null
messages: DraftingChatMessage[]
isTyping: boolean
currentDraft: DraftRevision | null
validationResult: ValidationResult | null
constraintCheck: ConstraintCheckResult | null
error: string | null
}
export interface DraftingEngineActions {
setMode: (mode: AgentMode) => void
setDocumentType: (type: ScopeDocumentType) => void
sendMessage: (content: string) => Promise<void>
requestDraft: (instructions?: string) => Promise<void>
validateDraft: () => Promise<void>
acceptDraft: () => void
stopGeneration: () => void
clearMessages: () => void
}
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
const { state, dispatch } = useSDK()
const abortControllerRef = useRef<AbortController | null>(null)
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
const [isTyping, setIsTyping] = useState(false)
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
const [error, setError] = useState<string | null>(null)
// Get state projection based on mode
const getProjection = useCallback(() => {
switch (currentMode) {
case 'draft':
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
case 'ask':
return stateProjector.projectForAsk(state)
case 'validate':
return activeDocumentType
? stateProjector.projectForValidate(state, [activeDocumentType])
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
default:
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
}
}, [state, currentMode, activeDocumentType])
const setMode = useCallback((mode: AgentMode) => {
setCurrentMode(mode)
}, [])
const setDocumentType = useCallback((type: ScopeDocumentType) => {
setActiveDocumentType(type)
}, [])
const sendMessage = useCallback(async (content: string) => {
if (!content.trim() || isTyping) return
setError(null)
// Auto-detect mode if needed
const classification = intentClassifier.classify(content)
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
setCurrentMode(classification.mode)
}
if (classification.detectedDocumentType && !activeDocumentType) {
setActiveDocumentType(classification.detectedDocumentType)
}
const userMessage: DraftingChatMessage = {
role: 'user',
content: content.trim(),
}
setMessages(prev => [...prev, userMessage])
setIsTyping(true)
abortControllerRef.current = new AbortController()
try {
const projection = getProjection()
const response = await fetch('/api/sdk/drafting-engine/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: content.trim(),
history: messages.map(m => ({ role: m.role, content: m.content })),
sdkStateProjection: projection,
mode: currentMode,
documentType: activeDocumentType,
}),
signal: abortControllerRef.current.signal,
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
}
const agentMessageId = `msg-${Date.now()}-agent`
setMessages(prev => [...prev, {
role: 'assistant',
content: '',
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
}])
// Stream response
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let accumulated = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
accumulated += decoder.decode(value, { stream: true })
const text = accumulated
setMessages(prev =>
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
)
}
setIsTyping(false)
} catch (err) {
if ((err as Error).name === 'AbortError') {
setIsTyping(false)
return
}
setError((err as Error).message)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Fehler: ${(err as Error).message}`,
}])
setIsTyping(false)
}
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
const requestDraft = useCallback(async (instructions?: string) => {
if (!activeDocumentType) {
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
return
}
setError(null)
setIsTyping(true)
try {
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
const response = await fetch('/api/sdk/drafting-engine/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType,
draftContext,
instructions,
existingDraft: currentDraft,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
}
setCurrentDraft(result.draft)
setConstraintCheck(result.constraintCheck)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const validateDraft = useCallback(async () => {
setError(null)
setIsTyping(true)
try {
const docTypes: ScopeDocumentType[] = activeDocumentType
? [activeDocumentType]
: ['vvt', 'tom', 'lf']
const validationContext = stateProjector.projectForValidate(state, docTypes)
const response = await fetch('/api/sdk/drafting-engine/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType || 'vvt',
draftContent: currentDraft?.content || '',
validationContext,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Validierung fehlgeschlagen')
}
setValidationResult(result)
const summary = result.passed
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
setMessages(prev => [...prev, {
role: 'assistant',
content: summary,
metadata: { mode: 'validate', hasValidation: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const acceptDraft = useCallback(() => {
if (!currentDraft || !activeDocumentType) return
// Dispatch the draft data into SDK state
switch (activeDocumentType) {
case 'vvt':
dispatch({
type: 'ADD_PROCESSING_ACTIVITY',
payload: {
id: `draft-vvt-${Date.now()}`,
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
case 'tom':
dispatch({
type: 'ADD_TOM',
payload: {
id: `draft-tom-${Date.now()}`,
name: 'TOM-Entwurf',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
default:
dispatch({
type: 'ADD_DOCUMENT',
payload: {
id: `draft-${activeDocumentType}-${Date.now()}`,
type: activeDocumentType,
content: currentDraft.content,
sections: currentDraft.sections,
},
})
}
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft wurde in den SDK-State uebernommen.`,
}])
setCurrentDraft(null)
}, [currentDraft, activeDocumentType, dispatch])
const stopGeneration = useCallback(() => {
abortControllerRef.current?.abort()
setIsTyping(false)
}, [])
const clearMessages = useCallback(() => {
setMessages([])
setCurrentDraft(null)
setValidationResult(null)
setConstraintCheck(null)
setError(null)
}, [])
return {
currentMode,
activeDocumentType,
messages,
isTyping,
currentDraft,
validationResult,
constraintCheck,
error,
setMode,
setDocumentType,
sendMessage,
requestDraft,
validateDraft,
acceptDraft,
stopGeneration,
clearMessages,
}
}
@@ -0,0 +1,355 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Mock fetch globally
const mockFetch = vi.fn()
global.fetch = mockFetch
describe('DSFA API Client', () => {
beforeEach(() => {
mockFetch.mockClear()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('listDSFAs', () => {
it('should fetch DSFAs without status filter', async () => {
const mockDSFAs = [
{ id: 'dsfa-1', name: 'Test DSFA 1', status: 'draft' },
{ id: 'dsfa-2', name: 'Test DSFA 2', status: 'approved' },
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ dsfas: mockDSFAs }),
})
const { listDSFAs } = await import('../api')
const result = await listDSFAs()
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result).toHaveLength(2)
expect(result[0].name).toBe('Test DSFA 1')
})
it('should fetch DSFAs with status filter', async () => {
const mockDSFAs = [{ id: 'dsfa-1', name: 'Draft DSFA', status: 'draft' }]
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ dsfas: mockDSFAs }),
})
const { listDSFAs } = await import('../api')
const result = await listDSFAs('draft')
expect(mockFetch).toHaveBeenCalledTimes(1)
const calledUrl = mockFetch.mock.calls[0][0]
expect(calledUrl).toContain('status=draft')
})
it('should return empty array when no DSFAs', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ dsfas: null }),
})
const { listDSFAs } = await import('../api')
const result = await listDSFAs()
expect(result).toEqual([])
})
})
describe('getDSFA', () => {
it('should fetch a single DSFA by ID', async () => {
const mockDSFA = {
id: 'dsfa-123',
name: 'Test DSFA',
status: 'draft',
risks: [],
mitigations: [],
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockDSFA),
})
const { getDSFA } = await import('../api')
const result = await getDSFA('dsfa-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.id).toBe('dsfa-123')
expect(result.name).toBe('Test DSFA')
})
it('should throw error for non-existent DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: () => Promise.resolve('{"error": "DSFA not found"}'),
})
const { getDSFA } = await import('../api')
await expect(getDSFA('non-existent')).rejects.toThrow()
})
})
describe('createDSFA', () => {
it('should create a new DSFA', async () => {
const newDSFA = {
name: 'New DSFA',
description: 'Test description',
processing_purpose: 'Testing',
}
const createdDSFA = {
id: 'dsfa-new',
...newDSFA,
status: 'draft',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(createdDSFA),
})
const { createDSFA } = await import('../api')
const result = await createDSFA(newDSFA)
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.id).toBe('dsfa-new')
expect(result.name).toBe('New DSFA')
})
})
describe('updateDSFA', () => {
it('should update an existing DSFA', async () => {
const updates = {
name: 'Updated DSFA Name',
processing_purpose: 'Updated purpose',
}
const updatedDSFA = {
id: 'dsfa-123',
...updates,
status: 'draft',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(updatedDSFA),
})
const { updateDSFA } = await import('../api')
const result = await updateDSFA('dsfa-123', updates)
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.name).toBe('Updated DSFA Name')
})
})
describe('deleteDSFA', () => {
it('should delete a DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
})
const { deleteDSFA } = await import('../api')
await deleteDSFA('dsfa-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
const calledConfig = mockFetch.mock.calls[0][1]
expect(calledConfig.method).toBe('DELETE')
})
it('should throw error when deletion fails', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
statusText: 'Not Found',
})
const { deleteDSFA } = await import('../api')
await expect(deleteDSFA('non-existent')).rejects.toThrow()
})
})
describe('updateDSFASection', () => {
it('should update a specific section', async () => {
const sectionData = {
processing_purpose: 'Updated purpose',
data_categories: ['personal_data', 'contact_data'],
}
const updatedDSFA = {
id: 'dsfa-123',
section_progress: {
section_1_complete: true,
},
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(updatedDSFA),
})
const { updateDSFASection } = await import('../api')
const result = await updateDSFASection('dsfa-123', 1, sectionData)
expect(mockFetch).toHaveBeenCalledTimes(1)
const calledUrl = mockFetch.mock.calls[0][0]
expect(calledUrl).toContain('/sections/1')
})
})
describe('submitDSFAForReview', () => {
it('should submit DSFA for review', async () => {
const response = {
message: 'DSFA submitted for review',
dsfa: {
id: 'dsfa-123',
status: 'in_review',
},
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(response),
})
const { submitDSFAForReview } = await import('../api')
const result = await submitDSFAForReview('dsfa-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.dsfa.status).toBe('in_review')
})
})
describe('approveDSFA', () => {
it('should approve a DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ message: 'DSFA approved' }),
})
const { approveDSFA } = await import('../api')
const result = await approveDSFA('dsfa-123', {
dpo_opinion: 'Approved after review',
approved: true,
})
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.message).toBe('DSFA approved')
})
it('should reject a DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ message: 'DSFA rejected' }),
})
const { approveDSFA } = await import('../api')
const result = await approveDSFA('dsfa-123', {
dpo_opinion: 'Needs more details',
approved: false,
})
expect(result.message).toBe('DSFA rejected')
})
})
describe('getDSFAStats', () => {
it('should fetch DSFA statistics', async () => {
const stats = {
total: 10,
status_stats: {
draft: 4,
in_review: 2,
approved: 3,
rejected: 1,
},
risk_stats: {
low: 3,
medium: 4,
high: 2,
very_high: 1,
},
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(stats),
})
const { getDSFAStats } = await import('../api')
const result = await getDSFAStats()
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.total).toBe(10)
expect(result.status_stats.approved).toBe(3)
})
})
describe('createDSFAFromAssessment', () => {
it('should create DSFA from UCCA assessment', async () => {
const response = {
dsfa: {
id: 'dsfa-new',
name: 'AI Chatbot DSFA',
status: 'draft',
},
prefilled: true,
message: 'DSFA created from assessment',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(response),
})
const { createDSFAFromAssessment } = await import('../api')
const result = await createDSFAFromAssessment('assessment-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.prefilled).toBe(true)
expect(result.dsfa.id).toBe('dsfa-new')
})
})
describe('getDSFAByAssessment', () => {
it('should return DSFA linked to assessment', async () => {
const dsfa = {
id: 'dsfa-123',
assessment_id: 'assessment-123',
name: 'Linked DSFA',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(dsfa),
})
const { getDSFAByAssessment } = await import('../api')
const result = await getDSFAByAssessment('assessment-123')
expect(result?.id).toBe('dsfa-123')
})
it('should return null when no DSFA exists for assessment', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: () => Promise.resolve('Not found'),
})
const { getDSFAByAssessment } = await import('../api')
const result = await getDSFAByAssessment('no-dsfa-assessment')
expect(result).toBeNull()
})
})
})
@@ -0,0 +1,255 @@
import { describe, it, expect } from 'vitest'
import {
calculateRiskLevel,
DSFA_SECTIONS,
DSFA_STATUS_LABELS,
DSFA_RISK_LEVEL_LABELS,
DSFA_LEGAL_BASES,
DSFA_AFFECTED_RIGHTS,
RISK_MATRIX,
type DSFARisk,
type DSFAMitigation,
type DSFASectionProgress,
type DSFA,
} from '../types'
describe('DSFA_SECTIONS', () => {
it('should have 5 sections defined', () => {
expect(DSFA_SECTIONS.length).toBe(5)
})
it('should have sections numbered 1-5', () => {
const numbers = DSFA_SECTIONS.map(s => s.number)
expect(numbers).toEqual([1, 2, 3, 4, 5])
})
it('should have GDPR references for all sections', () => {
DSFA_SECTIONS.forEach(section => {
expect(section.gdprRef).toBeDefined()
expect(section.gdprRef).toContain('Art. 35')
})
})
it('should mark first 4 sections as required', () => {
const requiredSections = DSFA_SECTIONS.filter(s => s.required)
expect(requiredSections.length).toBe(4)
expect(requiredSections.map(s => s.number)).toEqual([1, 2, 3, 4])
})
it('should mark section 5 as optional', () => {
const section5 = DSFA_SECTIONS.find(s => s.number === 5)
expect(section5?.required).toBe(false)
})
it('should have German titles for all sections', () => {
DSFA_SECTIONS.forEach(section => {
expect(section.titleDE).toBeDefined()
expect(section.titleDE.length).toBeGreaterThan(0)
})
})
})
describe('DSFA_STATUS_LABELS', () => {
it('should have all status labels defined', () => {
expect(DSFA_STATUS_LABELS.draft).toBe('Entwurf')
expect(DSFA_STATUS_LABELS.in_review).toBe('In Prüfung')
expect(DSFA_STATUS_LABELS.approved).toBe('Genehmigt')
expect(DSFA_STATUS_LABELS.rejected).toBe('Abgelehnt')
expect(DSFA_STATUS_LABELS.needs_update).toBe('Überarbeitung erforderlich')
})
})
describe('DSFA_RISK_LEVEL_LABELS', () => {
it('should have all risk level labels defined', () => {
expect(DSFA_RISK_LEVEL_LABELS.low).toBe('Niedrig')
expect(DSFA_RISK_LEVEL_LABELS.medium).toBe('Mittel')
expect(DSFA_RISK_LEVEL_LABELS.high).toBe('Hoch')
expect(DSFA_RISK_LEVEL_LABELS.very_high).toBe('Sehr Hoch')
})
})
describe('DSFA_LEGAL_BASES', () => {
it('should have 6 legal bases defined', () => {
expect(Object.keys(DSFA_LEGAL_BASES).length).toBe(6)
})
it('should reference GDPR Article 6', () => {
Object.values(DSFA_LEGAL_BASES).forEach(label => {
expect(label).toContain('Art. 6')
})
})
})
describe('DSFA_AFFECTED_RIGHTS', () => {
it('should have multiple affected rights defined', () => {
expect(DSFA_AFFECTED_RIGHTS.length).toBeGreaterThan(5)
})
it('should have id and label for each right', () => {
DSFA_AFFECTED_RIGHTS.forEach(right => {
expect(right.id).toBeDefined()
expect(right.label).toBeDefined()
})
})
it('should include GDPR data subject rights', () => {
const ids = DSFA_AFFECTED_RIGHTS.map(r => r.id)
expect(ids).toContain('right_of_access')
expect(ids).toContain('right_to_erasure')
expect(ids).toContain('right_to_data_portability')
})
})
describe('RISK_MATRIX', () => {
it('should have 9 cells defined (3x3 matrix)', () => {
expect(RISK_MATRIX.length).toBe(9)
})
it('should cover all combinations of likelihood and impact', () => {
const likelihoodValues = ['low', 'medium', 'high']
const impactValues = ['low', 'medium', 'high']
likelihoodValues.forEach(likelihood => {
impactValues.forEach(impact => {
const cell = RISK_MATRIX.find(
c => c.likelihood === likelihood && c.impact === impact
)
expect(cell).toBeDefined()
})
})
})
it('should have increasing scores for higher risks', () => {
const lowLow = RISK_MATRIX.find(c => c.likelihood === 'low' && c.impact === 'low')
const highHigh = RISK_MATRIX.find(c => c.likelihood === 'high' && c.impact === 'high')
expect(lowLow?.score).toBeLessThan(highHigh?.score || 0)
})
})
describe('calculateRiskLevel', () => {
it('should return low for low likelihood and low impact', () => {
const result = calculateRiskLevel('low', 'low')
expect(result.level).toBe('low')
expect(result.score).toBe(10)
})
it('should return very_high for high likelihood and high impact', () => {
const result = calculateRiskLevel('high', 'high')
expect(result.level).toBe('very_high')
expect(result.score).toBe(90)
})
it('should return medium for medium likelihood and medium impact', () => {
const result = calculateRiskLevel('medium', 'medium')
expect(result.level).toBe('medium')
expect(result.score).toBe(50)
})
it('should return high for high likelihood and medium impact', () => {
const result = calculateRiskLevel('high', 'medium')
expect(result.level).toBe('high')
expect(result.score).toBe(70)
})
it('should return medium for low likelihood and high impact', () => {
const result = calculateRiskLevel('low', 'high')
expect(result.level).toBe('medium')
expect(result.score).toBe(40)
})
})
describe('DSFARisk type', () => {
it('should accept valid risk data', () => {
const risk: DSFARisk = {
id: 'risk-001',
category: 'confidentiality',
description: 'Unauthorized access to personal data',
likelihood: 'medium',
impact: 'high',
risk_level: 'high',
affected_data: ['customer_data', 'financial_data'],
}
expect(risk.id).toBe('risk-001')
expect(risk.category).toBe('confidentiality')
expect(risk.likelihood).toBe('medium')
expect(risk.impact).toBe('high')
})
})
describe('DSFAMitigation type', () => {
it('should accept valid mitigation data', () => {
const mitigation: DSFAMitigation = {
id: 'mit-001',
risk_id: 'risk-001',
description: 'Implement encryption at rest',
type: 'technical',
status: 'implemented',
residual_risk: 'low',
responsible_party: 'IT Security Team',
}
expect(mitigation.id).toBe('mit-001')
expect(mitigation.type).toBe('technical')
expect(mitigation.status).toBe('implemented')
})
})
describe('DSFASectionProgress type', () => {
it('should track completion for all 5 sections', () => {
const progress: DSFASectionProgress = {
section_1_complete: true,
section_2_complete: true,
section_3_complete: false,
section_4_complete: false,
section_5_complete: false,
}
expect(progress.section_1_complete).toBe(true)
expect(progress.section_2_complete).toBe(true)
expect(progress.section_3_complete).toBe(false)
})
})
describe('DSFA type', () => {
it('should accept a complete DSFA object', () => {
const dsfa: DSFA = {
id: 'dsfa-001',
tenant_id: 'tenant-001',
name: 'AI Chatbot DSFA',
description: 'Data Protection Impact Assessment for AI Chatbot',
processing_description: 'Automated customer service using AI',
processing_purpose: 'Customer support automation',
data_categories: ['contact_data', 'inquiry_content'],
data_subjects: ['customers'],
recipients: ['internal_staff'],
legal_basis: 'legitimate_interest',
necessity_assessment: 'Required for efficient customer service',
proportionality_assessment: 'Minimal data processing for the purpose',
risks: [],
overall_risk_level: 'medium',
risk_score: 50,
mitigations: [],
dpo_consulted: false,
authority_consulted: false,
status: 'draft',
section_progress: {
section_1_complete: true,
section_2_complete: true,
section_3_complete: false,
section_4_complete: false,
section_5_complete: false,
},
conclusion: '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
created_by: 'user-001',
}
expect(dsfa.id).toBe('dsfa-001')
expect(dsfa.name).toBe('AI Chatbot DSFA')
expect(dsfa.status).toBe('draft')
expect(dsfa.data_categories).toHaveLength(2)
})
})
@@ -0,0 +1,417 @@
/**
* DSFA KI-Massnahmenbibliothek - Vordefinierte KI-spezifische Massnahmen
*
* ~25 Massnahmen gegliedert nach:
* - Bias-Praevention & Fairness
* - Erklaerbarkeit & Transparenz
* - Datenqualitaet & Governance
* - Sicherheit & Robustheit
* - Automatisierte Entscheidungen (Human Oversight)
* - Monitoring & Qualitaetssicherung
* - Privatsphaere & Datenschutz
*
* Quellen: Art. 9-15 AI Act, Art. 22/25/32 DSGVO, EDPB Guidelines,
* BSI-TR-03161, SDM V2.0
*/
import type { CatalogMitigation } from './mitigation-library'
// =============================================================================
// KI-MASSNAHMENBIBLIOTHEK
// =============================================================================
export const AI_MITIGATION_LIBRARY: CatalogMitigation[] = [
// =========================================================================
// BIAS-PRAEVENTION & FAIRNESS
// =========================================================================
{
id: 'M-AI-BIAS-01',
type: 'technical',
sdmGoals: ['nichtverkettung', 'intervenierbarkeit'],
title: 'Bias-Audit und Fairness-Testing',
description: 'Regelmaessige Durchfuehrung von Bias-Audits mit standardisierten Fairness-Metriken (z.B. Demographic Parity, Equalized Odds, Calibration). Automatisierte Tests vor jedem Modell-Update.',
legalBasis: 'Art. 10 AI Act, Art. 22 Abs. 3 DSGVO',
evidenceTypes: ['Bias-Audit-Report', 'Fairness-Metriken-Dashboard', 'Test-Protokoll'],
addressesRiskIds: ['R-AI-BIAS-01', 'R-AI-BIAS-03', 'R-AI-PRIV-04'],
effectiveness: 'high',
},
{
id: 'M-AI-BIAS-02',
type: 'technical',
sdmGoals: ['nichtverkettung', 'integritaet'],
title: 'Trainingsdaten-Debiasing und Rebalancing',
description: 'Systematische Analyse der Trainingsdaten auf Unterrepraesentation und Verzerrungen. Anwendung von Resampling, Reweighting oder synthetischer Datenerweiterung zur Herstellung von Balance.',
legalBasis: 'Art. 10 Abs. 2-3 AI Act',
evidenceTypes: ['Datenanalyse-Report', 'Rebalancing-Protokoll', 'Datenverteilungs-Bericht'],
addressesRiskIds: ['R-AI-BIAS-01', 'R-AI-BIAS-02'],
effectiveness: 'medium',
},
{
id: 'M-AI-BIAS-03',
type: 'organizational',
sdmGoals: ['nichtverkettung', 'transparenz'],
title: 'Diversitaet in KI-Entwicklungsteams',
description: 'Sicherstellung von Diversitaet in den Teams, die KI-Systeme entwickeln und bewerten. Einbeziehung von Betroffenengruppen in den Evaluierungsprozess.',
legalBasis: 'Art. 9 Abs. 9 AI Act',
evidenceTypes: ['Team-Diversity-Report', 'Stakeholder-Einbeziehungs-Protokoll'],
addressesRiskIds: ['R-AI-BIAS-03'],
effectiveness: 'medium',
},
// =========================================================================
// ERKLAERBARKEIT & TRANSPARENZ
// =========================================================================
{
id: 'M-AI-EXPL-01',
type: 'technical',
sdmGoals: ['transparenz', 'intervenierbarkeit'],
title: 'Explainable AI (XAI) - Erklaerbare KI-Methoden',
description: 'Einsatz von Erklaerbarkeitsmethoden wie SHAP, LIME oder Attention Maps, um KI-Entscheidungen nachvollziehbar zu machen. Bereitstellung von Erklaerungen in verstaendlicher Sprache.',
legalBasis: 'Art. 13 AI Act, Art. 13-14 DSGVO',
evidenceTypes: ['XAI-Implementierungsbericht', 'Erklaerbarkeits-Screenshots', 'Nutzer-Feedback'],
addressesRiskIds: ['R-AI-EXPL-01', 'R-AI-AUTO-02', 'R-AI-BIAS-03'],
effectiveness: 'high',
},
{
id: 'M-AI-EXPL-02',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'KI-Modellkarte (Model Card) und Datenblatt',
description: 'Erstellung und Pflege einer Modellkarte nach dem Model Card Framework. Dokumentation von Leistung, Einschraenkungen, beabsichtigter Nutzung und Fairness-Metriken.',
legalBasis: 'Art. 11 AI Act, Art. 13 DSGVO',
evidenceTypes: ['Model Card (PDF)', 'Data Sheet', 'Leistungsbericht'],
addressesRiskIds: ['R-AI-EXPL-03', 'R-AI-EXPL-01'],
effectiveness: 'medium',
},
{
id: 'M-AI-EXPL-03',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'KI-Kennzeichnung und Nutzertransparenz',
description: 'Deutliche Kennzeichnung von KI-generierten Inhalten und KI-Interaktionen. Informierung der Nutzer ueber den Einsatz von KI-Systemen, deren Zweck und Einschraenkungen.',
legalBasis: 'Art. 50 AI Act, Art. 13-14 DSGVO',
evidenceTypes: ['KI-Kennzeichnungs-Screenshots', 'Datenschutzhinweis-Auszug', 'Nutzerinformation'],
addressesRiskIds: ['R-AI-EXPL-02', 'R-AI-EXPL-01'],
effectiveness: 'high',
},
{
id: 'M-AI-TRANS-01',
type: 'legal',
sdmGoals: ['transparenz'],
title: 'KI-Transparenzbericht (jaehrlich)',
description: 'Veroeffentlichung eines jaehrlichen Transparenzberichts ueber den Einsatz von KI-Systemen, deren Auswirkungen, durchgefuehrte Audits und ergriffene Massnahmen.',
legalBasis: 'Art. 13 AI Act',
evidenceTypes: ['Transparenzbericht (PDF)', 'Veroeffentlichungsnachweis'],
addressesRiskIds: ['R-AI-EXPL-02'],
effectiveness: 'medium',
},
{
id: 'M-AI-DOC-01',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'Technische Dokumentation nach AI Act',
description: 'Vollstaendige technische Dokumentation des KI-Systems gemaess Art. 11 und Anhang IV AI Act: Systembeschreibung, Designentscheidungen, Datenmanagement, Monitoring-Plan.',
legalBasis: 'Art. 11, Anhang IV AI Act',
evidenceTypes: ['Technische Dokumentation', 'Systemarchitektur-Diagramm', 'Anhang-IV-Checkliste'],
addressesRiskIds: ['R-AI-EXPL-03'],
effectiveness: 'high',
},
// =========================================================================
// DATENQUALITAET & GOVERNANCE
// =========================================================================
{
id: 'M-AI-DATA-01',
type: 'organizational',
sdmGoals: ['integritaet', 'datenminimierung'],
title: 'Data Governance Framework fuer KI-Training',
description: 'Einrichtung eines strukturierten Data-Governance-Prozesses: Datenherkunft (Provenance), Qualitaetskontrolle, Versionierung, Dokumentation und regelmaessige Ueberpruefung der Trainingsdaten.',
legalBasis: 'Art. 10 AI Act, Art. 5 Abs. 1 lit. d DSGVO',
evidenceTypes: ['Data-Governance-Policy', 'Datenherkunfts-Dokumentation', 'Qualitaetskontroll-Protokoll'],
addressesRiskIds: ['R-AI-DATA-01', 'R-AI-DATA-03', 'R-AI-BIAS-01'],
effectiveness: 'high',
},
{
id: 'M-AI-DATA-02',
type: 'technical',
sdmGoals: ['integritaet'],
title: 'Automatisierte Datenqualitaetspruefung',
description: 'Automatisierte Pipelines zur Pruefung der Datenqualitaet: Erkennung von Ausreissern, Duplikaten, fehlenden Werten, Datenkonsistenz und statistischen Abweichungen.',
legalBasis: 'Art. 10 Abs. 2 AI Act',
evidenceTypes: ['Data-Quality-Pipeline-Logs', 'Qualitaetsmetriken-Dashboard'],
addressesRiskIds: ['R-AI-DATA-01', 'R-AI-DATA-04', 'R-AI-MON-01'],
effectiveness: 'medium',
},
{
id: 'M-AI-DATA-03',
type: 'legal',
sdmGoals: ['datenminimierung', 'nichtverkettung'],
title: 'Rechtsgrundlage und DSFA fuer KI-Trainingsdaten',
description: 'Sicherstellung einer validen Rechtsgrundlage fuer die Nutzung personenbezogener Daten im KI-Training. Durchfuehrung einer separaten DSFA fuer den Trainingsdaten-Verarbeitungszweck.',
legalBasis: 'Art. 6, Art. 35 DSGVO, Art. 10 Abs. 5 AI Act',
evidenceTypes: ['Rechtsgrundlagen-Bewertung', 'DSFA-Trainingsdaten', 'Einwilligungsformular'],
addressesRiskIds: ['R-AI-DATA-02', 'R-AI-PRIV-04', 'R-AI-DATA-03'],
effectiveness: 'high',
},
// =========================================================================
// SICHERHEIT & ROBUSTHEIT
// =========================================================================
{
id: 'M-AI-SEC-01',
type: 'technical',
sdmGoals: ['integritaet', 'verfuegbarkeit'],
title: 'Adversarial Robustness Testing',
description: 'Regelmaessige Tests des KI-Modells gegen Adversarial Attacks, Data Poisoning und Evasion-Angriffe. Einsatz von Robustness Toolkits (z.B. IBM ART, Foolbox).',
legalBasis: 'Art. 15 AI Act, Art. 32 DSGVO',
evidenceTypes: ['Adversarial-Test-Report', 'Robustness-Metriken', 'Penetrationstest-Bericht'],
addressesRiskIds: ['R-AI-SEC-01', 'R-AI-DATA-04', 'R-AI-SEC-03'],
effectiveness: 'high',
},
{
id: 'M-AI-SEC-02',
type: 'technical',
sdmGoals: ['verfuegbarkeit', 'integritaet'],
title: 'Input-Validierung und Sanitization',
description: 'Implementierung robuster Eingabevalidierung fuer KI-Systeme: Laengen- und Formatpruefung, Content-Filterung, Erkennung adversarialer Eingabemuster.',
legalBasis: 'Art. 15 AI Act, Art. 32 DSGVO',
evidenceTypes: ['Input-Validation-Policy', 'Filter-Regeln-Dokumentation'],
addressesRiskIds: ['R-AI-SEC-01', 'R-AI-SEC-02'],
effectiveness: 'medium',
},
{
id: 'M-AI-SEC-03',
type: 'technical',
sdmGoals: ['vertraulichkeit', 'integritaet'],
title: 'Prompt-Injection-Schutz und Output-Filterung',
description: 'Implementierung mehrschichtiger Schutzmassnahmen gegen Prompt Injection: System-Prompt-Isolation, Input-Sanitization, Output-Filterung und PII-Detection in Antworten.',
legalBasis: 'Art. 15 AI Act, Art. 32 DSGVO',
evidenceTypes: ['Security-Policy', 'Prompt-Injection-Test-Report', 'Filter-Konfiguration'],
addressesRiskIds: ['R-AI-SEC-02', 'R-AI-DATA-04'],
effectiveness: 'high',
},
{
id: 'M-AI-SEC-04',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'PII-Detection und Daten-Redaction in KI-Ausgaben',
description: 'Automatisierte Erkennung und Entfernung personenbezogener Daten (PII) in KI-Ausgaben. Echtzeit-Filterung sensibler Informationen vor der Auslieferung an Nutzer.',
legalBasis: 'Art. 32 DSGVO, Art. 25 DSGVO',
evidenceTypes: ['PII-Detection-Konfiguration', 'Redaction-Logs', 'False-Positive-Rate'],
addressesRiskIds: ['R-AI-SEC-02', 'R-AI-PRIV-02'],
effectiveness: 'high',
},
{
id: 'M-AI-SEC-05',
type: 'technical',
sdmGoals: ['integritaet', 'verfuegbarkeit'],
title: 'Retrieval-Augmented Generation (RAG) gegen Halluzinationen',
description: 'Einsatz von RAG-Systemen, die KI-Antworten auf verifizierte Quelldokumente stuetzen. Quellenangabe in jeder Antwort fuer Nachvollziehbarkeit.',
legalBasis: 'Art. 15 AI Act',
evidenceTypes: ['RAG-Architektur-Dokumentation', 'Quellengenauigkeits-Report', 'Halluzinations-Rate'],
addressesRiskIds: ['R-AI-SEC-04', 'R-AI-EXPL-01'],
effectiveness: 'high',
},
// =========================================================================
// AUTOMATISIERTE ENTSCHEIDUNGEN (HUMAN OVERSIGHT)
// =========================================================================
{
id: 'M-AI-AUTO-01',
type: 'organizational',
sdmGoals: ['intervenierbarkeit'],
title: 'Human-in-the-Loop / Human-on-the-Loop Prozess',
description: 'Etablierung eines strukturierten Prozesses fuer menschliche Aufsicht: Definierte Eskalationsschwellen, geschulte Entscheider, dokumentierte Ueberpruefungsschritte.',
legalBasis: 'Art. 14 AI Act, Art. 22 Abs. 3 DSGVO',
evidenceTypes: ['Human-Oversight-Prozessdokumentation', 'Eskalationsmatrix', 'Schulungsnachweis'],
addressesRiskIds: ['R-AI-AUTO-01', 'R-AI-AUTO-02', 'R-AI-MON-02'],
effectiveness: 'high',
},
{
id: 'M-AI-AUTO-02',
type: 'technical',
sdmGoals: ['intervenierbarkeit', 'transparenz'],
title: 'Konfidenzwert-basierte Entscheidungssteuerung',
description: 'KI-System gibt bei jeder Entscheidung einen Konfidenzwert aus. Entscheidungen unterhalb eines definierten Schwellwerts werden automatisch an menschliche Pruefer eskaliert.',
legalBasis: 'Art. 14 AI Act',
evidenceTypes: ['Konfidenzwert-Policy', 'Schwellwert-Konfiguration', 'Eskalationsstatistik'],
addressesRiskIds: ['R-AI-AUTO-01', 'R-AI-SEC-04'],
effectiveness: 'high',
},
{
id: 'M-AI-AUTO-03',
type: 'legal',
sdmGoals: ['intervenierbarkeit'],
title: 'Widerspruchsrecht und manuelle Ueberpruefung',
description: 'Implementierung eines transparenten Prozesses, ueber den Betroffene einer KI-Entscheidung widersprechen und eine manuelle Ueberpruefung durch eine qualifizierte Person verlangen koennen.',
legalBasis: 'Art. 22 Abs. 3 DSGVO',
evidenceTypes: ['Widerspruchsformular', 'Prozessbeschreibung', 'Bearbeitungsstatistik'],
addressesRiskIds: ['R-AI-AUTO-01', 'R-AI-AUTO-03'],
effectiveness: 'high',
},
{
id: 'M-AI-AUTO-04',
type: 'organizational',
sdmGoals: ['intervenierbarkeit'],
title: 'Anti-Automation-Bias-Training',
description: 'Schulung der menschlichen Ueberpruefer gegen Automation Bias: Kritisches Hinterfragen von KI-Empfehlungen, regelmaessige Kalibierung, Entscheidungsdokumentation.',
legalBasis: 'Art. 14 Abs. 4 AI Act',
evidenceTypes: ['Schulungsunterlagen', 'Teilnahmebestaetigung', 'Ueberpruefungsstatistik'],
addressesRiskIds: ['R-AI-AUTO-02'],
effectiveness: 'medium',
},
{
id: 'M-AI-AUTO-05',
type: 'legal',
sdmGoals: ['intervenierbarkeit', 'transparenz'],
title: 'Informationspflicht bei automatisierter Entscheidungsfindung',
description: 'Proaktive Information der Betroffenen ueber automatisierte Entscheidungsfindung, die angewandte Logik, die Tragweite und die Rechte der Betroffenen (Art. 13 Abs. 2 lit. f DSGVO).',
legalBasis: 'Art. 13 Abs. 2 lit. f, Art. 14 Abs. 2 lit. g DSGVO',
evidenceTypes: ['Datenschutzerklaerung-Auszug', 'Informationsschreiben', 'Transparenzhinweise-UI'],
addressesRiskIds: ['R-AI-AUTO-03'],
effectiveness: 'medium',
},
// =========================================================================
// MONITORING & QUALITAETSSICHERUNG
// =========================================================================
{
id: 'M-AI-MON-01',
type: 'technical',
sdmGoals: ['integritaet', 'verfuegbarkeit'],
title: 'KI-Performance-Monitoring und Drift-Detection',
description: 'Kontinuierliches Monitoring der KI-Leistungsmetriken (Accuracy, F1-Score, Fairness). Automatisierte Erkennung von Data Drift und Concept Drift mit Alerting.',
legalBasis: 'Art. 9 Abs. 2 AI Act, Art. 32 DSGVO',
evidenceTypes: ['Monitoring-Dashboard', 'Drift-Detection-Alerts', 'Performance-Trend-Report'],
addressesRiskIds: ['R-AI-MON-01', 'R-AI-DATA-01', 'R-AI-BIAS-02', 'R-AI-SEC-01'],
effectiveness: 'high',
},
{
id: 'M-AI-MON-02',
type: 'technical',
sdmGoals: ['integritaet', 'transparenz'],
title: 'KI-Audit-Logging und Entscheidungsprotokollierung',
description: 'Vollstaendige Protokollierung aller KI-Entscheidungen mit Eingabe, Ausgabe, Konfidenzwert und Zeitstempel. Aufbewahrung gemaess Art. 12 AI Act.',
legalBasis: 'Art. 12 AI Act, Art. 5 Abs. 2 DSGVO',
evidenceTypes: ['Audit-Log-Konfiguration', 'Log-Retention-Policy', 'Beispiel-Audit-Trail'],
addressesRiskIds: ['R-AI-MON-01', 'R-AI-BIAS-02', 'R-AI-SEC-02'],
effectiveness: 'medium',
},
{
id: 'M-AI-MON-03',
type: 'technical',
sdmGoals: ['verfuegbarkeit', 'intervenierbarkeit'],
title: 'Kill-Switch und Fallback-Mechanismus',
description: 'Implementierung eines Notfall-Abschaltmechanismus (Kill Switch) fuer das KI-System. Automatischer Fallback auf regelbasierte Verarbeitung oder manuelle Bearbeitung bei Stoerungen.',
legalBasis: 'Art. 14 Abs. 4 lit. e AI Act',
evidenceTypes: ['Kill-Switch-Dokumentation', 'Fallback-Prozess', 'Notfall-Testprotokoll'],
addressesRiskIds: ['R-AI-MON-02', 'R-AI-AUTO-01'],
effectiveness: 'high',
},
{
id: 'M-AI-MON-04',
type: 'organizational',
sdmGoals: ['verfuegbarkeit'],
title: 'Regelmaessiger KI-Systemtest und Wartungsplan',
description: 'Definierter Wartungsplan mit regelmaessigen Systemtests, Modell-Retraining-Zyklen und Leistungsueberpruefungen. Dokumentation aller Aenderungen und deren Auswirkungen.',
legalBasis: 'Art. 9 Abs. 3 AI Act',
evidenceTypes: ['Wartungsplan', 'Testprotokolle', 'Aenderungsdokumentation'],
addressesRiskIds: ['R-AI-MON-02', 'R-AI-MON-01'],
effectiveness: 'medium',
},
// =========================================================================
// PRIVATSPHAERE & DATENSCHUTZ
// =========================================================================
{
id: 'M-AI-PRIV-01',
type: 'technical',
sdmGoals: ['datenminimierung', 'nichtverkettung'],
title: 'Privacy-Preserving AI (Differential Privacy, Federated Learning)',
description: 'Einsatz von Privacy-Enhancing Technologies: Differential Privacy beim Training, Federated Learning fuer dezentrales Training, K-Anonymitaet bei Trainingsdaten.',
legalBasis: 'Art. 25 DSGVO, Art. 10 Abs. 5 AI Act',
evidenceTypes: ['Privacy-Technik-Beschreibung', 'Epsilon-Budget-Dokumentation', 'Anonymisierungs-Nachweis'],
addressesRiskIds: ['R-AI-PRIV-01', 'R-AI-DATA-02', 'R-AI-PRIV-04'],
effectiveness: 'high',
},
{
id: 'M-AI-PRIV-02',
type: 'technical',
sdmGoals: ['vertraulichkeit', 'datenminimierung'],
title: 'Trainingsdaten-Anonymisierung und Pseudonymisierung',
description: 'Konsequente Anonymisierung oder Pseudonymisierung personenbezogener Daten vor dem KI-Training. Anwendung von Data Masking, Tokenisierung und synthetischer Datengenerierung.',
legalBasis: 'Art. 25 Abs. 1 DSGVO, Art. 32 DSGVO',
evidenceTypes: ['Anonymisierungskonzept', 'Re-Identifizierungs-Risiko-Bewertung', 'Pseudonymisierungs-Policy'],
addressesRiskIds: ['R-AI-DATA-02', 'R-AI-SEC-03', 'R-AI-PRIV-02'],
effectiveness: 'high',
},
{
id: 'M-AI-PRIV-03',
type: 'organizational',
sdmGoals: ['datenminimierung'],
title: 'Datenminimierung im KI-Lebenszyklus',
description: 'Systematische Ueberpruefung und Minimierung der verarbeiteten Daten in jeder Phase des KI-Lebenszyklus: Training, Validierung, Inferenz. Loeschkonzept fuer nicht mehr benoetigte Daten.',
legalBasis: 'Art. 5 Abs. 1 lit. c DSGVO, Art. 10 AI Act',
evidenceTypes: ['Datenminimierungs-Assessment', 'Loeschkonzept-KI', 'Datenbestandsbericht'],
addressesRiskIds: ['R-AI-DATA-03', 'R-AI-PRIV-01'],
effectiveness: 'medium',
},
{
id: 'M-AI-PRIV-04',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'Machine Unlearning / Modell-Bereinigung',
description: 'Faehigkeit, einzelne Datenpunkte nachtraeglich aus dem trainierten Modell zu entfernen (Machine Unlearning). Unterstuetzung des Rechts auf Loeschung (Art. 17 DSGVO) auch fuer Trainingsdaten.',
legalBasis: 'Art. 17 DSGVO',
evidenceTypes: ['Unlearning-Verfahrensbeschreibung', 'Loeschanfrage-Protokoll', 'Verifikations-Test'],
addressesRiskIds: ['R-AI-SEC-03', 'R-AI-PRIV-02'],
effectiveness: 'medium',
},
{
id: 'M-AI-PRIV-05',
type: 'technical',
sdmGoals: ['vertraulichkeit', 'nichtverkettung'],
title: 'Self-Hosting / On-Premises KI-Betrieb',
description: 'Betrieb des KI-Systems auf eigener Infrastruktur (Self-Hosting/On-Premises) oder in EU-Rechenzentren, um Drittlandtransfers zu vermeiden und Datensouveraenitaet zu gewaehrleisten.',
legalBasis: 'Art. 44 ff. DSGVO',
evidenceTypes: ['Hosting-Dokumentation', 'Standort-Nachweis', 'Infrastruktur-Audit'],
addressesRiskIds: ['R-AI-PRIV-03'],
effectiveness: 'high',
},
{
id: 'M-AI-PRIV-06',
type: 'legal',
sdmGoals: ['nichtverkettung'],
title: 'Standardvertragsklauseln und TIA fuer KI-Cloud-Dienste',
description: 'Bei Nutzung von Cloud-basierten KI-Diensten mit Drittlandtransfer: Abschluss von Standardvertragsklauseln (SCC), Durchfuehrung eines Transfer Impact Assessment (TIA) und ergaenzende Massnahmen.',
legalBasis: 'Art. 46 Abs. 2 lit. c DSGVO, Schrems-II',
evidenceTypes: ['SCC-Vertrag', 'Transfer-Impact-Assessment', 'Ergaenzende-Massnahmen-Dokumentation'],
addressesRiskIds: ['R-AI-PRIV-03'],
effectiveness: 'medium',
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Gibt KI-Massnahmen zurueck, die ein bestimmtes Risiko adressieren
*/
export function getAIMitigationsForRisk(riskId: string): CatalogMitigation[] {
return AI_MITIGATION_LIBRARY.filter(m => m.addressesRiskIds.includes(riskId))
}
/**
* Gibt KI-Massnahmen zurueck, die einem bestimmten SDM-Gewaehrleistungsziel dienen
*/
export function getAIMitigationsBySDMGoal(goal: string): CatalogMitigation[] {
return AI_MITIGATION_LIBRARY.filter(m => m.sdmGoals.includes(goal as any))
}
/**
* Gibt alle technischen KI-Massnahmen zurueck
*/
export function getTechnicalAIMitigations(): CatalogMitigation[] {
return AI_MITIGATION_LIBRARY.filter(m => m.type === 'technical')
}
@@ -0,0 +1,375 @@
/**
* DSFA KI-Risikokatalog - Vordefinierte KI-spezifische Risiken
*
* ~25 Risiken gegliedert nach:
* - Bias & Diskriminierung
* - Erklaerbarkeit & Transparenz
* - Datenqualitaet & Training
* - Sicherheit & Robustheit
* - Automatisierte Entscheidungen
* - Ueberwachung & Kontrolle
* - Privatsphaere & Datenschutz
*
* Quellen: AI Act (EU 2024/1689), Art. 22 DSGVO, EDPB Guidelines,
* BSI-TR-03161, SDM V2.0
*/
import type { CatalogRisk } from './risk-catalog'
// =============================================================================
// KI-RISIKOKATALOG
// =============================================================================
export const AI_RISK_CATALOG: CatalogRisk[] = [
// =========================================================================
// BIAS & DISKRIMINIERUNG
// =========================================================================
{
id: 'R-AI-BIAS-01',
category: 'rights_freedoms',
sdmGoal: 'nichtverkettung',
title: 'Algorithmische Diskriminierung durch verzerrte Trainingsdaten',
description: 'Das KI-System reproduziert oder verstaerkt bestehende gesellschaftliche Vorurteile durch unausgewogene oder historisch verzerrte Trainingsdaten, was zu diskriminierenden Ergebnissen fuehrt.',
impactExamples: ['Benachteiligung geschuetzter Gruppen', 'Ungleichbehandlung bei Bewerbungsverfahren', 'Diskriminierende Kreditvergabe'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K7', 'K8'],
applicableTo: ['decision_support', 'automated_processing', 'recommendation', 'predictive', 'analytics'],
mitigationIds: ['M-AI-BIAS-01', 'M-AI-BIAS-02', 'M-AI-DATA-01'],
},
{
id: 'R-AI-BIAS-02',
category: 'rights_freedoms',
sdmGoal: 'nichtverkettung',
title: 'Feedback-Loop-Verstaerkung von Bias',
description: 'KI-Entscheidungen beeinflussen kuenftige Trainingsdaten und verstaerken damit bestehende Verzerrungen in einem Teufelskreis (Feedback Loop).',
impactExamples: ['Zunehmende Polarisierung', 'Verstaerkte Ungleichbehandlung ueber Zeit', 'Systematische Benachteiligung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K8'],
applicableTo: ['recommendation', 'predictive', 'automated_processing'],
mitigationIds: ['M-AI-BIAS-02', 'M-AI-MON-01', 'M-AI-MON-02'],
},
{
id: 'R-AI-BIAS-03',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Proxy-Diskriminierung durch korrelierte Merkmale',
description: 'Das KI-System diskriminiert indirekt anhand von Merkmalen, die mit geschuetzten Eigenschaften korrelieren (z.B. Postleitzahl als Proxy fuer Ethnizitaet).',
impactExamples: ['Indirekte Diskriminierung', 'Verletzung des Gleichheitsgrundsatzes', 'Schwer nachweisbare Benachteiligung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K6'],
applicableTo: ['predictive', 'decision_support', 'automated_processing', 'analytics'],
mitigationIds: ['M-AI-BIAS-01', 'M-AI-BIAS-03', 'M-AI-EXPL-01'],
},
// =========================================================================
// ERKLAERBARKEIT & TRANSPARENZ
// =========================================================================
{
id: 'R-AI-EXPL-01',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Fehlende Erklaerbarkeit von KI-Entscheidungen (Black Box)',
description: 'Entscheidungen des KI-Systems koennen nicht nachvollzogen oder erklaert werden, was die Wahrnehmung von Betroffenenrechten und die Aufsicht erschwert.',
impactExamples: ['Betroffene koennen Entscheidungen nicht anfechten', 'Aufsichtsbehoerden koennen nicht pruefen', 'Vertrauensverlust'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K2', 'K8', 'K9'],
applicableTo: ['decision_support', 'automated_processing', 'predictive', 'recommendation'],
mitigationIds: ['M-AI-EXPL-01', 'M-AI-EXPL-02', 'M-AI-EXPL-03'],
},
{
id: 'R-AI-EXPL-02',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Taeuschung ueber KI-Einsatz (fehlende Kennzeichnung)',
description: 'Nutzer wissen nicht, dass sie mit einem KI-System interagieren oder dass Inhalte KI-generiert sind (Verstoss gegen Transparenzpflicht AI Act Art. 50).',
impactExamples: ['Vertrauensmissbrauch', 'Manipulation durch Deepfakes', 'Fehlende informierte Einwilligung'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K8'],
applicableTo: ['chatbot', 'content_generation', 'speech_processing', 'image_recognition'],
mitigationIds: ['M-AI-EXPL-03', 'M-AI-TRANS-01'],
},
{
id: 'R-AI-EXPL-03',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Unzureichende Dokumentation der KI-Entscheidungslogik',
description: 'Die Funktionsweise des KI-Systems ist nicht ausreichend dokumentiert, um die gesetzlichen Anforderungen an Technische Dokumentation (Art. 11 AI Act) zu erfuellen.',
impactExamples: ['Verstoss gegen Dokumentationspflichten', 'Keine Reproduzierbarkeit', 'Erschwerter Audit'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K8'],
applicableTo: ['decision_support', 'automated_processing', 'predictive', 'recommendation', 'analytics'],
mitigationIds: ['M-AI-EXPL-02', 'M-AI-DOC-01'],
},
// =========================================================================
// DATENQUALITAET & TRAINING
// =========================================================================
{
id: 'R-AI-DATA-01',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Schlechte Trainingsdatenqualitaet fuehrt zu fehlerhaften Ergebnissen',
description: 'Unvollstaendige, veraltete oder fehlerhafte Trainingsdaten fuehren zu unzuverlaessigen KI-Ergebnissen, die Entscheidungen negativ beeinflussen.',
impactExamples: ['Falsche Vorhersagen', 'Fehlerhafte Klassifizierungen', 'Unzuverlaessige Empfehlungen'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K8'],
applicableTo: ['predictive', 'decision_support', 'recommendation', 'image_recognition', 'analytics'],
mitigationIds: ['M-AI-DATA-01', 'M-AI-DATA-02', 'M-AI-MON-01'],
},
{
id: 'R-AI-DATA-02',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Unbefugte Nutzung personenbezogener Daten im Training',
description: 'Personenbezogene Daten werden ohne ausreichende Rechtsgrundlage oder Einwilligung fuer das Training des KI-Modells verwendet.',
impactExamples: ['Verletzung des Zweckbindungsgrundsatzes', 'Fehlende Rechtsgrundlage', 'Verstoss gegen Informationspflichten'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K5', 'K8'],
applicableTo: ['content_generation', 'recommendation', 'predictive', 'speech_processing', 'image_recognition'],
mitigationIds: ['M-AI-DATA-03', 'M-AI-PRIV-01', 'M-AI-PRIV-02'],
},
{
id: 'R-AI-DATA-03',
category: 'integrity',
sdmGoal: 'datenminimierung',
title: 'Uebermassige Datenerhebung fuer KI-Training',
description: 'Fuer das Training werden mehr personenbezogene Daten erhoben als fuer den Zweck erforderlich, was den Grundsatz der Datenminimierung verletzt.',
impactExamples: ['Verstoss gegen Art. 5 Abs. 1 lit. c DSGVO', 'Erhoehtes Risiko bei Datenleck', 'Unnoetige Profilbildung'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K5', 'K8'],
applicableTo: ['predictive', 'recommendation', 'analytics', 'speech_processing'],
mitigationIds: ['M-AI-PRIV-01', 'M-AI-PRIV-03', 'M-AI-DATA-01'],
},
{
id: 'R-AI-DATA-04',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Data Poisoning / Manipulation der Trainingsdaten',
description: 'Boesartige Akteure fuegen gezielt manipulierte Daten in den Trainingsdatensatz ein, um das Verhalten des KI-Systems zu verfaelschen.',
impactExamples: ['Gezielte Fehlklassifizierungen', 'Umgehung von Sicherheitsmechanismen', 'Vertrauensverlust'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K8'],
applicableTo: ['image_recognition', 'content_generation', 'predictive', 'decision_support'],
mitigationIds: ['M-AI-SEC-01', 'M-AI-DATA-02', 'M-AI-SEC-03'],
},
// =========================================================================
// SICHERHEIT & ROBUSTHEIT
// =========================================================================
{
id: 'R-AI-SEC-01',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Adversarial Attacks auf KI-Modell',
description: 'Angreifer nutzen gezielt manipulierte Eingaben (Adversarial Examples), um das KI-System zu falschen Ausgaben zu verleiten.',
impactExamples: ['Umgehung von Schutzmechanismen', 'Falsche Identifikation', 'Fehlerhafte Entscheidungen'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K8'],
applicableTo: ['image_recognition', 'speech_processing', 'decision_support', 'automated_processing'],
mitigationIds: ['M-AI-SEC-01', 'M-AI-SEC-02', 'M-AI-MON-01'],
},
{
id: 'R-AI-SEC-02',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Prompt Injection / Jailbreaking',
description: 'Nutzer umgehen durch manipulierte Eingaben die Sicherheitsschranken des KI-Systems und extrahieren vertrauliche Informationen oder loesen unerwuenschtes Verhalten aus.',
impactExamples: ['Offenlegung von Systemprompts', 'Extraktion von Trainingsdaten', 'Generierung schaedlicher Inhalte'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K4', 'K8'],
applicableTo: ['chatbot', 'content_generation', 'decision_support'],
mitigationIds: ['M-AI-SEC-03', 'M-AI-SEC-04', 'M-AI-MON-02'],
},
{
id: 'R-AI-SEC-03',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Model Inversion / Membership Inference',
description: 'Angreifer rekonstruieren Trainingsdaten aus dem KI-Modell oder stellen fest, ob bestimmte Daten im Training verwendet wurden.',
impactExamples: ['Rekonstruktion personenbezogener Daten', 'Verletzung der Vertraulichkeit', 'Re-Identifizierung anonymisierter Daten'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K8'],
applicableTo: ['predictive', 'recommendation', 'image_recognition', 'content_generation'],
mitigationIds: ['M-AI-PRIV-02', 'M-AI-SEC-01', 'M-AI-PRIV-04'],
},
{
id: 'R-AI-SEC-04',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Halluzination / Generierung falscher Informationen',
description: 'Das KI-System generiert plausibel klingende aber faktisch falsche Informationen (Halluzinationen), die zu Fehlentscheidungen fuehren koennen.',
impactExamples: ['Falsche Rechtsauskunft', 'Fehlerhafte medizinische Empfehlung', 'Vertrauensverlust in das System'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K8', 'K9'],
applicableTo: ['chatbot', 'content_generation', 'decision_support', 'translation'],
mitigationIds: ['M-AI-SEC-05', 'M-AI-MON-01', 'M-AI-AUTO-02'],
},
// =========================================================================
// AUTOMATISIERTE ENTSCHEIDUNGEN (ART. 22 DSGVO)
// =========================================================================
{
id: 'R-AI-AUTO-01',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Automatisierte Einzelentscheidung ohne menschliche Beteiligung',
description: 'Das KI-System trifft Entscheidungen mit Rechtswirkung ohne angemessene menschliche Beteiligung, was gegen Art. 22 Abs. 1 DSGVO verstoesst.',
impactExamples: ['Rechtswidrige automatisierte Ablehnung', 'Fehlende Anfechtungsmoeglichkeit', 'Verletzung der Menschenwuerde'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K2', 'K8', 'K9'],
applicableTo: ['automated_processing', 'decision_support', 'predictive'],
mitigationIds: ['M-AI-AUTO-01', 'M-AI-AUTO-02', 'M-AI-AUTO-03'],
},
{
id: 'R-AI-AUTO-02',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Unzureichende menschliche Aufsicht (Human Oversight)',
description: 'Die menschliche Aufsicht ueber das KI-System ist unzureichend oder pro forma, sodass problematische Entscheidungen nicht erkannt und korrigiert werden.',
impactExamples: ['Automation Bias bei Entscheidern', 'Blindes Vertrauen in KI-Ergebnis', 'Fehlende Korrekturmoeglichkeit'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K2', 'K8'],
applicableTo: ['decision_support', 'automated_processing', 'predictive', 'analytics'],
mitigationIds: ['M-AI-AUTO-01', 'M-AI-AUTO-04', 'M-AI-EXPL-01'],
},
{
id: 'R-AI-AUTO-03',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Fehlende Widerspruchsmoeglichkeit bei KI-Entscheidungen',
description: 'Betroffene haben keine effektive Moeglichkeit, einer KI-gestuetzten Entscheidung zu widersprechen oder eine manuelle Ueberpruefung zu verlangen.',
impactExamples: ['Verletzung von Art. 22 Abs. 3 DSGVO', 'Rechtsschutzluecke', 'Machtungleichgewicht'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K2', 'K9'],
applicableTo: ['automated_processing', 'decision_support', 'predictive'],
mitigationIds: ['M-AI-AUTO-03', 'M-AI-AUTO-05'],
},
// =========================================================================
// UEBERWACHUNG & KONTROLLE
// =========================================================================
{
id: 'R-AI-MON-01',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Model Drift - Verschlechterung der KI-Leistung ueber Zeit',
description: 'Die Genauigkeit und Zuverlaessigkeit des KI-Modells verschlechtert sich durch veraenderte Datenverteilungen (Data Drift) oder Konzeptaenderungen (Concept Drift).',
impactExamples: ['Zunehmend fehlerhafte Entscheidungen', 'Unbemerkte Qualitaetsverschlechterung', 'Veraenderte Diskriminierungsmuster'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K8'],
applicableTo: ['predictive', 'recommendation', 'decision_support', 'analytics', 'image_recognition'],
mitigationIds: ['M-AI-MON-01', 'M-AI-MON-02', 'M-AI-DATA-02'],
},
{
id: 'R-AI-MON-02',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Fehlende Notfallmassnahmen bei KI-Fehlfunktion',
description: 'Es existieren keine Fallback-Mechanismen oder Notfallprozeduren fuer den Fall, dass das KI-System fehlerhafte oder schaedliche Ergebnisse produziert.',
impactExamples: ['Langandauernde Stoerung', 'Kaskadierende Fehler', 'Keine manuelle Alternative'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K8'],
applicableTo: ['automated_processing', 'decision_support', 'chatbot', 'predictive'],
mitigationIds: ['M-AI-MON-03', 'M-AI-AUTO-01', 'M-AI-MON-04'],
},
// =========================================================================
// PRIVATSPHAERE & DATENSCHUTZ
// =========================================================================
{
id: 'R-AI-PRIV-01',
category: 'confidentiality',
sdmGoal: 'datenminimierung',
title: 'Unbeabsichtigte Profilbildung durch KI-Analyse',
description: 'Das KI-System erstellt durch Zusammenfuehrung und Analyse verschiedener Datenpunkte detaillierte Persoenlichkeitsprofile, die ueber den urspruenglichen Verarbeitungszweck hinausgehen.',
impactExamples: ['Unerlaubtes Profiling', 'Verletzung der Zweckbindung', 'Erstellung umfassender Persoenlichkeitsbilder'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K6', 'K8'],
applicableTo: ['analytics', 'recommendation', 'predictive', 'chatbot'],
mitigationIds: ['M-AI-PRIV-01', 'M-AI-PRIV-03', 'M-AI-DATA-03'],
},
{
id: 'R-AI-PRIV-02',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Datenleck durch KI-Ausgaben (Output Leakage)',
description: 'Das KI-System gibt in seinen Antworten unbeabsichtigt personenbezogene oder vertrauliche Daten aus den Trainingsdaten preis.',
impactExamples: ['Preisgabe von Trainingsdaten', 'Offenlegung vertraulicher Geschaeftsinformationen', 'Verletzung des Berufsgeheimnisses'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K8'],
applicableTo: ['chatbot', 'content_generation', 'decision_support', 'translation'],
mitigationIds: ['M-AI-PRIV-02', 'M-AI-SEC-04', 'M-AI-PRIV-04'],
},
{
id: 'R-AI-PRIV-03',
category: 'confidentiality',
sdmGoal: 'nichtverkettung',
title: 'Drittlandtransfer durch Cloud-basierte KI-Dienste',
description: 'Personenbezogene Daten werden zur KI-Verarbeitung in Drittlaender uebertragen, ohne dass angemessene Garantien bestehen (Art. 44 ff. DSGVO).',
impactExamples: ['Unzulaessiger Drittlandtransfer', 'Zugriff durch auslaendische Behoerden', 'Verlust der Datensouveraenitaet'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K5', 'K8'],
applicableTo: ['chatbot', 'content_generation', 'translation', 'speech_processing', 'image_recognition'],
mitigationIds: ['M-AI-PRIV-05', 'M-AI-PRIV-06'],
},
{
id: 'R-AI-PRIV-04',
category: 'rights_freedoms',
sdmGoal: 'datenminimierung',
title: 'Verarbeitung besonderer Datenkategorien durch KI ohne explizite Einwilligung',
description: 'Das KI-System verarbeitet oder leitet besondere Kategorien personenbezogener Daten ab (Art. 9 DSGVO), z.B. Gesundheitsdaten aus Schreibmustern oder ethnische Herkunft aus Bildern.',
impactExamples: ['Verstoss gegen Art. 9 DSGVO', 'Diskriminierung aufgrund sensibler Merkmale', 'Verletzung der Menschenwuerde'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K7', 'K8'],
applicableTo: ['image_recognition', 'speech_processing', 'analytics', 'predictive'],
mitigationIds: ['M-AI-PRIV-01', 'M-AI-DATA-03', 'M-AI-BIAS-01'],
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Gibt KI-Risiken zurueck, die fuer einen bestimmten Use-Case-Typ relevant sind
*/
export function getAIRisksForUseCaseType(useCaseType: string): CatalogRisk[] {
return AI_RISK_CATALOG.filter(risk => risk.applicableTo.includes(useCaseType))
}
/**
* Gibt KI-Risiken zurueck, die einem bestimmten SDM-Gewaehrleistungsziel zugeordnet sind
*/
export function getAIRisksBySDMGoal(goal: string): CatalogRisk[] {
return AI_RISK_CATALOG.filter(risk => risk.sdmGoal === goal)
}
/**
* Gibt alle KI-Risiken zurueck, die fuer Art. 22 DSGVO relevant sind
*/
export function getArt22Risks(): CatalogRisk[] {
return AI_RISK_CATALOG.filter(risk => risk.wp248Criteria.includes('K2'))
}
+399
View File
@@ -0,0 +1,399 @@
/**
* DSFA API Client
*
* API client functions for DSFA (Data Protection Impact Assessment) endpoints.
*/
import type {
DSFA,
DSFAListResponse,
DSFAStatsResponse,
CreateDSFARequest,
CreateDSFAFromAssessmentRequest,
CreateDSFAFromAssessmentResponse,
UpdateDSFASectionRequest,
SubmitForReviewResponse,
ApproveDSFARequest,
DSFATriggerInfo,
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const getBaseUrl = () => {
if (typeof window !== 'undefined') {
// Browser environment
return process.env.NEXT_PUBLIC_SDK_API_URL || '/api/sdk/v1'
}
// Server environment
return process.env.SDK_API_URL || 'http://localhost:8080/api/sdk/v1'
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep HTTP status message
}
throw new Error(errorMessage)
}
return response.json()
}
function getHeaders(): HeadersInit {
return {
'Content-Type': 'application/json',
}
}
// =============================================================================
// DSFA CRUD OPERATIONS
// =============================================================================
/**
* List all DSFAs for the current tenant
*/
export async function listDSFAs(status?: string): Promise<DSFA[]> {
const url = new URL(`${getBaseUrl()}/dsgvo/dsfas`, window.location.origin)
if (status) {
url.searchParams.set('status', status)
}
const response = await fetch(url.toString(), {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
const data = await handleResponse<DSFAListResponse>(response)
return data.dsfas || []
}
/**
* Get a single DSFA by ID
*/
export async function getDSFA(id: string): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<DSFA>(response)
}
/**
* Create a new DSFA
*/
export async function createDSFA(data: CreateDSFARequest): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<DSFA>(response)
}
/**
* Update an existing DSFA
*/
export async function updateDSFA(id: string, data: Partial<DSFA>): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
method: 'PUT',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<DSFA>(response)
}
/**
* Delete a DSFA
*/
export async function deleteDSFA(id: string): Promise<void> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
method: 'DELETE',
headers: getHeaders(),
credentials: 'include',
})
if (!response.ok) {
throw new Error(`Failed to delete DSFA: ${response.statusText}`)
}
}
// =============================================================================
// DSFA SECTION OPERATIONS
// =============================================================================
/**
* Update a specific section of a DSFA
*/
export async function updateDSFASection(
id: string,
sectionNumber: number,
data: UpdateDSFASectionRequest
): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/sections/${sectionNumber}`, {
method: 'PUT',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<DSFA>(response)
}
// =============================================================================
// DSFA WORKFLOW OPERATIONS
// =============================================================================
/**
* Submit a DSFA for DPO review
*/
export async function submitDSFAForReview(id: string): Promise<SubmitForReviewResponse> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/submit-for-review`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<SubmitForReviewResponse>(response)
}
/**
* Approve or reject a DSFA (DPO/CISO/GF action)
*/
export async function approveDSFA(id: string, data: ApproveDSFARequest): Promise<{ message: string }> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/approve`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<{ message: string }>(response)
}
// =============================================================================
// DSFA STATISTICS
// =============================================================================
/**
* Get DSFA statistics for the dashboard
*/
export async function getDSFAStats(): Promise<DSFAStatsResponse> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/stats`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<DSFAStatsResponse>(response)
}
// =============================================================================
// UCCA INTEGRATION
// =============================================================================
/**
* Create a DSFA from a UCCA assessment (pre-filled)
*/
export async function createDSFAFromAssessment(
assessmentId: string,
data?: CreateDSFAFromAssessmentRequest
): Promise<CreateDSFAFromAssessmentResponse> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/from-assessment/${assessmentId}`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data || {}),
})
return handleResponse<CreateDSFAFromAssessmentResponse>(response)
}
/**
* Get a DSFA by its linked UCCA assessment ID
*/
export async function getDSFAByAssessment(assessmentId: string): Promise<DSFA | null> {
try {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/by-assessment/${assessmentId}`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
if (response.status === 404) {
return null
}
return handleResponse<DSFA>(response)
} catch (error) {
// Return null if DSFA not found
return null
}
}
/**
* Check if a DSFA is required for a UCCA assessment
*/
export async function checkDSFARequired(assessmentId: string): Promise<DSFATriggerInfo> {
const response = await fetch(`${getBaseUrl()}/ucca/assessments/${assessmentId}/dsfa-required`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<DSFATriggerInfo>(response)
}
// =============================================================================
// EXPORT
// =============================================================================
/**
* Export a DSFA as JSON
*/
export async function exportDSFAAsJSON(id: string): Promise<Blob> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/export?format=json`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
credentials: 'include',
})
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`)
}
return response.blob()
}
/**
* Export a DSFA as PDF
*/
export async function exportDSFAAsPDF(id: string): Promise<Blob> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/export/pdf`, {
method: 'GET',
headers: {
'Accept': 'application/pdf',
},
credentials: 'include',
})
if (!response.ok) {
throw new Error(`PDF export failed: ${response.statusText}`)
}
return response.blob()
}
// =============================================================================
// RISK & MITIGATION OPERATIONS
// =============================================================================
/**
* Add a risk to a DSFA
*/
export async function addDSFARisk(dsfaId: string, risk: {
category: string
description: string
likelihood: 'low' | 'medium' | 'high'
impact: 'low' | 'medium' | 'high'
affected_data?: string[]
}): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const newRisk = {
id: crypto.randomUUID(),
...risk,
risk_level: calculateRiskLevelString(risk.likelihood, risk.impact),
affected_data: risk.affected_data || [],
}
const updatedRisks = [...(dsfa.risks || []), newRisk]
return updateDSFA(dsfaId, { risks: updatedRisks } as Partial<DSFA>)
}
/**
* Remove a risk from a DSFA
*/
export async function removeDSFARisk(dsfaId: string, riskId: string): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const updatedRisks = (dsfa.risks || []).filter(r => r.id !== riskId)
return updateDSFA(dsfaId, { risks: updatedRisks } as Partial<DSFA>)
}
/**
* Add a mitigation to a DSFA
*/
export async function addDSFAMitigation(dsfaId: string, mitigation: {
risk_id: string
description: string
type: 'technical' | 'organizational' | 'legal'
responsible_party: string
}): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const newMitigation = {
id: crypto.randomUUID(),
...mitigation,
status: 'planned' as const,
residual_risk: 'medium' as const,
}
const updatedMitigations = [...(dsfa.mitigations || []), newMitigation]
return updateDSFA(dsfaId, { mitigations: updatedMitigations } as Partial<DSFA>)
}
/**
* Update mitigation status
*/
export async function updateDSFAMitigationStatus(
dsfaId: string,
mitigationId: string,
status: 'planned' | 'in_progress' | 'implemented' | 'verified'
): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const updatedMitigations = (dsfa.mitigations || []).map(m => {
if (m.id === mitigationId) {
return {
...m,
status,
...(status === 'implemented' && { implemented_at: new Date().toISOString() }),
...(status === 'verified' && { verified_at: new Date().toISOString() }),
}
}
return m
})
return updateDSFA(dsfaId, { mitigations: updatedMitigations } as Partial<DSFA>)
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function calculateRiskLevelString(
likelihood: 'low' | 'medium' | 'high',
impact: 'low' | 'medium' | 'high'
): string {
const matrix: Record<string, Record<string, string>> = {
low: { low: 'low', medium: 'low', high: 'medium' },
medium: { low: 'low', medium: 'medium', high: 'high' },
high: { low: 'medium', medium: 'high', high: 'very_high' },
}
return matrix[likelihood]?.[impact] || 'medium'
}
@@ -0,0 +1,622 @@
/**
* EU/EWR Rechtsgrundlagen pro Land
*
* Strukturierter Katalog der datenschutzrechtlichen Grundlagen fuer EU/EWR/DACH.
* 3-Schicht-Architektur: EU-Basis → Nationale Ergaenzungen → Dokumentspezifische Bausteine.
*
* Quellen: Amtliche Rechtstexte (gemeinfrei gemaess §5 UrhG / Art. 1 EU-Beschluss 2011/833/EU),
* EDPB-Leitlinien (CC-BY-4.0), nationale Aufsichtsbehoerden.
*/
// =============================================================================
// Types
// =============================================================================
export type CountryCode =
| 'EU' // EU-weite Basis
| 'DE' // Deutschland
| 'AT' // Oesterreich
| 'CH' // Schweiz (nicht EU, eigenes DSG)
| 'FR' // Frankreich
| 'ES' // Spanien
| 'IT' // Italien
| 'NL' // Niederlande
| 'GB' // Grossbritannien (post-Brexit)
| 'NO' // Norwegen (EWR)
| 'IS' // Island (EWR)
export type LegalDocumentType =
| 'regulation' // EU-Verordnung (unmittelbar geltendes Recht)
| 'directive' // EU-Richtlinie (nationale Umsetzung noetig)
| 'national_law' // Nationales Gesetz
| 'guideline' // Behoerdliche Leitlinie
| 'supervisory' // Aufsichtsbehoerden-Praxis
export type LicenseType =
| 'PUBLIC_DOMAIN' // Amtliche Werke, gemeinfrei
| 'CC-BY-4.0' // Creative Commons Attribution
| 'OGL-3.0' // UK Open Government Licence
| 'DL-DE-BY-2.0' // Datenlizenz Deutschland
/** Welche SDK-Dokumenttypen sind EU-weit einheitlich vs. laenderspezifisch? */
export type DocumentUniformity = 'eu_uniform' | 'needs_national_supplement' | 'country_specific'
// =============================================================================
// Interfaces
// =============================================================================
export interface LegalFramework {
id: string
countryCode: CountryCode
name: string
fullName: string
abbreviation: string
type: LegalDocumentType
description: string
sourceUrl: string | null
license: LicenseType
licenseNote: string
/** Welche DSGVO-Oeffnungsklauseln bedient dieses Gesetz? */
gdprOpeningClauses?: string[]
/** Spezialregelungen, die ueber die DSGVO hinausgehen */
specialProvisions?: string[]
/** Zustaendige Aufsichtsbehoerde(n) */
supervisoryAuthorities?: SupervisoryAuthority[]
/** Relevanz-Phase: wann sollte diese Quelle ins RAG? */
ragPhase: 1 | 2 | 3
}
export interface SupervisoryAuthority {
name: string
abbreviation: string
url: string
country: CountryCode
}
export interface DocumentTypeMatrix {
documentType: string
label: string
uniformity: DocumentUniformity
description: string
/** Welche Laender brauchen spezifische Logik? */
countrySpecificNotes?: Record<CountryCode, string>
}
// =============================================================================
// EU-Basis (Phase 1 — gilt fuer gesamte EU/EWR)
// =============================================================================
export const EU_BASE_FRAMEWORKS: LegalFramework[] = [
{
id: 'EU-GDPR',
countryCode: 'EU',
name: 'DSGVO / GDPR',
fullName: 'Verordnung (EU) 2016/679 — Datenschutz-Grundverordnung',
abbreviation: 'DSGVO',
type: 'regulation',
description:
'Die EU-Datenschutz-Grundverordnung gilt unmittelbar in allen EU-Mitgliedstaaten. ' +
'GDPR und DSGVO sind identisch — nur unterschiedliche Sprachfassungen derselben Verordnung. ' +
'Kern des europaeischen Datenschutzrechts.',
sourceUrl: 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/deu',
license: 'CC-BY-4.0',
licenseNote: 'EU-Recht, EUR-Lex, Wiederverwendung gemaess Beschluss 2011/833/EU',
ragPhase: 1,
},
{
id: 'EU-EPRIVACY',
countryCode: 'EU',
name: 'ePrivacy-Richtlinie',
fullName: 'Richtlinie 2002/58/EG — Datenschutz in der elektronischen Kommunikation',
abbreviation: 'ePrivacy-RL',
type: 'directive',
description:
'Ergaenzt die DSGVO fuer elektronische Kommunikation (Cookies, Tracking, Direktmarketing). ' +
'Als Richtlinie national umgesetzt (DE: TTDSG, FR: Loi Informatique et Libertés, etc.).',
sourceUrl: 'https://eur-lex.europa.eu/eli/dir/2002/58/oj',
license: 'CC-BY-4.0',
licenseNote: 'EU-Recht, EUR-Lex, Wiederverwendung gemaess Beschluss 2011/833/EU',
ragPhase: 1,
},
{
id: 'EU-AI-ACT',
countryCode: 'EU',
name: 'AI Act',
fullName: 'Verordnung (EU) 2024/1689 — KI-Verordnung',
abbreviation: 'AI Act',
type: 'regulation',
description:
'EU-weite Regulierung kuenstlicher Intelligenz. Risikobasierter Ansatz mit Verboten (Art. 5), ' +
'Hochrisiko-Anforderungen und Transparenzpflichten. Gilt unmittelbar in allen Mitgliedstaaten.',
sourceUrl: 'https://eur-lex.europa.eu/eli/reg/2024/1689/oj/deu',
license: 'CC-BY-4.0',
licenseNote: 'EU-Recht, EUR-Lex, Wiederverwendung gemaess Beschluss 2011/833/EU',
ragPhase: 1,
},
{
id: 'EU-EDPB',
countryCode: 'EU',
name: 'EDPB-Leitlinien',
fullName: 'Leitlinien des Europaeischen Datenschutzausschusses',
abbreviation: 'EDPB',
type: 'guideline',
description:
'Verbindliche Auslegungshilfen zur DSGVO (z.B. DSFA, Art. 25, Art. 28, Drittlandtransfer, ' +
'Pseudonymisierung). Gelten als autoritaetive Rechtsquelle in der gesamten EU.',
sourceUrl: 'https://edpb.europa.eu/our-work-tools/general-guidance/guidelines-recommendations-best-practices_en',
license: 'CC-BY-4.0',
licenseNote: 'EDPB-Publikationen, CC BY 4.0',
ragPhase: 1,
},
]
// =============================================================================
// Nationale Ergaenzungsgesetze (Phase 2 — modular pro Land)
// =============================================================================
export const NATIONAL_FRAMEWORKS: LegalFramework[] = [
// --- Deutschland ---
{
id: 'DE-BDSG',
countryCode: 'DE',
name: 'BDSG',
fullName: 'Bundesdatenschutzgesetz (2018)',
abbreviation: 'BDSG',
type: 'national_law',
description:
'Nationales Begleitgesetz zur DSGVO. Ergaenzt u.a. Beschaeftigtendatenschutz (§26), ' +
'Videoueberwachung (§4), Forschung/Statistik, Bussgeldpraxis.',
sourceUrl: 'https://www.gesetze-im-internet.de/bdsg_2018/',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Werk, gemeinfrei (§5 UrhG)',
gdprOpeningClauses: ['Art. 6 Abs. 2', 'Art. 9 Abs. 4', 'Art. 23', 'Art. 85', 'Art. 88'],
specialProvisions: [
'§26 BDSG — Beschaeftigtendatenschutz',
'§4 BDSG — Videoueberwachung oeffentlich zugaenglicher Raeume',
'§22 BDSG — Verarbeitung besonderer Kategorien',
'§41-43 BDSG — Straf- und Bussgeldvorschriften',
],
supervisoryAuthorities: [
{ name: 'Bundesbeauftragter fuer den Datenschutz', abbreviation: 'BfDI', url: 'https://www.bfdi.bund.de', country: 'DE' },
],
ragPhase: 2,
},
{
id: 'DE-TTDSG',
countryCode: 'DE',
name: 'TTDSG',
fullName: 'Telekommunikation-Telemedien-Datenschutz-Gesetz',
abbreviation: 'TTDSG',
type: 'national_law',
description:
'Deutsche Umsetzung der ePrivacy-Richtlinie. Regelt insbesondere Cookie-Consent (§25 TTDSG), ' +
'Endgeraetezugriff und Telekommunikations-Datenschutz.',
sourceUrl: 'https://www.gesetze-im-internet.de/ttdsg/',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Werk, gemeinfrei (§5 UrhG)',
specialProvisions: [
'§25 TTDSG — Einwilligung fuer Cookies/Tracking',
'§26 TTDSG — Anerkannte Dienste zur Einwilligungsverwaltung',
],
ragPhase: 2,
},
{
id: 'DE-TMG',
countryCode: 'DE',
name: 'TMG / DDG',
fullName: 'Telemediengesetz / Digitale-Dienste-Gesetz',
abbreviation: 'TMG',
type: 'national_law',
description:
'Impressumspflicht (§5 TMG/DDG) und Anbieterkennzeichnung fuer Online-Dienste in Deutschland.',
sourceUrl: 'https://www.gesetze-im-internet.de/tmg/',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Werk, gemeinfrei (§5 UrhG)',
specialProvisions: [
'§5 TMG — Impressumspflicht (Anbieterkennzeichnung)',
'§7-10 TMG — Verantwortlichkeit von Diensteanbietern',
],
ragPhase: 3,
},
// --- Oesterreich ---
{
id: 'AT-DSG',
countryCode: 'AT',
name: 'DSG (AT)',
fullName: 'Datenschutzgesetz (Oesterreich, 2018)',
abbreviation: 'DSG',
type: 'national_law',
description:
'Oesterreichisches Begleitgesetz zur DSGVO. Enthält Besonderheiten fuer Behoerden, ' +
'Strafverfolgung und teilweise andere Auslegungspraxis als Deutschland.',
sourceUrl: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10001597',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Werk, Rechtsinformationssystem des Bundes (RIS)',
supervisoryAuthorities: [
{ name: 'Oesterreichische Datenschutzbehoerde', abbreviation: 'DSB', url: 'https://www.dsb.gv.at', country: 'AT' },
],
ragPhase: 2,
},
// --- Schweiz (NICHT EU — eigenes Recht) ---
{
id: 'CH-DSG',
countryCode: 'CH',
name: 'revDSG (CH)',
fullName: 'Bundesgesetz ueber den Datenschutz (revidiertes DSG, seit 01.09.2023)',
abbreviation: 'revDSG',
type: 'national_law',
description:
'Die Schweiz ist nicht EU-Mitglied. Das revidierte DSG (2023) ist inhaltlich aehnlich der DSGVO, ' +
'aber nicht identisch. Unterschiede: andere Sanktionslogik (Busse bis 250.000 CHF gegen ' +
'natuerliche Personen), teils andere Begriffe, kein One-Stop-Shop.',
sourceUrl: 'https://www.fedlex.admin.ch/eli/cc/2022/491/de',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Bundesrecht, Fedlex (Schweiz)',
specialProvisions: [
'Art. 60-66 revDSG — Strafbestimmungen (gegen natuerliche Personen)',
'Art. 16-18 revDSG — Drittlandtransfer (eigene Laenderliste)',
'Art. 22 revDSG — Datenschutz-Folgenabschaetzung',
'Art. 12 revDSG — Verzeichnis der Bearbeitungstaetigkeiten',
],
supervisoryAuthorities: [
{ name: 'Eidgenoessischer Datenschutzbeauftragter', abbreviation: 'EDOEB', url: 'https://www.edoeb.admin.ch', country: 'CH' },
],
ragPhase: 2,
},
// --- Frankreich ---
{
id: 'FR-LIL',
countryCode: 'FR',
name: 'Loi Informatique et Libertés',
fullName: 'Loi n° 78-17 du 6 janvier 1978 relative à l\'informatique, aux fichiers et aux libertés',
abbreviation: 'LIL',
type: 'national_law',
description:
'Franzoesisches Begleitgesetz zur DSGVO (aktualisiert 2018). Spezialregelungen u.a. ' +
'zur Einwilligung Minderjaehriger (ab 15 Jahren), Forschungsdaten und Gesundheitsdaten.',
sourceUrl: 'https://www.legifrance.gouv.fr/loda/id/JORFTEXT000000886460',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Gesetz, Légifrance (gemeinfrei)',
supervisoryAuthorities: [
{ name: 'Commission Nationale de l\'Informatique et des Libertés', abbreviation: 'CNIL', url: 'https://www.cnil.fr', country: 'FR' },
],
ragPhase: 2,
},
// --- Spanien ---
{
id: 'ES-LOPDGDD',
countryCode: 'ES',
name: 'LOPDGDD',
fullName: 'Ley Orgánica 3/2018 de Protección de Datos Personales y garantía de los derechos digitales',
abbreviation: 'LOPDGDD',
type: 'national_law',
description:
'Spanisches Datenschutzgesetz. Ergaenzt DSGVO u.a. mit Regelungen zu ' +
'Kindereinwilligung, digitalem Testament und Rechten Verstorbener.',
sourceUrl: 'https://www.boe.es/diario_boe/txt.php?id=BOE-A-2018-16673',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Gesetz, Boletín Oficial del Estado (gemeinfrei)',
supervisoryAuthorities: [
{ name: 'Agencia Española de Protección de Datos', abbreviation: 'AEPD', url: 'https://www.aepd.es', country: 'ES' },
],
ragPhase: 2,
},
// --- Italien ---
{
id: 'IT-CODICE',
countryCode: 'IT',
name: 'Codice Privacy',
fullName: 'Decreto Legislativo 30 giugno 2003, n. 196 (Codice in materia di protezione dei dati personali)',
abbreviation: 'Codice Privacy',
type: 'national_law',
description:
'Italienischer Datenschutzkodex, angepasst an die DSGVO (D.Lgs. 101/2018). ' +
'Enthaelt Spezialregelungen fuer Gesundheitsdaten, Forschung und Journalismus.',
sourceUrl: 'https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:decreto.legislativo:2003-06-30;196!vig=',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Gesetz, Normattiva (gemeinfrei)',
supervisoryAuthorities: [
{ name: 'Garante per la protezione dei dati personali', abbreviation: 'Garante', url: 'https://www.garanteprivacy.it', country: 'IT' },
],
ragPhase: 2,
},
// --- Niederlande ---
{
id: 'NL-AVG',
countryCode: 'NL',
name: 'AVG / UAVG',
fullName: 'Uitvoeringswet Algemene verordening gegevensbescherming (UAVG)',
abbreviation: 'UAVG',
type: 'national_law',
description:
'Niederlaendisches Ausfuehrungsgesetz zur DSGVO.',
sourceUrl: 'https://wetten.overheid.nl/BWBR0040948/',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Gesetz, wetten.overheid.nl (gemeinfrei)',
supervisoryAuthorities: [
{ name: 'Autoriteit Persoonsgegevens', abbreviation: 'AP', url: 'https://www.autoriteitpersoonsgegevens.nl', country: 'NL' },
],
ragPhase: 2,
},
// --- Grossbritannien (post-Brexit) ---
{
id: 'GB-DPA',
countryCode: 'GB',
name: 'UK DPA 2018 / UK GDPR',
fullName: 'Data Protection Act 2018 + UK GDPR (retained EU law)',
abbreviation: 'DPA 2018',
type: 'national_law',
description:
'Nach Brexit: UK GDPR (inhaltlich weitgehend identisch mit EU-DSGVO) plus Data Protection Act 2018 ' +
'als nationales Begleitgesetz. ICO als Aufsichtsbehoerde.',
sourceUrl: 'https://www.legislation.gov.uk/ukpga/2018/12/contents',
license: 'OGL-3.0',
licenseNote: 'UK legislation, Open Government Licence v3.0',
supervisoryAuthorities: [
{ name: 'Information Commissioner\'s Office', abbreviation: 'ICO', url: 'https://ico.org.uk', country: 'GB' },
],
ragPhase: 2,
},
// --- Norwegen (EWR) ---
{
id: 'NO-PERSONOPPL',
countryCode: 'NO',
name: 'Personopplysningsloven',
fullName: 'Lov om behandling av personopplysninger (personopplysningsloven)',
abbreviation: 'POL',
type: 'national_law',
description:
'Norwegisches DSGVO-Ausfuehrungsgesetz (EWR-Mitglied, DSGVO gilt ueber EWR-Abkommen).',
sourceUrl: 'https://lovdata.no/dokument/NL/lov/2018-06-15-38',
license: 'PUBLIC_DOMAIN',
licenseNote: 'Amtliches Gesetz, Lovdata (gemeinfrei)',
supervisoryAuthorities: [
{ name: 'Datatilsynet', abbreviation: 'DT', url: 'https://www.datatilsynet.no', country: 'NO' },
],
ragPhase: 2,
},
]
// =============================================================================
// Dokumenttyp-Matrix: EU-einheitlich vs. laenderspezifisch
// =============================================================================
export const DOCUMENT_TYPE_MATRIX: DocumentTypeMatrix[] = [
{
documentType: 'privacy_policy',
label: 'Datenschutzerklaerung',
uniformity: 'needs_national_supplement',
description: 'DSGVO-Kern EU-weit gleich. Nationale Ergaenzungen fuer ePrivacy-Umsetzung, Behoerden-Praxis.',
},
{
documentType: 'ropa',
label: 'Verarbeitungsverzeichnis (VVT)',
uniformity: 'eu_uniform',
description: 'Art. 30 DSGVO — EU-weit identische Anforderungen.',
},
{
documentType: 'tom',
label: 'Technisch-Organisatorische Massnahmen',
uniformity: 'eu_uniform',
description: 'Art. 32 DSGVO — EU-weit identische Anforderungen.',
},
{
documentType: 'dpia',
label: 'Datenschutz-Folgenabschaetzung (DSFA)',
uniformity: 'eu_uniform',
description: 'Art. 35 DSGVO — EU-weit identisch. Muss-Listen variieren je Aufsichtsbehoerde.',
},
{
documentType: 'dpa',
label: 'Auftragsverarbeitungsvertrag (AVV)',
uniformity: 'eu_uniform',
description: 'Art. 28 DSGVO — EU-weit identische Anforderungen.',
},
{
documentType: 'deletion_concept',
label: 'Loeschkonzept',
uniformity: 'eu_uniform',
description: 'Art. 5(1)(e), Art. 17 DSGVO — EU-weit einheitlich.',
},
{
documentType: 'breach_process',
label: 'Data Breach / Incident Response',
uniformity: 'eu_uniform',
description: 'Art. 33-34 DSGVO — EU-weit identische 72-Stunden-Frist.',
},
{
documentType: 'dsar_process',
label: 'Betroffenenrechte-Prozess (DSAR)',
uniformity: 'eu_uniform',
description: 'Art. 12-22 DSGVO — EU-weit identische Rechte und Fristen.',
},
{
documentType: 'imprint',
label: 'Impressum',
uniformity: 'country_specific',
description: 'Kein DSGVO-Thema. Nationale Mediengesetze (DE: §5 TMG, AT: §5 ECG, CH: eigene Regeln).',
countrySpecificNotes: {
'EU': 'Keine EU-weite Regelung',
'DE': '§5 TMG / DDG — strenge Impressumspflicht',
'AT': '§5 ECG — aehnlich wie DE, nicht identisch',
'CH': 'Obligationenrecht + kantonale Regeln',
'FR': 'Loi pour la Confiance dans l\'Économie Numérique (LCEN)',
'ES': 'LSSI-CE Art. 10',
'IT': 'D.Lgs. 70/2003',
'NL': 'Handelsregisterpflicht + BW',
'GB': 'Companies Act 2006',
'NO': 'E-handelsloven',
'IS': 'Rafraeðislög',
},
},
{
documentType: 'terms_of_service',
label: 'AGB / Nutzungsbedingungen',
uniformity: 'country_specific',
description: 'Nationales Vertragsrecht (BGB, ABGB, OR). Verbraucherrecht teils EU-harmonisiert, aber national umgesetzt.',
},
{
documentType: 'withdrawal_notice',
label: 'Widerrufsbelehrung',
uniformity: 'country_specific',
description: 'EU-Verbraucherrechterichtlinie national umgesetzt. DE/AT: Muster-Widerrufsbelehrung. CH: eigene Logik.',
},
{
documentType: 'cookie_banner',
label: 'Cookie-Banner / Consent',
uniformity: 'needs_national_supplement',
description: 'ePrivacy + DSGVO EU-weit aehnlich, aber Aufsichtspraxis variiert (CNIL vs. DSK vs. DPC etc.).',
},
]
// =============================================================================
// RAG-Schichtmodell
// =============================================================================
export interface RAGLayer {
phase: 1 | 2 | 3
name: string
description: string
scope: string
sources: string[]
}
export const RAG_LAYERS: RAGLayer[] = [
{
phase: 1,
name: 'EU-Basis',
description: 'Einmal laden — gilt fuer gesamte EU/EWR (ausser CH)',
scope: 'EU/EWR',
sources: [
'DSGVO Volltext (EU 2016/679)',
'ePrivacy-Richtlinie (2002/58/EG)',
'AI Act (EU 2024/1689)',
'EDPB-Leitlinien (DSFA, Art. 25, Art. 28, Art. 32, Drittlandtransfer etc.)',
],
},
{
phase: 2,
name: 'Nationale Ergaenzungen',
description: 'Modular pro Land — nationale Begleitgesetze zur DSGVO',
scope: 'Je Land',
sources: [
'DE: BDSG, TTDSG, DSK-Kurzpapiere',
'AT: DSG (AT), DSB-Entscheidungen',
'CH: revDSG, EDOEB-Leitlinien (separater Stack!)',
'FR: Loi Informatique et Libertés, CNIL-Leitfaeden',
'ES: LOPDGDD, AEPD-Leitfaeden',
'IT: Codice Privacy, Garante-Leitfaeden',
'NL: UAVG, AP-Leitlinien',
'GB: UK DPA 2018, ICO Guidance',
],
},
{
phase: 3,
name: 'Dokumentspezifische Bausteine',
description: 'Templates und Mustertexte fuer laenderspezifische Dokumenttypen',
scope: 'Je Land + Dokumenttyp',
sources: [
'Impressum-Templates (DE §5 TMG, AT §5 ECG, CH)',
'AGB-Bausteine (SaaS/Webshop, B2B/B2C)',
'Widerrufsbelehrung (DE/AT Muster)',
'Cookie-Banner-Texte (EU-weit + Feinjustierung)',
],
},
]
// =============================================================================
// Helper Functions
// =============================================================================
/** Alle Rechtsgrundlagen zusammen (EU-Basis + National) */
export function getAllFrameworks(): LegalFramework[] {
return [...EU_BASE_FRAMEWORKS, ...NATIONAL_FRAMEWORKS]
}
/** Rechtsgrundlagen fuer ein bestimmtes Land (inkl. EU-Basis) */
export function getFrameworksForCountry(country: CountryCode): LegalFramework[] {
return getAllFrameworks().filter(
f => f.countryCode === country || f.countryCode === 'EU'
)
}
/** Nur nationale Ergaenzungsgesetze (ohne EU-Basis) */
export function getNationalFrameworks(country: CountryCode): LegalFramework[] {
return NATIONAL_FRAMEWORKS.filter(f => f.countryCode === country)
}
/** Alle Aufsichtsbehoerden */
export function getAllSupervisoryAuthorities(): SupervisoryAuthority[] {
const authorities: SupervisoryAuthority[] = []
for (const fw of getAllFrameworks()) {
if (fw.supervisoryAuthorities) {
for (const sa of fw.supervisoryAuthorities) {
if (!authorities.some(a => a.abbreviation === sa.abbreviation)) {
authorities.push(sa)
}
}
}
}
return authorities
}
/** Aufsichtsbehoerde(n) fuer ein Land */
export function getSupervisoryAuthority(country: CountryCode): SupervisoryAuthority[] {
return getAllSupervisoryAuthorities().filter(sa => sa.country === country)
}
/** Dokumenttypen, die fuer ein Land spezifische Logik brauchen */
export function getCountrySpecificDocTypes(country: CountryCode): DocumentTypeMatrix[] {
return DOCUMENT_TYPE_MATRIX.filter(
d => d.uniformity === 'country_specific' ||
(d.uniformity === 'needs_national_supplement' && country !== 'EU')
)
}
/** Dokumenttypen, die EU-weit einheitlich generiert werden koennen */
export function getEUUniformDocTypes(): DocumentTypeMatrix[] {
return DOCUMENT_TYPE_MATRIX.filter(d => d.uniformity === 'eu_uniform')
}
/** Pruefen ob ein Land EU/EWR-Mitglied ist (DSGVO direkt anwendbar) */
export function isGDPRCountry(country: CountryCode): boolean {
const gdprCountries: CountryCode[] = ['EU', 'DE', 'AT', 'FR', 'ES', 'IT', 'NL', 'NO', 'IS']
return gdprCountries.includes(country)
}
/** Pruefen ob ein Land einen separaten Rechtsrahmen hat (nicht DSGVO) */
export function hasSeparateLegalFramework(country: CountryCode): boolean {
return country === 'CH' || country === 'GB'
}
/** RAG-Quellen fuer eine bestimmte Phase */
export function getRAGSourcesForPhase(phase: 1 | 2 | 3): LegalFramework[] {
return getAllFrameworks().filter(f => f.ragPhase === phase)
}
/** Zusammenfassung: Was braucht ein Unternehmen in Land X? */
export function getRequiredFrameworkSummary(country: CountryCode): {
baseLaw: string
nationalLaw: string | null
supervisoryAuthority: string | null
separateFramework: boolean
} {
const isGDPR = isGDPRCountry(country)
const national = getNationalFrameworks(country)
const authorities = getSupervisoryAuthority(country)
return {
baseLaw: isGDPR ? 'DSGVO (EU 2016/679)' : (country === 'CH' ? 'revDSG (CH)' : 'UK GDPR'),
nationalLaw: national.length > 0 ? national.map(n => n.abbreviation).join(', ') : null,
supervisoryAuthority: authorities.length > 0 ? authorities.map(a => a.abbreviation).join(', ') : null,
separateFramework: hasSeparateLegalFramework(country),
}
}
@@ -0,0 +1,433 @@
/**
* DSGVO-Bussgeldentscheidungen und Durchsetzungsfaelle
*
* Strukturierter Katalog relevanter Bussgelder und Gerichtsentscheidungen
* als Referenz fuer DSFA-Risikobewertung und Compliance-Beratung.
*
* Quellen: Amtliche Entscheidungen/Bescheide (§5 UrhG), EDPB-Mitteilungen (CC-BY-4.0)
*/
// =============================================================================
// Types
// =============================================================================
export type GDPRViolationType =
| 'data_minimization' // Art. 5 — Datenminimierung/Zweckbindung
| 'security' // Art. 32 — Technische/Org. Massnahmen
| 'special_categories' // Art. 9 — Besondere Kategorien
| 'transparency' // Art. 12-14 — Informationspflichten
| 'lawfulness' // Art. 6 — Rechtsmaessigkeit
| 'data_subject_rights' // Art. 15-22 — Betroffenenrechte
| 'dpo_conflict' // Art. 38 — DSB-Interessenkonflikt
| 'accountability' // Art. 5 Abs. 2 — Rechenschaftspflicht
| 'international_transfer' // Art. 44-49 — Drittlandtransfer
export type EnforcementOutcome = 'final' | 'reduced_on_appeal' | 'pending' | 'overturned'
export interface GDPREnforcementCase {
id: string
company: string
country: string
authority: string
year: number
fineOriginal: number // Urspruengliches Bussgeld in Euro
fineAfterAppeal?: number // Nach Berufung (falls reduziert)
outcome: EnforcementOutcome
violationTypes: GDPRViolationType[]
gdprArticles: string[] // Verletzte Artikel
description: string
keyFacts: string[] // Wesentliche Tatbestandsmerkmale
lessons: string[] // Lehren fuer Unternehmen
/** Nur amtliche Quellen (Bescheide, Urteile, EDPB) */
officialSources: OfficialSource[]
}
export interface OfficialSource {
type: 'court_decision' | 'dsb_decision' | 'edpb_news' | 'bfdi_press'
title: string
reference?: string // Aktenzeichen
date?: string
url?: string
licenseNote: string // Lizenzhinweis
}
export interface GDPRLesson {
id: string
gdprArticles: string[]
title: string
description: string
relatedCaseIds: string[]
severity: 'critical' | 'high' | 'medium'
}
// =============================================================================
// Enforcement Cases
// =============================================================================
export const GDPR_ENFORCEMENT_CASES: GDPREnforcementCase[] = [
{
id: 'CASE-HM-2020',
company: 'H&M (Nuernberg)',
country: 'DE',
authority: 'Hamburger Datenschutzbehoerde (HmbBfDI)',
year: 2020,
fineOriginal: 35_300_000,
outcome: 'final',
violationTypes: ['special_categories', 'lawfulness'],
gdprArticles: ['Art. 6 DSGVO', 'Art. 9 DSGVO'],
description:
'Systematische Ausspaeung von Beschaeftigtendaten. In sog. "Welcome-Back-Talks" nach ' +
'Krankheit wurden private Informationen zu Gesundheit, Familie und Religion erfasst und ' +
'in einer Datenbank gespeichert. Die Daten waren fuer Fuehrungskraefte zugaenglich.',
keyFacts: [
'Erfassung und Auswertung privater Gesundheitsinformationen',
'Systematische Dokumentation in Datenbank',
'Zugang fuer breiten Kreis von Fuehrungskraeften',
'Verarbeitung besonderer Kategorien (Art. 9) ohne Rechtsgrundlage',
],
lessons: [
'Gesundheitsdaten duerfen nur mit strenger Rechtsgrundlage verarbeitet werden',
'Mitarbeiter-Gespraeche duerfen nicht zur verdeckten Datenerhebung missbraucht werden',
'Besondere Kategorien (Art. 9) erfordern besondere Schutzmassnahmen',
],
officialSources: [
{
type: 'dsb_decision',
title: 'HmbBfDI Bussgeld gegen H&M Servicecenter',
date: '2020-10-01',
licenseNote: 'Amtlicher Bescheid gem. §5 UrhG',
},
],
},
{
id: 'CASE-DW-2019',
company: 'Deutsche Wohnen SE (Berlin)',
country: 'DE',
authority: 'Berliner Beauftragte fuer Datenschutz (BlnBDI)',
year: 2019,
fineOriginal: 14_500_000,
outcome: 'final',
violationTypes: ['data_minimization', 'accountability'],
gdprArticles: ['Art. 5 DSGVO', 'Art. 25 DSGVO'],
description:
'Langjaehrige Speicherung von Mieterdaten ohne Rechtsgrundlage. Das Archivsystem erlaubte ' +
'keine Loeschung veralteter Daten (Gehaltsabrechnungen, Kontodaten, Mietvertraege). ' +
'Trotz vorheriger Beanstandung keine technischen Massnahmen zur Loeschung umgesetzt.',
keyFacts: [
'Archivsystem ohne Loeschmoeglichkeit fuer veraltete Daten',
'Vorherige Beanstandung durch Aufsichtsbehoerde ohne Abhilfe',
'Verstoss gegen Datenminimierung und Speicherbegrenzung',
'Keine Privacy-by-Design-Massnahmen (Art. 25)',
],
lessons: [
'Loeschkonzepte muessen technisch umsetzbar sein',
'Vorherige Beanstandungen erhoehen das Bussgeld erheblich',
'Art. 25 (Privacy by Design) betrifft auch bestehende Systeme',
],
officialSources: [
{
type: 'edpb_news',
title: 'Berlin Commissioner for Data Protection Imposes Fine on Real Estate Company',
url: 'https://www.edpb.europa.eu/news/national-news/2019/berlin-commissioner-data-protection-imposes-fine-real-estate-company_en',
licenseNote: 'EDPB Nachricht, CC-BY-4.0',
},
],
},
{
id: 'CASE-1U1-2019',
company: '1&1 Telecom GmbH',
country: 'DE',
authority: 'BfDI (Bundesbeauftragter)',
year: 2019,
fineOriginal: 9_550_000,
fineAfterAppeal: 900_000,
outcome: 'reduced_on_appeal',
violationTypes: ['security'],
gdprArticles: ['Art. 32 DSGVO'],
description:
'Unzureichende Kunden-Authentifizierung: Allein Name und Geburtsdatum genuegten fuer ' +
'telefonische Auskuenfte. Das LG Bonn bestaetigte den Verstoss, reduzierte das Bussgeld ' +
'jedoch auf 900.000 Euro, da kein massiver Datenskandal vorlag und das Unternehmen kooperativ war.',
keyFacts: [
'Nur Name und Geburtsdatum als Authentifizierung',
'Verstoesse gegen Art. 32 (technisch-organisatorische Massnahmen)',
'LG Bonn reduzierte Bussgeld von 9,55 Mio. auf 0,9 Mio. Euro',
'Kooperatives Verhalten als mildernder Umstand',
],
lessons: [
'Starke Authentifizierung ist Pflicht bei Kundenkontakt',
'Art. 32 verlangt dem Risiko angemessene Sicherheit',
'Kooperation mit der Aufsichtsbehoerde kann Bussgelder deutlich reduzieren',
],
officialSources: [
{
type: 'bfdi_press',
title: 'BfDI verhaengt Geldbusse gegen Telekommunikationsdienstleister',
date: '2019-12-09',
url: 'https://www.bfdi.bund.de/SharedDocs/Pressemitteilungen/DE/2019/30_BfDIverh%C3%A4ngtGeldbu%C3%9Fe1u1.html',
licenseNote: 'Amtliche Pressemitteilung gem. §5 UrhG',
},
{
type: 'court_decision',
title: 'LG Bonn, Urteil v. 11.11.2020 — Reduzierung auf 900.000 Euro',
reference: 'LG Bonn, 2020',
date: '2020-11-11',
licenseNote: 'Amtliches Werk gem. §5 UrhG',
},
],
},
{
id: 'CASE-WA-2021',
company: 'WhatsApp Ireland Ltd.',
country: 'IE',
authority: 'Irish Data Protection Commission (DPC)',
year: 2021,
fineOriginal: 225_000_000,
outcome: 'final',
violationTypes: ['transparency'],
gdprArticles: ['Art. 5 DSGVO', 'Art. 12 DSGVO', 'Art. 13 DSGVO', 'Art. 14 DSGVO'],
description:
'Verletzung der Transparenzpflichten: Unvollstaendige Information ueber Datenweitergabe ' +
'innerhalb der Meta/Facebook-Unternehmensgruppe. Betroffene wurden nicht ausreichend darueber ' +
'informiert, welche Daten zu welchen Zwecken mit welchen Empfaengern geteilt wurden.',
keyFacts: [
'Mangelnde Transparenz bei konzerninterner Datenweitergabe',
'Unzureichende Datenschutzhinweise (Art. 12-14)',
'EDPB-Streitbeilegung erhoehte das Bussgeld erheblich',
'Hoechstes Bussgeld gegen ein Unternehmen in der EU zum damaligen Zeitpunkt',
],
lessons: [
'Datenschutzhinweise muessen vollstaendig und verstaendlich sein',
'Konzerninterne Datenweitergabe muss transparent dokumentiert werden',
'Art. 12 verlangt klare, einfache Sprache',
],
officialSources: [
{
type: 'dsb_decision',
title: 'DPC Decision re WhatsApp Ireland Limited',
date: '2021-09-02',
licenseNote: 'Amtliche Entscheidung der irischen Aufsichtsbehoerde',
},
],
},
{
id: 'CASE-GOOGLE-2019',
company: 'Google LLC',
country: 'FR',
authority: 'CNIL (Franzoesische Datenschutzbehoerde)',
year: 2019,
fineOriginal: 50_000_000,
outcome: 'final',
violationTypes: ['transparency', 'lawfulness'],
gdprArticles: ['Art. 6 DSGVO', 'Art. 12 DSGVO', 'Art. 13 DSGVO'],
description:
'Maengel bei Einwilligung und Transparenz: Informationen zur Datenverarbeitung waren ueber ' +
'mehrere Seiten verstreut und schwer verstaendlich. Einwilligung fuer personalisierte Werbung ' +
'war vorausgewaehlt und nicht hinreichend spezifisch.',
keyFacts: [
'Informationen ueber 5-6 Klicks verstreut',
'Vorausgewaehlte Einwilligung (kein aktives Opt-in)',
'Einwilligung nicht granular genug',
'Erstes grosses DSGVO-Bussgeld gegen US-Tech-Konzern',
],
lessons: [
'Einwilligung muss aktiv, informiert und spezifisch sein',
'Datenschutzinformationen muessen leicht zugaenglich sein',
'Vorausgewaehlte Checkboxen sind keine gueltige Einwilligung',
],
officialSources: [
{
type: 'dsb_decision',
title: 'CNIL Deliberation No. SAN-2019-001 v. 21.01.2019',
date: '2019-01-21',
licenseNote: 'Amtliche Entscheidung der CNIL',
},
],
},
{
id: 'CASE-OLG-DD-2021',
company: 'GmbH-Geschaeftsfuehrer (Detektiveinsatz)',
country: 'DE',
authority: 'OLG Dresden',
year: 2021,
fineOriginal: 5_000, // Schadensersatz, kein Bussgeld
outcome: 'final',
violationTypes: ['lawfulness'],
gdprArticles: ['Art. 6 DSGVO', 'Art. 10 DSGVO', 'Art. 82 DSGVO'],
description:
'Persoenliche Haftung des Geschaeftsfuehrers: Ein GmbH-GF liess eine Privatperson durch ' +
'einen Detektiv ausspionieren. Das OLG Dresden sprach dem Betroffenen 5.000 Euro Schadensersatz ' +
'zu — gegen GmbH und Geschaeftsfuehrer als Gesamtschuldner. Begruendung: Der GF entscheide ' +
'selbst ueber die Datenverarbeitung und sei daher Verantwortlicher i.S.d. Art. 4 Nr. 7 DSGVO.',
keyFacts: [
'Geschaeftsfuehrer als Verantwortlicher (Art. 4 Nr. 7)',
'Persoenliche Haftung neben der GmbH (Gesamtschuldner)',
'Schadensersatz nach Art. 82 DSGVO',
'Ausspionierung durch Detektiv = Verstoss gegen Art. 6, 10',
],
lessons: [
'Geschaeftsfuehrer koennen persoenlich fuer DSGVO-Verstoesse haften',
'Art. 82 DSGVO ermoeglicht Schadensersatz gegen natuerliche Personen',
'Verantwortlichkeit i.S.d. Art. 4 Nr. 7 kann auch den GF persoenlich treffen',
],
officialSources: [
{
type: 'court_decision',
title: 'OLG Dresden, Urteil 2021 — Persoenliche GF-Haftung bei DSGVO-Verstoss',
reference: 'OLG Dresden, 2021',
date: '2021',
licenseNote: 'Amtliches Werk gem. §5 UrhG',
},
],
},
]
// =============================================================================
// Lessons Learned (aggregiert)
// =============================================================================
export const GDPR_LESSONS: GDPRLesson[] = [
{
id: 'LESSON-01',
gdprArticles: ['Art. 5 DSGVO', 'Art. 25 DSGVO'],
title: 'Datenminimierung und Loeschkonzepte sind Pflicht',
description:
'Personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer den Zweck ' +
'notwendig ist. Technische Loeschmechanismen muessen implementiert sein. Archivsysteme ' +
'ohne Loeschfunktion verstoessen gegen Art. 5 und Art. 25 DSGVO.',
relatedCaseIds: ['CASE-DW-2019'],
severity: 'critical',
},
{
id: 'LESSON-02',
gdprArticles: ['Art. 32 DSGVO'],
title: 'Angemessene IT-Sicherheit (Art. 32) ist unverzichtbar',
description:
'Technisch-organisatorische Massnahmen muessen dem Risiko angemessen sein. ' +
'Schwache Authentifizierung (z.B. nur Name/Geburtsdatum) reicht nicht aus. ' +
'Verschluesselung, Zugriffskontrollen und starke Authentifizierung sind Pflicht.',
relatedCaseIds: ['CASE-1U1-2019'],
severity: 'critical',
},
{
id: 'LESSON-03',
gdprArticles: ['Art. 9 DSGVO'],
title: 'Besondere Kategorien erfordern besondere Vorsicht',
description:
'Gesundheitsdaten, religioese Ueberzeugungen und andere besondere Kategorien (Art. 9) ' +
'duerfen nur mit expliziter Rechtsgrundlage verarbeitet werden. Informelle Gespraeche ' +
'duerfen nicht zur verdeckten Erhebung solcher Daten missbraucht werden.',
relatedCaseIds: ['CASE-HM-2020'],
severity: 'critical',
},
{
id: 'LESSON-04',
gdprArticles: ['Art. 12 DSGVO', 'Art. 13 DSGVO', 'Art. 14 DSGVO'],
title: 'Transparenz und vollstaendige Datenschutzhinweise',
description:
'Betroffene muessen klar, verstaendlich und vollstaendig informiert werden. ' +
'Konzerninterne Datenweitergabe muss dokumentiert sein. Informationen duerfen ' +
'nicht ueber zahlreiche Unterseiten verstreut werden.',
relatedCaseIds: ['CASE-WA-2021', 'CASE-GOOGLE-2019'],
severity: 'high',
},
{
id: 'LESSON-05',
gdprArticles: ['Art. 82 DSGVO', 'Art. 4 Nr. 7 DSGVO'],
title: 'Persoenliche Haftung von Geschaeftsfuehrern moeglich',
description:
'Geschaeftsfuehrer koennen neben dem Unternehmen persoenlich fuer DSGVO-Verstoesse ' +
'haften, wenn sie die Datenverarbeitung selbst veranlassen. Dies gilt insbesondere ' +
'bei vorsaetzlichen Verstoessen.',
relatedCaseIds: ['CASE-OLG-DD-2021'],
severity: 'high',
},
{
id: 'LESSON-06',
gdprArticles: ['Art. 83 DSGVO'],
title: 'Kooperation kann Bussgelder deutlich reduzieren',
description:
'Kooperatives Verhalten und zeitnahe Massnahmen zur Abhilfe werden von Aufsichtsbehoerden ' +
'und Gerichten als mildernde Umstaende anerkannt. Im 1&1-Fall wurde das Bussgeld um ueber ' +
'90% reduziert.',
relatedCaseIds: ['CASE-1U1-2019'],
severity: 'medium',
},
]
// =============================================================================
// Violation Type Labels
// =============================================================================
export const VIOLATION_TYPE_LABELS: Record<GDPRViolationType, string> = {
data_minimization: 'Datenminimierung / Speicherbegrenzung',
security: 'IT-Sicherheit (Art. 32)',
special_categories: 'Besondere Kategorien (Art. 9)',
transparency: 'Transparenz / Informationspflichten',
lawfulness: 'Rechtsmaessigkeit (Art. 6)',
data_subject_rights: 'Betroffenenrechte',
dpo_conflict: 'DSB-Interessenkonflikt',
accountability: 'Rechenschaftspflicht',
international_transfer: 'Drittlandtransfer',
}
// =============================================================================
// Helper Functions
// =============================================================================
/**
* Gibt Enforcement Cases zurueck, die fuer bestimmte DSGVO-Artikel relevant sind.
*/
export function getEnforcementCasesForArticle(article: string): GDPREnforcementCase[] {
return GDPR_ENFORCEMENT_CASES.filter(c =>
c.gdprArticles.some(a => a.toLowerCase().includes(article.toLowerCase()))
)
}
/**
* Gibt Enforcement Cases zurueck, die fuer einen bestimmten Verstosstyp relevant sind.
*/
export function getEnforcementCasesForViolationType(type: GDPRViolationType): GDPREnforcementCase[] {
return GDPR_ENFORCEMENT_CASES.filter(c => c.violationTypes.includes(type))
}
/**
* Gibt Lessons zurueck, die fuer bestimmte DSGVO-Artikel relevant sind.
*/
export function getLessonsForArticle(article: string): GDPRLesson[] {
return GDPR_LESSONS.filter(l =>
l.gdprArticles.some(a => a.toLowerCase().includes(article.toLowerCase()))
)
}
/**
* Gibt das hoechste bekannte Bussgeld fuer einen Verstosstyp zurueck.
*/
export function getMaxFineForViolationType(type: GDPRViolationType): {
amount: number
company: string
year: number
} | null {
const cases = getEnforcementCasesForViolationType(type)
if (cases.length === 0) return null
const sorted = [...cases].sort((a, b) => b.fineOriginal - a.fineOriginal)
return {
amount: sorted[0].fineOriginal,
company: sorted[0].company,
year: sorted[0].year,
}
}
/**
* Formatiert einen Euro-Betrag fuer die Anzeige.
*/
export function formatFineAmount(amount: number): string {
if (amount >= 1_000_000) {
return `${(amount / 1_000_000).toFixed(1)} Mio. Euro`
}
if (amount >= 1_000) {
return `${(amount / 1_000).toFixed(0)}.000 Euro`
}
return `${amount.toLocaleString('de-DE')} Euro`
}
+10
View File
@@ -0,0 +1,10 @@
/**
* DSFA Module
*
* Exports for DSFA (Data Protection Impact Assessment) functionality.
*/
export * from './types'
export * from './api'
export * from './risk-catalog'
export * from './mitigation-library'
@@ -0,0 +1,694 @@
/**
* DSFA Massnahmenbibliothek - Vordefinierte Massnahmen
*
* ~50 Massnahmen gegliedert nach SDM-Gewaehrleistungszielen
* (Vertraulichkeit, Integritaet, Verfuegbarkeit, Datenminimierung,
* Transparenz, Nichtverkettung, Intervenierbarkeit) sowie
* Automatisierung/KI, Rechtlich/Organisatorisch.
*
* Quellen: Art. 25/32 DSGVO, SDM V2.0, BSI Grundschutz,
* Baseline-DSFA Katalog
*/
import type { DSFAMitigationType } from './types'
import type { SDMGoal } from './types'
// =============================================================================
// TYPES
// =============================================================================
export interface CatalogMitigation {
id: string
type: DSFAMitigationType
sdmGoals: SDMGoal[]
title: string
description: string
legalBasis: string
evidenceTypes: string[]
addressesRiskIds: string[]
effectiveness: 'low' | 'medium' | 'high'
}
// =============================================================================
// MASSNAHMENBIBLIOTHEK
// =============================================================================
export const MITIGATION_LIBRARY: CatalogMitigation[] = [
// =========================================================================
// VERTRAULICHKEIT (Access Control & Encryption)
// =========================================================================
{
id: 'M-ACC-01',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'Multi-Faktor-Authentifizierung (MFA) & Conditional Access',
description: 'Einfuehrung von MFA fuer alle Benutzerkonten mit Zugriff auf personenbezogene Daten. Conditional Access Policies beschraenken den Zugriff basierend auf Standort, Geraet und Risikobewertung.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['MFA-Policy-Screenshot', 'Conditional-Access-Regeln', 'Login-Statistiken'],
addressesRiskIds: ['R-CONF-02', 'R-CONF-06'],
effectiveness: 'high',
},
{
id: 'M-ACC-02',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'Passwort-Policy & Credential-Schutz',
description: 'Durchsetzung starker Passwort-Richtlinien, Credential-Rotation, Einsatz eines Passwort-Managers und Monitoring auf kompromittierte Zugangsdaten (Breach Detection).',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['Passwort-Policy-Dokument', 'Breach-Detection-Report'],
addressesRiskIds: ['R-CONF-02'],
effectiveness: 'medium',
},
{
id: 'M-CONF-01',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'Rollenbasierte Zugriffskontrolle (RBAC) & Least Privilege',
description: 'Implementierung eines RBAC-Systems mit dem Prinzip der minimalen Berechtigung. Jeder Benutzer erhaelt nur die Rechte, die fuer seine Aufgabe erforderlich sind.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO, Art. 25 Abs. 2 DSGVO',
evidenceTypes: ['Rollen-Matrix', 'Berechtigungs-Audit-Report', 'Access-Review-Protokoll'],
addressesRiskIds: ['R-CONF-01', 'R-CONF-03', 'R-INT-04'],
effectiveness: 'high',
},
{
id: 'M-CONF-02',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'Security Configuration Management',
description: 'Regelmaessige Ueberpruefung und Haertung der Systemkonfiguration. Automatisierte Konfigurationschecks (CIS Benchmarks) und Monitoring auf Konfigurationsaenderungen.',
legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
evidenceTypes: ['CIS-Benchmark-Report', 'Konfigurationsaenderungs-Log'],
addressesRiskIds: ['R-CONF-01'],
effectiveness: 'high',
},
{
id: 'M-CONF-03',
type: 'organizational',
sdmGoals: ['vertraulichkeit'],
title: 'Regelmaessige Zugriffsrechte-Ueberpruefung (Access Review)',
description: 'Quartalsweiser Review aller Zugriffsberechtigungen durch Vorgesetzte. Entzug nicht mehr benoetigter Rechte, Offboarding-Prozess bei Mitarbeiteraustritt.',
legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
evidenceTypes: ['Access-Review-Protokoll', 'Offboarding-Checkliste'],
addressesRiskIds: ['R-CONF-01', 'R-CONF-03'],
effectiveness: 'medium',
},
{
id: 'M-CONF-04',
type: 'technical',
sdmGoals: ['vertraulichkeit', 'integritaet'],
title: 'Privileged Access Management (PAM)',
description: 'Absicherung administrativer Zugriffe durch Just-in-Time-Elevation, Session-Recording und Break-Glass-Prozeduren fuer Notfallzugriffe.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['PAM-Policy', 'Session-Recording-Logs', 'Break-Glass-Protokolle'],
addressesRiskIds: ['R-CONF-03', 'R-INT-04'],
effectiveness: 'high',
},
{
id: 'M-CONF-05',
type: 'organizational',
sdmGoals: ['vertraulichkeit'],
title: 'Vier-Augen-Prinzip fuer sensible Operationen',
description: 'Fuer den Zugriff auf besonders schutzwuerdige Daten oder kritische Systemoperationen ist die Genehmigung durch eine zweite Person erforderlich.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['Prozessbeschreibung', 'Genehmigungsprotokoll'],
addressesRiskIds: ['R-CONF-03'],
effectiveness: 'medium',
},
{
id: 'M-CONF-06',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'Verschluesselung at-rest und in-transit',
description: 'Vollstaendige Verschluesselung personenbezogener Daten bei Speicherung (AES-256) und Uebertragung (TLS 1.3). Verwaltung der Schluessel ueber ein zentrales Key-Management-System.',
legalBasis: 'Art. 32 Abs. 1 lit. a DSGVO',
evidenceTypes: ['Verschluesselungs-Policy', 'TLS-Konfigurationsreport', 'KMS-Audit'],
addressesRiskIds: ['R-CONF-04', 'R-TRANS-01', 'R-AUTO-05'],
effectiveness: 'high',
},
{
id: 'M-CONF-07',
type: 'technical',
sdmGoals: ['vertraulichkeit'],
title: 'End-to-End-Verschluesselung fuer Kommunikation',
description: 'Einsatz von End-to-End-Verschluesselung fuer sensible Kommunikation (E-Mail, Messaging), sodass auch der Betreiber keinen Zugriff auf die Inhalte hat.',
legalBasis: 'Art. 32 Abs. 1 lit. a DSGVO',
evidenceTypes: ['E2E-Konfiguration', 'Testbericht'],
addressesRiskIds: ['R-CONF-04'],
effectiveness: 'high',
},
{
id: 'M-CONF-08',
type: 'technical',
sdmGoals: ['vertraulichkeit', 'datenminimierung'],
title: 'Log-Sanitization & PII-Filtering',
description: 'Automatische Filterung personenbezogener Daten aus Logs, Fehlermeldungen und Debug-Ausgaben. Einsatz von Tokenisierung oder Maskierung.',
legalBasis: 'Art. 25 Abs. 1 DSGVO, Art. 32 Abs. 1 lit. a DSGVO',
evidenceTypes: ['Log-Policy', 'PII-Filter-Konfiguration', 'Stichproben-Audit'],
addressesRiskIds: ['R-CONF-07'],
effectiveness: 'medium',
},
// =========================================================================
// INTEGRITAET (Audit, Monitoring, Integrity Checks)
// =========================================================================
{
id: 'M-INT-01',
type: 'technical',
sdmGoals: ['integritaet'],
title: 'Input-Validierung & Injection-Schutz',
description: 'Konsequente Validierung aller Eingaben, Prepared Statements fuer Datenbankzugriffe, Content Security Policy und Output-Encoding zum Schutz vor Injection-Angriffen.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['SAST-Report', 'Penetrationstest-Bericht', 'WAF-Regeln'],
addressesRiskIds: ['R-INT-01'],
effectiveness: 'high',
},
{
id: 'M-INT-02',
type: 'technical',
sdmGoals: ['integritaet', 'transparenz'],
title: 'Audit-Logging & SIEM-Integration',
description: 'Lueckenlose Protokollierung aller sicherheitsrelevanten Ereignisse mit Weiterleitung an ein SIEM-System. Manipulation-sichere Logs mit Integritaetspruefung.',
legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
evidenceTypes: ['SIEM-Dashboard-Screenshot', 'Audit-Log-Beispiel', 'Alert-Regeln'],
addressesRiskIds: ['R-INT-01', 'R-INT-04', 'R-INT-05', 'R-CONF-03', 'R-ORG-04'],
effectiveness: 'high',
},
{
id: 'M-INT-03',
type: 'technical',
sdmGoals: ['integritaet'],
title: 'Web Application Firewall (WAF) & API-Gateway',
description: 'Einsatz einer WAF zum Schutz vor OWASP Top 10 Angriffen und eines API-Gateways fuer Rate-Limiting, Schema-Validierung und Anomalie-Erkennung.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['WAF-Regelset', 'API-Gateway-Konfiguration', 'Blockierungs-Statistiken'],
addressesRiskIds: ['R-INT-01'],
effectiveness: 'medium',
},
{
id: 'M-INT-04',
type: 'technical',
sdmGoals: ['integritaet'],
title: 'Daten-Synchronisations-Monitoring & Integritaetspruefung',
description: 'Automatische Ueberwachung von Synchronisationsprozessen mit Checksummen-Vergleich, Konflikterkennung und Alerting bei Inkonsistenzen.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['Sync-Monitoring-Dashboard', 'Checksummen-Report', 'Incident-Log'],
addressesRiskIds: ['R-INT-02'],
effectiveness: 'medium',
},
{
id: 'M-INT-05',
type: 'technical',
sdmGoals: ['integritaet'],
title: 'Versionierung & Change-Tracking fuer personenbezogene Daten',
description: 'Alle Aenderungen an personenbezogenen Daten werden versioniert gespeichert (Audit-Trail). Wer hat wann was geaendert ist jederzeit nachvollziehbar.',
legalBasis: 'Art. 5 Abs. 1 lit. f DSGVO',
evidenceTypes: ['Versionierungs-Schema', 'Change-Log-Beispiel'],
addressesRiskIds: ['R-INT-02', 'R-INT-05'],
effectiveness: 'medium',
},
// =========================================================================
// VERFUEGBARKEIT (Backup, Recovery, Redundancy)
// =========================================================================
{
id: 'M-AVAIL-01',
type: 'technical',
sdmGoals: ['verfuegbarkeit'],
title: 'Backup-Strategie mit 3-2-1-Regel',
description: 'Implementierung einer Backup-Strategie nach der 3-2-1-Regel: 3 Kopien, 2 verschiedene Medien, 1 Offsite. Verschluesselte Backups mit regelmaessiger Integritaetspruefung.',
legalBasis: 'Art. 32 Abs. 1 lit. c DSGVO',
evidenceTypes: ['Backup-Policy', 'Backup-Monitoring-Report', 'Offsite-Nachweis'],
addressesRiskIds: ['R-AVAIL-01', 'R-AVAIL-03', 'R-INT-03'],
effectiveness: 'high',
},
{
id: 'M-AVAIL-02',
type: 'organizational',
sdmGoals: ['verfuegbarkeit'],
title: 'Regelmaessige Restore-Tests & Disaster Recovery Uebungen',
description: 'Mindestens quartalsweise Durchfuehrung von Restore-Tests und jaehrliche Disaster-Recovery-Uebungen. Dokumentation der Ergebnisse und Lessons Learned.',
legalBasis: 'Art. 32 Abs. 1 lit. d DSGVO',
evidenceTypes: ['Restore-Test-Protokoll', 'DR-Uebungs-Dokumentation', 'RTO/RPO-Nachweis'],
addressesRiskIds: ['R-AVAIL-01', 'R-AVAIL-03', 'R-INT-03'],
effectiveness: 'high',
},
{
id: 'M-AVAIL-03',
type: 'technical',
sdmGoals: ['verfuegbarkeit'],
title: 'Endpoint Protection & Anti-Ransomware',
description: 'Einsatz von Endpoint-Detection-and-Response (EDR) Loesungen mit spezifischem Ransomware-Schutz, Verhaltensanalyse und automatischer Isolation kompromittierter Systeme.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['EDR-Dashboard', 'Threat-Detection-Statistiken', 'Incident-Response-Plan'],
addressesRiskIds: ['R-AVAIL-01'],
effectiveness: 'high',
},
{
id: 'M-AVAIL-04',
type: 'technical',
sdmGoals: ['verfuegbarkeit'],
title: 'Redundanz & High-Availability-Architektur',
description: 'Redundante Systemauslegung mit automatischem Failover, Load-Balancing und geo-redundanter Datenhaltung zur Sicherstellung der Verfuegbarkeit.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['HA-Architekturdiagramm', 'Failover-Testprotokoll', 'SLA-Dokumentation'],
addressesRiskIds: ['R-AVAIL-02', 'R-AVAIL-04'],
effectiveness: 'high',
},
{
id: 'M-AVAIL-05',
type: 'organizational',
sdmGoals: ['verfuegbarkeit', 'intervenierbarkeit'],
title: 'Exit-Strategie & Datenportabilitaetsplan',
description: 'Dokumentierte Exit-Strategie fuer jeden kritischen Anbieter mit Datenexport-Verfahren, Migrationsplan und Uebergangsfristen. Regelmaessiger Export-Test.',
legalBasis: 'Art. 28 Abs. 3 lit. g DSGVO, Art. 20 DSGVO',
evidenceTypes: ['Exit-Plan-Dokument', 'Export-Test-Protokoll', 'Vertragliche-Regelung'],
addressesRiskIds: ['R-AVAIL-02', 'R-AVAIL-05'],
effectiveness: 'medium',
},
{
id: 'M-AVAIL-06',
type: 'technical',
sdmGoals: ['verfuegbarkeit'],
title: 'DDoS-Schutz & Rate-Limiting',
description: 'Einsatz von DDoS-Mitigation-Services, CDN-basiertem Schutz und anwendungsspezifischem Rate-Limiting zur Abwehr von Verfuegbarkeitsangriffen.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['DDoS-Schutz-Konfiguration', 'Rate-Limit-Regeln', 'Traffic-Analyse'],
addressesRiskIds: ['R-AVAIL-04'],
effectiveness: 'high',
},
// =========================================================================
// DATENMINIMIERUNG (Retention, Anonymization, Purpose Limitation)
// =========================================================================
{
id: 'M-DMIN-01',
type: 'technical',
sdmGoals: ['datenminimierung'],
title: 'Privacy by Design: Datenerhebung auf das Minimum beschraenken',
description: 'Technische Massnahmen zur Beschraenkung der Datenerhebung: Pflichtfelder minimieren, optionale Felder deutlich kennzeichnen, Default-Einstellungen datenschutzfreundlich.',
legalBasis: 'Art. 25 Abs. 1 DSGVO',
evidenceTypes: ['Formular-Review', 'Default-Settings-Dokumentation'],
addressesRiskIds: ['R-RIGHTS-07', 'R-CONF-07'],
effectiveness: 'medium',
},
{
id: 'M-DMIN-02',
type: 'technical',
sdmGoals: ['datenminimierung', 'nichtverkettung'],
title: 'Pseudonymisierung & Anonymisierung',
description: 'Einsatz von Pseudonymisierungsverfahren (Token-basiert, Hash-basiert) und k-Anonymity/Differential Privacy bei der Weitergabe oder Analyse von Daten.',
legalBasis: 'Art. 25 Abs. 1 DSGVO, Art. 32 Abs. 1 lit. a DSGVO',
evidenceTypes: ['Pseudonymisierungs-Konzept', 'Re-Identifizierungs-Risiko-Analyse'],
addressesRiskIds: ['R-RIGHTS-04', 'R-RIGHTS-07'],
effectiveness: 'high',
},
{
id: 'M-DMIN-03',
type: 'technical',
sdmGoals: ['datenminimierung'],
title: 'Automatisiertes Loeschkonzept mit Aufbewahrungsfristen',
description: 'Implementierung automatischer Loeschroutinen basierend auf definierten Aufbewahrungsfristen. Monitoring der Loeschvorgaenge und Nachweis der Loeschung.',
legalBasis: 'Art. 5 Abs. 1 lit. e DSGVO, Art. 17 DSGVO',
evidenceTypes: ['Loeschkonzept-Dokument', 'Loeschfrist-Uebersicht', 'Loeschprotokoll'],
addressesRiskIds: ['R-RIGHTS-07', 'R-ORG-02'],
effectiveness: 'high',
},
{
id: 'M-DMIN-04',
type: 'organizational',
sdmGoals: ['datenminimierung'],
title: 'Regelmaessige Ueberpruefung der Datenbestaende',
description: 'Jaehrlicher Review aller gespeicherten personenbezogenen Daten auf Erforderlichkeit. Identifikation und Bereinigung von Altbestaenden, verwaisten Datensaetzen und redundanten Kopien.',
legalBasis: 'Art. 5 Abs. 1 lit. e DSGVO',
evidenceTypes: ['Datenbestand-Review-Bericht', 'Bereinigungs-Protokoll'],
addressesRiskIds: ['R-ORG-02'],
effectiveness: 'medium',
},
// =========================================================================
// TRANSPARENZ (Information, Documentation, Auditability)
// =========================================================================
{
id: 'M-TRANS-01',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'Datenschutzhinweise & Privacy Notices',
description: 'Umfassende, verstaendliche Datenschutzhinweise gemaess Art. 13/14 DSGVO an allen Erhebungsstellen. Layered-Approach fuer unterschiedliche Detailstufen.',
legalBasis: 'Art. 13, Art. 14 DSGVO',
evidenceTypes: ['Privacy-Notice-Review', 'Zustellungs-Nachweis', 'Usability-Test'],
addressesRiskIds: ['R-CONF-05', 'R-RIGHTS-02', 'R-RIGHTS-03', 'R-RIGHTS-06', 'R-TRANS-03', 'R-SPEC-02'],
effectiveness: 'medium',
},
{
id: 'M-TRANS-02',
type: 'technical',
sdmGoals: ['transparenz'],
title: 'Vollstaendiger Audit-Trail fuer personenbezogene Daten',
description: 'Lueckenloser, manipulationssicherer Audit-Trail fuer alle Verarbeitungsvorgaenge personenbezogener Daten. Wer hat wann auf welche Daten zugegriffen oder sie veraendert.',
legalBasis: 'Art. 5 Abs. 2 DSGVO (Rechenschaftspflicht)',
evidenceTypes: ['Audit-Trail-Architektur', 'Log-Integritaets-Nachweis', 'Beispiel-Audit-Export'],
addressesRiskIds: ['R-INT-05'],
effectiveness: 'high',
},
{
id: 'M-TRANS-03',
type: 'technical',
sdmGoals: ['transparenz'],
title: 'Erklaerbarkeit von KI-Entscheidungen (Explainability)',
description: 'Implementierung von Erklaerungsverfahren (SHAP, LIME, Feature-Importance) fuer automatisierte Entscheidungen. Bereitstellung verstaendlicher Begruendungen fuer Betroffene.',
legalBasis: 'Art. 22 Abs. 3 DSGVO, Art. 13 Abs. 2 lit. f DSGVO',
evidenceTypes: ['XAI-Konzept', 'Erklaerbarkeits-Beispiel', 'Betroffenen-Information'],
addressesRiskIds: ['R-AUTO-01', 'R-AUTO-03', 'R-RIGHTS-01'],
effectiveness: 'medium',
},
{
id: 'M-TRANS-04',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'Ueberwachungs-Folgenabschaetzung & Informationspflicht',
description: 'Bei systematischer Ueberwachung: Gesonderte Folgenabschaetzung, klare Beschilderung/Information, Verhaeltnismaessigkeitspruefung und zeitliche Begrenzung.',
legalBasis: 'Art. 35 Abs. 3 lit. c DSGVO, Art. 13 DSGVO',
evidenceTypes: ['Ueberwachungs-DSFA', 'Beschilderungs-Nachweis', 'Verhaeltnismaessigkeits-Bewertung'],
addressesRiskIds: ['R-RIGHTS-03'],
effectiveness: 'medium',
},
{
id: 'M-TRANS-05',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'Verzeichnis von Verarbeitungstaetigkeiten (VVT) pflegen',
description: 'Vollstaendiges und aktuelles VVT gemaess Art. 30 DSGVO fuer alle Verarbeitungstaetigkeiten. Regelmaessige Aktualisierung bei Aenderungen.',
legalBasis: 'Art. 30 DSGVO',
evidenceTypes: ['VVT-Export', 'Aktualisierungs-Log'],
addressesRiskIds: ['R-RIGHTS-06'],
effectiveness: 'medium',
},
{
id: 'M-TRANS-06',
type: 'legal',
sdmGoals: ['transparenz', 'vertraulichkeit'],
title: 'Transfer Impact Assessment (TIA) fuer Drittlandtransfer',
description: 'Durchfuehrung eines Transfer Impact Assessments vor jedem Drittlandtransfer. Bewertung des Schutzniveaus im Empfaengerland und Festlegung zusaetzlicher Garantien.',
legalBasis: 'Art. 46 DSGVO, Schrems-II-Urteil',
evidenceTypes: ['TIA-Dokument', 'Schutzniveau-Analyse', 'Zusaetzliche-Garantien-Vereinbarung'],
addressesRiskIds: ['R-TRANS-01', 'R-TRANS-02'],
effectiveness: 'high',
},
{
id: 'M-TRANS-07',
type: 'legal',
sdmGoals: ['vertraulichkeit'],
title: 'Standardvertragsklauseln (SCC) & Supplementary Measures',
description: 'Abschluss aktueller EU-Standardvertragsklauseln (2021/914) mit Auftragsverarbeitern im Drittland. Ergaenzende technische und organisatorische Massnahmen (Verschluesselung, Pseudonymisierung).',
legalBasis: 'Art. 46 Abs. 2 lit. c DSGVO',
evidenceTypes: ['Unterzeichnete SCC', 'Supplementary-Measures-Dokumentation'],
addressesRiskIds: ['R-TRANS-01', 'R-TRANS-02'],
effectiveness: 'medium',
},
// =========================================================================
// NICHTVERKETTUNG (Purpose Limitation, Data Separation, DLP)
// =========================================================================
{
id: 'M-NONL-01',
type: 'technical',
sdmGoals: ['nichtverkettung'],
title: 'Zweckbindung & Consent-Management',
description: 'Technische Durchsetzung der Zweckbindung: Daten werden nur fuer den erhobenen Zweck verwendet. Consent-Management-System protokolliert und erzwingt Einwilligungen.',
legalBasis: 'Art. 5 Abs. 1 lit. b DSGVO, Art. 6 Abs. 1 lit. a DSGVO',
evidenceTypes: ['Consent-Management-System', 'Zweckbindungs-Matrix', 'Consent-Protokolle'],
addressesRiskIds: ['R-CONF-05', 'R-RIGHTS-02', 'R-RIGHTS-03'],
effectiveness: 'high',
},
{
id: 'M-NONL-02',
type: 'technical',
sdmGoals: ['nichtverkettung'],
title: 'Data Loss Prevention (DLP) & Datenklassifikation',
description: 'Implementierung von DLP-Regeln zur Verhinderung unkontrollierter Datenweitergabe. Datenklassifikation (oeffentlich, intern, vertraulich, streng vertraulich) als Grundlage.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO',
evidenceTypes: ['DLP-Policy', 'Datenklassifikations-Schema', 'DLP-Incident-Report'],
addressesRiskIds: ['R-RIGHTS-02'],
effectiveness: 'medium',
},
{
id: 'M-NONL-03',
type: 'technical',
sdmGoals: ['nichtverkettung', 'datenminimierung'],
title: 'Differential Privacy & k-Anonymity bei Datenanalysen',
description: 'Einsatz von Differential Privacy oder k-Anonymity-Verfahren bei der Analyse personenbezogener Daten, um Re-Identifizierung zu verhindern.',
legalBasis: 'Art. 25 Abs. 1 DSGVO',
evidenceTypes: ['Anonymisierungs-Konzept', 'Privacy-Budget-Berechnung', 'k-Anonymity-Nachweis'],
addressesRiskIds: ['R-RIGHTS-04', 'R-AUTO-05'],
effectiveness: 'high',
},
{
id: 'M-NONL-04',
type: 'technical',
sdmGoals: ['nichtverkettung'],
title: 'Mandantentrennung & Datenisolierung',
description: 'Strikte logische oder physische Trennung personenbezogener Daten verschiedener Mandanten/Zwecke. Verhinderung unbeabsichtigter Zusammenfuehrung.',
legalBasis: 'Art. 5 Abs. 1 lit. b DSGVO',
evidenceTypes: ['Mandantentrennungs-Konzept', 'Isolierungs-Test-Bericht'],
addressesRiskIds: ['R-RIGHTS-04'],
effectiveness: 'high',
},
// =========================================================================
// INTERVENIERBARKEIT (Data Subject Rights, Correction, Deletion)
// =========================================================================
{
id: 'M-INTERV-01',
type: 'technical',
sdmGoals: ['intervenierbarkeit'],
title: 'DSAR-Workflow (Data Subject Access Request)',
description: 'Automatisierter Workflow fuer Betroffenenanfragen (Auskunft, Loeschung, Berichtigung, Export). Fristenmanagement (1 Monat), Identitaetspruefung und Dokumentation.',
legalBasis: 'Art. 15-22 DSGVO, Art. 12 Abs. 3 DSGVO',
evidenceTypes: ['DSAR-Workflow-Dokumentation', 'Bearbeitungszeiten-Statistik', 'Audit-Trail'],
addressesRiskIds: ['R-RIGHTS-05', 'R-AVAIL-05'],
effectiveness: 'high',
},
{
id: 'M-INTERV-02',
type: 'technical',
sdmGoals: ['intervenierbarkeit'],
title: 'Self-Service Datenverwaltung fuer Betroffene',
description: 'Bereitstellung eines Self-Service-Portals, ueber das Betroffene ihre Daten einsehen, korrigieren, exportieren und die Loeschung beantragen koennen.',
legalBasis: 'Art. 15-20 DSGVO',
evidenceTypes: ['Portal-Screenshot', 'Funktions-Testprotokoll', 'Nutzungs-Statistik'],
addressesRiskIds: ['R-RIGHTS-05'],
effectiveness: 'high',
},
{
id: 'M-INTERV-03',
type: 'organizational',
sdmGoals: ['intervenierbarkeit'],
title: 'Widerspruchs- und Einschraenkungsprozess',
description: 'Definierter Prozess fuer die Bearbeitung von Widerspruechen (Art. 21) und Einschraenkungsersuchen (Art. 18). Technische Moeglichkeit zur Sperrung einzelner Datensaetze.',
legalBasis: 'Art. 18, Art. 21 DSGVO',
evidenceTypes: ['Prozessbeschreibung', 'Sperr-Funktionalitaets-Nachweis'],
addressesRiskIds: ['R-RIGHTS-05'],
effectiveness: 'medium',
},
{
id: 'M-INTERV-04',
type: 'organizational',
sdmGoals: ['intervenierbarkeit'],
title: 'Human-in-the-Loop bei automatisierten Entscheidungen',
description: 'Sicherstellung menschlicher Ueberpruefung bei automatisierten Entscheidungen mit erheblicher Auswirkung. Eskalationsprozess und Einspruchsmoeglichkeit fuer Betroffene.',
legalBasis: 'Art. 22 Abs. 3 DSGVO',
evidenceTypes: ['HITL-Prozessbeschreibung', 'Eskalations-Statistik', 'Einspruchs-Protokoll'],
addressesRiskIds: ['R-AUTO-04'],
effectiveness: 'high',
},
// =========================================================================
// AUTOMATISIERUNG / KI
// =========================================================================
{
id: 'M-AUTO-01',
type: 'technical',
sdmGoals: ['nichtverkettung', 'transparenz'],
title: 'Bias-Monitoring & Fairness-Tests',
description: 'Regelmaessige Ueberpruefung von KI-Modellen auf Bias und Diskriminierung. Fairness-Metriken (Demographic Parity, Equal Opportunity) und Korrekturmassnahmen bei Abweichungen.',
legalBasis: 'Art. 22 Abs. 3 DSGVO, AI Act Art. 10',
evidenceTypes: ['Bias-Audit-Report', 'Fairness-Metriken-Dashboard', 'Korrektur-Dokumentation'],
addressesRiskIds: ['R-RIGHTS-01', 'R-AUTO-01', 'R-AUTO-02'],
effectiveness: 'high',
},
{
id: 'M-AUTO-02',
type: 'technical',
sdmGoals: ['transparenz'],
title: 'KI-Modell-Dokumentation & Model Cards',
description: 'Ausfuehrliche Dokumentation aller KI-Modelle: Trainingsdaten, Architektur, Performance-Metriken, bekannte Einschraenkungen, Einsatzzweck (Model Cards).',
legalBasis: 'Art. 13 Abs. 2 lit. f DSGVO, AI Act Art. 11',
evidenceTypes: ['Model-Card', 'Performance-Report', 'Einsatzbereich-Dokumentation'],
addressesRiskIds: ['R-AUTO-01', 'R-AUTO-03'],
effectiveness: 'medium',
},
{
id: 'M-AUTO-03',
type: 'organizational',
sdmGoals: ['intervenierbarkeit', 'transparenz'],
title: 'KI-Governance-Framework & Human Oversight Board',
description: 'Etablierung eines KI-Governance-Frameworks mit einem Human Oversight Board, das alle KI-Systeme mit hohem Risiko ueberwacht und Interventionsmoeglichkeiten hat.',
legalBasis: 'Art. 22 DSGVO, AI Act Art. 14',
evidenceTypes: ['Governance-Policy', 'Oversight-Board-Protokolle', 'Interventions-Log'],
addressesRiskIds: ['R-AUTO-01', 'R-AUTO-04'],
effectiveness: 'high',
},
{
id: 'M-AUTO-04',
type: 'technical',
sdmGoals: ['nichtverkettung', 'datenminimierung'],
title: 'Datenschutzkonformes KI-Training (Privacy-Preserving ML)',
description: 'Einsatz von Federated Learning, Differential Privacy beim Training oder synthetischen Trainingsdaten, um personenbezogene Daten im Modell zu schuetzen.',
legalBasis: 'Art. 25 Abs. 1 DSGVO',
evidenceTypes: ['Privacy-Preserving-ML-Konzept', 'Training-Daten-Analyse', 'Modell-Invertierbarkeiots-Test'],
addressesRiskIds: ['R-AUTO-02', 'R-AUTO-05'],
effectiveness: 'high',
},
// =========================================================================
// ORGANISATORISCHE MASSNAHMEN
// =========================================================================
{
id: 'M-ORG-01',
type: 'organizational',
sdmGoals: ['vertraulichkeit', 'integritaet'],
title: 'Datenschutz-Schulungen & Awareness-Programm',
description: 'Regelmaessige verpflichtende Datenschutz-Schulungen fuer alle Mitarbeiter. Awareness-Kampagnen zu Phishing, Social Engineering und sicherem Datenumgang.',
legalBasis: 'Art. 32 Abs. 1 lit. b DSGVO, Art. 39 Abs. 1 lit. a DSGVO',
evidenceTypes: ['Schulungsplan', 'Teilnahmequoten', 'Phishing-Simulations-Ergebnis'],
addressesRiskIds: ['R-CONF-06', 'R-ORG-03'],
effectiveness: 'medium',
},
{
id: 'M-ORG-02',
type: 'organizational',
sdmGoals: ['integritaet'],
title: 'Verpflichtung auf Vertraulichkeit & Datenschutz-Policy',
description: 'Schriftliche Verpflichtung aller Mitarbeiter und externen Dienstleister auf Vertraulichkeit und Einhaltung der Datenschutz-Policies.',
legalBasis: 'Art. 28 Abs. 3 lit. b DSGVO, Art. 29 DSGVO',
evidenceTypes: ['Unterzeichnete-Verpflichtungserklaerung', 'Datenschutz-Policy'],
addressesRiskIds: ['R-ORG-03'],
effectiveness: 'medium',
},
{
id: 'M-ORG-03',
type: 'organizational',
sdmGoals: ['transparenz'],
title: 'Datenpannen-Erkennungs- und Meldeprozess (Incident Response)',
description: 'Definierter Incident-Response-Prozess mit klaren Eskalationswegen, 72h-Meldepflicht-Tracking, Klassifizierungsschema und Kommunikationsplan.',
legalBasis: 'Art. 33, Art. 34 DSGVO',
evidenceTypes: ['Incident-Response-Plan', 'Melde-Template', 'Uebungs-Protokoll'],
addressesRiskIds: ['R-ORG-04'],
effectiveness: 'high',
},
{
id: 'M-ORG-04',
type: 'technical',
sdmGoals: ['transparenz', 'verfuegbarkeit'],
title: 'Automatisiertes Breach-Detection & Alerting',
description: 'Automatische Erkennung von Datenpannen durch Anomalie-Detection, ungewoehnliche Zugriffsmuster und Datenexfiltrations-Erkennung mit sofortigem Alert an den Incident-Response-Team.',
legalBasis: 'Art. 33 Abs. 1 DSGVO',
evidenceTypes: ['Alert-Regeln', 'Detection-Dashboard', 'Reaktionszeiten-Statistik'],
addressesRiskIds: ['R-ORG-04'],
effectiveness: 'high',
},
// =========================================================================
// RECHTLICHE MASSNAHMEN
// =========================================================================
{
id: 'M-LEGAL-01',
type: 'legal',
sdmGoals: ['transparenz'],
title: 'Angemessenheitsbeschluss oder Binding Corporate Rules (BCR)',
description: 'Sicherstellung, dass Drittlandtransfers auf einem Angemessenheitsbeschluss oder genehmigten BCRs basieren. Laufende Ueberwachung des Schutzniveaus.',
legalBasis: 'Art. 45, Art. 47 DSGVO',
evidenceTypes: ['Angemessenheitsbeschluss-Referenz', 'BCR-Genehmigung'],
addressesRiskIds: ['R-TRANS-02'],
effectiveness: 'high',
},
{
id: 'M-LEGAL-02',
type: 'legal',
sdmGoals: ['transparenz'],
title: 'Auftragsverarbeitungsvertrag (AVV) nach Art. 28 DSGVO',
description: 'Abschluss vollstaendiger AVVs mit allen Auftragsverarbeitern. Regelung von Zweck, Dauer, Datenkategorien, Weisungsbindung, Sub-Auftragsverarbeiter und Audit-Rechten.',
legalBasis: 'Art. 28 Abs. 3 DSGVO',
evidenceTypes: ['Unterzeichneter-AVV', 'Sub-Auftragsverarbeiter-Liste', 'Audit-Bericht'],
addressesRiskIds: ['R-ORG-01', 'R-TRANS-03'],
effectiveness: 'high',
},
{
id: 'M-LEGAL-03',
type: 'legal',
sdmGoals: ['transparenz'],
title: 'Regelmaessige Auftragsverarbeiter-Audits',
description: 'Jaehrliche Ueberpruefung der Auftragsverarbeiter auf Einhaltung der AVV-Vorgaben. Dokumentierte Audits vor Ort oder anhand von Zertifizierungen (SOC 2, ISO 27001).',
legalBasis: 'Art. 28 Abs. 3 lit. h DSGVO',
evidenceTypes: ['Audit-Bericht', 'Zertifizierungs-Nachweis', 'Massnahmenplan'],
addressesRiskIds: ['R-ORG-01'],
effectiveness: 'medium',
},
{
id: 'M-LEGAL-04',
type: 'legal',
sdmGoals: ['intervenierbarkeit', 'transparenz'],
title: 'Altersverifikation & Eltern-Einwilligung (Art. 8)',
description: 'Implementierung einer altersgerechten Verifikation und Einholung der Eltern-Einwilligung bei Minderjaehrigen unter 16 Jahren. Kindgerechte Datenschutzinformationen.',
legalBasis: 'Art. 8 DSGVO, EG 38 DSGVO',
evidenceTypes: ['Altersverifikations-Konzept', 'Eltern-Einwilligungs-Formular', 'Kindgerechte-Privacy-Notice'],
addressesRiskIds: ['R-SPEC-02'],
effectiveness: 'medium',
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function getMitigationsBySDMGoal(goal: SDMGoal): CatalogMitigation[] {
return MITIGATION_LIBRARY.filter(m => m.sdmGoals.includes(goal))
}
export function getMitigationsByType(type: DSFAMitigationType): CatalogMitigation[] {
return MITIGATION_LIBRARY.filter(m => m.type === type)
}
export function getMitigationsForRisk(riskId: string): CatalogMitigation[] {
return MITIGATION_LIBRARY.filter(m => m.addressesRiskIds.includes(riskId))
}
export function getCatalogMitigationById(id: string): CatalogMitigation | undefined {
return MITIGATION_LIBRARY.find(m => m.id === id)
}
export function getMitigationsByEffectiveness(effectiveness: 'low' | 'medium' | 'high'): CatalogMitigation[] {
return MITIGATION_LIBRARY.filter(m => m.effectiveness === effectiveness)
}
export const MITIGATION_TYPE_LABELS: Record<DSFAMitigationType, string> = {
technical: 'Technisch',
organizational: 'Organisatorisch',
legal: 'Rechtlich',
}
export const SDM_GOAL_LABELS: Record<SDMGoal, string> = {
datenminimierung: 'Datenminimierung',
verfuegbarkeit: 'Verfuegbarkeit',
integritaet: 'Integritaet',
vertraulichkeit: 'Vertraulichkeit',
nichtverkettung: 'Nichtverkettung',
transparenz: 'Transparenz',
intervenierbarkeit: 'Intervenierbarkeit',
}
export const EFFECTIVENESS_LABELS: Record<string, string> = {
low: 'Gering',
medium: 'Mittel',
high: 'Hoch',
}
@@ -0,0 +1,386 @@
/**
* Verbotene KI-Anwendungsfaelle nach Art. 5 EU AI Act (2024/1689)
*
* Seit Februar 2025 sind diese Praktiken in der EU ausdruecklich verboten.
* Dieses Modul stellt einen strukturierten Katalog bereit, der sowohl
* im SDK-Frontend als auch via RAG fuer den Compliance Advisor nutzbar ist.
*
* Quellen:
* - EU AI Act Art. 5 (Verbotene Praktiken), Verordnung (EU) 2024/1689
* - LG Muenchen I, 11.11.2025 (42 O 14139/24) — amtliches Werk nach §5 UrhG
*/
// =============================================================================
// Types
// =============================================================================
export type ProhibitionSeverity = 'absolute' | 'conditional'
export type ProhibitionCategory =
| 'manipulation'
| 'vulnerability_exploitation'
| 'social_scoring'
| 'biometric_surveillance'
| 'predictive_policing'
| 'emotion_recognition'
| 'biometric_categorization'
| 'copyright_violation'
export interface ProhibitedAIPractice {
id: string
title: string
titleEN: string
category: ProhibitionCategory
severity: ProhibitionSeverity
legalBasis: string
description: string
examples: string[]
exceptions?: string[]
/** Stichworte fuer automatische Erkennung */
detectionKeywords: string[]
/** Relevante Gerichtsentscheidungen */
caseLaw?: CaseLawReference[]
}
export interface CaseLawReference {
id: string
court: string
date: string
reference: string
title: string
summary: string
relevance: string
sourceUrl?: string
/** Amtliche Werke nach §5 UrhG — keine Lizenzpflicht */
licenseNote: string
}
// =============================================================================
// Verbotskatalog Art. 5 AI Act
// =============================================================================
export const PROHIBITED_AI_PRACTICES: ProhibitedAIPractice[] = [
// --- Absolut verboten ---
{
id: 'VERBOT-01',
title: 'Irrefuehrende Manipulation',
titleEN: 'Subliminal Manipulation',
category: 'manipulation',
severity: 'absolute',
legalBasis: 'Art. 5 Abs. 1 lit. a AI Act',
description:
'KI-Systeme, die durch unterschwellige Techniken oder absichtlich manipulative/taeuschende Methoden ' +
'das Verhalten von Personen wesentlich beeinflussen und ihnen dadurch erheblichen Schaden zufuegen oder ' +
'zufuegen koennen. Beispiel: Sprachgesteuertes Spielzeug, das Kinder zu gefaehrlichem Verhalten animiert.',
examples: [
'Sprachgesteuertes Spielzeug mit manipulativen Inhalten fuer Kinder',
'Unterschwellige Audio-/Videobotschaften zur Verhaltenssteuerung',
'Dark Patterns in KI-Interfaces, die zu schaedlichen Entscheidungen fuehren',
'KI-generierte Deepfakes zur politischen Manipulation',
],
detectionKeywords: [
'manipulation', 'unterschwellig', 'subliminal', 'taeuschung', 'dark pattern',
'verhaltenssteuerung', 'beeinflussung', 'deepfake', 'desinformation',
],
},
{
id: 'VERBOT-02',
title: 'Ausnutzung von Schwaechen',
titleEN: 'Exploitation of Vulnerabilities',
category: 'vulnerability_exploitation',
severity: 'absolute',
legalBasis: 'Art. 5 Abs. 1 lit. b AI Act',
description:
'KI-Systeme, die gezielt die Verletzlichkeit bestimmter Personen oder Gruppen ausnutzen — ' +
'etwa aufgrund von Alter, Behinderung, sozialer oder wirtschaftlicher Situation — ' +
'um deren Verhalten wesentlich zu beeinflussen und ihnen dadurch erheblichen Schaden zuzufuegen.',
examples: [
'KI-Werbung, die gezielt aeltere oder kognitiv eingeschraenkte Menschen zu Kaeufen verleitet',
'Sucht-foerdernde Algorithmen, die psychische Vulnerabilitaet ausnutzen',
'Finanzprodukt-Empfehlungen, die wirtschaftliche Notlagen ausnutzen',
'Gamification-KI, die Kinder zur uebermassigen Nutzung verleitet',
],
detectionKeywords: [
'schwaeche', 'vulnerabel', 'alter', 'behinderung', 'kinder', 'sucht',
'ausnutz', 'manipulation minderjahrig', 'schutzbeduerft',
],
},
{
id: 'VERBOT-03',
title: 'Social Scoring',
titleEN: 'Social Scoring',
category: 'social_scoring',
severity: 'absolute',
legalBasis: 'Art. 5 Abs. 1 lit. c AI Act',
description:
'KI-Systeme zur Bewertung oder Klassifizierung natuerlicher Personen auf Grundlage ihres Sozialverhaltens ' +
'oder persoenlicher Eigenschaften, wenn dies zu einer ungerechtfertigten oder unverhaeltnismaessigen ' +
'Benachteiligung fuehrt — insbesondere in Kontexten, die keinen Bezug zur urspruenglichen Datenerhebung haben.',
examples: [
'Bewertungssysteme auf Basis von Social-Media-Aktivitaeten',
'Scoring anhand von Internet-Surfverhalten fuer Kreditwuerdigkeit',
'Verhaltensbasierte Bewertung von Buergern durch Behoerden',
'Zugangssteuerung zu oeffentlichen Leistungen basierend auf Verhaltensdaten',
],
detectionKeywords: [
'social scoring', 'sozialkredit', 'verhaltensbewertung', 'buergerscore',
'personenbewertung', 'social media scoring', 'reputation score',
],
},
{
id: 'VERBOT-04',
title: 'Ungezielte Gesichtsdatensammlung',
titleEN: 'Untargeted Facial Image Scraping',
category: 'biometric_surveillance',
severity: 'absolute',
legalBasis: 'Art. 5 Abs. 1 lit. e AI Act',
description:
'KI-Systeme, die ungezielt Gesichtsbilder aus dem Internet oder von Ueberwachungskameras ' +
'sammeln, um biometrische Datenbanken aufzubauen. Prominentes Beispiel: Clearview AI.',
examples: [
'Scraping von Gesichtsbildern aus sozialen Netzwerken (Clearview AI)',
'Aufbau biometrischer Datenbanken aus Ueberwachungskameraaufnahmen',
'Sammlung von Portraetfotos ohne Wissen der Betroffenen',
'Web-Crawling zur Erstellung von Gesichtserkennungs-Trainingsdaten',
],
detectionKeywords: [
'gesichtserkennung', 'facial recognition', 'biometrisch', 'clearview',
'gesichtsdatenbank', 'scraping gesicht', 'face scraping',
],
},
{
id: 'VERBOT-05',
title: 'Biometrische Echtzeit-Ueberwachung',
titleEN: 'Real-time Biometric Surveillance in Public Spaces',
category: 'biometric_surveillance',
severity: 'absolute',
legalBasis: 'Art. 5 Abs. 1 lit. h AI Act',
description:
'Biometrische Echtzeit-Fernidentifizierung im oeffentlich zugaenglichen Raum zu Strafverfolgungszwecken ' +
'ist grundsaetzlich verboten. Ausnahmen bestehen nur bei konkretem, begruendetem Tatverdacht unter ' +
'strikten Voraussetzungen.',
examples: [
'Live-Gesichtserkennung durch Polizei an oeffentlichen Plaetzen',
'Echtzeit-Identifikation an Bahnhoefen oder Flughaefen ohne konkreten Anlass',
'Permanente biometrische Ueberwachung in Fussballstadien',
],
exceptions: [
'Gezielte Suche nach Opfern von Entfuehrung oder Menschenhandel',
'Verhinderung einer konkreten, erheblichen Bedrohung fuer Leib und Leben',
'Identifizierung von Verdaechtigen schwerer Straftaten (unter richterlicher Genehmigung)',
],
detectionKeywords: [
'echtzeit', 'live', 'ueberwachung', 'surveillance', 'oeffentlicher raum',
'fernidentifizierung', 'real-time', 'biometric identification',
],
},
// --- Bedingt / teilweise verboten ---
{
id: 'VERBOT-06',
title: 'Predictive Policing auf Personenprofilen',
titleEN: 'Profile-based Predictive Policing',
category: 'predictive_policing',
severity: 'conditional',
legalBasis: 'Art. 5 Abs. 1 lit. d AI Act',
description:
'KI-Systeme zur Erstellung von Risikobewertungen natuerlicher Personen zur Vorhersage von Straftaten ' +
'allein auf Grundlage von Profiling oder Persoenlichkeitsmerkmalen. Ortsbezogene Analysen ' +
'(Hotspot-Policing) sind hiervon nicht erfasst.',
examples: [
'Persoenlichkeitsprofilbasierte Straftaten-Vorhersage',
'KI-Score zur Bewertung der Rueckfallgefahr ohne konkreten Anlass',
'Vorausschauende Ueberwachung auf Basis ethnischer Profile',
],
exceptions: [
'Ortsbezogene Kriminalitaetsanalysen (Hotspot-Policing) ohne Personenbezug',
'Strafverfolgung mit konkretem Anfangsverdacht und richterlicher Anordnung',
],
detectionKeywords: [
'predictive policing', 'vorhersage straftat', 'risikobewertung person',
'profiling polizei', 'rueckfallprognose', 'kriminalitaetsprognose',
],
},
{
id: 'VERBOT-07',
title: 'Biometrische Kategorisierung sensibler Merkmale',
titleEN: 'Biometric Categorization of Sensitive Attributes',
category: 'biometric_categorization',
severity: 'conditional',
legalBasis: 'Art. 5 Abs. 1 lit. g AI Act',
description:
'KI-Systeme zur biometrischen Kategorisierung, die Rueckschluesse auf Rasse, politische Meinungen, ' +
'Gewerkschaftszugehoerigkeit, religioese oder weltanschauliche Ueberzeugungen, Sexualleben oder ' +
'sexuelle Orientierung ziehen. Ausnahme: Strafverfolgung unter engen Voraussetzungen.',
examples: [
'Gesichtsanalyse zur Erkennung ethnischer Zugehoerigkeit',
'Stimmanalyse zur Ableitung sexueller Orientierung',
'Gangerkennung zur Kategorisierung nach Religionszugehoerigkeit',
],
exceptions: [
'Strafverfolgung unter eng definierten gesetzlichen Voraussetzungen',
'Kennzeichnung oder Filterung rechtmaessig erworbener biometrischer Datensaetze',
],
detectionKeywords: [
'biometrisch', 'rasse', 'ethni', 'religion', 'sexuell', 'kategorisierung',
'gesichtsanalyse', 'stimmanalyse', 'gang erkennung',
],
},
{
id: 'VERBOT-08',
title: 'Emotionserkennung am Arbeitsplatz und in Schulen',
titleEN: 'Emotion Recognition in Workplaces and Schools',
category: 'emotion_recognition',
severity: 'conditional',
legalBasis: 'Art. 5 Abs. 1 lit. f AI Act',
description:
'KI-Systeme zur fortlaufenden Erkennung von Emotionen von Beschaeftigten am Arbeitsplatz oder ' +
'von Lernenden in Bildungseinrichtungen. Erlaubt nur mit medizinischem oder ' +
'sicherheitstechnischem Rechtfertigungsgrund.',
examples: [
'Webcam-basierte Aufmerksamkeitsueberwachung im Unterricht',
'Emotionsanalyse von Mitarbeitern waehrend Videokonferenzen',
'Stimmungsanalyse von Schuelern ueber Mikrofone',
'Stress-Detection bei Mitarbeitern ohne deren Einwilligung',
],
exceptions: [
'Medizinische Zwecke (z.B. Erkennung von Schmerzpatienten in Pflege)',
'Sicherheitstechnische Gruende (z.B. Muedigkeitserkennung bei Piloten/Fahrern)',
],
detectionKeywords: [
'emotionserkennung', 'emotion recognition', 'aufmerksamkeit', 'stimmungsanalyse',
'arbeitsplatz', 'schule', 'unterricht', 'webcam ueberwachung', 'stress detection',
],
},
]
// =============================================================================
// Relevante Rechtsprechung
// =============================================================================
export const AI_CASE_LAW: CaseLawReference[] = [
{
id: 'CASE-01',
court: 'LG Muenchen I',
date: '2025-11-11',
reference: '42 O 14139/24',
title: 'Urheberrecht bei KI-generierten Inhalten (Memorisierung)',
summary:
'Das Gericht stellte fest, dass ein KI-System urheberrechtlich geschuetzte Songtexte ' +
'inhaltsgleich reproduzierte, da diese im Modell „memorisiert" waren. Die KI-Modelle ' +
'stellen daher Vervielfaeltigungsstuecke i.S.d. Art. 2 InfoSoc-RL, §16 Abs. 1,2 UrhG dar.',
relevance:
'Zeigt, dass der Einsatz von KI-Systemen, die urheberrechtlich geschuetzte Inhalte ' +
'unautorisiert reproduzieren, urheberrechtlich unzulaessig ist. Relevant fuer alle ' +
'KI-Module, die auf urheberrechtlich geschuetzten Trainingsdaten basieren.',
sourceUrl: 'https://www.gesetze-bayern.de/Content/Document/Y-300-Z-GRURRS-B-2025-N-30204',
licenseNote: 'Amtliches Werk nach §5 UrhG — kein Urheberrechtsschutz, frei verwendbar im RAG.',
},
]
// =============================================================================
// Kategorisierung
// =============================================================================
export const PROHIBITION_CATEGORY_LABELS: Record<ProhibitionCategory, string> = {
manipulation: 'Manipulation & Taeuschung',
vulnerability_exploitation: 'Ausnutzung von Schwaechen',
social_scoring: 'Social Scoring',
biometric_surveillance: 'Biometrische Ueberwachung',
predictive_policing: 'Predictive Policing',
emotion_recognition: 'Emotionserkennung',
biometric_categorization: 'Biometrische Kategorisierung',
copyright_violation: 'Urheberrechtsverletzung',
}
export const SEVERITY_LABELS: Record<ProhibitionSeverity, string> = {
absolute: 'Absolut verboten',
conditional: 'Bedingt verboten (Ausnahmen moeglich)',
}
// =============================================================================
// Erkennung / Helper Functions
// =============================================================================
/**
* Prueft, ob eine Beschreibung oder ein Name auf verbotene Praktiken hinweist.
* Gibt alle potenziell zutreffenden Verbote zurueck, sortiert nach Relevanz.
*/
export function detectProhibitedPractices(
description: string,
name?: string
): { practice: ProhibitedAIPractice; matchCount: number; matchedKeywords: string[] }[] {
const searchText = `${name || ''} ${description}`.toLowerCase()
const results = PROHIBITED_AI_PRACTICES.map(practice => {
const matchedKeywords = practice.detectionKeywords.filter(kw =>
searchText.includes(kw.toLowerCase())
)
return {
practice,
matchCount: matchedKeywords.length,
matchedKeywords,
}
}).filter(r => r.matchCount > 0)
// Absolut verbotene zuerst, dann nach matchCount
results.sort((a, b) => {
if (a.practice.severity !== b.practice.severity) {
return a.practice.severity === 'absolute' ? -1 : 1
}
return b.matchCount - a.matchCount
})
return results
}
/**
* Gibt alle absolut verbotenen Praktiken zurueck.
*/
export function getAbsoluteProhibitions(): ProhibitedAIPractice[] {
return PROHIBITED_AI_PRACTICES.filter(p => p.severity === 'absolute')
}
/**
* Gibt alle bedingt verbotenen Praktiken zurueck.
*/
export function getConditionalProhibitions(): ProhibitedAIPractice[] {
return PROHIBITED_AI_PRACTICES.filter(p => p.severity === 'conditional')
}
/**
* Prueft, ob ein KI-Anwendungsfall Urheberrechts-Risiken birgt.
* Basierend auf LG Muenchen I Urteil.
*/
export function hasCopyrightRisk(trainingDataDescription?: string): boolean {
if (!trainingDataDescription) return false
const keywords = [
'urheberrecht', 'copyright', 'lizenz', 'geschuetzt', 'songtext',
'buch', 'artikel', 'literatur', 'musik', 'film', 'bild',
'web scraping', 'internet', 'crawl',
]
const lower = trainingDataDescription.toLowerCase()
return keywords.some(kw => lower.includes(kw))
}
/**
* Generiert einen formatierten Warnungstext fuer ein erkanntes Verbot.
*/
export function formatProhibitionWarning(practice: ProhibitedAIPractice): string {
const severity = practice.severity === 'absolute'
? 'ABSOLUT VERBOTEN'
: 'BEDINGT VERBOTEN'
let text = `${severity}: ${practice.title}\n`
text += `Rechtsgrundlage: ${practice.legalBasis}\n\n`
text += `${practice.description}\n`
if (practice.exceptions?.length) {
text += `\nAusnahmen:\n`
practice.exceptions.forEach(e => {
text += `- ${e}\n`
})
}
return text
}
+615
View File
@@ -0,0 +1,615 @@
/**
* DSFA Risikokatalog - Vordefinierte Risikoszenarien
*
* ~40 Risiken gegliedert nach Vertraulichkeit, Integritaet, Verfuegbarkeit,
* Rechte & Freiheiten, Drittlandtransfer und Automatisierung.
*
* Quellen: EG 75 DSGVO, Art. 32 DSGVO, Art. 28/46 DSGVO, Art. 22 DSGVO,
* Baseline-DSFA Katalog, SDM V2.0
*/
import type { DSFARiskCategory } from './types'
import type { SDMGoal } from './types'
// =============================================================================
// TYPES
// =============================================================================
export interface CatalogRisk {
id: string
category: DSFARiskCategory
sdmGoal: SDMGoal
title: string
description: string
impactExamples: string[]
typicalLikelihood: 'low' | 'medium' | 'high'
typicalImpact: 'low' | 'medium' | 'high'
wp248Criteria: string[]
applicableTo: string[]
mitigationIds: string[]
}
// =============================================================================
// RISIKOKATALOG
// =============================================================================
export const RISK_CATALOG: CatalogRisk[] = [
// =========================================================================
// VERTRAULICHKEIT (Confidentiality)
// =========================================================================
{
id: 'R-CONF-01',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Unbefugte Offenlegung durch Fehlkonfiguration',
description: 'Personenbezogene Daten werden durch fehlerhafte Systemkonfiguration (z.B. offene APIs, fehlerhafte Zugriffsrechte, oeffentliche Cloud-Speicher) unbefugt zugaenglich.',
impactExamples: ['Identitaetsdiebstahl', 'Reputationsschaden', 'Diskriminierung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K5'],
applicableTo: ['cloud_storage', 'web_application', 'api_service'],
mitigationIds: ['M-CONF-01', 'M-CONF-02', 'M-CONF-03'],
},
{
id: 'R-CONF-02',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Account Takeover / Credential Stuffing',
description: 'Angreifer uebernehmen Benutzerkonten durch gestohlene Zugangsdaten, Brute-Force-Angriffe oder Phishing und erlangen Zugriff auf personenbezogene Daten.',
impactExamples: ['Kontrollverlust ueber eigene Daten', 'Finanzieller Schaden', 'Missbrauch der Identitaet'],
typicalLikelihood: 'high',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K7'],
applicableTo: ['identity', 'web_application', 'email_service'],
mitigationIds: ['M-ACC-01', 'M-ACC-02'],
},
{
id: 'R-CONF-03',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Unbefugter Zugriff durch Support-/Administrationspersonal',
description: 'Administratoren oder Support-Mitarbeiter greifen ohne dienstliche Notwendigkeit auf personenbezogene Daten zu (Insider-Bedrohung).',
impactExamples: ['Verletzung der Privatsphaere', 'Datenmissbrauch', 'Vertrauensverlust'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K4'],
applicableTo: ['identity', 'crm', 'cloud_storage', 'support_system'],
mitigationIds: ['M-CONF-04', 'M-CONF-05', 'M-INT-02'],
},
{
id: 'R-CONF-04',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Datenleck durch unzureichende Verschluesselung',
description: 'Personenbezogene Daten werden bei Uebertragung oder Speicherung nicht oder unzureichend verschluesselt und koennen abgefangen werden.',
impactExamples: ['Man-in-the-Middle-Angriff', 'Datendiebstahl bei Speichermedien-Verlust'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K8'],
applicableTo: ['cloud_storage', 'email_service', 'mobile_app', 'api_service'],
mitigationIds: ['M-CONF-06', 'M-CONF-07'],
},
{
id: 'R-CONF-05',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Unkontrollierte Datenweitergabe an Dritte',
description: 'Personenbezogene Daten werden ohne Rechtsgrundlage oder ueber das vereinbarte Mass hinaus an Dritte weitergegeben (z.B. durch Tracking, Analyse-Tools, Sub-Auftragsverarbeiter).',
impactExamples: ['Unerwuenschte Werbung', 'Profiling ohne Wissen', 'Kontrollverlust'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K1', 'K6'],
applicableTo: ['web_application', 'analytics', 'marketing', 'crm'],
mitigationIds: ['M-NONL-01', 'M-TRANS-01'],
},
{
id: 'R-CONF-06',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Social Engineering / Phishing gegen Betroffene',
description: 'Betroffene werden durch manipulative Kommunikation dazu verleitet, personenbezogene Daten preiszugeben oder Zugriff zu gewaehren.',
impactExamples: ['Identitaetsdiebstahl', 'Finanzieller Schaden', 'Uebernahme von Konten'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K7'],
applicableTo: ['email_service', 'web_application', 'identity'],
mitigationIds: ['M-ACC-01', 'M-ORG-01'],
},
{
id: 'R-CONF-07',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Unbeabsichtigte Offenlegung in Logs/Debugging',
description: 'Personenbezogene Daten gelangen in Protokolldateien, Fehlermeldungen oder Debug-Ausgaben und werden dort nicht geschuetzt.',
impactExamples: ['Zugriff durch Unbefugte auf Logdaten', 'Langzeitspeicherung ohne Rechtsgrundlage'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K4'],
applicableTo: ['api_service', 'web_application', 'cloud_storage'],
mitigationIds: ['M-CONF-08', 'M-DMIN-01'],
},
// =========================================================================
// INTEGRITAET (Integrity)
// =========================================================================
{
id: 'R-INT-01',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Datenmanipulation durch externen Angriff',
description: 'Personenbezogene Daten werden durch einen Cyberangriff (SQL-Injection, API-Manipulation) veraendert, ohne dass dies erkannt wird.',
impactExamples: ['Falsche Entscheidungen auf Basis manipulierter Daten', 'Rufschaedigung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K8'],
applicableTo: ['api_service', 'web_application', 'database'],
mitigationIds: ['M-INT-01', 'M-INT-02', 'M-INT-03'],
},
{
id: 'R-INT-02',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Fehlerhafte Synchronisation zwischen Systemen',
description: 'Bei der Synchronisation personenbezogener Daten zwischen verschiedenen Systemen kommt es zu Inkonsistenzen, Duplikaten oder Datenverlust.',
impactExamples: ['Falsche Kontaktdaten', 'Doppelte Verarbeitung', 'Falsche Auskuenfte'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K6'],
applicableTo: ['crm', 'cloud_storage', 'erp', 'identity'],
mitigationIds: ['M-INT-04', 'M-INT-05'],
},
{
id: 'R-INT-03',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Backup-Korruption oder fehlerhafte Wiederherstellung',
description: 'Backups personenbezogener Daten sind beschaedigt, unvollstaendig oder veraltet, sodass eine zuverlaessige Wiederherstellung nicht moeglich ist.',
impactExamples: ['Datenverlust bei Wiederherstellung', 'Veraltete Datenbasis', 'Compliance-Verstoss'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K5'],
applicableTo: ['database', 'cloud_storage', 'erp'],
mitigationIds: ['M-AVAIL-01', 'M-AVAIL-02'],
},
{
id: 'R-INT-04',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Unbemerkte Aenderung von Zugriffsrechten',
description: 'Zugriffsberechtigungen werden unbefugt oder fehlerhaft geaendert, wodurch unberechtigte Personen Zugang zu personenbezogenen Daten erhalten.',
impactExamples: ['Privilege Escalation', 'Unbefugter Datenzugriff'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4'],
applicableTo: ['identity', 'cloud_storage', 'api_service'],
mitigationIds: ['M-INT-02', 'M-CONF-04'],
},
{
id: 'R-INT-05',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Fehlende Nachvollziehbarkeit von Datenveraenderungen',
description: 'Aenderungen an personenbezogenen Daten werden nicht protokolliert, sodass Manipulationen oder Fehler nicht erkannt oder nachvollzogen werden koennen.',
impactExamples: ['Unmoeglich festzustellen wer/wann Daten geaendert hat', 'Audit-Versagen'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K3'],
applicableTo: ['database', 'crm', 'erp', 'web_application'],
mitigationIds: ['M-INT-02', 'M-TRANS-02'],
},
// =========================================================================
// VERFUEGBARKEIT (Availability)
// =========================================================================
{
id: 'R-AVAIL-01',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Ransomware-Angriff mit Datenverschluesselung',
description: 'Schadsoftware verschluesselt personenbezogene Daten und macht sie unzugaenglich. Die Wiederherstellung erfordert entweder Loesegeldzahlung oder Backup-Restore.',
impactExamples: ['Verlust des Zugangs zu eigenen Daten', 'Betriebsunterbrechung', 'Loesegeld-Erpressung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K5', 'K8'],
applicableTo: ['cloud_storage', 'database', 'erp', 'web_application'],
mitigationIds: ['M-AVAIL-01', 'M-AVAIL-02', 'M-AVAIL-03'],
},
{
id: 'R-AVAIL-02',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Provider-Ausfall / Cloud-Service Nichtverfuegbarkeit',
description: 'Der Cloud-/Hosting-Provider faellt aus, was den Zugang zu personenbezogenen Daten verhindert. Betroffene koennen ihre Rechte nicht ausueben.',
impactExamples: ['Keine Auskunft moeglich', 'Vertragsverletzung', 'Geschaeftsunterbrechung'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K5', 'K9'],
applicableTo: ['cloud_storage', 'web_application', 'api_service'],
mitigationIds: ['M-AVAIL-04', 'M-AVAIL-05'],
},
{
id: 'R-AVAIL-03',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Datenverlust durch fehlende oder ungetestete Backups',
description: 'Personenbezogene Daten gehen unwiederbringlich verloren, weil keine ausreichenden Backups existieren oder Restore-Prozesse nicht getestet werden.',
impactExamples: ['Unwiderruflicher Datenverlust', 'Verlust von Beweismitteln', 'Compliance-Verstoss'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K5'],
applicableTo: ['database', 'cloud_storage', 'erp'],
mitigationIds: ['M-AVAIL-01', 'M-AVAIL-02'],
},
{
id: 'R-AVAIL-04',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'DDoS-Angriff auf oeffentliche Dienste',
description: 'Ein Distributed-Denial-of-Service-Angriff verhindert den Zugang zu Systemen, die personenbezogene Daten verarbeiten.',
impactExamples: ['Betroffene koennen Rechte nicht ausueben', 'Geschaeftsausfall'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K5', 'K9'],
applicableTo: ['web_application', 'api_service'],
mitigationIds: ['M-AVAIL-06', 'M-AVAIL-04'],
},
{
id: 'R-AVAIL-05',
category: 'availability',
sdmGoal: 'verfuegbarkeit',
title: 'Vendor Lock-in mit Kontrollverlust',
description: 'Abhaengigkeit von einem einzelnen Anbieter erschwert oder verhindert den Zugang zu personenbezogenen Daten bei Vertragsbeendigung oder Anbieterwechsel.',
impactExamples: ['Datenexport nicht moeglich', 'Erzwungene Weiternutzung', 'Datenverlust bei Kuendigung'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K9'],
applicableTo: ['cloud_storage', 'erp', 'crm'],
mitigationIds: ['M-AVAIL-05', 'M-INTERV-01'],
},
// =========================================================================
// RECHTE & FREIHEITEN (Rights & Freedoms)
// =========================================================================
{
id: 'R-RIGHTS-01',
category: 'rights_freedoms',
sdmGoal: 'nichtverkettung',
title: 'Diskriminierung durch automatisierte Verarbeitung',
description: 'Automatisierte Entscheidungssysteme fuehren zu einer diskriminierenden Behandlung bestimmter Personengruppen aufgrund von Merkmalen wie Alter, Geschlecht, Herkunft oder Gesundheitszustand.',
impactExamples: ['Benachteiligung bei Kreditvergabe', 'Ausschluss von Dienstleistungen', 'Ungleichbehandlung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K2', 'K7'],
applicableTo: ['ai_ml', 'scoring', 'identity'],
mitigationIds: ['M-AUTO-01', 'M-AUTO-02', 'M-TRANS-03'],
},
{
id: 'R-RIGHTS-02',
category: 'rights_freedoms',
sdmGoal: 'nichtverkettung',
title: 'Unzulaessiges Profiling ohne Einwilligung',
description: 'Nutzerverhalten wird systematisch analysiert und zu Profilen zusammengefuehrt, ohne dass eine Rechtsgrundlage oder Einwilligung vorliegt.',
impactExamples: ['Persoenlichkeitsprofile ohne Wissen', 'Gezielte Manipulation', 'Filterblase'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K3', 'K6'],
applicableTo: ['analytics', 'marketing', 'web_application', 'ai_ml'],
mitigationIds: ['M-NONL-01', 'M-NONL-02', 'M-TRANS-01'],
},
{
id: 'R-RIGHTS-03',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Systematische Ueberwachung von Betroffenen',
description: 'Betroffene werden systematisch ueberwacht (z.B. durch Standorttracking, E-Mail-Monitoring, Videoueberwachung), ohne angemessene Transparenz oder Rechtsgrundlage.',
impactExamples: ['Einschuechterungseffekt (Chilling Effect)', 'Verletzung der Privatsphaere', 'Vertrauensverlust'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K3', 'K4', 'K7'],
applicableTo: ['monitoring', 'hr_system', 'mobile_app'],
mitigationIds: ['M-TRANS-01', 'M-TRANS-04', 'M-NONL-01'],
},
{
id: 'R-RIGHTS-04',
category: 'rights_freedoms',
sdmGoal: 'nichtverkettung',
title: 'Re-Identifizierung pseudonymisierter Daten',
description: 'Pseudonymisierte oder anonymisierte Daten werden durch Zusammenfuehrung mit anderen Datenquellen re-identifiziert, wodurch der Schutz der Betroffenen aufgehoben wird.',
impactExamples: ['Verlust der Anonymitaet', 'Unerwuenschte Identifizierung', 'Zweckentfremdung'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K6', 'K8'],
applicableTo: ['analytics', 'ai_ml', 'research'],
mitigationIds: ['M-NONL-03', 'M-NONL-04', 'M-DMIN-02'],
},
{
id: 'R-RIGHTS-05',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Hinderung bei Ausuebung von Betroffenenrechten',
description: 'Betroffene werden an der Ausuebung ihrer Rechte (Auskunft, Loeschung, Berichtigung, Widerspruch) gehindert — z.B. durch fehlende Prozesse, technische Huerden oder Verzoegerungen.',
impactExamples: ['Keine Loeschung moeglich', 'Verzoegerte Auskunft', 'Bussgeld gem. Art. 83'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K9'],
applicableTo: ['web_application', 'crm', 'identity', 'cloud_storage'],
mitigationIds: ['M-INTERV-01', 'M-INTERV-02', 'M-INTERV-03'],
},
{
id: 'R-RIGHTS-06',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Fehlende oder unzureichende Informationspflichten',
description: 'Betroffene werden nicht oder unzureichend ueber die Verarbeitung ihrer Daten informiert (Verstoss gegen Art. 13/14 DSGVO).',
impactExamples: ['Keine informierte Einwilligung moeglich', 'Vertrauensverlust', 'Bussgeld'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K9'],
applicableTo: ['web_application', 'mobile_app', 'marketing'],
mitigationIds: ['M-TRANS-01', 'M-TRANS-05'],
},
{
id: 'R-RIGHTS-07',
category: 'rights_freedoms',
sdmGoal: 'datenminimierung',
title: 'Uebermassige Datenerhebung (Verstoss Datenminimierung)',
description: 'Es werden mehr personenbezogene Daten erhoben als fuer den Verarbeitungszweck notwendig (Verstoss gegen Art. 5 Abs. 1 lit. c DSGVO).',
impactExamples: ['Unnoetige Risikoexposition', 'Hoeherer Schaden bei Datenpanne'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K5'],
applicableTo: ['web_application', 'mobile_app', 'crm', 'hr_system'],
mitigationIds: ['M-DMIN-01', 'M-DMIN-02', 'M-DMIN-03'],
},
// =========================================================================
// DRITTLANDTRANSFER
// =========================================================================
{
id: 'R-TRANS-01',
category: 'rights_freedoms',
sdmGoal: 'vertraulichkeit',
title: 'Zugriff durch Drittland-Behoerden (FISA/CLOUD Act)',
description: 'Behoerden eines Drittlandes (z.B. USA) greifen auf personenbezogene Daten zu, die bei einem Cloud-Provider in der EU oder im Drittland gespeichert sind.',
impactExamples: ['Ueberwachung ohne Wissen', 'Kein Rechtsschutz', 'Schrems-II-Risiko'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K5', 'K7'],
applicableTo: ['cloud_storage', 'email_service', 'crm', 'analytics'],
mitigationIds: ['M-TRANS-06', 'M-TRANS-07', 'M-CONF-06'],
},
{
id: 'R-TRANS-02',
category: 'rights_freedoms',
sdmGoal: 'vertraulichkeit',
title: 'Unzureichende Schutzgarantien bei Drittlandtransfer',
description: 'Personenbezogene Daten werden in Drittlaender uebermittelt, ohne dass angemessene Garantien (SCC, BCR, Angemessenheitsbeschluss) vorhanden sind.',
impactExamples: ['Rechtswidriger Transfer', 'Bussgeld', 'Untersagung der Verarbeitung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K5', 'K7'],
applicableTo: ['cloud_storage', 'email_service', 'crm', 'analytics'],
mitigationIds: ['M-TRANS-06', 'M-TRANS-07', 'M-LEGAL-01'],
},
{
id: 'R-TRANS-03',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Intransparente Sub-Auftragsverarbeiter-Kette',
description: 'Die Kette der Sub-Auftragsverarbeiter ist nicht transparent. Betroffene und Verantwortliche wissen nicht, wo ihre Daten tatsaechlich verarbeitet werden.',
impactExamples: ['Unkontrollierte Datenweitergabe', 'Unbekannter Verarbeitungsort'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K5'],
applicableTo: ['cloud_storage', 'crm', 'analytics'],
mitigationIds: ['M-TRANS-01', 'M-LEGAL-02'],
},
// =========================================================================
// AUTOMATISIERUNG / KI
// =========================================================================
{
id: 'R-AUTO-01',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'KI-Fehlentscheidung mit erheblicher Auswirkung',
description: 'Ein KI-System trifft eine fehlerhafte automatisierte Entscheidung (z.B. Ablehnung, Sperrung, Bewertung), die erhebliche Auswirkungen auf eine betroffene Person hat.',
impactExamples: ['Unrechtmaessige Ablehnung', 'Falsche Risikoeinstufung', 'Benachteiligung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K2', 'K8'],
applicableTo: ['ai_ml', 'scoring', 'hr_system'],
mitigationIds: ['M-AUTO-01', 'M-AUTO-02', 'M-AUTO-03'],
},
{
id: 'R-AUTO-02',
category: 'rights_freedoms',
sdmGoal: 'nichtverkettung',
title: 'Algorithmischer Bias in Trainingsdaten',
description: 'KI-Modelle spiegeln Vorurteile in den Trainingsdaten wider und treffen diskriminierende Entscheidungen bezueglich geschuetzter Merkmale.',
impactExamples: ['Diskriminierung nach Geschlecht/Herkunft', 'Systematische Benachteiligung'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K1', 'K2', 'K7', 'K8'],
applicableTo: ['ai_ml', 'scoring'],
mitigationIds: ['M-AUTO-01', 'M-AUTO-04'],
},
{
id: 'R-AUTO-03',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Fehlende Erklaerbarkeit automatisierter Entscheidungen',
description: 'Automatisierte Entscheidungen koennen den Betroffenen nicht erklaert werden ("Black Box"), sodass der Anspruch auf aussagekraeftige Informationen (Art. 22 Abs. 3) nicht erfuellt wird.',
impactExamples: ['Keine Anfechtbarkeit', 'Vertrauensverlust', 'Verstoss gegen Art. 22'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K2', 'K8'],
applicableTo: ['ai_ml', 'scoring'],
mitigationIds: ['M-AUTO-02', 'M-TRANS-03'],
},
{
id: 'R-AUTO-04',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Fehlende menschliche Aufsicht bei KI-Entscheidungen',
description: 'Automatisierte Entscheidungen werden ohne menschliche Ueberpruefung oder Interventionsmoeglichkeit getroffen, obwohl dies erforderlich waere.',
impactExamples: ['Keine Korrekturmoeglichkeit', 'Eskalation von Fehlern'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K2', 'K8'],
applicableTo: ['ai_ml', 'scoring', 'hr_system'],
mitigationIds: ['M-AUTO-03', 'M-INTERV-04'],
},
{
id: 'R-AUTO-05',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Datenleck durch KI-Training mit personenbezogenen Daten',
description: 'Personenbezogene Daten, die fuer das Training von KI-Modellen verwendet werden, koennen durch das Modell reproduziert oder extrahiert werden (Model Inversion, Membership Inference).',
impactExamples: ['Offenlegung von Trainingsdaten', 'Re-Identifizierung'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K8'],
applicableTo: ['ai_ml'],
mitigationIds: ['M-CONF-06', 'M-NONL-03', 'M-AUTO-04'],
},
// =========================================================================
// ORGANISATORISCHE RISIKEN
// =========================================================================
{
id: 'R-ORG-01',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Fehlende oder fehlerhafte Auftragsverarbeitungsvertraege',
description: 'Mit Auftragsverarbeitern existieren keine oder unzureichende Vertraege gemaess Art. 28 DSGVO, sodass Pflichten und Rechte nicht geregelt sind.',
impactExamples: ['Keine Kontrolle ueber Verarbeiter', 'Bussgeld', 'Datenmissbrauch durch Verarbeiter'],
typicalLikelihood: 'medium',
typicalImpact: 'medium',
wp248Criteria: ['K5'],
applicableTo: ['cloud_storage', 'crm', 'analytics', 'email_service'],
mitigationIds: ['M-LEGAL-02', 'M-LEGAL-03'],
},
{
id: 'R-ORG-02',
category: 'rights_freedoms',
sdmGoal: 'datenminimierung',
title: 'Fehlende Loeschprozesse / Ueberschreitung von Aufbewahrungsfristen',
description: 'Personenbezogene Daten werden laenger als notwendig gespeichert, weil keine automatischen Loeschprozesse oder Aufbewahrungsfristen definiert sind.',
impactExamples: ['Unnoetige Risikoexposition', 'Verstoss gegen Speicherbegrenzung', 'Bussgeld'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K5'],
applicableTo: ['database', 'cloud_storage', 'crm', 'erp', 'email_service'],
mitigationIds: ['M-DMIN-03', 'M-DMIN-04'],
},
{
id: 'R-ORG-03',
category: 'integrity',
sdmGoal: 'integritaet',
title: 'Unzureichende Schulung/Sensibilisierung der Mitarbeiter',
description: 'Mitarbeiter sind nicht ausreichend im Umgang mit personenbezogenen Daten geschult und verursachen durch Unkenntnis Datenpannen oder Verarbeitungsfehler.',
impactExamples: ['Versehentliche Datenweitergabe', 'Phishing-Erfolg', 'Fehlerhafte Verarbeitung'],
typicalLikelihood: 'high',
typicalImpact: 'medium',
wp248Criteria: ['K5', 'K7'],
applicableTo: ['hr_system', 'email_service', 'crm', 'web_application'],
mitigationIds: ['M-ORG-01', 'M-ORG-02'],
},
{
id: 'R-ORG-04',
category: 'rights_freedoms',
sdmGoal: 'transparenz',
title: 'Fehlende Datenpannen-Erkennung und -Meldung',
description: 'Datenpannen werden nicht rechtzeitig erkannt oder nicht innerhalb der 72-Stunden-Frist (Art. 33 DSGVO) an die Aufsichtsbehoerde gemeldet.',
impactExamples: ['Verspaetete Meldung', 'Bussgeld', 'Verzoegerte Benachrichtigung Betroffener'],
typicalLikelihood: 'medium',
typicalImpact: 'high',
wp248Criteria: ['K5'],
applicableTo: ['web_application', 'cloud_storage', 'database', 'api_service'],
mitigationIds: ['M-ORG-03', 'M-ORG-04', 'M-INT-02'],
},
// =========================================================================
// BESONDERE DATENKATEGORIEN
// =========================================================================
{
id: 'R-SPEC-01',
category: 'confidentiality',
sdmGoal: 'vertraulichkeit',
title: 'Kompromittierung besonderer Datenkategorien (Art. 9)',
description: 'Besonders schutzwuerdige Daten (Gesundheit, Religion, Biometrie, Gewerkschaftszugehoerigkeit) werden offengelegt oder missbraucht.',
impactExamples: ['Schwerwiegende Diskriminierung', 'Existenzielle Bedrohung', 'Soziale Ausgrenzung'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K7'],
applicableTo: ['hr_system', 'health_system', 'identity'],
mitigationIds: ['M-CONF-06', 'M-CONF-01', 'M-CONF-04'],
},
{
id: 'R-SPEC-02',
category: 'rights_freedoms',
sdmGoal: 'intervenierbarkeit',
title: 'Verarbeitung von Kinderdaten ohne angemessenen Schutz',
description: 'Daten von Minderjaehrigen werden verarbeitet, ohne die besonderen Schutzmassnahmen fuer Kinder (Art. 8, EG 38 DSGVO) zu beachten.',
impactExamples: ['Langzeitfolgen fuer Minderjaehrige', 'Einschraenkung der Entwicklung', 'Manipulation'],
typicalLikelihood: 'low',
typicalImpact: 'high',
wp248Criteria: ['K4', 'K7'],
applicableTo: ['web_application', 'mobile_app', 'education'],
mitigationIds: ['M-LEGAL-04', 'M-DMIN-01', 'M-TRANS-01'],
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function getRisksByCategory(category: DSFARiskCategory): CatalogRisk[] {
return RISK_CATALOG.filter(r => r.category === category)
}
export function getRisksBySDMGoal(goal: SDMGoal): CatalogRisk[] {
return RISK_CATALOG.filter(r => r.sdmGoal === goal)
}
export function getRisksByWP248Criterion(criterionCode: string): CatalogRisk[] {
return RISK_CATALOG.filter(r => r.wp248Criteria.includes(criterionCode))
}
export function getRisksByComponent(component: string): CatalogRisk[] {
return RISK_CATALOG.filter(r => r.applicableTo.includes(component))
}
export function getCatalogRiskById(id: string): CatalogRisk | undefined {
return RISK_CATALOG.find(r => r.id === id)
}
export const RISK_CATEGORY_LABELS: Record<DSFARiskCategory, string> = {
confidentiality: 'Vertraulichkeit',
integrity: 'Integritaet',
availability: 'Verfuegbarkeit',
rights_freedoms: 'Rechte & Freiheiten',
}
export const COMPONENT_FAMILY_LABELS: Record<string, string> = {
identity: 'Identitaet & Zugang',
cloud_storage: 'Cloud-Speicher',
web_application: 'Web-Anwendung',
api_service: 'API-Service',
email_service: 'E-Mail-Dienst',
mobile_app: 'Mobile App',
database: 'Datenbank',
crm: 'CRM-System',
erp: 'ERP-System',
analytics: 'Analyse/Tracking',
marketing: 'Marketing',
ai_ml: 'KI / Machine Learning',
scoring: 'Scoring / Bewertung',
hr_system: 'HR-System',
health_system: 'Gesundheitssystem',
monitoring: 'Ueberwachungssystem',
support_system: 'Support-System',
education: 'Bildungsplattform',
research: 'Forschung',
}
File diff suppressed because it is too large Load Diff
+881
View File
@@ -0,0 +1,881 @@
/**
* DSR API Client
*
* API client for Data Subject Request management
* Connects to the Go Consent Service backend
*/
import {
DSRRequest,
DSRListResponse,
DSRFilters,
DSRCreateRequest,
DSRUpdateRequest,
DSRVerifyIdentityRequest,
DSRCompleteRequest,
DSRRejectRequest,
DSRExtendDeadlineRequest,
DSRSendCommunicationRequest,
DSRCommunication,
DSRAuditEntry,
DSRStatistics,
DSRDataExport,
DSRErasureChecklist
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const DSR_API_BASE = process.env.NEXT_PUBLIC_CONSENT_SERVICE_URL || 'http://localhost:8081'
const API_TIMEOUT = 30000 // 30 seconds
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function getTenantId(): string {
// In a real app, this would come from auth context or localStorage
if (typeof window !== 'undefined') {
return localStorage.getItem('tenantId') || 'default-tenant'
}
return 'default-tenant'
}
function getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId()
}
// Add auth token if available
if (typeof window !== 'undefined') {
const token = localStorage.getItem('authToken')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
return headers
}
async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...getAuthHeaders(),
...options.headers
}
})
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep the HTTP status message
}
throw new Error(errorMessage)
}
// Handle empty responses
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
}
return {} as T
} finally {
clearTimeout(timeoutId)
}
}
// =============================================================================
// DSR LIST & CRUD
// =============================================================================
/**
* Fetch all DSR requests with optional filters
*/
export async function fetchDSRList(filters?: DSRFilters): Promise<DSRListResponse> {
const params = new URLSearchParams()
if (filters) {
if (filters.status) {
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
statuses.forEach(s => params.append('status', s))
}
if (filters.type) {
const types = Array.isArray(filters.type) ? filters.type : [filters.type]
types.forEach(t => params.append('type', t))
}
if (filters.priority) params.set('priority', filters.priority)
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
if (filters.search) params.set('search', filters.search)
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
if (filters.dateTo) params.set('dateTo', filters.dateTo)
}
const queryString = params.toString()
const url = `${DSR_API_BASE}/api/v1/admin/dsr${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<DSRListResponse>(url)
}
/**
* Fetch a single DSR request by ID
*/
export async function fetchDSR(id: string): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`)
}
/**
* Create a new DSR request
*/
export async function createDSR(request: DSRCreateRequest): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr`, {
method: 'POST',
body: JSON.stringify(request)
})
}
/**
* Update a DSR request
*/
export async function updateDSR(id: string, update: DSRUpdateRequest): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
method: 'PUT',
body: JSON.stringify(update)
})
}
/**
* Delete a DSR request (soft delete - marks as cancelled)
*/
export async function deleteDSR(id: string): Promise<void> {
await fetchWithTimeout<void>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
method: 'DELETE'
})
}
// =============================================================================
// DSR WORKFLOW ACTIONS
// =============================================================================
/**
* Verify the identity of the requester
*/
export async function verifyIdentity(
dsrId: string,
verification: DSRVerifyIdentityRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/verify-identity`,
{
method: 'POST',
body: JSON.stringify(verification)
}
)
}
/**
* Complete a DSR request
*/
export async function completeDSR(
dsrId: string,
completion?: DSRCompleteRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/complete`,
{
method: 'POST',
body: JSON.stringify(completion || {})
}
)
}
/**
* Reject a DSR request
*/
export async function rejectDSR(
dsrId: string,
rejection: DSRRejectRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/reject`,
{
method: 'POST',
body: JSON.stringify(rejection)
}
)
}
/**
* Extend the deadline for a DSR request
*/
export async function extendDeadline(
dsrId: string,
extension: DSRExtendDeadlineRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/extend`,
{
method: 'POST',
body: JSON.stringify(extension)
}
)
}
/**
* Assign a DSR request to a user
*/
export async function assignDSR(
dsrId: string,
assignedTo: string
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/assign`,
{
method: 'POST',
body: JSON.stringify({ assignedTo })
}
)
}
// =============================================================================
// COMMUNICATION
// =============================================================================
/**
* Get all communications for a DSR request
*/
export async function getCommunications(dsrId: string): Promise<DSRCommunication[]> {
return fetchWithTimeout<DSRCommunication[]>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/communications`
)
}
/**
* Send a communication (email, letter, internal note)
*/
export async function sendCommunication(
dsrId: string,
communication: DSRSendCommunicationRequest
): Promise<DSRCommunication> {
return fetchWithTimeout<DSRCommunication>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/send-communication`,
{
method: 'POST',
body: JSON.stringify(communication)
}
)
}
// =============================================================================
// AUDIT LOG
// =============================================================================
/**
* Get audit log entries for a DSR request
*/
export async function getAuditLog(dsrId: string): Promise<DSRAuditEntry[]> {
return fetchWithTimeout<DSRAuditEntry[]>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/audit`
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Get DSR statistics
*/
export async function getDSRStatistics(): Promise<DSRStatistics> {
return fetchWithTimeout<DSRStatistics>(
`${DSR_API_BASE}/api/v1/admin/dsr/statistics`
)
}
// =============================================================================
// DATA EXPORT (Art. 15, 20)
// =============================================================================
/**
* Generate data export for Art. 15 (access) or Art. 20 (portability)
*/
export async function generateDataExport(
dsrId: string,
format: 'json' | 'csv' | 'xml' | 'pdf' = 'json'
): Promise<DSRDataExport> {
return fetchWithTimeout<DSRDataExport>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export`,
{
method: 'POST',
body: JSON.stringify({ format })
}
)
}
/**
* Download generated data export
*/
export async function downloadDataExport(dsrId: string): Promise<Blob> {
const response = await fetch(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export/download`,
{
headers: getAuthHeaders()
}
)
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`)
}
return response.blob()
}
// =============================================================================
// ERASURE CHECKLIST (Art. 17)
// =============================================================================
/**
* Get the erasure checklist for an Art. 17 request
*/
export async function getErasureChecklist(dsrId: string): Promise<DSRErasureChecklist> {
return fetchWithTimeout<DSRErasureChecklist>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`
)
}
/**
* Update the erasure checklist
*/
export async function updateErasureChecklist(
dsrId: string,
checklist: DSRErasureChecklist
): Promise<DSRErasureChecklist> {
return fetchWithTimeout<DSRErasureChecklist>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`,
{
method: 'PUT',
body: JSON.stringify(checklist)
}
)
}
// =============================================================================
// EMAIL TEMPLATES
// =============================================================================
/**
* Get available email templates
*/
export async function getEmailTemplates(): Promise<{ id: string; name: string; stage: string }[]> {
return fetchWithTimeout<{ id: string; name: string; stage: string }[]>(
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates`
)
}
/**
* Preview an email with variables filled in
*/
export async function previewEmail(
templateId: string,
dsrId: string
): Promise<{ subject: string; body: string }> {
return fetchWithTimeout<{ subject: string; body: string }>(
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates/${templateId}/preview`,
{
method: 'POST',
body: JSON.stringify({ dsrId })
}
)
}
// =============================================================================
// SDK API FUNCTIONS (via Next.js proxy to ai-compliance-sdk)
// =============================================================================
interface BackendDSR {
id: string
tenant_id: string
namespace_id?: string
request_type: string
status: string
subject_name: string
subject_email: string
subject_identifier?: string
request_description: string
request_channel: string
received_at: string
verified_at?: string
verification_method?: string
deadline_at: string
extended_deadline_at?: string
extension_reason?: string
completed_at?: string
response_sent: boolean
response_sent_at?: string
response_method?: string
rejection_reason?: string
notes?: string
affected_systems?: string[]
assigned_to?: string
created_at: string
updated_at: string
}
function mapBackendStatus(status: string): import('./types').DSRStatus {
const mapping: Record<string, import('./types').DSRStatus> = {
'received': 'intake',
'verified': 'identity_verification',
'in_progress': 'processing',
'completed': 'completed',
'rejected': 'rejected',
'extended': 'processing',
}
return mapping[status] || 'intake'
}
function mapBackendChannel(channel: string): import('./types').DSRSource {
const mapping: Record<string, import('./types').DSRSource> = {
'email': 'email',
'form': 'web_form',
'phone': 'phone',
'letter': 'letter',
}
return mapping[channel] || 'other'
}
/**
* Transform flat backend DSR to nested SDK DSRRequest format
*/
export function transformBackendDSR(b: BackendDSR): DSRRequest {
const deadlineAt = b.extended_deadline_at || b.deadline_at
const receivedDate = new Date(b.received_at)
const defaultDeadlineDays = 30
const originalDeadline = b.deadline_at || new Date(receivedDate.getTime() + defaultDeadlineDays * 24 * 60 * 60 * 1000).toISOString()
return {
id: b.id,
referenceNumber: `DSR-${new Date(b.created_at).getFullYear()}-${b.id.slice(0, 6).toUpperCase()}`,
type: b.request_type as DSRRequest['type'],
status: mapBackendStatus(b.status),
priority: 'normal',
requester: {
name: b.subject_name,
email: b.subject_email,
customerId: b.subject_identifier,
},
source: mapBackendChannel(b.request_channel),
requestText: b.request_description,
receivedAt: b.received_at,
deadline: {
originalDeadline,
currentDeadline: deadlineAt,
extended: !!b.extended_deadline_at,
extensionReason: b.extension_reason,
},
completedAt: b.completed_at,
identityVerification: {
verified: !!b.verified_at,
verifiedAt: b.verified_at,
method: b.verification_method as any,
},
assignment: {
assignedTo: b.assigned_to || null,
},
notes: b.notes,
createdAt: b.created_at,
createdBy: 'system',
updatedAt: b.updated_at,
tenantId: b.tenant_id,
}
}
function getSdkHeaders(): HeadersInit {
if (typeof window === 'undefined') return {}
return {
'Content-Type': 'application/json',
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
}
/**
* Fetch DSR list from SDK backend via proxy
*/
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
headers: getSdkHeaders(),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
const backendDSRs: BackendDSR[] = data.dsrs || []
const requests = backendDSRs.map(transformBackendDSR)
// Calculate statistics locally
const now = new Date()
const statistics: DSRStatistics = {
total: requests.length,
byStatus: {
intake: requests.filter(r => r.status === 'intake').length,
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
processing: requests.filter(r => r.status === 'processing').length,
completed: requests.filter(r => r.status === 'completed').length,
rejected: requests.filter(r => r.status === 'rejected').length,
cancelled: requests.filter(r => r.status === 'cancelled').length,
},
byType: {
access: requests.filter(r => r.type === 'access').length,
rectification: requests.filter(r => r.type === 'rectification').length,
erasure: requests.filter(r => r.type === 'erasure').length,
restriction: requests.filter(r => r.type === 'restriction').length,
portability: requests.filter(r => r.type === 'portability').length,
objection: requests.filter(r => r.type === 'objection').length,
},
overdue: requests.filter(r => {
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
return new Date(r.deadline.currentDeadline) < now
}).length,
dueThisWeek: requests.filter(r => {
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
const deadline = new Date(r.deadline.currentDeadline)
const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
return deadline >= now && deadline <= weekFromNow
}).length,
averageProcessingDays: 0,
completedThisMonth: requests.filter(r => {
if (r.status !== 'completed' || !r.completedAt) return false
const completed = new Date(r.completedAt)
return completed.getMonth() === now.getMonth() && completed.getFullYear() === now.getFullYear()
}).length,
}
return { requests, statistics }
}
/**
* Create a new DSR via SDK backend
*/
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
const body = {
request_type: request.type,
subject_name: request.requester.name,
subject_email: request.requester.email,
subject_identifier: request.requester.customerId || '',
request_description: request.requestText || '',
request_channel: request.source === 'web_form' ? 'form' : request.source,
notes: '',
}
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify(body),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
}
/**
* Fetch a single DSR by ID from SDK backend
*/
export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
headers: getSdkHeaders(),
})
if (!res.ok) {
return null
}
const data = await res.json()
if (!data || !data.id) return null
return transformBackendDSR(data)
}
/**
* Update DSR status via SDK backend
*/
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
method: 'PUT',
headers: getSdkHeaders(),
body: JSON.stringify({ status }),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
}
// =============================================================================
// MOCK DATA FUNCTIONS (kept as fallback)
// =============================================================================
export function createMockDSRList(): DSRRequest[] {
const now = new Date()
return [
{
id: 'dsr-001',
referenceNumber: 'DSR-2025-000001',
type: 'access',
status: 'intake',
priority: 'high',
requester: {
name: 'Max Mustermann',
email: 'max.mustermann@example.de'
},
source: 'web_form',
sourceDetails: 'Kontaktformular auf breakpilot.de',
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: false
},
assignment: {
assignedTo: null
},
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-002',
referenceNumber: 'DSR-2025-000002',
type: 'erasure',
status: 'identity_verification',
priority: 'high',
requester: {
name: 'Anna Schmidt',
email: 'anna.schmidt@example.de',
phone: '+49 170 1234567'
},
source: 'email',
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: false
},
assignment: {
assignedTo: 'DSB Mueller',
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
},
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-003',
referenceNumber: 'DSR-2025-000003',
type: 'rectification',
status: 'processing',
priority: 'normal',
requester: {
name: 'Peter Meier',
email: 'peter.meier@example.de'
},
source: 'email',
requestText: 'Meine Adresse ist falsch gespeichert.',
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: true,
method: 'existing_account',
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'DSB Mueller',
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
},
rectificationDetails: {
fieldsToCorrect: [
{
field: 'Adresse',
currentValue: 'Musterstr. 1, 12345 Berlin',
requestedValue: 'Musterstr. 10, 12345 Berlin',
corrected: false
}
]
},
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-004',
referenceNumber: 'DSR-2025-000004',
type: 'portability',
status: 'processing',
priority: 'normal',
requester: {
name: 'Lisa Weber',
email: 'lisa.weber@example.de'
},
source: 'web_form',
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: true,
method: 'id_document',
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'IT Team',
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
},
notes: 'JSON-Export wird vorbereitet',
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-005',
referenceNumber: 'DSR-2025-000005',
type: 'objection',
status: 'rejected',
priority: 'low',
requester: {
name: 'Thomas Klein',
email: 'thomas.klein@example.de'
},
source: 'letter',
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
identityVerification: {
verified: true,
method: 'postal',
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'Rechtsabteilung',
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
},
objectionDetails: {
processingPurpose: 'Marketing',
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
objectionGrounds: 'Keine konkreten Gruende genannt',
decision: 'rejected',
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
decisionBy: 'Rechtsabteilung',
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
},
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-006',
referenceNumber: 'DSR-2025-000006',
type: 'access',
status: 'completed',
priority: 'normal',
requester: {
name: 'Sarah Braun',
email: 'sarah.braun@example.de'
},
source: 'email',
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
identityVerification: {
verified: true,
method: 'id_document',
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'DSB Mueller',
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
},
dataExport: {
format: 'pdf',
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
generatedBy: 'DSB Mueller',
fileName: 'datenauskunft_sarah_braun.pdf',
fileSize: 245000,
includesThirdPartyData: false
},
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
}
]
}
export function createMockStatistics(): DSRStatistics {
return {
total: 6,
byStatus: {
intake: 1,
identity_verification: 1,
processing: 2,
completed: 1,
rejected: 1,
cancelled: 0
},
byType: {
access: 2,
rectification: 1,
erasure: 1,
restriction: 0,
portability: 1,
objection: 1
},
overdue: 0,
dueThisWeek: 2,
averageProcessingDays: 18,
completedThisMonth: 1
}
}
+6
View File
@@ -0,0 +1,6 @@
/**
* DSR Module Exports
*/
export * from './types'
export * from './api'
+581
View File
@@ -0,0 +1,581 @@
/**
* DSR (Data Subject Request) Types
*
* TypeScript definitions for GDPR Art. 15-21 Data Subject Requests
* Based on the Go Consent Service backend API structure
*/
// =============================================================================
// ENUMS & CONSTANTS
// =============================================================================
export type DSRType =
| 'access' // Art. 15 - Auskunftsrecht
| 'rectification' // Art. 16 - Berichtigungsrecht
| 'erasure' // Art. 17 - Loeschungsrecht
| 'restriction' // Art. 18 - Einschraenkungsrecht
| 'portability' // Art. 20 - Datenuebertragbarkeit
| 'objection' // Art. 21 - Widerspruchsrecht
export type DSRStatus =
| 'intake' // Eingang - Anfrage dokumentiert
| 'identity_verification' // Identitaetspruefung
| 'processing' // In Bearbeitung
| 'completed' // Abgeschlossen
| 'rejected' // Abgelehnt
| 'cancelled' // Storniert
export type DSRPriority = 'low' | 'normal' | 'high' | 'critical'
export type DSRSource =
| 'web_form' // Kontaktformular/Portal
| 'email' // E-Mail
| 'letter' // Brief
| 'phone' // Telefon
| 'in_person' // Persoenlich
| 'other' // Sonstiges
export type IdentityVerificationMethod =
| 'id_document' // Ausweiskopie
| 'email' // E-Mail-Bestaetigung
| 'phone' // Telefonische Bestaetigung
| 'postal' // Postalische Bestaetigung
| 'existing_account' // Bestehendes Kundenkonto
| 'other' // Sonstiges
export type CommunicationType =
| 'incoming' // Eingehend (vom Betroffenen)
| 'outgoing' // Ausgehend (an Betroffenen)
| 'internal' // Intern (Notizen)
export type CommunicationChannel =
| 'email'
| 'letter'
| 'phone'
| 'portal'
| 'internal_note'
// =============================================================================
// DSR TYPE METADATA
// =============================================================================
export interface DSRTypeInfo {
type: DSRType
article: string
label: string
labelShort: string
description: string
defaultDeadlineDays: number
maxExtensionMonths: number
color: string
bgColor: string
processDocument?: string // Reference to process document
}
export const DSR_TYPE_INFO: Record<DSRType, DSRTypeInfo> = {
access: {
type: 'access',
article: 'Art. 15',
label: 'Auskunftsrecht',
labelShort: 'Auskunft',
description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten',
defaultDeadlineDays: 30,
maxExtensionMonths: 2,
color: 'text-blue-700',
bgColor: 'bg-blue-100',
processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf'
},
rectification: {
type: 'rectification',
article: 'Art. 16',
label: 'Berichtigungsrecht',
labelShort: 'Berichtigung',
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
defaultDeadlineDays: 14,
maxExtensionMonths: 2,
color: 'text-yellow-700',
bgColor: 'bg-yellow-100',
processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf'
},
erasure: {
type: 'erasure',
article: 'Art. 17',
label: 'Loeschungsrecht',
labelShort: 'Loeschung',
description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")',
defaultDeadlineDays: 14,
maxExtensionMonths: 2,
color: 'text-red-700',
bgColor: 'bg-red-100',
processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf'
},
restriction: {
type: 'restriction',
article: 'Art. 18',
label: 'Einschraenkungsrecht',
labelShort: 'Einschraenkung',
description: 'Recht auf Einschraenkung der Verarbeitung',
defaultDeadlineDays: 14,
maxExtensionMonths: 2,
color: 'text-orange-700',
bgColor: 'bg-orange-100',
processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf'
},
portability: {
type: 'portability',
article: 'Art. 20',
label: 'Datenuebertragbarkeit',
labelShort: 'Uebertragung',
description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format',
defaultDeadlineDays: 30,
maxExtensionMonths: 2,
color: 'text-purple-700',
bgColor: 'bg-purple-100',
processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf'
},
objection: {
type: 'objection',
article: 'Art. 21',
label: 'Widerspruchsrecht',
labelShort: 'Widerspruch',
description: 'Recht auf Widerspruch gegen die Verarbeitung',
defaultDeadlineDays: 30,
maxExtensionMonths: 0, // No extension allowed for objections
color: 'text-gray-700',
bgColor: 'bg-gray-100'
}
}
export const DSR_STATUS_INFO: Record<DSRStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
intake: {
label: 'Eingang',
color: 'text-blue-700',
bgColor: 'bg-blue-100',
borderColor: 'border-blue-200'
},
identity_verification: {
label: 'ID-Pruefung',
color: 'text-yellow-700',
bgColor: 'bg-yellow-100',
borderColor: 'border-yellow-200'
},
processing: {
label: 'In Bearbeitung',
color: 'text-purple-700',
bgColor: 'bg-purple-100',
borderColor: 'border-purple-200'
},
completed: {
label: 'Abgeschlossen',
color: 'text-green-700',
bgColor: 'bg-green-100',
borderColor: 'border-green-200'
},
rejected: {
label: 'Abgelehnt',
color: 'text-red-700',
bgColor: 'bg-red-100',
borderColor: 'border-red-200'
},
cancelled: {
label: 'Storniert',
color: 'text-gray-700',
bgColor: 'bg-gray-100',
borderColor: 'border-gray-200'
}
}
// =============================================================================
// MAIN INTERFACES
// =============================================================================
export interface DSRRequester {
name: string
email: string
phone?: string
address?: string
customerId?: string // If existing customer
}
export interface DSRIdentityVerification {
verified: boolean
method?: IdentityVerificationMethod
verifiedAt?: string
verifiedBy?: string
notes?: string
documentRef?: string // Reference to uploaded ID document
}
export interface DSRAssignment {
assignedTo: string | null
assignedAt?: string
assignedBy?: string
}
export interface DSRDeadline {
originalDeadline: string
currentDeadline: string
extended: boolean
extensionReason?: string
extensionApprovedBy?: string
extensionApprovedAt?: string
}
export interface DSRRequest {
id: string
referenceNumber: string // e.g., "DSR-2025-000042"
type: DSRType
status: DSRStatus
priority: DSRPriority
// Requester info
requester: DSRRequester
// Request details
source: DSRSource
sourceDetails?: string // e.g., "Kontaktformular auf website.de"
requestText?: string // Original request text
// Dates
receivedAt: string
deadline: DSRDeadline
completedAt?: string
// Verification
identityVerification: DSRIdentityVerification
// Assignment
assignment: DSRAssignment
// Processing
notes?: string
internalNotes?: string
// Type-specific data
erasureChecklist?: DSRErasureChecklist // For Art. 17
dataExport?: DSRDataExport // For Art. 15, 20
rectificationDetails?: DSRRectificationDetails // For Art. 16
objectionDetails?: DSRObjectionDetails // For Art. 21
// Audit
createdAt: string
createdBy: string
updatedAt: string
updatedBy?: string
// Metadata
tenantId: string
}
// =============================================================================
// TYPE-SPECIFIC INTERFACES
// =============================================================================
// Art. 17(3) Erasure Exceptions Checklist
export interface DSRErasureChecklistItem {
id: string
article: string // e.g., "17(3)(a)"
label: string
description: string
checked: boolean
applies: boolean
notes?: string
}
export interface DSRErasureChecklist {
items: DSRErasureChecklistItem[]
canProceedWithErasure: boolean
reviewedBy?: string
reviewedAt?: string
}
export const ERASURE_EXCEPTIONS: Omit<DSRErasureChecklistItem, 'checked' | 'applies' | 'notes'>[] = [
{
id: 'art17_3_a',
article: '17(3)(a)',
label: 'Meinungs- und Informationsfreiheit',
description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information'
},
{
id: 'art17_3_b',
article: '17(3)(b)',
label: 'Rechtliche Verpflichtung',
description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)'
},
{
id: 'art17_3_c',
article: '17(3)(c)',
label: 'Oeffentliches Interesse',
description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit'
},
{
id: 'art17_3_d',
article: '17(3)(d)',
label: 'Archivzwecke',
description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik'
},
{
id: 'art17_3_e',
article: '17(3)(e)',
label: 'Rechtsansprueche',
description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen'
}
]
// Data Export for Art. 15, 20
export interface DSRDataExport {
format: 'json' | 'csv' | 'xml' | 'pdf'
generatedAt?: string
generatedBy?: string
fileUrl?: string
fileName?: string
fileSize?: number
includesThirdPartyData: boolean
anonymizedFields?: string[]
transferMethod?: 'download' | 'email' | 'third_party' // For Art. 20 transfer
transferRecipient?: string // For Art. 20 transfer to another controller
}
// Rectification Details for Art. 16
export interface DSRRectificationDetails {
fieldsToCorrect: {
field: string
currentValue: string
requestedValue: string
corrected: boolean
correctedAt?: string
correctedBy?: string
}[]
}
// Objection Details for Art. 21
export interface DSRObjectionDetails {
processingPurpose: string
legalBasis: string
objectionGrounds: string
decision: 'accepted' | 'rejected' | 'pending'
decisionReason?: string
decisionBy?: string
decisionAt?: string
}
// =============================================================================
// COMMUNICATION
// =============================================================================
export interface DSRCommunication {
id: string
dsrId: string
type: CommunicationType
channel: CommunicationChannel
subject?: string
content: string
templateUsed?: string // Reference to email template
attachments?: {
name: string
url: string
size: number
type: string
}[]
sentAt?: string
sentBy?: string
receivedAt?: string
createdAt: string
createdBy: string
}
// =============================================================================
// AUDIT LOG
// =============================================================================
export interface DSRAuditEntry {
id: string
dsrId: string
action: string // e.g., "status_changed", "identity_verified", "assigned"
previousValue?: string
newValue?: string
performedBy: string
performedAt: string
notes?: string
}
// =============================================================================
// EMAIL TEMPLATES
// =============================================================================
export interface DSREmailTemplate {
id: string
name: string
subject: string
body: string
type: DSRType | 'general'
stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion'
language: 'de' | 'en'
variables: string[] // e.g., ["requesterName", "referenceNumber", "deadline"]
}
export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [
{
id: 'intake_confirmation',
name: 'Eingangsbestaetigung',
subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}',
body: `Sehr geehrte(r) {{requesterName}},
wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}.
Referenznummer: {{referenceNumber}}
Art der Anfrage: {{requestType}}
Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten.
Mit freundlichen Gruessen
{{senderName}}
Datenschutzbeauftragter`,
type: 'general',
stage: 'intake',
language: 'de',
variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName']
},
{
id: 'identity_request',
name: 'Identitaetsanfrage',
subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}',
body: `Sehr geehrte(r) {{requesterName}},
um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet.
Bitte senden Sie uns eines der folgenden Dokumente:
- Kopie Ihres Personalausweises (Vorder- und Rueckseite)
- Kopie Ihres Reisepasses
Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht.
Mit freundlichen Gruessen
{{senderName}}
Datenschutzbeauftragter`,
type: 'general',
stage: 'identity_request',
language: 'de',
variables: ['requesterName', 'referenceNumber', 'senderName']
}
]
// =============================================================================
// API TYPES
// =============================================================================
export interface DSRFilters {
status?: DSRStatus | DSRStatus[]
type?: DSRType | DSRType[]
priority?: DSRPriority
assignedTo?: string
overdue?: boolean
search?: string
dateFrom?: string
dateTo?: string
}
export interface DSRListResponse {
requests: DSRRequest[]
total: number
page: number
pageSize: number
}
export interface DSRCreateRequest {
type: DSRType
requester: DSRRequester
source: DSRSource
sourceDetails?: string
requestText?: string
priority?: DSRPriority
}
export interface DSRUpdateRequest {
status?: DSRStatus
priority?: DSRPriority
notes?: string
internalNotes?: string
assignment?: DSRAssignment
}
export interface DSRVerifyIdentityRequest {
method: IdentityVerificationMethod
notes?: string
documentRef?: string
}
export interface DSRCompleteRequest {
completionNotes?: string
dataExport?: DSRDataExport
}
export interface DSRRejectRequest {
reason: string
legalBasis?: string // e.g., Art. 17(3) exception
}
export interface DSRExtendDeadlineRequest {
extensionMonths: 1 | 2
reason: string
}
export interface DSRSendCommunicationRequest {
type: CommunicationType
channel: CommunicationChannel
subject?: string
content: string
templateId?: string
}
// =============================================================================
// STATISTICS
// =============================================================================
export interface DSRStatistics {
total: number
byStatus: Record<DSRStatus, number>
byType: Record<DSRType, number>
overdue: number
dueThisWeek: number
averageProcessingDays: number
completedThisMonth: number
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function getDaysRemaining(deadline: string): number {
const deadlineDate = new Date(deadline)
const now = new Date()
const diff = deadlineDate.getTime() - now.getTime()
return Math.ceil(diff / (1000 * 60 * 60 * 24))
}
export function isOverdue(request: DSRRequest): boolean {
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
return false
}
return getDaysRemaining(request.deadline.currentDeadline) < 0
}
export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean {
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
return false
}
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
return daysRemaining >= 0 && daysRemaining <= thresholdDays
}
export function generateReferenceNumber(year: number, sequence: number): string {
return `DSR-${year}-${String(sequence).padStart(6, '0')}`
}
export function getTypeInfo(type: DSRType): DSRTypeInfo {
return DSR_TYPE_INFO[type]
}
export function getStatusInfo(status: DSRStatus) {
return DSR_STATUS_INFO[status]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
/**
* YAML Catalog Loader - Erweiterte Version mit 128 Datenpunkten
*/
import {
DataPoint,
DataPointCategory,
RetentionMatrixEntry,
CookieBannerCategory,
LocalizedText,
DataPointCatalog,
} from '../types'
function l(de: string, en: string): LocalizedText {
return { de, en }
}
// =============================================================================
// VORDEFINIERTE DATENPUNKTE (128 Stueck in 18 Kategorien)
// =============================================================================
export const PREDEFINED_DATA_POINTS: DataPoint[] = [
// KATEGORIE A: STAMMDATEN (8)
{ id: 'dp-a1-firstname', code: 'A1', category: 'MASTER_DATA', name: l('Vorname', 'First Name'), description: l('Vorname der betroffenen Person', 'First name of the data subject'), purpose: l('Identifikation, Vertragserfuellung', 'Identification, contract fulfillment'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Erforderlich zur Vertragserfuellung', 'Required for contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['identity', 'master-data'] },
{ id: 'dp-a2-lastname', code: 'A2', category: 'MASTER_DATA', name: l('Nachname', 'Last Name'), description: l('Nachname der betroffenen Person', 'Last name of the data subject'), purpose: l('Identifikation, Vertragserfuellung', 'Identification, contract fulfillment'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Erforderlich zur Vertragserfuellung', 'Required for contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['identity', 'master-data'] },
{ id: 'dp-a3-birthdate', code: 'A3', category: 'MASTER_DATA', name: l('Geburtsdatum', 'Date of Birth'), description: l('Geburtsdatum zur Altersverifikation', 'Date of birth for age verification'), purpose: l('Altersverifikation, Identitaetspruefung', 'Age verification, identity check'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Erforderlich zur Altersverifikation', 'Required for age verification'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Fuer Identifikationszwecke', 'For identification purposes'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['identity', 'master-data'] },
{ id: 'dp-a4-gender', code: 'A4', category: 'MASTER_DATA', name: l('Geschlecht', 'Gender'), description: l('Geschlechtsangabe (optional)', 'Gender (optional)'), purpose: l('Personalisierte Ansprache', 'Personalized communication'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['personal', 'master-data'] },
{ id: 'dp-a5-title', code: 'A5', category: 'MASTER_DATA', name: l('Anrede/Titel', 'Salutation/Title'), description: l('Akademischer Titel oder Anrede', 'Academic title or salutation'), purpose: l('Korrekte Ansprache', 'Correct salutation'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Bestandteil der Kommunikation', 'Part of communication'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['personal', 'master-data'] },
{ id: 'dp-a6-profile-picture', code: 'A6', category: 'MASTER_DATA', name: l('Profilbild', 'Profile Picture'), description: l('Vom Nutzer hochgeladenes Profilbild', 'User-uploaded profile picture'), purpose: l('Visuelle Identifikation', 'Visual identification'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Upload', 'Voluntary upload'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Loeschung bei Kontoschliessung', 'Deletion on account closure'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['CDN'], technicalMeasures: ['Verschluesselung'], tags: ['image', 'master-data'] },
{ id: 'dp-a7-nationality', code: 'A7', category: 'MASTER_DATA', name: l('Staatsangehoerigkeit', 'Nationality'), description: l('Staatsangehoerigkeit der Person', 'Nationality of the person'), purpose: l('Compliance-Pruefung', 'Compliance check'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Compliance (Sanktionslisten)', 'Compliance (sanction lists)'), retentionPeriod: '10_YEARS', retentionJustification: l('Aufbewahrungspflichten', 'Retention obligations'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['compliance', 'master-data'] },
{ id: 'dp-a8-username', code: 'A8', category: 'MASTER_DATA', name: l('Benutzername', 'Username'), description: l('Selbst gewaehlter Benutzername', 'Self-chosen username'), purpose: l('Identifikation, Login', 'Identification, login'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Kontoverwaltung', 'For account management'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['account', 'master-data'] },
// KATEGORIE B: KONTAKTDATEN (10)
{ id: 'dp-b1-email', code: 'B1', category: 'CONTACT_DATA', name: l('E-Mail-Adresse', 'Email Address'), description: l('Primaere E-Mail-Adresse', 'Primary email address'), purpose: l('Kommunikation, Benachrichtigungen', 'Communication, notifications'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['E-Mail-Dienstleister'], technicalMeasures: ['TLS-Verschluesselung'], tags: ['contact', 'essential'] },
{ id: 'dp-b2-phone', code: 'B2', category: 'CONTACT_DATA', name: l('Telefonnummer', 'Phone Number'), description: l('Festnetz-Telefonnummer', 'Landline phone number'), purpose: l('Telefonische Kontaktaufnahme', 'Phone contact'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Kundensupport', 'For customer support'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'phone'] },
{ id: 'dp-b3-mobile', code: 'B3', category: 'CONTACT_DATA', name: l('Mobilnummer', 'Mobile Number'), description: l('Mobiltelefonnummer', 'Mobile phone number'), purpose: l('SMS-Benachrichtigungen, 2FA', 'SMS notifications, 2FA'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Sicherheit und Kommunikation', 'For security and communication'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SMS-Provider'], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'mobile', '2fa'] },
{ id: 'dp-b4-address-street', code: 'B4', category: 'CONTACT_DATA', name: l('Strasse/Hausnummer', 'Street/House Number'), description: l('Strassenadresse', 'Street address'), purpose: l('Lieferung, Rechnungsstellung', 'Delivery, billing'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Versanddienstleister'], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'address'] },
{ id: 'dp-b5-address-city', code: 'B5', category: 'CONTACT_DATA', name: l('PLZ/Ort', 'Postal Code/City'), description: l('Postleitzahl und Stadt', 'Postal code and city'), purpose: l('Lieferung, Rechnungsstellung', 'Delivery, billing'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Versanddienstleister'], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'address'] },
{ id: 'dp-b6-address-country', code: 'B6', category: 'CONTACT_DATA', name: l('Land', 'Country'), description: l('Wohnsitzland', 'Country of residence'), purpose: l('Lieferung, Steuer', 'Delivery, tax'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'address'] },
{ id: 'dp-b7-secondary-email', code: 'B7', category: 'CONTACT_DATA', name: l('Sekundaere E-Mail', 'Secondary Email'), description: l('Alternative E-Mail-Adresse', 'Alternative email address'), purpose: l('Backup-Kontakt, Wiederherstellung', 'Backup contact, recovery'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['TLS'], tags: ['contact', 'backup'] },
{ id: 'dp-b8-fax', code: 'B8', category: 'CONTACT_DATA', name: l('Faxnummer', 'Fax Number'), description: l('Faxnummer (geschaeftlich)', 'Fax number (business)'), purpose: l('Geschaeftliche Kommunikation', 'Business communication'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer geschaeftliche Kommunikation', 'For business communication'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'business'] },
{ id: 'dp-b9-emergency-contact', code: 'B9', category: 'CONTACT_DATA', name: l('Notfallkontakt', 'Emergency Contact'), description: l('Kontaktdaten fuer Notfaelle', 'Emergency contact details'), purpose: l('Notfallbenachrichtigung', 'Emergency notification'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'emergency'] },
{ id: 'dp-b10-social-profiles', code: 'B10', category: 'CONTACT_DATA', name: l('Social-Media-Profile', 'Social Media Profiles'), description: l('Links zu sozialen Netzwerken', 'Links to social networks'), purpose: l('Vernetzung, Kommunikation', 'Networking, communication'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'social'] },
// KATEGORIE C: AUTHENTIFIZIERUNGSDATEN (8)
{ id: 'dp-c1-password-hash', code: 'C1', category: 'AUTHENTICATION', name: l('Passwort-Hash', 'Password Hash'), description: l('Kryptografisch gehashtes Passwort', 'Cryptographically hashed password'), purpose: l('Sichere Authentifizierung', 'Secure authentication'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer sichere Kontoverwaltung', 'For secure account management'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['bcrypt/Argon2', 'Salting'], tags: ['auth', 'security'] },
{ id: 'dp-c2-session-token', code: 'C2', category: 'AUTHENTICATION', name: l('Session-Token', 'Session Token'), description: l('JWT oder Session-ID', 'JWT or Session ID'), purpose: l('Aufrechterhaltung der Sitzung', 'Maintaining session'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch erforderlich', 'Technically required'), retentionPeriod: '24_HOURS', retentionJustification: l('Kurze Lebensdauer', 'Short lifespan'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['JWT-Signatur', 'HttpOnly'], tags: ['auth', 'session'] },
{ id: 'dp-c3-refresh-token', code: 'C3', category: 'AUTHENTICATION', name: l('Refresh-Token', 'Refresh Token'), description: l('Token zur Session-Erneuerung', 'Token for session renewal'), purpose: l('Session-Erneuerung', 'Session renewal'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Benutzerfreundlichkeit', 'For user experience'), retentionPeriod: '30_DAYS', retentionJustification: l('Balance Sicherheit/UX', 'Balance security/UX'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Token-Rotation'], tags: ['auth', 'token'] },
{ id: 'dp-c4-2fa-secret', code: 'C4', category: 'AUTHENTICATION', name: l('2FA-Secret', '2FA Secret'), description: l('TOTP-Geheimnis fuer Zwei-Faktor-Auth', 'TOTP secret for two-factor auth'), purpose: l('Erhoehte Kontosicherheit', 'Enhanced account security'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Sicherheit', 'For security'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange 2FA aktiv', 'While 2FA active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'HSM'], tags: ['auth', '2fa', 'security'] },
{ id: 'dp-c5-passkey', code: 'C5', category: 'AUTHENTICATION', name: l('Passkey/WebAuthn', 'Passkey/WebAuthn'), description: l('FIDO2/WebAuthn Credential', 'FIDO2/WebAuthn Credential'), purpose: l('Passwortlose Authentifizierung', 'Passwordless authentication'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer sichere Anmeldung', 'For secure login'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Passkey aktiv', 'While passkey active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Asymmetrische Kryptografie'], tags: ['auth', 'passkey'] },
{ id: 'dp-c6-api-keys', code: 'C6', category: 'AUTHENTICATION', name: l('API-Keys', 'API Keys'), description: l('API-Schluessel fuer Integrationen', 'API keys for integrations'), purpose: l('API-Zugriff', 'API access'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer API-Nutzung', 'For API usage'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Hashing', 'Rate-Limiting'], tags: ['auth', 'api'] },
{ id: 'dp-c7-oauth-provider', code: 'C7', category: 'AUTHENTICATION', name: l('OAuth-Provider-ID', 'OAuth Provider ID'), description: l('ID vom externen Auth-Provider', 'ID from external auth provider'), purpose: l('Social Login', 'Social Login'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Social Login', 'For social login'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange verknuepft', 'While linked'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['OAuth-Provider'], technicalMeasures: ['Minimale Daten'], tags: ['auth', 'oauth'] },
{ id: 'dp-c8-recovery-codes', code: 'C8', category: 'AUTHENTICATION', name: l('Wiederherstellungscodes', 'Recovery Codes'), description: l('Backup-Codes fuer 2FA', 'Backup codes for 2FA'), purpose: l('Kontowiederherstellung', 'Account recovery'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Notfallzugriff', 'For emergency access'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange 2FA aktiv', 'While 2FA active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Hashing', 'Einmalnutzung'], tags: ['auth', 'recovery'] },
// KATEGORIE D: EINWILLIGUNGSDATEN (6)
{ id: 'dp-d1-consent-records', code: 'D1', category: 'CONSENT', name: l('Consent-Protokolle', 'Consent Records'), description: l('Protokollierte Einwilligungen', 'Recorded consents'), purpose: l('Nachweis gegenueber Behoerden', 'Proof to authorities'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Nachweispflicht Art. 7 DSGVO', 'Accountability Art. 7 GDPR'), retentionPeriod: '6_YEARS', retentionJustification: l('Audit-Zwecke', 'Audit purposes'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Unveraenderbare Logs'], tags: ['consent', 'compliance'] },
{ id: 'dp-d2-cookie-preferences', code: 'D2', category: 'CONSENT', name: l('Cookie-Praeferenzen', 'Cookie Preferences'), description: l('Cookie-Einstellungen des Nutzers', 'User cookie settings'), purpose: l('Speicherung der Consent-Entscheidung', 'Storage of consent decision'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Speicherung der Entscheidung', 'Storage of decision'), retentionPeriod: '12_MONTHS', retentionJustification: l('Branchenueblich', 'Industry standard'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['First-Party Cookie'], tags: ['consent', 'cookie'] },
{ id: 'dp-d3-marketing-consent', code: 'D3', category: 'CONSENT', name: l('Marketing-Einwilligung', 'Marketing Consent'), description: l('Einwilligung fuer Werbung', 'Consent for advertising'), purpose: l('Marketing-Kommunikation', 'Marketing communication'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Einwilligung', 'Voluntary consent'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Double-Opt-In'], tags: ['consent', 'marketing'] },
{ id: 'dp-d4-data-sharing', code: 'D4', category: 'CONSENT', name: l('Datenweitergabe-Einwilligung', 'Data Sharing Consent'), description: l('Einwilligung zur Datenweitergabe', 'Consent to data sharing'), purpose: l('Weitergabe an Partner', 'Sharing with partners'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Einwilligung', 'Voluntary consent'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Dokumentation'], tags: ['consent', 'sharing'] },
{ id: 'dp-d5-locale-preferences', code: 'D5', category: 'CONSENT', name: l('Sprach-/Regionspraeferenz', 'Language/Region Preference'), description: l('Bevorzugte Sprache und Region', 'Preferred language and region'), purpose: l('Lokalisierung', 'Localization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nutzereinstellungen', 'User settings'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['First-Party Cookie'], tags: ['preferences', 'locale'] },
{ id: 'dp-d6-newsletter-consent', code: 'D6', category: 'CONSENT', name: l('Newsletter-Einwilligung', 'Newsletter Consent'), description: l('Einwilligung fuer Newsletter', 'Newsletter subscription consent'), purpose: l('E-Mail-Marketing', 'Email marketing'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Double-Opt-In erforderlich', 'Double opt-in required'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['Double-Opt-In', 'Abmelde-Link'], tags: ['consent', 'newsletter'] },
// KATEGORIE E: KOMMUNIKATIONSDATEN (7)
{ id: 'dp-e1-support-tickets', code: 'E1', category: 'COMMUNICATION', name: l('Support-Tickets', 'Support Tickets'), description: l('Inhalt von Kundenanfragen', 'Content of customer inquiries'), purpose: l('Kundenservice', 'Customer service'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Supportleistungen', 'For support services'), retentionPeriod: '24_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Helpdesk-Software'], technicalMeasures: ['Verschluesselung'], tags: ['support', 'communication'] },
{ id: 'dp-e2-chat-history', code: 'E2', category: 'COMMUNICATION', name: l('Chat-Verlaeufe', 'Chat Histories'), description: l('Live-Chat und Chatbot-Verlaeufe', 'Live chat and chatbot histories'), purpose: l('Kundenservice, QA', 'Customer service, QA'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicequalitaet', 'Service quality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Qualitaetssicherung', 'Quality assurance'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Chat-Software'], technicalMeasures: ['Pseudonymisierung'], tags: ['support', 'chat'] },
{ id: 'dp-e3-call-recordings', code: 'E3', category: 'COMMUNICATION', name: l('Anrufaufzeichnungen', 'Call Recordings'), description: l('Aufzeichnungen von Telefonaten', 'Recordings of phone calls'), purpose: l('Qualitaetssicherung, Schulung', 'Quality assurance, training'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung vor Aufzeichnung', 'Explicit consent before recording'), retentionPeriod: '90_DAYS', retentionJustification: l('Begrenzte Qualitaetspruefung', 'Limited quality review'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Telefonie-Anbieter'], technicalMeasures: ['Verschluesselung', 'Auto-Loeschung'], tags: ['support', 'recording'] },
{ id: 'dp-e4-email-content', code: 'E4', category: 'COMMUNICATION', name: l('E-Mail-Inhalte', 'Email Content'), description: l('Inhalt von E-Mail-Korrespondenz', 'Content of email correspondence'), purpose: l('Kommunikation, Dokumentation', 'Communication, documentation'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Kommunikation', 'For communication'), retentionPeriod: '24_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['TLS', 'Archivierung'], tags: ['communication', 'email'] },
{ id: 'dp-e5-feedback', code: 'E5', category: 'COMMUNICATION', name: l('Feedback/Bewertungen', 'Feedback/Reviews'), description: l('Nutzerbewertungen und Feedback', 'User ratings and feedback'), purpose: l('Qualitaetsmessung', 'Quality measurement'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktqualitaet', 'Product quality'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['feedback', 'quality'] },
{ id: 'dp-e6-notifications', code: 'E6', category: 'COMMUNICATION', name: l('Benachrichtigungsverlauf', 'Notification History'), description: l('Historie gesendeter Benachrichtigungen', 'History of sent notifications'), purpose: l('Nachvollziehbarkeit', 'Traceability'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support-Zwecke', 'Support purposes'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Logging'], tags: ['communication', 'notifications'] },
{ id: 'dp-e7-forum-posts', code: 'E7', category: 'COMMUNICATION', name: l('Forum-/Community-Beitraege', 'Forum/Community Posts'), description: l('Beitraege in Foren oder Communities', 'Posts in forums or communities'), purpose: l('Community-Interaktion', 'Community interaction'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Community-Nutzung', 'Community usage'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Moderation'], tags: ['community', 'content'] },
// KATEGORIE F: ZAHLUNGSDATEN (8)
{ id: 'dp-f1-billing-address', code: 'F1', category: 'PAYMENT', name: l('Rechnungsadresse', 'Billing Address'), description: l('Vollstaendige Rechnungsanschrift', 'Complete billing address'), purpose: l('Rechnungsstellung, Steuer', 'Invoicing, tax'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO, §257 HGB', '§147 AO, §257 HGB'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Verschluesselung', 'Archivierung'], tags: ['payment', 'billing'] },
{ id: 'dp-f2-payment-token', code: 'F2', category: 'PAYMENT', name: l('Zahlungs-Token', 'Payment Token'), description: l('Tokenisierte Zahlungsinformationen', 'Tokenized payment information'), purpose: l('Wiederkehrende Zahlungen', 'Recurring payments'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Zahlungsabwicklung', 'For payment processing'), retentionPeriod: '36_MONTHS', retentionJustification: l('Kundenbeziehung plus Rueckbuchungsfrist', 'Customer relationship plus chargeback period'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Payment Provider'], technicalMeasures: ['PCI-DSS', 'Tokenisierung'], tags: ['payment', 'token'] },
{ id: 'dp-f3-transactions', code: 'F3', category: 'PAYMENT', name: l('Transaktionshistorie', 'Transaction History'), description: l('Historie aller Transaktionen', 'History of all transactions'), purpose: l('Buchfuehrung, Nachweis', 'Accounting, proof'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO', '§147 AO'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater', 'Wirtschaftspruefer'], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['payment', 'transactions'] },
{ id: 'dp-f4-iban', code: 'F4', category: 'PAYMENT', name: l('IBAN/Bankverbindung', 'IBAN/Bank Details'), description: l('Bankverbindung fuer Lastschrift', 'Bank details for direct debit'), purpose: l('Lastschrifteinzug', 'Direct debit'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer SEPA-Lastschrift', 'For SEPA direct debit'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Bank'], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['payment', 'bank'] },
{ id: 'dp-f5-invoices', code: 'F5', category: 'PAYMENT', name: l('Rechnungen', 'Invoices'), description: l('Ausgestellte Rechnungen', 'Issued invoices'), purpose: l('Buchfuehrung, Steuer', 'Accounting, tax'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO, §257 HGB', '§147 AO, §257 HGB'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['payment', 'invoices'] },
{ id: 'dp-f6-tax-id', code: 'F6', category: 'PAYMENT', name: l('USt-IdNr./Steuernummer', 'VAT ID/Tax Number'), description: l('Umsatzsteuer-ID oder Steuernummer', 'VAT ID or tax number'), purpose: l('Steuerliche Dokumentation', 'Tax documentation'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Steuerrecht', 'Tax law'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Verschluesselung'], tags: ['payment', 'tax'] },
{ id: 'dp-f7-subscription', code: 'F7', category: 'PAYMENT', name: l('Abonnement-Daten', 'Subscription Data'), description: l('Abonnement-Details und Status', 'Subscription details and status'), purpose: l('Abonnementverwaltung', 'Subscription management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['payment', 'subscription'] },
{ id: 'dp-f8-refunds', code: 'F8', category: 'PAYMENT', name: l('Erstattungen', 'Refunds'), description: l('Erstattungshistorie', 'Refund history'), purpose: l('Buchfuehrung', 'Accounting'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO', '§147 AO'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['payment', 'refunds'] },
// KATEGORIE G: NUTZUNGSDATEN (8)
{ id: 'dp-g1-session-duration', code: 'G1', category: 'USAGE_DATA', name: l('Sitzungsdauer', 'Session Duration'), description: l('Dauer einzelner Sitzungen', 'Duration of individual sessions'), purpose: l('Nutzungsanalyse', 'Usage analysis'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['usage', 'analytics'] },
{ id: 'dp-g2-page-views', code: 'G2', category: 'USAGE_DATA', name: l('Seitenaufrufe', 'Page Views'), description: l('Aufgerufene Seiten', 'Visited pages'), purpose: l('Nutzungsanalyse', 'Usage analysis'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['usage', 'analytics'] },
{ id: 'dp-g3-click-paths', code: 'G3', category: 'USAGE_DATA', name: l('Klickpfade', 'Click Paths'), description: l('Navigationsverhalten', 'Navigation behavior'), purpose: l('UX-Optimierung', 'UX optimization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '12_MONTHS', retentionJustification: l('UX-Analyse', 'UX analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Pseudonymisierung'], tags: ['usage', 'ux'] },
{ id: 'dp-g4-search-queries', code: 'G4', category: 'USAGE_DATA', name: l('Suchanfragen', 'Search Queries'), description: l('Interne Suchanfragen', 'Internal search queries'), purpose: l('Suchoptimierung', 'Search optimization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '90_DAYS', retentionJustification: l('Kurzfristige Analyse', 'Short-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Anonymisierung'], tags: ['usage', 'search'] },
{ id: 'dp-g5-feature-usage', code: 'G5', category: 'USAGE_DATA', name: l('Feature-Nutzung', 'Feature Usage'), description: l('Nutzung einzelner Features', 'Usage of individual features'), purpose: l('Produktentwicklung', 'Product development'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['usage', 'features'] },
{ id: 'dp-g6-error-logs', code: 'G6', category: 'USAGE_DATA', name: l('Fehlerprotokolle', 'Error Logs'), description: l('Client-seitige Fehler', 'Client-side errors'), purpose: l('Fehlerbehebung', 'Bug fixing'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Qualitaetssicherung', 'Quality assurance'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Error-Tracking-Dienst'], technicalMeasures: ['Pseudonymisierung'], tags: ['usage', 'errors'] },
{ id: 'dp-g7-preferences', code: 'G7', category: 'USAGE_DATA', name: l('Nutzereinstellungen', 'User Preferences'), description: l('Individuelle Einstellungen', 'Individual settings'), purpose: l('Personalisierung', 'Personalization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['usage', 'preferences'] },
{ id: 'dp-g8-ab-tests', code: 'G8', category: 'USAGE_DATA', name: l('A/B-Test-Zuordnung', 'A/B Test Assignment'), description: l('Zuordnung zu Testvarianten', 'Assignment to test variants'), purpose: l('Produktoptimierung', 'Product optimization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '90_DAYS', retentionJustification: l('Testdauer', 'Test duration'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Pseudonymisierung'], tags: ['usage', 'testing'] },
// KATEGORIE H: STANDORTDATEN (7)
{ id: 'dp-h1-gps', code: 'H1', category: 'LOCATION', name: l('GPS-Standort', 'GPS Location'), description: l('Praeziser GPS-Standort', 'Precise GPS location'), purpose: l('Standortbasierte Dienste', 'Location-based services'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung', 'Explicit consent'), retentionPeriod: '30_DAYS', retentionJustification: l('Datensparsamkeit', 'Data minimization'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'On-Device'], tags: ['location', 'gps'] },
{ id: 'dp-h2-ip-geo', code: 'H2', category: 'LOCATION', name: l('IP-Geolokation', 'IP Geolocation'), description: l('Ungefaehrer Standort aus IP', 'Approximate location from IP'), purpose: l('Regionalisierung', 'Regionalization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '90_DAYS', retentionJustification: l('Sicherheitsanalyse', 'Security analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Nur Landesebene'], tags: ['location', 'ip'] },
{ id: 'dp-h3-timezone', code: 'H3', category: 'LOCATION', name: l('Zeitzone', 'Timezone'), description: l('Zeitzone des Nutzers', 'User timezone'), purpose: l('Lokalisierung', 'Localization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Nutzereinstellung', 'User setting'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['location', 'timezone'] },
{ id: 'dp-h4-location-history', code: 'H4', category: 'LOCATION', name: l('Standortverlauf', 'Location History'), description: l('Historie von Standorten', 'History of locations'), purpose: l('Personalisierung', 'Personalization'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Funktion', 'Optional feature'), retentionPeriod: '30_DAYS', retentionJustification: l('Datensparsamkeit', 'Data minimization'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['location', 'history'] },
{ id: 'dp-h5-country', code: 'H5', category: 'LOCATION', name: l('Herkunftsland', 'Country of Origin'), description: l('Land basierend auf IP', 'Country based on IP'), purpose: l('Compliance, Geo-Blocking', 'Compliance, geo-blocking'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Compliance', 'Compliance'), retentionPeriod: '12_MONTHS', retentionJustification: l('Sicherheit', 'Security'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['location', 'country'] },
{ id: 'dp-h6-wifi-networks', code: 'H6', category: 'LOCATION', name: l('WLAN-Netzwerke', 'WiFi Networks'), description: l('Erkannte WLAN-Netzwerke', 'Detected WiFi networks'), purpose: l('Standortbestimmung', 'Location detection'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Nur mit Einwilligung', 'Only with consent'), retentionPeriod: '24_HOURS', retentionJustification: l('Kurzlebig', 'Short-lived'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['On-Device'], tags: ['location', 'wifi'] },
{ id: 'dp-h7-travel-info', code: 'H7', category: 'LOCATION', name: l('Reiseinformationen', 'Travel Information'), description: l('Reiseziele und Plaene', 'Travel destinations and plans'), purpose: l('Reiseservices', 'Travel services'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Reisedienste', 'For travel services'), retentionPeriod: '12_MONTHS', retentionJustification: l('Serviceerbringung', 'Service delivery'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Reiseanbieter'], technicalMeasures: ['Verschluesselung'], tags: ['location', 'travel'] },
// KATEGORIE I: GERAETEDATEN (10)
{ id: 'dp-i1-ip-address', code: 'I1', category: 'DEVICE_DATA', name: l('IP-Adresse', 'IP Address'), description: l('IP-Adresse des Nutzers', 'User IP address'), purpose: l('Sicherheit, Routing', 'Security, routing'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '90_DAYS', retentionJustification: l('Sicherheitsanalyse', 'Security analysis'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Security-Monitoring'], technicalMeasures: ['IP-Anonymisierung'], tags: ['device', 'network'] },
{ id: 'dp-i2-fingerprint', code: 'I2', category: 'DEVICE_DATA', name: l('Device Fingerprint', 'Device Fingerprint'), description: l('Hash aus Geraetemerkmalen', 'Hash of device characteristics'), purpose: l('Betrugspraevention', 'Fraud prevention'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Betrugspraevention', 'Fraud prevention'), retentionPeriod: '30_DAYS', retentionJustification: l('Kurze Speicherung', 'Short storage'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Einweg-Hashing'], tags: ['device', 'fingerprint'] },
{ id: 'dp-i3-browser', code: 'I3', category: 'DEVICE_DATA', name: l('Browser/User-Agent', 'Browser/User Agent'), description: l('Browser und Version', 'Browser and version'), purpose: l('Kompatibilitaet', 'Compatibility'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch notwendig', 'Technically necessary'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support', 'Support'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'browser'] },
{ id: 'dp-i4-os', code: 'I4', category: 'DEVICE_DATA', name: l('Betriebssystem', 'Operating System'), description: l('OS und Version', 'OS and version'), purpose: l('Kompatibilitaet', 'Compatibility'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch notwendig', 'Technically necessary'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support', 'Support'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'os'] },
{ id: 'dp-i5-screen', code: 'I5', category: 'DEVICE_DATA', name: l('Bildschirmaufloesung', 'Screen Resolution'), description: l('Bildschirmgroesse', 'Screen size'), purpose: l('Responsive Design', 'Responsive design'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('UX-Optimierung', 'UX optimization'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analytics', 'Analytics'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'screen'] },
{ id: 'dp-i6-language', code: 'I6', category: 'DEVICE_DATA', name: l('Browser-Sprache', 'Browser Language'), description: l('Spracheinstellung des Browsers', 'Browser language setting'), purpose: l('Lokalisierung', 'Localization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nutzereinstellung', 'User setting'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'language'] },
{ id: 'dp-i7-push-token', code: 'I7', category: 'DEVICE_DATA', name: l('Push-Token', 'Push Token'), description: l('Token fuer Push-Nachrichten', 'Token for push notifications'), purpose: l('Push-Benachrichtigungen', 'Push notifications'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Opt-In fuer Push', 'Opt-in for push'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Deaktivierung', 'Until deactivation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Push-Dienst'], technicalMeasures: ['Verschluesselung'], tags: ['device', 'push'] },
{ id: 'dp-i8-device-id', code: 'I8', category: 'DEVICE_DATA', name: l('Geraete-ID', 'Device ID'), description: l('Eindeutige Geraetekennung', 'Unique device identifier'), purpose: l('Geraeteverwaltung', 'Device management'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Multi-Device-Support', 'Multi-device support'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange verknuepft', 'While linked'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Hashing'], tags: ['device', 'id'] },
{ id: 'dp-i9-app-version', code: 'I9', category: 'DEVICE_DATA', name: l('App-Version', 'App Version'), description: l('Installierte App-Version', 'Installed app version'), purpose: l('Support, Updates', 'Support, updates'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch notwendig', 'Technically necessary'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support', 'Support'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'app'] },
{ id: 'dp-i10-hardware', code: 'I10', category: 'DEVICE_DATA', name: l('Hardware-Info', 'Hardware Info'), description: l('Geraetetyp, Hersteller', 'Device type, manufacturer'), purpose: l('Kompatibilitaet', 'Compatibility'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktentwicklung', 'Product development'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analytics', 'Analytics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'hardware'] },
// KATEGORIE J: MARKETINGDATEN (8)
{ id: 'dp-j1-tracking-pixel', code: 'J1', category: 'MARKETING', name: l('Tracking-Pixel', 'Tracking Pixel'), description: l('Conversion-Tracking-Pixel', 'Conversion tracking pixel'), purpose: l('Werbemessung', 'Ad measurement'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent erforderlich', 'Cookie consent required'), retentionPeriod: '90_DAYS', retentionJustification: l('Conversion-Fenster', 'Conversion window'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Google Ads', 'Meta'], technicalMeasures: ['Nur bei Consent'], tags: ['marketing', 'tracking'] },
{ id: 'dp-j2-advertising-id', code: 'J2', category: 'MARKETING', name: l('Werbe-ID', 'Advertising ID'), description: l('Geraetuebergreifende Werbe-ID', 'Cross-device advertising ID'), purpose: l('Personalisierte Werbung', 'Personalized advertising'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Wegen Profilbildung', 'Due to profiling'), retentionPeriod: '90_DAYS', retentionJustification: l('Kampagnenzeitraum', 'Campaign period'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Werbenetzwerke'], technicalMeasures: ['Opt-out'], tags: ['marketing', 'advertising'] },
{ id: 'dp-j3-utm', code: 'J3', category: 'MARKETING', name: l('UTM-Parameter', 'UTM Parameters'), description: l('Kampagnen-Tracking-Parameter', 'Campaign tracking parameters'), purpose: l('Kampagnen-Attribution', 'Campaign attribution'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Kampagnenmessung', 'Campaign measurement'), retentionPeriod: '30_DAYS', retentionJustification: l('Session-Attribution', 'Session attribution'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Analytics'], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'utm'] },
{ id: 'dp-j4-newsletter', code: 'J4', category: 'MARKETING', name: l('Newsletter-Daten', 'Newsletter Data'), description: l('E-Mail und Praeferenzen', 'Email and preferences'), purpose: l('Newsletter-Versand', 'Newsletter delivery'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Double-Opt-In', 'Double opt-in'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Abmeldung', 'Until unsubscribe'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['Double-Opt-In'], tags: ['marketing', 'newsletter'] },
{ id: 'dp-j5-remarketing', code: 'J5', category: 'MARKETING', name: l('Remarketing-Listen', 'Remarketing Lists'), description: l('Zielgruppen fuer Remarketing', 'Audiences for remarketing'), purpose: l('Remarketing', 'Remarketing'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Profilbildung', 'Profiling'), retentionPeriod: '90_DAYS', retentionJustification: l('Kampagnenzeitraum', 'Campaign period'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Werbenetzwerke'], technicalMeasures: ['Hashing'], tags: ['marketing', 'remarketing'] },
{ id: 'dp-j6-email-opens', code: 'J6', category: 'MARKETING', name: l('E-Mail-Oeffnungen', 'Email Opens'), description: l('Oeffnungsraten von E-Mails', 'Email open rates'), purpose: l('E-Mail-Optimierung', 'Email optimization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Teil der Newsletter-Einwilligung', 'Part of newsletter consent'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analyse', 'Analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'email'] },
{ id: 'dp-j7-ad-clicks', code: 'J7', category: 'MARKETING', name: l('Anzeigen-Klicks', 'Ad Clicks'), description: l('Klicks auf Werbeanzeigen', 'Clicks on advertisements'), purpose: l('Werbemessung', 'Ad measurement'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Teil der Werbe-Einwilligung', 'Part of advertising consent'), retentionPeriod: '90_DAYS', retentionJustification: l('Conversion-Fenster', 'Conversion window'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Werbenetzwerke'], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'advertising'] },
{ id: 'dp-j8-referrer', code: 'J8', category: 'MARKETING', name: l('Referrer-URL', 'Referrer URL'), description: l('Herkunftsseite', 'Source page'), purpose: l('Traffic-Analyse', 'Traffic analysis'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Marketing-Attribution', 'Marketing attribution'), retentionPeriod: '30_DAYS', retentionJustification: l('Kurzfristige Analyse', 'Short-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'referrer'] },
// KATEGORIE K: ANALYSEDATEN (7)
{ id: 'dp-k1-google-analytics', code: 'K1', category: 'ANALYTICS', name: l('Google Analytics', 'Google Analytics'), description: l('GA4-Analysedaten', 'GA4 analytics data'), purpose: l('Web-Analyse', 'Web analytics'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent erforderlich', 'Cookie consent required'), retentionPeriod: '26_MONTHS', retentionJustification: l('GA4-Standard', 'GA4 standard'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Google'], technicalMeasures: ['IP-Anonymisierung', 'Consent-Mode'], tags: ['analytics', 'ga4'] },
{ id: 'dp-k2-heatmaps', code: 'K2', category: 'ANALYTICS', name: l('Heatmaps', 'Heatmaps'), description: l('Klick- und Scroll-Heatmaps', 'Click and scroll heatmaps'), purpose: l('UX-Analyse', 'UX analysis'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent erforderlich', 'Cookie consent required'), retentionPeriod: '12_MONTHS', retentionJustification: l('UX-Optimierung', 'UX optimization'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Hotjar/Clarity'], technicalMeasures: ['Anonymisierung'], tags: ['analytics', 'heatmaps'] },
{ id: 'dp-k3-session-recording', code: 'K3', category: 'ANALYTICS', name: l('Session-Recordings', 'Session Recordings'), description: l('Aufzeichnung von Sitzungen', 'Recording of sessions'), purpose: l('UX-Analyse, Debugging', 'UX analysis, debugging'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung', 'Explicit consent'), retentionPeriod: '90_DAYS', retentionJustification: l('Begrenzte Aufbewahrung', 'Limited retention'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Recording-Dienst'], technicalMeasures: ['Passwort-Maskierung', 'PII-Filterung'], tags: ['analytics', 'recording'] },
{ id: 'dp-k4-events', code: 'K4', category: 'ANALYTICS', name: l('Event-Tracking', 'Event Tracking'), description: l('Benutzerdefinierte Events', 'Custom events'), purpose: l('Produktanalyse', 'Product analysis'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'events'] },
{ id: 'dp-k5-conversion', code: 'K5', category: 'ANALYTICS', name: l('Conversion-Daten', 'Conversion Data'), description: l('Konversions-Events', 'Conversion events'), purpose: l('Conversion-Optimierung', 'Conversion optimization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Business-Analyse', 'Business analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'conversion'] },
{ id: 'dp-k6-funnel', code: 'K6', category: 'ANALYTICS', name: l('Funnel-Analyse', 'Funnel Analysis'), description: l('Trichterdaten', 'Funnel data'), purpose: l('Conversion-Optimierung', 'Conversion optimization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Business-Analyse', 'Business analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'funnel'] },
{ id: 'dp-k7-cohort', code: 'K7', category: 'ANALYTICS', name: l('Kohorten-Analyse', 'Cohort Analysis'), description: l('Kohortenbasierte Daten', 'Cohort-based data'), purpose: l('Nutzeranalyse', 'User analysis'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'cohort'] },
// KATEGORIE L: SOCIAL-MEDIA-DATEN (6)
{ id: 'dp-l1-profile-id', code: 'L1', category: 'SOCIAL_MEDIA', name: l('Social-Profil-ID', 'Social Profile ID'), description: l('ID aus Social Login', 'ID from social login'), purpose: l('Social Login', 'Social login'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Social Login', 'Voluntary social login'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange verknuepft', 'While linked'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Social-Network'], technicalMeasures: ['Minimale Daten'], tags: ['social', 'login'] },
{ id: 'dp-l2-avatar', code: 'L2', category: 'SOCIAL_MEDIA', name: l('Social-Avatar', 'Social Avatar'), description: l('Profilbild aus Social Network', 'Profile picture from social network'), purpose: l('Personalisierung', 'Personalization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Import', 'Voluntary import'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange gewuenscht', 'While desired'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Lokale Kopie'], tags: ['social', 'avatar'] },
{ id: 'dp-l3-connections', code: 'L3', category: 'SOCIAL_MEDIA', name: l('Social-Verbindungen', 'Social Connections'), description: l('Freunde/Follower', 'Friends/followers'), purpose: l('Social Features', 'Social features'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Import', 'Voluntary import'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Social-Network'], technicalMeasures: ['Minimale Daten'], tags: ['social', 'connections'] },
{ id: 'dp-l4-shares', code: 'L4', category: 'SOCIAL_MEDIA', name: l('Geteilte Inhalte', 'Shared Content'), description: l('Auf Social Media geteilte Inhalte', 'Content shared on social media'), purpose: l('Social Sharing', 'Social sharing'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliges Teilen', 'Voluntary sharing'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Social-Networks'], technicalMeasures: ['Logging'], tags: ['social', 'sharing'] },
{ id: 'dp-l5-likes', code: 'L5', category: 'SOCIAL_MEDIA', name: l('Likes/Reaktionen', 'Likes/Reactions'), description: l('Social-Media-Interaktionen', 'Social media interactions'), purpose: l('Social Features', 'Social features'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Teil des Services', 'Part of service'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Nutzerfunktion', 'User feature'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['social', 'interactions'] },
{ id: 'dp-l6-oauth-tokens', code: 'L6', category: 'SOCIAL_MEDIA', name: l('OAuth-Tokens', 'OAuth Tokens'), description: l('Zugangs-Token fuer Social APIs', 'Access tokens for social APIs'), purpose: l('API-Zugriff', 'API access'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Verknuepfung', 'Voluntary linking'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Social-Network'], technicalMeasures: ['Verschluesselung', 'Token-Rotation'], tags: ['social', 'oauth'] },
// KATEGORIE M: GESUNDHEITSDATEN (7) - ART. 9 DSGVO!
{ id: 'dp-m1-health-status', code: 'M1', category: 'HEALTH_DATA', name: l('Gesundheitszustand', 'Health Status'), description: l('Allgemeiner Gesundheitszustand', 'General health status'), purpose: l('Gesundheitsdienste', 'Health services'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO - Ausdrueckliche Einwilligung', 'Art. 9(2)(a) GDPR - Explicit consent'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Aufbewahrung', 'Medical retention'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung', 'Zugriffskontrolle', 'Audit-Logging'], tags: ['health', 'article9', 'sensitive'] },
{ id: 'dp-m2-fitness-data', code: 'M2', category: 'HEALTH_DATA', name: l('Fitnessdaten', 'Fitness Data'), description: l('Schritte, Kalorien, Aktivitaet', 'Steps, calories, activity'), purpose: l('Fitness-Tracking', 'Fitness tracking'), riskLevel: 'MEDIUM', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeit-Tracking', 'Long-term tracking'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: ['Fitness-App'], technicalMeasures: ['Verschluesselung', 'Pseudonymisierung'], tags: ['health', 'fitness', 'article9'] },
{ id: 'dp-m3-medication', code: 'M3', category: 'HEALTH_DATA', name: l('Medikation', 'Medication'), description: l('Aktuelle Medikamente', 'Current medications'), purpose: l('Gesundheitsmanagement', 'Health management'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Dokumentation', 'Medical documentation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung', 'Strenge Zugriffskontrolle'], tags: ['health', 'medication', 'article9'] },
{ id: 'dp-m4-biometric', code: 'M4', category: 'HEALTH_DATA', name: l('Biometrische Daten', 'Biometric Data'), description: l('Fingerabdruck, Face-ID (zur Identifikation)', 'Fingerprint, Face ID (for identification)'), purpose: l('Biometrische Authentifizierung', 'Biometric authentication'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange gewuenscht', 'While desired'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['On-Device-Speicherung', 'Keine Cloud-Uebertragung'], tags: ['health', 'biometric', 'article9'] },
{ id: 'dp-m5-allergies', code: 'M5', category: 'HEALTH_DATA', name: l('Allergien', 'Allergies'), description: l('Bekannte Allergien', 'Known allergies'), purpose: l('Gesundheitsschutz', 'Health protection'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Dokumentation', 'Medical documentation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung'], tags: ['health', 'allergies', 'article9'] },
{ id: 'dp-m6-vital-signs', code: 'M6', category: 'HEALTH_DATA', name: l('Vitalzeichen', 'Vital Signs'), description: l('Blutdruck, Puls, etc.', 'Blood pressure, pulse, etc.'), purpose: l('Gesundheitsmonitoring', 'Health monitoring'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Dokumentation', 'Medical documentation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung', 'Audit-Logging'], tags: ['health', 'vitals', 'article9'] },
{ id: 'dp-m7-disability', code: 'M7', category: 'HEALTH_DATA', name: l('Behinderung/Einschraenkung', 'Disability/Impairment'), description: l('Informationen zu Behinderungen', 'Information about disabilities'), purpose: l('Barrierefreiheit', 'Accessibility'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Strenge Zugriffskontrolle'], tags: ['health', 'disability', 'article9'] },
// KATEGORIE N: BESCHAEFTIGTENDATEN (10) - BDSG § 26
{ id: 'dp-n1-employee-id', code: 'N1', category: 'EMPLOYEE_DATA', name: l('Personalnummer', 'Employee ID'), description: l('Eindeutige Mitarbeiter-ID', 'Unique employee ID'), purpose: l('Personalverwaltung', 'HR management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26 - Beschaeftigungsverhaeltnis', 'BDSG § 26 - Employment relationship'), retentionPeriod: '10_YEARS', retentionJustification: l('Aufbewahrungspflichten', 'Retention obligations'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'hr'] },
{ id: 'dp-n2-salary', code: 'N2', category: 'EMPLOYEE_DATA', name: l('Gehalt/Verguetung', 'Salary/Compensation'), description: l('Gehaltsinformationen', 'Salary information'), purpose: l('Lohnabrechnung', 'Payroll'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Lohnbuero', 'Finanzamt'], technicalMeasures: ['Verschluesselung', 'Strenge Zugriffskontrolle'], tags: ['employee', 'payroll'] },
{ id: 'dp-n3-tax-id', code: 'N3', category: 'EMPLOYEE_DATA', name: l('Steuer-ID', 'Tax ID'), description: l('Steueridentifikationsnummer', 'Tax identification number'), purpose: l('Lohnsteuer', 'Payroll tax'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Steuerrecht', 'Tax law'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Finanzamt'], technicalMeasures: ['Verschluesselung'], tags: ['employee', 'tax'] },
{ id: 'dp-n4-social-security', code: 'N4', category: 'EMPLOYEE_DATA', name: l('Sozialversicherungsnummer', 'Social Security Number'), description: l('SV-Nummer', 'Social security number'), purpose: l('Sozialversicherung', 'Social security'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Sozialversicherungsrecht', 'Social security law'), retentionPeriod: '10_YEARS', retentionJustification: l('Gesetzliche Pflicht', 'Legal obligation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Krankenkasse', 'Rentenversicherung'], technicalMeasures: ['Verschluesselung'], tags: ['employee', 'social-security'] },
{ id: 'dp-n5-working-hours', code: 'N5', category: 'EMPLOYEE_DATA', name: l('Arbeitszeiten', 'Working Hours'), description: l('Erfasste Arbeitszeiten', 'Recorded working hours'), purpose: l('Arbeitszeiterfassung', 'Time tracking'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('ArbZG', 'Working Time Act'), retentionPeriod: '6_YEARS', retentionJustification: l('Gesetzliche Aufbewahrung', 'Legal retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'time-tracking'] },
{ id: 'dp-n6-vacation', code: 'N6', category: 'EMPLOYEE_DATA', name: l('Urlaubsdaten', 'Vacation Data'), description: l('Urlaubsanspruch und -nutzung', 'Vacation entitlement and usage'), purpose: l('Urlaubsverwaltung', 'Vacation management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'vacation'] },
{ id: 'dp-n7-sick-leave', code: 'N7', category: 'EMPLOYEE_DATA', name: l('Krankheitstage', 'Sick Leave'), description: l('Krankheitstage (ohne Diagnose)', 'Sick days (without diagnosis)'), purpose: l('Personalplanung', 'HR planning'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Lohnfortzahlung', 'Sick pay'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Krankenkasse'], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'sick-leave'] },
{ id: 'dp-n8-performance', code: 'N8', category: 'EMPLOYEE_DATA', name: l('Leistungsbeurteilung', 'Performance Review'), description: l('Mitarbeiterbeurteilungen', 'Employee evaluations'), purpose: l('Personalentwicklung', 'HR development'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Personalakte', 'Personnel file'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'performance'] },
{ id: 'dp-n9-training', code: 'N9', category: 'EMPLOYEE_DATA', name: l('Schulungen/Weiterbildung', 'Training/Development'), description: l('Absolvierte Schulungen', 'Completed training'), purpose: l('Personalentwicklung', 'HR development'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Personalakte', 'Personnel file'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'training'] },
{ id: 'dp-n10-contract', code: 'N10', category: 'EMPLOYEE_DATA', name: l('Arbeitsvertrag', 'Employment Contract'), description: l('Arbeitsvertragsdaten', 'Employment contract data'), purpose: l('Vertragsverwaltung', 'Contract management'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '10_YEARS', retentionJustification: l('Verjaehrungsfristen', 'Limitation periods'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Archivierung'], tags: ['employee', 'contract'] },
// KATEGORIE O: VERTRAGSDATEN (7)
{ id: 'dp-o1-contract-number', code: 'O1', category: 'CONTRACT_DATA', name: l('Vertragsnummer', 'Contract Number'), description: l('Eindeutige Vertragsnummer', 'Unique contract number'), purpose: l('Vertragsverwaltung', 'Contract management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contract', 'id'] },
{ id: 'dp-o2-contract-duration', code: 'O2', category: 'CONTRACT_DATA', name: l('Vertragslaufzeit', 'Contract Duration'), description: l('Start- und Enddatum', 'Start and end date'), purpose: l('Vertragsverwaltung', 'Contract management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['contract', 'duration'] },
{ id: 'dp-o3-signature', code: 'O3', category: 'CONTRACT_DATA', name: l('Unterschrift', 'Signature'), description: l('Digitale oder gescannte Unterschrift', 'Digital or scanned signature'), purpose: l('Vertragsschluss', 'Contract conclusion'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('Beweissicherung', 'Evidence preservation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Integritaetsschutz'], tags: ['contract', 'signature'] },
{ id: 'dp-o4-contract-documents', code: 'O4', category: 'CONTRACT_DATA', name: l('Vertragsdokumente', 'Contract Documents'), description: l('PDFs und Anlagen', 'PDFs and attachments'), purpose: l('Dokumentation', 'Documentation'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['contract', 'documents'] },
{ id: 'dp-o5-contract-terms', code: 'O5', category: 'CONTRACT_DATA', name: l('Vertragskonditionen', 'Contract Terms'), description: l('Preise, Rabatte, Bedingungen', 'Prices, discounts, conditions'), purpose: l('Vertragsabwicklung', 'Contract processing'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contract', 'terms'] },
{ id: 'dp-o6-contract-history', code: 'O6', category: 'CONTRACT_DATA', name: l('Vertragshistorie', 'Contract History'), description: l('Aenderungen und Versionen', 'Changes and versions'), purpose: l('Nachvollziehbarkeit', 'Traceability'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Dokumentationspflicht', 'Documentation obligation'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Versionierung'], tags: ['contract', 'history'] },
{ id: 'dp-o7-termination', code: 'O7', category: 'CONTRACT_DATA', name: l('Kuendigungsdaten', 'Termination Data'), description: l('Kuendigungen und Gruende', 'Terminations and reasons'), purpose: l('Vertragsbeendigung', 'Contract termination'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('Beweissicherung', 'Evidence preservation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Archivierung'], tags: ['contract', 'termination'] },
// KATEGORIE P: PROTOKOLLDATEN (7)
{ id: 'dp-p1-login-logs', code: 'P1', category: 'LOG_DATA', name: l('Login-Protokolle', 'Login Logs'), description: l('Erfolgreiche und fehlgeschlagene Logins', 'Successful and failed logins'), purpose: l('Sicherheitsaudit', 'Security audit'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Sicherheitsforensik', 'Security forensics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SIEM'], technicalMeasures: ['Unveraenderbare Logs'], tags: ['logs', 'security'] },
{ id: 'dp-p2-access-logs', code: 'P2', category: 'LOG_DATA', name: l('Zugriffsprotokolle', 'Access Logs'), description: l('HTTP-Zugriffe', 'HTTP accesses'), purpose: l('Sicherheit, Debugging', 'Security, debugging'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['IP-Anonymisierung'], tags: ['logs', 'access'] },
{ id: 'dp-p3-api-logs', code: 'P3', category: 'LOG_DATA', name: l('API-Protokolle', 'API Logs'), description: l('API-Aufrufe', 'API calls'), purpose: l('Debugging, Monitoring', 'Debugging, monitoring'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicequalitaet', 'Service quality'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Pseudonymisierung'], tags: ['logs', 'api'] },
{ id: 'dp-p4-admin-logs', code: 'P4', category: 'LOG_DATA', name: l('Admin-Aktionen', 'Admin Actions'), description: l('Protokoll von Admin-Aktivitaeten', 'Log of admin activities'), purpose: l('Revisionssicherheit', 'Audit compliance'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Dokumentationspflicht', 'Documentation obligation'), retentionPeriod: '6_YEARS', retentionJustification: l('Revisionssicherheit', 'Audit compliance'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Unveraenderbare Logs', 'Signatur'], tags: ['logs', 'admin'] },
{ id: 'dp-p5-change-logs', code: 'P5', category: 'LOG_DATA', name: l('Aenderungshistorie', 'Change History'), description: l('Audit-Trail von Aenderungen', 'Audit trail of changes'), purpose: l('Nachvollziehbarkeit', 'Traceability'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Dokumentationspflicht', 'Documentation obligation'), retentionPeriod: '6_YEARS', retentionJustification: l('Revisionssicherheit', 'Audit compliance'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Unveraenderbare Logs'], tags: ['logs', 'audit'] },
{ id: 'dp-p6-error-logs', code: 'P6', category: 'LOG_DATA', name: l('Fehlerprotokolle', 'Error Logs'), description: l('System- und Anwendungsfehler', 'System and application errors'), purpose: l('Fehlerbehebung', 'Bug fixing'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicequalitaet', 'Service quality'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Error-Tracking'], technicalMeasures: ['PII-Filterung'], tags: ['logs', 'errors'] },
{ id: 'dp-p7-security-logs', code: 'P7', category: 'LOG_DATA', name: l('Sicherheitsprotokolle', 'Security Logs'), description: l('Security Events', 'Security events'), purpose: l('Sicherheitsmonitoring', 'Security monitoring'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Forensik', 'Forensics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SIEM'], technicalMeasures: ['Unveraenderbare Logs'], tags: ['logs', 'security'] },
// KATEGORIE Q: KI-DATEN (7) - AI ACT
{ id: 'dp-q1-ai-prompts', code: 'Q1', category: 'AI_DATA', name: l('KI-Prompts', 'AI Prompts'), description: l('Nutzereingaben an KI', 'User inputs to AI'), purpose: l('KI-Funktionalitaet', 'AI functionality'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer KI-Service', 'For AI service'), retentionPeriod: '90_DAYS', retentionJustification: l('Kontexterhaltung', 'Context preservation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['KI-Provider'], technicalMeasures: ['Keine Verwendung fuer Training', 'Verschluesselung'], tags: ['ai', 'prompts'] },
{ id: 'dp-q2-ai-responses', code: 'Q2', category: 'AI_DATA', name: l('KI-Antworten', 'AI Responses'), description: l('Generierte KI-Antworten', 'Generated AI responses'), purpose: l('Qualitaetssicherung', 'Quality assurance'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer KI-Service', 'For AI service'), retentionPeriod: '90_DAYS', retentionJustification: l('QA', 'QA'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Logging'], tags: ['ai', 'responses'] },
{ id: 'dp-q3-rag-context', code: 'Q3', category: 'AI_DATA', name: l('RAG-Kontext', 'RAG Context'), description: l('Retrieval-Kontext', 'Retrieval context'), purpose: l('Kontextuelle KI-Antworten', 'Contextual AI responses'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer RAG-Funktionalitaet', 'For RAG functionality'), retentionPeriod: '24_HOURS', retentionJustification: l('Session-Kontext', 'Session context'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['In-Memory', 'Auto-Loeschung'], tags: ['ai', 'rag'] },
{ id: 'dp-q4-ai-feedback', code: 'Q4', category: 'AI_DATA', name: l('KI-Feedback', 'AI Feedback'), description: l('Nutzerfeedback zu KI-Antworten', 'User feedback on AI responses'), purpose: l('KI-Verbesserung', 'AI improvement'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliges Feedback', 'Voluntary feedback'), retentionPeriod: '24_MONTHS', retentionJustification: l('Qualitaetsanalyse', 'Quality analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Anonymisierung'], tags: ['ai', 'feedback'] },
{ id: 'dp-q5-training-data', code: 'Q5', category: 'AI_DATA', name: l('Trainingsdaten (mit Einwilligung)', 'Training Data (with consent)'), description: l('Fuer KI-Training freigegebene Daten', 'Data released for AI training'), purpose: l('Modellverbesserung', 'Model improvement'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung', 'Explicit consent'), retentionPeriod: '36_MONTHS', retentionJustification: l('Modellentwicklung', 'Model development'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Anonymisierung', 'Zugriffskontrolle'], tags: ['ai', 'training'] },
{ id: 'dp-q6-model-outputs', code: 'Q6', category: 'AI_DATA', name: l('Modell-Outputs', 'Model Outputs'), description: l('KI-generierte Inhalte', 'AI-generated content'), purpose: l('Dokumentation', 'Documentation'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Teil des Services', 'Part of service'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Kennzeichnung als KI-generiert'], tags: ['ai', 'outputs'] },
{ id: 'dp-q7-ai-usage', code: 'Q7', category: 'AI_DATA', name: l('KI-Nutzungsstatistik', 'AI Usage Statistics'), description: l('Aggregierte KI-Nutzungsdaten', 'Aggregated AI usage data'), purpose: l('Kapazitaetsplanung', 'Capacity planning'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Serviceoptimierung', 'Service optimization'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['ai', 'usage'] },
// KATEGORIE R: SICHERHEITSDATEN (7)
{ id: 'dp-r1-failed-logins', code: 'R1', category: 'SECURITY', name: l('Fehlgeschlagene Logins', 'Failed Logins'), description: l('Fehlgeschlagene Anmeldeversuche', 'Failed login attempts'), purpose: l('Angriffserkennung', 'Attack detection'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Forensik', 'Forensics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SIEM'], technicalMeasures: ['Alerting', 'Rate-Limiting'], tags: ['security', 'auth'] },
{ id: 'dp-r2-fraud-score', code: 'R2', category: 'SECURITY', name: l('Betrugsrisiko-Score', 'Fraud Risk Score'), description: l('Berechnetes Betrugsrisiko', 'Calculated fraud risk'), purpose: l('Betrugspraevention', 'Fraud prevention'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Betrugspraevention', 'Fraud prevention'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analyse', 'Analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Maschinelles Lernen'], tags: ['security', 'fraud'] },
{ id: 'dp-r3-incident-reports', code: 'R3', category: 'SECURITY', name: l('Vorfallberichte', 'Incident Reports'), description: l('Sicherheitsvorfaelle', 'Security incidents'), purpose: l('Vorfallmanagement', 'Incident management'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Art. 33 DSGVO Meldepflicht', 'Art. 33 GDPR notification obligation'), retentionPeriod: '6_YEARS', retentionJustification: l('Dokumentationspflicht', 'Documentation obligation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Aufsichtsbehoerde'], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['security', 'incidents'] },
{ id: 'dp-r4-threat-intel', code: 'R4', category: 'SECURITY', name: l('Bedrohungsinformationen', 'Threat Intelligence'), description: l('Erkannte Bedrohungen', 'Detected threats'), purpose: l('Sicherheitsmonitoring', 'Security monitoring'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analyse', 'Analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['security', 'threats'] },
{ id: 'dp-r5-blocked-ips', code: 'R5', category: 'SECURITY', name: l('Gesperrte IPs', 'Blocked IPs'), description: l('Blacklist von IP-Adressen', 'IP address blacklist'), purpose: l('Angriffspraevention', 'Attack prevention'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Sicherheit', 'Security'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Automatische Bereinigung'], tags: ['security', 'blocking'] },
{ id: 'dp-r6-vulnerability-scans', code: 'R6', category: 'SECURITY', name: l('Schwachstellen-Scans', 'Vulnerability Scans'), description: l('Ergebnisse von Security-Scans', 'Results of security scans'), purpose: l('Schwachstellenmanagement', 'Vulnerability management'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Trend-Analyse', 'Trend analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['security', 'vulnerabilities'] },
{ id: 'dp-r7-penetration-tests', code: 'R7', category: 'SECURITY', name: l('Penetrationstests', 'Penetration Tests'), description: l('Ergebnisse von Pentests', 'Results of pentests'), purpose: l('Sicherheitspruefung', 'Security testing'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '6_YEARS', retentionJustification: l('Compliance-Nachweis', 'Compliance evidence'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Strenge Zugriffskontrolle'], tags: ['security', 'pentests'] },
]
// =============================================================================
// RETENTION MATRIX (18 Kategorien)
// =============================================================================
export const RETENTION_MATRIX: RetentionMatrixEntry[] = [
{ category: 'MASTER_DATA', categoryName: l('Stammdaten', 'Master Data'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO', exceptions: [] },
{ category: 'CONTACT_DATA', categoryName: l('Kontaktdaten', 'Contact Data'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO', exceptions: [] },
{ category: 'AUTHENTICATION', categoryName: l('Authentifizierung', 'Authentication'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO', exceptions: [{ condition: l('Session-Token', 'Session token'), period: '24_HOURS', reason: l('Sicherheit', 'Security') }] },
{ category: 'CONSENT', categoryName: l('Einwilligungsdaten', 'Consent Data'), standardPeriod: '6_YEARS', legalBasis: 'Art. 7 DSGVO, § 147 AO', exceptions: [] },
{ category: 'COMMUNICATION', categoryName: l('Kommunikation', 'Communication'), standardPeriod: '24_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. b/f DSGVO', exceptions: [] },
{ category: 'PAYMENT', categoryName: l('Zahlungsdaten', 'Payment Data'), standardPeriod: '10_YEARS', legalBasis: '§ 147 AO, § 257 HGB', exceptions: [] },
{ category: 'USAGE_DATA', categoryName: l('Nutzungsdaten', 'Usage Data'), standardPeriod: '24_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
{ category: 'LOCATION', categoryName: l('Standortdaten', 'Location Data'), standardPeriod: '90_DAYS', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
{ category: 'DEVICE_DATA', categoryName: l('Geraetedaten', 'Device Data'), standardPeriod: '12_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
{ category: 'MARKETING', categoryName: l('Marketingdaten', 'Marketing Data'), standardPeriod: '90_DAYS', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
{ category: 'ANALYTICS', categoryName: l('Analysedaten', 'Analytics Data'), standardPeriod: '26_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
{ category: 'SOCIAL_MEDIA', categoryName: l('Social-Media-Daten', 'Social Media Data'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
{ category: 'HEALTH_DATA', categoryName: l('Gesundheitsdaten', 'Health Data'), standardPeriod: '10_YEARS', legalBasis: 'Art. 9 Abs. 2 lit. a DSGVO', exceptions: [] },
{ category: 'EMPLOYEE_DATA', categoryName: l('Beschaeftigtendaten', 'Employee Data'), standardPeriod: '10_YEARS', legalBasis: 'BDSG § 26', exceptions: [] },
{ category: 'CONTRACT_DATA', categoryName: l('Vertragsdaten', 'Contract Data'), standardPeriod: '10_YEARS', legalBasis: '§ 147 AO, § 257 HGB', exceptions: [] },
{ category: 'LOG_DATA', categoryName: l('Protokolldaten', 'Log Data'), standardPeriod: '12_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
{ category: 'AI_DATA', categoryName: l('KI-Daten', 'AI Data'), standardPeriod: '90_DAYS', legalBasis: 'Art. 6 Abs. 1 lit. a/b DSGVO', exceptions: [] },
{ category: 'SECURITY', categoryName: l('Sicherheitsdaten', 'Security Data'), standardPeriod: '12_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
]
// =============================================================================
// COOKIE CATEGORIES
// =============================================================================
export const DEFAULT_COOKIE_CATEGORIES: CookieBannerCategory[] = [
{ id: 'ESSENTIAL', name: l('Technisch notwendig', 'Essential'), description: l('Diese Cookies sind fuer den Betrieb erforderlich', 'These cookies are required for operation'), isRequired: true, defaultEnabled: true, dataPointIds: ['dp-c2-session-token', 'dp-c3-refresh-token', 'dp-d1-consent-records', 'dp-d2-cookie-preferences'], cookies: [] },
{ id: 'PERFORMANCE', name: l('Analyse & Performance', 'Analytics & Performance'), description: l('Helfen uns die Nutzung zu verstehen', 'Help us understand usage'), isRequired: false, defaultEnabled: false, dataPointIds: ['dp-g1-session-duration', 'dp-g2-page-views'], cookies: [] },
{ id: 'PERSONALIZATION', name: l('Personalisierung', 'Personalization'), description: l('Ermoeglichen personalisierte Werbung', 'Enable personalized advertising'), isRequired: false, defaultEnabled: false, dataPointIds: [], cookies: [] },
{ id: 'EXTERNAL_MEDIA', name: l('Externe Medien', 'External Media'), description: l('Erlauben Einbindung externer Medien', 'Allow embedding external media'), isRequired: false, defaultEnabled: false, dataPointIds: [], cookies: [] },
]
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
export function getDataPointById(id: string): DataPoint | undefined {
return PREDEFINED_DATA_POINTS.find((dp) => dp.id === id)
}
export function getDataPointByCode(code: string): DataPoint | undefined {
return PREDEFINED_DATA_POINTS.find((dp) => dp.code === code)
}
export function getDataPointsByCategory(category: DataPointCategory): DataPoint[] {
return PREDEFINED_DATA_POINTS.filter((dp) => dp.category === category)
}
export function getDataPointsByLegalBasis(legalBasis: string): DataPoint[] {
return PREDEFINED_DATA_POINTS.filter((dp) => dp.legalBasis === legalBasis)
}
export function getDataPointsByCookieCategory(cookieCategory: string): DataPoint[] {
return PREDEFINED_DATA_POINTS.filter((dp) => dp.cookieCategory === cookieCategory)
}
export function getDataPointsRequiringConsent(): DataPoint[] {
return PREDEFINED_DATA_POINTS.filter((dp) => dp.requiresExplicitConsent)
}
export function getHighRiskDataPoints(): DataPoint[] {
return PREDEFINED_DATA_POINTS.filter((dp) => dp.riskLevel === 'HIGH')
}
export function getSpecialCategoryDataPoints(): DataPoint[] {
return PREDEFINED_DATA_POINTS.filter((dp) => dp.isSpecialCategory)
}
export function countDataPointsByCategory(): Record<DataPointCategory, number> {
const counts = {} as Record<DataPointCategory, number>
for (const dp of PREDEFINED_DATA_POINTS) {
counts[dp.category] = (counts[dp.category] || 0) + 1
}
return counts
}
export function countDataPointsByRiskLevel(): Record<'LOW' | 'MEDIUM' | 'HIGH', number> {
const counts = { LOW: 0, MEDIUM: 0, HIGH: 0 }
for (const dp of PREDEFINED_DATA_POINTS) {
counts[dp.riskLevel]++
}
return counts
}
export function createDefaultCatalog(tenantId: string): DataPointCatalog {
return {
id: `catalog-${tenantId}`,
tenantId,
version: '2.0.0',
dataPoints: PREDEFINED_DATA_POINTS.map((dp) => ({ ...dp, isActive: true })),
customDataPoints: [],
retentionMatrix: RETENTION_MATRIX,
createdAt: new Date(),
updatedAt: new Date(),
}
}
export function searchDataPoints(dataPoints: DataPoint[], query: string, language: 'de' | 'en' = 'de'): DataPoint[] {
const lowerQuery = query.toLowerCase()
return dataPoints.filter(
(dp) =>
dp.code.toLowerCase().includes(lowerQuery) ||
dp.name[language].toLowerCase().includes(lowerQuery) ||
dp.description[language].toLowerCase().includes(lowerQuery) ||
dp.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
)
}
@@ -0,0 +1,669 @@
'use client'
/**
* Einwilligungen Context & Reducer
*
* Zentrale State-Verwaltung fuer das Datenpunktkatalog & DSI-Generator Modul.
* Verwendet React Context + useReducer fuer vorhersehbare State-Updates.
*/
import {
createContext,
useContext,
useReducer,
useCallback,
useMemo,
ReactNode,
Dispatch,
} from 'react'
import {
EinwilligungenState,
EinwilligungenAction,
EinwilligungenTab,
DataPoint,
DataPointCatalog,
GeneratedPrivacyPolicy,
CookieBannerConfig,
CompanyInfo,
ConsentStatistics,
PrivacyPolicySection,
SupportedLanguage,
ExportFormat,
DataPointCategory,
LegalBasis,
RiskLevel,
} from './types'
import {
PREDEFINED_DATA_POINTS,
RETENTION_MATRIX,
DEFAULT_COOKIE_CATEGORIES,
createDefaultCatalog,
getDataPointById,
getDataPointsByCategory,
countDataPointsByCategory,
countDataPointsByRiskLevel,
} from './catalog/loader'
// =============================================================================
// INITIAL STATE
// =============================================================================
const initialState: EinwilligungenState = {
// Data
catalog: null,
selectedDataPoints: [],
privacyPolicy: null,
cookieBannerConfig: null,
companyInfo: null,
consentStatistics: null,
// UI State
activeTab: 'catalog',
isLoading: false,
isSaving: false,
error: null,
// Editor State
editingDataPoint: null,
editingSection: null,
// Preview
previewLanguage: 'de',
previewFormat: 'HTML',
}
// =============================================================================
// REDUCER
// =============================================================================
function einwilligungenReducer(
state: EinwilligungenState,
action: EinwilligungenAction
): EinwilligungenState {
switch (action.type) {
case 'SET_CATALOG':
return {
...state,
catalog: action.payload,
// Automatisch alle aktiven Datenpunkte auswaehlen
selectedDataPoints: [
...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
],
}
case 'SET_SELECTED_DATA_POINTS':
return {
...state,
selectedDataPoints: action.payload,
}
case 'TOGGLE_DATA_POINT': {
const id = action.payload
const isSelected = state.selectedDataPoints.includes(id)
return {
...state,
selectedDataPoints: isSelected
? state.selectedDataPoints.filter((dpId) => dpId !== id)
: [...state.selectedDataPoints, id],
}
}
case 'ADD_CUSTOM_DATA_POINT':
if (!state.catalog) return state
return {
...state,
catalog: {
...state.catalog,
customDataPoints: [...state.catalog.customDataPoints, action.payload],
updatedAt: new Date(),
},
selectedDataPoints: [...state.selectedDataPoints, action.payload.id],
}
case 'UPDATE_DATA_POINT': {
if (!state.catalog) return state
const { id, data } = action.payload
// Pruefe ob es ein vordefinierter oder kundenspezifischer Datenpunkt ist
const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id)
if (isCustom) {
return {
...state,
catalog: {
...state.catalog,
customDataPoints: state.catalog.customDataPoints.map((dp) =>
dp.id === id ? { ...dp, ...data } : dp
),
updatedAt: new Date(),
},
}
} else {
// Vordefinierte Datenpunkte: nur isActive aendern
return {
...state,
catalog: {
...state.catalog,
dataPoints: state.catalog.dataPoints.map((dp) =>
dp.id === id ? { ...dp, ...data } : dp
),
updatedAt: new Date(),
},
}
}
}
case 'DELETE_CUSTOM_DATA_POINT':
if (!state.catalog) return state
return {
...state,
catalog: {
...state.catalog,
customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload),
updatedAt: new Date(),
},
selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload),
}
case 'SET_PRIVACY_POLICY':
return {
...state,
privacyPolicy: action.payload,
}
case 'SET_COOKIE_BANNER_CONFIG':
return {
...state,
cookieBannerConfig: action.payload,
}
case 'UPDATE_COOKIE_BANNER_STYLING':
if (!state.cookieBannerConfig) return state
return {
...state,
cookieBannerConfig: {
...state.cookieBannerConfig,
styling: {
...state.cookieBannerConfig.styling,
...action.payload,
},
updatedAt: new Date(),
},
}
case 'UPDATE_COOKIE_BANNER_TEXTS':
if (!state.cookieBannerConfig) return state
return {
...state,
cookieBannerConfig: {
...state.cookieBannerConfig,
texts: {
...state.cookieBannerConfig.texts,
...action.payload,
},
updatedAt: new Date(),
},
}
case 'SET_COMPANY_INFO':
return {
...state,
companyInfo: action.payload,
}
case 'SET_CONSENT_STATISTICS':
return {
...state,
consentStatistics: action.payload,
}
case 'SET_ACTIVE_TAB':
return {
...state,
activeTab: action.payload,
}
case 'SET_LOADING':
return {
...state,
isLoading: action.payload,
}
case 'SET_SAVING':
return {
...state,
isSaving: action.payload,
}
case 'SET_ERROR':
return {
...state,
error: action.payload,
}
case 'SET_EDITING_DATA_POINT':
return {
...state,
editingDataPoint: action.payload,
}
case 'SET_EDITING_SECTION':
return {
...state,
editingSection: action.payload,
}
case 'SET_PREVIEW_LANGUAGE':
return {
...state,
previewLanguage: action.payload,
}
case 'SET_PREVIEW_FORMAT':
return {
...state,
previewFormat: action.payload,
}
case 'RESET_STATE':
return initialState
default:
return state
}
}
// =============================================================================
// CONTEXT
// =============================================================================
interface EinwilligungenContextValue {
state: EinwilligungenState
dispatch: Dispatch<EinwilligungenAction>
// Computed Values
allDataPoints: DataPoint[]
selectedDataPointsData: DataPoint[]
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
categoryStats: Record<DataPointCategory, number>
riskStats: Record<RiskLevel, number>
legalBasisStats: Record<LegalBasis, number>
// Actions
initializeCatalog: (tenantId: string) => void
loadCatalog: (tenantId: string) => Promise<void>
saveCatalog: () => Promise<void>
toggleDataPoint: (id: string) => void
addCustomDataPoint: (dataPoint: DataPoint) => void
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
deleteCustomDataPoint: (id: string) => void
setActiveTab: (tab: EinwilligungenTab) => void
setPreviewLanguage: (language: SupportedLanguage) => void
setPreviewFormat: (format: ExportFormat) => void
setCompanyInfo: (info: CompanyInfo) => void
generatePrivacyPolicy: () => Promise<void>
generateCookieBannerConfig: () => void
}
const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
// =============================================================================
// PROVIDER
// =============================================================================
interface EinwilligungenProviderProps {
children: ReactNode
tenantId?: string
}
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
// ---------------------------------------------------------------------------
// COMPUTED VALUES
// ---------------------------------------------------------------------------
const allDataPoints = useMemo(() => {
if (!state.catalog) return PREDEFINED_DATA_POINTS
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
}, [state.catalog])
const selectedDataPointsData = useMemo(() => {
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
}, [allDataPoints, state.selectedDataPoints])
const dataPointsByCategory = useMemo(() => {
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
// 18 Kategorien (A-R)
const categories: DataPointCategory[] = [
'MASTER_DATA', // A
'CONTACT_DATA', // B
'AUTHENTICATION', // C
'CONSENT', // D
'COMMUNICATION', // E
'PAYMENT', // F
'USAGE_DATA', // G
'LOCATION', // H
'DEVICE_DATA', // I
'MARKETING', // J
'ANALYTICS', // K
'SOCIAL_MEDIA', // L
'HEALTH_DATA', // M - Art. 9 DSGVO
'EMPLOYEE_DATA', // N - BDSG § 26
'CONTRACT_DATA', // O
'LOG_DATA', // P
'AI_DATA', // Q - AI Act
'SECURITY', // R
]
for (const cat of categories) {
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
}
return result as Record<DataPointCategory, DataPoint[]>
}, [selectedDataPointsData])
const categoryStats = useMemo(() => {
const counts: Partial<Record<DataPointCategory, number>> = {}
for (const dp of selectedDataPointsData) {
counts[dp.category] = (counts[dp.category] || 0) + 1
}
return counts as Record<DataPointCategory, number>
}, [selectedDataPointsData])
const riskStats = useMemo(() => {
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
for (const dp of selectedDataPointsData) {
counts[dp.riskLevel]++
}
return counts
}, [selectedDataPointsData])
const legalBasisStats = useMemo(() => {
// Alle 7 Rechtsgrundlagen
const counts: Record<LegalBasis, number> = {
CONTRACT: 0,
CONSENT: 0,
EXPLICIT_CONSENT: 0,
LEGITIMATE_INTEREST: 0,
LEGAL_OBLIGATION: 0,
VITAL_INTERESTS: 0,
PUBLIC_INTEREST: 0,
}
for (const dp of selectedDataPointsData) {
counts[dp.legalBasis]++
}
return counts
}, [selectedDataPointsData])
// ---------------------------------------------------------------------------
// ACTIONS
// ---------------------------------------------------------------------------
const initializeCatalog = useCallback(
(tid: string) => {
const catalog = createDefaultCatalog(tid)
dispatch({ type: 'SET_CATALOG', payload: catalog })
},
[dispatch]
)
const loadCatalog = useCallback(
async (tid: string) => {
dispatch({ type: 'SET_LOADING', payload: true })
dispatch({ type: 'SET_ERROR', payload: null })
try {
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
headers: {
'X-Tenant-ID': tid,
},
})
if (response.ok) {
const data = await response.json()
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
if (data.companyInfo) {
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
}
if (data.cookieBannerConfig) {
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
}
} else if (response.status === 404) {
// Katalog existiert noch nicht - erstelle Default
initializeCatalog(tid)
} else {
throw new Error('Failed to load catalog')
}
} catch (error) {
console.error('Error loading catalog:', error)
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
// Fallback zu Default
initializeCatalog(tid)
} finally {
dispatch({ type: 'SET_LOADING', payload: false })
}
},
[dispatch, initializeCatalog]
)
const saveCatalog = useCallback(async () => {
if (!state.catalog) return
dispatch({ type: 'SET_SAVING', payload: true })
dispatch({ type: 'SET_ERROR', payload: null })
try {
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': state.catalog.tenantId,
},
body: JSON.stringify({
catalog: state.catalog,
companyInfo: state.companyInfo,
cookieBannerConfig: state.cookieBannerConfig,
}),
})
if (!response.ok) {
throw new Error('Failed to save catalog')
}
} catch (error) {
console.error('Error saving catalog:', error)
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
} finally {
dispatch({ type: 'SET_SAVING', payload: false })
}
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
const toggleDataPoint = useCallback(
(id: string) => {
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
},
[dispatch]
)
const addCustomDataPoint = useCallback(
(dataPoint: DataPoint) => {
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
},
[dispatch]
)
const updateDataPoint = useCallback(
(id: string, data: Partial<DataPoint>) => {
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
},
[dispatch]
)
const deleteCustomDataPoint = useCallback(
(id: string) => {
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
},
[dispatch]
)
const setActiveTab = useCallback(
(tab: EinwilligungenTab) => {
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
},
[dispatch]
)
const setPreviewLanguage = useCallback(
(language: SupportedLanguage) => {
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
},
[dispatch]
)
const setPreviewFormat = useCallback(
(format: ExportFormat) => {
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
},
[dispatch]
)
const setCompanyInfo = useCallback(
(info: CompanyInfo) => {
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
},
[dispatch]
)
const generatePrivacyPolicy = useCallback(async () => {
if (!state.catalog || !state.companyInfo) {
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
return
}
dispatch({ type: 'SET_LOADING', payload: true })
dispatch({ type: 'SET_ERROR', payload: null })
try {
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': state.catalog.tenantId,
},
body: JSON.stringify({
dataPointIds: state.selectedDataPoints,
companyInfo: state.companyInfo,
language: state.previewLanguage,
format: state.previewFormat,
}),
})
if (response.ok) {
const policy = await response.json()
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
} else {
throw new Error('Failed to generate privacy policy')
}
} catch (error) {
console.error('Error generating privacy policy:', error)
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
} finally {
dispatch({ type: 'SET_LOADING', payload: false })
}
}, [
state.catalog,
state.companyInfo,
state.selectedDataPoints,
state.previewLanguage,
state.previewFormat,
dispatch,
])
const generateCookieBannerConfig = useCallback(() => {
if (!state.catalog) return
const config: CookieBannerConfig = {
id: `cookie-banner-${state.catalog.tenantId}`,
tenantId: state.catalog.tenantId,
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
...cat,
// Filtere nur die ausgewaehlten Datenpunkte
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
})),
styling: {
position: 'BOTTOM',
theme: 'LIGHT',
primaryColor: '#6366f1',
borderRadius: 12,
},
texts: {
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
description: {
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
en: 'We use cookies to provide you with the best possible experience on our website.',
},
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
customize: { de: 'Anpassen', en: 'Customize' },
save: { de: 'Auswahl speichern', en: 'Save Selection' },
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
},
updatedAt: new Date(),
}
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
}, [state.catalog, state.selectedDataPoints, dispatch])
// ---------------------------------------------------------------------------
// CONTEXT VALUE
// ---------------------------------------------------------------------------
const value: EinwilligungenContextValue = {
state,
dispatch,
// Computed Values
allDataPoints,
selectedDataPointsData,
dataPointsByCategory,
categoryStats,
riskStats,
legalBasisStats,
// Actions
initializeCatalog,
loadCatalog,
saveCatalog,
toggleDataPoint,
addCustomDataPoint,
updateDataPoint,
deleteCustomDataPoint,
setActiveTab,
setPreviewLanguage,
setPreviewFormat,
setCompanyInfo,
generatePrivacyPolicy,
generateCookieBannerConfig,
}
return (
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
)
}
// =============================================================================
// HOOK
// =============================================================================
export function useEinwilligungen(): EinwilligungenContextValue {
const context = useContext(EinwilligungenContext)
if (!context) {
throw new Error('useEinwilligungen must be used within EinwilligungenProvider')
}
return context
}
// =============================================================================
// EXPORTS
// =============================================================================
export { initialState, einwilligungenReducer }
@@ -0,0 +1,493 @@
// =============================================================================
// Privacy Policy DOCX Export
// Export Datenschutzerklaerung to Microsoft Word format
// =============================================================================
import {
GeneratedPrivacyPolicy,
PrivacyPolicySection,
CompanyInfo,
SupportedLanguage,
DataPoint,
CATEGORY_METADATA,
RETENTION_PERIOD_INFO,
} from '../types'
// =============================================================================
// TYPES
// =============================================================================
export interface DOCXExportOptions {
language: SupportedLanguage
includeTableOfContents: boolean
includeDataPointList: boolean
companyLogo?: string
primaryColor?: string
}
const DEFAULT_OPTIONS: DOCXExportOptions = {
language: 'de',
includeTableOfContents: true,
includeDataPointList: true,
primaryColor: '#6366f1',
}
// =============================================================================
// DOCX CONTENT STRUCTURE
// =============================================================================
export interface DocxParagraph {
type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet' | 'title'
content: string
style?: Record<string, string>
}
export interface DocxTableRow {
cells: string[]
isHeader?: boolean
}
export interface DocxTable {
type: 'table'
headers: string[]
rows: DocxTableRow[]
}
export type DocxElement = DocxParagraph | DocxTable
// =============================================================================
// DOCX CONTENT GENERATION
// =============================================================================
/**
* Generate DOCX content structure for Privacy Policy
*/
export function generateDOCXContent(
policy: GeneratedPrivacyPolicy,
companyInfo: CompanyInfo,
dataPoints: DataPoint[],
options: Partial<DOCXExportOptions> = {}
): DocxElement[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const elements: DocxElement[] = []
const lang = opts.language
// Title
elements.push({
type: 'title',
content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy',
})
elements.push({
type: 'paragraph',
content: lang === 'de'
? 'gemaess Art. 13, 14 DSGVO'
: 'according to Art. 13, 14 GDPR',
style: { fontStyle: 'italic', textAlign: 'center' },
})
// Company Info
elements.push({
type: 'heading2',
content: lang === 'de' ? 'Verantwortlicher' : 'Controller',
})
elements.push({
type: 'paragraph',
content: companyInfo.name,
style: { fontWeight: 'bold' },
})
elements.push({
type: 'paragraph',
content: `${companyInfo.address}`,
})
elements.push({
type: 'paragraph',
content: `${companyInfo.postalCode} ${companyInfo.city}`,
})
if (companyInfo.country) {
elements.push({
type: 'paragraph',
content: companyInfo.country,
})
}
elements.push({
type: 'paragraph',
content: `${lang === 'de' ? 'E-Mail' : 'Email'}: ${companyInfo.email}`,
})
if (companyInfo.phone) {
elements.push({
type: 'paragraph',
content: `${lang === 'de' ? 'Telefon' : 'Phone'}: ${companyInfo.phone}`,
})
}
if (companyInfo.website) {
elements.push({
type: 'paragraph',
content: `Website: ${companyInfo.website}`,
})
}
// DPO Info
if (companyInfo.dpoName || companyInfo.dpoEmail) {
elements.push({
type: 'heading3',
content: lang === 'de' ? 'Datenschutzbeauftragter' : 'Data Protection Officer',
})
if (companyInfo.dpoName) {
elements.push({
type: 'paragraph',
content: companyInfo.dpoName,
})
}
if (companyInfo.dpoEmail) {
elements.push({
type: 'paragraph',
content: `${lang === 'de' ? 'E-Mail' : 'Email'}: ${companyInfo.dpoEmail}`,
})
}
if (companyInfo.dpoPhone) {
elements.push({
type: 'paragraph',
content: `${lang === 'de' ? 'Telefon' : 'Phone'}: ${companyInfo.dpoPhone}`,
})
}
}
// Document metadata
elements.push({
type: 'paragraph',
content: lang === 'de'
? `Stand: ${new Date(policy.generatedAt).toLocaleDateString('de-DE')}`
: `Date: ${new Date(policy.generatedAt).toLocaleDateString('en-US')}`,
style: { marginTop: '20px' },
})
elements.push({
type: 'paragraph',
content: `Version: ${policy.version}`,
})
// Table of Contents
if (opts.includeTableOfContents) {
elements.push({
type: 'heading2',
content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
})
policy.sections.forEach((section, idx) => {
elements.push({
type: 'bullet',
content: `${idx + 1}. ${section.title[lang]}`,
})
})
if (opts.includeDataPointList) {
elements.push({
type: 'bullet',
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
})
}
}
// Privacy Policy Sections
policy.sections.forEach((section, idx) => {
elements.push({
type: 'heading1',
content: `${idx + 1}. ${section.title[lang]}`,
})
// Parse content
const content = section.content[lang]
const paragraphs = content.split('\n\n')
for (const para of paragraphs) {
if (para.startsWith('- ')) {
// List items
const items = para.split('\n').filter(l => l.startsWith('- '))
for (const item of items) {
elements.push({
type: 'bullet',
content: item.substring(2),
})
}
} else if (para.startsWith('### ')) {
elements.push({
type: 'heading3',
content: para.substring(4),
})
} else if (para.startsWith('## ')) {
elements.push({
type: 'heading2',
content: para.substring(3),
})
} else if (para.trim()) {
elements.push({
type: 'paragraph',
content: para.replace(/\*\*(.*?)\*\*/g, '$1'),
})
}
}
})
// Data Point Catalog Appendix
if (opts.includeDataPointList && dataPoints.length > 0) {
elements.push({
type: 'heading1',
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
})
elements.push({
type: 'paragraph',
content: lang === 'de'
? 'Die folgende Tabelle zeigt alle verarbeiteten personenbezogenen Daten:'
: 'The following table shows all processed personal data:',
})
// Group by category
const categories = [...new Set(dataPoints.map(dp => dp.category))]
for (const category of categories) {
const categoryDPs = dataPoints.filter(dp => dp.category === category)
const categoryMeta = CATEGORY_METADATA[category]
elements.push({
type: 'heading3',
content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`,
})
elements.push({
type: 'table',
headers: lang === 'de'
? ['Code', 'Datenpunkt', 'Rechtsgrundlage', 'Loeschfrist']
: ['Code', 'Data Point', 'Legal Basis', 'Retention'],
rows: categoryDPs.map(dp => ({
cells: [
dp.code,
dp.name[lang],
formatLegalBasis(dp.legalBasis, lang),
RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod,
],
})),
})
}
}
// Footer
elements.push({
type: 'paragraph',
content: lang === 'de'
? `Dieses Dokument wurde automatisch generiert mit dem Datenschutzerklaerung-Generator am ${new Date().toLocaleDateString('de-DE')}.`
: `This document was automatically generated with the Privacy Policy Generator on ${new Date().toLocaleDateString('en-US')}.`,
style: { fontStyle: 'italic', fontSize: '9pt' },
})
return elements
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function formatLegalBasis(basis: string, language: SupportedLanguage): string {
const bases: Record<string, Record<SupportedLanguage, string>> = {
CONTRACT: { de: 'Vertrag (Art. 6 Abs. 1 lit. b)', en: 'Contract (Art. 6(1)(b))' },
CONSENT: { de: 'Einwilligung (Art. 6 Abs. 1 lit. a)', en: 'Consent (Art. 6(1)(a))' },
LEGITIMATE_INTEREST: { de: 'Ber. Interesse (Art. 6 Abs. 1 lit. f)', en: 'Legitimate Interest (Art. 6(1)(f))' },
LEGAL_OBLIGATION: { de: 'Rechtspflicht (Art. 6 Abs. 1 lit. c)', en: 'Legal Obligation (Art. 6(1)(c))' },
}
return bases[basis]?.[language] || basis
}
// =============================================================================
// DOCX BLOB GENERATION
// =============================================================================
/**
* Generate a DOCX file as a Blob
* This generates HTML that Word can open
*/
export async function generateDOCXBlob(
policy: GeneratedPrivacyPolicy,
companyInfo: CompanyInfo,
dataPoints: DataPoint[],
options: Partial<DOCXExportOptions> = {}
): Promise<Blob> {
const content = generateDOCXContent(policy, companyInfo, dataPoints, options)
const opts = { ...DEFAULT_OPTIONS, ...options }
const html = generateHTMLFromContent(content, opts)
return new Blob([html], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
}
function generateHTMLFromContent(
content: DocxElement[],
options: DOCXExportOptions
): string {
let html = `
<!DOCTYPE html>
<html lang="${options.language}">
<head>
<meta charset="utf-8">
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
<style>
body {
font-family: Calibri, Arial, sans-serif;
font-size: 11pt;
line-height: 1.5;
color: #1e293b;
max-width: 800px;
margin: 0 auto;
padding: 40px;
}
h1 {
font-size: 18pt;
color: ${options.primaryColor};
border-bottom: 1px solid ${options.primaryColor};
padding-bottom: 8px;
margin-top: 24pt;
}
h2 {
font-size: 14pt;
color: ${options.primaryColor};
margin-top: 18pt;
}
h3 {
font-size: 12pt;
color: #334155;
margin-top: 14pt;
}
.title {
font-size: 24pt;
color: ${options.primaryColor};
text-align: center;
font-weight: bold;
margin-bottom: 12pt;
}
p {
margin: 8pt 0;
text-align: justify;
}
table {
width: 100%;
border-collapse: collapse;
margin: 12pt 0;
font-size: 10pt;
}
th, td {
border: 1px solid #cbd5e1;
padding: 6pt 10pt;
text-align: left;
}
th {
background-color: ${options.primaryColor};
color: white;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f8fafc;
}
ul {
margin: 8pt 0;
padding-left: 20pt;
}
li {
margin: 4pt 0;
}
</style>
</head>
<body>
`
for (const element of content) {
if (element.type === 'table') {
html += '<table>\n<thead><tr>\n'
for (const header of element.headers) {
html += ` <th>${escapeHtml(header)}</th>\n`
}
html += '</tr></thead>\n<tbody>\n'
for (const row of element.rows) {
html += '<tr>\n'
for (const cell of row.cells) {
html += ` <td>${escapeHtml(cell)}</td>\n`
}
html += '</tr>\n'
}
html += '</tbody></table>\n'
} else {
const tag = getHtmlTag(element.type)
const className = element.type === 'title' ? ' class="title"' : ''
const processedContent = escapeHtml(element.content)
html += `<${tag}${className}>${processedContent}</${tag}>\n`
}
}
html += '</body></html>'
return html
}
function getHtmlTag(type: string): string {
switch (type) {
case 'title':
return 'div'
case 'heading1':
return 'h1'
case 'heading2':
return 'h2'
case 'heading3':
return 'h3'
case 'bullet':
return 'li'
default:
return 'p'
}
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the DOCX export
*/
export function generateDOCXFilename(
companyInfo: CompanyInfo,
language: SupportedLanguage = 'de'
): string {
const companyName = companyInfo.name.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy-Policy'
return `${prefix}-${companyName}-${date}.doc`
}
@@ -0,0 +1,8 @@
/**
* Einwilligungen Export Module
*
* PDF and DOCX export functionality for Privacy Policy documents.
*/
export * from './pdf'
export * from './docx'
@@ -0,0 +1,505 @@
// =============================================================================
// Privacy Policy PDF Export
// Export Datenschutzerklaerung to PDF format
// =============================================================================
import {
GeneratedPrivacyPolicy,
PrivacyPolicySection,
CompanyInfo,
SupportedLanguage,
DataPoint,
CATEGORY_METADATA,
RETENTION_PERIOD_INFO,
} from '../types'
// =============================================================================
// TYPES
// =============================================================================
export interface PDFExportOptions {
language: SupportedLanguage
includeTableOfContents: boolean
includeDataPointList: boolean
companyLogo?: string
primaryColor?: string
pageSize?: 'A4' | 'LETTER'
orientation?: 'portrait' | 'landscape'
fontSize?: number
}
const DEFAULT_OPTIONS: PDFExportOptions = {
language: 'de',
includeTableOfContents: true,
includeDataPointList: true,
primaryColor: '#6366f1',
pageSize: 'A4',
orientation: 'portrait',
fontSize: 11,
}
// =============================================================================
// PDF CONTENT STRUCTURE
// =============================================================================
export interface PDFSection {
type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak'
content?: string
items?: string[]
table?: {
headers: string[]
rows: string[][]
}
style?: {
color?: string
fontSize?: number
bold?: boolean
italic?: boolean
align?: 'left' | 'center' | 'right'
}
}
// =============================================================================
// PDF CONTENT GENERATION
// =============================================================================
/**
* Generate PDF content structure for Privacy Policy
*/
export function generatePDFContent(
policy: GeneratedPrivacyPolicy,
companyInfo: CompanyInfo,
dataPoints: DataPoint[],
options: Partial<PDFExportOptions> = {}
): PDFSection[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const sections: PDFSection[] = []
const lang = opts.language
// Title page
sections.push({
type: 'title',
content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy',
style: { color: opts.primaryColor, fontSize: 28, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: lang === 'de'
? 'gemaess Art. 13, 14 DSGVO'
: 'according to Art. 13, 14 GDPR',
style: { fontSize: 14, align: 'center', italic: true },
})
// Company information
sections.push({
type: 'paragraph',
content: companyInfo.name,
style: { fontSize: 16, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${companyInfo.address}, ${companyInfo.postalCode} ${companyInfo.city}`,
style: { align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${lang === 'de' ? 'Stand' : 'Date'}: ${new Date(policy.generatedAt).toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')}`,
style: { align: 'center' },
})
sections.push({
type: 'paragraph',
content: `Version: ${policy.version}`,
style: { align: 'center', fontSize: 10 },
})
sections.push({ type: 'pagebreak' })
// Table of Contents
if (opts.includeTableOfContents) {
sections.push({
type: 'heading',
content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
style: { color: opts.primaryColor },
})
const tocItems = policy.sections.map((section, idx) =>
`${idx + 1}. ${section.title[lang]}`
)
if (opts.includeDataPointList) {
tocItems.push(lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog')
}
sections.push({
type: 'list',
items: tocItems,
})
sections.push({ type: 'pagebreak' })
}
// Privacy Policy Sections
policy.sections.forEach((section, idx) => {
sections.push({
type: 'heading',
content: `${idx + 1}. ${section.title[lang]}`,
style: { color: opts.primaryColor },
})
// Convert markdown-like content to paragraphs
const content = section.content[lang]
const paragraphs = content.split('\n\n')
for (const para of paragraphs) {
if (para.startsWith('- ')) {
// List items
const items = para.split('\n').filter(l => l.startsWith('- ')).map(l => l.substring(2))
sections.push({
type: 'list',
items,
})
} else if (para.startsWith('### ')) {
sections.push({
type: 'subheading',
content: para.substring(4),
})
} else if (para.startsWith('## ')) {
sections.push({
type: 'subheading',
content: para.substring(3),
style: { bold: true },
})
} else if (para.trim()) {
sections.push({
type: 'paragraph',
content: para.replace(/\*\*(.*?)\*\*/g, '$1'), // Remove markdown bold for plain text
})
}
}
// Add related data points if this section has them
if (section.dataPointIds.length > 0 && opts.includeDataPointList) {
const relatedDPs = dataPoints.filter(dp => section.dataPointIds.includes(dp.id))
if (relatedDPs.length > 0) {
sections.push({
type: 'paragraph',
content: lang === 'de'
? `Betroffene Datenkategorien: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`
: `Affected data categories: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`,
style: { italic: true, fontSize: 10 },
})
}
}
})
// Data Point Catalog Appendix
if (opts.includeDataPointList && dataPoints.length > 0) {
sections.push({ type: 'pagebreak' })
sections.push({
type: 'heading',
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
style: { color: opts.primaryColor },
})
sections.push({
type: 'paragraph',
content: lang === 'de'
? 'Die folgende Tabelle zeigt alle verarbeiteten personenbezogenen Daten:'
: 'The following table shows all processed personal data:',
})
// Group by category
const categories = [...new Set(dataPoints.map(dp => dp.category))]
for (const category of categories) {
const categoryDPs = dataPoints.filter(dp => dp.category === category)
const categoryMeta = CATEGORY_METADATA[category]
sections.push({
type: 'subheading',
content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`,
})
sections.push({
type: 'table',
table: {
headers: lang === 'de'
? ['Code', 'Datenpunkt', 'Zweck', 'Loeschfrist']
: ['Code', 'Data Point', 'Purpose', 'Retention'],
rows: categoryDPs.map(dp => [
dp.code,
dp.name[lang],
dp.purpose[lang].substring(0, 50) + (dp.purpose[lang].length > 50 ? '...' : ''),
RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod,
]),
},
})
}
}
// Footer
sections.push({
type: 'paragraph',
content: lang === 'de'
? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem Datenschutzerklaerung-Generator`
: `Generated on ${new Date().toLocaleDateString('en-US')} with the Privacy Policy Generator`,
style: { italic: true, align: 'center', fontSize: 9 },
})
return sections
}
// =============================================================================
// PDF BLOB GENERATION
// =============================================================================
/**
* Generate a PDF file as a Blob
* This generates HTML that can be printed to PDF or used with a PDF library
*/
export async function generatePDFBlob(
policy: GeneratedPrivacyPolicy,
companyInfo: CompanyInfo,
dataPoints: DataPoint[],
options: Partial<PDFExportOptions> = {}
): Promise<Blob> {
const content = generatePDFContent(policy, companyInfo, dataPoints, options)
const opts = { ...DEFAULT_OPTIONS, ...options }
// Generate HTML for PDF conversion
const html = generateHTMLFromContent(content, opts)
return new Blob([html], { type: 'text/html' })
}
/**
* Generate printable HTML from PDF content
*/
function generateHTMLFromContent(
content: PDFSection[],
options: PDFExportOptions
): string {
const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in'
const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in'
let html = `
<!DOCTYPE html>
<html lang="${options.language}">
<head>
<meta charset="utf-8">
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
<style>
@page {
size: ${pageWidth} ${pageHeight};
margin: 20mm;
}
body {
font-family: 'Segoe UI', Calibri, Arial, sans-serif;
font-size: ${options.fontSize}pt;
line-height: 1.6;
color: #1e293b;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
font-size: 24pt;
color: ${options.primaryColor};
border-bottom: 2px solid ${options.primaryColor};
padding-bottom: 10px;
margin-top: 30px;
}
h2 {
font-size: 16pt;
color: ${options.primaryColor};
margin-top: 24px;
}
h3 {
font-size: 13pt;
color: #334155;
margin-top: 18px;
}
p {
margin: 12px 0;
text-align: justify;
}
.title {
font-size: 28pt;
text-align: center;
color: ${options.primaryColor};
font-weight: bold;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
font-style: italic;
color: #64748b;
}
.center {
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 10pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 8px 12px;
text-align: left;
}
th {
background-color: ${options.primaryColor};
color: white;
font-weight: 600;
}
tr:nth-child(even) {
background-color: #f8fafc;
}
ul {
margin: 12px 0;
padding-left: 24px;
}
li {
margin: 6px 0;
}
.pagebreak {
page-break-after: always;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
font-size: 9pt;
color: #94a3b8;
text-align: center;
}
@media print {
body {
padding: 0;
}
.pagebreak {
page-break-after: always;
}
}
</style>
</head>
<body>
`
for (const section of content) {
switch (section.type) {
case 'title':
html += `<div class="title" style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</div>\n`
break
case 'heading':
html += `<h1 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h1>\n`
break
case 'subheading':
html += `<h3 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h3>\n`
break
case 'paragraph':
const alignClass = section.style?.align === 'center' ? ' class="center"' : ''
html += `<p${alignClass} style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</p>\n`
break
case 'list':
html += '<ul>\n'
for (const item of section.items || []) {
html += ` <li>${escapeHtml(item)}</li>\n`
}
html += '</ul>\n'
break
case 'table':
if (section.table) {
html += '<table>\n<thead><tr>\n'
for (const header of section.table.headers) {
html += ` <th>${escapeHtml(header)}</th>\n`
}
html += '</tr></thead>\n<tbody>\n'
for (const row of section.table.rows) {
html += '<tr>\n'
for (const cell of row) {
html += ` <td>${escapeHtml(cell)}</td>\n`
}
html += '</tr>\n'
}
html += '</tbody></table>\n'
}
break
case 'pagebreak':
html += '<div class="pagebreak"></div>\n'
break
}
}
html += '</body></html>'
return html
}
function getStyleString(style?: PDFSection['style']): string {
if (!style) return ''
const parts: string[] = []
if (style.color) parts.push(`color: ${style.color}`)
if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`)
if (style.bold) parts.push('font-weight: bold')
if (style.italic) parts.push('font-style: italic')
if (style.align) parts.push(`text-align: ${style.align}`)
return parts.join('; ')
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the PDF export
*/
export function generatePDFFilename(
companyInfo: CompanyInfo,
language: SupportedLanguage = 'de'
): string {
const companyName = companyInfo.name.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy-Policy'
return `${prefix}-${companyName}-${date}.html`
}
@@ -0,0 +1,595 @@
/**
* Cookie Banner Generator
*
* Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog.
* Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet.
*/
import {
DataPoint,
CookieCategory,
CookieBannerCategory,
CookieBannerConfig,
CookieBannerStyling,
CookieBannerTexts,
CookieBannerEmbedCode,
CookieInfo,
LocalizedText,
SupportedLanguage,
} from '../types'
import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Holt den lokalisierten Text
*/
function t(text: LocalizedText, language: SupportedLanguage): string {
return text[language]
}
// =============================================================================
// COOKIE BANNER CONFIGURATION
// =============================================================================
/**
* Standard Cookie Banner Texte
*/
export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
title: {
de: 'Cookie-Einstellungen',
en: 'Cookie Settings',
},
description: {
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
},
acceptAll: {
de: 'Alle akzeptieren',
en: 'Accept All',
},
rejectAll: {
de: 'Nur notwendige',
en: 'Essential Only',
},
customize: {
de: 'Einstellungen',
en: 'Customize',
},
save: {
de: 'Auswahl speichern',
en: 'Save Selection',
},
privacyPolicyLink: {
de: 'Mehr in unserer Datenschutzerklaerung',
en: 'More in our Privacy Policy',
},
}
/**
* Standard Styling fuer Cookie Banner
*/
export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
position: 'BOTTOM',
theme: 'LIGHT',
primaryColor: '#6366f1', // Indigo
secondaryColor: '#f1f5f9', // Slate-100
textColor: '#1e293b', // Slate-800
backgroundColor: '#ffffff',
borderRadius: 12,
maxWidth: 480,
}
// =============================================================================
// GENERATOR FUNCTIONS
// =============================================================================
/**
* Generiert Cookie-Banner Kategorien aus Datenpunkten
*/
export function generateCookieCategories(
dataPoints: DataPoint[]
): CookieBannerCategory[] {
// Filtere nur Datenpunkte mit Cookie-Kategorie
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
// Erstelle die Kategorien basierend auf den Defaults
return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
// Filtere die Datenpunkte fuer diese Kategorie
const categoryDataPoints = cookieDataPoints.filter(
(dp) => dp.cookieCategory === defaultCat.id
)
// Erstelle Cookie-Infos aus den Datenpunkten
const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
name: dp.code,
provider: 'First Party',
purpose: dp.purpose,
expiry: getExpiryFromRetention(dp.retentionPeriod),
type: 'FIRST_PARTY',
}))
return {
...defaultCat,
dataPointIds: categoryDataPoints.map((dp) => dp.id),
cookies,
}
}).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
}
/**
* Konvertiert Retention Period zu Cookie-Expiry String
*/
function getExpiryFromRetention(retention: string): string {
const mapping: Record<string, string> = {
'24_HOURS': '24 Stunden / 24 hours',
'30_DAYS': '30 Tage / 30 days',
'90_DAYS': '90 Tage / 90 days',
'12_MONTHS': '1 Jahr / 1 year',
'24_MONTHS': '2 Jahre / 2 years',
'36_MONTHS': '3 Jahre / 3 years',
'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
'UNTIL_PURPOSE_FULFILLED': 'Session',
'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
}
return mapping[retention] || 'Session'
}
/**
* Generiert die vollstaendige Cookie Banner Konfiguration
*/
export function generateCookieBannerConfig(
tenantId: string,
dataPoints: DataPoint[],
customTexts?: Partial<CookieBannerTexts>,
customStyling?: Partial<CookieBannerStyling>
): CookieBannerConfig {
const categories = generateCookieCategories(dataPoints)
return {
id: `cookie-banner-${tenantId}`,
tenantId,
categories,
styling: {
...DEFAULT_COOKIE_BANNER_STYLING,
...customStyling,
},
texts: {
...DEFAULT_COOKIE_BANNER_TEXTS,
...customTexts,
},
updatedAt: new Date(),
}
}
// =============================================================================
// EMBED CODE GENERATION
// =============================================================================
/**
* Generiert den Embed-Code fuer den Cookie Banner
*/
export function generateEmbedCode(
config: CookieBannerConfig,
privacyPolicyUrl: string = '/datenschutz'
): CookieBannerEmbedCode {
const css = generateCSS(config.styling)
const html = generateHTML(config, privacyPolicyUrl)
const js = generateJS(config)
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
return {
html,
css,
js,
scriptTag,
}
}
/**
* Generiert das CSS fuer den Cookie Banner
*/
function generateCSS(styling: CookieBannerStyling): string {
const positionStyles: Record<string, string> = {
BOTTOM: 'bottom: 0; left: 0; right: 0;',
TOP: 'top: 0; left: 0; right: 0;',
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
}
const isDark = styling.theme === 'DARK'
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
return `
/* Cookie Banner Styles */
.cookie-banner-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9998;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.cookie-banner-overlay.active {
opacity: 1;
visibility: visible;
}
.cookie-banner {
position: fixed;
${positionStyles[styling.position]}
z-index: 9999;
background: ${bgColor};
color: ${textColor};
border-radius: ${styling.borderRadius || 12}px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
padding: 24px;
max-width: ${styling.maxWidth}px;
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
transform: translateY(100%);
opacity: 0;
transition: all 0.3s ease;
}
.cookie-banner.active {
transform: translateY(0);
opacity: 1;
}
.cookie-banner-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
.cookie-banner-description {
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
opacity: 0.8;
}
.cookie-banner-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.cookie-banner-btn {
flex: 1;
min-width: 120px;
padding: 12px 20px;
border-radius: ${(styling.borderRadius || 12) / 2}px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.cookie-banner-btn-primary {
background: ${styling.primaryColor};
color: white;
}
.cookie-banner-btn-primary:hover {
filter: brightness(1.1);
}
.cookie-banner-btn-secondary {
background: ${styling.secondaryColor || borderColor};
color: ${textColor};
}
.cookie-banner-btn-secondary:hover {
filter: brightness(0.95);
}
.cookie-banner-link {
display: block;
margin-top: 16px;
font-size: 12px;
color: ${styling.primaryColor};
text-decoration: none;
}
.cookie-banner-link:hover {
text-decoration: underline;
}
/* Category Details */
.cookie-banner-details {
margin-top: 16px;
border-top: 1px solid ${borderColor};
padding-top: 16px;
display: none;
}
.cookie-banner-details.active {
display: block;
}
.cookie-banner-category {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid ${borderColor};
}
.cookie-banner-category:last-child {
border-bottom: none;
}
.cookie-banner-category-info {
flex: 1;
}
.cookie-banner-category-name {
font-weight: 500;
font-size: 14px;
}
.cookie-banner-category-desc {
font-size: 12px;
opacity: 0.7;
margin-top: 4px;
}
.cookie-banner-toggle {
position: relative;
width: 48px;
height: 28px;
background: ${borderColor};
border-radius: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.cookie-banner-toggle.active {
background: ${styling.primaryColor};
}
.cookie-banner-toggle.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cookie-banner-toggle::after {
content: '';
position: absolute;
top: 4px;
left: 4px;
width: 20px;
height: 20px;
background: white;
border-radius: 50%;
transition: all 0.2s ease;
}
.cookie-banner-toggle.active::after {
left: 24px;
}
@media (max-width: 640px) {
.cookie-banner {
margin: 0;
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
max-width: 100%;
}
.cookie-banner-buttons {
flex-direction: column;
}
.cookie-banner-btn {
width: 100%;
}
}
`.trim()
}
/**
* Generiert das HTML fuer den Cookie Banner
*/
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
const categoriesHTML = config.categories
.map((cat) => {
const isRequired = cat.isRequired
return `
<div class="cookie-banner-category" data-category="${cat.id}">
<div class="cookie-banner-category-info">
<div class="cookie-banner-category-name">${cat.name.de}</div>
<div class="cookie-banner-category-desc">${cat.description.de}</div>
</div>
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
data-category="${cat.id}"
data-required="${isRequired}"></div>
</div>
`
})
.join('')
return `
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
<div class="cookie-banner-description">${config.texts.description.de}</div>
<div class="cookie-banner-buttons">
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
${config.texts.rejectAll.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
${config.texts.customize.de}
</button>
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
${config.texts.acceptAll.de}
</button>
</div>
<div class="cookie-banner-details" id="cookieBannerDetails">
${categoriesHTML}
<div class="cookie-banner-buttons" style="margin-top: 16px;">
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
${config.texts.save.de}
</button>
</div>
</div>
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
${config.texts.privacyPolicyLink.de}
</a>
</div>
`.trim()
}
/**
* Generiert das JavaScript fuer den Cookie Banner
*/
function generateJS(config: CookieBannerConfig): string {
const categoryIds = config.categories.map((c) => c.id)
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
return `
(function() {
'use strict';
const COOKIE_NAME = 'cookie_consent';
const COOKIE_EXPIRY_DAYS = 365;
const CATEGORIES = ${JSON.stringify(categoryIds)};
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
// Get consent from cookie
function getConsent() {
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
if (!cookie) return null;
try {
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
} catch {
return null;
}
}
// Save consent to cookie
function saveConsent(consent) {
const date = new Date();
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
';expires=' + date.toUTCString() +
';path=/;SameSite=Lax';
// Dispatch event
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
}
// Check if category is consented
function hasConsent(category) {
const consent = getConsent();
if (!consent) return REQUIRED_CATEGORIES.includes(category);
return consent[category] === true;
}
// Initialize banner
function initBanner() {
const banner = document.getElementById('cookieBanner');
const overlay = document.getElementById('cookieBannerOverlay');
const details = document.getElementById('cookieBannerDetails');
if (!banner) return;
const consent = getConsent();
if (consent) {
// User has already consented
return;
}
// Show banner
setTimeout(() => {
banner.classList.add('active');
overlay.classList.add('active');
}, 500);
// Accept all
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
const consent = {};
CATEGORIES.forEach(cat => consent[cat] = true);
saveConsent(consent);
closeBanner();
});
// Reject all (only essential)
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
const consent = {};
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
saveConsent(consent);
closeBanner();
});
// Customize
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
details.classList.toggle('active');
});
// Save selection
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
const consent = {};
CATEGORIES.forEach(cat => {
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
});
saveConsent(consent);
closeBanner();
});
// Toggle handlers
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
if (toggle.dataset.required === 'true') return;
toggle.addEventListener('click', () => {
toggle.classList.toggle('active');
});
});
// Close on overlay click
overlay?.addEventListener('click', () => {
// Don't close - user must make a choice
});
}
function closeBanner() {
const banner = document.getElementById('cookieBanner');
const overlay = document.getElementById('cookieBannerOverlay');
banner?.classList.remove('active');
overlay?.classList.remove('active');
}
// Expose API
window.CookieConsent = {
getConsent,
saveConsent,
hasConsent,
show: () => {
document.getElementById('cookieBanner')?.classList.add('active');
document.getElementById('cookieBannerOverlay')?.classList.add('active');
},
hide: closeBanner
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBanner);
} else {
initBanner();
}
})();
`.trim()
}
// Note: All exports are defined inline with 'export const' and 'export function'
@@ -0,0 +1,965 @@
/**
* Privacy Policy Generator
*
* Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog.
* Die DSI wird aus 9 Abschnitten generiert:
*
* 1. Verantwortlicher (companyInfo)
* 2. Erhobene Daten (dataPoints nach Kategorie)
* 3. Verarbeitungszwecke (dataPoints.purpose)
* 4. Rechtsgrundlagen (dataPoints.legalBasis)
* 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients)
* 6. Speicherdauer (retentionMatrix)
* 7. Betroffenenrechte (statischer Text + Links)
* 8. Cookies (cookieCategory-basiert)
* 9. Aenderungen (statischer Text + Versionierung)
*/
import {
DataPoint,
DataPointCategory,
CompanyInfo,
PrivacyPolicySection,
GeneratedPrivacyPolicy,
SupportedLanguage,
ExportFormat,
LocalizedText,
RetentionMatrixEntry,
LegalBasis,
CATEGORY_METADATA,
LEGAL_BASIS_INFO,
RETENTION_PERIOD_INFO,
ARTICLE_9_WARNING,
} from '../types'
import { RETENTION_MATRIX } from '../catalog/loader'
// =============================================================================
// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge
// =============================================================================
const ALL_CATEGORIES: DataPointCategory[] = [
'MASTER_DATA', // A
'CONTACT_DATA', // B
'AUTHENTICATION', // C
'CONSENT', // D
'COMMUNICATION', // E
'PAYMENT', // F
'USAGE_DATA', // G
'LOCATION', // H
'DEVICE_DATA', // I
'MARKETING', // J
'ANALYTICS', // K
'SOCIAL_MEDIA', // L
'HEALTH_DATA', // M - Art. 9 DSGVO
'EMPLOYEE_DATA', // N - BDSG § 26
'CONTRACT_DATA', // O
'LOG_DATA', // P
'AI_DATA', // Q - AI Act
'SECURITY', // R
]
// Alle Rechtsgrundlagen in der richtigen Reihenfolge
const ALL_LEGAL_BASES: LegalBasis[] = [
'CONTRACT',
'CONSENT',
'EXPLICIT_CONSENT',
'LEGITIMATE_INTEREST',
'LEGAL_OBLIGATION',
'VITAL_INTERESTS',
'PUBLIC_INTEREST',
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Holt den lokalisierten Text
*/
function t(text: LocalizedText, language: SupportedLanguage): string {
return text[language]
}
/**
* Gruppiert Datenpunkte nach Kategorie
*/
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
const grouped = new Map<DataPointCategory, DataPoint[]>()
for (const dp of dataPoints) {
const existing = grouped.get(dp.category) || []
grouped.set(dp.category, [...existing, dp])
}
return grouped
}
/**
* Gruppiert Datenpunkte nach Rechtsgrundlage
*/
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
const grouped = new Map<LegalBasis, DataPoint[]>()
for (const dp of dataPoints) {
const existing = grouped.get(dp.legalBasis) || []
grouped.set(dp.legalBasis, [...existing, dp])
}
return grouped
}
/**
* Extrahiert alle einzigartigen Drittanbieter
*/
function extractThirdParties(dataPoints: DataPoint[]): string[] {
const thirdParties = new Set<string>()
for (const dp of dataPoints) {
for (const recipient of dp.thirdPartyRecipients) {
thirdParties.add(recipient)
}
}
return Array.from(thirdParties).sort()
}
/**
* Formatiert ein Datum fuer die Anzeige
*/
function formatDate(date: Date, language: SupportedLanguage): string {
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
// =============================================================================
// SECTION GENERATORS
// =============================================================================
/**
* Abschnitt 1: Verantwortlicher
*/
function generateControllerSection(
companyInfo: CompanyInfo,
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '1. Verantwortlicher',
en: '1. Data Controller',
}
const dpoSection = companyInfo.dpoName
? language === 'de'
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
: ''
const content: LocalizedText = {
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
**${companyInfo.name}**
${companyInfo.address}
${companyInfo.postalCode} ${companyInfo.city}
${companyInfo.country}
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
en: `The controller responsible for data processing on this website is:
**${companyInfo.name}**
${companyInfo.address}
${companyInfo.postalCode} ${companyInfo.city}
${companyInfo.country}
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
}
return {
id: 'controller',
order: 1,
title,
content,
dataPointIds: [],
isRequired: true,
isGenerated: false,
}
}
/**
* Abschnitt 2: Erhobene Daten (18 Kategorien)
*/
function generateDataCollectionSection(
dataPoints: DataPoint[],
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '2. Erhobene personenbezogene Daten',
en: '2. Personal Data We Collect',
}
const grouped = groupByCategory(dataPoints)
const sections: string[] = []
// Prüfe ob Art. 9 Daten enthalten sind
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
for (const category of ALL_CATEGORIES) {
const categoryData = grouped.get(category)
if (!categoryData || categoryData.length === 0) continue
const categoryMeta = CATEGORY_METADATA[category]
if (!categoryMeta) continue
const categoryTitle = t(categoryMeta.name, language)
// Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten)
let categoryNote = ''
if (category === 'HEALTH_DATA') {
categoryNote = language === 'de'
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
} else if (category === 'EMPLOYEE_DATA') {
categoryNote = language === 'de'
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
} else if (category === 'AI_DATA') {
categoryNote = language === 'de'
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
}
const dataList = categoryData
.map((dp) => {
const specialTag = dp.isSpecialCategory
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
: ''
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
})
.join('\n')
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
}
const intro: LocalizedText = {
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
en: 'We collect and process the following personal data:',
}
// Zusätzlicher Hinweis für Art. 9 Daten
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
? {
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
}
: { de: '', en: '' }
const content: LocalizedText = {
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
}
return {
id: 'data-collection',
order: 2,
title,
content,
dataPointIds: dataPoints.map((dp) => dp.id),
isRequired: true,
isGenerated: true,
}
}
/**
* Abschnitt 3: Verarbeitungszwecke
*/
function generatePurposesSection(
dataPoints: DataPoint[],
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '3. Zwecke der Datenverarbeitung',
en: '3. Purposes of Data Processing',
}
// Gruppiere nach Zweck (unique purposes)
const purposes = new Map<string, DataPoint[]>()
for (const dp of dataPoints) {
const purpose = t(dp.purpose, language)
const existing = purposes.get(purpose) || []
purposes.set(purpose, [...existing, dp])
}
const purposeList = Array.from(purposes.entries())
.map(([purpose, dps]) => {
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
})
.join('\n\n')
const content: LocalizedText = {
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
}
return {
id: 'purposes',
order: 3,
title,
content,
dataPointIds: dataPoints.map((dp) => dp.id),
isRequired: true,
isGenerated: true,
}
}
/**
* Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen)
*/
function generateLegalBasisSection(
dataPoints: DataPoint[],
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '4. Rechtsgrundlagen der Verarbeitung',
en: '4. Legal Basis for Processing',
}
const grouped = groupByLegalBasis(dataPoints)
const sections: string[] = []
// Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge
for (const basis of ALL_LEGAL_BASES) {
const basisData = grouped.get(basis)
if (!basisData || basisData.length === 0) continue
const basisInfo = LEGAL_BASIS_INFO[basis]
if (!basisInfo) continue
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
const basisDesc = t(basisInfo.description, language)
// Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen
let additionalWarning = ''
if (basis === 'EXPLICIT_CONSENT') {
additionalWarning = language === 'de'
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
}
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
const dataList = basisData
.map((dp) => {
const specialTag = dp.isSpecialCategory
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
: ''
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
})
.join('\n')
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
}
const content: LocalizedText = {
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
}
return {
id: 'legal-basis',
order: 4,
title,
content,
dataPointIds: dataPoints.map((dp) => dp.id),
isRequired: true,
isGenerated: true,
}
}
/**
* Abschnitt 5: Empfaenger / Dritte
*/
function generateRecipientsSection(
dataPoints: DataPoint[],
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '5. Empfaenger und Datenweitergabe',
en: '5. Recipients and Data Sharing',
}
const thirdParties = extractThirdParties(dataPoints)
if (thirdParties.length === 0) {
const content: LocalizedText = {
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
}
return {
id: 'recipients',
order: 5,
title,
content,
dataPointIds: [],
isRequired: true,
isGenerated: false,
}
}
// Gruppiere nach Drittanbieter
const recipientDetails = new Map<string, DataPoint[]>()
for (const dp of dataPoints) {
for (const recipient of dp.thirdPartyRecipients) {
const existing = recipientDetails.get(recipient) || []
recipientDetails.set(recipient, [...existing, dp])
}
}
const recipientList = Array.from(recipientDetails.entries())
.map(([recipient, dps]) => {
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
return `- **${recipient}**: ${dataNames}`
})
.join('\n')
const content: LocalizedText = {
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
}
return {
id: 'recipients',
order: 5,
title,
content,
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
isRequired: true,
isGenerated: true,
}
}
/**
* Abschnitt 6: Speicherdauer
*/
function generateRetentionSection(
dataPoints: DataPoint[],
retentionMatrix: RetentionMatrixEntry[],
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '6. Speicherdauer',
en: '6. Data Retention',
}
const grouped = groupByCategory(dataPoints)
const sections: string[] = []
for (const entry of retentionMatrix) {
const categoryData = grouped.get(entry.category)
if (!categoryData || categoryData.length === 0) continue
const categoryName = t(entry.categoryName, language)
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
const dataRetention = categoryData
.map((dp) => {
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
return `- ${t(dp.name, language)}: ${period}`
})
.join('\n')
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
}
const content: LocalizedText = {
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
}
return {
id: 'retention',
order: 6,
title,
content,
dataPointIds: dataPoints.map((dp) => dp.id),
isRequired: true,
isGenerated: true,
}
}
/**
* Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO)
* Wird nur generiert, wenn Art. 9 Daten vorhanden sind
*/
function generateSpecialCategoriesSection(
dataPoints: DataPoint[],
language: SupportedLanguage
): PrivacyPolicySection | null {
// Filtere Art. 9 Datenpunkte
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
if (specialCategoryDataPoints.length === 0) {
return null
}
const title: LocalizedText = {
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
}
const dataList = specialCategoryDataPoints
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
.join('\n')
const content: LocalizedText = {
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
${dataList}
### Ihre ausdrueckliche Einwilligung
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
### Ihre Rechte bei Art. 9 Daten
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
### Besondere Schutzmassnahmen
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
- Ende-zu-Ende-Verschluesselung
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
- Audit-Logging aller Zugriffe
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
${dataList}
### Your Explicit Consent
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
### Your Rights Regarding Art. 9 Data
- You can **withdraw your consent at any time**
- Withdrawal does not affect the lawfulness of previous processing
- Upon withdrawal, your data will be deleted immediately
- You have the right to **access, rectification, and erasure**
### Special Protection Measures
For this sensitive data, we have implemented special technical and organizational measures:
- End-to-end encryption
- Strict access control (need-to-know principle)
- Audit logging of all access
- Regular data protection impact assessments`,
}
return {
id: 'special-categories',
order: 6.5, // Zwischen Speicherdauer (6) und Rechte (7)
title,
content,
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
isRequired: false,
isGenerated: true,
}
}
/**
* Abschnitt 7: Betroffenenrechte
*/
function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
const title: LocalizedText = {
de: '7. Ihre Rechte als betroffene Person',
en: '7. Your Rights as a Data Subject',
}
const content: LocalizedText = {
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
### Auskunftsrecht (Art. 15 DSGVO)
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
### Recht auf Berichtigung (Art. 16 DSGVO)
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
### Recht auf Loeschung (Art. 17 DSGVO)
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
### Widerspruchsrecht (Art. 21 DSGVO)
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
en: `You have the following rights regarding your personal data:
### Right of Access (Art. 15 GDPR)
You have the right to request information about the personal data we process about you.
### Right to Rectification (Art. 16 GDPR)
You have the right to request the correction of inaccurate data or the completion of incomplete data.
### Right to Erasure (Art. 17 GDPR)
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
### Right to Restriction of Processing (Art. 18 GDPR)
You have the right to request the restriction of processing of your data.
### Right to Data Portability (Art. 20 GDPR)
You have the right to receive your data in a structured, commonly used, and machine-readable format.
### Right to Object (Art. 21 GDPR)
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
### Right to Withdraw Consent (Art. 7(3) GDPR)
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
**To exercise your rights, please contact us using the contact details provided above.**`,
}
return {
id: 'rights',
order: 7,
title,
content,
dataPointIds: [],
isRequired: true,
isGenerated: false,
}
}
/**
* Abschnitt 8: Cookies
*/
function generateCookiesSection(
dataPoints: DataPoint[],
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '8. Cookies und aehnliche Technologien',
en: '8. Cookies and Similar Technologies',
}
// Filtere Datenpunkte mit Cookie-Kategorie
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
if (cookieDataPoints.length === 0) {
const content: LocalizedText = {
de: 'Wir verwenden auf dieser Website keine Cookies.',
en: 'We do not use cookies on this website.',
}
return {
id: 'cookies',
order: 8,
title,
content,
dataPointIds: [],
isRequired: false,
isGenerated: false,
}
}
// Gruppiere nach Cookie-Kategorie
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
const sections: string[] = []
if (essential.length > 0) {
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
sections.push(
language === 'de'
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
)
}
if (performance.length > 0) {
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
sections.push(
language === 'de'
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
)
}
if (personalization.length > 0) {
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
sections.push(
language === 'de'
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
)
}
if (externalMedia.length > 0) {
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
sections.push(
language === 'de'
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
)
}
const intro: LocalizedText = {
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
}
const content: LocalizedText = {
de: `${intro.de}\n\n${sections.join('\n\n')}`,
en: `${intro.en}\n\n${sections.join('\n\n')}`,
}
return {
id: 'cookies',
order: 8,
title,
content,
dataPointIds: cookieDataPoints.map((dp) => dp.id),
isRequired: true,
isGenerated: true,
}
}
/**
* Abschnitt 9: Aenderungen
*/
function generateChangesSection(
version: string,
date: Date,
language: SupportedLanguage
): PrivacyPolicySection {
const title: LocalizedText = {
de: '9. Aenderungen dieser Datenschutzerklaerung',
en: '9. Changes to this Privacy Policy',
}
const formattedDate = formatDate(date, language)
const content: LocalizedText = {
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
The new privacy policy will then apply for your next visit.`,
}
return {
id: 'changes',
order: 9,
title,
content,
dataPointIds: [],
isRequired: true,
isGenerated: false,
}
}
// =============================================================================
// MAIN GENERATOR FUNCTIONS
// =============================================================================
/**
* Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9)
*/
export function generatePrivacyPolicySections(
dataPoints: DataPoint[],
companyInfo: CompanyInfo,
language: SupportedLanguage,
version: string = '1.0.0'
): PrivacyPolicySection[] {
const now = new Date()
const sections: PrivacyPolicySection[] = [
generateControllerSection(companyInfo, language),
generateDataCollectionSection(dataPoints, language),
generatePurposesSection(dataPoints, language),
generateLegalBasisSection(dataPoints, language),
generateRecipientsSection(dataPoints, language),
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
]
// Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
if (specialCategoriesSection) {
sections.push(specialCategoriesSection)
}
sections.push(
generateRightsSection(language),
generateCookiesSection(dataPoints, language),
generateChangesSection(version, now, language)
)
// Abschnittsnummern neu vergeben
sections.forEach((section, index) => {
section.order = index + 1
// Titel-Nummer aktualisieren
const titleDe = section.title.de
const titleEn = section.title.en
if (titleDe.match(/^\d+[a-z]?\./)) {
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
}
if (titleEn.match(/^\d+[a-z]?\./)) {
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
}
})
return sections
}
/**
* Generiert die vollstaendige Privacy Policy
*/
export function generatePrivacyPolicy(
tenantId: string,
dataPoints: DataPoint[],
companyInfo: CompanyInfo,
language: SupportedLanguage,
format: ExportFormat = 'HTML'
): GeneratedPrivacyPolicy {
const version = '1.0.0'
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
// Generiere den Inhalt
const content = renderPrivacyPolicy(sections, language, format)
return {
id: `privacy-policy-${tenantId}-${Date.now()}`,
tenantId,
language,
sections,
companyInfo,
generatedAt: new Date(),
version,
format,
content,
}
}
/**
* Rendert die Privacy Policy im gewuenschten Format
*/
function renderPrivacyPolicy(
sections: PrivacyPolicySection[],
language: SupportedLanguage,
format: ExportFormat
): string {
switch (format) {
case 'HTML':
return renderAsHTML(sections, language)
case 'MARKDOWN':
return renderAsMarkdown(sections, language)
default:
return renderAsMarkdown(sections, language)
}
}
/**
* Rendert als HTML
*/
function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
const sectionsHTML = sections
.map((section) => {
const content = t(section.content, language)
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/### (.+)/g, '<h3>$1</h3>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
return `
<section id="${section.id}">
<h2>${t(section.title, language)}</h2>
<p>${content}</p>
</section>
`
})
.join('\n')
return `<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
color: #333;
}
h1 { font-size: 2rem; margin-bottom: 2rem; }
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
p { margin: 1rem 0; }
ul, ol { margin: 1rem 0; padding-left: 2rem; }
li { margin: 0.5rem 0; }
strong { font-weight: 600; }
</style>
</head>
<body>
<h1>${title}</h1>
${sectionsHTML}
</body>
</html>`
}
/**
* Rendert als Markdown
*/
function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
const sectionsMarkdown = sections
.map((section) => {
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
})
.join('\n\n---\n\n')
return `# ${title}\n\n${sectionsMarkdown}`
}
// =============================================================================
// EXPORTS
// =============================================================================
export {
generateControllerSection,
generateDataCollectionSection,
generatePurposesSection,
generateLegalBasisSection,
generateRecipientsSection,
generateRetentionSection,
generateSpecialCategoriesSection,
generateRightsSection,
generateCookiesSection,
generateChangesSection,
renderAsHTML,
renderAsMarkdown,
}
@@ -0,0 +1,79 @@
/**
* Datenpunktkatalog & Datenschutzinformationen-Generator
*
* Dieses Modul erweitert das SDK Einwilligungen-Modul um:
* - Datenpunktkatalog mit 28 vordefinierten + kundenspezifischen Datenpunkten
* - Automatische Privacy Policy Generierung
* - Cookie Banner Konfiguration
* - Retention Matrix Visualisierung
*
* @module lib/sdk/einwilligungen
*/
// =============================================================================
// TYPES
// =============================================================================
export * from './types'
// =============================================================================
// CATALOG
// =============================================================================
export {
PREDEFINED_DATA_POINTS,
RETENTION_MATRIX,
DEFAULT_COOKIE_CATEGORIES,
getDataPointById,
getDataPointByCode,
getDataPointsByCategory,
getDataPointsByLegalBasis,
getDataPointsByCookieCategory,
getDataPointsRequiringConsent,
getHighRiskDataPoints,
countDataPointsByCategory,
countDataPointsByRiskLevel,
createDefaultCatalog,
searchDataPoints,
} from './catalog/loader'
// =============================================================================
// CONTEXT
// =============================================================================
export {
EinwilligungenProvider,
useEinwilligungen,
initialState as einwilligungenInitialState,
einwilligungenReducer,
} from './context'
// =============================================================================
// GENERATORS (to be implemented)
// =============================================================================
// Privacy Policy Generator
export { generatePrivacyPolicy, generatePrivacyPolicySections } from './generator/privacy-policy'
// Cookie Banner Generator
export { generateCookieBannerConfig, generateEmbedCode } from './generator/cookie-banner'
// =============================================================================
// EXPORT
// =============================================================================
// PDF Export
export {
generatePDFContent as generatePrivacyPolicyPDFContent,
generatePDFBlob as generatePrivacyPolicyPDFBlob,
generatePDFFilename as generatePrivacyPolicyPDFFilename,
} from './export/pdf'
export type { PDFExportOptions as PrivacyPolicyPDFExportOptions } from './export/pdf'
// DOCX Export
export {
generateDOCXContent as generatePrivacyPolicyDOCXContent,
generateDOCXBlob as generatePrivacyPolicyDOCXBlob,
generateDOCXFilename as generatePrivacyPolicyDOCXFilename,
} from './export/docx'
export type { DOCXExportOptions as PrivacyPolicyDOCXExportOptions } from './export/docx'
@@ -0,0 +1,838 @@
/**
* Datenpunktkatalog & Datenschutzinformationen-Generator
* TypeScript Interfaces
*
* Dieses Modul definiert alle Typen für:
* - Datenpunktkatalog (32 vordefinierte + kundenspezifische)
* - Privacy Policy Generator
* - Cookie Banner Configuration
* - Retention Matrix
*/
// =============================================================================
// ENUMS
// =============================================================================
/**
* Kategorien für Datenpunkte (18 Kategorien: A-R)
*/
export type DataPointCategory =
| 'MASTER_DATA' // A: Stammdaten
| 'CONTACT_DATA' // B: Kontaktdaten
| 'AUTHENTICATION' // C: Authentifizierungsdaten
| 'CONSENT' // D: Einwilligungsdaten
| 'COMMUNICATION' // E: Kommunikationsdaten
| 'PAYMENT' // F: Zahlungsdaten
| 'USAGE_DATA' // G: Nutzungsdaten
| 'LOCATION' // H: Standortdaten
| 'DEVICE_DATA' // I: Gerätedaten
| 'MARKETING' // J: Marketingdaten
| 'ANALYTICS' // K: Analysedaten
| 'SOCIAL_MEDIA' // L: Social-Media-Daten
| 'HEALTH_DATA' // M: Gesundheitsdaten (Art. 9 DSGVO)
| 'EMPLOYEE_DATA' // N: Beschäftigtendaten
| 'CONTRACT_DATA' // O: Vertragsdaten
| 'LOG_DATA' // P: Protokolldaten
| 'AI_DATA' // Q: KI-Daten
| 'SECURITY' // R: Sicherheitsdaten
/**
* Risikoniveau für Datenpunkte
*/
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
/**
* Rechtsgrundlagen nach DSGVO Art. 6 und Art. 9
*/
export type LegalBasis =
| 'CONTRACT' // Art. 6 Abs. 1 lit. b DSGVO
| 'CONSENT' // Art. 6 Abs. 1 lit. a DSGVO
| 'EXPLICIT_CONSENT' // Art. 9 Abs. 2 lit. a DSGVO (für Art. 9 Daten)
| 'LEGITIMATE_INTEREST' // Art. 6 Abs. 1 lit. f DSGVO
| 'LEGAL_OBLIGATION' // Art. 6 Abs. 1 lit. c DSGVO
| 'VITAL_INTERESTS' // Art. 6 Abs. 1 lit. d DSGVO
| 'PUBLIC_INTEREST' // Art. 6 Abs. 1 lit. e DSGVO
/**
* Aufbewahrungsfristen
*/
export type RetentionPeriod =
| '24_HOURS'
| '30_DAYS'
| '90_DAYS'
| '12_MONTHS'
| '24_MONTHS'
| '26_MONTHS' // Google Analytics Standard
| '36_MONTHS'
| '48_MONTHS'
| '6_YEARS'
| '10_YEARS'
| 'UNTIL_REVOCATION'
| 'UNTIL_PURPOSE_FULFILLED'
| 'UNTIL_ACCOUNT_DELETION'
/**
* Cookie-Kategorien für Cookie-Banner
*/
export type CookieCategory =
| 'ESSENTIAL' // Technisch notwendig
| 'PERFORMANCE' // Analyse & Performance
| 'PERSONALIZATION' // Personalisierung
| 'EXTERNAL_MEDIA' // Externe Medien
/**
* Export-Formate für Privacy Policy
*/
export type ExportFormat = 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX'
/**
* Sprachen
*/
export type SupportedLanguage = 'de' | 'en'
// =============================================================================
// DATA POINT
// =============================================================================
/**
* Lokalisierter Text (DE/EN)
*/
export interface LocalizedText {
de: string
en: string
}
/**
* Einzelner Datenpunkt im Katalog
*/
export interface DataPoint {
id: string
code: string // z.B. "A1", "B2", "C3"
category: DataPointCategory
name: LocalizedText
description: LocalizedText
purpose: LocalizedText
riskLevel: RiskLevel
legalBasis: LegalBasis
legalBasisJustification: LocalizedText
retentionPeriod: RetentionPeriod
retentionJustification: LocalizedText
cookieCategory: CookieCategory | null // null = kein Cookie
isSpecialCategory: boolean // Art. 9 DSGVO (sensible Daten)
requiresExplicitConsent: boolean
thirdPartyRecipients: string[]
technicalMeasures: string[]
tags: string[]
isCustom?: boolean // Kundenspezifischer Datenpunkt
isActive?: boolean // Aktiviert fuer diesen Tenant
}
/**
* YAML-Struktur fuer Datenpunkte (fuer Loader)
*/
export interface DataPointYAML {
id: string
code: string
category: string
name_de: string
name_en: string
description_de: string
description_en: string
purpose_de: string
purpose_en: string
risk_level: string
legal_basis: string
legal_basis_justification_de: string
legal_basis_justification_en: string
retention_period: string
retention_justification_de: string
retention_justification_en: string
cookie_category: string | null
is_special_category: boolean
requires_explicit_consent: boolean
third_party_recipients: string[]
technical_measures: string[]
tags: string[]
}
// =============================================================================
// CATALOG & RETENTION MATRIX
// =============================================================================
/**
* Gesamter Datenpunktkatalog eines Tenants
*/
export interface DataPointCatalog {
id: string
tenantId: string
version: string
dataPoints: DataPoint[] // Vordefinierte (32)
customDataPoints: DataPoint[] // Kundenspezifische
retentionMatrix: RetentionMatrixEntry[]
createdAt: Date
updatedAt: Date
}
/**
* Eintrag in der Retention Matrix
*/
export interface RetentionMatrixEntry {
category: DataPointCategory
categoryName: LocalizedText
standardPeriod: RetentionPeriod
legalBasis: string
exceptions: RetentionException[]
}
/**
* Ausnahme von der Standard-Loeschfrist
*/
export interface RetentionException {
condition: LocalizedText
period: RetentionPeriod
reason: LocalizedText
}
// =============================================================================
// PRIVACY POLICY GENERATION
// =============================================================================
/**
* Abschnitt in der Privacy Policy
*/
export interface PrivacyPolicySection {
id: string
order: number
title: LocalizedText
content: LocalizedText
dataPointIds: string[]
isRequired: boolean
isGenerated: boolean // true = aus Datenpunkten generiert
}
/**
* Unternehmensinfo fuer Privacy Policy
*/
export interface CompanyInfo {
name: string
address: string
city: string
postalCode: string
country: string
email: string
phone?: string
website?: string
dpoName?: string // Datenschutzbeauftragter
dpoEmail?: string
dpoPhone?: string
registrationNumber?: string // Handelsregister
vatId?: string // USt-IdNr
}
/**
* Generierte Privacy Policy
*/
export interface GeneratedPrivacyPolicy {
id: string
tenantId: string
language: SupportedLanguage
sections: PrivacyPolicySection[]
companyInfo: CompanyInfo
generatedAt: Date
version: string
format: ExportFormat
content?: string // Rendered content (HTML/MD)
}
/**
* Optionen fuer Privacy Policy Generierung
*/
export interface PrivacyPolicyGenerationOptions {
language: SupportedLanguage
format: ExportFormat
includeDataPoints: string[] // Welche Datenpunkte einschliessen
customSections?: PrivacyPolicySection[] // Zusaetzliche Abschnitte
styling?: PrivacyPolicyStyling
}
/**
* Styling-Optionen fuer PDF/HTML Export
*/
export interface PrivacyPolicyStyling {
primaryColor?: string
fontFamily?: string
fontSize?: number
headerFontSize?: number
includeTableOfContents?: boolean
includeDateFooter?: boolean
logoUrl?: string
}
// =============================================================================
// COOKIE BANNER CONFIG
// =============================================================================
/**
* Einzelner Cookie in einer Kategorie
*/
export interface CookieInfo {
name: string
provider: string
purpose: LocalizedText
expiry: string
type: 'FIRST_PARTY' | 'THIRD_PARTY'
}
/**
* Cookie-Banner Kategorie
*/
export interface CookieBannerCategory {
id: CookieCategory
name: LocalizedText
description: LocalizedText
isRequired: boolean // Essentiell = required
defaultEnabled: boolean
dataPointIds: string[] // Verknuepfte Datenpunkte
cookies: CookieInfo[]
}
/**
* Styling fuer Cookie Banner
*/
export interface CookieBannerStyling {
position: 'BOTTOM' | 'TOP' | 'CENTER'
theme: 'LIGHT' | 'DARK' | 'CUSTOM'
primaryColor?: string
secondaryColor?: string
textColor?: string
backgroundColor?: string
borderRadius?: number
maxWidth?: number
}
/**
* Texte fuer Cookie Banner
*/
export interface CookieBannerTexts {
title: LocalizedText
description: LocalizedText
acceptAll: LocalizedText
rejectAll: LocalizedText
customize: LocalizedText
save: LocalizedText
privacyPolicyLink: LocalizedText
}
/**
* Generierter Code fuer Cookie Banner
*/
export interface CookieBannerEmbedCode {
html: string
css: string
js: string
scriptTag: string // Fertiger Script-Tag zum Einbinden
}
/**
* Vollstaendige Cookie Banner Konfiguration
*/
export interface CookieBannerConfig {
id: string
tenantId: string
categories: CookieBannerCategory[]
styling: CookieBannerStyling
texts: CookieBannerTexts
embedCode?: CookieBannerEmbedCode
updatedAt: Date
}
// =============================================================================
// CONSENT MANAGEMENT
// =============================================================================
/**
* Einzelne Einwilligung eines Nutzers
*/
export interface ConsentEntry {
id: string
userId: string
dataPointId: string
granted: boolean
grantedAt: Date
revokedAt?: Date
ipAddress?: string
userAgent?: string
consentVersion: string
}
/**
* Aggregierte Consent-Statistiken
*/
export interface ConsentStatistics {
totalConsents: number
activeConsents: number
revokedConsents: number
byCategory: Record<DataPointCategory, {
total: number
active: number
revoked: number
}>
byLegalBasis: Record<LegalBasis, {
total: number
active: number
}>
conversionRate: number // Prozent der Nutzer mit Consent
}
// =============================================================================
// EINWILLIGUNGEN STATE & ACTIONS
// =============================================================================
/**
* Aktiver Tab in der Einwilligungen-Ansicht
*/
export type EinwilligungenTab =
| 'catalog'
| 'privacy-policy'
| 'cookie-banner'
| 'retention'
| 'consents'
/**
* State fuer Einwilligungen-Modul
*/
export interface EinwilligungenState {
// Data
catalog: DataPointCatalog | null
selectedDataPoints: string[]
privacyPolicy: GeneratedPrivacyPolicy | null
cookieBannerConfig: CookieBannerConfig | null
companyInfo: CompanyInfo | null
consentStatistics: ConsentStatistics | null
// UI State
activeTab: EinwilligungenTab
isLoading: boolean
isSaving: boolean
error: string | null
// Editor State
editingDataPoint: DataPoint | null
editingSection: PrivacyPolicySection | null
// Preview
previewLanguage: SupportedLanguage
previewFormat: ExportFormat
}
/**
* Actions fuer Einwilligungen-Reducer
*/
export type EinwilligungenAction =
| { type: 'SET_CATALOG'; payload: DataPointCatalog }
| { type: 'SET_SELECTED_DATA_POINTS'; payload: string[] }
| { type: 'TOGGLE_DATA_POINT'; payload: string }
| { type: 'ADD_CUSTOM_DATA_POINT'; payload: DataPoint }
| { type: 'UPDATE_DATA_POINT'; payload: { id: string; data: Partial<DataPoint> } }
| { type: 'DELETE_CUSTOM_DATA_POINT'; payload: string }
| { type: 'SET_PRIVACY_POLICY'; payload: GeneratedPrivacyPolicy }
| { type: 'SET_COOKIE_BANNER_CONFIG'; payload: CookieBannerConfig }
| { type: 'UPDATE_COOKIE_BANNER_STYLING'; payload: Partial<CookieBannerStyling> }
| { type: 'UPDATE_COOKIE_BANNER_TEXTS'; payload: Partial<CookieBannerTexts> }
| { type: 'SET_COMPANY_INFO'; payload: CompanyInfo }
| { type: 'SET_CONSENT_STATISTICS'; payload: ConsentStatistics }
| { type: 'SET_ACTIVE_TAB'; payload: EinwilligungenTab }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_SAVING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_EDITING_DATA_POINT'; payload: DataPoint | null }
| { type: 'SET_EDITING_SECTION'; payload: PrivacyPolicySection | null }
| { type: 'SET_PREVIEW_LANGUAGE'; payload: SupportedLanguage }
| { type: 'SET_PREVIEW_FORMAT'; payload: ExportFormat }
| { type: 'RESET_STATE' }
// =============================================================================
// HELPER TYPES
// =============================================================================
/**
* Kategorie-Metadaten
*/
export interface CategoryMetadata {
id: DataPointCategory
code: string // A, B, C, etc.
name: LocalizedText
description: LocalizedText
icon: string // Icon name
color: string // Tailwind color class
}
/**
* Mapping von Kategorie zu Metadaten (18 Kategorien)
*/
export const CATEGORY_METADATA: Record<DataPointCategory, CategoryMetadata> = {
MASTER_DATA: {
id: 'MASTER_DATA',
code: 'A',
name: { de: 'Stammdaten', en: 'Master Data' },
description: { de: 'Grundlegende personenbezogene Daten', en: 'Basic personal data' },
icon: 'User',
color: 'blue'
},
CONTACT_DATA: {
id: 'CONTACT_DATA',
code: 'B',
name: { de: 'Kontaktdaten', en: 'Contact Data' },
description: { de: 'Kontaktinformationen und Erreichbarkeit', en: 'Contact information and availability' },
icon: 'Mail',
color: 'sky'
},
AUTHENTICATION: {
id: 'AUTHENTICATION',
code: 'C',
name: { de: 'Authentifizierungsdaten', en: 'Authentication Data' },
description: { de: 'Daten zur Benutzeranmeldung und Session-Verwaltung', en: 'Data for user login and session management' },
icon: 'Key',
color: 'slate'
},
CONSENT: {
id: 'CONSENT',
code: 'D',
name: { de: 'Einwilligungsdaten', en: 'Consent Data' },
description: { de: 'Einwilligungen und Datenschutzpraeferenzen', en: 'Consents and privacy preferences' },
icon: 'CheckCircle',
color: 'green'
},
COMMUNICATION: {
id: 'COMMUNICATION',
code: 'E',
name: { de: 'Kommunikationsdaten', en: 'Communication Data' },
description: { de: 'Kundenservice und Kommunikationsdaten', en: 'Customer service and communication data' },
icon: 'MessageSquare',
color: 'cyan'
},
PAYMENT: {
id: 'PAYMENT',
code: 'F',
name: { de: 'Zahlungsdaten', en: 'Payment Data' },
description: { de: 'Rechnungs- und Zahlungsinformationen', en: 'Billing and payment information' },
icon: 'CreditCard',
color: 'amber'
},
USAGE_DATA: {
id: 'USAGE_DATA',
code: 'G',
name: { de: 'Nutzungsdaten', en: 'Usage Data' },
description: { de: 'Daten zur Nutzung des Dienstes', en: 'Data about service usage' },
icon: 'Activity',
color: 'violet'
},
LOCATION: {
id: 'LOCATION',
code: 'H',
name: { de: 'Standortdaten', en: 'Location Data' },
description: { de: 'Geografische Standortinformationen', en: 'Geographic location information' },
icon: 'MapPin',
color: 'emerald'
},
DEVICE_DATA: {
id: 'DEVICE_DATA',
code: 'I',
name: { de: 'Geraetedaten', en: 'Device Data' },
description: { de: 'Technische Geraete- und Browserinformationen', en: 'Technical device and browser information' },
icon: 'Smartphone',
color: 'zinc'
},
MARKETING: {
id: 'MARKETING',
code: 'J',
name: { de: 'Marketingdaten', en: 'Marketing Data' },
description: { de: 'Marketing- und Werbedaten', en: 'Marketing and advertising data' },
icon: 'Megaphone',
color: 'purple'
},
ANALYTICS: {
id: 'ANALYTICS',
code: 'K',
name: { de: 'Analysedaten', en: 'Analytics Data' },
description: { de: 'Web-Analyse und Nutzungsstatistiken', en: 'Web analytics and usage statistics' },
icon: 'BarChart3',
color: 'indigo'
},
SOCIAL_MEDIA: {
id: 'SOCIAL_MEDIA',
code: 'L',
name: { de: 'Social-Media-Daten', en: 'Social Media Data' },
description: { de: 'Daten aus sozialen Netzwerken', en: 'Data from social networks' },
icon: 'Share2',
color: 'pink'
},
HEALTH_DATA: {
id: 'HEALTH_DATA',
code: 'M',
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
description: { de: 'Besondere Kategorie nach Art. 9 DSGVO - Gesundheitsbezogene Daten', en: 'Special category under Art. 9 GDPR - Health-related data' },
icon: 'Heart',
color: 'rose'
},
EMPLOYEE_DATA: {
id: 'EMPLOYEE_DATA',
code: 'N',
name: { de: 'Beschaeftigtendaten', en: 'Employee Data' },
description: { de: 'Personalverwaltung und Arbeitnehmerinformationen (BDSG § 26)', en: 'HR management and employee information' },
icon: 'Briefcase',
color: 'orange'
},
CONTRACT_DATA: {
id: 'CONTRACT_DATA',
code: 'O',
name: { de: 'Vertragsdaten', en: 'Contract Data' },
description: { de: 'Vertragsinformationen und -dokumente', en: 'Contract information and documents' },
icon: 'FileText',
color: 'teal'
},
LOG_DATA: {
id: 'LOG_DATA',
code: 'P',
name: { de: 'Protokolldaten', en: 'Log Data' },
description: { de: 'System- und Zugriffsprotokolle', en: 'System and access logs' },
icon: 'FileCode',
color: 'gray'
},
AI_DATA: {
id: 'AI_DATA',
code: 'Q',
name: { de: 'KI-Daten', en: 'AI Data' },
description: { de: 'KI-Interaktionen, Prompts und generierte Inhalte (AI Act)', en: 'AI interactions, prompts and generated content (AI Act)' },
icon: 'Bot',
color: 'fuchsia'
},
SECURITY: {
id: 'SECURITY',
code: 'R',
name: { de: 'Sicherheitsdaten', en: 'Security Data' },
description: { de: 'Sicherheitsrelevante Daten und Vorfallberichte', en: 'Security-relevant data and incident reports' },
icon: 'Shield',
color: 'red'
}
}
/**
* Mapping von Rechtsgrundlage zu Beschreibung
*/
export const LEGAL_BASIS_INFO: Record<LegalBasis, { article: string; name: LocalizedText; description: LocalizedText }> = {
CONTRACT: {
article: 'Art. 6 Abs. 1 lit. b DSGVO',
name: { de: 'Vertragserfuellung', en: 'Contract Performance' },
description: {
de: 'Die Verarbeitung ist erforderlich fuer die Erfuellung eines Vertrags oder zur Durchfuehrung vorvertraglicher Massnahmen.',
en: 'Processing is necessary for the performance of a contract or pre-contractual measures.'
}
},
CONSENT: {
article: 'Art. 6 Abs. 1 lit. a DSGVO',
name: { de: 'Einwilligung', en: 'Consent' },
description: {
de: 'Die betroffene Person hat ihre Einwilligung zu der Verarbeitung gegeben.',
en: 'The data subject has given consent to the processing.'
}
},
EXPLICIT_CONSENT: {
article: 'Art. 9 Abs. 2 lit. a DSGVO',
name: { de: 'Ausdrueckliche Einwilligung', en: 'Explicit Consent' },
description: {
de: 'Die betroffene Person hat ausdruecklich in die Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) eingewilligt. Dies betrifft Gesundheitsdaten, biometrische Daten, Daten zur ethnischen Herkunft, politische Meinungen, religiöse Überzeugungen etc.',
en: 'The data subject has given explicit consent to the processing of special categories of personal data (Art. 9 GDPR). This includes health data, biometric data, racial or ethnic origin, political opinions, religious beliefs, etc.'
}
},
LEGITIMATE_INTEREST: {
article: 'Art. 6 Abs. 1 lit. f DSGVO',
name: { de: 'Berechtigtes Interesse', en: 'Legitimate Interest' },
description: {
de: 'Die Verarbeitung ist zur Wahrung berechtigter Interessen des Verantwortlichen erforderlich.',
en: 'Processing is necessary for legitimate interests pursued by the controller.'
}
},
LEGAL_OBLIGATION: {
article: 'Art. 6 Abs. 1 lit. c DSGVO',
name: { de: 'Rechtliche Verpflichtung', en: 'Legal Obligation' },
description: {
de: 'Die Verarbeitung ist zur Erfuellung einer rechtlichen Verpflichtung erforderlich.',
en: 'Processing is necessary for compliance with a legal obligation.'
}
},
VITAL_INTERESTS: {
article: 'Art. 6 Abs. 1 lit. d DSGVO',
name: { de: 'Lebenswichtige Interessen', en: 'Vital Interests' },
description: {
de: 'Die Verarbeitung ist erforderlich, um lebenswichtige Interessen der betroffenen Person oder einer anderen natuerlichen Person zu schuetzen.',
en: 'Processing is necessary to protect the vital interests of the data subject or another natural person.'
}
},
PUBLIC_INTEREST: {
article: 'Art. 6 Abs. 1 lit. e DSGVO',
name: { de: 'Oeffentliches Interesse', en: 'Public Interest' },
description: {
de: 'Die Verarbeitung ist fuer die Wahrnehmung einer Aufgabe erforderlich, die im oeffentlichen Interesse liegt oder in Ausuebung oeffentlicher Gewalt erfolgt.',
en: 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.'
}
}
}
/**
* Mapping von Aufbewahrungsfrist zu Beschreibung
*/
export const RETENTION_PERIOD_INFO: Record<RetentionPeriod, { label: LocalizedText; days: number | null }> = {
'24_HOURS': { label: { de: '24 Stunden', en: '24 Hours' }, days: 1 },
'30_DAYS': { label: { de: '30 Tage', en: '30 Days' }, days: 30 },
'90_DAYS': { label: { de: '90 Tage', en: '90 Days' }, days: 90 },
'12_MONTHS': { label: { de: '12 Monate', en: '12 Months' }, days: 365 },
'24_MONTHS': { label: { de: '24 Monate', en: '24 Months' }, days: 730 },
'26_MONTHS': { label: { de: '26 Monate (Google Analytics)', en: '26 Months (Google Analytics)' }, days: 790 },
'36_MONTHS': { label: { de: '36 Monate', en: '36 Months' }, days: 1095 },
'48_MONTHS': { label: { de: '48 Monate', en: '48 Months' }, days: 1460 },
'6_YEARS': { label: { de: '6 Jahre', en: '6 Years' }, days: 2190 },
'10_YEARS': { label: { de: '10 Jahre', en: '10 Years' }, days: 3650 },
'UNTIL_REVOCATION': { label: { de: 'Bis Widerruf', en: 'Until Revocation' }, days: null },
'UNTIL_PURPOSE_FULFILLED': { label: { de: 'Bis Zweckerfuellung', en: 'Until Purpose Fulfilled' }, days: null },
'UNTIL_ACCOUNT_DELETION': { label: { de: 'Bis Kontoschliessung', en: 'Until Account Deletion' }, days: null }
}
/**
* Spezielle Hinweise für Art. 9 DSGVO Kategorien
*/
export interface Article9Warning {
title: LocalizedText
description: LocalizedText
requirements: LocalizedText[]
}
export const ARTICLE_9_WARNING: Article9Warning = {
title: {
de: 'Besondere Kategorie personenbezogener Daten (Art. 9 DSGVO)',
en: 'Special Category of Personal Data (Art. 9 GDPR)'
},
description: {
de: 'Die Verarbeitung dieser Daten unterliegt besonderen Anforderungen nach Art. 9 DSGVO. Diese Daten sind besonders schuetzenswert.',
en: 'Processing of this data is subject to special requirements under Art. 9 GDPR. This data requires special protection.'
},
requirements: [
{
de: 'Ausdrueckliche Einwilligung erforderlich (Art. 9 Abs. 2 lit. a DSGVO)',
en: 'Explicit consent required (Art. 9(2)(a) GDPR)'
},
{
de: 'Separate Einwilligungserklaerung im UI notwendig',
en: 'Separate consent declaration required in UI'
},
{
de: 'Hoehere Dokumentationspflichten',
en: 'Higher documentation requirements'
},
{
de: 'Spezielle Loeschverfahren erforderlich',
en: 'Special deletion procedures required'
},
{
de: 'Datenschutz-Folgenabschaetzung (DSFA) empfohlen',
en: 'Data Protection Impact Assessment (DPIA) recommended'
}
]
}
/**
* Spezielle Hinweise für Beschäftigtendaten (BDSG § 26)
*/
export interface EmployeeDataWarning {
title: LocalizedText
description: LocalizedText
requirements: LocalizedText[]
}
export const EMPLOYEE_DATA_WARNING: EmployeeDataWarning = {
title: {
de: 'Beschaeftigtendaten (BDSG § 26)',
en: 'Employee Data (BDSG § 26)'
},
description: {
de: 'Die Verarbeitung von Beschaeftigtendaten unterliegt besonderen Anforderungen nach § 26 BDSG.',
en: 'Processing of employee data is subject to special requirements under § 26 BDSG (German Federal Data Protection Act).'
},
requirements: [
{
de: 'Aufbewahrungspflichten fuer Lohnunterlagen (6-10 Jahre)',
en: 'Retention obligations for payroll records (6-10 years)'
},
{
de: 'Betriebsrat-Beteiligung ggf. erforderlich',
en: 'Works council involvement may be required'
},
{
de: 'Verarbeitung nur fuer Zwecke des Beschaeftigungsverhaeltnisses',
en: 'Processing only for employment purposes'
},
{
de: 'Besondere Vertraulichkeit bei Gesundheitsdaten',
en: 'Special confidentiality for health data'
}
]
}
/**
* Spezielle Hinweise für KI-Daten (AI Act)
*/
export interface AIDataWarning {
title: LocalizedText
description: LocalizedText
requirements: LocalizedText[]
}
export const AI_DATA_WARNING: AIDataWarning = {
title: {
de: 'KI-Daten (AI Act)',
en: 'AI Data (AI Act)'
},
description: {
de: 'Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.',
en: 'Processing of AI-related data is subject to AI Act transparency requirements.'
},
requirements: [
{
de: 'Transparenzpflichten bei KI-Verarbeitung',
en: 'Transparency obligations for AI processing'
},
{
de: 'Kennzeichnung von KI-generierten Inhalten',
en: 'Labeling of AI-generated content'
},
{
de: 'Dokumentation der KI-Modell-Nutzung',
en: 'Documentation of AI model usage'
},
{
de: 'Keine Verwendung fuer unerlaubtes Training ohne Einwilligung',
en: 'No use for unauthorized training without consent'
}
]
}
/**
* Risk Level Styling
*/
export const RISK_LEVEL_STYLING: Record<RiskLevel, { label: LocalizedText; color: string; bgColor: string }> = {
LOW: {
label: { de: 'Niedrig', en: 'Low' },
color: 'text-green-700',
bgColor: 'bg-green-100'
},
MEDIUM: {
label: { de: 'Mittel', en: 'Medium' },
color: 'text-yellow-700',
bgColor: 'bg-yellow-100'
},
HIGH: {
label: { de: 'Hoch', en: 'High' },
color: 'text-red-700',
bgColor: 'bg-red-100'
}
}
+753
View File
@@ -0,0 +1,753 @@
/**
* SDK Export Utilities
* Handles PDF and ZIP export of SDK state and documents
*/
import jsPDF from 'jspdf'
import JSZip from 'jszip'
import { SDKState, SDK_STEPS, getStepById } from './types'
// =============================================================================
// TYPES
// =============================================================================
export interface ExportOptions {
includeEvidence?: boolean
includeDocuments?: boolean
includeRawData?: boolean
language?: 'de' | 'en'
}
const DEFAULT_OPTIONS: ExportOptions = {
includeEvidence: true,
includeDocuments: true,
includeRawData: true,
language: 'de',
}
// =============================================================================
// LABELS (German)
// =============================================================================
const LABELS_DE = {
title: 'AI Compliance SDK - Export',
subtitle: 'Compliance-Dokumentation',
generatedAt: 'Generiert am',
page: 'Seite',
summary: 'Zusammenfassung',
progress: 'Fortschritt',
phase1: 'Phase 1: Automatisches Compliance Assessment',
phase2: 'Phase 2: Dokumentengenerierung',
useCases: 'Use Cases',
risks: 'Risiken',
controls: 'Controls',
requirements: 'Anforderungen',
modules: 'Compliance-Module',
evidence: 'Nachweise',
checkpoints: 'Checkpoints',
noData: 'Keine Daten vorhanden',
status: 'Status',
completed: 'Abgeschlossen',
pending: 'Ausstehend',
inProgress: 'In Bearbeitung',
severity: 'Schweregrad',
mitigation: 'Mitigation',
description: 'Beschreibung',
category: 'Kategorie',
implementation: 'Implementierung',
}
// =============================================================================
// PDF EXPORT
// =============================================================================
function formatDate(date: Date | string | undefined): string {
if (!date) return '-'
const d = typeof date === 'string' ? new Date(date) : date
return d.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void {
const pageWidth = doc.internal.pageSize.getWidth()
// Header line
doc.setDrawColor(147, 51, 234) // Purple
doc.setLineWidth(0.5)
doc.line(20, 15, pageWidth - 20, 15)
// Title
doc.setFontSize(10)
doc.setTextColor(100)
doc.text(title, 20, 12)
// Page number
doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12)
}
function addFooter(doc: jsPDF, state: SDKState): void {
const pageWidth = doc.internal.pageSize.getWidth()
const pageHeight = doc.internal.pageSize.getHeight()
// Footer line
doc.setDrawColor(200)
doc.setLineWidth(0.3)
doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15)
// Footer text
doc.setFontSize(8)
doc.setTextColor(150)
doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10)
}
function addSectionTitle(doc: jsPDF, title: string, y: number): number {
doc.setFontSize(14)
doc.setTextColor(147, 51, 234) // Purple
doc.setFont('helvetica', 'bold')
doc.text(title, 20, y)
doc.setFont('helvetica', 'normal')
return y + 10
}
function addSubsectionTitle(doc: jsPDF, title: string, y: number): number {
doc.setFontSize(11)
doc.setTextColor(60)
doc.setFont('helvetica', 'bold')
doc.text(title, 25, y)
doc.setFont('helvetica', 'normal')
return y + 7
}
function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number {
doc.setFontSize(10)
doc.setTextColor(60)
const lines = doc.splitTextToSize(text, maxWidth)
doc.text(lines, x, y)
return y + lines.length * 5
}
function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number {
const pageHeight = doc.internal.pageSize.getHeight()
if (y + requiredSpace > pageHeight - 25) {
doc.addPage()
return 30
}
return y
}
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
const opts = { ...DEFAULT_OPTIONS, ...options }
const doc = new jsPDF()
let y = 30
const pageWidth = doc.internal.pageSize.getWidth()
// ==========================================================================
// Title Page
// ==========================================================================
// Logo/Title area
doc.setFillColor(147, 51, 234)
doc.rect(0, 0, pageWidth, 60, 'F')
doc.setFontSize(24)
doc.setTextColor(255)
doc.setFont('helvetica', 'bold')
doc.text(LABELS_DE.title, 20, 35)
doc.setFontSize(14)
doc.setFont('helvetica', 'normal')
doc.text(LABELS_DE.subtitle, 20, 48)
// Reset for content
y = 80
// Summary box
doc.setDrawColor(200)
doc.setFillColor(249, 250, 251)
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
y += 15
doc.setFontSize(12)
doc.setTextColor(60)
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
y += 10
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
y += 10
doc.text(`Version: ${state.version}`, 30, y)
y += 10
const completedSteps = state.completedSteps.length
const totalSteps = SDK_STEPS.length
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
y += 30
// Table of Contents
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
const tocItems = [
{ title: 'Zusammenfassung', page: 2 },
{ title: 'Phase 1: Compliance Assessment', page: 3 },
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
{ title: 'Risiken & Controls', page: 5 },
{ title: 'Checkpoints', page: 6 },
]
doc.setFontSize(10)
doc.setTextColor(80)
tocItems.forEach((item, idx) => {
doc.text(`${idx + 1}. ${item.title}`, 25, y)
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
y += 7
})
// ==========================================================================
// Summary Page
// ==========================================================================
doc.addPage()
y = 30
y = addSectionTitle(doc, LABELS_DE.summary, y)
// Progress overview
doc.setFillColor(249, 250, 251)
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
y += 15
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
doc.setFontSize(10)
doc.setTextColor(60)
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
y += 8
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
y += 25
// Key metrics
y = addSubsectionTitle(doc, 'Kennzahlen', y)
const metrics = [
{ label: 'Use Cases', value: state.useCases.length },
{ label: 'Risiken identifiziert', value: state.risks.length },
{ label: 'Controls definiert', value: state.controls.length },
{ label: 'Anforderungen', value: state.requirements.length },
{ label: 'Nachweise', value: state.evidence.length },
]
metrics.forEach(metric => {
doc.text(`${metric.label}: ${metric.value}`, 30, y)
y += 7
})
// ==========================================================================
// Use Cases
// ==========================================================================
y += 10
y = checkPageBreak(doc, y)
y = addSectionTitle(doc, LABELS_DE.useCases, y)
if (state.useCases.length === 0) {
y = addText(doc, LABELS_DE.noData, 25, y)
} else {
state.useCases.forEach((uc, idx) => {
y = checkPageBreak(doc, y, 50)
doc.setFillColor(249, 250, 251)
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
doc.setFontSize(11)
doc.setTextColor(40)
doc.setFont('helvetica', 'bold')
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
doc.setFont('helvetica', 'normal')
doc.setFontSize(9)
doc.setTextColor(100)
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
if (uc.description) {
y = addText(doc, uc.description, 25, y + 21, 160)
}
y += 40
})
}
// ==========================================================================
// Risks
// ==========================================================================
doc.addPage()
y = 30
y = addSectionTitle(doc, LABELS_DE.risks, y)
if (state.risks.length === 0) {
y = addText(doc, LABELS_DE.noData, 25, y)
} else {
// Sort by severity
const sortedRisks = [...state.risks].sort((a, b) => {
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
return (order[a.severity] || 4) - (order[b.severity] || 4)
})
sortedRisks.forEach((risk, idx) => {
y = checkPageBreak(doc, y, 45)
// Severity color
const severityColors: Record<string, [number, number, number]> = {
CRITICAL: [220, 38, 38],
HIGH: [234, 88, 12],
MEDIUM: [234, 179, 8],
LOW: [34, 197, 94],
}
const color = severityColors[risk.severity] || [100, 100, 100]
doc.setFillColor(color[0], color[1], color[2])
doc.rect(20, y - 3, 3, 30, 'F')
doc.setFontSize(11)
doc.setTextColor(40)
doc.setFont('helvetica', 'bold')
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
doc.setFont('helvetica', 'normal')
doc.setFontSize(9)
doc.setTextColor(100)
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
if (risk.description) {
y = addText(doc, risk.description, 28, y + 21, 155)
}
if (risk.mitigation && risk.mitigation.length > 0) {
y += 5
doc.setFontSize(9)
doc.setTextColor(34, 197, 94)
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
}
y += 15
})
}
// ==========================================================================
// Controls
// ==========================================================================
doc.addPage()
y = 30
y = addSectionTitle(doc, LABELS_DE.controls, y)
if (state.controls.length === 0) {
y = addText(doc, LABELS_DE.noData, 25, y)
} else {
state.controls.forEach((ctrl, idx) => {
y = checkPageBreak(doc, y, 35)
doc.setFillColor(249, 250, 251)
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
doc.setFontSize(10)
doc.setTextColor(40)
doc.setFont('helvetica', 'bold')
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
doc.setFont('helvetica', 'normal')
doc.setFontSize(9)
doc.setTextColor(100)
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
if (ctrl.description) {
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
}
y += 35
})
}
// ==========================================================================
// Checkpoints
// ==========================================================================
doc.addPage()
y = 30
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
const checkpointIds = Object.keys(state.checkpoints)
if (checkpointIds.length === 0) {
y = addText(doc, LABELS_DE.noData, 25, y)
} else {
checkpointIds.forEach((cpId) => {
const cp = state.checkpoints[cpId]
y = checkPageBreak(doc, y, 25)
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
doc.circle(25, y + 2, 3, 'F')
doc.setFontSize(10)
doc.setTextColor(40)
doc.text(cpId, 35, y + 5)
doc.setFontSize(9)
doc.setTextColor(100)
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
if (cp.errors && cp.errors.length > 0) {
doc.setTextColor(220, 38, 38)
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
y += 7
}
y += 20
})
}
// ==========================================================================
// Add page numbers
// ==========================================================================
const pageCount = doc.getNumberOfPages()
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i)
if (i > 1) {
addHeader(doc, LABELS_DE.title, i, pageCount)
}
addFooter(doc, state)
}
return doc.output('blob')
}
// =============================================================================
// ZIP EXPORT
// =============================================================================
export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
const opts = { ...DEFAULT_OPTIONS, ...options }
const zip = new JSZip()
// Create folder structure
const rootFolder = zip.folder('ai-compliance-sdk-export')
if (!rootFolder) throw new Error('Failed to create ZIP folder')
const phase1Folder = rootFolder.folder('phase1-assessment')
const phase2Folder = rootFolder.folder('phase2-documents')
const dataFolder = rootFolder.folder('data')
// ==========================================================================
// Main State JSON
// ==========================================================================
if (opts.includeRawData && dataFolder) {
dataFolder.file('state.json', JSON.stringify(state, null, 2))
}
// ==========================================================================
// README
// ==========================================================================
const readmeContent = `# AI Compliance SDK Export
Generated: ${formatDate(new Date())}
Tenant: ${state.tenantId}
Version: ${state.version}
## Folder Structure
- **phase1-assessment/**: Compliance Assessment Ergebnisse
- use-cases.json: Alle Use Cases
- risks.json: Identifizierte Risiken
- controls.json: Definierte Controls
- requirements.json: Compliance-Anforderungen
- **phase2-documents/**: Generierte Dokumente
- dsfa.json: Datenschutz-Folgenabschaetzung
- toms.json: Technische und organisatorische Massnahmen
- vvt.json: Verarbeitungsverzeichnis
- documents.json: Rechtliche Dokumente
- **data/**: Rohdaten
- state.json: Kompletter SDK State
## Progress
Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed
Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed
## Key Metrics
- Use Cases: ${state.useCases.length}
- Risks: ${state.risks.length}
- Controls: ${state.controls.length}
- Requirements: ${state.requirements.length}
- Evidence: ${state.evidence.length}
`
rootFolder.file('README.md', readmeContent)
// ==========================================================================
// Phase 1 Files
// ==========================================================================
if (phase1Folder) {
// Use Cases
phase1Folder.file('use-cases.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.useCases.length,
useCases: state.useCases,
}, null, 2))
// Risks
phase1Folder.file('risks.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.risks.length,
risks: state.risks,
summary: {
critical: state.risks.filter(r => r.severity === 'CRITICAL').length,
high: state.risks.filter(r => r.severity === 'HIGH').length,
medium: state.risks.filter(r => r.severity === 'MEDIUM').length,
low: state.risks.filter(r => r.severity === 'LOW').length,
},
}, null, 2))
// Controls
phase1Folder.file('controls.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.controls.length,
controls: state.controls,
}, null, 2))
// Requirements
phase1Folder.file('requirements.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.requirements.length,
requirements: state.requirements,
}, null, 2))
// Modules
phase1Folder.file('modules.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.modules.length,
modules: state.modules,
}, null, 2))
// Evidence
if (opts.includeEvidence) {
phase1Folder.file('evidence.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.evidence.length,
evidence: state.evidence,
}, null, 2))
}
// Checkpoints
phase1Folder.file('checkpoints.json', JSON.stringify({
exportedAt: new Date().toISOString(),
checkpoints: state.checkpoints,
}, null, 2))
// Screening
if (state.screening) {
phase1Folder.file('screening.json', JSON.stringify({
exportedAt: new Date().toISOString(),
screening: state.screening,
}, null, 2))
}
}
// ==========================================================================
// Phase 2 Files
// ==========================================================================
if (phase2Folder) {
// DSFA
if (state.dsfa) {
phase2Folder.file('dsfa.json', JSON.stringify({
exportedAt: new Date().toISOString(),
dsfa: state.dsfa,
}, null, 2))
}
// TOMs
phase2Folder.file('toms.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.toms.length,
toms: state.toms,
}, null, 2))
// VVT (Processing Activities)
phase2Folder.file('vvt.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.vvt.length,
processingActivities: state.vvt,
}, null, 2))
// Legal Documents
if (opts.includeDocuments) {
phase2Folder.file('documents.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.documents.length,
documents: state.documents,
}, null, 2))
}
// Cookie Banner Config
if (state.cookieBanner) {
phase2Folder.file('cookie-banner.json', JSON.stringify({
exportedAt: new Date().toISOString(),
config: state.cookieBanner,
}, null, 2))
}
// Retention Policies
phase2Folder.file('retention-policies.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.retentionPolicies.length,
policies: state.retentionPolicies,
}, null, 2))
// AI Act Classification
if (state.aiActClassification) {
phase2Folder.file('ai-act-classification.json', JSON.stringify({
exportedAt: new Date().toISOString(),
classification: state.aiActClassification,
}, null, 2))
}
// Obligations
phase2Folder.file('obligations.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.obligations.length,
obligations: state.obligations,
}, null, 2))
// Consent Records
phase2Folder.file('consents.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.consents.length,
consents: state.consents,
}, null, 2))
// DSR Config
if (state.dsrConfig) {
phase2Folder.file('dsr-config.json', JSON.stringify({
exportedAt: new Date().toISOString(),
config: state.dsrConfig,
}, null, 2))
}
// Escalation Workflows
phase2Folder.file('escalation-workflows.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.escalationWorkflows.length,
workflows: state.escalationWorkflows,
}, null, 2))
}
// ==========================================================================
// Security Data
// ==========================================================================
if (dataFolder) {
if (state.sbom) {
dataFolder.file('sbom.json', JSON.stringify({
exportedAt: new Date().toISOString(),
sbom: state.sbom,
}, null, 2))
}
if (state.securityIssues.length > 0) {
dataFolder.file('security-issues.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.securityIssues.length,
issues: state.securityIssues,
}, null, 2))
}
if (state.securityBacklog.length > 0) {
dataFolder.file('security-backlog.json', JSON.stringify({
exportedAt: new Date().toISOString(),
count: state.securityBacklog.length,
backlog: state.securityBacklog,
}, null, 2))
}
}
// ==========================================================================
// Generate PDF and include in ZIP
// ==========================================================================
try {
const pdfBlob = await exportToPDF(state, options)
const pdfArrayBuffer = await pdfBlob.arrayBuffer()
rootFolder.file('compliance-report.pdf', pdfArrayBuffer)
} catch (error) {
console.error('Failed to generate PDF for ZIP:', error)
// Continue without PDF
}
// Generate ZIP
return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' })
}
// =============================================================================
// EXPORT HELPER
// =============================================================================
export async function downloadExport(
state: SDKState,
format: 'json' | 'pdf' | 'zip',
options: ExportOptions = {}
): Promise<void> {
let blob: Blob
let filename: string
const timestamp = new Date().toISOString().slice(0, 10)
switch (format) {
case 'json':
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
filename = `ai-compliance-sdk-${timestamp}.json`
break
case 'pdf':
blob = await exportToPDF(state, options)
filename = `ai-compliance-sdk-${timestamp}.pdf`
break
case 'zip':
blob = await exportToZIP(state, options)
filename = `ai-compliance-sdk-${timestamp}.zip`
break
default:
throw new Error(`Unknown export format: ${format}`)
}
// Create download link
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
+10
View File
@@ -0,0 +1,10 @@
/**
* SDK - Simplified for Admin-Lehrer
*
* Only exports catalog-manager related functionality.
* Full SDK context replaced with simplified version.
*/
// Context & Provider (simplified - catalog-manager only)
export { SDKProvider, useSDK, SDKContext, initialState } from './context'
export type { SDKState, SDKAction } from './context'
@@ -0,0 +1,578 @@
/**
* Loeschfristen Baseline-Katalog
*
* 18 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
* Datenobjekte in deutschen Unternehmen. Basierend auf AO, HGB,
* UStG, BGB, ArbZG, AGG, BDSG und BSIG.
*
* Werden genutzt, um neue Loeschfrist-Policies schnell aus
* bewaehrten Vorlagen zu erstellen.
*/
import type {
LoeschfristPolicy,
RetentionDriverType,
DeletionMethodType,
StorageLocation,
PolicyStatus,
ReviewInterval,
RetentionUnit,
DeletionTriggerLevel,
} from './loeschfristen-types'
import { createEmptyPolicy } from './loeschfristen-types'
// =============================================================================
// BASELINE TEMPLATE INTERFACE
// =============================================================================
export interface BaselineTemplate {
templateId: string
dataObjectName: string
description: string
affectedGroups: string[]
dataCategories: string[]
primaryPurpose: string
deletionTrigger: DeletionTriggerLevel
retentionDriver: RetentionDriverType | null
retentionDriverDetail: string
retentionDuration: number | null
retentionUnit: RetentionUnit | null
retentionDescription: string
startEvent: string
deletionMethod: DeletionMethodType
deletionMethodDetail: string
responsibleRole: string
reviewInterval: ReviewInterval
tags: string[]
}
// =============================================================================
// BASELINE TEMPLATES (18 Vorlagen)
// =============================================================================
export const BASELINE_TEMPLATES: BaselineTemplate[] = [
// ==================== 1. Personalakten ====================
{
templateId: 'personal-akten',
dataObjectName: 'Personalakten',
description:
'Vollstaendige Personalakten inkl. Arbeitsvertraege, Zeugnisse, Abmahnungen und sonstige beschaeftigungsrelevante Dokumente.',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Stammdaten', 'Vertragsdaten', 'Gehaltsdaten', 'Zeugnisse'],
primaryPurpose:
'Dokumentation und Nachweisfuehrung des Beschaeftigungsverhaeltnisses sowie Erfuellung steuerrechtlicher Aufbewahrungspflichten.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'AO_147',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 147 AO fuer steuerlich relevante Unterlagen der Personalakte.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
startEvent: 'Ende des Beschaeftigungsverhaeltnisses',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung aller digitalen Personalakten-Dokumente nach Ablauf der Aufbewahrungsfrist. Papierakten werden datenschutzkonform vernichtet.',
responsibleRole: 'HR-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['hr', 'steuer'],
},
// ==================== 2. Buchhaltungsbelege ====================
{
templateId: 'buchhaltungsbelege',
dataObjectName: 'Buchhaltungsbelege',
description:
'Buchungsbelege, Kontoauszuege, Kassenbuecher und sonstige Belege der laufenden Buchhaltung.',
affectedGroups: ['Kunden', 'Lieferanten'],
dataCategories: ['Finanzdaten', 'Transaktionsdaten', 'Kontodaten'],
primaryPurpose:
'Ordnungsgemaesse Buchfuehrung und Erfuellung handelsrechtlicher Aufbewahrungspflichten nach HGB.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 257 HGB fuer Handelsbuecher und Buchungsbelege.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre nach Ende des Geschaeftsjahres',
startEvent: 'Ende des Geschaeftsjahres',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch die Buchhaltung vor Loeschung, um sicherzustellen, dass keine laufenden Pruefungen oder Rechtsstreitigkeiten bestehen.',
responsibleRole: 'Buchhaltung',
reviewInterval: 'ANNUAL',
tags: ['finanzen', 'hgb'],
},
// ==================== 3. Rechnungen ====================
{
templateId: 'rechnungen',
dataObjectName: 'Rechnungen',
description:
'Eingangs- und Ausgangsrechnungen inkl. Rechnungsanhaenge und rechnungsbegruendende Unterlagen.',
affectedGroups: ['Kunden', 'Lieferanten'],
dataCategories: ['Rechnungsdaten', 'Umsatzsteuerdaten', 'Adressdaten'],
primaryPurpose:
'Dokumentation umsatzsteuerrelevanter Vorgaenge und Erfuellung der Aufbewahrungspflicht nach UStG.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'USTG_14B',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 14b UStG fuer Rechnungen und rechnungsbegruendende Unterlagen.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre ab Rechnungsdatum',
startEvent: 'Rechnungsdatum',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung nach Ablauf der 10-Jahres-Frist. Vor Loeschung wird geprueft, ob Rechnungen in laufenden Betriebspruefungen benoetigt werden.',
responsibleRole: 'Buchhaltung',
reviewInterval: 'ANNUAL',
tags: ['finanzen', 'ustg'],
},
// ==================== 4. Geschaeftsbriefe ====================
{
templateId: 'geschaeftsbriefe',
dataObjectName: 'Geschaeftsbriefe',
description:
'Empfangene und versandte Handelsbriefe, Geschaeftskorrespondenz und geschaeftsrelevante E-Mails.',
affectedGroups: ['Kunden', 'Lieferanten'],
dataCategories: ['Korrespondenz', 'Vertragskommunikation', 'Angebote'],
primaryPurpose:
'Nachweisfuehrung geschaeftlicher Kommunikation und Erfuellung der handelsrechtlichen Aufbewahrungspflicht fuer Handelsbriefe.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 257 HGB fuer empfangene und versandte Handelsbriefe (6 Jahre).',
retentionDuration: 6,
retentionUnit: 'YEARS',
retentionDescription: '6 Jahre ab Eingang oder Versand des Geschaeftsbriefes',
startEvent: 'Eingang bzw. Versand des Geschaeftsbriefes',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch die Geschaeftsleitung, da Geschaeftsbriefe ggf. als Beweismittel in Rechtsstreitigkeiten dienen koennen.',
responsibleRole: 'Geschaeftsleitung',
reviewInterval: 'ANNUAL',
tags: ['kommunikation', 'hgb'],
},
// ==================== 5. Bewerbungsunterlagen ====================
{
templateId: 'bewerbungsunterlagen',
dataObjectName: 'Bewerbungsunterlagen',
description:
'Eingereichte Bewerbungsunterlagen inkl. Anschreiben, Lebenslauf, Zeugnisse und Korrespondenz mit Bewerbern.',
affectedGroups: ['Bewerber'],
dataCategories: ['Bewerbungsdaten', 'Qualifikationen', 'Kontaktdaten'],
primaryPurpose:
'Durchfuehrung des Bewerbungsverfahrens und Absicherung gegen Entschaedigungsansprueche nach dem AGG.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'AGG_15',
retentionDriverDetail:
'Aufbewahrung fuer 6 Monate nach Absage gemaess 15 Abs. 4 AGG (Frist fuer Geltendmachung von Entschaedigungsanspruechen).',
retentionDuration: 6,
retentionUnit: 'MONTHS',
retentionDescription: '6 Monate nach Absage oder Stellenbesetzung',
startEvent: 'Absage oder endgueltige Stellenbesetzung',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung aller Bewerbungsunterlagen und zugehoeriger Kommunikation nach Ablauf der 6-Monats-Frist.',
responsibleRole: 'HR-Abteilung',
reviewInterval: 'QUARTERLY',
tags: ['hr', 'bewerbung'],
},
// ==================== 6. Kundenstammdaten ====================
{
templateId: 'kundenstammdaten',
dataObjectName: 'Kundenstammdaten',
description:
'Stammdaten von Kunden inkl. Kontaktdaten, Anschrift, Kundennummer und Kommunikationspraeferenzen.',
affectedGroups: ['Kunden'],
dataCategories: ['Stammdaten', 'Kontaktdaten', 'Adressdaten'],
primaryPurpose:
'Pflege der Kundenbeziehung, Vertragserfuellung und Absicherung gegen Verjaehrung vertraglicher Ansprueche.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BGB_195',
retentionDriverDetail:
'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB (3 Jahre).',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach letzter geschaeftlicher Interaktion',
startEvent: 'Letzte geschaeftliche Interaktion mit dem Kunden',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch den Vertrieb vor Loeschung, um sicherzustellen, dass keine aktiven Geschaeftsbeziehungen oder offenen Forderungen bestehen.',
responsibleRole: 'Vertrieb',
reviewInterval: 'ANNUAL',
tags: ['crm', 'kunden'],
},
// ==================== 7. Newsletter-Einwilligungen ====================
{
templateId: 'newsletter-einwilligungen',
dataObjectName: 'Newsletter-Einwilligungen',
description:
'Einwilligungserklaerungen fuer den Newsletter-Versand inkl. Double-Opt-in-Nachweis und Abmeldezeitpunkt.',
affectedGroups: ['Abonnenten'],
dataCategories: ['Einwilligungsdaten', 'E-Mail-Adresse', 'Opt-in-Nachweis'],
primaryPurpose:
'Nachweis der wirksamen Einwilligung zum Newsletter-Versand gemaess Art. 7 DSGVO und Dokumentation des Widerrufs.',
deletionTrigger: 'PURPOSE_END',
retentionDriver: null,
retentionDriverDetail:
'Keine gesetzliche Aufbewahrungspflicht. Daten werden bis zum Widerruf der Einwilligung gespeichert.',
retentionDuration: null,
retentionUnit: null,
retentionDescription: 'Bis zum Widerruf der Einwilligung durch den Abonnenten',
startEvent: 'Widerruf der Einwilligung durch den Abonnenten',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung der personenbezogenen Daten nach Eingang des Widerrufs. Der Einwilligungsnachweis selbst wird fuer die Dauer der Nachweispflicht aufbewahrt.',
responsibleRole: 'Marketing',
reviewInterval: 'SEMI_ANNUAL',
tags: ['marketing', 'einwilligung'],
},
// ==================== 8. Webserver-Logs ====================
{
templateId: 'webserver-logs',
dataObjectName: 'Webserver-Logs',
description:
'Server-Zugriffsprotokolle inkl. IP-Adressen, Zeitstempel, aufgerufene URLs und HTTP-Statuscodes.',
affectedGroups: ['Website-Besucher'],
dataCategories: ['IP-Adressen', 'Zugriffszeitpunkte', 'User-Agent-Daten'],
primaryPurpose:
'Sicherstellung der IT-Sicherheit, Erkennung von Angriffen und Stoerungen sowie Erfuellung der Protokollierungspflicht nach BSIG.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BSIG',
retentionDriverDetail:
'Aufbewahrung gemaess BSI-Gesetz / IT-Sicherheitsgesetz 2.0 fuer die Analyse von Sicherheitsvorfaellen.',
retentionDuration: 7,
retentionUnit: 'DAYS',
retentionDescription: '7 Tage nach Zeitpunkt des Zugriffs',
startEvent: 'Zeitpunkt des Server-Zugriffs',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Rotation und Loeschung der Logdateien nach 7 Tagen durch den Webserver (logrotate).',
responsibleRole: 'IT-Abteilung',
reviewInterval: 'QUARTERLY',
tags: ['it', 'logs'],
},
// ==================== 9. Videoueberwachung ====================
{
templateId: 'videoueberwachung',
dataObjectName: 'Videoueberwachung',
description:
'Aufnahmen der Videoueberwachung in Geschaeftsraeumen, Eingangsbereichen und Parkplaetzen.',
affectedGroups: ['Besucher', 'Mitarbeiter'],
dataCategories: ['Videodaten', 'Bilddaten', 'Zeitstempel'],
primaryPurpose:
'Schutz des Eigentums und der Sicherheit von Personen sowie Aufklaerung von Vorfaellen in den ueberwachten Bereichen.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BDSG_35',
retentionDriverDetail:
'Unverzuegliche Loeschung nach Zweckwegfall gemaess 35 BDSG bzw. Art. 17 DSGVO. Maximale Speicherdauer 48 Stunden.',
retentionDuration: 2,
retentionUnit: 'DAYS',
retentionDescription: '48 Stunden (2 Tage) nach Aufnahmezeitpunkt',
startEvent: 'Aufnahmezeitpunkt der Videosequenz',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatisches Ueberschreiben der Aufnahmen durch das Videomanagementsystem nach Ablauf der 48-Stunden-Frist.',
responsibleRole: 'Facility Management',
reviewInterval: 'QUARTERLY',
tags: ['sicherheit', 'video'],
},
// ==================== 10. Gehaltsabrechnungen ====================
{
templateId: 'gehaltsabrechnungen',
dataObjectName: 'Gehaltsabrechnungen',
description:
'Monatliche Gehaltsabrechnungen, Lohnsteuerbescheinigungen und Sozialversicherungsmeldungen.',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Gehaltsdaten', 'Steuerdaten', 'Sozialversicherungsdaten'],
primaryPurpose:
'Dokumentation der Lohn- und Gehaltszahlungen sowie Erfuellung steuerrechtlicher und sozialversicherungsrechtlicher Aufbewahrungspflichten.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'AO_147',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 147 AO fuer lohnsteuerrelevante Unterlagen und Gehaltsbuchungen.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre nach Ende des Geschaeftsjahres',
startEvent: 'Ende des Geschaeftsjahres der jeweiligen Abrechnung',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung der digitalen Gehaltsabrechnungen nach Ablauf der Aufbewahrungsfrist. Papierbelege werden datenschutzkonform vernichtet.',
responsibleRole: 'Lohnbuchhaltung',
reviewInterval: 'ANNUAL',
tags: ['hr', 'steuer'],
},
// ==================== 11. Vertraege ====================
{
templateId: 'vertraege',
dataObjectName: 'Vertraege',
description:
'Geschaeftsvertraege, Rahmenvereinbarungen, Dienstleistungsvertraege und zugehoerige Anlagen und Nachtraege.',
affectedGroups: ['Vertragspartner'],
dataCategories: ['Vertragsdaten', 'Kontaktdaten', 'Konditionen'],
primaryPurpose:
'Dokumentation vertraglicher Vereinbarungen und Sicherung von Beweismitteln fuer die Dauer moeglicher Rechtsstreitigkeiten.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 257 HGB fuer handelsrechtlich relevante Vertragsunterlagen.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre nach Ende der Vertragslaufzeit',
startEvent: 'Ende der Vertragslaufzeit bzw. Vertragsbeendigung',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch die Rechtsabteilung vor Loeschung, um sicherzustellen, dass keine laufenden oder angedrohten Rechtsstreitigkeiten bestehen.',
responsibleRole: 'Rechtsabteilung',
reviewInterval: 'ANNUAL',
tags: ['recht', 'vertraege'],
},
// ==================== 12. Zeiterfassungsdaten ====================
{
templateId: 'zeiterfassung',
dataObjectName: 'Zeiterfassungsdaten',
description:
'Arbeitszeitaufzeichnungen inkl. Beginn, Ende, Pausen und Ueberstunden der Beschaeftigten.',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Arbeitszeiten', 'Pausenzeiten', 'Ueberstunden'],
primaryPurpose:
'Erfuellung der gesetzlichen Aufzeichnungspflicht fuer Arbeitszeiten und Nachweis der Einhaltung des Arbeitszeitgesetzes.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'ARBZG_16',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 16 Abs. 2 ArbZG fuer Aufzeichnungen ueber die Arbeitszeit.',
retentionDuration: 2,
retentionUnit: 'YEARS',
retentionDescription: '2 Jahre nach Ende des Erfassungszeitraums',
startEvent: 'Ende des jeweiligen Erfassungszeitraums',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung der Zeiterfassungsdaten nach Ablauf der 2-Jahres-Frist im Zeiterfassungssystem.',
responsibleRole: 'HR-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['hr', 'arbzg'],
},
// ==================== 13. Krankmeldungen ====================
{
templateId: 'krankmeldungen',
dataObjectName: 'Krankmeldungen',
description:
'Arbeitsunfaehigkeitsbescheinigungen, Krankmeldungen und zugehoerige Abwesenheitsdokumentationen.',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Gesundheitsdaten', 'Abwesenheitszeiten', 'AU-Bescheinigungen'],
primaryPurpose:
'Dokumentation von Fehlzeiten, Entgeltfortzahlung im Krankheitsfall und Absicherung gegen Verjaehrung arbeitsrechtlicher Ansprueche.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BGB_195',
retentionDriverDetail:
'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB zur Absicherung von Erstattungsanspruechen.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
startEvent: 'Ende des Beschaeftigungsverhaeltnisses',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch die HR-Abteilung vor Loeschung, da Krankmeldungen besondere Kategorien personenbezogener Daten (Gesundheitsdaten) enthalten.',
responsibleRole: 'HR-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['hr', 'gesundheit'],
},
// ==================== 14. Steuererklaerungen ====================
{
templateId: 'steuererklaerungen',
dataObjectName: 'Steuererklaerungen',
description:
'Koerperschaftsteuer-, Gewerbesteuer- und Umsatzsteuererklaerungen inkl. Anlagen und Bescheide.',
affectedGroups: ['Unternehmen'],
dataCategories: ['Steuerdaten', 'Finanzkennzahlen', 'Bescheide'],
primaryPurpose:
'Erfuellung steuerrechtlicher Dokumentationspflichten und Nachweisfuehrung gegenueber den Finanzbehoerden.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'AO_147',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 147 AO fuer Steuererklaerungen und zugehoerige Unterlagen.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre ab dem jeweiligen Steuerjahr',
startEvent: 'Ende des betreffenden Steuerjahres',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung nach Ablauf der 10-Jahres-Frist, sofern keine laufende Betriebspruefung oder Einspruchsverfahren vorliegen.',
responsibleRole: 'Steuerberater/Buchhaltung',
reviewInterval: 'ANNUAL',
tags: ['finanzen', 'steuer'],
},
// ==================== 15. Gesellschafterprotokolle ====================
{
templateId: 'protokolle-gesellschafter',
dataObjectName: 'Gesellschafterprotokolle',
description:
'Protokolle der Gesellschafterversammlungen, Beschluesse, Abstimmungsergebnisse und notarielle Urkunden.',
affectedGroups: ['Gesellschafter'],
dataCategories: ['Beschlussdaten', 'Abstimmungsergebnisse', 'Protokolle'],
primaryPurpose:
'Dokumentation gesellschaftsrechtlicher Beschluesse und Erfuellung handelsrechtlicher Aufbewahrungspflichten.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 257 HGB fuer Eroeffnungsbilanzen, Jahresabschluesse und zugehoerige Beschluesse.',
retentionDuration: 10,
retentionUnit: 'YEARS',
retentionDescription: '10 Jahre ab Beschlussdatum',
startEvent: 'Datum des jeweiligen Gesellschafterbeschlusses',
deletionMethod: 'PHYSICAL_DESTROY',
deletionMethodDetail:
'Physische Vernichtung der Papieroriginale durch zertifizierten Aktenvernichtungsdienstleister (DIN 66399, Sicherheitsstufe P-4). Digitale Kopien werden parallel geloescht.',
responsibleRole: 'Geschaeftsleitung',
reviewInterval: 'ANNUAL',
tags: ['recht', 'gesellschaft'],
},
// ==================== 16. CRM-Kontakthistorie ====================
{
templateId: 'crm-kontakthistorie',
dataObjectName: 'CRM-Kontakthistorie',
description:
'Kontaktverlauf im CRM-System inkl. Anrufe, E-Mails, Termine, Notizen und Angebotsverlauf.',
affectedGroups: ['Kunden', 'Interessenten'],
dataCategories: ['Kommunikationsdaten', 'Interaktionshistorie', 'Angebotsdaten'],
primaryPurpose:
'Pflege der Kundenbeziehung und Nachverfolgung geschaeftlicher Interaktionen fuer Vertriebs- und Servicezwecke.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BGB_195',
retentionDriverDetail:
'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB zur Absicherung vertraglicher Ansprueche.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach letztem Kontakt mit dem Kunden oder Interessenten',
startEvent: 'Letzter dokumentierter Kontakt im CRM-System',
deletionMethod: 'ANONYMIZATION',
deletionMethodDetail:
'Anonymisierung der personenbezogenen Daten im CRM-System, sodass statistische Auswertungen weiterhin moeglich sind, aber kein Personenbezug mehr hergestellt werden kann.',
responsibleRole: 'Vertrieb',
reviewInterval: 'SEMI_ANNUAL',
tags: ['crm', 'kunden'],
},
// ==================== 17. Backup-Daten ====================
{
templateId: 'backup-daten',
dataObjectName: 'Backup-Daten',
description:
'Vollstaendige und inkrementelle Sicherungskopien aller Systeme, Datenbanken und Dateisysteme.',
affectedGroups: ['Alle Betroffenengruppen'],
dataCategories: ['Systemsicherungen', 'Datenbankkopien', 'Dateisystemsicherungen'],
primaryPurpose:
'Sicherstellung der Datenwiederherstellung im Katastrophenfall und Gewaehrleistung der Geschaeftskontinuitaet.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BSIG',
retentionDriverDetail:
'Aufbewahrung von Backups fuer 90 Tage gemaess BSI-Grundschutz-Empfehlungen zur Sicherstellung der Wiederherstellbarkeit.',
retentionDuration: 90,
retentionUnit: 'DAYS',
retentionDescription: '90 Tage nach Erstellung des Backups',
startEvent: 'Erstellungsdatum des jeweiligen Backups',
deletionMethod: 'CRYPTO_ERASE',
deletionMethodDetail:
'Kryptographische Loeschung durch Vernichtung der Verschluesselungsschluessel, sodass die verschluesselten Backup-Daten nicht mehr entschluesselt werden koennen.',
responsibleRole: 'IT-Abteilung',
reviewInterval: 'QUARTERLY',
tags: ['it', 'backup'],
},
// ==================== 18. Cookie-Consent-Nachweise ====================
{
templateId: 'cookie-consent-logs',
dataObjectName: 'Cookie-Consent-Nachweise',
description:
'Nachweise ueber Cookie-Einwilligungen der Website-Besucher inkl. Consent-ID, Zeitstempel, gesetzte Praeferenzen und IP-Adresse.',
affectedGroups: ['Website-Besucher'],
dataCategories: ['Consent-Daten', 'IP-Adressen', 'Zeitstempel', 'Praeferenzen'],
primaryPurpose:
'Nachweisfuehrung der Einwilligung in die Cookie-Nutzung gemaess Art. 7 Abs. 1 DSGVO und ePrivacy-Richtlinie.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BGB_195',
retentionDriverDetail:
'Aufbewahrung der Consent-Nachweise fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB zur Absicherung gegen Abmahnungen.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach Zeitpunkt der Einwilligung',
startEvent: 'Zeitpunkt der Cookie-Einwilligung (Consent-Zeitstempel)',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung der Consent-Nachweise nach Ablauf der 3-Jahres-Frist durch das Consent-Management-System.',
responsibleRole: 'Datenschutzbeauftragter',
reviewInterval: 'ANNUAL',
tags: ['datenschutz', 'consent'],
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Erstellt eine vollstaendige LoeschfristPolicy aus einem BaselineTemplate.
* Nutzt createEmptyPolicy() als Basis und ueberlagert die Template-Felder.
*/
export function templateToPolicy(template: BaselineTemplate): LoeschfristPolicy {
const base = createEmptyPolicy()
return {
...base,
dataObjectName: template.dataObjectName,
description: template.description,
affectedGroups: [...template.affectedGroups],
dataCategories: [...template.dataCategories],
primaryPurpose: template.primaryPurpose,
deletionTrigger: template.deletionTrigger,
retentionDriver: template.retentionDriver,
retentionDriverDetail: template.retentionDriverDetail,
retentionDuration: template.retentionDuration,
retentionUnit: template.retentionUnit,
retentionDescription: template.retentionDescription,
startEvent: template.startEvent,
deletionMethod: template.deletionMethod,
deletionMethodDetail: template.deletionMethodDetail,
responsibleRole: template.responsibleRole,
reviewInterval: template.reviewInterval,
tags: [...template.tags],
}
}
/**
* Gibt alle Templates zurueck, die einen bestimmten Tag enthalten.
*/
export function getTemplatesByTag(tag: string): BaselineTemplate[] {
return BASELINE_TEMPLATES.filter(t => t.tags.includes(tag))
}
/**
* Findet ein Template anhand seiner templateId.
*/
export function getTemplateById(templateId: string): BaselineTemplate | undefined {
return BASELINE_TEMPLATES.find(t => t.templateId === templateId)
}
/**
* Gibt alle im Katalog verwendeten Tags als sortierte Liste zurueck.
*/
export function getAllTemplateTags(): string[] {
const tags = new Set<string>()
BASELINE_TEMPLATES.forEach(t => t.tags.forEach(tag => tags.add(tag)))
return Array.from(tags).sort()
}
@@ -0,0 +1,325 @@
// =============================================================================
// Loeschfristen Module - Compliance Check Engine
// Prueft Policies auf Vollstaendigkeit, Konsistenz und DSGVO-Konformitaet
// =============================================================================
import {
LoeschfristPolicy,
PolicyStatus,
isPolicyOverdue,
getActiveLegalHolds,
} from './loeschfristen-types'
// =============================================================================
// TYPES
// =============================================================================
export type ComplianceIssueType =
| 'MISSING_TRIGGER'
| 'MISSING_LEGAL_BASIS'
| 'OVERDUE_REVIEW'
| 'NO_RESPONSIBLE'
| 'LEGAL_HOLD_CONFLICT'
| 'STALE_DRAFT'
| 'UNCOVERED_VVT_CATEGORY'
export type ComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
export interface ComplianceIssue {
id: string
policyId: string
policyName: string
type: ComplianceIssueType
severity: ComplianceIssueSeverity
title: string
description: string
recommendation: string
}
export interface ComplianceCheckResult {
issues: ComplianceIssue[]
score: number // 0-100
stats: {
total: number
passed: number
failed: number
bySeverity: Record<ComplianceIssueSeverity, number>
}
}
// =============================================================================
// HELPERS
// =============================================================================
let issueCounter = 0
function createIssueId(): string {
issueCounter++
return `CI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
}
function createIssue(
policy: LoeschfristPolicy,
type: ComplianceIssueType,
severity: ComplianceIssueSeverity,
title: string,
description: string,
recommendation: string
): ComplianceIssue {
return {
id: createIssueId(),
policyId: policy.policyId,
policyName: policy.dataObjectName || policy.policyId,
type,
severity,
title,
description,
recommendation,
}
}
function daysBetween(dateStr: string, now: Date): number {
const date = new Date(dateStr)
const diffMs = now.getTime() - date.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
}
// =============================================================================
// INDIVIDUAL CHECKS
// =============================================================================
/**
* Check 1: MISSING_TRIGGER (HIGH)
* Policy has no deletionTrigger set, or trigger is PURPOSE_END but no startEvent defined.
*/
function checkMissingTrigger(policy: LoeschfristPolicy): ComplianceIssue | null {
if (!policy.deletionTrigger) {
return createIssue(
policy,
'MISSING_TRIGGER',
'HIGH',
'Kein Loeschtrigger definiert',
`Die Policy "${policy.dataObjectName}" hat keinen Loeschtrigger gesetzt. Ohne Trigger ist unklar, wann die Daten geloescht werden.`,
'Definieren Sie einen Loeschtrigger (Zweckende, Aufbewahrungspflicht oder Legal Hold) fuer diese Policy.'
)
}
if (policy.deletionTrigger === 'PURPOSE_END' && !policy.startEvent.trim()) {
return createIssue(
policy,
'MISSING_TRIGGER',
'HIGH',
'Zweckende ohne Startereignis',
`Die Policy "${policy.dataObjectName}" nutzt "Zweckende" als Trigger, hat aber kein Startereignis definiert. Ohne Startereignis laesst sich der Loeschzeitpunkt nicht berechnen.`,
'Definieren Sie ein konkretes Startereignis (z.B. "Vertragsende", "Abmeldung", "Projektabschluss").'
)
}
return null
}
/**
* Check 2: MISSING_LEGAL_BASIS (HIGH)
* Policy with RETENTION_DRIVER trigger but no retentionDriver set.
*/
function checkMissingLegalBasis(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.deletionTrigger === 'RETENTION_DRIVER' && !policy.retentionDriver) {
return createIssue(
policy,
'MISSING_LEGAL_BASIS',
'HIGH',
'Aufbewahrungspflicht ohne Rechtsgrundlage',
`Die Policy "${policy.dataObjectName}" hat "Aufbewahrungspflicht" als Trigger, aber keinen konkreten Aufbewahrungstreiber (z.B. AO 147, HGB 257) zugeordnet.`,
'Waehlen Sie den passenden gesetzlichen Aufbewahrungstreiber aus oder wechseln Sie den Trigger-Typ.'
)
}
return null
}
/**
* Check 3: OVERDUE_REVIEW (MEDIUM)
* Policy where nextReviewDate is in the past.
*/
function checkOverdueReview(policy: LoeschfristPolicy): ComplianceIssue | null {
if (isPolicyOverdue(policy)) {
const overdueDays = daysBetween(policy.nextReviewDate, new Date())
return createIssue(
policy,
'OVERDUE_REVIEW',
'MEDIUM',
'Ueberfaellige Pruefung',
`Die Policy "${policy.dataObjectName}" haette am ${new Date(policy.nextReviewDate).toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig.`,
'Fuehren Sie umgehend eine Pruefung dieser Policy durch und aktualisieren Sie das naechste Pruefungsdatum.'
)
}
return null
}
/**
* Check 4: NO_RESPONSIBLE (MEDIUM)
* Policy with no responsiblePerson AND no responsibleRole.
*/
function checkNoResponsible(policy: LoeschfristPolicy): ComplianceIssue | null {
if (!policy.responsiblePerson.trim() && !policy.responsibleRole.trim()) {
return createIssue(
policy,
'NO_RESPONSIBLE',
'MEDIUM',
'Keine verantwortliche Person/Rolle',
`Die Policy "${policy.dataObjectName}" hat weder eine verantwortliche Person noch eine verantwortliche Rolle zugewiesen. Ohne Verantwortlichkeit kann die Loeschung nicht zuverlaessig durchgefuehrt werden.`,
'Weisen Sie eine verantwortliche Person oder zumindest eine verantwortliche Rolle (z.B. "Datenschutzbeauftragter", "IT-Leitung") zu.'
)
}
return null
}
/**
* Check 5: LEGAL_HOLD_CONFLICT (CRITICAL)
* Policy has active legal hold but deletionMethod is AUTO_DELETE.
*/
function checkLegalHoldConflict(policy: LoeschfristPolicy): ComplianceIssue | null {
const activeHolds = getActiveLegalHolds(policy)
if (activeHolds.length > 0 && policy.deletionMethod === 'AUTO_DELETE') {
const holdReasons = activeHolds.map((h) => h.reason).join(', ')
return createIssue(
policy,
'LEGAL_HOLD_CONFLICT',
'CRITICAL',
'Legal Hold mit automatischer Loeschung',
`Die Policy "${policy.dataObjectName}" hat ${activeHolds.length} aktive(n) Legal Hold(s) (${holdReasons}), aber die Loeschmethode ist auf "Automatische Loeschung" gesetzt. Dies kann zu unbeabsichtigter Vernichtung von Beweismitteln fuehren.`,
'Aendern Sie die Loeschmethode auf "Manuelle Pruefung & Loeschung" oder deaktivieren Sie die automatische Loeschung, solange der Legal Hold aktiv ist.'
)
}
return null
}
/**
* Check 6: STALE_DRAFT (LOW)
* Policy in DRAFT status older than 90 days.
*/
function checkStaleDraft(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status === 'DRAFT') {
const ageInDays = daysBetween(policy.createdAt, new Date())
if (ageInDays > 90) {
return createIssue(
policy,
'STALE_DRAFT',
'LOW',
'Veralteter Entwurf',
`Die Policy "${policy.dataObjectName}" ist seit ${ageInDays} Tagen im Entwurfsstatus. Entwuerfe, die laenger als 90 Tage nicht finalisiert werden, deuten auf unvollstaendige Dokumentation hin.`,
'Finalisieren Sie den Entwurf und setzen Sie den Status auf "Aktiv", oder archivieren Sie die Policy, falls sie nicht mehr benoetigt wird.'
)
}
}
return null
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
/**
* Fuehrt einen vollstaendigen Compliance-Check ueber alle Policies durch.
*
* @param policies - Alle Loeschfrist-Policies
* @param vvtDataCategories - Optionale Datenkategorien aus dem VVT (localStorage)
* @returns ComplianceCheckResult mit Issues, Score und Statistiken
*/
export function runComplianceCheck(
policies: LoeschfristPolicy[],
vvtDataCategories?: string[]
): ComplianceCheckResult {
// Reset counter for deterministic IDs within a single check run
issueCounter = 0
const issues: ComplianceIssue[] = []
// Run checks 1-6 for each policy
for (const policy of policies) {
const checks = [
checkMissingTrigger(policy),
checkMissingLegalBasis(policy),
checkOverdueReview(policy),
checkNoResponsible(policy),
checkLegalHoldConflict(policy),
checkStaleDraft(policy),
]
for (const issue of checks) {
if (issue !== null) {
issues.push(issue)
}
}
}
// Check 7: UNCOVERED_VVT_CATEGORY (MEDIUM)
if (vvtDataCategories && vvtDataCategories.length > 0) {
const coveredCategories = new Set<string>()
for (const policy of policies) {
for (const category of policy.dataCategories) {
coveredCategories.add(category.toLowerCase().trim())
}
}
for (const vvtCategory of vvtDataCategories) {
const normalized = vvtCategory.toLowerCase().trim()
if (!coveredCategories.has(normalized)) {
issues.push({
id: createIssueId(),
policyId: '-',
policyName: '-',
type: 'UNCOVERED_VVT_CATEGORY',
severity: 'MEDIUM',
title: `Datenkategorie ohne Loeschfrist: "${vvtCategory}"`,
description: `Die Datenkategorie "${vvtCategory}" ist im Verzeichnis der Verarbeitungstaetigkeiten (VVT) erfasst, hat aber keine zugehoerige Loeschfrist-Policy. Gemaess DSGVO Art. 5 Abs. 1 lit. e muss fuer jede Datenkategorie eine Speicherbegrenzung definiert sein.`,
recommendation: `Erstellen Sie eine neue Loeschfrist-Policy fuer die Datenkategorie "${vvtCategory}" oder ordnen Sie sie einer bestehenden Policy zu.`,
})
}
}
}
// Calculate score
const bySeverity: Record<ComplianceIssueSeverity, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0,
CRITICAL: 0,
}
for (const issue of issues) {
bySeverity[issue.severity]++
}
const rawScore =
100 -
(bySeverity.CRITICAL * 15 +
bySeverity.HIGH * 10 +
bySeverity.MEDIUM * 5 +
bySeverity.LOW * 2)
const score = Math.max(0, rawScore)
// Calculate pass/fail per policy
const failedPolicyIds = new Set(
issues.filter((i) => i.policyId !== '-').map((i) => i.policyId)
)
const totalPolicies = policies.length
const failedCount = failedPolicyIds.size
const passedCount = totalPolicies - failedCount
return {
issues,
score,
stats: {
total: totalPolicies,
passed: passedCount,
failed: failedCount,
bySeverity,
},
}
}
@@ -0,0 +1,353 @@
// =============================================================================
// Loeschfristen Module - Export & Report Generation
// JSON, CSV, Markdown-Compliance-Report und Browser-Download
// =============================================================================
import {
LoeschfristPolicy,
RETENTION_DRIVER_META,
DELETION_METHOD_LABELS,
STATUS_LABELS,
TRIGGER_LABELS,
formatRetentionDuration,
getEffectiveDeletionTrigger,
} from './loeschfristen-types'
import {
runComplianceCheck,
ComplianceCheckResult,
ComplianceIssueSeverity,
} from './loeschfristen-compliance'
// =============================================================================
// JSON EXPORT
// =============================================================================
interface PolicyExportEnvelope {
exportDate: string
version: string
totalPolicies: number
policies: LoeschfristPolicy[]
}
/**
* Exportiert alle Policies als pretty-printed JSON.
* Enthaelt Metadaten (Exportdatum, Version, Anzahl).
*/
export function exportPoliciesAsJSON(policies: LoeschfristPolicy[]): string {
const exportData: PolicyExportEnvelope = {
exportDate: new Date().toISOString(),
version: '1.0',
totalPolicies: policies.length,
policies: policies,
}
return JSON.stringify(exportData, null, 2)
}
// =============================================================================
// CSV EXPORT
// =============================================================================
/**
* Escapes a CSV field value according to RFC 4180.
* Fields containing commas, double quotes, or newlines are wrapped in quotes.
* Existing double quotes are doubled.
*/
function escapeCSVField(value: string): string {
if (
value.includes(',') ||
value.includes('"') ||
value.includes('\n') ||
value.includes('\r') ||
value.includes(';')
) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
/**
* Formats a date string to German locale format (DD.MM.YYYY).
* Returns empty string for null/undefined/empty values.
*/
function formatDateDE(dateStr: string | null | undefined): string {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return ''
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
} catch {
return ''
}
}
/**
* Exportiert alle Policies als CSV mit BOM fuer Excel-Kompatibilitaet.
* Trennzeichen ist Semikolon (;) fuer deutschsprachige Excel-Versionen.
*/
export function exportPoliciesAsCSV(policies: LoeschfristPolicy[]): string {
const BOM = '\uFEFF'
const SEPARATOR = ';'
const headers = [
'LF-Nr.',
'Datenobjekt',
'Beschreibung',
'Loeschtrigger',
'Aufbewahrungstreiber',
'Frist',
'Startereignis',
'Loeschmethode',
'Verantwortlich',
'Status',
'Legal Hold aktiv',
'Letzte Pruefung',
'Naechste Pruefung',
]
const rows: string[] = []
// Header row
rows.push(headers.map(escapeCSVField).join(SEPARATOR))
// Data rows
for (const policy of policies) {
const effectiveTrigger = getEffectiveDeletionTrigger(policy)
const triggerLabel = TRIGGER_LABELS[effectiveTrigger]
const driverLabel = policy.retentionDriver
? RETENTION_DRIVER_META[policy.retentionDriver].label
: ''
const durationLabel = formatRetentionDuration(
policy.retentionDuration,
policy.retentionUnit
)
const methodLabel = DELETION_METHOD_LABELS[policy.deletionMethod]
const statusLabel = STATUS_LABELS[policy.status]
// Combine responsiblePerson and responsibleRole
const responsible = [policy.responsiblePerson, policy.responsibleRole]
.filter((s) => s.trim())
.join(' / ')
const legalHoldActive = policy.hasActiveLegalHold ? 'Ja' : 'Nein'
const row = [
policy.policyId,
policy.dataObjectName,
policy.description,
triggerLabel,
driverLabel,
durationLabel,
policy.startEvent,
methodLabel,
responsible || '-',
statusLabel,
legalHoldActive,
formatDateDE(policy.lastReviewDate),
formatDateDE(policy.nextReviewDate),
]
rows.push(row.map(escapeCSVField).join(SEPARATOR))
}
return BOM + rows.join('\r\n')
}
// =============================================================================
// COMPLIANCE SUMMARY (MARKDOWN)
// =============================================================================
const SEVERITY_LABELS: Record<ComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
const SEVERITY_EMOJI: Record<ComplianceIssueSeverity, string> = {
CRITICAL: '[!!!]',
HIGH: '[!!]',
MEDIUM: '[!]',
LOW: '[i]',
}
/**
* Returns a textual rating based on the compliance score.
*/
function getScoreRating(score: number): string {
if (score >= 90) return 'Ausgezeichnet'
if (score >= 75) return 'Gut'
if (score >= 50) return 'Verbesserungswuerdig'
if (score >= 25) return 'Mangelhaft'
return 'Kritisch'
}
/**
* Generiert einen Markdown-formatierten Compliance-Bericht.
* Enthaelt: Uebersicht, Score, Issue-Liste, Empfehlungen.
*/
export function generateComplianceSummary(
policies: LoeschfristPolicy[],
vvtDataCategories?: string[]
): string {
const result: ComplianceCheckResult = runComplianceCheck(policies, vvtDataCategories)
const now = new Date()
const lines: string[] = []
// Header
lines.push('# Compliance-Bericht: Loeschfristen')
lines.push('')
lines.push(
`**Erstellt am:** ${now.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} um ${now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr`
)
lines.push('')
// Overview
lines.push('## Uebersicht')
lines.push('')
lines.push(`| Kennzahl | Wert |`)
lines.push(`|----------|------|`)
lines.push(`| Gepruefte Policies | ${result.stats.total} |`)
lines.push(`| Bestanden | ${result.stats.passed} |`)
lines.push(`| Beanstandungen | ${result.stats.failed} |`)
lines.push(`| Compliance-Score | **${result.score}/100** (${getScoreRating(result.score)}) |`)
lines.push('')
// Severity breakdown
lines.push('## Befunde nach Schweregrad')
lines.push('')
lines.push('| Schweregrad | Anzahl |')
lines.push('|-------------|--------|')
const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const severity of severityOrder) {
const count = result.stats.bySeverity[severity]
lines.push(`| ${SEVERITY_LABELS[severity]} | ${count} |`)
}
lines.push('')
// Status distribution of policies
const statusCounts: Record<string, number> = {}
for (const policy of policies) {
const label = STATUS_LABELS[policy.status]
statusCounts[label] = (statusCounts[label] || 0) + 1
}
lines.push('## Policy-Status-Verteilung')
lines.push('')
lines.push('| Status | Anzahl |')
lines.push('|--------|--------|')
for (const [label, count] of Object.entries(statusCounts)) {
lines.push(`| ${label} | ${count} |`)
}
lines.push('')
// Issues list
if (result.issues.length === 0) {
lines.push('## Befunde')
lines.push('')
lines.push('Keine Beanstandungen gefunden. Alle Policies sind konform.')
lines.push('')
} else {
lines.push('## Befunde')
lines.push('')
// Group issues by severity
for (const severity of severityOrder) {
const issuesForSeverity = result.issues.filter((i) => i.severity === severity)
if (issuesForSeverity.length === 0) continue
lines.push(`### ${SEVERITY_LABELS[severity]} ${SEVERITY_EMOJI[severity]}`)
lines.push('')
for (const issue of issuesForSeverity) {
const policyRef =
issue.policyId !== '-' ? ` (${issue.policyId})` : ''
lines.push(`**${issue.title}**${policyRef}`)
lines.push('')
lines.push(`> ${issue.description}`)
lines.push('')
lines.push(`Empfehlung: ${issue.recommendation}`)
lines.push('')
lines.push('---')
lines.push('')
}
}
}
// Recommendations summary
lines.push('## Zusammenfassung der Empfehlungen')
lines.push('')
if (result.stats.bySeverity.CRITICAL > 0) {
lines.push(
`1. **Sofortmassnahmen erforderlich:** ${result.stats.bySeverity.CRITICAL} kritische(r) Befund(e) muessen umgehend behoben werden (Legal Hold-Konflikte).`
)
}
if (result.stats.bySeverity.HIGH > 0) {
lines.push(
`${result.stats.bySeverity.CRITICAL > 0 ? '2' : '1'}. **Hohe Prioritaet:** ${result.stats.bySeverity.HIGH} Befund(e) mit hoher Prioritaet (fehlende Trigger/Rechtsgrundlagen) sollten zeitnah bearbeitet werden.`
)
}
if (result.stats.bySeverity.MEDIUM > 0) {
lines.push(
`- **Mittlere Prioritaet:** ${result.stats.bySeverity.MEDIUM} Befund(e) betreffen ueberfaellige Pruefungen, fehlende Verantwortlichkeiten oder nicht abgedeckte Datenkategorien.`
)
}
if (result.stats.bySeverity.LOW > 0) {
lines.push(
`- **Niedrige Prioritaet:** ${result.stats.bySeverity.LOW} Befund(e) betreffen veraltete Entwuerfe, die finalisiert oder archiviert werden sollten.`
)
}
if (result.issues.length === 0) {
lines.push(
'Alle Policies sind konform. Stellen Sie sicher, dass die naechsten Pruefungstermine eingehalten werden.'
)
}
lines.push('')
// Footer
lines.push('---')
lines.push('')
lines.push(
'*Dieser Bericht wurde automatisch generiert und ersetzt keine rechtliche Beratung. Die Verantwortung fuer die DSGVO-Konformitaet liegt beim Verantwortlichen (Art. 4 Nr. 7 DSGVO).*'
)
return lines.join('\n')
}
// =============================================================================
// BROWSER DOWNLOAD UTILITY
// =============================================================================
/**
* Loest einen Datei-Download im Browser aus.
* Erstellt ein temporaeres Blob-URL und simuliert einen Link-Klick.
*
* @param content - Der Dateiinhalt als String
* @param filename - Der gewuenschte Dateiname (z.B. "loeschfristen-export.json")
* @param mimeType - Der MIME-Typ (z.B. "application/json", "text/csv;charset=utf-8")
*/
export function downloadFile(
content: string,
filename: string,
mimeType: string
): void {
const blob = new Blob([content], { type: mimeType })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
@@ -0,0 +1,538 @@
// =============================================================================
// Loeschfristen Module - Profiling Wizard
// 4-Step Profiling (15 Fragen) zur Generierung von Baseline-Loeschrichtlinien
// =============================================================================
import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types'
import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog'
// =============================================================================
// TYPES
// =============================================================================
export type ProfilingStepId = 'organization' | 'data-categories' | 'systems' | 'special'
export interface ProfilingQuestion {
id: string
step: ProfilingStepId
question: string // German
helpText?: string
type: 'single' | 'multi' | 'boolean' | 'number'
options?: { value: string; label: string }[]
required: boolean
}
export interface ProfilingAnswer {
questionId: string
value: string | string[] | boolean | number
}
export interface ProfilingStep {
id: ProfilingStepId
title: string
description: string
questions: ProfilingQuestion[]
}
export interface ProfilingResult {
matchedTemplates: BaselineTemplate[]
generatedPolicies: LoeschfristPolicy[]
additionalStorageLocations: StorageLocation[]
hasLegalHoldRequirement: boolean
}
// =============================================================================
// PROFILING STEPS (4 Steps, 15 Questions)
// =============================================================================
export const PROFILING_STEPS: ProfilingStep[] = [
// =========================================================================
// Step 1: Organisation (4 Fragen)
// =========================================================================
{
id: 'organization',
title: 'Organisation',
description: 'Allgemeine Informationen zu Ihrem Unternehmen, um branchenspezifische Loeschfristen zu ermitteln.',
questions: [
{
id: 'org-branche',
step: 'organization',
question: 'In welcher Branche ist Ihr Unternehmen taetig?',
helpText: 'Die Branche bestimmt, welche branchenspezifischen Aufbewahrungspflichten relevant sind.',
type: 'single',
options: [
{ value: 'it-software', label: 'IT / Software' },
{ value: 'handel', label: 'Handel' },
{ value: 'dienstleistung', label: 'Dienstleistung' },
{ value: 'gesundheitswesen', label: 'Gesundheitswesen' },
{ value: 'bildung', label: 'Bildung' },
{ value: 'fertigung-industrie', label: 'Fertigung / Industrie' },
{ value: 'finanzwesen', label: 'Finanzwesen' },
{ value: 'oeffentlicher-sektor', label: 'Oeffentlicher Sektor' },
{ value: 'sonstige', label: 'Sonstige' },
],
required: true,
},
{
id: 'org-mitarbeiter',
step: 'organization',
question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?',
helpText: 'Die Unternehmensgroesse beeinflusst den Umfang der erforderlichen Loeschkonzepte.',
type: 'single',
options: [
{ value: '<10', label: 'Weniger als 10' },
{ value: '10-49', label: '10 bis 49' },
{ value: '50-249', label: '50 bis 249' },
{ value: '250+', label: '250 und mehr' },
],
required: true,
},
{
id: 'org-geschaeftsmodell',
step: 'organization',
question: 'Welches Geschaeftsmodell verfolgen Sie?',
helpText: 'B2B und B2C haben unterschiedliche Anforderungen an die Datenhaltung.',
type: 'single',
options: [
{ value: 'b2b', label: 'B2B (Geschaeftskunden)' },
{ value: 'b2c', label: 'B2C (Endkunden)' },
{ value: 'beides', label: 'Beides (B2B und B2C)' },
],
required: true,
},
{
id: 'org-website',
step: 'organization',
question: 'Betreiben Sie eine Website oder Online-Praesenz?',
helpText: 'Websites erzeugen Webserver-Logs und erfordern Cookie-Consent-Verwaltung.',
type: 'boolean',
required: true,
},
],
},
// =========================================================================
// Step 2: Datenkategorien (5 Fragen)
// =========================================================================
{
id: 'data-categories',
title: 'Datenkategorien',
description: 'Welche Arten personenbezogener Daten verarbeiten Sie? Dies bestimmt die relevanten Aufbewahrungsfristen.',
questions: [
{
id: 'data-hr',
step: 'data-categories',
question: 'Verarbeiten Sie HR-/Personaldaten (Personalakten, Gehaltsabrechnungen, Zeiterfassung)?',
helpText: 'Personalakten unterliegen umfangreichen gesetzlichen Aufbewahrungspflichten (bis zu 10 Jahre).',
type: 'boolean',
required: true,
},
{
id: 'data-buchhaltung',
step: 'data-categories',
question: 'Fuehren Sie eine Buchhaltung mit Finanzdaten (Rechnungen, Belege, Steuererklarungen)?',
helpText: 'Buchhaltungsunterlagen muessen gemaess HGB und AO bis zu 10 Jahre aufbewahrt werden.',
type: 'boolean',
required: true,
},
{
id: 'data-vertraege',
step: 'data-categories',
question: 'Verwalten Sie Vertraege mit Kunden oder Lieferanten?',
helpText: 'Vertragsunterlagen und Geschaeftsbriefe haben spezifische Aufbewahrungspflichten.',
type: 'boolean',
required: true,
},
{
id: 'data-marketing',
step: 'data-categories',
question: 'Betreiben Sie Marketing-Aktivitaeten (Newsletter, CRM-Kampagnen)?',
helpText: 'Marketing-Einwilligungen und Kontakthistorien muessen dokumentiert und verwaltet werden.',
type: 'boolean',
required: true,
},
{
id: 'data-video',
step: 'data-categories',
question: 'Setzen Sie Videoueberwachung ein?',
helpText: 'Videoueberwachungsdaten haben besonders kurze Loeschfristen (in der Regel 72 Stunden).',
type: 'boolean',
required: true,
},
],
},
// =========================================================================
// Step 3: Systeme (3 Fragen)
// =========================================================================
{
id: 'systems',
title: 'Systeme & Infrastruktur',
description: 'Welche IT-Systeme und Infrastruktur nutzen Sie? Dies beeinflusst die Speicherorte in Ihrem Loeschkonzept.',
questions: [
{
id: 'sys-cloud',
step: 'systems',
question: 'Nutzen Sie Cloud-Dienste zur Datenspeicherung oder -verarbeitung?',
helpText: 'Cloud-Speicherorte muessen in den Loeschrichtlinien als separate Speicherorte dokumentiert werden.',
type: 'boolean',
required: true,
},
{
id: 'sys-backup',
step: 'systems',
question: 'Haben Sie Backup-Systeme im Einsatz?',
helpText: 'Backups erfordern eine eigene Loeschstrategie, da Daten dort nach der primaeren Loeschung weiter existieren koennen.',
type: 'boolean',
required: true,
},
{
id: 'sys-erp',
step: 'systems',
question: 'Setzen Sie ein ERP- oder CRM-System ein?',
helpText: 'ERP-/CRM-Systeme sind haeufig zentrale Speicherorte fuer Kunden- und Geschaeftsdaten.',
type: 'boolean',
required: true,
},
],
},
// =========================================================================
// Step 4: Spezielle Anforderungen (3 Fragen)
// =========================================================================
{
id: 'special',
title: 'Spezielle Anforderungen',
description: 'Gibt es besondere rechtliche oder organisatorische Anforderungen, die Ihr Loeschkonzept beeinflussen?',
questions: [
{
id: 'special-legal-hold',
step: 'special',
question: 'Gibt es Legal-Hold-Anforderungen (z.B. laufende Rechtsstreitigkeiten, behoerdliche Untersuchungen)?',
helpText: 'Bei einem Legal Hold muessen betroffene Daten trotz abgelaufener Loeschfristen aufbewahrt werden.',
type: 'boolean',
required: true,
},
{
id: 'special-archivierung',
step: 'special',
question: 'Benoetigen Sie eine Langzeitarchivierung von Dokumenten?',
helpText: 'Langzeitarchivierung kann ueber die gesetzlichen Mindestfristen hinausgehen und erfordert eine gesonderte Rechtfertigung.',
type: 'boolean',
required: true,
},
{
id: 'special-gesundheit',
step: 'special',
question: 'Verarbeiten Sie Gesundheitsdaten (z.B. Krankmeldungen, Arbeitsmedizin)?',
helpText: 'Gesundheitsdaten sind besonders schuetzenswerte Daten nach Art. 9 DSGVO und unterliegen strengeren Anforderungen.',
type: 'boolean',
required: true,
},
],
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Retrieve the value of a specific answer by question ID.
*/
export function getAnswerValue(answers: ProfilingAnswer[], questionId: string): unknown {
const answer = answers.find(a => a.questionId === questionId)
return answer?.value ?? undefined
}
/**
* Check whether all required questions in a given step have been answered.
*/
export function isStepComplete(answers: ProfilingAnswer[], stepId: ProfilingStepId): boolean {
const step = PROFILING_STEPS.find(s => s.id === stepId)
if (!step) return false
return step.questions
.filter(q => q.required)
.every(q => {
const answer = answers.find(a => a.questionId === q.id)
if (!answer) return false
// Check that the value is not empty
const val = answer.value
if (val === undefined || val === null) return false
if (typeof val === 'string' && val.trim() === '') return false
if (Array.isArray(val) && val.length === 0) return false
return true
})
}
/**
* Calculate overall profiling progress as a percentage (0-100).
*/
export function getProfilingProgress(answers: ProfilingAnswer[]): number {
const totalRequired = PROFILING_STEPS.reduce(
(sum, step) => sum + step.questions.filter(q => q.required).length,
0
)
if (totalRequired === 0) return 100
const answeredRequired = PROFILING_STEPS.reduce((sum, step) => {
return (
sum +
step.questions.filter(q => q.required).filter(q => {
const answer = answers.find(a => a.questionId === q.id)
if (!answer) return false
const val = answer.value
if (val === undefined || val === null) return false
if (typeof val === 'string' && val.trim() === '') return false
if (Array.isArray(val) && val.length === 0) return false
return true
}).length
)
}, 0)
return Math.round((answeredRequired / totalRequired) * 100)
}
// =============================================================================
// CORE GENERATOR
// =============================================================================
/**
* Generate deletion policies based on the profiling answers.
*
* Logic:
* - Match baseline templates based on boolean and categorical answers
* - Deduplicate matched templates by templateId
* - Convert matched templates to full LoeschfristPolicy objects
* - Add additional storage locations (Cloud, Backup) if applicable
* - Detect legal hold requirements
*/
export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): ProfilingResult {
const matchedTemplateIds = new Set<string>()
// -------------------------------------------------------------------------
// Helper to get a boolean answer
// -------------------------------------------------------------------------
const getBool = (questionId: string): boolean => {
const val = getAnswerValue(answers, questionId)
return val === true
}
const getString = (questionId: string): string => {
const val = getAnswerValue(answers, questionId)
return typeof val === 'string' ? val : ''
}
// -------------------------------------------------------------------------
// Always-included templates (universally recommended)
// -------------------------------------------------------------------------
matchedTemplateIds.add('protokolle-gesellschafter')
// -------------------------------------------------------------------------
// HR data (data-hr = true)
// -------------------------------------------------------------------------
if (getBool('data-hr')) {
matchedTemplateIds.add('personal-akten')
matchedTemplateIds.add('gehaltsabrechnungen')
matchedTemplateIds.add('zeiterfassung')
matchedTemplateIds.add('bewerbungsunterlagen')
matchedTemplateIds.add('krankmeldungen')
}
// -------------------------------------------------------------------------
// Buchhaltung (data-buchhaltung = true)
// -------------------------------------------------------------------------
if (getBool('data-buchhaltung')) {
matchedTemplateIds.add('buchhaltungsbelege')
matchedTemplateIds.add('rechnungen')
matchedTemplateIds.add('steuererklaerungen')
}
// -------------------------------------------------------------------------
// Vertraege (data-vertraege = true)
// -------------------------------------------------------------------------
if (getBool('data-vertraege')) {
matchedTemplateIds.add('vertraege')
matchedTemplateIds.add('geschaeftsbriefe')
matchedTemplateIds.add('kundenstammdaten')
}
// -------------------------------------------------------------------------
// Marketing (data-marketing = true)
// -------------------------------------------------------------------------
if (getBool('data-marketing')) {
matchedTemplateIds.add('newsletter-einwilligungen')
matchedTemplateIds.add('crm-kontakthistorie')
matchedTemplateIds.add('cookie-consent-logs')
}
// -------------------------------------------------------------------------
// Video (data-video = true)
// -------------------------------------------------------------------------
if (getBool('data-video')) {
matchedTemplateIds.add('videoueberwachung')
}
// -------------------------------------------------------------------------
// Website (org-website = true)
// -------------------------------------------------------------------------
if (getBool('org-website')) {
matchedTemplateIds.add('webserver-logs')
matchedTemplateIds.add('cookie-consent-logs')
}
// -------------------------------------------------------------------------
// ERP/CRM (sys-erp = true)
// -------------------------------------------------------------------------
if (getBool('sys-erp')) {
matchedTemplateIds.add('kundenstammdaten')
matchedTemplateIds.add('crm-kontakthistorie')
}
// -------------------------------------------------------------------------
// Backup (sys-backup = true)
// -------------------------------------------------------------------------
if (getBool('sys-backup')) {
matchedTemplateIds.add('backup-daten')
}
// -------------------------------------------------------------------------
// Gesundheitsdaten (special-gesundheit = true)
// -------------------------------------------------------------------------
if (getBool('special-gesundheit')) {
// Ensure krankmeldungen is included even without full HR data
matchedTemplateIds.add('krankmeldungen')
}
// -------------------------------------------------------------------------
// Resolve matched templates from catalog
// -------------------------------------------------------------------------
const matchedTemplates: BaselineTemplate[] = []
for (const templateId of matchedTemplateIds) {
const template = BASELINE_TEMPLATES.find(t => t.templateId === templateId)
if (template) {
matchedTemplates.push(template)
}
}
// -------------------------------------------------------------------------
// Convert to policies
// -------------------------------------------------------------------------
const generatedPolicies: LoeschfristPolicy[] = matchedTemplates.map(template =>
templateToPolicy(template)
)
// -------------------------------------------------------------------------
// Additional storage locations
// -------------------------------------------------------------------------
const additionalStorageLocations: StorageLocation[] = []
if (getBool('sys-cloud')) {
const cloudLocation: StorageLocation = {
id: crypto.randomUUID(),
name: 'Cloud-Speicher',
type: 'CLOUD',
isBackup: false,
provider: null,
deletionCapable: true,
}
additionalStorageLocations.push(cloudLocation)
// Add Cloud storage location to all generated policies
for (const policy of generatedPolicies) {
policy.storageLocations.push({ ...cloudLocation, id: crypto.randomUUID() })
}
}
if (getBool('sys-backup')) {
const backupLocation: StorageLocation = {
id: crypto.randomUUID(),
name: 'Backup-System',
type: 'BACKUP',
isBackup: true,
provider: null,
deletionCapable: true,
}
additionalStorageLocations.push(backupLocation)
// Add Backup storage location to all generated policies
for (const policy of generatedPolicies) {
policy.storageLocations.push({ ...backupLocation, id: crypto.randomUUID() })
}
}
// -------------------------------------------------------------------------
// Legal Hold
// -------------------------------------------------------------------------
const hasLegalHoldRequirement = getBool('special-legal-hold')
// If legal hold is active, mark all generated policies accordingly
if (hasLegalHoldRequirement) {
for (const policy of generatedPolicies) {
policy.hasActiveLegalHold = true
policy.deletionTrigger = 'LEGAL_HOLD'
}
}
// -------------------------------------------------------------------------
// Tag policies with profiling metadata
// -------------------------------------------------------------------------
const branche = getString('org-branche')
const mitarbeiter = getString('org-mitarbeiter')
for (const policy of generatedPolicies) {
policy.tags = [
...policy.tags,
'profiling-generated',
...(branche ? [`branche:${branche}`] : []),
...(mitarbeiter ? [`groesse:${mitarbeiter}`] : []),
]
}
return {
matchedTemplates,
generatedPolicies,
additionalStorageLocations,
hasLegalHoldRequirement,
}
}
// =============================================================================
// COMPLIANCE SCOPE INTEGRATION
// =============================================================================
/**
* Prefill Loeschfristen profiling answers from Compliance Scope Engine answers.
* The Scope Engine acts as the "Single Source of Truth" for organizational questions.
*/
export function prefillFromScopeAnswers(
scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[]
): ProfilingAnswer[] {
const { exportToLoeschfristenAnswers } = require('./compliance-scope-profiling')
const exported = exportToLoeschfristenAnswers(scopeAnswers) as Array<{ questionId: string; value: unknown }>
return exported.map(item => ({
questionId: item.questionId,
value: item.value as string | string[] | boolean | number,
}))
}
/**
* Get the list of Loeschfristen question IDs that are prefilled from Scope answers.
* These questions should show "Aus Scope-Analyse uebernommen" hint.
*/
export const SCOPE_PREFILLED_LF_QUESTIONS = [
'org-branche',
'org-mitarbeiter',
'org-geschaeftsmodell',
'org-website',
'data-hr',
'data-buchhaltung',
'data-vertraege',
'data-marketing',
'data-video',
'sys-cloud',
'sys-erp',
]
+346
View File
@@ -0,0 +1,346 @@
// =============================================================================
// Loeschfristen Module - TypeScript Types
// 3-Level Loeschlogik: Zweckende -> Aufbewahrungstreiber -> Legal Hold
// =============================================================================
// =============================================================================
// ENUMS & LITERAL TYPES
// =============================================================================
export type DeletionTriggerLevel = 'PURPOSE_END' | 'RETENTION_DRIVER' | 'LEGAL_HOLD'
export type RetentionDriverType =
| 'AO_147' // 10 Jahre Steuerunterlagen
| 'HGB_257' // 10/6 Jahre Handelsbuecher/-briefe
| 'USTG_14B' // 10 Jahre Rechnungen
| 'BGB_195' // 3 Jahre Verjaehrung
| 'ARBZG_16' // 2 Jahre Zeiterfassung
| 'AGG_15' // 6 Monate Bewerbungen
| 'BDSG_35' // Unverzuegliche Loeschung
| 'BSIG' // 90 Tage Sicherheitslogs
| 'CUSTOM'
export type DeletionMethodType =
| 'AUTO_DELETE'
| 'MANUAL_REVIEW_DELETE'
| 'ANONYMIZATION'
| 'AGGREGATION'
| 'CRYPTO_ERASE'
| 'PHYSICAL_DESTROY'
export type PolicyStatus = 'DRAFT' | 'ACTIVE' | 'REVIEW_NEEDED' | 'ARCHIVED'
export type ReviewInterval = 'QUARTERLY' | 'SEMI_ANNUAL' | 'ANNUAL'
export type RetentionUnit = 'DAYS' | 'MONTHS' | 'YEARS'
export type StorageLocationType =
| 'DATABASE' | 'FILE_SYSTEM' | 'CLOUD' | 'EMAIL' | 'BACKUP' | 'PAPER' | 'OTHER'
export type LegalHoldStatus = 'ACTIVE' | 'RELEASED' | 'EXPIRED'
// =============================================================================
// INTERFACES
// =============================================================================
export interface LegalHold {
id: string
reason: string
legalBasis: string
responsiblePerson: string
startDate: string
expectedEndDate: string | null
actualEndDate: string | null
status: LegalHoldStatus
affectedDataCategories: string[]
}
export interface StorageLocation {
id: string
name: string
type: StorageLocationType
isBackup: boolean
provider: string | null
deletionCapable: boolean
}
export interface LoeschfristPolicy {
id: string
policyId: string // LF-2026-001
dataObjectName: string
description: string
affectedGroups: string[]
dataCategories: string[]
primaryPurpose: string
// 3-Level Loeschlogik
deletionTrigger: DeletionTriggerLevel
retentionDriver: RetentionDriverType | null
retentionDriverDetail: string
retentionDuration: number | null
retentionUnit: RetentionUnit | null
retentionDescription: string
startEvent: string
hasActiveLegalHold: boolean
legalHolds: LegalHold[]
// Speicherorte & Loeschung
storageLocations: StorageLocation[]
deletionMethod: DeletionMethodType
deletionMethodDetail: string
// Verantwortung & Workflow
responsibleRole: string
responsiblePerson: string
releaseProcess: string
linkedVVTActivityIds: string[]
// Status & Review
status: PolicyStatus
lastReviewDate: string
nextReviewDate: string
reviewInterval: ReviewInterval
tags: string[]
createdAt: string
updatedAt: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
export interface RetentionDriverMeta {
label: string
statute: string
defaultDuration: number | null
defaultUnit: RetentionUnit | null
description: string
}
export const RETENTION_DRIVER_META: Record<RetentionDriverType, RetentionDriverMeta> = {
AO_147: {
label: 'Abgabenordnung (AO) 147',
statute: '147 AO',
defaultDuration: 10,
defaultUnit: 'YEARS',
description: 'Aufbewahrung steuerrelevanter Unterlagen (Buchungsbelege, Bilanzen, Jahresabschluesse)',
},
HGB_257: {
label: 'Handelsgesetzbuch (HGB) 257',
statute: '257 HGB',
defaultDuration: 10,
defaultUnit: 'YEARS',
description: 'Handelsbuecher und Buchungsbelege (10 J.), empfangene/gesendete Handelsbriefe (6 J.)',
},
USTG_14B: {
label: 'Umsatzsteuergesetz (UStG) 14b',
statute: '14b UStG',
defaultDuration: 10,
defaultUnit: 'YEARS',
description: 'Aufbewahrung von Rechnungen und rechnungsbegruendenden Unterlagen',
},
BGB_195: {
label: 'Buergerliches Gesetzbuch (BGB) 195',
statute: '195 BGB',
defaultDuration: 3,
defaultUnit: 'YEARS',
description: 'Regelmaessige Verjaehrungsfrist fuer vertragliche Ansprueche',
},
ARBZG_16: {
label: 'Arbeitszeitgesetz (ArbZG) 16',
statute: '16 Abs. 2 ArbZG',
defaultDuration: 2,
defaultUnit: 'YEARS',
description: 'Aufbewahrung von Arbeitszeitaufzeichnungen',
},
AGG_15: {
label: 'Allg. Gleichbehandlungsgesetz (AGG) 15',
statute: '15 Abs. 4 AGG',
defaultDuration: 6,
defaultUnit: 'MONTHS',
description: 'Frist fuer Geltendmachung von Entschaedigungsanspruechen nach Absage',
},
BDSG_35: {
label: 'BDSG 35 / DSGVO Art. 17',
statute: '35 BDSG / Art. 17 DSGVO',
defaultDuration: null,
defaultUnit: null,
description: 'Unverzuegliche Loeschung nach Zweckwegfall (kein fester Zeitraum)',
},
BSIG: {
label: 'BSI-Gesetz (BSIG)',
statute: 'BSIG / IT-SiG 2.0',
defaultDuration: 90,
defaultUnit: 'DAYS',
description: 'Aufbewahrung von Sicherheitslogs fuer Vorfallsanalyse',
},
CUSTOM: {
label: 'Individuelle Frist',
statute: 'Individuell',
defaultDuration: null,
defaultUnit: null,
description: 'Benutzerdefinierte Aufbewahrungsfrist',
},
}
export const DELETION_METHOD_LABELS: Record<DeletionMethodType, string> = {
AUTO_DELETE: 'Automatische Loeschung',
MANUAL_REVIEW_DELETE: 'Manuelle Pruefung & Loeschung',
ANONYMIZATION: 'Anonymisierung',
AGGREGATION: 'Aggregation (statistische Verdichtung)',
CRYPTO_ERASE: 'Kryptographische Loeschung',
PHYSICAL_DESTROY: 'Physische Vernichtung',
}
export const STATUS_LABELS: Record<PolicyStatus, string> = {
DRAFT: 'Entwurf',
ACTIVE: 'Aktiv',
REVIEW_NEEDED: 'Pruefung erforderlich',
ARCHIVED: 'Archiviert',
}
export const STATUS_COLORS: Record<PolicyStatus, string> = {
DRAFT: 'bg-gray-100 text-gray-700 border-gray-200',
ACTIVE: 'bg-green-100 text-green-700 border-green-200',
REVIEW_NEEDED: 'bg-yellow-100 text-yellow-700 border-yellow-200',
ARCHIVED: 'bg-blue-100 text-blue-700 border-blue-200',
}
export const TRIGGER_LABELS: Record<DeletionTriggerLevel, string> = {
PURPOSE_END: 'Zweckende',
RETENTION_DRIVER: 'Aufbewahrungspflicht',
LEGAL_HOLD: 'Legal Hold',
}
export const TRIGGER_COLORS: Record<DeletionTriggerLevel, string> = {
PURPOSE_END: 'bg-green-100 text-green-700',
RETENTION_DRIVER: 'bg-blue-100 text-blue-700',
LEGAL_HOLD: 'bg-red-100 text-red-700',
}
export const REVIEW_INTERVAL_LABELS: Record<ReviewInterval, string> = {
QUARTERLY: 'Vierteljaehrlich',
SEMI_ANNUAL: 'Halbjaehrlich',
ANNUAL: 'Jaehrlich',
}
export const STORAGE_LOCATION_LABELS: Record<StorageLocationType, string> = {
DATABASE: 'Datenbank',
FILE_SYSTEM: 'Dateisystem',
CLOUD: 'Cloud-Speicher',
EMAIL: 'E-Mail-System',
BACKUP: 'Backup-System',
PAPER: 'Papierarchiv',
OTHER: 'Sonstiges',
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
let policyCounter = 0
export function generatePolicyId(): string {
policyCounter++
const year = new Date().getFullYear()
const num = String(policyCounter).padStart(3, '0')
return `LF-${year}-${num}`
}
export function createEmptyPolicy(): LoeschfristPolicy {
const now = new Date().toISOString()
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
return {
id: crypto.randomUUID(),
policyId: generatePolicyId(),
dataObjectName: '',
description: '',
affectedGroups: [],
dataCategories: [],
primaryPurpose: '',
deletionTrigger: 'PURPOSE_END',
retentionDriver: null,
retentionDriverDetail: '',
retentionDuration: null,
retentionUnit: null,
retentionDescription: '',
startEvent: '',
hasActiveLegalHold: false,
legalHolds: [],
storageLocations: [],
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail: '',
responsibleRole: '',
responsiblePerson: '',
releaseProcess: '',
linkedVVTActivityIds: [],
status: 'DRAFT',
lastReviewDate: now,
nextReviewDate: nextYear.toISOString(),
reviewInterval: 'ANNUAL',
tags: [],
createdAt: now,
updatedAt: now,
}
}
export function createEmptyLegalHold(): LegalHold {
return {
id: crypto.randomUUID(),
reason: '',
legalBasis: '',
responsiblePerson: '',
startDate: new Date().toISOString().split('T')[0],
expectedEndDate: null,
actualEndDate: null,
status: 'ACTIVE',
affectedDataCategories: [],
}
}
export function createEmptyStorageLocation(): StorageLocation {
return {
id: crypto.randomUUID(),
name: '',
type: 'DATABASE',
isBackup: false,
provider: null,
deletionCapable: true,
}
}
export function formatRetentionDuration(
duration: number | null,
unit: RetentionUnit | null
): string {
if (duration === null || unit === null) return 'Bis Zweckwegfall'
const unitLabels: Record<RetentionUnit, string> = {
DAYS: duration === 1 ? 'Tag' : 'Tage',
MONTHS: duration === 1 ? 'Monat' : 'Monate',
YEARS: duration === 1 ? 'Jahr' : 'Jahre',
}
return `${duration} ${unitLabels[unit]}`
}
export function isPolicyOverdue(policy: LoeschfristPolicy): boolean {
if (!policy.nextReviewDate) return false
return new Date(policy.nextReviewDate) <= new Date()
}
export function getActiveLegalHolds(policy: LoeschfristPolicy): LegalHold[] {
return policy.legalHolds.filter(h => h.status === 'ACTIVE')
}
export function getEffectiveDeletionTrigger(policy: LoeschfristPolicy): DeletionTriggerLevel {
if (policy.hasActiveLegalHold && getActiveLegalHolds(policy).length > 0) {
return 'LEGAL_HOLD'
}
if (policy.retentionDriver && policy.retentionDriver !== 'CUSTOM') {
return 'RETENTION_DRIVER'
}
return 'PURPOSE_END'
}
// =============================================================================
// LOCALSTORAGE KEY
// =============================================================================
export const LOESCHFRISTEN_STORAGE_KEY = 'bp_loeschfristen'
+327
View File
@@ -0,0 +1,327 @@
/**
* SDK Backend Client
*
* Client for communicating with the SDK Backend (Go service)
* for RAG search and document generation.
*/
// =============================================================================
// TYPES
// =============================================================================
export interface SearchResult {
id: string
content: string
source: string
score: number
metadata?: Record<string, string>
highlights?: string[]
}
export interface SearchResponse {
query: string
topK: number
results: SearchResult[]
source: 'qdrant' | 'mock'
}
export interface CorpusStatus {
status: 'ready' | 'unavailable' | 'indexing'
collections: string[]
documents: number
lastUpdated?: string
}
export interface GenerateRequest {
tenantId: string
context: Record<string, unknown>
template?: string
language?: 'de' | 'en'
useRag?: boolean
ragQuery?: string
maxTokens?: number
temperature?: number
}
export interface GenerateResponse {
content: string
generatedAt: string
model: string
tokensUsed: number
ragSources?: SearchResult[]
confidence?: number
}
export interface SDKBackendResponse<T> {
success: boolean
data?: T
error?: string
code?: string
}
// =============================================================================
// CONFIGURATION
// =============================================================================
const SDK_BACKEND_URL = process.env.NEXT_PUBLIC_SDK_BACKEND_URL || 'http://localhost:8085'
const DEFAULT_TIMEOUT = 60000 // 60 seconds for generation
// =============================================================================
// SDK BACKEND CLIENT
// =============================================================================
export class SDKBackendClient {
private baseUrl: string
private timeout: number
constructor(options: {
baseUrl?: string
timeout?: number
} = {}) {
this.baseUrl = options.baseUrl || SDK_BACKEND_URL
this.timeout = options.timeout || DEFAULT_TIMEOUT
}
// ---------------------------------------------------------------------------
// Private Methods
// ---------------------------------------------------------------------------
private async fetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
try {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
})
const data = await response.json() as SDKBackendResponse<T>
if (!response.ok || !data.success) {
throw new Error(data.error || `HTTP ${response.status}`)
}
return data.data as T
} finally {
clearTimeout(timeoutId)
}
}
// ---------------------------------------------------------------------------
// RAG Search
// ---------------------------------------------------------------------------
/**
* Search the legal corpus using semantic search
*/
async search(
query: string,
options: {
topK?: number
collection?: string
filter?: string
} = {}
): Promise<SearchResponse> {
const params = new URLSearchParams({
q: query,
top_k: String(options.topK || 5),
})
if (options.collection) {
params.set('collection', options.collection)
}
if (options.filter) {
params.set('filter', options.filter)
}
return this.fetch<SearchResponse>(`/sdk/v1/rag/search?${params}`)
}
/**
* Get the status of the legal corpus
*/
async getCorpusStatus(): Promise<CorpusStatus> {
return this.fetch<CorpusStatus>('/sdk/v1/rag/status')
}
/**
* Index a new document into the corpus
*/
async indexDocument(
collection: string,
id: string,
content: string,
metadata?: Record<string, string>
): Promise<{ indexed: boolean; id: string }> {
return this.fetch('/sdk/v1/rag/index', {
method: 'POST',
body: JSON.stringify({
collection,
id,
content,
metadata,
}),
})
}
// ---------------------------------------------------------------------------
// Document Generation
// ---------------------------------------------------------------------------
/**
* Generate a Data Protection Impact Assessment (DSFA)
*/
async generateDSFA(request: GenerateRequest): Promise<GenerateResponse> {
return this.fetch<GenerateResponse>('/sdk/v1/generate/dsfa', {
method: 'POST',
body: JSON.stringify(request),
})
}
/**
* Generate Technical and Organizational Measures (TOM)
*/
async generateTOM(request: GenerateRequest): Promise<GenerateResponse> {
return this.fetch<GenerateResponse>('/sdk/v1/generate/tom', {
method: 'POST',
body: JSON.stringify(request),
})
}
/**
* Generate a Processing Activity Register (VVT)
*/
async generateVVT(request: GenerateRequest): Promise<GenerateResponse> {
return this.fetch<GenerateResponse>('/sdk/v1/generate/vvt', {
method: 'POST',
body: JSON.stringify(request),
})
}
/**
* Generate an expert opinion/assessment (Gutachten)
*/
async generateGutachten(request: GenerateRequest): Promise<GenerateResponse> {
return this.fetch<GenerateResponse>('/sdk/v1/generate/gutachten', {
method: 'POST',
body: JSON.stringify(request),
})
}
// ---------------------------------------------------------------------------
// Health Check
// ---------------------------------------------------------------------------
/**
* Check if the SDK backend is healthy
*/
async healthCheck(): Promise<{
status: string
timestamp: string
services: {
database: boolean
rag: boolean
llm: boolean
}
}> {
try {
const response = await fetch(`${this.baseUrl}/health`, {
method: 'GET',
})
return response.json()
} catch {
return {
status: 'unhealthy',
timestamp: new Date().toISOString(),
services: {
database: false,
rag: false,
llm: false,
},
}
}
}
}
// =============================================================================
// SINGLETON INSTANCE
// =============================================================================
let clientInstance: SDKBackendClient | null = null
export function getSDKBackendClient(): SDKBackendClient {
if (!clientInstance) {
clientInstance = new SDKBackendClient()
}
return clientInstance
}
export function resetSDKBackendClient(): void {
clientInstance = null
}
// =============================================================================
// UTILITY FUNCTIONS
// =============================================================================
/**
* Check if a query is likely a legal/compliance search
*/
export function isLegalQuery(query: string): boolean {
const legalKeywords = [
'dsgvo', 'gdpr', 'datenschutz', 'privacy',
'ai act', 'ki-gesetz', 'ai-verordnung',
'nis2', 'cybersicherheit',
'artikel', 'article', 'art.',
'verordnung', 'regulation', 'richtlinie', 'directive',
'gesetz', 'law', 'compliance',
'dsfa', 'dpia', 'folgenabschätzung',
'tom', 'maßnahmen', 'measures',
'vvt', 'verarbeitungsverzeichnis',
]
const normalizedQuery = query.toLowerCase()
return legalKeywords.some(keyword => normalizedQuery.includes(keyword))
}
/**
* Extract regulation references from text
*/
export function extractRegulationReferences(text: string): Array<{
regulation: string
article: string
fullReference: string
}> {
const references: Array<{
regulation: string
article: string
fullReference: string
}> = []
// Match patterns like "Art. 5 DSGVO", "Article 6 GDPR", "Art. 35 Abs. 1 DSGVO"
const patterns = [
/Art(?:ikel|\.|\s)*(\d+)(?:\s*Abs\.\s*(\d+))?\s*(DSGVO|GDPR|AI\s*Act|NIS2)/gi,
/Article\s*(\d+)(?:\s*para(?:graph)?\s*(\d+))?\s*(DSGVO|GDPR|AI\s*Act|NIS2)/gi,
]
for (const pattern of patterns) {
let match
while ((match = pattern.exec(text)) !== null) {
references.push({
regulation: match[3].toUpperCase().replace(/\s+/g, ' '),
article: match[2] ? `${match[1]} Abs. ${match[2]}` : match[1],
fullReference: match[0],
})
}
}
return references
}
+482
View File
@@ -0,0 +1,482 @@
/**
* SDK State Synchronization
*
* Handles offline/online sync, multi-tab coordination,
* and conflict resolution for SDK state.
*/
import { SDKState } from './types'
import { SDKApiClient, StateResponse } from './api-client'
// =============================================================================
// TYPES
// =============================================================================
export type SyncStatus = 'idle' | 'syncing' | 'error' | 'offline' | 'conflict'
export interface SyncState {
status: SyncStatus
lastSyncedAt: Date | null
localVersion: number
serverVersion: number
pendingChanges: number
error: string | null
}
export interface ConflictResolution {
strategy: 'local' | 'server' | 'merge'
mergedState?: SDKState
}
export interface SyncOptions {
debounceMs?: number
maxRetries?: number
conflictHandler?: (local: SDKState, server: SDKState) => Promise<ConflictResolution>
}
export interface SyncCallbacks {
onSyncStart?: () => void
onSyncComplete?: (state: SDKState) => void
onSyncError?: (error: Error) => void
onConflict?: (local: SDKState, server: SDKState) => void
onOffline?: () => void
onOnline?: () => void
}
// =============================================================================
// CONSTANTS
// =============================================================================
const STORAGE_KEY_PREFIX = 'ai-compliance-sdk-state'
const SYNC_CHANNEL = 'sdk-state-sync'
const DEFAULT_DEBOUNCE_MS = 2000
const DEFAULT_MAX_RETRIES = 3
// =============================================================================
// STATE SYNC MANAGER
// =============================================================================
export class StateSyncManager {
private apiClient: SDKApiClient
private tenantId: string
private options: Required<SyncOptions>
private callbacks: SyncCallbacks
private syncState: SyncState
private broadcastChannel: BroadcastChannel | null = null
private debounceTimeout: ReturnType<typeof setTimeout> | null = null
private pendingState: SDKState | null = null
private isOnline: boolean = true
constructor(
apiClient: SDKApiClient,
tenantId: string,
options: SyncOptions = {},
callbacks: SyncCallbacks = {}
) {
this.apiClient = apiClient
this.tenantId = tenantId
this.callbacks = callbacks
this.options = {
debounceMs: options.debounceMs ?? DEFAULT_DEBOUNCE_MS,
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
conflictHandler: options.conflictHandler ?? this.defaultConflictHandler,
}
this.syncState = {
status: 'idle',
lastSyncedAt: null,
localVersion: 0,
serverVersion: 0,
pendingChanges: 0,
error: null,
}
this.setupBroadcastChannel()
this.setupOnlineListener()
}
// ---------------------------------------------------------------------------
// Setup Methods
// ---------------------------------------------------------------------------
private setupBroadcastChannel(): void {
if (typeof window === 'undefined' || !('BroadcastChannel' in window)) {
return
}
try {
this.broadcastChannel = new BroadcastChannel(`${SYNC_CHANNEL}-${this.tenantId}`)
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this)
} catch (error) {
console.warn('BroadcastChannel not available:', error)
}
}
private setupOnlineListener(): void {
if (typeof window === 'undefined') {
return
}
window.addEventListener('online', () => {
this.isOnline = true
this.syncState.status = 'idle'
this.callbacks.onOnline?.()
// Attempt to sync any pending changes
if (this.pendingState) {
this.syncToServer(this.pendingState)
}
})
window.addEventListener('offline', () => {
this.isOnline = false
this.syncState.status = 'offline'
this.callbacks.onOffline?.()
})
// Check initial online status
this.isOnline = navigator.onLine
if (!this.isOnline) {
this.syncState.status = 'offline'
}
}
// ---------------------------------------------------------------------------
// Broadcast Channel Methods
// ---------------------------------------------------------------------------
private handleBroadcastMessage(event: MessageEvent): void {
const { type, state, version, tabId } = event.data
switch (type) {
case 'STATE_UPDATED':
// Another tab updated the state
if (version > this.syncState.localVersion) {
this.syncState.localVersion = version
this.saveToLocalStorage(state)
this.callbacks.onSyncComplete?.(state)
}
break
case 'SYNC_COMPLETE':
// Another tab completed a sync
this.syncState.serverVersion = version
break
case 'REQUEST_STATE':
// Another tab is requesting the current state
this.broadcastState()
break
}
}
private broadcastState(): void {
if (!this.broadcastChannel) return
const state = this.loadFromLocalStorage()
if (state) {
this.broadcastChannel.postMessage({
type: 'STATE_UPDATED',
state,
version: this.syncState.localVersion,
tabId: this.getTabId(),
})
}
}
private broadcastSyncComplete(version: number): void {
if (!this.broadcastChannel) return
this.broadcastChannel.postMessage({
type: 'SYNC_COMPLETE',
version,
tabId: this.getTabId(),
})
}
private getTabId(): string {
if (typeof window === 'undefined') return 'server'
let tabId = sessionStorage.getItem('sdk-tab-id')
if (!tabId) {
tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
sessionStorage.setItem('sdk-tab-id', tabId)
}
return tabId
}
// ---------------------------------------------------------------------------
// Local Storage Methods
// ---------------------------------------------------------------------------
private getStorageKey(): string {
return `${STORAGE_KEY_PREFIX}-${this.tenantId}`
}
saveToLocalStorage(state: SDKState): void {
if (typeof window === 'undefined') return
try {
const data = {
state,
version: this.syncState.localVersion,
savedAt: new Date().toISOString(),
}
localStorage.setItem(this.getStorageKey(), JSON.stringify(data))
} catch (error) {
console.error('Failed to save to localStorage:', error)
}
}
loadFromLocalStorage(): SDKState | null {
if (typeof window === 'undefined') return null
try {
const stored = localStorage.getItem(this.getStorageKey())
if (stored) {
const data = JSON.parse(stored)
this.syncState.localVersion = data.version || 0
return data.state
}
} catch (error) {
console.error('Failed to load from localStorage:', error)
}
return null
}
clearLocalStorage(): void {
if (typeof window === 'undefined') return
try {
localStorage.removeItem(this.getStorageKey())
} catch (error) {
console.error('Failed to clear localStorage:', error)
}
}
// ---------------------------------------------------------------------------
// Sync Methods
// ---------------------------------------------------------------------------
/**
* Queue a state change for syncing (debounced)
*/
queueSync(state: SDKState): void {
this.pendingState = state
this.syncState.pendingChanges++
// Save to localStorage immediately
this.syncState.localVersion++
this.saveToLocalStorage(state)
// Broadcast to other tabs
this.broadcastState()
// Debounce server sync
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
this.debounceTimeout = setTimeout(() => {
this.syncToServer(state)
}, this.options.debounceMs)
}
/**
* Force immediate sync to server
*/
async forcSync(state: SDKState): Promise<void> {
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
this.debounceTimeout = null
}
await this.syncToServer(state)
}
/**
* Sync state to server
*/
private async syncToServer(state: SDKState): Promise<void> {
if (!this.isOnline) {
this.syncState.status = 'offline'
return
}
this.syncState.status = 'syncing'
this.callbacks.onSyncStart?.()
try {
const response = await this.apiClient.saveState(state, this.syncState.serverVersion)
this.syncState = {
...this.syncState,
status: 'idle',
lastSyncedAt: new Date(),
serverVersion: response.version,
pendingChanges: 0,
error: null,
}
this.pendingState = null
this.broadcastSyncComplete(response.version)
this.callbacks.onSyncComplete?.(state)
} catch (error) {
// Handle version conflict (409)
if ((error as { status?: number }).status === 409) {
await this.handleConflict(state)
} else {
this.syncState.status = 'error'
this.syncState.error = (error as Error).message
this.callbacks.onSyncError?.(error as Error)
}
}
}
/**
* Load state from server
*/
async loadFromServer(): Promise<SDKState | null> {
if (!this.isOnline) {
return this.loadFromLocalStorage()
}
try {
const response = await this.apiClient.getState()
if (response) {
this.syncState.serverVersion = response.version
this.syncState.localVersion = response.version
this.saveToLocalStorage(response.state)
return response.state
}
// No server state, return local if available
return this.loadFromLocalStorage()
} catch (error) {
console.error('Failed to load from server:', error)
// Fallback to local storage
return this.loadFromLocalStorage()
}
}
// ---------------------------------------------------------------------------
// Conflict Resolution
// ---------------------------------------------------------------------------
private async handleConflict(localState: SDKState): Promise<void> {
this.syncState.status = 'conflict'
try {
// Fetch server state
const serverResponse = await this.apiClient.getState()
if (!serverResponse) {
// Server has no state, use local
await this.apiClient.saveState(localState)
return
}
const serverState = serverResponse.state
this.callbacks.onConflict?.(localState, serverState)
// Resolve conflict
const resolution = await this.options.conflictHandler(localState, serverState)
let resolvedState: SDKState
switch (resolution.strategy) {
case 'local':
resolvedState = localState
break
case 'server':
resolvedState = serverState
break
case 'merge':
resolvedState = resolution.mergedState || localState
break
}
// Save resolved state
const response = await this.apiClient.saveState(resolvedState)
this.syncState.serverVersion = response.version
this.syncState.localVersion = response.version
this.saveToLocalStorage(resolvedState)
this.syncState.status = 'idle'
this.callbacks.onSyncComplete?.(resolvedState)
} catch (error) {
this.syncState.status = 'error'
this.syncState.error = (error as Error).message
this.callbacks.onSyncError?.(error as Error)
}
}
private async defaultConflictHandler(
local: SDKState,
server: SDKState
): Promise<ConflictResolution> {
// Default: Server wins, but we preserve certain local-only data
const localTime = new Date(local.lastModified).getTime()
const serverTime = new Date(server.lastModified).getTime()
if (localTime > serverTime) {
// Local is newer, use local
return { strategy: 'local' }
}
// Merge: Use server state but preserve local UI preferences
const mergedState: SDKState = {
...server,
preferences: local.preferences,
commandBarHistory: [
...local.commandBarHistory,
...server.commandBarHistory.filter(
h => !local.commandBarHistory.some(lh => lh.id === h.id)
),
].slice(0, 50),
recentSearches: [...new Set([...local.recentSearches, ...server.recentSearches])].slice(0, 20),
}
return { strategy: 'merge', mergedState }
}
// ---------------------------------------------------------------------------
// Getters & Cleanup
// ---------------------------------------------------------------------------
getSyncState(): SyncState {
return { ...this.syncState }
}
isOnlineStatus(): boolean {
return this.isOnline
}
hasPendingChanges(): boolean {
return this.syncState.pendingChanges > 0 || this.pendingState !== null
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.debounceTimeout) {
clearTimeout(this.debounceTimeout)
}
if (this.broadcastChannel) {
this.broadcastChannel.close()
}
}
}
// =============================================================================
// FACTORY FUNCTION
// =============================================================================
export function createStateSyncManager(
apiClient: SDKApiClient,
tenantId: string,
options?: SyncOptions,
callbacks?: SyncCallbacks
): StateSyncManager {
return new StateSyncManager(apiClient, tenantId, options, callbacks)
}
@@ -0,0 +1,414 @@
// =============================================================================
// TOM Generator Document Analyzer
// AI-powered analysis of uploaded evidence documents
// =============================================================================
import {
EvidenceDocument,
AIDocumentAnalysis,
ExtractedClause,
DocumentType,
} from '../types'
import {
getDocumentAnalysisPrompt,
getDocumentTypeDetectionPrompt,
DocumentAnalysisPromptContext,
} from './prompts'
import { getAllControls } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface AnalysisResult {
success: boolean
analysis: AIDocumentAnalysis | null
error?: string
}
export interface DocumentTypeDetectionResult {
documentType: DocumentType
confidence: number
reasoning: string
}
// =============================================================================
// DOCUMENT ANALYZER CLASS
// =============================================================================
export class TOMDocumentAnalyzer {
private apiEndpoint: string
private apiKey: string | null
constructor(options?: { apiEndpoint?: string; apiKey?: string }) {
this.apiEndpoint = options?.apiEndpoint || '/api/sdk/v1/tom-generator/evidence/analyze'
this.apiKey = options?.apiKey || null
}
/**
* Analyze a document and extract relevant TOM information
*/
async analyzeDocument(
document: EvidenceDocument,
documentText: string,
language: 'de' | 'en' = 'de'
): Promise<AnalysisResult> {
try {
// Get all control IDs for context
const allControls = getAllControls()
const controlIds = allControls.map((c) => c.id)
// Build the prompt context
const promptContext: DocumentAnalysisPromptContext = {
documentType: document.documentType,
documentText,
controlIds,
language,
}
const prompt = getDocumentAnalysisPrompt(promptContext)
// Call the AI API
const response = await this.callAI(prompt)
if (!response.success || !response.data) {
return {
success: false,
analysis: null,
error: response.error || 'Failed to analyze document',
}
}
// Parse the AI response
const parsedResponse = this.parseAnalysisResponse(response.data)
const analysis: AIDocumentAnalysis = {
summary: parsedResponse.summary,
extractedClauses: parsedResponse.extractedClauses,
applicableControls: parsedResponse.applicableControls,
gaps: parsedResponse.gaps,
confidence: parsedResponse.confidence,
analyzedAt: new Date(),
}
return {
success: true,
analysis,
}
} catch (error) {
return {
success: false,
analysis: null,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Detect the document type from content
*/
async detectDocumentType(
documentText: string,
filename: string
): Promise<DocumentTypeDetectionResult> {
try {
const prompt = getDocumentTypeDetectionPrompt(documentText, filename)
const response = await this.callAI(prompt)
if (!response.success || !response.data) {
return {
documentType: 'OTHER',
confidence: 0,
reasoning: 'Could not detect document type',
}
}
const parsed = this.parseJSONResponse(response.data)
return {
documentType: this.mapDocumentType(String(parsed.documentType || 'OTHER')),
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0,
reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : '',
}
} catch (error) {
return {
documentType: 'OTHER',
confidence: 0,
reasoning: error instanceof Error ? error.message : 'Detection failed',
}
}
}
/**
* Link document to applicable controls based on analysis
*/
async suggestControlLinks(
analysis: AIDocumentAnalysis
): Promise<string[]> {
// Use the applicable controls from the analysis
const suggestedControls = [...analysis.applicableControls]
// Also check extracted clauses for related controls
for (const clause of analysis.extractedClauses) {
if (clause.relatedControlId && !suggestedControls.includes(clause.relatedControlId)) {
suggestedControls.push(clause.relatedControlId)
}
}
return suggestedControls
}
/**
* Calculate evidence coverage for a control
*/
calculateEvidenceCoverage(
controlId: string,
documents: EvidenceDocument[]
): {
coverage: number
linkedDocuments: string[]
missingEvidence: string[]
} {
const control = getAllControls().find((c) => c.id === controlId)
if (!control) {
return { coverage: 0, linkedDocuments: [], missingEvidence: [] }
}
const linkedDocuments: string[] = []
const coveredRequirements = new Set<string>()
for (const doc of documents) {
// Check if document is explicitly linked
if (doc.linkedControlIds.includes(controlId)) {
linkedDocuments.push(doc.id)
}
// Check if AI analysis suggests this document covers the control
if (doc.aiAnalysis?.applicableControls.includes(controlId)) {
if (!linkedDocuments.includes(doc.id)) {
linkedDocuments.push(doc.id)
}
}
// Check which evidence requirements are covered
if (doc.aiAnalysis) {
for (const requirement of control.evidenceRequirements) {
const reqLower = requirement.toLowerCase()
if (
doc.aiAnalysis.summary.toLowerCase().includes(reqLower) ||
doc.aiAnalysis.extractedClauses.some((c) =>
c.text.toLowerCase().includes(reqLower)
)
) {
coveredRequirements.add(requirement)
}
}
}
}
const missingEvidence = control.evidenceRequirements.filter(
(req) => !coveredRequirements.has(req)
)
const coverage =
control.evidenceRequirements.length > 0
? Math.round(
(coveredRequirements.size / control.evidenceRequirements.length) * 100
)
: 100
return {
coverage,
linkedDocuments,
missingEvidence,
}
}
/**
* Call the AI API
*/
private async callAI(
prompt: string
): Promise<{ success: boolean; data?: string; error?: string }> {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`
}
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers,
body: JSON.stringify({ prompt }),
})
if (!response.ok) {
return {
success: false,
error: `API error: ${response.status} ${response.statusText}`,
}
}
const data = await response.json()
return {
success: true,
data: data.response || data.content || JSON.stringify(data),
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'API call failed',
}
}
}
/**
* Parse the AI analysis response
*/
private parseAnalysisResponse(response: string): {
summary: string
extractedClauses: ExtractedClause[]
applicableControls: string[]
gaps: string[]
confidence: number
} {
const parsed = this.parseJSONResponse(response)
return {
summary: typeof parsed.summary === 'string' ? parsed.summary : '',
extractedClauses: (Array.isArray(parsed.extractedClauses) ? parsed.extractedClauses : []).map(
(clause: Record<string, unknown>) => ({
id: String(clause.id || ''),
text: String(clause.text || ''),
type: String(clause.type || ''),
relatedControlId: clause.relatedControlId
? String(clause.relatedControlId)
: null,
})
),
applicableControls: Array.isArray(parsed.applicableControls)
? parsed.applicableControls.map(String)
: [],
gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [],
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0,
}
}
/**
* Parse JSON from AI response (handles markdown code blocks)
*/
private parseJSONResponse(response: string): Record<string, unknown> {
let jsonStr = response.trim()
// Remove markdown code blocks if present
if (jsonStr.startsWith('```json')) {
jsonStr = jsonStr.slice(7)
} else if (jsonStr.startsWith('```')) {
jsonStr = jsonStr.slice(3)
}
if (jsonStr.endsWith('```')) {
jsonStr = jsonStr.slice(0, -3)
}
jsonStr = jsonStr.trim()
try {
return JSON.parse(jsonStr)
} catch {
// Try to extract JSON from the response
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/)
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0])
} catch {
return {}
}
}
return {}
}
}
/**
* Map string to DocumentType
*/
private mapDocumentType(type: string): DocumentType {
const typeMap: Record<string, DocumentType> = {
AVV: 'AVV',
DPA: 'DPA',
SLA: 'SLA',
NDA: 'NDA',
POLICY: 'POLICY',
CERTIFICATE: 'CERTIFICATE',
AUDIT_REPORT: 'AUDIT_REPORT',
OTHER: 'OTHER',
}
return typeMap[type.toUpperCase()] || 'OTHER'
}
}
// =============================================================================
// SINGLETON INSTANCE
// =============================================================================
let analyzerInstance: TOMDocumentAnalyzer | null = null
export function getDocumentAnalyzer(
options?: { apiEndpoint?: string; apiKey?: string }
): TOMDocumentAnalyzer {
if (!analyzerInstance) {
analyzerInstance = new TOMDocumentAnalyzer(options)
}
return analyzerInstance
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Quick document analysis
*/
export async function analyzeEvidenceDocument(
document: EvidenceDocument,
documentText: string,
language: 'de' | 'en' = 'de'
): Promise<AnalysisResult> {
return getDocumentAnalyzer().analyzeDocument(document, documentText, language)
}
/**
* Quick document type detection
*/
export async function detectEvidenceDocumentType(
documentText: string,
filename: string
): Promise<DocumentTypeDetectionResult> {
return getDocumentAnalyzer().detectDocumentType(documentText, filename)
}
/**
* Get evidence gaps for all controls
*/
export function getEvidenceGapsForAllControls(
documents: EvidenceDocument[]
): Map<string, { coverage: number; missing: string[] }> {
const analyzer = getDocumentAnalyzer()
const allControls = getAllControls()
const gaps = new Map<string, { coverage: number; missing: string[] }>()
for (const control of allControls) {
const result = analyzer.calculateEvidenceCoverage(control.id, documents)
gaps.set(control.id, {
coverage: result.coverage,
missing: result.missingEvidence,
})
}
return gaps
}
@@ -0,0 +1,427 @@
// =============================================================================
// TOM Generator AI Prompts
// Prompts for document analysis and TOM description generation
// =============================================================================
import {
CompanyProfile,
DataProfile,
ArchitectureProfile,
RiskProfile,
DocumentType,
ControlLibraryEntry,
} from '../types'
// =============================================================================
// DOCUMENT ANALYSIS PROMPT
// =============================================================================
export interface DocumentAnalysisPromptContext {
documentType: DocumentType
documentText: string
controlIds?: string[]
language?: 'de' | 'en'
}
export function getDocumentAnalysisPrompt(
context: DocumentAnalysisPromptContext
): string {
const { documentType, documentText, controlIds, language = 'de' } = context
const controlContext = controlIds?.length
? `\nRELEVANT CONTROL IDS: ${controlIds.join(', ')}`
: ''
if (language === 'de') {
return `Du bist ein Experte für Datenschutz-Compliance und analysierst ein Dokument für die TOM-Dokumentation nach DSGVO Art. 32.
DOKUMENTTYP: ${documentType}
${controlContext}
DOKUMENTTEXT:
${documentText}
AUFGABE: Analysiere das Dokument und extrahiere die folgenden Informationen:
1. SUMMARY: Eine Zusammenfassung in 2-3 Sätzen, die die Relevanz für den Datenschutz beschreibt.
2. EXTRACTED_CLAUSES: Alle Klauseln, die sich auf technische und organisatorische Sicherheitsmaßnahmen beziehen. Für jede Klausel:
- id: Eindeutige ID (z.B. "clause-1")
- text: Der extrahierte Text
- type: Art der Maßnahme (z.B. "encryption", "access-control", "backup", "training")
- relatedControlId: Falls zutreffend, die TOM-Control-ID (z.B. "TOM-ENC-01")
3. APPLICABLE_CONTROLS: Liste der TOM-Control-IDs, die durch dieses Dokument belegt werden könnten.
4. GAPS: Identifizierte Lücken oder fehlende Maßnahmen, die im Dokument nicht adressiert werden.
5. CONFIDENCE: Dein Vertrauenswert für die Analyse (0.0 bis 1.0).
Antworte im JSON-Format:
{
"summary": "...",
"extractedClauses": [
{ "id": "...", "text": "...", "type": "...", "relatedControlId": "..." }
],
"applicableControls": ["TOM-..."],
"gaps": ["..."],
"confidence": 0.85
}`
}
return `You are a data protection compliance expert analyzing a document for TOM documentation according to GDPR Art. 32.
DOCUMENT TYPE: ${documentType}
${controlContext}
DOCUMENT TEXT:
${documentText}
TASK: Analyze the document and extract the following information:
1. SUMMARY: A 2-3 sentence summary describing the relevance for data protection.
2. EXTRACTED_CLAUSES: All clauses related to technical and organizational security measures. For each clause:
- id: Unique ID (e.g., "clause-1")
- text: The extracted text
- type: Type of measure (e.g., "encryption", "access-control", "backup", "training")
- relatedControlId: If applicable, the TOM control ID (e.g., "TOM-ENC-01")
3. APPLICABLE_CONTROLS: List of TOM control IDs that could be evidenced by this document.
4. GAPS: Identified gaps or missing measures not addressed in the document.
5. CONFIDENCE: Your confidence score for the analysis (0.0 to 1.0).
Respond in JSON format:
{
"summary": "...",
"extractedClauses": [
{ "id": "...", "text": "...", "type": "...", "relatedControlId": "..." }
],
"applicableControls": ["TOM-..."],
"gaps": ["..."],
"confidence": 0.85
}`
}
// =============================================================================
// TOM DESCRIPTION GENERATION PROMPT
// =============================================================================
export interface TOMDescriptionPromptContext {
control: ControlLibraryEntry
companyProfile: CompanyProfile
dataProfile: DataProfile
architectureProfile: ArchitectureProfile
riskProfile: RiskProfile
language?: 'de' | 'en'
}
export function getTOMDescriptionPrompt(
context: TOMDescriptionPromptContext
): string {
const {
control,
companyProfile,
dataProfile,
architectureProfile,
riskProfile,
language = 'de',
} = context
if (language === 'de') {
return `Du bist ein Experte für Datenschutz-Compliance und erstellst eine unternehmensspezifische TOM-Beschreibung.
KONTROLLE:
- Name: ${control.name.de}
- Beschreibung: ${control.description.de}
- Kategorie: ${control.category}
- Typ: ${control.type}
UNTERNEHMENSPROFIL:
- Branche: ${companyProfile.industry}
- Größe: ${companyProfile.size}
- Rolle: ${companyProfile.role}
- Produkte/Services: ${companyProfile.products.join(', ')}
DATENPROFIL:
- Datenkategorien: ${dataProfile.categories.join(', ')}
- Besondere Kategorien: ${dataProfile.hasSpecialCategories ? 'Ja' : 'Nein'}
- Betroffene: ${dataProfile.subjects.join(', ')}
- Datenvolumen: ${dataProfile.dataVolume}
ARCHITEKTUR:
- Hosting-Modell: ${architectureProfile.hostingModel}
- Standort: ${architectureProfile.hostingLocation}
- Mandantentrennung: ${architectureProfile.multiTenancy}
SCHUTZBEDARF: ${riskProfile.protectionLevel}
AUFGABE: Erstelle eine unternehmensspezifische Beschreibung dieser TOM in 3-5 Sätzen.
Die Beschreibung soll:
- Auf das spezifische Unternehmensprofil zugeschnitten sein
- Konkrete Maßnahmen beschreiben, die für dieses Unternehmen relevant sind
- In formeller Geschäftssprache verfasst sein
- Keine Platzhalter oder generischen Formulierungen enthalten
Antworte nur mit der Beschreibung, ohne zusätzliche Erklärungen.`
}
return `You are a data protection compliance expert creating a company-specific TOM description.
CONTROL:
- Name: ${control.name.en}
- Description: ${control.description.en}
- Category: ${control.category}
- Type: ${control.type}
COMPANY PROFILE:
- Industry: ${companyProfile.industry}
- Size: ${companyProfile.size}
- Role: ${companyProfile.role}
- Products/Services: ${companyProfile.products.join(', ')}
DATA PROFILE:
- Data Categories: ${dataProfile.categories.join(', ')}
- Special Categories: ${dataProfile.hasSpecialCategories ? 'Yes' : 'No'}
- Data Subjects: ${dataProfile.subjects.join(', ')}
- Data Volume: ${dataProfile.dataVolume}
ARCHITECTURE:
- Hosting Model: ${architectureProfile.hostingModel}
- Location: ${architectureProfile.hostingLocation}
- Multi-tenancy: ${architectureProfile.multiTenancy}
PROTECTION LEVEL: ${riskProfile.protectionLevel}
TASK: Create a company-specific description of this TOM in 3-5 sentences.
The description should:
- Be tailored to the specific company profile
- Describe concrete measures relevant to this company
- Be written in formal business language
- Contain no placeholders or generic formulations
Respond only with the description, without additional explanations.`
}
// =============================================================================
// GAP RECOMMENDATIONS PROMPT
// =============================================================================
export interface GapRecommendationsPromptContext {
missingControls: Array<{ controlId: string; name: string; priority: string }>
partialControls: Array<{ controlId: string; name: string; missingAspects: string[] }>
companyProfile: CompanyProfile
riskProfile: RiskProfile
language?: 'de' | 'en'
}
export function getGapRecommendationsPrompt(
context: GapRecommendationsPromptContext
): string {
const {
missingControls,
partialControls,
companyProfile,
riskProfile,
language = 'de',
} = context
const missingList = missingControls
.map((c) => `- ${c.name} (${c.controlId}, Priorität: ${c.priority})`)
.join('\n')
const partialList = partialControls
.map((c) => `- ${c.name} (${c.controlId}): Fehlend: ${c.missingAspects.join(', ')}`)
.join('\n')
if (language === 'de') {
return `Du bist ein Experte für Datenschutz-Compliance und erstellst Handlungsempfehlungen für TOM-Lücken.
UNTERNEHMEN:
- Branche: ${companyProfile.industry}
- Größe: ${companyProfile.size}
- Rolle: ${companyProfile.role}
SCHUTZBEDARF: ${riskProfile.protectionLevel}
FEHLENDE KONTROLLEN:
${missingList || 'Keine'}
TEILWEISE IMPLEMENTIERTE KONTROLLEN:
${partialList || 'Keine'}
AUFGABE: Erstelle konkrete Handlungsempfehlungen, um die Lücken zu schließen.
Für jede Empfehlung:
1. Priorisiere nach Schutzbedarf und DSGVO-Relevanz
2. Berücksichtige die Unternehmensgröße und Branche
3. Gib konkrete, umsetzbare Schritte an
4. Schätze den Aufwand ein (niedrig/mittel/hoch)
Antworte im JSON-Format:
{
"recommendations": [
{
"priority": "HIGH",
"title": "...",
"description": "...",
"steps": ["..."],
"effort": "MEDIUM",
"relatedControls": ["TOM-..."]
}
],
"summary": "Kurze Zusammenfassung der wichtigsten Maßnahmen"
}`
}
return `You are a data protection compliance expert creating recommendations for TOM gaps.
COMPANY:
- Industry: ${companyProfile.industry}
- Size: ${companyProfile.size}
- Role: ${companyProfile.role}
PROTECTION LEVEL: ${riskProfile.protectionLevel}
MISSING CONTROLS:
${missingList || 'None'}
PARTIALLY IMPLEMENTED CONTROLS:
${partialList || 'None'}
TASK: Create concrete recommendations to close the gaps.
For each recommendation:
1. Prioritize by protection level and GDPR relevance
2. Consider company size and industry
3. Provide concrete, actionable steps
4. Estimate effort (low/medium/high)
Respond in JSON format:
{
"recommendations": [
{
"priority": "HIGH",
"title": "...",
"description": "...",
"steps": ["..."],
"effort": "MEDIUM",
"relatedControls": ["TOM-..."]
}
],
"summary": "Brief summary of the most important measures"
}`
}
// =============================================================================
// DOCUMENT TYPE DETECTION PROMPT
// =============================================================================
export function getDocumentTypeDetectionPrompt(
documentText: string,
filename: string
): string {
return `Du bist ein Experte für Datenschutz-Dokumente und sollst den Dokumenttyp erkennen.
DATEINAME: ${filename}
DOKUMENTTEXT (Auszug):
${documentText.substring(0, 2000)}
MÖGLICHE DOKUMENTTYPEN:
- AVV: Auftragsverarbeitungsvertrag
- DPA: Data Processing Agreement (englisch)
- SLA: Service Level Agreement
- NDA: Geheimhaltungsvereinbarung
- POLICY: Interne Richtlinie (z.B. Passwortrichtlinie, IT-Sicherheitsrichtlinie)
- CERTIFICATE: Zertifikat (z.B. ISO 27001, SOC 2)
- AUDIT_REPORT: Audit-Bericht oder Prüfbericht
- OTHER: Sonstiges Dokument
Antworte im JSON-Format:
{
"documentType": "...",
"confidence": 0.85,
"reasoning": "Kurze Begründung"
}`
}
// =============================================================================
// CLAUSE EXTRACTION PROMPT
// =============================================================================
export function getClauseExtractionPrompt(
documentText: string,
controlCategory: string
): string {
return `Du bist ein Experte für Datenschutz-Compliance und extrahierst Klauseln aus einem Dokument.
GESUCHTE KATEGORIE: ${controlCategory}
DOKUMENTTEXT:
${documentText}
AUFGABE: Extrahiere alle Klauseln, die sich auf die Kategorie "${controlCategory}" beziehen.
Antworte im JSON-Format:
{
"clauses": [
{
"id": "clause-1",
"text": "Der extrahierte Text der Klausel",
"section": "Abschnittsnummer oder -name falls vorhanden",
"relevance": "Kurze Erklärung der Relevanz",
"matchScore": 0.9
}
],
"totalFound": 3
}`
}
// =============================================================================
// COMPLIANCE ASSESSMENT PROMPT
// =============================================================================
export function getComplianceAssessmentPrompt(
tomDescription: string,
evidenceDescriptions: string[],
controlRequirements: string[]
): string {
return `Du bist ein Experte für Datenschutz-Compliance und bewertest die Umsetzung einer TOM.
TOM-BESCHREIBUNG:
${tomDescription}
ANFORDERUNGEN AN NACHWEISE:
${controlRequirements.map((r, i) => `${i + 1}. ${r}`).join('\n')}
VORHANDENE NACHWEISE:
${evidenceDescriptions.map((e, i) => `${i + 1}. ${e}`).join('\n') || 'Keine Nachweise vorhanden'}
AUFGABE: Bewerte den Umsetzungsgrad dieser TOM.
Antworte im JSON-Format:
{
"implementationStatus": "NOT_IMPLEMENTED" | "PARTIAL" | "IMPLEMENTED",
"score": 0-100,
"coveredRequirements": ["..."],
"missingRequirements": ["..."],
"recommendations": ["..."],
"reasoning": "Begründung der Bewertung"
}`
}
// =============================================================================
// EXPORT FUNCTIONS
// =============================================================================
export const AI_PROMPTS = {
documentAnalysis: getDocumentAnalysisPrompt,
tomDescription: getTOMDescriptionPrompt,
gapRecommendations: getGapRecommendationsPrompt,
documentTypeDetection: getDocumentTypeDetectionPrompt,
clauseExtraction: getClauseExtractionPrompt,
complianceAssessment: getComplianceAssessmentPrompt,
}
@@ -0,0 +1,710 @@
'use client'
// =============================================================================
// TOM Generator Context
// State management for the TOM Generator Wizard
// =============================================================================
import React, {
createContext,
useContext,
useReducer,
useCallback,
useEffect,
useRef,
ReactNode,
} from 'react'
import {
TOMGeneratorState,
TOMGeneratorStepId,
CompanyProfile,
DataProfile,
ArchitectureProfile,
SecurityProfile,
RiskProfile,
EvidenceDocument,
DerivedTOM,
GapAnalysisResult,
ExportRecord,
WizardStep,
createInitialTOMGeneratorState,
TOM_GENERATOR_STEPS,
getStepIndex,
calculateProtectionLevel,
isDSFARequired,
hasSpecialCategories,
} from './types'
import { TOMRulesEngine } from './rules-engine'
// =============================================================================
// ACTION TYPES
// =============================================================================
type TOMGeneratorAction =
| { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } }
| { type: 'RESET'; payload: { tenantId: string } }
| { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId }
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial<CompanyProfile> }
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
| { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } }
| { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId }
| { type: 'ADD_EVIDENCE'; payload: EvidenceDocument }
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<EvidenceDocument> } }
| { type: 'DELETE_EVIDENCE'; payload: string }
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
| { type: 'ADD_EXPORT'; payload: ExportRecord }
| { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial<DerivedTOM> }> } }
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
// =============================================================================
// REDUCER
// =============================================================================
function tomGeneratorReducer(
state: TOMGeneratorState,
action: TOMGeneratorAction
): TOMGeneratorState {
const updateState = (updates: Partial<TOMGeneratorState>): TOMGeneratorState => ({
...state,
...updates,
updatedAt: new Date(),
})
switch (action.type) {
case 'INITIALIZE': {
if (action.payload.state) {
return action.payload.state
}
return createInitialTOMGeneratorState(action.payload.tenantId)
}
case 'RESET': {
return createInitialTOMGeneratorState(action.payload.tenantId)
}
case 'SET_CURRENT_STEP': {
return updateState({ currentStep: action.payload })
}
case 'SET_COMPANY_PROFILE': {
return updateState({ companyProfile: action.payload })
}
case 'UPDATE_COMPANY_PROFILE': {
if (!state.companyProfile) return state
return updateState({
companyProfile: { ...state.companyProfile, ...action.payload },
})
}
case 'SET_DATA_PROFILE': {
// Automatically set hasSpecialCategories based on categories
const profile: DataProfile = {
...action.payload,
hasSpecialCategories: hasSpecialCategories(action.payload.categories),
}
return updateState({ dataProfile: profile })
}
case 'UPDATE_DATA_PROFILE': {
if (!state.dataProfile) return state
const updatedProfile = { ...state.dataProfile, ...action.payload }
// Recalculate hasSpecialCategories if categories changed
if (action.payload.categories) {
updatedProfile.hasSpecialCategories = hasSpecialCategories(
action.payload.categories
)
}
return updateState({ dataProfile: updatedProfile })
}
case 'SET_ARCHITECTURE_PROFILE': {
return updateState({ architectureProfile: action.payload })
}
case 'UPDATE_ARCHITECTURE_PROFILE': {
if (!state.architectureProfile) return state
return updateState({
architectureProfile: { ...state.architectureProfile, ...action.payload },
})
}
case 'SET_SECURITY_PROFILE': {
return updateState({ securityProfile: action.payload })
}
case 'UPDATE_SECURITY_PROFILE': {
if (!state.securityProfile) return state
return updateState({
securityProfile: { ...state.securityProfile, ...action.payload },
})
}
case 'SET_RISK_PROFILE': {
// Automatically calculate protection level and DSFA requirement
const profile: RiskProfile = {
...action.payload,
protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment),
dsfaRequired: isDSFARequired(state.dataProfile, action.payload),
}
return updateState({ riskProfile: profile })
}
case 'UPDATE_RISK_PROFILE': {
if (!state.riskProfile) return state
const updatedProfile = { ...state.riskProfile, ...action.payload }
// Recalculate protection level if CIA assessment changed
if (action.payload.ciaAssessment) {
updatedProfile.protectionLevel = calculateProtectionLevel(
action.payload.ciaAssessment
)
}
// Recalculate DSFA requirement
updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile)
return updateState({ riskProfile: updatedProfile })
}
case 'COMPLETE_STEP': {
const updatedSteps = state.steps.map((step) =>
step.id === action.payload.stepId
? {
...step,
completed: true,
data: action.payload.data,
validatedAt: new Date(),
}
: step
)
return updateState({ steps: updatedSteps })
}
case 'UNCOMPLETE_STEP': {
const updatedSteps = state.steps.map((step) =>
step.id === action.payload
? { ...step, completed: false, validatedAt: null }
: step
)
return updateState({ steps: updatedSteps })
}
case 'ADD_EVIDENCE': {
return updateState({
documents: [...state.documents, action.payload],
})
}
case 'UPDATE_EVIDENCE': {
const updatedDocuments = state.documents.map((doc) =>
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
)
return updateState({ documents: updatedDocuments })
}
case 'DELETE_EVIDENCE': {
return updateState({
documents: state.documents.filter((doc) => doc.id !== action.payload),
})
}
case 'SET_DERIVED_TOMS': {
return updateState({ derivedTOMs: action.payload })
}
case 'UPDATE_DERIVED_TOM': {
const updatedTOMs = state.derivedTOMs.map((tom) =>
tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom
)
return updateState({ derivedTOMs: updatedTOMs })
}
case 'SET_GAP_ANALYSIS': {
return updateState({ gapAnalysis: action.payload })
}
case 'ADD_EXPORT': {
return updateState({
exports: [...state.exports, action.payload],
})
}
case 'BULK_UPDATE_TOMS': {
let updatedTOMs = [...state.derivedTOMs]
for (const update of action.payload.updates) {
updatedTOMs = updatedTOMs.map((tom) =>
tom.id === update.id ? { ...tom, ...update.data } : tom
)
}
return updateState({ derivedTOMs: updatedTOMs })
}
case 'LOAD_STATE': {
return action.payload
}
default:
return state
}
}
// =============================================================================
// CONTEXT VALUE INTERFACE
// =============================================================================
interface TOMGeneratorContextValue {
state: TOMGeneratorState
dispatch: React.Dispatch<TOMGeneratorAction>
// Navigation
currentStepIndex: number
totalSteps: number
canGoNext: boolean
canGoPrevious: boolean
goToStep: (stepId: TOMGeneratorStepId) => void
goToNextStep: () => void
goToPreviousStep: () => void
completeCurrentStep: (data: unknown) => void
// Profile setters
setCompanyProfile: (profile: CompanyProfile) => void
updateCompanyProfile: (data: Partial<CompanyProfile>) => void
setDataProfile: (profile: DataProfile) => void
updateDataProfile: (data: Partial<DataProfile>) => void
setArchitectureProfile: (profile: ArchitectureProfile) => void
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
setSecurityProfile: (profile: SecurityProfile) => void
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
setRiskProfile: (profile: RiskProfile) => void
updateRiskProfile: (data: Partial<RiskProfile>) => void
// Evidence management
addEvidence: (document: EvidenceDocument) => void
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
deleteEvidence: (id: string) => void
// TOM derivation
deriveTOMs: () => void
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => void
// Gap analysis
runGapAnalysis: () => void
// Export
addExport: (record: ExportRecord) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
resetState: () => void
// Status
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
getCompletionPercentage: () => number
isLoading: boolean
error: string | null
}
// =============================================================================
// CONTEXT
// =============================================================================
const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(null)
// =============================================================================
// STORAGE KEYS
// =============================================================================
const STORAGE_KEY_PREFIX = 'tom-generator-state-'
function getStorageKey(tenantId: string): string {
return `${STORAGE_KEY_PREFIX}${tenantId}`
}
// =============================================================================
// PROVIDER COMPONENT
// =============================================================================
interface TOMGeneratorProviderProps {
children: ReactNode
tenantId: string
initialState?: TOMGeneratorState
enablePersistence?: boolean
}
export function TOMGeneratorProvider({
children,
tenantId,
initialState,
enablePersistence = true,
}: TOMGeneratorProviderProps) {
const [state, dispatch] = useReducer(
tomGeneratorReducer,
initialState ?? createInitialTOMGeneratorState(tenantId)
)
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const rulesEngineRef = useRef<TOMRulesEngine | null>(null)
// Initialize rules engine
useEffect(() => {
if (!rulesEngineRef.current) {
rulesEngineRef.current = new TOMRulesEngine()
}
}, [])
// Load state from localStorage on mount
useEffect(() => {
if (enablePersistence && typeof window !== 'undefined') {
try {
const stored = localStorage.getItem(getStorageKey(tenantId))
if (stored) {
const parsed = JSON.parse(stored)
// Convert date strings back to Date objects
if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt)
if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt)
if (parsed.steps) {
parsed.steps = parsed.steps.map((step: WizardStep) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
}))
}
if (parsed.documents) {
parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({
...doc,
uploadedAt: new Date(doc.uploadedAt),
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
aiAnalysis: doc.aiAnalysis
? {
...doc.aiAnalysis,
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
}
: null,
}))
}
if (parsed.derivedTOMs) {
parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({
...tom,
implementationDate: tom.implementationDate
? new Date(tom.implementationDate)
: null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
}))
}
if (parsed.gapAnalysis?.generatedAt) {
parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt)
}
if (parsed.exports) {
parsed.exports = parsed.exports.map((exp: ExportRecord) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
}))
}
dispatch({ type: 'LOAD_STATE', payload: parsed })
}
} catch (e) {
console.error('Failed to load TOM Generator state from localStorage:', e)
}
}
}, [tenantId, enablePersistence])
// Save state to localStorage on changes
useEffect(() => {
if (enablePersistence && typeof window !== 'undefined') {
try {
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state))
} catch (e) {
console.error('Failed to save TOM Generator state to localStorage:', e)
}
}
}, [state, tenantId, enablePersistence])
// Navigation helpers
const currentStepIndex = getStepIndex(state.currentStep)
const totalSteps = TOM_GENERATOR_STEPS.length
const canGoNext = currentStepIndex < totalSteps - 1
const canGoPrevious = currentStepIndex > 0
const goToStep = useCallback((stepId: TOMGeneratorStepId) => {
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
}, [])
const goToNextStep = useCallback(() => {
if (canGoNext) {
const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1]
dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id })
}
}, [canGoNext, currentStepIndex])
const goToPreviousStep = useCallback(() => {
if (canGoPrevious) {
const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1]
dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id })
}
}, [canGoPrevious, currentStepIndex])
const completeCurrentStep = useCallback(
(data: unknown) => {
dispatch({
type: 'COMPLETE_STEP',
payload: { stepId: state.currentStep, data },
})
},
[state.currentStep]
)
// Profile setters
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
}, [])
const updateCompanyProfile = useCallback((data: Partial<CompanyProfile>) => {
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
}, [])
const setDataProfile = useCallback((profile: DataProfile) => {
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
}, [])
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
}, [])
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
}, [])
const updateArchitectureProfile = useCallback(
(data: Partial<ArchitectureProfile>) => {
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
},
[]
)
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
}, [])
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
}, [])
const setRiskProfile = useCallback((profile: RiskProfile) => {
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
}, [])
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data })
}, [])
// Evidence management
const addEvidence = useCallback((document: EvidenceDocument) => {
dispatch({ type: 'ADD_EVIDENCE', payload: document })
}, [])
const updateEvidence = useCallback(
(id: string, data: Partial<EvidenceDocument>) => {
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
},
[]
)
const deleteEvidence = useCallback((id: string) => {
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
}, [])
// TOM derivation
const deriveTOMs = useCallback(() => {
if (!rulesEngineRef.current) return
const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({
companyProfile: state.companyProfile,
dataProfile: state.dataProfile,
architectureProfile: state.architectureProfile,
securityProfile: state.securityProfile,
riskProfile: state.riskProfile,
})
dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs })
}, [
state.companyProfile,
state.dataProfile,
state.architectureProfile,
state.securityProfile,
state.riskProfile,
])
const updateDerivedTOM = useCallback(
(id: string, data: Partial<DerivedTOM>) => {
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
},
[]
)
// Gap analysis
const runGapAnalysis = useCallback(() => {
if (!rulesEngineRef.current) return
const result = rulesEngineRef.current.performGapAnalysis(
state.derivedTOMs,
state.documents
)
dispatch({ type: 'SET_GAP_ANALYSIS', payload: result })
}, [state.derivedTOMs, state.documents])
// Export
const addExport = useCallback((record: ExportRecord) => {
dispatch({ type: 'ADD_EXPORT', payload: record })
}, [])
// Persistence
const saveState = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
// API call to save state
const response = await fetch('/api/sdk/v1/tom-generator/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenantId, state }),
})
if (!response.ok) {
throw new Error('Failed to save state')
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error')
throw e
} finally {
setIsLoading(false)
}
}, [tenantId, state])
const loadState = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(
`/api/sdk/v1/tom-generator/state?tenantId=${tenantId}`
)
if (!response.ok) {
throw new Error('Failed to load state')
}
const data = await response.json()
if (data.state) {
dispatch({ type: 'LOAD_STATE', payload: data.state })
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error')
throw e
} finally {
setIsLoading(false)
}
}, [tenantId])
const resetState = useCallback(() => {
dispatch({ type: 'RESET', payload: { tenantId } })
}, [tenantId])
// Status helpers
const isStepCompleted = useCallback(
(stepId: TOMGeneratorStepId) => {
const step = state.steps.find((s) => s.id === stepId)
return step?.completed ?? false
},
[state.steps]
)
const getCompletionPercentage = useCallback(() => {
const completedSteps = state.steps.filter((s) => s.completed).length
return Math.round((completedSteps / totalSteps) * 100)
}, [state.steps, totalSteps])
const contextValue: TOMGeneratorContextValue = {
state,
dispatch,
currentStepIndex,
totalSteps,
canGoNext,
canGoPrevious,
goToStep,
goToNextStep,
goToPreviousStep,
completeCurrentStep,
setCompanyProfile,
updateCompanyProfile,
setDataProfile,
updateDataProfile,
setArchitectureProfile,
updateArchitectureProfile,
setSecurityProfile,
updateSecurityProfile,
setRiskProfile,
updateRiskProfile,
addEvidence,
updateEvidence,
deleteEvidence,
deriveTOMs,
updateDerivedTOM,
runGapAnalysis,
addExport,
saveState,
loadState,
resetState,
isStepCompleted,
getCompletionPercentage,
isLoading,
error,
}
return (
<TOMGeneratorContext.Provider value={contextValue}>
{children}
</TOMGeneratorContext.Provider>
)
}
// =============================================================================
// HOOK
// =============================================================================
export function useTOMGenerator(): TOMGeneratorContextValue {
const context = useContext(TOMGeneratorContext)
if (!context) {
throw new Error(
'useTOMGenerator must be used within a TOMGeneratorProvider'
)
}
return context
}
// =============================================================================
// EXPORTS
// =============================================================================
export { TOMGeneratorContext }
export type { TOMGeneratorAction, TOMGeneratorContextValue }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,518 @@
// =============================================================================
// TOM Generator Demo Data
// Sample data for demonstration and testing
// =============================================================================
import {
TOMGeneratorState,
CompanyProfile,
DataProfile,
ArchitectureProfile,
SecurityProfile,
RiskProfile,
EvidenceDocument,
DerivedTOM,
GapAnalysisResult,
TOM_GENERATOR_STEPS,
} from '../types'
import { getTOMRulesEngine } from '../rules-engine'
// =============================================================================
// DEMO COMPANY PROFILES
// =============================================================================
export const DEMO_COMPANY_PROFILES: Record<string, CompanyProfile> = {
saas: {
id: 'demo-company-saas',
name: 'CloudTech Solutions GmbH',
industry: 'Software / SaaS',
size: 'MEDIUM',
role: 'PROCESSOR',
products: ['Cloud CRM', 'Analytics Platform', 'API Services'],
dpoPerson: 'Dr. Maria Schmidt',
dpoEmail: 'dpo@cloudtech.de',
itSecurityContact: 'Thomas Müller',
},
healthcare: {
id: 'demo-company-health',
name: 'MediCare Digital GmbH',
industry: 'Gesundheitswesen / HealthTech',
size: 'SMALL',
role: 'CONTROLLER',
products: ['Patientenportal', 'Telemedizin-App', 'Terminbuchung'],
dpoPerson: 'Dr. Klaus Weber',
dpoEmail: 'datenschutz@medicare.de',
itSecurityContact: 'Anna Bauer',
},
enterprise: {
id: 'demo-company-enterprise',
name: 'GlobalCorp AG',
industry: 'Finanzdienstleistungen',
size: 'ENTERPRISE',
role: 'CONTROLLER',
products: ['Online Banking', 'Investment Platform', 'Payment Services'],
dpoPerson: 'Prof. Dr. Hans Meyer',
dpoEmail: 'privacy@globalcorp.de',
itSecurityContact: 'Security Team',
},
}
// =============================================================================
// DEMO DATA PROFILES
// =============================================================================
export const DEMO_DATA_PROFILES: Record<string, DataProfile> = {
saas: {
categories: ['IDENTIFICATION', 'CONTACT', 'PROFESSIONAL', 'BEHAVIORAL'],
subjects: ['CUSTOMERS', 'EMPLOYEES'],
hasSpecialCategories: false,
processesMinors: false,
dataVolume: 'HIGH',
thirdCountryTransfers: true,
thirdCountryList: ['USA'],
},
healthcare: {
categories: ['IDENTIFICATION', 'CONTACT', 'HEALTH', 'BIOMETRIC'],
subjects: ['PATIENTS', 'EMPLOYEES'],
hasSpecialCategories: true,
processesMinors: true,
dataVolume: 'MEDIUM',
thirdCountryTransfers: false,
thirdCountryList: [],
},
enterprise: {
categories: ['IDENTIFICATION', 'CONTACT', 'FINANCIAL', 'BEHAVIORAL'],
subjects: ['CUSTOMERS', 'EMPLOYEES', 'PROSPECTS'],
hasSpecialCategories: false,
processesMinors: false,
dataVolume: 'VERY_HIGH',
thirdCountryTransfers: true,
thirdCountryList: ['USA', 'UK', 'Schweiz'],
},
}
// =============================================================================
// DEMO ARCHITECTURE PROFILES
// =============================================================================
export const DEMO_ARCHITECTURE_PROFILES: Record<string, ArchitectureProfile> = {
saas: {
hostingModel: 'PUBLIC_CLOUD',
hostingLocation: 'EU',
providers: [
{ name: 'AWS', location: 'EU', certifications: ['ISO 27001', 'SOC 2', 'C5'] },
{ name: 'Cloudflare', location: 'EU', certifications: ['ISO 27001'] },
],
multiTenancy: 'MULTI_TENANT',
hasSubprocessors: true,
subprocessorCount: 5,
encryptionAtRest: true,
encryptionInTransit: true,
},
healthcare: {
hostingModel: 'PRIVATE_CLOUD',
hostingLocation: 'DE',
providers: [
{ name: 'Telekom Cloud', location: 'DE', certifications: ['ISO 27001', 'C5', 'TISAX'] },
],
multiTenancy: 'SINGLE_TENANT',
hasSubprocessors: true,
subprocessorCount: 2,
encryptionAtRest: true,
encryptionInTransit: true,
},
enterprise: {
hostingModel: 'HYBRID',
hostingLocation: 'DE',
providers: [
{ name: 'Private Datacenter', location: 'DE', certifications: ['ISO 27001', 'SOC 2'] },
{ name: 'Azure', location: 'EU', certifications: ['ISO 27001', 'C5', 'SOC 2'] },
],
multiTenancy: 'DEDICATED',
hasSubprocessors: true,
subprocessorCount: 10,
encryptionAtRest: true,
encryptionInTransit: true,
},
}
// =============================================================================
// DEMO SECURITY PROFILES
// =============================================================================
export const DEMO_SECURITY_PROFILES: Record<string, SecurityProfile> = {
saas: {
authMethods: [
{ type: 'PASSWORD', provider: null },
{ type: 'MFA', provider: 'Auth0' },
{ type: 'SSO', provider: 'Auth0' },
],
hasMFA: true,
hasSSO: true,
hasIAM: true,
hasPAM: false,
hasEncryptionAtRest: true,
hasEncryptionInTransit: true,
hasLogging: true,
logRetentionDays: 90,
hasBackup: true,
backupFrequency: 'DAILY',
backupRetentionDays: 30,
hasDRPlan: true,
rtoHours: 4,
rpoHours: 1,
hasVulnerabilityManagement: true,
hasPenetrationTests: true,
hasSecurityTraining: true,
},
healthcare: {
authMethods: [
{ type: 'PASSWORD', provider: null },
{ type: 'MFA', provider: 'Microsoft Authenticator' },
{ type: 'CERTIFICATE', provider: 'Internal PKI' },
],
hasMFA: true,
hasSSO: false,
hasIAM: true,
hasPAM: true,
hasEncryptionAtRest: true,
hasEncryptionInTransit: true,
hasLogging: true,
logRetentionDays: 365,
hasBackup: true,
backupFrequency: 'HOURLY',
backupRetentionDays: 90,
hasDRPlan: true,
rtoHours: 2,
rpoHours: 0.5,
hasVulnerabilityManagement: true,
hasPenetrationTests: true,
hasSecurityTraining: true,
},
enterprise: {
authMethods: [
{ type: 'PASSWORD', provider: null },
{ type: 'MFA', provider: 'Okta' },
{ type: 'SSO', provider: 'Okta' },
{ type: 'BIOMETRIC', provider: 'Windows Hello' },
],
hasMFA: true,
hasSSO: true,
hasIAM: true,
hasPAM: true,
hasEncryptionAtRest: true,
hasEncryptionInTransit: true,
hasLogging: true,
logRetentionDays: 730,
hasBackup: true,
backupFrequency: 'HOURLY',
backupRetentionDays: 365,
hasDRPlan: true,
rtoHours: 1,
rpoHours: 0.25,
hasVulnerabilityManagement: true,
hasPenetrationTests: true,
hasSecurityTraining: true,
},
}
// =============================================================================
// DEMO RISK PROFILES
// =============================================================================
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
saas: {
ciaAssessment: {
confidentiality: 3,
integrity: 3,
availability: 4,
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
},
protectionLevel: 'HIGH',
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
hasHighRiskProcessing: false,
dsfaRequired: false,
},
healthcare: {
ciaAssessment: {
confidentiality: 5,
integrity: 5,
availability: 4,
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
enterprise: {
ciaAssessment: {
confidentiality: 4,
integrity: 5,
availability: 5,
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
}
// =============================================================================
// DEMO EVIDENCE DOCUMENTS
// =============================================================================
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
{
id: 'demo-evidence-1',
filename: 'iso27001-certificate.pdf',
originalName: 'ISO 27001 Zertifikat.pdf',
mimeType: 'application/pdf',
size: 245678,
uploadedAt: new Date('2025-01-15'),
uploadedBy: 'admin@company.de',
documentType: 'CERTIFICATE',
detectedType: 'CERTIFICATE',
hash: 'sha256:abc123def456',
validFrom: new Date('2024-06-01'),
validUntil: new Date('2027-05-31'),
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
aiAnalysis: {
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
extractedClauses: [
{
id: 'clause-1',
text: 'Zertifiziert nach ISO/IEC 27001:2022',
type: 'certification',
relatedControlId: 'TOM-RV-04',
},
],
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
gaps: [],
confidence: 0.95,
analyzedAt: new Date('2025-01-15'),
},
status: 'VERIFIED',
},
{
id: 'demo-evidence-2',
filename: 'passwort-richtlinie.pdf',
originalName: 'Passwortrichtlinie v2.1.pdf',
mimeType: 'application/pdf',
size: 128456,
uploadedAt: new Date('2025-01-10'),
uploadedBy: 'admin@company.de',
documentType: 'POLICY',
detectedType: 'POLICY',
hash: 'sha256:xyz789abc012',
validFrom: new Date('2024-09-01'),
validUntil: null,
linkedControlIds: ['TOM-ADM-02'],
aiAnalysis: {
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
extractedClauses: [
{
id: 'clause-1',
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
{
id: 'clause-2',
text: 'Passwörter müssen alle 90 Tage geändert werden',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
],
applicableControls: ['TOM-ADM-02'],
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
confidence: 0.85,
analyzedAt: new Date('2025-01-10'),
},
status: 'ANALYZED',
},
{
id: 'demo-evidence-3',
filename: 'aws-avv.pdf',
originalName: 'AWS Data Processing Addendum.pdf',
mimeType: 'application/pdf',
size: 456789,
uploadedAt: new Date('2025-01-05'),
uploadedBy: 'admin@company.de',
documentType: 'AVV',
detectedType: 'DPA',
hash: 'sha256:qwe123rty456',
validFrom: new Date('2024-01-01'),
validUntil: null,
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
aiAnalysis: {
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
extractedClauses: [
{
id: 'clause-1',
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
type: 'data-processing',
relatedControlId: 'TOM-OR-01',
},
{
id: 'clause-2',
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
type: 'audit',
relatedControlId: 'TOM-OR-02',
},
],
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
gaps: [],
confidence: 0.9,
analyzedAt: new Date('2025-01-05'),
},
status: 'VERIFIED',
},
]
// =============================================================================
// DEMO STATE GENERATOR
// =============================================================================
export type DemoScenario = 'saas' | 'healthcare' | 'enterprise'
/**
* Generate a complete demo state for a given scenario
*/
export function generateDemoState(
tenantId: string,
scenario: DemoScenario = 'saas'
): TOMGeneratorState {
const companyProfile = DEMO_COMPANY_PROFILES[scenario]
const dataProfile = DEMO_DATA_PROFILES[scenario]
const architectureProfile = DEMO_ARCHITECTURE_PROFILES[scenario]
const securityProfile = DEMO_SECURITY_PROFILES[scenario]
const riskProfile = DEMO_RISK_PROFILES[scenario]
// Generate derived TOMs using the rules engine
const rulesEngine = getTOMRulesEngine()
const derivedTOMs = rulesEngine.deriveAllTOMs({
companyProfile,
dataProfile,
architectureProfile,
securityProfile,
riskProfile,
})
// Set some TOMs as implemented for demo
const implementedTOMs = derivedTOMs.map((tom, index) => ({
...tom,
implementationStatus:
index % 3 === 0
? 'IMPLEMENTED' as const
: index % 3 === 1
? 'PARTIAL' as const
: 'NOT_IMPLEMENTED' as const,
responsiblePerson:
index % 2 === 0 ? 'IT Security Team' : 'Datenschutzbeauftragter',
implementationDate:
index % 3 === 0 ? new Date('2024-06-15') : null,
}))
// Generate gap analysis
const gapAnalysis = rulesEngine.performGapAnalysis(
implementedTOMs,
DEMO_EVIDENCE_DOCUMENTS
)
const now = new Date()
return {
id: `demo-state-${scenario}-${Date.now()}`,
tenantId,
companyProfile,
dataProfile,
architectureProfile,
securityProfile,
riskProfile,
currentStep: 'review-export',
steps: TOM_GENERATOR_STEPS.map((step) => ({
id: step.id,
completed: true,
data: null,
validatedAt: now,
})),
documents: DEMO_EVIDENCE_DOCUMENTS,
derivedTOMs: implementedTOMs,
gapAnalysis,
exports: [],
createdAt: now,
updatedAt: now,
}
}
/**
* Generate an empty starter state
*/
export function generateEmptyState(tenantId: string): TOMGeneratorState {
const now = new Date()
return {
id: `new-state-${Date.now()}`,
tenantId,
companyProfile: null,
dataProfile: null,
architectureProfile: null,
securityProfile: null,
riskProfile: null,
currentStep: 'scope-roles',
steps: TOM_GENERATOR_STEPS.map((step) => ({
id: step.id,
completed: false,
data: null,
validatedAt: null,
})),
documents: [],
derivedTOMs: [],
gapAnalysis: null,
exports: [],
createdAt: now,
updatedAt: now,
}
}
/**
* Generate partial state (first 3 steps completed)
*/
export function generatePartialState(
tenantId: string,
scenario: DemoScenario = 'saas'
): TOMGeneratorState {
const state = generateEmptyState(tenantId)
const now = new Date()
state.companyProfile = DEMO_COMPANY_PROFILES[scenario]
state.dataProfile = DEMO_DATA_PROFILES[scenario]
state.architectureProfile = DEMO_ARCHITECTURE_PROFILES[scenario]
state.currentStep = 'security-profile'
state.steps = state.steps.map((step, index) => ({
...step,
completed: index < 3,
validatedAt: index < 3 ? now : null,
}))
return state
}
// =============================================================================
// EXPORTS
// =============================================================================
export {
DEMO_COMPANY_PROFILES as demoCompanyProfiles,
DEMO_DATA_PROFILES as demoDataProfiles,
DEMO_ARCHITECTURE_PROFILES as demoArchitectureProfiles,
DEMO_SECURITY_PROFILES as demoSecurityProfiles,
DEMO_RISK_PROFILES as demoRiskProfiles,
DEMO_EVIDENCE_DOCUMENTS as demoEvidenceDocuments,
}
@@ -0,0 +1,67 @@
// =============================================================================
// TOM Generator Evidence Store
// Shared in-memory storage for evidence documents
// =============================================================================
import { EvidenceDocument, DocumentType } from './types'
interface StoredEvidence {
tenantId: string
documents: EvidenceDocument[]
}
class InMemoryEvidenceStore {
private store: Map<string, StoredEvidence> = new Map()
async getAll(tenantId: string): Promise<EvidenceDocument[]> {
const stored = this.store.get(tenantId)
return stored?.documents || []
}
async getById(tenantId: string, documentId: string): Promise<EvidenceDocument | null> {
const stored = this.store.get(tenantId)
return stored?.documents.find((d) => d.id === documentId) || null
}
async add(tenantId: string, document: EvidenceDocument): Promise<EvidenceDocument> {
const stored = this.store.get(tenantId) || { tenantId, documents: [] }
stored.documents.push(document)
this.store.set(tenantId, stored)
return document
}
async update(tenantId: string, documentId: string, updates: Partial<EvidenceDocument>): Promise<EvidenceDocument | null> {
const stored = this.store.get(tenantId)
if (!stored) return null
const index = stored.documents.findIndex((d) => d.id === documentId)
if (index === -1) return null
stored.documents[index] = { ...stored.documents[index], ...updates }
this.store.set(tenantId, stored)
return stored.documents[index]
}
async delete(tenantId: string, documentId: string): Promise<boolean> {
const stored = this.store.get(tenantId)
if (!stored) return false
const initialLength = stored.documents.length
stored.documents = stored.documents.filter((d) => d.id !== documentId)
this.store.set(tenantId, stored)
return stored.documents.length < initialLength
}
async getByType(tenantId: string, type: DocumentType): Promise<EvidenceDocument[]> {
const stored = this.store.get(tenantId)
return stored?.documents.filter((d) => d.documentType === type) || []
}
async getByStatus(tenantId: string, status: string): Promise<EvidenceDocument[]> {
const stored = this.store.get(tenantId)
return stored?.documents.filter((d) => d.status === status) || []
}
}
// Singleton instance for the application
export const evidenceStore = new InMemoryEvidenceStore()
@@ -0,0 +1,525 @@
// =============================================================================
// TOM Generator DOCX Export
// Export TOMs to Microsoft Word format
// =============================================================================
import {
TOMGeneratorState,
DerivedTOM,
ControlCategory,
CONTROL_CATEGORIES,
} from '../types'
import { getControlById, getCategoryMetadata } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface DOCXExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
companyLogo?: string
primaryColor?: string
}
const DEFAULT_OPTIONS: DOCXExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
primaryColor: '#1a56db',
}
// =============================================================================
// DOCX CONTENT GENERATION
// =============================================================================
export interface DocxParagraph {
type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet'
content: string
style?: Record<string, string>
}
export interface DocxTableRow {
cells: string[]
isHeader?: boolean
}
export interface DocxTable {
type: 'table'
headers: string[]
rows: DocxTableRow[]
}
export type DocxElement = DocxParagraph | DocxTable
/**
* Generate DOCX content structure for TOMs
*/
export function generateDOCXContent(
state: TOMGeneratorState,
options: Partial<DOCXExportOptions> = {}
): DocxElement[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const elements: DocxElement[] = []
// Title page
elements.push({
type: 'heading1',
content: opts.language === 'de'
? 'Technische und Organisatorische Maßnahmen (TOMs)'
: 'Technical and Organizational Measures (TOMs)',
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `gemäß Art. 32 DSGVO`
: 'according to Art. 32 GDPR',
})
// Company info
if (state.companyProfile) {
elements.push({
type: 'heading2',
content: opts.language === 'de' ? 'Unternehmen' : 'Company',
})
elements.push({
type: 'paragraph',
content: `${state.companyProfile.name}`,
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Branche: ${state.companyProfile.industry}`
: `Industry: ${state.companyProfile.industry}`,
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Rolle: ${formatRole(state.companyProfile.role, opts.language)}`
: `Role: ${formatRole(state.companyProfile.role, opts.language)}`,
})
if (state.companyProfile.dpoPerson) {
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Datenschutzbeauftragter: ${state.companyProfile.dpoPerson}`
: `Data Protection Officer: ${state.companyProfile.dpoPerson}`,
})
}
}
// Document metadata
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Stand: ${new Date().toLocaleDateString('de-DE')}`
: `Date: ${new Date().toLocaleDateString('en-US')}`,
})
// Protection level summary
if (state.riskProfile) {
elements.push({
type: 'heading2',
content: opts.language === 'de' ? 'Schutzbedarf' : 'Protection Level',
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Ermittelter Schutzbedarf: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`
: `Determined Protection Level: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`,
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `CIA-Bewertung: Vertraulichkeit ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrität ${state.riskProfile.ciaAssessment.integrity}/5, Verfügbarkeit ${state.riskProfile.ciaAssessment.availability}/5`
: `CIA Assessment: Confidentiality ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrity ${state.riskProfile.ciaAssessment.integrity}/5, Availability ${state.riskProfile.ciaAssessment.availability}/5`,
})
if (state.riskProfile.dsfaRequired) {
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? '⚠️ Eine Datenschutz-Folgenabschätzung (DSFA) ist erforderlich.'
: '⚠️ A Data Protection Impact Assessment (DPIA) is required.',
})
}
}
// TOMs by category
elements.push({
type: 'heading2',
content: opts.language === 'de'
? 'Übersicht der Maßnahmen'
: 'Measures Overview',
})
// Group TOMs by category
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs, opts.includeNotApplicable)
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = tomsByCategory.get(category.id)
if (!categoryTOMs || categoryTOMs.length === 0) continue
const categoryName = category.name[opts.language]
elements.push({
type: 'heading3',
content: `${categoryName} (${category.gdprReference})`,
})
// Create table for this category
const tableHeaders = opts.language === 'de'
? ['ID', 'Maßnahme', 'Typ', 'Status', 'Anwendbarkeit']
: ['ID', 'Measure', 'Type', 'Status', 'Applicability']
const tableRows: DocxTableRow[] = categoryTOMs.map((tom) => ({
cells: [
tom.controlId,
tom.name,
formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language),
formatImplementationStatus(tom.implementationStatus, opts.language),
formatApplicability(tom.applicability, opts.language),
],
}))
elements.push({
type: 'table',
headers: tableHeaders,
rows: tableRows,
})
// Add detailed descriptions
for (const tom of categoryTOMs) {
if (tom.applicability === 'NOT_APPLICABLE' && !opts.includeNotApplicable) {
continue
}
elements.push({
type: 'paragraph',
content: `**${tom.controlId}: ${tom.name}**`,
})
elements.push({
type: 'paragraph',
content: tom.aiGeneratedDescription || tom.description,
})
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Anwendbarkeit: ${formatApplicability(tom.applicability, opts.language)}`
: `Applicability: ${formatApplicability(tom.applicability, opts.language)}`,
})
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Begründung: ${tom.applicabilityReason}`
: `Reason: ${tom.applicabilityReason}`,
})
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Umsetzungsstatus: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`
: `Implementation Status: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`,
})
if (tom.responsiblePerson) {
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Verantwortlich: ${tom.responsiblePerson}`
: `Responsible: ${tom.responsiblePerson}`,
})
}
if (opts.includeEvidence && tom.linkedEvidence.length > 0) {
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Nachweise: ${tom.linkedEvidence.length} Dokument(e) verknüpft`
: `Evidence: ${tom.linkedEvidence.length} document(s) linked`,
})
}
if (tom.evidenceGaps.length > 0) {
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Fehlende Nachweise: ${tom.evidenceGaps.join(', ')}`
: `Missing Evidence: ${tom.evidenceGaps.join(', ')}`,
})
}
}
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
elements.push({
type: 'heading2',
content: opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis',
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Gesamtscore: ${state.gapAnalysis.overallScore}%`
: `Overall Score: ${state.gapAnalysis.overallScore}%`,
})
if (state.gapAnalysis.missingControls.length > 0) {
elements.push({
type: 'heading3',
content: opts.language === 'de'
? 'Fehlende Maßnahmen'
: 'Missing Measures',
})
for (const missing of state.gapAnalysis.missingControls) {
const control = getControlById(missing.controlId)
elements.push({
type: 'bullet',
content: `${missing.controlId}: ${control?.name[opts.language] || 'Unknown'} (${missing.priority})`,
})
}
}
if (state.gapAnalysis.recommendations.length > 0) {
elements.push({
type: 'heading3',
content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations',
})
for (const rec of state.gapAnalysis.recommendations) {
elements.push({
type: 'bullet',
content: rec,
})
}
}
}
// Footer
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Dieses Dokument wurde automatisch generiert mit dem TOM Generator am ${new Date().toLocaleDateString('de-DE')}.`
: `This document was automatically generated with the TOM Generator on ${new Date().toLocaleDateString('en-US')}.`,
})
return elements
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function groupTOMsByCategory(
toms: DerivedTOM[],
includeNotApplicable: boolean
): Map<ControlCategory, DerivedTOM[]> {
const grouped = new Map<ControlCategory, DerivedTOM[]>()
for (const tom of toms) {
if (!includeNotApplicable && tom.applicability === 'NOT_APPLICABLE') {
continue
}
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
const existing = grouped.get(category) || []
existing.push(tom)
grouped.set(category, existing)
}
return grouped
}
function formatRole(role: string, language: 'de' | 'en'): string {
const roles: Record<string, Record<'de' | 'en', string>> = {
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
}
return roles[role]?.[language] || role
}
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
const levels: Record<string, Record<'de' | 'en', string>> = {
NORMAL: { de: 'Normal', en: 'Normal' },
HIGH: { de: 'Hoch', en: 'High' },
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
}
return levels[level]?.[language] || level
}
function formatType(type: string, language: 'de' | 'en'): string {
const types: Record<string, Record<'de' | 'en', string>> = {
TECHNICAL: { de: 'Technisch', en: 'Technical' },
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
}
return types[type]?.[language] || type
}
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
const statuses: Record<string, Record<'de' | 'en', string>> = {
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
PARTIAL: { de: 'Teilweise umgesetzt', en: 'Partially Implemented' },
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
}
return statuses[status]?.[language] || status
}
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
const apps: Record<string, Record<'de' | 'en', string>> = {
REQUIRED: { de: 'Erforderlich', en: 'Required' },
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
OPTIONAL: { de: 'Optional', en: 'Optional' },
NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' },
}
return apps[applicability]?.[language] || applicability
}
// =============================================================================
// DOCX BLOB GENERATION
// Uses simple XML structure compatible with docx libraries
// =============================================================================
/**
* Generate a DOCX file as a Blob
* Note: For production, use docx library (npm install docx)
* This is a simplified version that generates XML-based content
*/
export async function generateDOCXBlob(
state: TOMGeneratorState,
options: Partial<DOCXExportOptions> = {}
): Promise<Blob> {
const content = generateDOCXContent(state, options)
// Generate simple HTML that can be converted to DOCX
// In production, use the docx library for proper DOCX generation
const html = generateHTMLFromContent(content, options)
// Return as a Word-compatible HTML blob
// The proper way would be to use the docx library
const blob = new Blob([html], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
return blob
}
function generateHTMLFromContent(
content: DocxElement[],
options: Partial<DOCXExportOptions>
): string {
const opts = { ...DEFAULT_OPTIONS, ...options }
let html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.5; }
h1 { font-size: 24pt; color: ${opts.primaryColor}; border-bottom: 2px solid ${opts.primaryColor}; }
h2 { font-size: 18pt; color: ${opts.primaryColor}; margin-top: 24pt; }
h3 { font-size: 14pt; color: #333; margin-top: 18pt; }
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
th, td { border: 1px solid #ddd; padding: 8pt; text-align: left; }
th { background-color: ${opts.primaryColor}; color: white; }
tr:nth-child(even) { background-color: #f9f9f9; }
ul { margin: 6pt 0; }
li { margin: 3pt 0; }
.warning { color: #dc2626; font-weight: bold; }
</style>
</head>
<body>
`
for (const element of content) {
if (element.type === 'table') {
html += '<table>'
html += '<tr>'
for (const header of element.headers) {
html += `<th>${escapeHtml(header)}</th>`
}
html += '</tr>'
for (const row of element.rows) {
html += '<tr>'
for (const cell of row.cells) {
html += `<td>${escapeHtml(cell)}</td>`
}
html += '</tr>'
}
html += '</table>'
} else {
const tag = getHtmlTag(element.type)
const processedContent = processContent(element.content)
html += `<${tag}>${processedContent}</${tag}>\n`
}
}
html += '</body></html>'
return html
}
function getHtmlTag(type: string): string {
switch (type) {
case 'heading1':
return 'h1'
case 'heading2':
return 'h2'
case 'heading3':
return 'h3'
case 'bullet':
return 'li'
default:
return 'p'
}
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function processContent(content: string): string {
// Convert markdown-style bold to HTML
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the DOCX export
*/
export function generateDOCXFilename(
state: TOMGeneratorState,
language: 'de' | 'en' = 'de'
): string {
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'TOMs' : 'TOMs'
return `${prefix}-${companyName}-${date}.docx`
}
// Types are exported at their definition site above
@@ -0,0 +1,517 @@
// =============================================================================
// TOM Generator PDF Export
// Export TOMs to PDF format
// =============================================================================
import {
TOMGeneratorState,
DerivedTOM,
CONTROL_CATEGORIES,
} from '../types'
import { getControlById } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface PDFExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
companyLogo?: string
primaryColor?: string
pageSize?: 'A4' | 'LETTER'
orientation?: 'portrait' | 'landscape'
}
const DEFAULT_OPTIONS: PDFExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
primaryColor: '#1a56db',
pageSize: 'A4',
orientation: 'portrait',
}
// =============================================================================
// PDF CONTENT STRUCTURE
// =============================================================================
export interface PDFSection {
type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak'
content?: string
items?: string[]
table?: {
headers: string[]
rows: string[][]
}
style?: {
color?: string
fontSize?: number
bold?: boolean
italic?: boolean
align?: 'left' | 'center' | 'right'
}
}
/**
* Generate PDF content structure for TOMs
*/
export function generatePDFContent(
state: TOMGeneratorState,
options: Partial<PDFExportOptions> = {}
): PDFSection[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const sections: PDFSection[] = []
// Title page
sections.push({
type: 'title',
content: opts.language === 'de'
? 'Technische und Organisatorische Maßnahmen (TOMs)'
: 'Technical and Organizational Measures (TOMs)',
style: { color: opts.primaryColor, fontSize: 24, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? 'gemäß Art. 32 DSGVO'
: 'according to Art. 32 GDPR',
style: { fontSize: 14, align: 'center' },
})
// Company information
if (state.companyProfile) {
sections.push({
type: 'paragraph',
content: state.companyProfile.name,
style: { fontSize: 16, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${opts.language === 'de' ? 'Branche' : 'Industry'}: ${state.companyProfile.industry}`,
style: { align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${opts.language === 'de' ? 'Stand' : 'Date'}: ${new Date().toLocaleDateString(opts.language === 'de' ? 'de-DE' : 'en-US')}`,
style: { align: 'center' },
})
}
sections.push({ type: 'pagebreak' })
// Table of Contents
sections.push({
type: 'heading',
content: opts.language === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
style: { color: opts.primaryColor },
})
const tocItems = [
opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary',
opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level',
opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview',
]
let sectionNum = 4
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
return control?.category === category.id &&
(opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
})
if (categoryTOMs.length > 0) {
tocItems.push(`${sectionNum}. ${category.name[opts.language]}`)
sectionNum++
}
}
if (opts.includeGapAnalysis && state.gapAnalysis) {
tocItems.push(`${sectionNum}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`)
}
sections.push({
type: 'list',
items: tocItems,
})
sections.push({ type: 'pagebreak' })
// Executive Summary
sections.push({
type: 'heading',
content: opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary',
style: { color: opts.primaryColor },
})
const totalTOMs = state.derivedTOMs.length
const requiredTOMs = state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length
const implementedTOMs = state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Dieses Dokument beschreibt die technischen und organisatorischen Maßnahmen (TOMs) gemäß Art. 32 DSGVO. Insgesamt wurden ${totalTOMs} Kontrollen bewertet, davon ${requiredTOMs} als erforderlich eingestuft. Aktuell sind ${implementedTOMs} Maßnahmen vollständig umgesetzt.`
: `This document describes the technical and organizational measures (TOMs) according to Art. 32 GDPR. A total of ${totalTOMs} controls were evaluated, of which ${requiredTOMs} are classified as required. Currently, ${implementedTOMs} measures are fully implemented.`,
})
// Summary statistics table
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['Kategorie', 'Anzahl', 'Erforderlich', 'Umgesetzt']
: ['Category', 'Count', 'Required', 'Implemented'],
rows: generateCategorySummary(state.derivedTOMs, opts),
},
})
// Protection Level
sections.push({
type: 'heading',
content: opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level',
style: { color: opts.primaryColor },
})
if (state.riskProfile) {
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Der ermittelte Schutzbedarf beträgt: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`
: `The determined protection level is: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`,
})
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['Schutzziel', 'Bewertung (1-5)', 'Bedeutung']
: ['Protection Goal', 'Rating (1-5)', 'Meaning'],
rows: [
[
opts.language === 'de' ? 'Vertraulichkeit' : 'Confidentiality',
String(state.riskProfile.ciaAssessment.confidentiality),
getCIAMeaning(state.riskProfile.ciaAssessment.confidentiality, opts.language),
],
[
opts.language === 'de' ? 'Integrität' : 'Integrity',
String(state.riskProfile.ciaAssessment.integrity),
getCIAMeaning(state.riskProfile.ciaAssessment.integrity, opts.language),
],
[
opts.language === 'de' ? 'Verfügbarkeit' : 'Availability',
String(state.riskProfile.ciaAssessment.availability),
getCIAMeaning(state.riskProfile.ciaAssessment.availability, opts.language),
],
],
},
})
if (state.riskProfile.dsfaRequired) {
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? '⚠️ HINWEIS: Aufgrund der Verarbeitung ist eine Datenschutz-Folgenabschätzung (DSFA) nach Art. 35 DSGVO erforderlich.'
: '⚠️ NOTE: Due to the processing, a Data Protection Impact Assessment (DPIA) according to Art. 35 GDPR is required.',
style: { bold: true, color: '#dc2626' },
})
}
}
// Measures Overview
sections.push({
type: 'heading',
content: opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview',
style: { color: opts.primaryColor },
})
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['ID', 'Maßnahme', 'Anwendbarkeit', 'Status']
: ['ID', 'Measure', 'Applicability', 'Status'],
rows: state.derivedTOMs
.filter((tom) => opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
.map((tom) => [
tom.controlId,
tom.name,
formatApplicability(tom.applicability, opts.language),
formatImplementationStatus(tom.implementationStatus, opts.language),
]),
},
})
// Detailed sections by category
let currentSection = 4
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
return control?.category === category.id &&
(opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
})
if (categoryTOMs.length === 0) continue
sections.push({ type: 'pagebreak' })
sections.push({
type: 'heading',
content: `${currentSection}. ${category.name[opts.language]}`,
style: { color: opts.primaryColor },
})
sections.push({
type: 'paragraph',
content: `${opts.language === 'de' ? 'Rechtsgrundlage' : 'Legal Basis'}: ${category.gdprReference}`,
style: { italic: true },
})
for (const tom of categoryTOMs) {
sections.push({
type: 'subheading',
content: `${tom.controlId}: ${tom.name}`,
})
sections.push({
type: 'paragraph',
content: tom.aiGeneratedDescription || tom.description,
})
sections.push({
type: 'list',
items: [
`${opts.language === 'de' ? 'Typ' : 'Type'}: ${formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language)}`,
`${opts.language === 'de' ? 'Anwendbarkeit' : 'Applicability'}: ${formatApplicability(tom.applicability, opts.language)}`,
`${opts.language === 'de' ? 'Begründung' : 'Reason'}: ${tom.applicabilityReason}`,
`${opts.language === 'de' ? 'Umsetzungsstatus' : 'Implementation Status'}: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`,
...(tom.responsiblePerson ? [`${opts.language === 'de' ? 'Verantwortlich' : 'Responsible'}: ${tom.responsiblePerson}`] : []),
...(opts.includeEvidence && tom.linkedEvidence.length > 0
? [`${opts.language === 'de' ? 'Verknüpfte Nachweise' : 'Linked Evidence'}: ${tom.linkedEvidence.length}`]
: []),
],
})
}
currentSection++
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
sections.push({ type: 'pagebreak' })
sections.push({
type: 'heading',
content: `${currentSection}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`,
style: { color: opts.primaryColor },
})
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Gesamtscore: ${state.gapAnalysis.overallScore}%`
: `Overall Score: ${state.gapAnalysis.overallScore}%`,
style: { fontSize: 16, bold: true },
})
if (state.gapAnalysis.missingControls.length > 0) {
sections.push({
type: 'subheading',
content: opts.language === 'de' ? 'Fehlende Maßnahmen' : 'Missing Measures',
})
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['ID', 'Maßnahme', 'Priorität']
: ['ID', 'Measure', 'Priority'],
rows: state.gapAnalysis.missingControls.map((mc) => {
const control = getControlById(mc.controlId)
return [
mc.controlId,
control?.name[opts.language] || 'Unknown',
mc.priority,
]
}),
},
})
}
if (state.gapAnalysis.recommendations.length > 0) {
sections.push({
type: 'subheading',
content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations',
})
sections.push({
type: 'list',
items: state.gapAnalysis.recommendations,
})
}
}
// Footer
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem TOM Generator`
: `Generated on ${new Date().toLocaleDateString('en-US')} with the TOM Generator`,
style: { italic: true, align: 'center', fontSize: 10 },
})
return sections
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateCategorySummary(
toms: DerivedTOM[],
opts: PDFExportOptions
): string[][] {
const summary: string[][] = []
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = toms.filter((tom) => {
const control = getControlById(tom.controlId)
return control?.category === category.id
})
if (categoryTOMs.length === 0) continue
const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length
const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
summary.push([
category.name[opts.language],
String(categoryTOMs.length),
String(required),
String(implemented),
])
}
return summary
}
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
const levels: Record<string, Record<'de' | 'en', string>> = {
NORMAL: { de: 'Normal', en: 'Normal' },
HIGH: { de: 'Hoch', en: 'High' },
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
}
return levels[level]?.[language] || level
}
function formatType(type: string, language: 'de' | 'en'): string {
const types: Record<string, Record<'de' | 'en', string>> = {
TECHNICAL: { de: 'Technisch', en: 'Technical' },
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
}
return types[type]?.[language] || type
}
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
const statuses: Record<string, Record<'de' | 'en', string>> = {
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
PARTIAL: { de: 'Teilweise', en: 'Partial' },
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
}
return statuses[status]?.[language] || status
}
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
const apps: Record<string, Record<'de' | 'en', string>> = {
REQUIRED: { de: 'Erforderlich', en: 'Required' },
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
OPTIONAL: { de: 'Optional', en: 'Optional' },
NOT_APPLICABLE: { de: 'N/A', en: 'N/A' },
}
return apps[applicability]?.[language] || applicability
}
function getCIAMeaning(rating: number, language: 'de' | 'en'): string {
const meanings: Record<number, Record<'de' | 'en', string>> = {
1: { de: 'Sehr gering', en: 'Very Low' },
2: { de: 'Gering', en: 'Low' },
3: { de: 'Mittel', en: 'Medium' },
4: { de: 'Hoch', en: 'High' },
5: { de: 'Sehr hoch', en: 'Very High' },
}
return meanings[rating]?.[language] || String(rating)
}
// =============================================================================
// PDF BLOB GENERATION
// Note: For production, use jspdf or pdfmake library
// =============================================================================
/**
* Generate a PDF file as a Blob
* This is a placeholder - in production, use jspdf or similar library
*/
export async function generatePDFBlob(
state: TOMGeneratorState,
options: Partial<PDFExportOptions> = {}
): Promise<Blob> {
const content = generatePDFContent(state, options)
// Convert to simple text-based content for now
// In production, use jspdf library
const textContent = content
.map((section) => {
switch (section.type) {
case 'title':
return `\n\n${'='.repeat(60)}\n${section.content}\n${'='.repeat(60)}\n`
case 'heading':
return `\n\n${section.content}\n${'-'.repeat(40)}\n`
case 'subheading':
return `\n${section.content}\n`
case 'paragraph':
return `${section.content}\n`
case 'list':
return section.items?.map((item) => `${item}`).join('\n') + '\n'
case 'table':
if (section.table) {
const headerLine = section.table.headers.join(' | ')
const separator = '-'.repeat(headerLine.length)
const rows = section.table.rows.map((row) => row.join(' | ')).join('\n')
return `\n${headerLine}\n${separator}\n${rows}\n`
}
return ''
case 'pagebreak':
return '\n\n' + '='.repeat(60) + '\n\n'
default:
return ''
}
})
.join('')
return new Blob([textContent], { type: 'application/pdf' })
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the PDF export
*/
export function generatePDFFilename(
state: TOMGeneratorState,
language: 'de' | 'en' = 'de'
): string {
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'TOMs' : 'TOMs'
return `${prefix}-${companyName}-${date}.pdf`
}
// Types are exported at their definition site above
@@ -0,0 +1,544 @@
// =============================================================================
// TOM Generator ZIP Export
// Export complete TOM package as ZIP archive
// =============================================================================
import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
import { generateDOCXContent, DOCXExportOptions } from './docx'
import { generatePDFContent, PDFExportOptions } from './pdf'
import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface ZIPExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
includeControlLibrary: boolean
includeRawData: boolean
formats: Array<'json' | 'docx' | 'pdf'>
}
const DEFAULT_OPTIONS: ZIPExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
includeControlLibrary: true,
includeRawData: true,
formats: ['json', 'docx'],
}
// =============================================================================
// ZIP CONTENT STRUCTURE
// =============================================================================
export interface ZIPFileEntry {
path: string
content: string | Blob
mimeType: string
}
/**
* Generate all files for the ZIP archive
*/
export function generateZIPFiles(
state: TOMGeneratorState,
options: Partial<ZIPExportOptions> = {}
): ZIPFileEntry[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const files: ZIPFileEntry[] = []
// README
files.push({
path: 'README.md',
content: generateReadme(state, opts),
mimeType: 'text/markdown',
})
// State JSON
if (opts.includeRawData) {
files.push({
path: 'data/state.json',
content: JSON.stringify(state, null, 2),
mimeType: 'application/json',
})
}
// Profile data
files.push({
path: 'data/profiles/company-profile.json',
content: JSON.stringify(state.companyProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/data-profile.json',
content: JSON.stringify(state.dataProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/architecture-profile.json',
content: JSON.stringify(state.architectureProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/security-profile.json',
content: JSON.stringify(state.securityProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/risk-profile.json',
content: JSON.stringify(state.riskProfile, null, 2),
mimeType: 'application/json',
})
// Derived TOMs
files.push({
path: 'data/toms/derived-toms.json',
content: JSON.stringify(state.derivedTOMs, null, 2),
mimeType: 'application/json',
})
// TOMs by category
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs)
for (const [category, toms] of tomsByCategory.entries()) {
files.push({
path: `data/toms/by-category/${category.toLowerCase()}.json`,
content: JSON.stringify(toms, null, 2),
mimeType: 'application/json',
})
}
// Required TOMs summary
const requiredTOMs = state.derivedTOMs.filter(
(tom) => tom.applicability === 'REQUIRED'
)
files.push({
path: 'data/toms/required-toms.json',
content: JSON.stringify(requiredTOMs, null, 2),
mimeType: 'application/json',
})
// Implementation status summary
const implementationSummary = generateImplementationSummary(state.derivedTOMs)
files.push({
path: 'data/toms/implementation-summary.json',
content: JSON.stringify(implementationSummary, null, 2),
mimeType: 'application/json',
})
// Evidence documents
if (opts.includeEvidence && state.documents.length > 0) {
files.push({
path: 'data/evidence/documents.json',
content: JSON.stringify(state.documents, null, 2),
mimeType: 'application/json',
})
// Evidence by control
const evidenceByControl = groupEvidenceByControl(state.documents)
files.push({
path: 'data/evidence/by-control.json',
content: JSON.stringify(Object.fromEntries(evidenceByControl), null, 2),
mimeType: 'application/json',
})
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
files.push({
path: 'data/gap-analysis/analysis.json',
content: JSON.stringify(state.gapAnalysis, null, 2),
mimeType: 'application/json',
})
// Missing controls details
if (state.gapAnalysis.missingControls.length > 0) {
files.push({
path: 'data/gap-analysis/missing-controls.json',
content: JSON.stringify(state.gapAnalysis.missingControls, null, 2),
mimeType: 'application/json',
})
}
// Recommendations
if (state.gapAnalysis.recommendations.length > 0) {
files.push({
path: 'data/gap-analysis/recommendations.md',
content: generateRecommendationsMarkdown(
state.gapAnalysis.recommendations,
opts.language
),
mimeType: 'text/markdown',
})
}
}
// Control Library
if (opts.includeControlLibrary) {
const controls = getAllControls()
const metadata = getLibraryMetadata()
files.push({
path: 'reference/control-library/metadata.json',
content: JSON.stringify(metadata, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'reference/control-library/all-controls.json',
content: JSON.stringify(controls, null, 2),
mimeType: 'application/json',
})
// Controls by category
for (const category of new Set(controls.map((c) => c.category))) {
const categoryControls = controls.filter((c) => c.category === category)
files.push({
path: `reference/control-library/by-category/${category.toLowerCase()}.json`,
content: JSON.stringify(categoryControls, null, 2),
mimeType: 'application/json',
})
}
}
// Export history
if (state.exports.length > 0) {
files.push({
path: 'data/exports/history.json',
content: JSON.stringify(state.exports, null, 2),
mimeType: 'application/json',
})
}
// DOCX content structure (if requested)
if (opts.formats.includes('docx')) {
const docxOptions: Partial<DOCXExportOptions> = {
language: opts.language,
includeNotApplicable: opts.includeNotApplicable,
includeEvidence: opts.includeEvidence,
includeGapAnalysis: opts.includeGapAnalysis,
}
const docxContent = generateDOCXContent(state, docxOptions)
files.push({
path: 'documents/tom-document-structure.json',
content: JSON.stringify(docxContent, null, 2),
mimeType: 'application/json',
})
}
// PDF content structure (if requested)
if (opts.formats.includes('pdf')) {
const pdfOptions: Partial<PDFExportOptions> = {
language: opts.language,
includeNotApplicable: opts.includeNotApplicable,
includeEvidence: opts.includeEvidence,
includeGapAnalysis: opts.includeGapAnalysis,
}
const pdfContent = generatePDFContent(state, pdfOptions)
files.push({
path: 'documents/tom-document-structure-pdf.json',
content: JSON.stringify(pdfContent, null, 2),
mimeType: 'application/json',
})
}
// Markdown summary
files.push({
path: 'documents/tom-summary.md',
content: generateMarkdownSummary(state, opts),
mimeType: 'text/markdown',
})
// CSV export for spreadsheet import
files.push({
path: 'documents/toms.csv',
content: generateCSV(state.derivedTOMs, opts),
mimeType: 'text/csv',
})
return files
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateReadme(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const date = new Date().toISOString().split('T')[0]
const lang = opts.language
return `# TOM Export Package
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
### /data
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
### /reference
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
### /documents
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
---
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
`
}
function groupTOMsByCategory(
toms: DerivedTOM[]
): Map<string, DerivedTOM[]> {
const grouped = new Map<string, DerivedTOM[]>()
for (const tom of toms) {
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
const existing: DerivedTOM[] = grouped.get(category) || []
existing.push(tom)
grouped.set(category, existing)
}
return grouped
}
function generateImplementationSummary(
toms: Array<{ implementationStatus: string; applicability: string }>
): Record<string, number> {
return {
total: toms.length,
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
}
}
function groupEvidenceByControl(
documents: Array<{ id: string; linkedControlIds: string[] }>
): Map<string, string[]> {
const grouped = new Map<string, string[]>()
for (const doc of documents) {
for (const controlId of doc.linkedControlIds) {
const existing = grouped.get(controlId) || []
existing.push(doc.id)
grouped.set(controlId, existing)
}
}
return grouped
}
function generateRecommendationsMarkdown(
recommendations: string[],
language: 'de' | 'en'
): string {
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
return `# ${title}
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
---
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
`
}
function generateMarkdownSummary(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const lang = opts.language
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|--------|-------|
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
`
if (state.gapAnalysis) {
md += `
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
**${state.gapAnalysis.overallScore}%**
`
}
// Add required TOMs table
const requiredTOMs = state.derivedTOMs.filter(
(t) => t.applicability === 'REQUIRED'
)
if (requiredTOMs.length > 0) {
md += `
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|----|----------|--------|
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
`
}
return md
}
function generateCSV(
toms: Array<{
controlId: string
name: string
description: string
applicability: string
implementationStatus: string
responsiblePerson: string | null
}>,
opts: ZIPExportOptions
): string {
const lang = opts.language
const headers = lang === 'de'
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
const rows = toms.map((tom) => [
tom.controlId,
escapeCSV(tom.name),
escapeCSV(tom.description),
tom.applicability,
tom.implementationStatus,
tom.responsiblePerson || '',
])
return [
headers.join(','),
...rows.map((row) => row.join(',')),
].join('\n')
}
function escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
function formatStatus(status: string, lang: 'de' | 'en'): string {
const statuses: Record<string, Record<'de' | 'en', string>> = {
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
PARTIAL: { de: 'Teilweise', en: 'Partial' },
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
}
return statuses[status]?.[lang] || status
}
// =============================================================================
// ZIP BLOB GENERATION
// Note: For production, use jszip library
// =============================================================================
/**
* Generate a ZIP file as a Blob
* This is a placeholder - in production, use jszip library
*/
export async function generateZIPBlob(
state: TOMGeneratorState,
options: Partial<ZIPExportOptions> = {}
): Promise<Blob> {
const files = generateZIPFiles(state, options)
// Create a simple JSON representation for now
// In production, use JSZip library
const manifest = {
generated: new Date().toISOString(),
files: files.map((f) => ({
path: f.path,
mimeType: f.mimeType,
size: typeof f.content === 'string' ? f.content.length : 0,
})),
}
const allContent = files
.filter((f) => typeof f.content === 'string')
.map((f) => `\n\n=== ${f.path} ===\n\n${f.content}`)
.join('\n')
const output = `TOM Export Package
Generated: ${manifest.generated}
Files:
${manifest.files.map((f) => ` - ${f.path} (${f.mimeType})`).join('\n')}
${allContent}`
return new Blob([output], { type: 'application/zip' })
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for ZIP export
*/
export function generateZIPFilename(
state: TOMGeneratorState,
language: 'de' | 'en' = 'de'
): string {
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'TOMs-Export' : 'TOMs-Export'
return `${prefix}-${companyName}-${date}.zip`
}
// =============================================================================
// EXPORT
// =============================================================================
// Types are exported at their definition site above
+206
View File
@@ -0,0 +1,206 @@
// =============================================================================
// TOM Generator Module - Public API
// =============================================================================
// Types
export * from './types'
// Context and Hooks
export {
TOMGeneratorProvider,
useTOMGenerator,
TOMGeneratorContext,
} from './context'
export type {
TOMGeneratorAction,
TOMGeneratorContextValue,
} from './context'
// Rules Engine
export {
TOMRulesEngine,
getTOMRulesEngine,
evaluateControlsForContext,
deriveTOMsForContext,
performQuickGapAnalysis,
} from './rules-engine'
// Control Library
export {
getControlLibrary,
getAllControls,
getControlById,
getControlsByCategory,
getControlsByType,
getControlsByPriority,
getControlsByTag,
getAllTags,
getCategoryMetadata,
getAllCategories,
getLibraryMetadata,
searchControls,
getControlsByFramework,
getControlsCountByCategory,
} from './controls/loader'
export type { ControlLibrary } from './controls/loader'
// AI Integration
export {
AI_PROMPTS,
getDocumentAnalysisPrompt,
getTOMDescriptionPrompt,
getGapRecommendationsPrompt,
getDocumentTypeDetectionPrompt,
getClauseExtractionPrompt,
getComplianceAssessmentPrompt,
} from './ai/prompts'
export type {
DocumentAnalysisPromptContext,
TOMDescriptionPromptContext,
GapRecommendationsPromptContext,
} from './ai/prompts'
export {
TOMDocumentAnalyzer,
getDocumentAnalyzer,
analyzeEvidenceDocument,
detectEvidenceDocumentType,
getEvidenceGapsForAllControls,
} from './ai/document-analyzer'
export type {
AnalysisResult,
DocumentTypeDetectionResult,
} from './ai/document-analyzer'
// Export Functions
export {
generateDOCXContent,
generateDOCXBlob,
} from './export/docx'
export type {
DOCXExportOptions,
DocxElement,
DocxParagraph,
DocxTable,
DocxTableRow,
} from './export/docx'
export {
generatePDFContent,
generatePDFBlob,
} from './export/pdf'
export type {
PDFExportOptions,
PDFSection,
} from './export/pdf'
export {
generateZIPFiles,
generateZIPBlob,
} from './export/zip'
export type {
ZIPExportOptions,
ZIPFileEntry,
} from './export/zip'
// Demo Data
export {
generateDemoState,
generateEmptyState,
generatePartialState,
DEMO_COMPANY_PROFILES,
DEMO_DATA_PROFILES,
DEMO_ARCHITECTURE_PROFILES,
DEMO_SECURITY_PROFILES,
DEMO_RISK_PROFILES,
DEMO_EVIDENCE_DOCUMENTS,
} from './demo-data'
export type { DemoScenario } from './demo-data'
// =============================================================================
// CONVENIENCE EXPORTS
// =============================================================================
import { TOMRulesEngine } from './rules-engine'
import {
TOMGeneratorState,
RulesEngineEvaluationContext,
DerivedTOM,
GapAnalysisResult,
EvidenceDocument,
} from './types'
/**
* Create a new TOM Rules Engine instance
*/
export function createRulesEngine(): TOMRulesEngine {
return new TOMRulesEngine()
}
/**
* Derive TOMs for a given state
*/
export function deriveTOMsFromState(state: TOMGeneratorState): DerivedTOM[] {
const engine = new TOMRulesEngine()
return engine.deriveAllTOMs({
companyProfile: state.companyProfile,
dataProfile: state.dataProfile,
architectureProfile: state.architectureProfile,
securityProfile: state.securityProfile,
riskProfile: state.riskProfile,
})
}
/**
* Perform gap analysis on a state
*/
export function analyzeGapsFromState(
state: TOMGeneratorState
): GapAnalysisResult {
const engine = new TOMRulesEngine()
return engine.performGapAnalysis(state.derivedTOMs, state.documents)
}
/**
* Get completion statistics for a state
*/
export function getStateStatistics(state: TOMGeneratorState): {
totalControls: number
requiredControls: number
implementedControls: number
partialControls: number
notImplementedControls: number
complianceScore: number
stepsCompleted: number
totalSteps: number
documentsUploaded: number
} {
const totalControls = state.derivedTOMs.length
const requiredControls = state.derivedTOMs.filter(
(t) => t.applicability === 'REQUIRED'
).length
const implementedControls = state.derivedTOMs.filter(
(t) => t.implementationStatus === 'IMPLEMENTED'
).length
const partialControls = state.derivedTOMs.filter(
(t) => t.implementationStatus === 'PARTIAL'
).length
const notImplementedControls = state.derivedTOMs.filter(
(t) => t.implementationStatus === 'NOT_IMPLEMENTED'
).length
const stepsCompleted = state.steps.filter((s) => s.completed).length
const totalSteps = state.steps.length
return {
totalControls,
requiredControls,
implementedControls,
partialControls,
notImplementedControls,
complianceScore: state.gapAnalysis?.overallScore ?? 0,
stepsCompleted,
totalSteps,
documentsUploaded: state.documents.length,
}
}
@@ -0,0 +1,560 @@
// =============================================================================
// TOM Rules Engine
// Evaluates control applicability based on company context
// =============================================================================
import {
ControlLibraryEntry,
ApplicabilityCondition,
ControlApplicability,
RulesEngineResult,
RulesEngineEvaluationContext,
DerivedTOM,
EvidenceDocument,
GapAnalysisResult,
MissingControl,
PartialControl,
MissingEvidence,
ConditionOperator,
} from './types'
import { getAllControls, getControlById } from './controls/loader'
// =============================================================================
// RULES ENGINE CLASS
// =============================================================================
export class TOMRulesEngine {
private controls: ControlLibraryEntry[]
constructor() {
this.controls = getAllControls()
}
/**
* Evaluate all controls against the current context
*/
evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] {
return this.controls.map((control) => this.evaluateControl(control, context))
}
/**
* Evaluate a single control against the context
*/
evaluateControl(
control: ControlLibraryEntry,
context: RulesEngineEvaluationContext
): RulesEngineResult {
// Sort conditions by priority (highest first)
const sortedConditions = [...control.applicabilityConditions].sort(
(a, b) => b.priority - a.priority
)
// Evaluate conditions in priority order
for (const condition of sortedConditions) {
const matches = this.evaluateCondition(condition, context)
if (matches) {
return {
controlId: control.id,
applicability: condition.result,
reason: this.formatConditionReason(condition, context),
matchedCondition: condition,
}
}
}
// No condition matched, use default applicability
return {
controlId: control.id,
applicability: control.defaultApplicability,
reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfüllt)',
}
}
/**
* Evaluate a single condition
*/
private evaluateCondition(
condition: ApplicabilityCondition,
context: RulesEngineEvaluationContext
): boolean {
const value = this.getFieldValue(condition.field, context)
if (value === undefined || value === null) {
return false
}
return this.evaluateOperator(condition.operator, value, condition.value)
}
/**
* Get a nested field value from the context
*/
private getFieldValue(
fieldPath: string,
context: RulesEngineEvaluationContext
): unknown {
const parts = fieldPath.split('.')
let current: unknown = context
for (const part of parts) {
if (current === null || current === undefined) {
return undefined
}
if (typeof current === 'object') {
current = (current as Record<string, unknown>)[part]
} else {
return undefined
}
}
return current
}
/**
* Evaluate an operator with given values
*/
private evaluateOperator(
operator: ConditionOperator,
actualValue: unknown,
expectedValue: unknown
): boolean {
switch (operator) {
case 'EQUALS':
return actualValue === expectedValue
case 'NOT_EQUALS':
return actualValue !== expectedValue
case 'CONTAINS':
if (Array.isArray(actualValue)) {
return actualValue.includes(expectedValue)
}
if (typeof actualValue === 'string' && typeof expectedValue === 'string') {
return actualValue.includes(expectedValue)
}
return false
case 'GREATER_THAN':
if (typeof actualValue === 'number' && typeof expectedValue === 'number') {
return actualValue > expectedValue
}
return false
case 'IN':
if (Array.isArray(expectedValue)) {
return expectedValue.includes(actualValue)
}
return false
default:
return false
}
}
/**
* Format a human-readable reason for the condition match
*/
private formatConditionReason(
condition: ApplicabilityCondition,
context: RulesEngineEvaluationContext
): string {
const fieldValue = this.getFieldValue(condition.field, context)
const fieldLabel = this.getFieldLabel(condition.field)
switch (condition.operator) {
case 'EQUALS':
return `${fieldLabel} ist "${this.formatValue(fieldValue)}"`
case 'NOT_EQUALS':
return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"`
case 'CONTAINS':
return `${fieldLabel} enthält "${this.formatValue(condition.value)}"`
case 'GREATER_THAN':
return `${fieldLabel} ist größer als ${this.formatValue(condition.value)}`
case 'IN':
return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]`
default:
return `Bedingung erfüllt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}`
}
}
/**
* Get a human-readable label for a field path
*/
private getFieldLabel(fieldPath: string): string {
const labels: Record<string, string> = {
'companyProfile.role': 'Unternehmensrolle',
'companyProfile.size': 'Unternehmensgröße',
'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien',
'dataProfile.processesMinors': 'Verarbeitung von Minderjährigen-Daten',
'dataProfile.dataVolume': 'Datenvolumen',
'dataProfile.thirdCountryTransfers': 'Drittlandübermittlungen',
'architectureProfile.hostingModel': 'Hosting-Modell',
'architectureProfile.hostingLocation': 'Hosting-Standort',
'architectureProfile.multiTenancy': 'Mandantentrennung',
'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter',
'architectureProfile.encryptionAtRest': 'Verschlüsselung ruhender Daten',
'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung',
'securityProfile.hasSSO': 'Single Sign-On',
'securityProfile.hasPAM': 'Privileged Access Management',
'riskProfile.protectionLevel': 'Schutzbedarf',
'riskProfile.dsfaRequired': 'DSFA erforderlich',
'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit',
'riskProfile.ciaAssessment.integrity': 'Integrität',
'riskProfile.ciaAssessment.availability': 'Verfügbarkeit',
}
return labels[fieldPath] || fieldPath
}
/**
* Format a value for display
*/
private formatValue(value: unknown): string {
if (value === true) return 'Ja'
if (value === false) return 'Nein'
if (value === null || value === undefined) return 'nicht gesetzt'
if (Array.isArray(value)) return value.join(', ')
return String(value)
}
/**
* Derive all TOMs based on the current context
*/
deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
const results = this.evaluateControls(context)
return results.map((result) => {
const control = getControlById(result.controlId)
if (!control) {
throw new Error(`Control not found: ${result.controlId}`)
}
return {
id: `derived-${result.controlId}`,
controlId: result.controlId,
name: control.name.de,
description: control.description.de,
applicability: result.applicability,
applicabilityReason: result.reason,
implementationStatus: 'NOT_IMPLEMENTED',
responsiblePerson: null,
responsibleDepartment: null,
implementationDate: null,
reviewDate: null,
linkedEvidence: [],
evidenceGaps: [...control.evidenceRequirements],
aiGeneratedDescription: null,
aiRecommendations: [],
}
})
}
/**
* Get only required and recommended TOMs
*/
getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
const allTOMs = this.deriveAllTOMs(context)
return allTOMs.filter(
(tom) =>
tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
)
}
/**
* Get only required TOMs
*/
getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
const allTOMs = this.deriveAllTOMs(context)
return allTOMs.filter((tom) => tom.applicability === 'REQUIRED')
}
/**
* Perform gap analysis on derived TOMs and evidence
*/
performGapAnalysis(
derivedTOMs: DerivedTOM[],
documents: EvidenceDocument[]
): GapAnalysisResult {
const missingControls: MissingControl[] = []
const partialControls: PartialControl[] = []
const missingEvidence: MissingEvidence[] = []
const recommendations: string[] = []
let totalScore = 0
let totalWeight = 0
// Analyze each required/recommended TOM
const applicableTOMs = derivedTOMs.filter(
(tom) =>
tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
)
for (const tom of applicableTOMs) {
const control = getControlById(tom.controlId)
if (!control) continue
const weight = tom.applicability === 'REQUIRED' ? 3 : 1
totalWeight += weight
// Check implementation status
if (tom.implementationStatus === 'NOT_IMPLEMENTED') {
missingControls.push({
controlId: tom.controlId,
reason: `${control.name.de} ist nicht implementiert`,
priority: control.priority,
})
// Score: 0 for not implemented
} else if (tom.implementationStatus === 'PARTIAL') {
partialControls.push({
controlId: tom.controlId,
missingAspects: tom.evidenceGaps,
})
// Score: 50% for partial
totalScore += weight * 0.5
} else {
// Fully implemented
totalScore += weight
}
// Check evidence
const linkedEvidenceIds = tom.linkedEvidence
const requiredEvidence = control.evidenceRequirements
const providedEvidence = documents.filter((doc) =>
linkedEvidenceIds.includes(doc.id)
)
if (providedEvidence.length < requiredEvidence.length) {
const missing = requiredEvidence.filter(
(req) =>
!providedEvidence.some(
(doc) =>
doc.documentType === 'POLICY' ||
doc.documentType === 'CERTIFICATE' ||
doc.originalName.toLowerCase().includes(req.toLowerCase())
)
)
if (missing.length > 0) {
missingEvidence.push({
controlId: tom.controlId,
requiredEvidence: missing,
})
}
}
}
// Calculate overall score as percentage
const overallScore =
totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0
// Generate recommendations
if (missingControls.length > 0) {
const criticalMissing = missingControls.filter(
(mc) => mc.priority === 'CRITICAL'
)
if (criticalMissing.length > 0) {
recommendations.push(
`${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.`
)
}
}
if (partialControls.length > 0) {
recommendations.push(
`${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollständigen Sie die Implementierung.`
)
}
if (missingEvidence.length > 0) {
recommendations.push(
`Für ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.`
)
}
if (overallScore >= 80) {
recommendations.push(
'Ihr TOM-Compliance-Score ist gut. Führen Sie regelmäßige Überprüfungen durch.'
)
} else if (overallScore >= 50) {
recommendations.push(
'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Lücken.'
)
} else {
recommendations.push(
'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Überarbeitung der Maßnahmen wird empfohlen.'
)
}
return {
overallScore,
missingControls,
partialControls,
missingEvidence,
recommendations,
generatedAt: new Date(),
}
}
/**
* Get controls by applicability level
*/
getControlsByApplicability(
context: RulesEngineEvaluationContext,
applicability: ControlApplicability
): ControlLibraryEntry[] {
const results = this.evaluateControls(context)
return results
.filter((r) => r.applicability === applicability)
.map((r) => getControlById(r.controlId))
.filter((c): c is ControlLibraryEntry => c !== undefined)
}
/**
* Get summary statistics for the evaluation
*/
getSummaryStatistics(context: RulesEngineEvaluationContext): {
total: number
required: number
recommended: number
optional: number
notApplicable: number
byCategory: Map<string, { required: number; recommended: number }>
} {
const results = this.evaluateControls(context)
const stats = {
total: results.length,
required: 0,
recommended: 0,
optional: 0,
notApplicable: 0,
byCategory: new Map<string, { required: number; recommended: number }>(),
}
for (const result of results) {
switch (result.applicability) {
case 'REQUIRED':
stats.required++
break
case 'RECOMMENDED':
stats.recommended++
break
case 'OPTIONAL':
stats.optional++
break
case 'NOT_APPLICABLE':
stats.notApplicable++
break
}
// Count by category
const control = getControlById(result.controlId)
if (control) {
const category = control.category
const existing = stats.byCategory.get(category) || {
required: 0,
recommended: 0,
}
if (result.applicability === 'REQUIRED') {
existing.required++
} else if (result.applicability === 'RECOMMENDED') {
existing.recommended++
}
stats.byCategory.set(category, existing)
}
}
return stats
}
/**
* Check if a specific control is applicable
*/
isControlApplicable(
controlId: string,
context: RulesEngineEvaluationContext
): boolean {
const control = getControlById(controlId)
if (!control) return false
const result = this.evaluateControl(control, context)
return (
result.applicability === 'REQUIRED' ||
result.applicability === 'RECOMMENDED'
)
}
/**
* Get all controls that match a specific tag
*/
getControlsByTagWithApplicability(
tag: string,
context: RulesEngineEvaluationContext
): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> {
return this.controls
.filter((control) => control.tags.includes(tag))
.map((control) => ({
control,
result: this.evaluateControl(control, context),
}))
}
/**
* Reload controls (useful if the control library is updated)
*/
reloadControls(): void {
this.controls = getAllControls()
}
}
// =============================================================================
// SINGLETON INSTANCE
// =============================================================================
let rulesEngineInstance: TOMRulesEngine | null = null
export function getTOMRulesEngine(): TOMRulesEngine {
if (!rulesEngineInstance) {
rulesEngineInstance = new TOMRulesEngine()
}
return rulesEngineInstance
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Quick evaluation of controls for a context
*/
export function evaluateControlsForContext(
context: RulesEngineEvaluationContext
): RulesEngineResult[] {
return getTOMRulesEngine().evaluateControls(context)
}
/**
* Quick derivation of TOMs for a context
*/
export function deriveTOMsForContext(
context: RulesEngineEvaluationContext
): DerivedTOM[] {
return getTOMRulesEngine().deriveAllTOMs(context)
}
/**
* Quick gap analysis
*/
export function performQuickGapAnalysis(
derivedTOMs: DerivedTOM[],
documents: EvidenceDocument[]
): GapAnalysisResult {
return getTOMRulesEngine().performGapAnalysis(derivedTOMs, documents)
}
@@ -0,0 +1,192 @@
// =============================================================================
// SDM (Standard-Datenschutzmodell) Mapping
// Maps ControlCategories to SDM Gewaehrleistungsziele and Spec Modules
// =============================================================================
import { ControlCategory } from './types'
// =============================================================================
// TYPES
// =============================================================================
export type SDMGewaehrleistungsziel =
| 'Verfuegbarkeit'
| 'Integritaet'
| 'Vertraulichkeit'
| 'Nichtverkettung'
| 'Intervenierbarkeit'
| 'Transparenz'
| 'Datenminimierung'
export type TOMModuleCategory =
| 'IDENTITY_AUTH'
| 'LOGGING'
| 'DOCUMENTATION'
| 'SEPARATION'
| 'RETENTION'
| 'DELETION'
| 'TRAINING'
| 'REVIEW'
export const SDM_GOAL_LABELS: Record<SDMGewaehrleistungsziel, string> = {
Verfuegbarkeit: 'Verfuegbarkeit',
Integritaet: 'Integritaet',
Vertraulichkeit: 'Vertraulichkeit',
Nichtverkettung: 'Nichtverkettung',
Intervenierbarkeit: 'Intervenierbarkeit',
Transparenz: 'Transparenz',
Datenminimierung: 'Datenminimierung',
}
export const SDM_GOAL_DESCRIPTIONS: Record<SDMGewaehrleistungsziel, string> = {
Verfuegbarkeit: 'Personenbezogene Daten muessen zeitgerecht zur Verfuegung stehen und ordnungsgemaess verarbeitet werden koennen.',
Integritaet: 'Personenbezogene Daten muessen unversehrt, vollstaendig und aktuell bleiben.',
Vertraulichkeit: 'Nur Befugte duerfen personenbezogene Daten zur Kenntnis nehmen.',
Nichtverkettung: 'Daten duerfen nicht ohne Weiteres fuer andere Zwecke zusammengefuehrt werden.',
Intervenierbarkeit: 'Betroffene muessen ihre Rechte wahrnehmen koennen (Auskunft, Berichtigung, Loeschung).',
Transparenz: 'Verarbeitungsvorgaenge muessen nachvollziehbar dokumentiert sein.',
Datenminimierung: 'Nur die fuer den Zweck erforderlichen Daten duerfen verarbeitet werden.',
}
export const MODULE_LABELS: Record<TOMModuleCategory, string> = {
IDENTITY_AUTH: 'Identitaet & Authentifizierung',
LOGGING: 'Protokollierung',
DOCUMENTATION: 'Dokumentation',
SEPARATION: 'Trennung',
RETENTION: 'Aufbewahrung',
DELETION: 'Loeschung & Vernichtung',
TRAINING: 'Schulung & Vertraulichkeit',
REVIEW: 'Ueberpruefung & Bewertung',
}
// =============================================================================
// MAPPINGS
// =============================================================================
/**
* Maps ControlCategory to its primary SDM Gewaehrleistungsziele
*/
export const SDM_CATEGORY_MAPPING: Record<ControlCategory, SDMGewaehrleistungsziel[]> = {
ACCESS_CONTROL: ['Vertraulichkeit'],
ADMISSION_CONTROL: ['Vertraulichkeit', 'Integritaet'],
ACCESS_AUTHORIZATION: ['Vertraulichkeit', 'Nichtverkettung'],
TRANSFER_CONTROL: ['Vertraulichkeit', 'Integritaet'],
INPUT_CONTROL: ['Integritaet', 'Transparenz'],
ORDER_CONTROL: ['Transparenz', 'Intervenierbarkeit'],
AVAILABILITY: ['Verfuegbarkeit'],
SEPARATION: ['Nichtverkettung', 'Datenminimierung'],
ENCRYPTION: ['Vertraulichkeit', 'Integritaet'],
PSEUDONYMIZATION: ['Datenminimierung', 'Nichtverkettung'],
RESILIENCE: ['Verfuegbarkeit'],
RECOVERY: ['Verfuegbarkeit', 'Integritaet'],
REVIEW: ['Transparenz', 'Intervenierbarkeit'],
}
/**
* Maps ControlCategory to Spec Module Categories
*/
export const MODULE_CATEGORY_MAPPING: Record<ControlCategory, TOMModuleCategory[]> = {
ACCESS_CONTROL: ['IDENTITY_AUTH'],
ADMISSION_CONTROL: ['IDENTITY_AUTH'],
ACCESS_AUTHORIZATION: ['IDENTITY_AUTH', 'DOCUMENTATION'],
TRANSFER_CONTROL: ['DOCUMENTATION'],
INPUT_CONTROL: ['LOGGING'],
ORDER_CONTROL: ['DOCUMENTATION'],
AVAILABILITY: ['REVIEW'],
SEPARATION: ['SEPARATION'],
ENCRYPTION: ['IDENTITY_AUTH'],
PSEUDONYMIZATION: ['SEPARATION', 'DELETION'],
RESILIENCE: ['REVIEW'],
RECOVERY: ['REVIEW'],
REVIEW: ['REVIEW', 'TRAINING'],
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
import type { DerivedTOM, ControlLibraryEntry } from './types'
import { getControlById } from './controls/loader'
/**
* Get SDM goals for a given control (by looking up its category)
*/
export function getSDMGoalsForControl(controlId: string): SDMGewaehrleistungsziel[] {
const control = getControlById(controlId)
if (!control) return []
return SDM_CATEGORY_MAPPING[control.category] || []
}
/**
* Get derived TOMs that map to a specific SDM goal
*/
export function getTOMsBySDMGoal(
toms: DerivedTOM[],
goal: SDMGewaehrleistungsziel
): DerivedTOM[] {
return toms.filter(tom => {
const goals = getSDMGoalsForControl(tom.controlId)
return goals.includes(goal)
})
}
/**
* Get derived TOMs belonging to a specific module
*/
export function getTOMsByModule(
toms: DerivedTOM[],
module: TOMModuleCategory
): DerivedTOM[] {
return toms.filter(tom => {
const control = getControlById(tom.controlId)
if (!control) return false
const modules = MODULE_CATEGORY_MAPPING[control.category] || []
return modules.includes(module)
})
}
/**
* Get SDM goal coverage statistics
*/
export function getSDMCoverageStats(toms: DerivedTOM[]): Record<SDMGewaehrleistungsziel, {
total: number
implemented: number
partial: number
missing: number
}> {
const goals = Object.keys(SDM_GOAL_LABELS) as SDMGewaehrleistungsziel[]
const stats = {} as Record<SDMGewaehrleistungsziel, { total: number; implemented: number; partial: number; missing: number }>
for (const goal of goals) {
const goalTOMs = getTOMsBySDMGoal(toms, goal)
stats[goal] = {
total: goalTOMs.length,
implemented: goalTOMs.filter(t => t.implementationStatus === 'IMPLEMENTED').length,
partial: goalTOMs.filter(t => t.implementationStatus === 'PARTIAL').length,
missing: goalTOMs.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length,
}
}
return stats
}
/**
* Get module coverage statistics
*/
export function getModuleCoverageStats(toms: DerivedTOM[]): Record<TOMModuleCategory, {
total: number
implemented: number
}> {
const modules = Object.keys(MODULE_LABELS) as TOMModuleCategory[]
const stats = {} as Record<TOMModuleCategory, { total: number; implemented: number }>
for (const mod of modules) {
const modTOMs = getTOMsByModule(toms, mod)
stats[mod] = {
total: modTOMs.length,
implemented: modTOMs.filter(t => t.implementationStatus === 'IMPLEMENTED').length,
}
}
return stats
}
+963
View File
@@ -0,0 +1,963 @@
// =============================================================================
// TOM Generator Module - TypeScript Types
// DSGVO Art. 32 Technical and Organizational Measures
// =============================================================================
// =============================================================================
// ENUMS & LITERAL TYPES
// =============================================================================
export type TOMGeneratorStepId =
| 'scope-roles'
| 'data-categories'
| 'architecture-hosting'
| 'security-profile'
| 'risk-protection'
| 'review-export'
export type CompanyRole = 'CONTROLLER' | 'PROCESSOR' | 'JOINT_CONTROLLER'
export type DataCategory =
| 'IDENTIFICATION'
| 'CONTACT'
| 'FINANCIAL'
| 'PROFESSIONAL'
| 'LOCATION'
| 'BEHAVIORAL'
| 'BIOMETRIC'
| 'HEALTH'
| 'GENETIC'
| 'POLITICAL'
| 'RELIGIOUS'
| 'SEXUAL_ORIENTATION'
| 'CRIMINAL'
export type DataSubject =
| 'EMPLOYEES'
| 'CUSTOMERS'
| 'PROSPECTS'
| 'SUPPLIERS'
| 'MINORS'
| 'PATIENTS'
| 'STUDENTS'
| 'GENERAL_PUBLIC'
export type HostingLocation =
| 'DE'
| 'EU'
| 'EEA'
| 'THIRD_COUNTRY_ADEQUATE'
| 'THIRD_COUNTRY'
export type HostingModel = 'ON_PREMISE' | 'PRIVATE_CLOUD' | 'PUBLIC_CLOUD' | 'HYBRID'
export type MultiTenancy = 'SINGLE_TENANT' | 'MULTI_TENANT' | 'DEDICATED'
export type ControlApplicability =
| 'REQUIRED'
| 'RECOMMENDED'
| 'OPTIONAL'
| 'NOT_APPLICABLE'
export type DocumentType =
| 'AVV'
| 'DPA'
| 'SLA'
| 'NDA'
| 'POLICY'
| 'CERTIFICATE'
| 'AUDIT_REPORT'
| 'OTHER'
export type ProtectionLevel = 'NORMAL' | 'HIGH' | 'VERY_HIGH'
export type CIARating = 1 | 2 | 3 | 4 | 5
export type ControlCategory =
| 'ACCESS_CONTROL'
| 'ADMISSION_CONTROL'
| 'ACCESS_AUTHORIZATION'
| 'TRANSFER_CONTROL'
| 'INPUT_CONTROL'
| 'ORDER_CONTROL'
| 'AVAILABILITY'
| 'SEPARATION'
| 'ENCRYPTION'
| 'PSEUDONYMIZATION'
| 'RESILIENCE'
| 'RECOVERY'
| 'REVIEW'
export type CompanySize = 'MICRO' | 'SMALL' | 'MEDIUM' | 'LARGE' | 'ENTERPRISE'
export type DataVolume = 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
export type AuthMethodType =
| 'PASSWORD'
| 'MFA'
| 'SSO'
| 'CERTIFICATE'
| 'BIOMETRIC'
export type BackupFrequency = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY'
export type ReviewFrequency = 'MONTHLY' | 'QUARTERLY' | 'SEMI_ANNUAL' | 'ANNUAL'
export type ControlPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
export type ControlComplexity = 'LOW' | 'MEDIUM' | 'HIGH'
export type ImplementationStatus = 'NOT_IMPLEMENTED' | 'PARTIAL' | 'IMPLEMENTED'
export type EvidenceStatus = 'PENDING' | 'ANALYZED' | 'VERIFIED' | 'REJECTED'
export type ConditionOperator =
| 'EQUALS'
| 'NOT_EQUALS'
| 'CONTAINS'
| 'GREATER_THAN'
| 'IN'
// =============================================================================
// PROFILE INTERFACES (Wizard Steps 1-5)
// =============================================================================
export interface CompanyProfile {
id: string
name: string
industry: string
size: CompanySize
role: CompanyRole
products: string[]
dpoPerson: string | null
dpoEmail: string | null
itSecurityContact: string | null
}
export interface DataProfile {
categories: DataCategory[]
subjects: DataSubject[]
hasSpecialCategories: boolean
processesMinors: boolean
dataVolume: DataVolume
thirdCountryTransfers: boolean
thirdCountryList: string[]
}
export interface CloudProvider {
name: string
location: HostingLocation
certifications: string[]
}
export interface ArchitectureProfile {
hostingModel: HostingModel
hostingLocation: HostingLocation
providers: CloudProvider[]
multiTenancy: MultiTenancy
hasSubprocessors: boolean
subprocessorCount: number
encryptionAtRest: boolean
encryptionInTransit: boolean
}
export interface AuthMethod {
type: AuthMethodType
provider: string | null
}
export interface SecurityProfile {
authMethods: AuthMethod[]
hasMFA: boolean
hasSSO: boolean
hasIAM: boolean
hasPAM: boolean
hasEncryptionAtRest: boolean
hasEncryptionInTransit: boolean
hasLogging: boolean
logRetentionDays: number
hasBackup: boolean
backupFrequency: BackupFrequency
backupRetentionDays: number
hasDRPlan: boolean
rtoHours: number | null
rpoHours: number | null
hasVulnerabilityManagement: boolean
hasPenetrationTests: boolean
hasSecurityTraining: boolean
}
export interface CIAAssessment {
confidentiality: CIARating
integrity: CIARating
availability: CIARating
justification: string
}
export interface RiskProfile {
ciaAssessment: CIAAssessment
protectionLevel: ProtectionLevel
specialRisks: string[]
regulatoryRequirements: string[]
hasHighRiskProcessing: boolean
dsfaRequired: boolean
}
// =============================================================================
// EVIDENCE DOCUMENT
// =============================================================================
export interface ExtractedClause {
id: string
text: string
type: string
relatedControlId: string | null
}
export interface AIDocumentAnalysis {
summary: string
extractedClauses: ExtractedClause[]
applicableControls: string[]
gaps: string[]
confidence: number
analyzedAt: Date
}
export interface EvidenceDocument {
id: string
filename: string
originalName: string
mimeType: string
size: number
uploadedAt: Date
uploadedBy: string
documentType: DocumentType
detectedType: DocumentType | null
hash: string
validFrom: Date | null
validUntil: Date | null
linkedControlIds: string[]
aiAnalysis: AIDocumentAnalysis | null
status: EvidenceStatus
}
// =============================================================================
// CONTROL LIBRARY
// =============================================================================
export interface LocalizedString {
de: string
en: string
}
export interface FrameworkMapping {
framework: string
reference: string
}
export interface ApplicabilityCondition {
field: string
operator: ConditionOperator
value: unknown
result: ControlApplicability
priority: number
}
export interface ControlLibraryEntry {
id: string
code: string
category: ControlCategory
type: 'TECHNICAL' | 'ORGANIZATIONAL'
name: LocalizedString
description: LocalizedString
mappings: FrameworkMapping[]
applicabilityConditions: ApplicabilityCondition[]
defaultApplicability: ControlApplicability
evidenceRequirements: string[]
reviewFrequency: ReviewFrequency
priority: ControlPriority
complexity: ControlComplexity
tags: string[]
}
// =============================================================================
// DERIVED TOM
// =============================================================================
export interface DerivedTOM {
id: string
controlId: string
name: string
description: string
applicability: ControlApplicability
applicabilityReason: string
implementationStatus: ImplementationStatus
responsiblePerson: string | null
responsibleDepartment: string | null
implementationDate: Date | null
reviewDate: Date | null
linkedEvidence: string[]
evidenceGaps: string[]
aiGeneratedDescription: string | null
aiRecommendations: string[]
}
// =============================================================================
// GAP ANALYSIS
// =============================================================================
export interface MissingControl {
controlId: string
reason: string
priority: string
}
export interface PartialControl {
controlId: string
missingAspects: string[]
}
export interface MissingEvidence {
controlId: string
requiredEvidence: string[]
}
export interface GapAnalysisResult {
overallScore: number
missingControls: MissingControl[]
partialControls: PartialControl[]
missingEvidence: MissingEvidence[]
recommendations: string[]
generatedAt: Date
}
// =============================================================================
// WIZARD STEP
// =============================================================================
export interface WizardStep {
id: TOMGeneratorStepId
completed: boolean
data: unknown
validatedAt: Date | null
}
// =============================================================================
// EXPORT RECORD
// =============================================================================
export interface ExportRecord {
id: string
format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
generatedAt: Date
filename: string
}
// =============================================================================
// TOM GENERATOR STATE
// =============================================================================
export interface TOMGeneratorState {
id: string
tenantId: string
companyProfile: CompanyProfile | null
dataProfile: DataProfile | null
architectureProfile: ArchitectureProfile | null
securityProfile: SecurityProfile | null
riskProfile: RiskProfile | null
currentStep: TOMGeneratorStepId
steps: WizardStep[]
documents: EvidenceDocument[]
derivedTOMs: DerivedTOM[]
gapAnalysis: GapAnalysisResult | null
exports: ExportRecord[]
createdAt: Date
updatedAt: Date
}
// =============================================================================
// RULES ENGINE TYPES
// =============================================================================
export interface RulesEngineResult {
controlId: string
applicability: ControlApplicability
reason: string
matchedCondition?: ApplicabilityCondition
}
export interface RulesEngineEvaluationContext {
companyProfile: CompanyProfile | null
dataProfile: DataProfile | null
architectureProfile: ArchitectureProfile | null
securityProfile: SecurityProfile | null
riskProfile: RiskProfile | null
}
// =============================================================================
// API TYPES
// =============================================================================
export interface TOMGeneratorStateRequest {
tenantId: string
}
export interface TOMGeneratorStateResponse {
success: boolean
state: TOMGeneratorState | null
error?: string
}
export interface ControlsEvaluationRequest {
tenantId: string
context: RulesEngineEvaluationContext
}
export interface ControlsEvaluationResponse {
success: boolean
results: RulesEngineResult[]
evaluatedAt: string
}
export interface EvidenceUploadRequest {
tenantId: string
documentType: DocumentType
validFrom?: string
validUntil?: string
}
export interface EvidenceUploadResponse {
success: boolean
document: EvidenceDocument | null
error?: string
}
export interface EvidenceAnalyzeRequest {
documentId: string
tenantId: string
}
export interface EvidenceAnalyzeResponse {
success: boolean
analysis: AIDocumentAnalysis | null
error?: string
}
export interface ExportRequest {
tenantId: string
format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
language: 'de' | 'en'
}
export interface ExportResponse {
success: boolean
exportId: string
filename: string
downloadUrl?: string
error?: string
}
export interface GapAnalysisRequest {
tenantId: string
}
export interface GapAnalysisResponse {
success: boolean
result: GapAnalysisResult | null
error?: string
}
// =============================================================================
// STEP CONFIGURATION
// =============================================================================
export interface StepConfig {
id: TOMGeneratorStepId
title: LocalizedString
description: LocalizedString
checkpointId: string
path: string
/** Alias for path (for convenience) */
url: string
/** German title for display (for convenience) */
name: string
}
export const TOM_GENERATOR_STEPS: StepConfig[] = [
{
id: 'scope-roles',
title: { de: 'Scope & Rollen', en: 'Scope & Roles' },
description: {
de: 'Unternehmensname, Branche, Größe und Rolle definieren',
en: 'Define company name, industry, size and role',
},
checkpointId: 'CP-TOM-SCOPE',
path: '/sdk/tom-generator/scope',
url: '/sdk/tom-generator/scope',
name: 'Scope & Rollen',
},
{
id: 'data-categories',
title: { de: 'Datenkategorien', en: 'Data Categories' },
description: {
de: 'Datenkategorien und betroffene Personen erfassen',
en: 'Capture data categories and data subjects',
},
checkpointId: 'CP-TOM-DATA',
path: '/sdk/tom-generator/data',
url: '/sdk/tom-generator/data',
name: 'Datenkategorien',
},
{
id: 'architecture-hosting',
title: { de: 'Architektur & Hosting', en: 'Architecture & Hosting' },
description: {
de: 'Hosting-Modell, Standort und Provider definieren',
en: 'Define hosting model, location and providers',
},
checkpointId: 'CP-TOM-ARCH',
path: '/sdk/tom-generator/architecture',
url: '/sdk/tom-generator/architecture',
name: 'Architektur & Hosting',
},
{
id: 'security-profile',
title: { de: 'Security-Profil', en: 'Security Profile' },
description: {
de: 'Authentifizierung, Verschlüsselung und Backup konfigurieren',
en: 'Configure authentication, encryption and backup',
},
checkpointId: 'CP-TOM-SEC',
path: '/sdk/tom-generator/security',
url: '/sdk/tom-generator/security',
name: 'Security-Profil',
},
{
id: 'risk-protection',
title: { de: 'Risiko & Schutzbedarf', en: 'Risk & Protection Level' },
description: {
de: 'CIA-Bewertung und Schutzbedarf ermitteln',
en: 'Determine CIA assessment and protection level',
},
checkpointId: 'CP-TOM-RISK',
path: '/sdk/tom-generator/risk',
url: '/sdk/tom-generator/risk',
name: 'Risiko & Schutzbedarf',
},
{
id: 'review-export',
title: { de: 'Review & Export', en: 'Review & Export' },
description: {
de: 'Zusammenfassung prüfen und TOMs exportieren',
en: 'Review summary and export TOMs',
},
checkpointId: 'CP-TOM-REVIEW',
path: '/sdk/tom-generator/review',
url: '/sdk/tom-generator/review',
name: 'Review & Export',
},
]
// =============================================================================
// CATEGORY METADATA
// =============================================================================
export interface CategoryMetadata {
id: ControlCategory
name: LocalizedString
gdprReference: string
icon?: string
}
export const CONTROL_CATEGORIES: CategoryMetadata[] = [
{
id: 'ACCESS_CONTROL',
name: { de: 'Zutrittskontrolle', en: 'Physical Access Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ADMISSION_CONTROL',
name: { de: 'Zugangskontrolle', en: 'System Access Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ACCESS_AUTHORIZATION',
name: { de: 'Zugriffskontrolle', en: 'Access Authorization' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'TRANSFER_CONTROL',
name: { de: 'Weitergabekontrolle', en: 'Transfer Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'INPUT_CONTROL',
name: { de: 'Eingabekontrolle', en: 'Input Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ORDER_CONTROL',
name: { de: 'Auftragskontrolle', en: 'Order Control' },
gdprReference: 'Art. 28',
},
{
id: 'AVAILABILITY',
name: { de: 'Verfügbarkeit', en: 'Availability' },
gdprReference: 'Art. 32 Abs. 1 lit. b, c',
},
{
id: 'SEPARATION',
name: { de: 'Trennbarkeit', en: 'Separation' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ENCRYPTION',
name: { de: 'Verschlüsselung', en: 'Encryption' },
gdprReference: 'Art. 32 Abs. 1 lit. a',
},
{
id: 'PSEUDONYMIZATION',
name: { de: 'Pseudonymisierung', en: 'Pseudonymization' },
gdprReference: 'Art. 32 Abs. 1 lit. a',
},
{
id: 'RESILIENCE',
name: { de: 'Belastbarkeit', en: 'Resilience' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'RECOVERY',
name: { de: 'Wiederherstellbarkeit', en: 'Recovery' },
gdprReference: 'Art. 32 Abs. 1 lit. c',
},
{
id: 'REVIEW',
name: { de: 'Überprüfung & Bewertung', en: 'Review & Assessment' },
gdprReference: 'Art. 32 Abs. 1 lit. d',
},
]
// =============================================================================
// DATA CATEGORY METADATA
// =============================================================================
export interface DataCategoryMetadata {
id: DataCategory
name: LocalizedString
isSpecialCategory: boolean
gdprReference?: string
}
export const DATA_CATEGORIES_METADATA: DataCategoryMetadata[] = [
{
id: 'IDENTIFICATION',
name: { de: 'Identifikationsdaten', en: 'Identification Data' },
isSpecialCategory: false,
},
{
id: 'CONTACT',
name: { de: 'Kontaktdaten', en: 'Contact Data' },
isSpecialCategory: false,
},
{
id: 'FINANCIAL',
name: { de: 'Finanzdaten', en: 'Financial Data' },
isSpecialCategory: false,
},
{
id: 'PROFESSIONAL',
name: { de: 'Berufliche Daten', en: 'Professional Data' },
isSpecialCategory: false,
},
{
id: 'LOCATION',
name: { de: 'Standortdaten', en: 'Location Data' },
isSpecialCategory: false,
},
{
id: 'BEHAVIORAL',
name: { de: 'Verhaltensdaten', en: 'Behavioral Data' },
isSpecialCategory: false,
},
{
id: 'BIOMETRIC',
name: { de: 'Biometrische Daten', en: 'Biometric Data' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'HEALTH',
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'GENETIC',
name: { de: 'Genetische Daten', en: 'Genetic Data' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'POLITICAL',
name: { de: 'Politische Meinungen', en: 'Political Opinions' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'RELIGIOUS',
name: { de: 'Religiöse Überzeugungen', en: 'Religious Beliefs' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'SEXUAL_ORIENTATION',
name: { de: 'Sexuelle Orientierung', en: 'Sexual Orientation' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'CRIMINAL',
name: { de: 'Strafrechtliche Daten', en: 'Criminal Data' },
isSpecialCategory: true,
gdprReference: 'Art. 10',
},
]
// =============================================================================
// DATA SUBJECT METADATA
// =============================================================================
export interface DataSubjectMetadata {
id: DataSubject
name: LocalizedString
isVulnerable: boolean
}
export const DATA_SUBJECTS_METADATA: DataSubjectMetadata[] = [
{
id: 'EMPLOYEES',
name: { de: 'Mitarbeiter', en: 'Employees' },
isVulnerable: false,
},
{
id: 'CUSTOMERS',
name: { de: 'Kunden', en: 'Customers' },
isVulnerable: false,
},
{
id: 'PROSPECTS',
name: { de: 'Interessenten', en: 'Prospects' },
isVulnerable: false,
},
{
id: 'SUPPLIERS',
name: { de: 'Lieferanten', en: 'Suppliers' },
isVulnerable: false,
},
{
id: 'MINORS',
name: { de: 'Minderjährige', en: 'Minors' },
isVulnerable: true,
},
{
id: 'PATIENTS',
name: { de: 'Patienten', en: 'Patients' },
isVulnerable: true,
},
{
id: 'STUDENTS',
name: { de: 'Schüler/Studenten', en: 'Students' },
isVulnerable: false,
},
{
id: 'GENERAL_PUBLIC',
name: { de: 'Allgemeine Öffentlichkeit', en: 'General Public' },
isVulnerable: false,
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function getStepByIndex(index: number): StepConfig | undefined {
return TOM_GENERATOR_STEPS[index]
}
export function getStepById(id: TOMGeneratorStepId): StepConfig | undefined {
return TOM_GENERATOR_STEPS.find((step) => step.id === id)
}
export function getStepIndex(id: TOMGeneratorStepId): number {
return TOM_GENERATOR_STEPS.findIndex((step) => step.id === id)
}
export function getNextStep(
currentId: TOMGeneratorStepId
): StepConfig | undefined {
const currentIndex = getStepIndex(currentId)
return TOM_GENERATOR_STEPS[currentIndex + 1]
}
export function getPreviousStep(
currentId: TOMGeneratorStepId
): StepConfig | undefined {
const currentIndex = getStepIndex(currentId)
return currentIndex > 0 ? TOM_GENERATOR_STEPS[currentIndex - 1] : undefined
}
export function isSpecialCategory(category: DataCategory): boolean {
const meta = DATA_CATEGORIES_METADATA.find((c) => c.id === category)
return meta?.isSpecialCategory ?? false
}
export function hasSpecialCategories(categories: DataCategory[]): boolean {
return categories.some(isSpecialCategory)
}
export function isVulnerableSubject(subject: DataSubject): boolean {
const meta = DATA_SUBJECTS_METADATA.find((s) => s.id === subject)
return meta?.isVulnerable ?? false
}
export function hasVulnerableSubjects(subjects: DataSubject[]): boolean {
return subjects.some(isVulnerableSubject)
}
export function calculateProtectionLevel(
ciaAssessment: CIAAssessment
): ProtectionLevel {
const maxRating = Math.max(
ciaAssessment.confidentiality,
ciaAssessment.integrity,
ciaAssessment.availability
)
if (maxRating >= 4) return 'VERY_HIGH'
if (maxRating >= 3) return 'HIGH'
return 'NORMAL'
}
export function isDSFARequired(
dataProfile: DataProfile | null,
riskProfile: RiskProfile | null
): boolean {
if (!dataProfile) return false
// DSFA required if:
// 1. Special categories are processed
if (dataProfile.hasSpecialCategories) return true
// 2. Minors data is processed
if (dataProfile.processesMinors) return true
// 3. Large scale processing
if (dataProfile.dataVolume === 'VERY_HIGH') return true
// 4. High risk processing indicated
if (riskProfile?.hasHighRiskProcessing) return true
// 5. Very high protection level
if (riskProfile?.protectionLevel === 'VERY_HIGH') return true
return false
}
// =============================================================================
// INITIAL STATE FACTORY
// =============================================================================
export function createInitialTOMGeneratorState(
tenantId: string
): TOMGeneratorState {
const now = new Date()
return {
id: crypto.randomUUID(),
tenantId,
companyProfile: null,
dataProfile: null,
architectureProfile: null,
securityProfile: null,
riskProfile: null,
currentStep: 'scope-roles',
steps: TOM_GENERATOR_STEPS.map((step) => ({
id: step.id,
completed: false,
data: null,
validatedAt: null,
})),
documents: [],
derivedTOMs: [],
gapAnalysis: null,
exports: [],
createdAt: now,
updatedAt: now,
}
}
/**
* Alias for createInitialTOMGeneratorState (for API compatibility)
*/
export const createEmptyTOMGeneratorState = createInitialTOMGeneratorState
// =============================================================================
// SDM TYPES (Standard-Datenschutzmodell)
// =============================================================================
export type SDMGewaehrleistungsziel =
| 'Verfuegbarkeit'
| 'Integritaet'
| 'Vertraulichkeit'
| 'Nichtverkettung'
| 'Intervenierbarkeit'
| 'Transparenz'
| 'Datenminimierung'
export type TOMModuleCategory =
| 'IDENTITY_AUTH'
| 'LOGGING'
| 'DOCUMENTATION'
| 'SEPARATION'
| 'RETENTION'
| 'DELETION'
| 'TRAINING'
| 'REVIEW'
/**
* Maps ControlCategory to SDM Gewaehrleistungsziele.
* Used by the TOM Dashboard to display SDM coverage.
*/
export const SDM_CATEGORY_MAPPING: Record<ControlCategory, SDMGewaehrleistungsziel[]> = {
ACCESS_CONTROL: ['Vertraulichkeit'],
ADMISSION_CONTROL: ['Vertraulichkeit', 'Integritaet'],
ACCESS_AUTHORIZATION: ['Vertraulichkeit', 'Nichtverkettung'],
TRANSFER_CONTROL: ['Vertraulichkeit', 'Integritaet'],
INPUT_CONTROL: ['Integritaet', 'Transparenz'],
ORDER_CONTROL: ['Transparenz', 'Intervenierbarkeit'],
AVAILABILITY: ['Verfuegbarkeit'],
SEPARATION: ['Nichtverkettung', 'Datenminimierung'],
ENCRYPTION: ['Vertraulichkeit', 'Integritaet'],
PSEUDONYMIZATION: ['Datenminimierung', 'Nichtverkettung'],
RESILIENCE: ['Verfuegbarkeit'],
RECOVERY: ['Verfuegbarkeit', 'Integritaet'],
REVIEW: ['Transparenz', 'Intervenierbarkeit'],
}
/**
* Maps ControlCategory to Spec Module Categories.
*/
export const MODULE_CATEGORY_MAPPING: Record<ControlCategory, TOMModuleCategory[]> = {
ACCESS_CONTROL: ['IDENTITY_AUTH'],
ADMISSION_CONTROL: ['IDENTITY_AUTH'],
ACCESS_AUTHORIZATION: ['IDENTITY_AUTH', 'DOCUMENTATION'],
TRANSFER_CONTROL: ['DOCUMENTATION'],
INPUT_CONTROL: ['LOGGING'],
ORDER_CONTROL: ['DOCUMENTATION'],
AVAILABILITY: ['REVIEW'],
SEPARATION: ['SEPARATION'],
ENCRYPTION: ['IDENTITY_AUTH'],
PSEUDONYMIZATION: ['SEPARATION', 'DELETION'],
RESILIENCE: ['REVIEW'],
RECOVERY: ['REVIEW'],
REVIEW: ['REVIEW', 'TRAINING'],
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
/**
* Catalog exports
*
* Pre-defined templates, categories, and reference data
*/
export * from './processing-activities'
export * from './vendor-templates'
export * from './legal-basis'
@@ -0,0 +1,562 @@
/**
* Legal Basis Catalog
*
* Comprehensive information about GDPR legal bases (Art. 6, 9, 10)
*/
import { LegalBasisType, LocalizedText, PersonalDataCategory } from '../types'
export interface LegalBasisInfo {
type: LegalBasisType
article: string
name: LocalizedText
shortName: LocalizedText
description: LocalizedText
requirements: LocalizedText[]
suitableFor: string[]
notSuitableFor: string[]
documentationNeeded: LocalizedText[]
isSpecialCategory: boolean
notes?: LocalizedText
}
export interface RetentionPeriodInfo {
id: string
name: LocalizedText
legalBasis: string
duration: {
value: number
unit: 'DAYS' | 'MONTHS' | 'YEARS'
}
description: LocalizedText
applicableTo: string[]
}
// ==========================================
// LEGAL BASIS INFORMATION (Art. 6 DSGVO)
// ==========================================
export const LEGAL_BASIS_INFO: LegalBasisInfo[] = [
// Art. 6 Abs. 1 DSGVO - Standard legal bases
{
type: 'CONSENT',
article: 'Art. 6 Abs. 1 lit. a DSGVO',
name: { de: 'Einwilligung', en: 'Consent' },
shortName: { de: 'Einwilligung', en: 'Consent' },
description: {
de: 'Die betroffene Person hat ihre Einwilligung zu der Verarbeitung der sie betreffenden personenbezogenen Daten für einen oder mehrere bestimmte Zwecke gegeben.',
en: 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes.',
},
requirements: [
{ de: 'Freiwillig erteilt', en: 'Freely given' },
{ de: 'Für bestimmten Zweck', en: 'For a specific purpose' },
{ de: 'Informiert', en: 'Informed' },
{ de: 'Unmissverständlich', en: 'Unambiguous' },
{ de: 'Jederzeit widerrufbar', en: 'Revocable at any time' },
{ de: 'Nachweis muss möglich sein', en: 'Must be demonstrable' },
],
suitableFor: ['Newsletter', 'Marketing', 'Cookies', 'Tracking', 'Fotos/Videos'],
notSuitableFor: ['Vertragsdurchführung', 'Gesetzliche Pflichten', 'Arbeitsverhältnis'],
documentationNeeded: [
{ de: 'Einwilligungstext', en: 'Consent text' },
{ de: 'Zeitpunkt der Einwilligung', en: 'Time of consent' },
{ de: 'Art der Erteilung (Opt-in)', en: 'Method of consent (opt-in)' },
{ de: 'Widerrufsbelehrung', en: 'Information about withdrawal' },
],
isSpecialCategory: false,
},
{
type: 'CONTRACT',
article: 'Art. 6 Abs. 1 lit. b DSGVO',
name: { de: 'Vertragserfüllung', en: 'Contract Performance' },
shortName: { de: 'Vertrag', en: 'Contract' },
description: {
de: 'Die Verarbeitung ist für die Erfüllung eines Vertrags, dessen Vertragspartei die betroffene Person ist, oder zur Durchführung vorvertraglicher Maßnahmen erforderlich.',
en: 'Processing is necessary for the performance of a contract to which the data subject is party or for pre-contractual measures.',
},
requirements: [
{ de: 'Vertrag besteht oder wird angebahnt', en: 'Contract exists or is being initiated' },
{ de: 'Verarbeitung ist für Erfüllung erforderlich', en: 'Processing is necessary for performance' },
{ de: 'Betroffene Person ist Vertragspartei', en: 'Data subject is a party to the contract' },
],
suitableFor: ['Kundendaten', 'Bestellabwicklung', 'Lieferung', 'Rechnungsstellung', 'Kundenservice'],
notSuitableFor: ['Marketing', 'Profiling', 'Weitergabe an Dritte ohne Vertragsbezug'],
documentationNeeded: [
{ de: 'Vertrag oder AGB', en: 'Contract or T&C' },
{ de: 'Zusammenhang zur Verarbeitung', en: 'Connection to processing' },
],
isSpecialCategory: false,
},
{
type: 'LEGAL_OBLIGATION',
article: 'Art. 6 Abs. 1 lit. c DSGVO',
name: { de: 'Rechtliche Verpflichtung', en: 'Legal Obligation' },
shortName: { de: 'Gesetz', en: 'Legal' },
description: {
de: 'Die Verarbeitung ist zur Erfüllung einer rechtlichen Verpflichtung erforderlich, der der Verantwortliche unterliegt.',
en: 'Processing is necessary for compliance with a legal obligation to which the controller is subject.',
},
requirements: [
{ de: 'Rechtliche Verpflichtung im EU/nationalen Recht', en: 'Legal obligation in EU/national law' },
{ de: 'Verarbeitung ist zur Erfüllung erforderlich', en: 'Processing is necessary for compliance' },
{ de: 'Konkrete Rechtsgrundlage benennen', en: 'Cite specific legal basis' },
],
suitableFor: ['Steuerliche Aufbewahrung', 'Sozialversicherung', 'AML/KYC', 'Meldepflichten'],
notSuitableFor: ['Freiwillige Maßnahmen', 'Marketing'],
documentationNeeded: [
{ de: 'Konkrete Rechtsvorschrift', en: 'Specific legal provision' },
{ de: 'HGB, AO, SGB, etc.', en: 'Commercial code, tax code, etc.' },
],
isSpecialCategory: false,
},
{
type: 'VITAL_INTEREST',
article: 'Art. 6 Abs. 1 lit. d DSGVO',
name: { de: 'Lebenswichtige Interessen', en: 'Vital Interests' },
shortName: { de: 'Vital', en: 'Vital' },
description: {
de: 'Die Verarbeitung ist erforderlich, um lebenswichtige Interessen der betroffenen Person oder einer anderen natürlichen Person zu schützen.',
en: 'Processing is necessary to protect the vital interests of the data subject or of another natural person.',
},
requirements: [
{ de: 'Gefahr für Leben oder Gesundheit', en: 'Danger to life or health' },
{ de: 'Keine andere Rechtsgrundlage möglich', en: 'No other legal basis possible' },
{ de: 'Subsidiär zu anderen Rechtsgrundlagen', en: 'Subsidiary to other legal bases' },
],
suitableFor: ['Notfall', 'Medizinische Notversorgung', 'Katastrophenschutz'],
notSuitableFor: ['Regelmäßige Verarbeitung', 'Vorsorgemaßnahmen'],
documentationNeeded: [
{ de: 'Dokumentation des Notfalls', en: 'Documentation of emergency' },
{ de: 'Begründung der Erforderlichkeit', en: 'Justification of necessity' },
],
isSpecialCategory: false,
notes: {
de: 'Sollte nur in Ausnahmefällen verwendet werden, wenn keine andere Rechtsgrundlage greift.',
en: 'Should only be used in exceptional cases when no other legal basis applies.',
},
},
{
type: 'PUBLIC_TASK',
article: 'Art. 6 Abs. 1 lit. e DSGVO',
name: { de: 'Öffentliche Aufgabe', en: 'Public Task' },
shortName: { de: 'Öffentlich', en: 'Public' },
description: {
de: 'Die Verarbeitung ist für die Wahrnehmung einer Aufgabe erforderlich, die im öffentlichen Interesse liegt oder in Ausübung öffentlicher Gewalt erfolgt.',
en: 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.',
},
requirements: [
{ de: 'Öffentliches Interesse oder hoheitliche Aufgabe', en: 'Public interest or official authority' },
{ de: 'Rechtsgrundlage im EU/nationalen Recht', en: 'Legal basis in EU/national law' },
],
suitableFor: ['Behörden', 'Öffentlich-rechtliche Einrichtungen', 'Bildungseinrichtungen'],
notSuitableFor: ['Private Unternehmen (in der Regel)'],
documentationNeeded: [
{ de: 'Rechtsgrundlage für öffentliche Aufgabe', en: 'Legal basis for public task' },
{ de: 'Zusammenhang zur Aufgabe', en: 'Connection to task' },
],
isSpecialCategory: false,
},
{
type: 'LEGITIMATE_INTEREST',
article: 'Art. 6 Abs. 1 lit. f DSGVO',
name: { de: 'Berechtigtes Interesse', en: 'Legitimate Interest' },
shortName: { de: 'Ber. Interesse', en: 'Leg. Interest' },
description: {
de: 'Die Verarbeitung ist zur Wahrung der berechtigten Interessen des Verantwortlichen oder eines Dritten erforderlich, sofern nicht die Interessen oder Grundrechte der betroffenen Person überwiegen.',
en: 'Processing is necessary for the legitimate interests pursued by the controller or a third party, except where such interests are overridden by the interests or rights of the data subject.',
},
requirements: [
{ de: 'Berechtigtes Interesse identifizieren', en: 'Identify legitimate interest' },
{ de: 'Erforderlichkeit prüfen', en: 'Check necessity' },
{ de: 'Interessenabwägung durchführen', en: 'Conduct balancing test' },
{ de: 'Dokumentieren', en: 'Document' },
],
suitableFor: ['B2B-Marketing', 'IT-Sicherheit', 'Betrugsprävention', 'Konzerninterner Datenaustausch'],
notSuitableFor: ['Behörden', 'Verarbeitung sensibler Daten', 'Wenn Einwilligung verweigert wurde'],
documentationNeeded: [
{ de: 'Interessenabwägung (LIA)', en: 'Legitimate Interest Assessment (LIA)' },
{ de: 'Konkrete Interessen', en: 'Specific interests' },
{ de: 'Abwägung der Betroffenenrechte', en: 'Balancing of data subject rights' },
],
isSpecialCategory: false,
notes: {
de: 'Nicht anwendbar für Behörden bei Aufgabenerfüllung. Interessenabwägung (LIA) erforderlich.',
en: 'Not applicable for public authorities performing their tasks. Legitimate Interest Assessment (LIA) required.',
},
},
// Art. 9 Abs. 2 DSGVO - Special categories
{
type: 'ART9_CONSENT',
article: 'Art. 9 Abs. 2 lit. a DSGVO',
name: { de: 'Ausdrückliche Einwilligung', en: 'Explicit Consent' },
shortName: { de: 'Ausd. Einwilligung', en: 'Explicit Consent' },
description: {
de: 'Die betroffene Person hat in die Verarbeitung der besonderen Kategorien personenbezogener Daten ausdrücklich eingewilligt.',
en: 'The data subject has given explicit consent to the processing of special categories of personal data.',
},
requirements: [
{ de: 'Alle Anforderungen der normalen Einwilligung', en: 'All requirements of normal consent' },
{ de: 'Zusätzlich: Ausdrücklich', en: 'Additionally: Explicit' },
{ de: 'Besonderer Hinweis auf sensible Daten', en: 'Special notice about sensitive data' },
],
suitableFor: ['Gesundheitsdaten mit Einwilligung', 'Religiöse Daten mit Einwilligung'],
notSuitableFor: ['Arbeitsverhältnis (in der Regel)', 'Wenn nationales Recht es verbietet'],
documentationNeeded: [
{ de: 'Einwilligungstext mit Hinweis auf sensible Daten', en: 'Consent text with reference to sensitive data' },
{ de: 'Nachweis der ausdrücklichen Erteilung', en: 'Proof of explicit consent' },
],
isSpecialCategory: true,
},
{
type: 'ART9_EMPLOYMENT',
article: 'Art. 9 Abs. 2 lit. b DSGVO',
name: { de: 'Arbeitsrecht', en: 'Employment Law' },
shortName: { de: 'Arbeitsrecht', en: 'Employment' },
description: {
de: 'Die Verarbeitung ist erforderlich für arbeitsrechtliche Zwecke auf Grundlage von nationalen Rechtsvorschriften.',
en: 'Processing is necessary for employment law purposes based on national law provisions.',
},
requirements: [
{ de: 'Arbeitsrechtliche Grundlage (z.B. § 26 BDSG)', en: 'Employment law basis (e.g., § 26 BDSG)' },
{ de: 'Erforderlichkeit für Beschäftigung', en: 'Necessity for employment' },
{ de: 'Angemessene Garantien', en: 'Appropriate safeguards' },
],
suitableFor: ['Gesundheitsdaten im Arbeitsverhältnis', 'Schwerbehinderung', 'Gewerkschaftszugehörigkeit'],
notSuitableFor: ['Verarbeitung über das Erforderliche hinaus'],
documentationNeeded: [
{ de: 'Rechtsgrundlage (§ 26 BDSG)', en: 'Legal basis (§ 26 BDSG)' },
{ de: 'Erforderlichkeit dokumentieren', en: 'Document necessity' },
],
isSpecialCategory: true,
},
{
type: 'ART9_VITAL_INTEREST',
article: 'Art. 9 Abs. 2 lit. c DSGVO',
name: { de: 'Lebenswichtige Interessen (Art. 9)', en: 'Vital Interests (Art. 9)' },
shortName: { de: 'Vital (Art. 9)', en: 'Vital (Art. 9)' },
description: {
de: 'Die Verarbeitung ist zum Schutz lebenswichtiger Interessen erforderlich und die betroffene Person ist nicht einwilligungsfähig.',
en: 'Processing is necessary to protect vital interests and the data subject is incapable of giving consent.',
},
requirements: [
{ de: 'Schutz lebenswichtiger Interessen', en: 'Protection of vital interests' },
{ de: 'Betroffene Person nicht einwilligungsfähig', en: 'Data subject incapable of consent' },
],
suitableFor: ['Medizinische Notfälle', 'Bewusstlose Personen'],
notSuitableFor: ['Regelmäßige Verarbeitung'],
documentationNeeded: [
{ de: 'Dokumentation des Notfalls', en: 'Documentation of emergency' },
{ de: 'Nachweis der fehlenden Einwilligungsfähigkeit', en: 'Proof of incapacity to consent' },
],
isSpecialCategory: true,
},
{
type: 'ART9_HEALTH',
article: 'Art. 9 Abs. 2 lit. h DSGVO',
name: { de: 'Gesundheitsversorgung', en: 'Health Care' },
shortName: { de: 'Gesundheit', en: 'Health' },
description: {
de: 'Die Verarbeitung ist für Zwecke der Gesundheitsvorsorge oder Arbeitsmedizin erforderlich, auf Grundlage von EU- oder nationalem Recht.',
en: 'Processing is necessary for health care purposes or occupational medicine based on EU or national law.',
},
requirements: [
{ de: 'Gesundheitsvorsorge, Arbeitsmedizin', en: 'Health care, occupational medicine' },
{ de: 'Rechtsgrundlage im EU/nationalen Recht', en: 'Legal basis in EU/national law' },
{ de: 'Verarbeitung durch Fachpersonal', en: 'Processing by health professionals' },
{ de: 'Berufsgeheimnis beachten', en: 'Professional secrecy' },
],
suitableFor: ['Medizinische Behandlung', 'Betriebsärztliche Untersuchungen', 'Gesundheitsmanagement'],
notSuitableFor: ['Verarbeitung ohne medizinischen Kontext'],
documentationNeeded: [
{ de: 'Rechtsgrundlage', en: 'Legal basis' },
{ de: 'Fachliche Zuständigkeit', en: 'Professional competence' },
],
isSpecialCategory: true,
},
{
type: 'ART9_PUBLIC_HEALTH',
article: 'Art. 9 Abs. 2 lit. i DSGVO',
name: { de: 'Öffentliche Gesundheit', en: 'Public Health' },
shortName: { de: 'Öff. Gesundheit', en: 'Public Health' },
description: {
de: 'Die Verarbeitung ist aus Gründen des öffentlichen Interesses im Bereich der öffentlichen Gesundheit erforderlich.',
en: 'Processing is necessary for reasons of public interest in the area of public health.',
},
requirements: [
{ de: 'Öffentliches Interesse an öffentlicher Gesundheit', en: 'Public interest in public health' },
{ de: 'Rechtsgrundlage im EU/nationalen Recht', en: 'Legal basis in EU/national law' },
{ de: 'Angemessene Garantien', en: 'Appropriate safeguards' },
],
suitableFor: ['Pandemiebekämpfung', 'Seuchenprävention', 'Qualitätssicherung im Gesundheitswesen'],
notSuitableFor: ['Private Interessen'],
documentationNeeded: [
{ de: 'Rechtsgrundlage', en: 'Legal basis' },
{ de: 'Nachweis öffentliches Interesse', en: 'Proof of public interest' },
],
isSpecialCategory: true,
},
{
type: 'ART9_LEGAL_CLAIMS',
article: 'Art. 9 Abs. 2 lit. f DSGVO',
name: { de: 'Rechtsansprüche', en: 'Legal Claims' },
shortName: { de: 'Rechtsansprüche', en: 'Legal Claims' },
description: {
de: 'Die Verarbeitung ist zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen erforderlich.',
en: 'Processing is necessary for the establishment, exercise or defence of legal claims.',
},
requirements: [
{ de: 'Rechtsansprüche bestehen oder drohen', en: 'Legal claims exist or are anticipated' },
{ de: 'Verarbeitung ist erforderlich', en: 'Processing is necessary' },
],
suitableFor: ['Rechtsstreitigkeiten', 'Compliance-Untersuchungen', 'Interne Ermittlungen'],
notSuitableFor: ['Präventive Maßnahmen ohne konkreten Anlass'],
documentationNeeded: [
{ de: 'Dokumentation des Rechtsstreits/Anspruchs', en: 'Documentation of legal dispute/claim' },
{ de: 'Erforderlichkeit der Verarbeitung', en: 'Necessity of processing' },
],
isSpecialCategory: true,
},
]
// ==========================================
// RETENTION PERIODS
// ==========================================
export const STANDARD_RETENTION_PERIODS: RetentionPeriodInfo[] = [
// Handelsrechtliche Aufbewahrung
{
id: 'hgb-257',
name: { de: 'Handelsbücher und Buchungsbelege', en: 'Commercial Books and Vouchers' },
legalBasis: '§ 257 HGB',
duration: { value: 10, unit: 'YEARS' },
description: {
de: 'Handelsbücher, Inventare, Eröffnungsbilanzen, Jahresabschlüsse, Lageberichte, Konzernabschlüsse, Buchungsbelege',
en: 'Commercial books, inventories, opening balance sheets, annual financial statements, management reports, consolidated financial statements, accounting vouchers',
},
applicableTo: ['Buchhaltung', 'Jahresabschlüsse', 'Rechnungen', 'Verträge'],
},
{
id: 'hgb-257-6',
name: { de: 'Handels- und Geschäftsbriefe', en: 'Commercial and Business Correspondence' },
legalBasis: '§ 257 Abs. 1 Nr. 2, 3 HGB',
duration: { value: 6, unit: 'YEARS' },
description: {
de: 'Empfangene Handels- und Geschäftsbriefe, Wiedergaben der abgesandten Handels- und Geschäftsbriefe',
en: 'Received commercial and business correspondence, copies of sent correspondence',
},
applicableTo: ['Geschäftskorrespondenz', 'Angebote', 'Auftragsbestätigungen'],
},
// Steuerrechtliche Aufbewahrung
{
id: 'ao-147',
name: { de: 'Steuerrechtliche Unterlagen', en: 'Tax Documents' },
legalBasis: '§ 147 AO',
duration: { value: 10, unit: 'YEARS' },
description: {
de: 'Bücher und Aufzeichnungen, Inventare, Jahresabschlüsse, Buchungsbelege, steuerrelevante Unterlagen',
en: 'Books and records, inventories, annual financial statements, accounting vouchers, tax-relevant documents',
},
applicableTo: ['Steuererklärungen', 'Buchhaltung', 'Belege'],
},
// Arbeitsrechtliche Aufbewahrung
{
id: 'arbeitsrecht-personal',
name: { de: 'Personalunterlagen', en: 'Personnel Records' },
legalBasis: 'Verschiedene (AGG, ArbZG, etc.)',
duration: { value: 3, unit: 'YEARS' },
description: {
de: 'Personalakte nach Beendigung des Arbeitsverhältnisses (Regelverjährung)',
en: 'Personnel file after termination of employment (standard limitation period)',
},
applicableTo: ['Personalakten', 'Arbeitsverträge', 'Zeugnisse'],
},
{
id: 'arbzg',
name: { de: 'Arbeitszeitaufzeichnungen', en: 'Working Time Records' },
legalBasis: '§ 16 Abs. 2 ArbZG',
duration: { value: 2, unit: 'YEARS' },
description: {
de: 'Aufzeichnungen über Arbeitszeiten, die über 8 Stunden hinausgehen',
en: 'Records of working hours exceeding 8 hours',
},
applicableTo: ['Zeiterfassung', 'Überstunden'],
},
{
id: 'lohnsteuer',
name: { de: 'Lohnunterlagen', en: 'Payroll Documents' },
legalBasis: '§ 41 EStG, § 28f SGB IV',
duration: { value: 6, unit: 'YEARS' },
description: {
de: 'Lohnkonten und Unterlagen für den Lohnsteuerabzug',
en: 'Payroll accounts and documents for wage tax deduction',
},
applicableTo: ['Lohnabrechnungen', 'Lohnsteuerbescheinigungen'],
},
{
id: 'sozialversicherung',
name: { de: 'Sozialversicherungsunterlagen', en: 'Social Security Documents' },
legalBasis: '§ 28f SGB IV',
duration: { value: 5, unit: 'YEARS' },
description: {
de: 'Unterlagen zum Gesamtsozialversicherungsbeitrag',
en: 'Documents for total social security contributions',
},
applicableTo: ['Sozialversicherungsmeldungen', 'Beitragsnachweise'],
},
// Bewerberdaten
{
id: 'bewerbung',
name: { de: 'Bewerbungsunterlagen', en: 'Application Documents' },
legalBasis: '§ 15 Abs. 4 AGG',
duration: { value: 6, unit: 'MONTHS' },
description: {
de: 'Bewerbungsunterlagen nach Absage (AGG-Frist)',
en: 'Application documents after rejection (AGG deadline)',
},
applicableTo: ['Bewerbungen', 'Lebensläufe', 'Zeugnisse von Bewerbern'],
},
// Datenschutzrechtliche Fristen
{
id: 'einwilligung',
name: { de: 'Einwilligungen', en: 'Consents' },
legalBasis: 'Art. 7 Abs. 1 DSGVO',
duration: { value: 3, unit: 'YEARS' },
description: {
de: 'Dokumentation der Einwilligung (Regelverjährung)',
en: 'Documentation of consent (standard limitation period)',
},
applicableTo: ['Einwilligungsnachweise', 'Opt-in-Dokumentation'],
},
{
id: 'videoüberwachung',
name: { de: 'Videoüberwachung', en: 'Video Surveillance' },
legalBasis: 'Verhältnismäßigkeit',
duration: { value: 72, unit: 'DAYS' },
description: {
de: 'Videoaufnahmen (max. 72 Stunden, sofern kein Vorfall)',
en: 'Video recordings (max. 72 hours, unless incident occurred)',
},
applicableTo: ['CCTV-Aufnahmen', 'Überwachungsvideos'],
},
// Löschung nach Vertrag
{
id: 'avv-loeschung',
name: { de: 'AVV-Daten nach Vertragsende', en: 'DPA Data after Contract End' },
legalBasis: 'Art. 28 Abs. 3 lit. g DSGVO',
duration: { value: 30, unit: 'DAYS' },
description: {
de: 'Löschung oder Rückgabe aller personenbezogenen Daten nach Vertragsende',
en: 'Deletion or return of all personal data after contract end',
},
applicableTo: ['Auftragsverarbeitung', 'Dienstleister-Daten'],
},
]
// ==========================================
// HELPER FUNCTIONS
// ==========================================
/**
* Get legal basis info by type
*/
export function getLegalBasisInfo(type: LegalBasisType): LegalBasisInfo | undefined {
return LEGAL_BASIS_INFO.find((lb) => lb.type === type)
}
/**
* Get legal bases for standard data (non-special categories)
*/
export function getStandardLegalBases(): LegalBasisInfo[] {
return LEGAL_BASIS_INFO.filter((lb) => !lb.isSpecialCategory)
}
/**
* Get legal bases for special category data (Art. 9)
*/
export function getSpecialCategoryLegalBases(): LegalBasisInfo[] {
return LEGAL_BASIS_INFO.filter((lb) => lb.isSpecialCategory)
}
/**
* Get appropriate legal bases for data categories
*/
export function getAppropriateLegalBases(
dataCategories: PersonalDataCategory[]
): LegalBasisInfo[] {
const hasSpecialCategory = dataCategories.some((cat) =>
[
'HEALTH_DATA', 'GENETIC_DATA', 'BIOMETRIC_DATA', 'RACIAL_ETHNIC',
'POLITICAL_OPINIONS', 'RELIGIOUS_BELIEFS', 'TRADE_UNION', 'SEX_LIFE',
].includes(cat)
)
if (hasSpecialCategory) {
// Return Art. 9 bases plus compatible Art. 6 bases
return [
...getSpecialCategoryLegalBases(),
...getStandardLegalBases().filter((lb) =>
['LEGAL_OBLIGATION', 'VITAL_INTEREST', 'PUBLIC_TASK'].includes(lb.type)
),
]
}
return getStandardLegalBases()
}
/**
* Get retention period by ID
*/
export function getRetentionPeriod(id: string): RetentionPeriodInfo | undefined {
return STANDARD_RETENTION_PERIODS.find((rp) => rp.id === id)
}
/**
* Get retention periods applicable to a category
*/
export function getRetentionPeriodsForCategory(category: string): RetentionPeriodInfo[] {
return STANDARD_RETENTION_PERIODS.filter((rp) =>
rp.applicableTo.some((a) => a.toLowerCase().includes(category.toLowerCase()))
)
}
/**
* Get longest applicable retention period
*/
export function getLongestRetentionPeriod(categories: string[]): RetentionPeriodInfo | undefined {
const applicable = categories.flatMap((cat) => getRetentionPeriodsForCategory(cat))
if (applicable.length === 0) return undefined
return applicable.reduce((longest, current) => {
const longestMonths = toMonths(longest.duration)
const currentMonths = toMonths(current.duration)
return currentMonths > longestMonths ? current : longest
})
}
function toMonths(duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' }): number {
switch (duration.unit) {
case 'DAYS':
return duration.value / 30
case 'MONTHS':
return duration.value
case 'YEARS':
return duration.value * 12
}
}
/**
* Format retention period for display
*/
export function formatRetentionPeriod(
duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' },
locale: 'de' | 'en' = 'de'
): string {
const units = {
de: { DAYS: 'Tage', MONTHS: 'Monate', YEARS: 'Jahre' },
en: { DAYS: 'days', MONTHS: 'months', YEARS: 'years' },
}
return `${duration.value} ${units[locale][duration.unit]}`
}
@@ -0,0 +1,813 @@
/**
* Standard Processing Activities Catalog
*
* 28 predefined processing activities templates following Art. 30 DSGVO
*/
import {
ProcessingActivityFormData,
DataSubjectCategory,
PersonalDataCategory,
LegalBasisType,
ProtectionLevel,
LocalizedText,
} from '../types'
export interface ProcessingActivityTemplate {
id: string
category: ProcessingActivityCategory
name: LocalizedText
description: LocalizedText
purposes: LocalizedText[]
dataSubjectCategories: DataSubjectCategory[]
personalDataCategories: PersonalDataCategory[]
suggestedLegalBasis: LegalBasisType[]
suggestedRetentionYears: number
suggestedProtectionLevel: ProtectionLevel
dpiaLikely: boolean
commonSystems: string[]
commonVendorCategories: string[]
}
export type ProcessingActivityCategory =
| 'HR' // Human Resources
| 'SALES' // Vertrieb
| 'MARKETING' // Marketing
| 'FINANCE' // Finanzen
| 'IT' // IT & Sicherheit
| 'CUSTOMER_SERVICE' // Kundenservice
| 'WEBSITE' // Website & Apps
| 'GENERAL' // Allgemein
export const PROCESSING_ACTIVITY_CATEGORY_META: Record<ProcessingActivityCategory, LocalizedText> = {
HR: { de: 'Personal', en: 'Human Resources' },
SALES: { de: 'Vertrieb', en: 'Sales' },
MARKETING: { de: 'Marketing', en: 'Marketing' },
FINANCE: { de: 'Finanzen', en: 'Finance' },
IT: { de: 'IT & Sicherheit', en: 'IT & Security' },
CUSTOMER_SERVICE: { de: 'Kundenservice', en: 'Customer Service' },
WEBSITE: { de: 'Website & Apps', en: 'Website & Apps' },
GENERAL: { de: 'Allgemein', en: 'General' },
}
export const PROCESSING_ACTIVITY_TEMPLATES: ProcessingActivityTemplate[] = [
// ==========================================
// HR - Human Resources
// ==========================================
{
id: 'tpl-hr-recruitment',
category: 'HR',
name: {
de: 'Bewerbermanagement',
en: 'Recruitment Management',
},
description: {
de: 'Verarbeitung von Bewerberdaten im Rahmen des Recruiting-Prozesses',
en: 'Processing of applicant data as part of the recruitment process',
},
purposes: [
{ de: 'Durchführung des Bewerbungsverfahrens', en: 'Conducting the application process' },
{ de: 'Prüfung der Eignung', en: 'Assessing suitability' },
{ de: 'Aufbau eines Talentpools (bei Einwilligung)', en: 'Building a talent pool (with consent)' },
],
dataSubjectCategories: ['APPLICANTS'],
personalDataCategories: [
'NAME', 'CONTACT', 'ADDRESS', 'DOB', 'EDUCATION_DATA',
'EMPLOYMENT_DATA', 'PHOTO_VIDEO',
],
suggestedLegalBasis: ['CONTRACT', 'CONSENT'],
suggestedRetentionYears: 0.5, // 6 Monate nach Absage
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['E-Recruiting', 'Personio', 'Workday'],
commonVendorCategories: ['HR_SOFTWARE', 'CLOUD_INFRASTRUCTURE'],
},
{
id: 'tpl-hr-personnel',
category: 'HR',
name: {
de: 'Personalverwaltung',
en: 'Personnel Administration',
},
description: {
de: 'Führung der Personalakte und Verwaltung des Beschäftigungsverhältnisses',
en: 'Maintaining personnel files and managing employment relationships',
},
purposes: [
{ de: 'Führung der Personalakte', en: 'Maintaining personnel files' },
{ de: 'Durchführung des Arbeitsverhältnisses', en: 'Executing the employment relationship' },
{ de: 'Erfüllung gesetzlicher Pflichten', en: 'Fulfilling legal obligations' },
],
dataSubjectCategories: ['EMPLOYEES'],
personalDataCategories: [
'NAME', 'CONTACT', 'ADDRESS', 'DOB', 'ID_NUMBER',
'SOCIAL_SECURITY', 'TAX_ID', 'BANK_ACCOUNT', 'EMPLOYMENT_DATA',
'SALARY_DATA', 'EDUCATION_DATA', 'PHOTO_VIDEO',
],
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
suggestedRetentionYears: 10, // Nach Beendigung
suggestedProtectionLevel: 'HIGH',
dpiaLikely: false,
commonSystems: ['SAP HCM', 'Personio', 'DATEV'],
commonVendorCategories: ['HR_SOFTWARE', 'ERP'],
},
{
id: 'tpl-hr-payroll',
category: 'HR',
name: {
de: 'Lohn- und Gehaltsabrechnung',
en: 'Payroll Processing',
},
description: {
de: 'Berechnung und Auszahlung von Gehältern, Abführung von Steuern und Sozialabgaben',
en: 'Calculation and payment of salaries, tax and social security contributions',
},
purposes: [
{ de: 'Gehaltsberechnung und -auszahlung', en: 'Salary calculation and payment' },
{ de: 'Abführung von Lohnsteuer und Sozialabgaben', en: 'Payment of payroll taxes and social contributions' },
{ de: 'Erstellung von Lohnabrechnungen', en: 'Creating payslips' },
],
dataSubjectCategories: ['EMPLOYEES'],
personalDataCategories: [
'NAME', 'ADDRESS', 'DOB', 'SOCIAL_SECURITY', 'TAX_ID',
'BANK_ACCOUNT', 'SALARY_DATA', 'EMPLOYMENT_DATA',
],
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
suggestedRetentionYears: 10, // Handels- und Steuerrecht
suggestedProtectionLevel: 'HIGH',
dpiaLikely: false,
commonSystems: ['DATEV', 'SAP', 'Lexware'],
commonVendorCategories: ['ACCOUNTING', 'HR_SOFTWARE'],
},
{
id: 'tpl-hr-time-tracking',
category: 'HR',
name: {
de: 'Arbeitszeiterfassung',
en: 'Time Tracking',
},
description: {
de: 'Erfassung der Arbeitszeiten zur Einhaltung des Arbeitszeitgesetzes',
en: 'Recording working hours for compliance with working time regulations',
},
purposes: [
{ de: 'Erfassung der Arbeitszeiten', en: 'Recording working hours' },
{ de: 'Einhaltung des Arbeitszeitgesetzes', en: 'Compliance with working time regulations' },
{ de: 'Grundlage für Gehaltsabrechnung', en: 'Basis for payroll' },
],
dataSubjectCategories: ['EMPLOYEES'],
personalDataCategories: ['NAME', 'EMPLOYMENT_DATA', 'USAGE_DATA'],
suggestedLegalBasis: ['LEGAL_OBLIGATION', 'CONTRACT'],
suggestedRetentionYears: 2,
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['ATOSS', 'Clockodo', 'Toggl'],
commonVendorCategories: ['HR_SOFTWARE'],
},
{
id: 'tpl-hr-health-management',
category: 'HR',
name: {
de: 'Betriebliches Gesundheitsmanagement',
en: 'Occupational Health Management',
},
description: {
de: 'Verwaltung von Arbeitsunfähigkeitsbescheinigungen und betriebsärztlichen Untersuchungen',
en: 'Management of sick notes and occupational health examinations',
},
purposes: [
{ de: 'Verwaltung von Krankmeldungen', en: 'Managing sick leave' },
{ de: 'Organisation betriebsärztlicher Untersuchungen', en: 'Organizing occupational health examinations' },
{ de: 'Betriebliches Eingliederungsmanagement', en: 'Occupational reintegration management' },
],
dataSubjectCategories: ['EMPLOYEES'],
personalDataCategories: ['NAME', 'EMPLOYMENT_DATA', 'HEALTH_DATA'],
suggestedLegalBasis: ['ART9_EMPLOYMENT', 'LEGAL_OBLIGATION'],
suggestedRetentionYears: 3,
suggestedProtectionLevel: 'HIGH',
dpiaLikely: true,
commonSystems: ['HR-Software', 'BEM-System'],
commonVendorCategories: ['HR_SOFTWARE', 'CONSULTING'],
},
// ==========================================
// SALES - Vertrieb
// ==========================================
{
id: 'tpl-sales-crm',
category: 'SALES',
name: {
de: 'Kundenbeziehungsmanagement (CRM)',
en: 'Customer Relationship Management (CRM)',
},
description: {
de: 'Verwaltung von Kundenbeziehungen, Kontakthistorie und Verkaufschancen',
en: 'Managing customer relationships, contact history, and sales opportunities',
},
purposes: [
{ de: 'Pflege von Kundenbeziehungen', en: 'Maintaining customer relationships' },
{ de: 'Dokumentation von Kundenkontakten', en: 'Documenting customer contacts' },
{ de: 'Vertriebssteuerung', en: 'Sales management' },
],
dataSubjectCategories: ['CUSTOMERS', 'PROSPECTIVE_CUSTOMERS', 'BUSINESS_PARTNERS'],
personalDataCategories: [
'NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'COMMUNICATION_DATA',
],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 3, // Nach letztem Kontakt
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['Salesforce', 'HubSpot', 'Pipedrive', 'Microsoft Dynamics'],
commonVendorCategories: ['CRM'],
},
{
id: 'tpl-sales-contract-management',
category: 'SALES',
name: {
de: 'Vertragsmanagement',
en: 'Contract Management',
},
description: {
de: 'Verwaltung von Kundenverträgen, Angeboten und Aufträgen',
en: 'Managing customer contracts, quotes, and orders',
},
purposes: [
{ de: 'Erstellung und Verwaltung von Verträgen', en: 'Creating and managing contracts' },
{ de: 'Angebotsverfolgung', en: 'Quote tracking' },
{ de: 'Auftragsabwicklung', en: 'Order processing' },
],
dataSubjectCategories: ['CUSTOMERS', 'BUSINESS_PARTNERS'],
personalDataCategories: [
'NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'PAYMENT_DATA',
],
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
suggestedRetentionYears: 10, // Handelsrechtlich
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['ERP', 'CRM', 'Vertragsverwaltung'],
commonVendorCategories: ['ERP', 'CRM'],
},
// ==========================================
// MARKETING
// ==========================================
{
id: 'tpl-marketing-newsletter',
category: 'MARKETING',
name: {
de: 'Newsletter-Versand',
en: 'Newsletter Distribution',
},
description: {
de: 'Versand von E-Mail-Newslettern und Marketing-Kommunikation',
en: 'Sending email newsletters and marketing communications',
},
purposes: [
{ de: 'Versand von Newsletter und Marketing-E-Mails', en: 'Sending newsletters and marketing emails' },
{ de: 'Messung von Öffnungs- und Klickraten', en: 'Measuring open and click rates' },
],
dataSubjectCategories: ['NEWSLETTER_SUBSCRIBERS', 'CUSTOMERS'],
personalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
suggestedLegalBasis: ['CONSENT'],
suggestedRetentionYears: 0, // Bis Widerruf
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['Mailchimp', 'CleverReach', 'Sendinblue'],
commonVendorCategories: ['EMAIL', 'MARKETING'],
},
{
id: 'tpl-marketing-advertising',
category: 'MARKETING',
name: {
de: 'Online-Werbung',
en: 'Online Advertising',
},
description: {
de: 'Schaltung und Auswertung von Online-Werbeanzeigen',
en: 'Running and analyzing online advertisements',
},
purposes: [
{ de: 'Schaltung von Online-Werbung', en: 'Running online advertisements' },
{ de: 'Conversion-Tracking', en: 'Conversion tracking' },
{ de: 'Retargeting', en: 'Retargeting' },
],
dataSubjectCategories: ['WEBSITE_USERS'],
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA'],
suggestedLegalBasis: ['CONSENT'],
suggestedRetentionYears: 1,
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: true,
commonSystems: ['Google Ads', 'Meta Ads', 'LinkedIn Ads'],
commonVendorCategories: ['MARKETING', 'ANALYTICS'],
},
{
id: 'tpl-marketing-events',
category: 'MARKETING',
name: {
de: 'Veranstaltungsmanagement',
en: 'Event Management',
},
description: {
de: 'Organisation und Durchführung von Veranstaltungen, Messen und Webinaren',
en: 'Organizing and conducting events, trade shows, and webinars',
},
purposes: [
{ de: 'Teilnehmerregistrierung', en: 'Participant registration' },
{ de: 'Veranstaltungsdurchführung', en: 'Event execution' },
{ de: 'Nachbereitung und Follow-up', en: 'Follow-up activities' },
],
dataSubjectCategories: ['CUSTOMERS', 'PROSPECTIVE_CUSTOMERS', 'BUSINESS_PARTNERS'],
personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'PHOTO_VIDEO'],
suggestedLegalBasis: ['CONTRACT', 'CONSENT'],
suggestedRetentionYears: 2,
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['Eventbrite', 'GoToWebinar', 'Zoom'],
commonVendorCategories: ['MARKETING', 'COMMUNICATION'],
},
// ==========================================
// FINANCE
// ==========================================
{
id: 'tpl-finance-accounting',
category: 'FINANCE',
name: {
de: 'Finanzbuchhaltung',
en: 'Financial Accounting',
},
description: {
de: 'Führung der Finanzbuchhaltung, Rechnungsstellung und Zahlungsabwicklung',
en: 'Financial accounting, invoicing, and payment processing',
},
purposes: [
{ de: 'Buchführung und Rechnungswesen', en: 'Bookkeeping and accounting' },
{ de: 'Rechnungsstellung', en: 'Invoicing' },
{ de: 'Zahlungsabwicklung', en: 'Payment processing' },
],
dataSubjectCategories: ['CUSTOMERS', 'SUPPLIERS', 'BUSINESS_PARTNERS'],
personalDataCategories: [
'NAME', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA', 'CONTRACT_DATA', 'TAX_ID',
],
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
suggestedRetentionYears: 10, // HGB/AO
suggestedProtectionLevel: 'HIGH',
dpiaLikely: false,
commonSystems: ['DATEV', 'SAP', 'Lexware', 'Xero'],
commonVendorCategories: ['ACCOUNTING', 'ERP'],
},
{
id: 'tpl-finance-debt-collection',
category: 'FINANCE',
name: {
de: 'Forderungsmanagement',
en: 'Debt Collection',
},
description: {
de: 'Verwaltung offener Forderungen und Mahnwesen',
en: 'Managing outstanding receivables and dunning',
},
purposes: [
{ de: 'Überwachung offener Forderungen', en: 'Monitoring outstanding receivables' },
{ de: 'Mahnwesen', en: 'Dunning process' },
{ de: 'Inkasso bei Bedarf', en: 'Debt collection if necessary' },
],
dataSubjectCategories: ['CUSTOMERS'],
personalDataCategories: ['NAME', 'ADDRESS', 'CONTACT', 'PAYMENT_DATA', 'CONTRACT_DATA'],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 10,
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['ERP', 'Inkasso-Software'],
commonVendorCategories: ['ACCOUNTING', 'LEGAL'],
},
// ==========================================
// IT & SICHERHEIT
// ==========================================
{
id: 'tpl-it-user-management',
category: 'IT',
name: {
de: 'IT-Benutzerverwaltung',
en: 'IT User Management',
},
description: {
de: 'Verwaltung von Benutzerkonten, Zugriffsrechten und Authentifizierung',
en: 'Managing user accounts, access rights, and authentication',
},
purposes: [
{ de: 'Verwaltung von Benutzerkonten', en: 'Managing user accounts' },
{ de: 'Zugriffssteuerung', en: 'Access control' },
{ de: 'Single Sign-On', en: 'Single Sign-On' },
],
dataSubjectCategories: ['EMPLOYEES'],
personalDataCategories: ['NAME', 'CONTACT', 'LOGIN_DATA', 'USAGE_DATA'],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 1, // Nach Kontoschließung
suggestedProtectionLevel: 'HIGH',
dpiaLikely: false,
commonSystems: ['Active Directory', 'Okta', 'Azure AD'],
commonVendorCategories: ['SECURITY', 'CLOUD_INFRASTRUCTURE'],
},
{
id: 'tpl-it-logging',
category: 'IT',
name: {
de: 'IT-Protokollierung',
en: 'IT Logging',
},
description: {
de: 'Protokollierung von IT-Aktivitäten zur Sicherheit und Fehleranalyse',
en: 'Logging IT activities for security and error analysis',
},
purposes: [
{ de: 'Sicherheitsüberwachung', en: 'Security monitoring' },
{ de: 'Fehleranalyse', en: 'Error analysis' },
{ de: 'Nachvollziehbarkeit', en: 'Traceability' },
],
dataSubjectCategories: ['EMPLOYEES', 'CUSTOMERS', 'WEBSITE_USERS'],
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOGIN_DATA'],
suggestedLegalBasis: ['LEGITIMATE_INTEREST'],
suggestedRetentionYears: 1,
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['Splunk', 'ELK Stack', 'Datadog'],
commonVendorCategories: ['SECURITY', 'ANALYTICS'],
},
{
id: 'tpl-it-video-surveillance',
category: 'IT',
name: {
de: 'Videoüberwachung',
en: 'Video Surveillance',
},
description: {
de: 'Videoüberwachung von Geschäftsräumen zum Schutz vor Diebstahl und Vandalismus',
en: 'Video surveillance of business premises for theft and vandalism prevention',
},
purposes: [
{ de: 'Schutz vor Diebstahl und Vandalismus', en: 'Protection against theft and vandalism' },
{ de: 'Zugangskontrolle', en: 'Access control' },
{ de: 'Beweissicherung', en: 'Evidence preservation' },
],
dataSubjectCategories: ['EMPLOYEES', 'VISITORS', 'CUSTOMERS'],
personalDataCategories: ['PHOTO_VIDEO', 'BIOMETRIC_DATA'],
suggestedLegalBasis: ['LEGITIMATE_INTEREST'],
suggestedRetentionYears: 0.1, // 72 Stunden
suggestedProtectionLevel: 'HIGH',
dpiaLikely: true,
commonSystems: ['CCTV-System'],
commonVendorCategories: ['SECURITY'],
},
{
id: 'tpl-it-backup',
category: 'IT',
name: {
de: 'Datensicherung (Backup)',
en: 'Data Backup',
},
description: {
de: 'Regelmäßige Sicherung von Unternehmensdaten',
en: 'Regular backup of company data',
},
purposes: [
{ de: 'Datensicherung', en: 'Data backup' },
{ de: 'Disaster Recovery', en: 'Disaster Recovery' },
{ de: 'Geschäftskontinuität', en: 'Business continuity' },
],
dataSubjectCategories: ['EMPLOYEES', 'CUSTOMERS', 'SUPPLIERS'],
personalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
suggestedLegalBasis: ['LEGITIMATE_INTEREST', 'LEGAL_OBLIGATION'],
suggestedRetentionYears: 1, // Je nach Backup-Konzept
suggestedProtectionLevel: 'HIGH',
dpiaLikely: false,
commonSystems: ['Veeam', 'AWS Backup', 'Azure Backup'],
commonVendorCategories: ['BACKUP', 'CLOUD_INFRASTRUCTURE'],
},
// ==========================================
// CUSTOMER SERVICE
// ==========================================
{
id: 'tpl-cs-support',
category: 'CUSTOMER_SERVICE',
name: {
de: 'Kundenbetreuung und Support',
en: 'Customer Support',
},
description: {
de: 'Bearbeitung von Kundenanfragen, Beschwerden und Support-Tickets',
en: 'Handling customer inquiries, complaints, and support tickets',
},
purposes: [
{ de: 'Bearbeitung von Kundenanfragen', en: 'Handling customer inquiries' },
{ de: 'Beschwerdemanagement', en: 'Complaint management' },
{ de: 'Technischer Support', en: 'Technical support' },
],
dataSubjectCategories: ['CUSTOMERS'],
personalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 3,
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['Zendesk', 'Freshdesk', 'Intercom'],
commonVendorCategories: ['SUPPORT', 'CRM'],
},
{
id: 'tpl-cs-satisfaction',
category: 'CUSTOMER_SERVICE',
name: {
de: 'Kundenzufriedenheitsbefragungen',
en: 'Customer Satisfaction Surveys',
},
description: {
de: 'Durchführung von Umfragen zur Messung der Kundenzufriedenheit',
en: 'Conducting surveys to measure customer satisfaction',
},
purposes: [
{ de: 'Messung der Kundenzufriedenheit', en: 'Measuring customer satisfaction' },
{ de: 'Qualitätsverbesserung', en: 'Quality improvement' },
],
dataSubjectCategories: ['CUSTOMERS'],
personalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
suggestedLegalBasis: ['CONSENT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 2,
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['SurveyMonkey', 'Typeform', 'NPS-Tools'],
commonVendorCategories: ['ANALYTICS', 'MARKETING'],
},
// ==========================================
// WEBSITE & APPS
// ==========================================
{
id: 'tpl-web-analytics',
category: 'WEBSITE',
name: {
de: 'Web-Analyse',
en: 'Web Analytics',
},
description: {
de: 'Analyse des Nutzerverhaltens auf der Website zur Optimierung',
en: 'Analyzing user behavior on the website for optimization',
},
purposes: [
{ de: 'Analyse des Nutzerverhaltens', en: 'Analyzing user behavior' },
{ de: 'Website-Optimierung', en: 'Website optimization' },
{ de: 'Conversion-Tracking', en: 'Conversion tracking' },
],
dataSubjectCategories: ['WEBSITE_USERS'],
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOCATION_DATA'],
suggestedLegalBasis: ['CONSENT'],
suggestedRetentionYears: 2,
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['Google Analytics', 'Matomo', 'Plausible'],
commonVendorCategories: ['ANALYTICS'],
},
{
id: 'tpl-web-contact-form',
category: 'WEBSITE',
name: {
de: 'Kontaktformular',
en: 'Contact Form',
},
description: {
de: 'Verarbeitung von Anfragen über das Website-Kontaktformular',
en: 'Processing inquiries submitted via the website contact form',
},
purposes: [
{ de: 'Bearbeitung von Kontaktanfragen', en: 'Processing contact inquiries' },
{ de: 'Kommunikation mit Interessenten', en: 'Communication with prospects' },
],
dataSubjectCategories: ['PROSPECTIVE_CUSTOMERS', 'WEBSITE_USERS'],
personalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 1,
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['CRM', 'E-Mail-System'],
commonVendorCategories: ['CRM', 'EMAIL'],
},
{
id: 'tpl-web-user-accounts',
category: 'WEBSITE',
name: {
de: 'Benutzerkonten / Kundenportal',
en: 'User Accounts / Customer Portal',
},
description: {
de: 'Verwaltung von Benutzerkonten im Kundenportal oder Online-Shop',
en: 'Managing user accounts in customer portal or online shop',
},
purposes: [
{ de: 'Bereitstellung des Kundenportals', en: 'Providing customer portal' },
{ de: 'Benutzerverwaltung', en: 'User management' },
{ de: 'Personalisierung', en: 'Personalization' },
],
dataSubjectCategories: ['CUSTOMERS', 'APP_USERS'],
personalDataCategories: ['NAME', 'CONTACT', 'LOGIN_DATA', 'USAGE_DATA', 'CONTRACT_DATA'],
suggestedLegalBasis: ['CONTRACT'],
suggestedRetentionYears: 1, // Nach Kontoschließung
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['E-Commerce', 'CRM', 'Auth0'],
commonVendorCategories: ['HOSTING', 'CRM', 'SECURITY'],
},
{
id: 'tpl-web-cookies',
category: 'WEBSITE',
name: {
de: 'Cookie-Verwaltung',
en: 'Cookie Management',
},
description: {
de: 'Verwaltung von Cookies und Einholung von Cookie-Einwilligungen',
en: 'Managing cookies and obtaining cookie consents',
},
purposes: [
{ de: 'Speicherung von Cookie-Präferenzen', en: 'Storing cookie preferences' },
{ de: 'Einwilligungsmanagement', en: 'Consent management' },
],
dataSubjectCategories: ['WEBSITE_USERS'],
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA'],
suggestedLegalBasis: ['CONSENT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 1,
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['Cookiebot', 'Usercentrics', 'OneTrust'],
commonVendorCategories: ['ANALYTICS', 'SECURITY'],
},
// ==========================================
// GENERAL
// ==========================================
{
id: 'tpl-gen-communication',
category: 'GENERAL',
name: {
de: 'Geschäftliche Kommunikation',
en: 'Business Communication',
},
description: {
de: 'E-Mail-Kommunikation, Telefonie und Messaging im Geschäftsverkehr',
en: 'Email communication, telephony, and messaging in business operations',
},
purposes: [
{ de: 'Geschäftliche Kommunikation', en: 'Business communication' },
{ de: 'Dokumentation von Korrespondenz', en: 'Documentation of correspondence' },
],
dataSubjectCategories: ['CUSTOMERS', 'SUPPLIERS', 'BUSINESS_PARTNERS', 'EMPLOYEES'],
personalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 6, // Handelsrechtlich relevant
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['Microsoft 365', 'Google Workspace', 'Slack'],
commonVendorCategories: ['EMAIL', 'COMMUNICATION', 'CLOUD_INFRASTRUCTURE'],
},
{
id: 'tpl-gen-visitor',
category: 'GENERAL',
name: {
de: 'Besucherverwaltung',
en: 'Visitor Management',
},
description: {
de: 'Erfassung und Verwaltung von Besuchern in Geschäftsräumen',
en: 'Recording and managing visitors in business premises',
},
purposes: [
{ de: 'Zutrittskontrolle', en: 'Access control' },
{ de: 'Sicherheit', en: 'Security' },
{ de: 'Nachvollziehbarkeit', en: 'Traceability' },
],
dataSubjectCategories: ['VISITORS'],
personalDataCategories: ['NAME', 'CONTACT', 'PHOTO_VIDEO'],
suggestedLegalBasis: ['LEGITIMATE_INTEREST'],
suggestedRetentionYears: 0.1, // 1 Monat
suggestedProtectionLevel: 'LOW',
dpiaLikely: false,
commonSystems: ['Besuchermanagement-System'],
commonVendorCategories: ['SECURITY'],
},
{
id: 'tpl-gen-supplier',
category: 'GENERAL',
name: {
de: 'Lieferantenverwaltung',
en: 'Supplier Management',
},
description: {
de: 'Verwaltung von Lieferantenbeziehungen und Beschaffung',
en: 'Managing supplier relationships and procurement',
},
purposes: [
{ de: 'Lieferantenverwaltung', en: 'Supplier management' },
{ de: 'Beschaffung', en: 'Procurement' },
{ de: 'Qualitätsmanagement', en: 'Quality management' },
],
dataSubjectCategories: ['SUPPLIERS', 'BUSINESS_PARTNERS'],
personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'BANK_ACCOUNT'],
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
suggestedRetentionYears: 10,
suggestedProtectionLevel: 'MEDIUM',
dpiaLikely: false,
commonSystems: ['ERP', 'Lieferantenportal'],
commonVendorCategories: ['ERP'],
},
{
id: 'tpl-gen-whistleblower',
category: 'GENERAL',
name: {
de: 'Hinweisgebersystem',
en: 'Whistleblower System',
},
description: {
de: 'Entgegennahme und Bearbeitung von Hinweisen gemäß Hinweisgeberschutzgesetz',
en: 'Receiving and processing reports according to whistleblower protection law',
},
purposes: [
{ de: 'Entgegennahme von Hinweisen', en: 'Receiving reports' },
{ de: 'Untersuchung von Verstößen', en: 'Investigating violations' },
{ de: 'Schutz von Hinweisgebern', en: 'Protecting whistleblowers' },
],
dataSubjectCategories: ['EMPLOYEES', 'BUSINESS_PARTNERS'],
personalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
suggestedLegalBasis: ['LEGAL_OBLIGATION'],
suggestedRetentionYears: 3,
suggestedProtectionLevel: 'HIGH',
dpiaLikely: true,
commonSystems: ['Hinweisgeberportal'],
commonVendorCategories: ['SECURITY', 'LEGAL'],
},
]
/**
* Get templates by category
*/
export function getTemplatesByCategory(
category: ProcessingActivityCategory
): ProcessingActivityTemplate[] {
return PROCESSING_ACTIVITY_TEMPLATES.filter((t) => t.category === category)
}
/**
* Get template by ID
*/
export function getTemplateById(id: string): ProcessingActivityTemplate | undefined {
return PROCESSING_ACTIVITY_TEMPLATES.find((t) => t.id === id)
}
/**
* Get all categories with their templates
*/
export function getGroupedTemplates(): Map<ProcessingActivityCategory, ProcessingActivityTemplate[]> {
const grouped = new Map<ProcessingActivityCategory, ProcessingActivityTemplate[]>()
for (const template of PROCESSING_ACTIVITY_TEMPLATES) {
const existing = grouped.get(template.category) || []
grouped.set(template.category, [...existing, template])
}
return grouped
}
/**
* Create form data from template
*/
export function createFormDataFromTemplate(
template: ProcessingActivityTemplate,
organizationDefaults?: {
responsible?: ProcessingActivityFormData['responsible']
dpoContact?: ProcessingActivityFormData['dpoContact']
}
): Partial<ProcessingActivityFormData> {
return {
vvtId: '', // Will be generated
name: template.name,
purposes: template.purposes,
dataSubjectCategories: template.dataSubjectCategories,
personalDataCategories: template.personalDataCategories,
legalBasis: template.suggestedLegalBasis.map((type) => ({ type })),
protectionLevel: template.suggestedProtectionLevel,
dpiaRequired: template.dpiaLikely,
retentionPeriod: {
duration: template.suggestedRetentionYears,
durationUnit: 'YEARS',
description: { de: '', en: '' },
},
recipientCategories: [],
thirdCountryTransfers: [],
technicalMeasures: [],
dataSources: [],
systems: [],
dataFlows: [],
subProcessors: [],
owner: '',
responsible: organizationDefaults?.responsible,
dpoContact: organizationDefaults?.dpoContact,
}
}
@@ -0,0 +1,564 @@
/**
* Vendor Templates and Categories
*
* Pre-defined vendor templates and risk profiles
*/
import {
VendorFormData,
VendorRole,
ServiceCategory,
DataAccessLevel,
TransferMechanismType,
DocumentType,
ReviewFrequency,
LocalizedText,
PersonalDataCategory,
} from '../types'
export interface VendorTemplate {
id: string
name: LocalizedText
description: LocalizedText
serviceCategory: ServiceCategory
suggestedRole: VendorRole
suggestedDataAccess: DataAccessLevel
suggestedTransferMechanisms: TransferMechanismType[]
suggestedContractTypes: DocumentType[]
typicalDataCategories: PersonalDataCategory[]
typicalCertifications: string[]
inherentRiskFactors: RiskFactorWeight[]
commonProviders: string[]
}
export interface RiskFactorWeight {
factor: string
weight: number // 0-1
description: LocalizedText
}
export interface CountryRiskProfile {
code: string // ISO 3166-1 alpha-2
name: LocalizedText
isEU: boolean
isEEA: boolean
hasAdequacyDecision: boolean
adequacyDecisionDate?: string
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
notes?: LocalizedText
}
// ==========================================
// VENDOR TEMPLATES
// ==========================================
export const VENDOR_TEMPLATES: VendorTemplate[] = [
// Cloud & Infrastructure
{
id: 'tpl-vendor-cloud-iaas',
name: { de: 'Cloud IaaS-Anbieter', en: 'Cloud IaaS Provider' },
description: {
de: 'Infrastructure-as-a-Service Anbieter (AWS, Azure, GCP)',
en: 'Infrastructure-as-a-Service provider (AWS, Azure, GCP)',
},
serviceCategory: 'CLOUD_INFRASTRUCTURE',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA', 'TOM_ANNEX'],
typicalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA', 'IP_ADDRESS'],
typicalCertifications: ['ISO 27001', 'SOC 2', 'C5'],
inherentRiskFactors: [
{ factor: 'data_volume', weight: 0.9, description: { de: 'Hohes Datenvolumen', en: 'High data volume' } },
{ factor: 'criticality', weight: 0.9, description: { de: 'Geschäftskritisch', en: 'Business critical' } },
{ factor: 'sub_processors', weight: 0.7, description: { de: 'Viele Unterauftragnehmer', en: 'Many sub-processors' } },
],
commonProviders: ['AWS', 'Microsoft Azure', 'Google Cloud Platform', 'Hetzner', 'OVH'],
},
{
id: 'tpl-vendor-hosting',
name: { de: 'Webhosting-Anbieter', en: 'Web Hosting Provider' },
description: {
de: 'Hosting von Websites und Webanwendungen',
en: 'Hosting of websites and web applications',
},
serviceCategory: 'HOSTING',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'ADMINISTRATIVE',
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
typicalDataCategories: ['IP_ADDRESS', 'USAGE_DATA', 'LOGIN_DATA'],
typicalCertifications: ['ISO 27001'],
inherentRiskFactors: [
{ factor: 'data_volume', weight: 0.6, description: { de: 'Mittleres Datenvolumen', en: 'Medium data volume' } },
{ factor: 'criticality', weight: 0.7, description: { de: 'Wichtig für Betrieb', en: 'Important for operations' } },
],
commonProviders: ['Hetzner', 'All-Inkl', 'IONOS', 'Strato', 'DigitalOcean'],
},
{
id: 'tpl-vendor-cdn',
name: { de: 'CDN-Anbieter', en: 'CDN Provider' },
description: {
de: 'Content Delivery Network für schnelle Inhaltsauslieferung',
en: 'Content Delivery Network for fast content delivery',
},
serviceCategory: 'CDN',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'POTENTIAL',
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA'],
typicalDataCategories: ['IP_ADDRESS', 'USAGE_DATA'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'data_transit', weight: 0.5, description: { de: 'Daten im Transit', en: 'Data in transit' } },
{ factor: 'global_presence', weight: 0.6, description: { de: 'Globale Präsenz', en: 'Global presence' } },
],
commonProviders: ['Cloudflare', 'Fastly', 'Akamai', 'AWS CloudFront'],
},
// Business Software
{
id: 'tpl-vendor-crm',
name: { de: 'CRM-System', en: 'CRM System' },
description: {
de: 'Customer Relationship Management System',
en: 'Customer Relationship Management System',
},
serviceCategory: 'CRM',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA', 'TOM_ANNEX'],
typicalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'COMMUNICATION_DATA', 'CONTRACT_DATA'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'customer_data', weight: 0.8, description: { de: 'Kundendaten', en: 'Customer data' } },
{ factor: 'data_volume', weight: 0.7, description: { de: 'Hohes Datenvolumen', en: 'High data volume' } },
],
commonProviders: ['Salesforce', 'HubSpot', 'Pipedrive', 'Microsoft Dynamics', 'Zoho CRM'],
},
{
id: 'tpl-vendor-erp',
name: { de: 'ERP-System', en: 'ERP System' },
description: {
de: 'Enterprise Resource Planning System',
en: 'Enterprise Resource Planning System',
},
serviceCategory: 'ERP',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA', 'TOM_ANNEX'],
typicalDataCategories: [
'NAME', 'CONTACT', 'ADDRESS', 'BANK_ACCOUNT', 'CONTRACT_DATA',
'EMPLOYMENT_DATA', 'SALARY_DATA',
],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'data_volume', weight: 0.9, description: { de: 'Sehr hohes Datenvolumen', en: 'Very high data volume' } },
{ factor: 'criticality', weight: 0.95, description: { de: 'Geschäftskritisch', en: 'Business critical' } },
{ factor: 'sensitive_data', weight: 0.8, description: { de: 'Sensible Daten', en: 'Sensitive data' } },
],
commonProviders: ['SAP', 'Oracle', 'Microsoft Dynamics', 'Sage', 'Odoo'],
},
{
id: 'tpl-vendor-hr',
name: { de: 'HR-Software', en: 'HR Software' },
description: {
de: 'Personalverwaltung und HR-Management',
en: 'Personnel administration and HR management',
},
serviceCategory: 'HR_SOFTWARE',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA', 'TOM_ANNEX'],
typicalDataCategories: [
'NAME', 'CONTACT', 'ADDRESS', 'DOB', 'SOCIAL_SECURITY', 'TAX_ID',
'BANK_ACCOUNT', 'EMPLOYMENT_DATA', 'SALARY_DATA', 'HEALTH_DATA',
],
typicalCertifications: ['ISO 27001'],
inherentRiskFactors: [
{ factor: 'employee_data', weight: 0.9, description: { de: 'Mitarbeiterdaten', en: 'Employee data' } },
{ factor: 'sensitive_data', weight: 0.85, description: { de: 'Sensible Daten', en: 'Sensitive data' } },
{ factor: 'special_categories', weight: 0.7, description: { de: 'Besondere Kategorien möglich', en: 'Special categories possible' } },
],
commonProviders: ['Personio', 'Workday', 'SAP SuccessFactors', 'HRworks', 'Factorial'],
},
{
id: 'tpl-vendor-accounting',
name: { de: 'Buchhaltungssoftware', en: 'Accounting Software' },
description: {
de: 'Finanzbuchhaltung und Rechnungswesen',
en: 'Financial accounting and bookkeeping',
},
serviceCategory: 'ACCOUNTING',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['ADEQUACY_DECISION'],
suggestedContractTypes: ['AVV', 'MSA'],
typicalDataCategories: ['NAME', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA', 'TAX_ID'],
typicalCertifications: ['ISO 27001', 'IDW PS 951'],
inherentRiskFactors: [
{ factor: 'financial_data', weight: 0.85, description: { de: 'Finanzdaten', en: 'Financial data' } },
{ factor: 'legal_retention', weight: 0.7, description: { de: 'Aufbewahrungspflichten', en: 'Retention requirements' } },
],
commonProviders: ['DATEV', 'Lexware', 'SevDesk', 'Xero', 'Sage'],
},
// Communication & Collaboration
{
id: 'tpl-vendor-email',
name: { de: 'E-Mail-Dienst', en: 'Email Service' },
description: {
de: 'E-Mail-Hosting und -Kommunikation',
en: 'Email hosting and communication',
},
serviceCategory: 'EMAIL',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
typicalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'communication_data', weight: 0.8, description: { de: 'Kommunikationsdaten', en: 'Communication data' } },
{ factor: 'criticality', weight: 0.8, description: { de: 'Geschäftskritisch', en: 'Business critical' } },
],
commonProviders: ['Microsoft 365', 'Google Workspace', 'Zoho Mail', 'ProtonMail'],
},
{
id: 'tpl-vendor-communication',
name: { de: 'Kollaborations-Tool', en: 'Collaboration Tool' },
description: {
de: 'Team-Kommunikation und Zusammenarbeit',
en: 'Team communication and collaboration',
},
serviceCategory: 'COMMUNICATION',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
suggestedContractTypes: ['AVV', 'MSA'],
typicalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA', 'PHOTO_VIDEO'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'communication_data', weight: 0.7, description: { de: 'Kommunikationsdaten', en: 'Communication data' } },
{ factor: 'file_sharing', weight: 0.6, description: { de: 'Dateifreigabe', en: 'File sharing' } },
],
commonProviders: ['Slack', 'Microsoft Teams', 'Zoom', 'Google Meet', 'Webex'],
},
// Marketing & Analytics
{
id: 'tpl-vendor-analytics',
name: { de: 'Analytics-Tool', en: 'Analytics Tool' },
description: {
de: 'Web-Analyse und Nutzerverhalten',
en: 'Web analytics and user behavior',
},
serviceCategory: 'ANALYTICS',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA'],
typicalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOCATION_DATA'],
typicalCertifications: ['ISO 27001'],
inherentRiskFactors: [
{ factor: 'tracking', weight: 0.7, description: { de: 'Tracking', en: 'Tracking' } },
{ factor: 'profiling', weight: 0.6, description: { de: 'Profiling möglich', en: 'Profiling possible' } },
],
commonProviders: ['Google Analytics', 'Matomo', 'Plausible', 'Mixpanel', 'Amplitude'],
},
{
id: 'tpl-vendor-marketing-automation',
name: { de: 'Marketing-Automatisierung', en: 'Marketing Automation' },
description: {
de: 'E-Mail-Marketing und Automatisierung',
en: 'Email marketing and automation',
},
serviceCategory: 'MARKETING',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA'],
typicalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
typicalCertifications: ['ISO 27001'],
inherentRiskFactors: [
{ factor: 'marketing_data', weight: 0.6, description: { de: 'Marketing-Daten', en: 'Marketing data' } },
{ factor: 'consent_management', weight: 0.7, description: { de: 'Einwilligungsmanagement', en: 'Consent management' } },
],
commonProviders: ['Mailchimp', 'HubSpot', 'Sendinblue', 'CleverReach', 'ActiveCampaign'],
},
// Support & Service
{
id: 'tpl-vendor-support',
name: { de: 'Support-/Ticketsystem', en: 'Support/Ticket System' },
description: {
de: 'Kundenservice und Ticket-Management',
en: 'Customer service and ticket management',
},
serviceCategory: 'SUPPORT',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA'],
typicalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA', 'CONTRACT_DATA'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'customer_data', weight: 0.7, description: { de: 'Kundendaten', en: 'Customer data' } },
{ factor: 'communication', weight: 0.6, description: { de: 'Kommunikationsinhalte', en: 'Communication content' } },
],
commonProviders: ['Zendesk', 'Freshdesk', 'Intercom', 'HelpScout', 'Jira Service Management'],
},
// Payment & Finance
{
id: 'tpl-vendor-payment',
name: { de: 'Zahlungsdienstleister', en: 'Payment Service Provider' },
description: {
de: 'Zahlungsabwicklung und Payment Gateway',
en: 'Payment processing and payment gateway',
},
serviceCategory: 'PAYMENT',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
typicalDataCategories: ['NAME', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA'],
typicalCertifications: ['PCI DSS', 'ISO 27001'],
inherentRiskFactors: [
{ factor: 'financial_data', weight: 0.9, description: { de: 'Finanzdaten', en: 'Financial data' } },
{ factor: 'pci_scope', weight: 0.8, description: { de: 'PCI-Scope', en: 'PCI scope' } },
],
commonProviders: ['Stripe', 'PayPal', 'Adyen', 'Mollie', 'Klarna'],
},
// Security
{
id: 'tpl-vendor-security',
name: { de: 'Sicherheitsdienstleister', en: 'Security Service Provider' },
description: {
de: 'IT-Sicherheit, Penetrationstests, SIEM',
en: 'IT security, penetration testing, SIEM',
},
serviceCategory: 'SECURITY',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'ADMINISTRATIVE',
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA', 'NDA'],
typicalDataCategories: ['IP_ADDRESS', 'USAGE_DATA', 'LOGIN_DATA'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'system_access', weight: 0.8, description: { de: 'Systemzugriff', en: 'System access' } },
{ factor: 'security_data', weight: 0.7, description: { de: 'Sicherheitsdaten', en: 'Security data' } },
],
commonProviders: ['CrowdStrike', 'Splunk', 'Palo Alto Networks', 'Tenable'],
},
// Backup & Storage
{
id: 'tpl-vendor-backup',
name: { de: 'Backup-Anbieter', en: 'Backup Provider' },
description: {
de: 'Datensicherung und Disaster Recovery',
en: 'Data backup and disaster recovery',
},
serviceCategory: 'BACKUP',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'CONTENT',
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
typicalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA'],
typicalCertifications: ['ISO 27001', 'SOC 2'],
inherentRiskFactors: [
{ factor: 'full_backup', weight: 0.9, description: { de: 'Vollständige Kopie', en: 'Full copy' } },
{ factor: 'retention', weight: 0.7, description: { de: 'Lange Aufbewahrung', en: 'Long retention' } },
],
commonProviders: ['Veeam', 'Acronis', 'Commvault', 'AWS Backup'],
},
// Consulting
{
id: 'tpl-vendor-consulting',
name: { de: 'Beratungsunternehmen', en: 'Consulting Company' },
description: {
de: 'IT-Beratung, Projektunterstützung',
en: 'IT consulting, project support',
},
serviceCategory: 'CONSULTING',
suggestedRole: 'PROCESSOR',
suggestedDataAccess: 'POTENTIAL',
suggestedTransferMechanisms: ['ADEQUACY_DECISION'],
suggestedContractTypes: ['AVV', 'MSA', 'NDA'],
typicalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA'],
typicalCertifications: ['ISO 27001'],
inherentRiskFactors: [
{ factor: 'project_access', weight: 0.5, description: { de: 'Projektzugriff', en: 'Project access' } },
{ factor: 'temporary', weight: 0.4, description: { de: 'Temporär', en: 'Temporary' } },
],
commonProviders: ['Accenture', 'McKinsey', 'Deloitte', 'PwC', 'KPMG'],
},
]
// ==========================================
// COUNTRY RISK PROFILES
// ==========================================
export const COUNTRY_RISK_PROFILES: CountryRiskProfile[] = [
// EU Countries (Low Risk)
{ code: 'DE', name: { de: 'Deutschland', en: 'Germany' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'AT', name: { de: 'Österreich', en: 'Austria' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'FR', name: { de: 'Frankreich', en: 'France' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'NL', name: { de: 'Niederlande', en: 'Netherlands' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'BE', name: { de: 'Belgien', en: 'Belgium' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'IT', name: { de: 'Italien', en: 'Italy' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'ES', name: { de: 'Spanien', en: 'Spain' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'PT', name: { de: 'Portugal', en: 'Portugal' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'PL', name: { de: 'Polen', en: 'Poland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'CZ', name: { de: 'Tschechien', en: 'Czech Republic' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'SE', name: { de: 'Schweden', en: 'Sweden' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'DK', name: { de: 'Dänemark', en: 'Denmark' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'FI', name: { de: 'Finnland', en: 'Finland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'IE', name: { de: 'Irland', en: 'Ireland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'LU', name: { de: 'Luxemburg', en: 'Luxembourg' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
// EEA Countries
{ code: 'NO', name: { de: 'Norwegen', en: 'Norway' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'IS', name: { de: 'Island', en: 'Iceland' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'LI', name: { de: 'Liechtenstein', en: 'Liechtenstein' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
// Adequacy Decision Countries
{ code: 'CH', name: { de: 'Schweiz', en: 'Switzerland' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'GB', name: { de: 'Vereinigtes Königreich', en: 'United Kingdom' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2021-06-28', riskLevel: 'LOW' },
{ code: 'JP', name: { de: 'Japan', en: 'Japan' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2019-01-23', riskLevel: 'LOW' },
{ code: 'KR', name: { de: 'Südkorea', en: 'South Korea' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2022-12-17', riskLevel: 'LOW' },
{ code: 'IL', name: { de: 'Israel', en: 'Israel' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'NZ', name: { de: 'Neuseeland', en: 'New Zealand' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'CA', name: { de: 'Kanada', en: 'Canada' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW', notes: { de: 'Nur PIPEDA-Bereich', en: 'PIPEDA scope only' } },
{ code: 'AR', name: { de: 'Argentinien', en: 'Argentina' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
{ code: 'UY', name: { de: 'Uruguay', en: 'Uruguay' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
// US (Special - DPF)
{ code: 'US', name: { de: 'USA', en: 'United States' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2023-07-10', riskLevel: 'MEDIUM', notes: { de: 'EU-US Data Privacy Framework erforderlich', en: 'EU-US Data Privacy Framework required' } },
// Third Countries without Adequacy (High Risk)
{ code: 'CN', name: { de: 'China', en: 'China' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Staatlicher Datenzugriff möglich', en: 'Government data access possible' } },
{ code: 'RU', name: { de: 'Russland', en: 'Russia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Sanktionen beachten', en: 'Consider sanctions' } },
{ code: 'IN', name: { de: 'Indien', en: 'India' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' },
{ code: 'BR', name: { de: 'Brasilien', en: 'Brazil' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'LGPD vorhanden', en: 'LGPD in place' } },
{ code: 'AU', name: { de: 'Australien', en: 'Australia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM' },
{ code: 'SG', name: { de: 'Singapur', en: 'Singapore' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'PDPA vorhanden', en: 'PDPA in place' } },
{ code: 'HK', name: { de: 'Hongkong', en: 'Hong Kong' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' },
{ code: 'AE', name: { de: 'VAE', en: 'UAE' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' },
{ code: 'ZA', name: { de: 'Südafrika', en: 'South Africa' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'POPIA vorhanden', en: 'POPIA in place' } },
]
// ==========================================
// HELPER FUNCTIONS
// ==========================================
/**
* Get vendor template by ID
*/
export function getVendorTemplateById(id: string): VendorTemplate | undefined {
return VENDOR_TEMPLATES.find((t) => t.id === id)
}
/**
* Get vendor templates by category
*/
export function getVendorTemplatesByCategory(category: ServiceCategory): VendorTemplate[] {
return VENDOR_TEMPLATES.filter((t) => t.serviceCategory === category)
}
/**
* Get country risk profile
*/
export function getCountryRiskProfile(countryCode: string): CountryRiskProfile | undefined {
return COUNTRY_RISK_PROFILES.find((c) => c.code === countryCode.toUpperCase())
}
/**
* Check if country requires transfer mechanism
*/
export function requiresTransferMechanism(countryCode: string): boolean {
const profile = getCountryRiskProfile(countryCode)
if (!profile) return true // Unknown country = requires mechanism
return !profile.isEU && !profile.isEEA && !profile.hasAdequacyDecision
}
/**
* Get suggested transfer mechanisms for country
*/
export function getSuggestedTransferMechanisms(countryCode: string): TransferMechanismType[] {
const profile = getCountryRiskProfile(countryCode)
if (!profile) {
return ['SCC_PROCESSOR']
}
if (profile.isEU || profile.isEEA) {
return [] // No mechanism needed
}
if (profile.hasAdequacyDecision) {
return ['ADEQUACY_DECISION']
}
// Third country without adequacy
return ['SCC_PROCESSOR', 'BCR']
}
/**
* Calculate inherent risk score for vendor template
*/
export function calculateTemplateRiskScore(template: VendorTemplate): number {
const baseScore = template.inherentRiskFactors.reduce(
(sum, factor) => sum + factor.weight * 100,
0
)
return Math.min(100, baseScore / template.inherentRiskFactors.length)
}
/**
* Create form data from vendor template
*/
export function createVendorFormDataFromTemplate(
template: VendorTemplate
): Partial<VendorFormData> {
return {
serviceCategory: template.serviceCategory,
role: template.suggestedRole,
dataAccessLevel: template.suggestedDataAccess,
transferMechanisms: template.suggestedTransferMechanisms,
contractTypes: template.suggestedContractTypes,
certifications: template.typicalCertifications.map((type) => ({
type,
issuedDate: undefined,
expirationDate: undefined,
})),
reviewFrequency: 'ANNUAL',
}
}
/**
* Get all EU/EEA countries
*/
export function getEUEEACountries(): CountryRiskProfile[] {
return COUNTRY_RISK_PROFILES.filter((c) => c.isEU || c.isEEA)
}
/**
* Get all countries with adequacy decision
*/
export function getAdequateCountries(): CountryRiskProfile[] {
return COUNTRY_RISK_PROFILES.filter((c) => c.hasAdequacyDecision)
}
/**
* Get all high-risk countries
*/
export function getHighRiskCountries(): CountryRiskProfile[] {
return COUNTRY_RISK_PROFILES.filter((c) => c.riskLevel === 'HIGH' || c.riskLevel === 'VERY_HIGH')
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,459 @@
/**
* Contract Analyzer
*
* LLM-based contract review for GDPR compliance
*/
import {
Finding,
Citation,
FindingType,
FindingCategory,
FindingSeverity,
DocumentType,
LocalizedText,
} from '../types'
import { AVV_CHECKLIST, INCIDENT_CHECKLIST, TRANSFER_CHECKLIST } from './checklists'
// ==========================================
// TYPES
// ==========================================
export interface ContractAnalysisRequest {
contractId: string
vendorId: string
tenantId: string
documentText: string
documentType?: DocumentType
language?: 'de' | 'en'
analysisScope?: AnalysisScope[]
}
export interface ContractAnalysisResponse {
documentType: DocumentType
language: 'de' | 'en'
parties: ContractPartyInfo[]
findings: Finding[]
complianceScore: number
topRisks: LocalizedText[]
requiredActions: LocalizedText[]
metadata: ExtractedMetadata
}
export interface ContractPartyInfo {
role: 'CONTROLLER' | 'PROCESSOR' | 'PARTY'
name: string
address?: string
}
export interface ExtractedMetadata {
effectiveDate?: string
expirationDate?: string
autoRenewal?: boolean
terminationNoticePeriod?: number
governingLaw?: string
jurisdiction?: string
}
export type AnalysisScope =
| 'AVV_COMPLIANCE'
| 'SUBPROCESSOR'
| 'INCIDENT_RESPONSE'
| 'AUDIT_RIGHTS'
| 'DELETION'
| 'TOM'
| 'TRANSFER'
| 'LIABILITY'
| 'SLA'
// ==========================================
// SYSTEM PROMPTS
// ==========================================
export const CONTRACT_REVIEW_SYSTEM_PROMPT = `Du bist ein Datenschutz-Rechtsexperte, der Verträge auf DSGVO-Konformität prüft.
WICHTIG:
1. Jede Feststellung MUSS mit einer Textstelle belegt werden (Citation)
2. Gib niemals Rechtsberatung - nur Compliance-Hinweise
3. Markiere unklare Stellen als UNKNOWN, nicht als GAP
4. Sei konservativ: im Zweifel RISK statt OK
PRÜFUNGSSCHEMA Art. 28 DSGVO AVV:
${AVV_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')}
INCIDENT RESPONSE:
${INCIDENT_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')}
DRITTLANDTRANSFER:
${TRANSFER_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')}
AUSGABEFORMAT (JSON):
{
"document_type": "AVV|MSA|SLA|SCC|NDA|TOM_ANNEX|OTHER|UNKNOWN",
"language": "de|en",
"parties": [
{
"role": "CONTROLLER|PROCESSOR|PARTY",
"name": "...",
"address": "..."
}
],
"findings": [
{
"category": "AVV_CONTENT|SUBPROCESSOR|INCIDENT|AUDIT_RIGHTS|DELETION|TOM|TRANSFER|LIABILITY|SLA|DATA_SUBJECT_RIGHTS|CONFIDENTIALITY|INSTRUCTION|GENERAL",
"type": "OK|GAP|RISK|UNKNOWN",
"severity": "LOW|MEDIUM|HIGH|CRITICAL",
"title_de": "...",
"title_en": "...",
"description_de": "...",
"description_en": "...",
"recommendation_de": "...",
"recommendation_en": "...",
"citations": [
{
"page": 3,
"quoted_text": "Der Auftragnehmer...",
"start_char": 1234,
"end_char": 1456
}
],
"affected_requirement": "Art. 28 Abs. 3 lit. a DSGVO"
}
],
"compliance_score": 72,
"top_risks": [
{"de": "...", "en": "..."}
],
"required_actions": [
{"de": "...", "en": "..."}
],
"metadata": {
"effective_date": "2024-01-01",
"expiration_date": "2025-12-31",
"auto_renewal": true,
"termination_notice_period": 90,
"governing_law": "Germany",
"jurisdiction": "Frankfurt am Main"
}
}`
export const CONTRACT_CLASSIFICATION_PROMPT = `Analysiere den folgenden Vertragstext und klassifiziere ihn:
1. Dokumenttyp (AVV, MSA, SLA, SCC, NDA, TOM_ANNEX, OTHER)
2. Sprache (de, en)
3. Vertragsparteien mit Rollen
Antworte im JSON-Format:
{
"document_type": "...",
"language": "...",
"parties": [...]
}`
export const METADATA_EXTRACTION_PROMPT = `Extrahiere die folgenden Metadaten aus dem Vertrag:
1. Inkrafttreten / Effective Date
2. Laufzeit / Ablaufdatum
3. Automatische Verlängerung
4. Kündigungsfrist
5. Anwendbares Recht
6. Gerichtsstand
Antworte im JSON-Format.`
// ==========================================
// ANALYSIS FUNCTIONS
// ==========================================
/**
* Analyze a contract for GDPR compliance
*/
export async function analyzeContract(
request: ContractAnalysisRequest
): Promise<ContractAnalysisResponse> {
// This function would typically call an LLM API
// For now, we provide the structure that would be used
const apiEndpoint = '/api/sdk/v1/vendor-compliance/contracts/analyze'
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contract_id: request.contractId,
vendor_id: request.vendorId,
tenant_id: request.tenantId,
document_text: request.documentText,
document_type: request.documentType,
language: request.language || 'de',
analysis_scope: request.analysisScope || [
'AVV_COMPLIANCE',
'SUBPROCESSOR',
'INCIDENT_RESPONSE',
'AUDIT_RIGHTS',
'DELETION',
'TOM',
'TRANSFER',
],
}),
})
if (!response.ok) {
throw new Error('Contract analysis failed')
}
const result = await response.json()
return transformAnalysisResponse(result, request)
}
/**
* Transform LLM response to typed response
*/
function transformAnalysisResponse(
llmResponse: Record<string, unknown>,
request: ContractAnalysisRequest
): ContractAnalysisResponse {
const findings: Finding[] = (llmResponse.findings as Array<Record<string, unknown>> || []).map((f, idx) => ({
id: `finding-${request.contractId}-${idx}`,
tenantId: request.tenantId,
contractId: request.contractId,
vendorId: request.vendorId,
type: (f.type as FindingType) || 'UNKNOWN',
category: (f.category as FindingCategory) || 'GENERAL',
severity: (f.severity as FindingSeverity) || 'MEDIUM',
title: {
de: (f.title_de as string) || '',
en: (f.title_en as string) || '',
},
description: {
de: (f.description_de as string) || '',
en: (f.description_en as string) || '',
},
recommendation: f.recommendation_de ? {
de: f.recommendation_de as string,
en: (f.recommendation_en as string) || '',
} : undefined,
citations: ((f.citations as Array<Record<string, unknown>>) || []).map((c) => ({
documentId: request.contractId,
page: (c.page as number) || 1,
startChar: (c.start_char as number) || 0,
endChar: (c.end_char as number) || 0,
quotedText: (c.quoted_text as string) || '',
quoteHash: generateQuoteHash((c.quoted_text as string) || ''),
})),
affectedRequirement: f.affected_requirement as string | undefined,
triggeredControls: [],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
}))
const metadata = llmResponse.metadata as Record<string, unknown> || {}
return {
documentType: (llmResponse.document_type as DocumentType) || 'OTHER',
language: (llmResponse.language as 'de' | 'en') || 'de',
parties: ((llmResponse.parties as Array<Record<string, unknown>>) || []).map((p) => ({
role: (p.role as 'CONTROLLER' | 'PROCESSOR' | 'PARTY') || 'PARTY',
name: (p.name as string) || '',
address: p.address as string | undefined,
})),
findings,
complianceScore: (llmResponse.compliance_score as number) || 0,
topRisks: ((llmResponse.top_risks as Array<Record<string, string>>) || []).map((r) => ({
de: r.de || '',
en: r.en || '',
})),
requiredActions: ((llmResponse.required_actions as Array<Record<string, string>>) || []).map((a) => ({
de: a.de || '',
en: a.en || '',
})),
metadata: {
effectiveDate: metadata.effective_date as string | undefined,
expirationDate: metadata.expiration_date as string | undefined,
autoRenewal: metadata.auto_renewal as boolean | undefined,
terminationNoticePeriod: metadata.termination_notice_period as number | undefined,
governingLaw: metadata.governing_law as string | undefined,
jurisdiction: metadata.jurisdiction as string | undefined,
},
}
}
/**
* Generate a hash for quote verification
*/
function generateQuoteHash(text: string): string {
// Simple hash for demo - in production use crypto.subtle.digest
let hash = 0
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return Math.abs(hash).toString(16).padStart(16, '0')
}
// ==========================================
// CITATION UTILITIES
// ==========================================
/**
* Verify citation integrity
*/
export function verifyCitation(
citation: Citation,
documentText: string
): boolean {
const extractedText = documentText.substring(citation.startChar, citation.endChar)
const expectedHash = generateQuoteHash(extractedText)
return citation.quoteHash === expectedHash
}
/**
* Find citation context in document
*/
export function getCitationContext(
citation: Citation,
documentText: string,
contextChars: number = 100
): {
before: string
quoted: string
after: string
} {
const start = Math.max(0, citation.startChar - contextChars)
const end = Math.min(documentText.length, citation.endChar + contextChars)
return {
before: documentText.substring(start, citation.startChar),
quoted: documentText.substring(citation.startChar, citation.endChar),
after: documentText.substring(citation.endChar, end),
}
}
/**
* Highlight citations in text
*/
export function highlightCitations(
documentText: string,
citations: Citation[]
): string {
// Sort citations by start position (reverse to avoid offset issues)
const sortedCitations = [...citations].sort((a, b) => b.startChar - a.startChar)
let result = documentText
for (const citation of sortedCitations) {
const before = result.substring(0, citation.startChar)
const quoted = result.substring(citation.startChar, citation.endChar)
const after = result.substring(citation.endChar)
result = `${before}<mark data-citation-id="${citation.documentId}">${quoted}</mark>${after}`
}
return result
}
// ==========================================
// COMPLIANCE SCORE CALCULATION
// ==========================================
export interface ComplianceScoreBreakdown {
totalScore: number
categoryScores: Record<FindingCategory, number>
severityCounts: Record<FindingSeverity, number>
findingCounts: {
total: number
gaps: number
risks: number
ok: number
unknown: number
}
}
/**
* Calculate detailed compliance score
*/
export function calculateComplianceScore(findings: Finding[]): ComplianceScoreBreakdown {
const severityWeights: Record<FindingSeverity, number> = {
CRITICAL: 25,
HIGH: 15,
MEDIUM: 8,
LOW: 3,
}
const categoryWeights: Partial<Record<FindingCategory, number>> = {
AVV_CONTENT: 1.5,
SUBPROCESSOR: 1.3,
INCIDENT: 1.3,
DELETION: 1.2,
AUDIT_RIGHTS: 1.1,
TOM: 1.2,
TRANSFER: 1.4,
}
let totalDeductions = 0
const maxPossibleDeductions = 100
const categoryScores: Partial<Record<FindingCategory, number>> = {}
const severityCounts: Record<FindingSeverity, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0,
CRITICAL: 0,
}
let gaps = 0
let risks = 0
let ok = 0
let unknown = 0
for (const finding of findings) {
severityCounts[finding.severity]++
switch (finding.type) {
case 'GAP':
gaps++
totalDeductions += severityWeights[finding.severity] * (categoryWeights[finding.category] || 1)
break
case 'RISK':
risks++
totalDeductions += severityWeights[finding.severity] * 0.7 * (categoryWeights[finding.category] || 1)
break
case 'OK':
ok++
break
case 'UNKNOWN':
unknown++
totalDeductions += severityWeights[finding.severity] * 0.3 * (categoryWeights[finding.category] || 1)
break
}
}
// Calculate category scores
const categories = new Set(findings.map((f) => f.category))
for (const category of categories) {
const categoryFindings = findings.filter((f) => f.category === category)
const categoryOk = categoryFindings.filter((f) => f.type === 'OK').length
const categoryTotal = categoryFindings.length
categoryScores[category] = categoryTotal > 0 ? Math.round((categoryOk / categoryTotal) * 100) : 100
}
const totalScore = Math.max(0, Math.round(100 - (totalDeductions / maxPossibleDeductions) * 100))
return {
totalScore,
categoryScores: categoryScores as Record<FindingCategory, number>,
severityCounts,
findingCounts: {
total: findings.length,
gaps,
risks,
ok,
unknown,
},
}
}

Some files were not shown because too many files have changed in this diff Show More