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:
@@ -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`
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './constants'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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})`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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 }
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
}))
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* DSR Module Exports
|
||||
*/
|
||||
|
||||
export * from './types'
|
||||
export * from './api'
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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',
|
||||
]
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user