feat: DSFA Section 8 KI-Anwendungsfälle + Bundesland RAG-Ingest
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- Migration 028: ai_use_case_modules JSONB + section_8_complete auf compliance_dsfas - Neues ai-use-case-types.ts: AIUseCaseModule Interface, 8 Typen, Art22Assessment, AI Act Risikoklassen, WP248-Kriterien, Privacy by Design, createEmptyModule() Helper - types.ts: Section 8 in DSFA_SECTIONS, ai_use_case_modules im DSFA Interface, section_8_complete in DSFASectionProgress - api.ts: addAIUseCaseModule, updateAIUseCaseModule, removeAIUseCaseModule - 5 neue UI-Komponenten: AIUseCaseTypeSelector, Art22AssessmentPanel, AIRiskCriteriaChecklist, AIUseCaseModuleEditor (7 Tabs), AIUseCaseSection - DSFASidebar: Section 8 Eintrag + calculateSectionProgress case 8 - ReviewScheduleSection: ai_use_case_module Trigger-Typ ergänzt - page.tsx: Section 8 Rendering + Weiter-Button auf activeSection < 8 + KI-Module Counter - scripts/ingest-dsfa-bundesland.sh: WP248 + alle 17 Behörden → bp_dsfa_corpus - Docs: dsfa.md Section 8 + RAG-Corpus, Developer Portal DSFA mit AI-Modul-Code-Beispielen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,7 @@ import {
|
|||||||
StakeholderConsultationSection,
|
StakeholderConsultationSection,
|
||||||
Art36Warning,
|
Art36Warning,
|
||||||
ReviewScheduleSection,
|
ReviewScheduleSection,
|
||||||
|
AIUseCaseSection,
|
||||||
} from '@/components/sdk/dsfa'
|
} from '@/components/sdk/dsfa'
|
||||||
import { SourceAttribution } from '@/components/sdk/dsfa/SourceAttribution'
|
import { SourceAttribution } from '@/components/sdk/dsfa/SourceAttribution'
|
||||||
import type { DSFALicenseCode, SourceAttributionProps } from '@/lib/sdk/types'
|
import type { DSFALicenseCode, SourceAttributionProps } from '@/lib/sdk/types'
|
||||||
@@ -1834,6 +1835,15 @@ export default function DSFAEditorPage() {
|
|||||||
isSubmitting={isSaving}
|
isSubmitting={isSaving}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Section 8: KI-Anwendungsfälle (NEW) */}
|
||||||
|
{activeSection === 8 && (
|
||||||
|
<AIUseCaseSection
|
||||||
|
dsfa={dsfa}
|
||||||
|
onUpdate={handleGenericUpdate}
|
||||||
|
isSubmitting={isSaving}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1852,7 +1862,7 @@ export default function DSFAEditorPage() {
|
|||||||
Zurueck
|
Zurueck
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{activeSection < 7 && (
|
{activeSection < 8 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveSection(activeSection + 1)}
|
onClick={() => setActiveSection(activeSection + 1)}
|
||||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||||
@@ -1869,6 +1879,7 @@ export default function DSFAEditorPage() {
|
|||||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||||
<span>Risiken: {(dsfa.risks || []).length}</span>
|
<span>Risiken: {(dsfa.risks || []).length}</span>
|
||||||
<span>Massnahmen: {(dsfa.mitigations || []).length}</span>
|
<span>Massnahmen: {(dsfa.mitigations || []).length}</span>
|
||||||
|
<span>KI-Module: {(dsfa.ai_use_case_modules || []).length}</span>
|
||||||
<span>Version: {dsfa.version || 1}</span>
|
<span>Version: {dsfa.version || 1}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
129
admin-compliance/components/sdk/dsfa/AIRiskCriteriaChecklist.tsx
Normal file
129
admin-compliance/components/sdk/dsfa/AIRiskCriteriaChecklist.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { AI_RISK_CRITERIA, AIUseCaseRiskCriterion } from '@/lib/sdk/dsfa/ai-use-case-types'
|
||||||
|
|
||||||
|
interface AIRiskCriteriaChecklistProps {
|
||||||
|
criteria: AIUseCaseRiskCriterion[]
|
||||||
|
onChange: (updated: AIUseCaseRiskCriterion[]) => void
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEVERITY_LABELS: Record<string, string> = {
|
||||||
|
low: 'Niedrig',
|
||||||
|
medium: 'Mittel',
|
||||||
|
high: 'Hoch',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEVERITY_COLORS: Record<string, string> = {
|
||||||
|
low: 'bg-green-100 text-green-700 border-green-200',
|
||||||
|
medium: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||||
|
high: 'bg-red-100 text-red-700 border-red-200',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIRiskCriteriaChecklist({ criteria, onChange, readonly }: AIRiskCriteriaChecklistProps) {
|
||||||
|
const appliedCount = criteria.filter(c => c.applies).length
|
||||||
|
|
||||||
|
const updateCriterion = (id: string, updates: Partial<AIUseCaseRiskCriterion>) => {
|
||||||
|
onChange(criteria.map(c => c.id === id ? { ...c, ...updates } : c))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Summary Banner */}
|
||||||
|
{appliedCount > 0 && (
|
||||||
|
<div className={`p-3 rounded-lg border text-sm ${
|
||||||
|
appliedCount >= 3
|
||||||
|
? 'bg-red-50 border-red-200 text-red-700'
|
||||||
|
: appliedCount >= 2
|
||||||
|
? 'bg-orange-50 border-orange-200 text-orange-700'
|
||||||
|
: 'bg-yellow-50 border-yellow-200 text-yellow-700'
|
||||||
|
}`}>
|
||||||
|
{appliedCount} von 6 Risikokriterien erfüllt
|
||||||
|
{appliedCount >= 2 && ' – DSFA ist erforderlich'}
|
||||||
|
{appliedCount >= 4 && ' – behördliche Konsultation prüfen'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Criteria Cards */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{AI_RISK_CRITERIA.map(critDef => {
|
||||||
|
const criterion = criteria.find(c => c.id === critDef.id) || {
|
||||||
|
id: critDef.id,
|
||||||
|
applies: false,
|
||||||
|
severity: critDef.default_severity,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={critDef.id}
|
||||||
|
className={`rounded-xl border p-4 transition-all ${
|
||||||
|
criterion.applies
|
||||||
|
? 'border-red-300 bg-red-50'
|
||||||
|
: 'border-gray-200 bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
{/* Checkbox */}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={criterion.applies}
|
||||||
|
onChange={e => updateCriterion(critDef.id, { applies: e.target.checked })}
|
||||||
|
disabled={readonly}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className={`font-medium text-sm ${criterion.applies ? 'text-red-800' : 'text-gray-900'}`}>
|
||||||
|
{critDef.label}
|
||||||
|
</span>
|
||||||
|
<span className={`text-[10px] px-1.5 py-0.5 rounded border ${SEVERITY_COLORS[criterion.severity]}`}>
|
||||||
|
{SEVERITY_LABELS[criterion.severity]}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-500 rounded font-mono">
|
||||||
|
{critDef.gdpr_ref.split(',')[0]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">{critDef.description}</p>
|
||||||
|
|
||||||
|
{/* Justification (shown when applies) */}
|
||||||
|
{criterion.applies && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<textarea
|
||||||
|
value={criterion.justification || ''}
|
||||||
|
onChange={e => updateCriterion(critDef.id, { justification: e.target.value })}
|
||||||
|
disabled={readonly}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Begründung, warum dieses Kriterium zutrifft..."
|
||||||
|
className="w-full px-3 py-2 text-xs border border-red-200 rounded-lg bg-white focus:ring-2 focus:ring-red-400 focus:border-red-400 resize-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Severity Override */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-gray-500">Schwere:</span>
|
||||||
|
{(['low', 'medium', 'high'] as const).map(sev => (
|
||||||
|
<button
|
||||||
|
key={sev}
|
||||||
|
onClick={() => !readonly && updateCriterion(critDef.id, { severity: sev })}
|
||||||
|
className={`text-xs px-2 py-0.5 rounded border transition-colors ${
|
||||||
|
criterion.severity === sev
|
||||||
|
? SEVERITY_COLORS[sev]
|
||||||
|
: 'bg-white text-gray-500 border-gray-200 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{SEVERITY_LABELS[sev]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
698
admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx
Normal file
698
admin-compliance/components/sdk/dsfa/AIUseCaseModuleEditor.tsx
Normal file
@@ -0,0 +1,698 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import {
|
||||||
|
AIUseCaseModule,
|
||||||
|
AIUseCaseType,
|
||||||
|
AIActRiskClass,
|
||||||
|
AI_USE_CASE_TYPES,
|
||||||
|
AI_ACT_RISK_CLASSES,
|
||||||
|
PRIVACY_BY_DESIGN_CATEGORIES,
|
||||||
|
PrivacyByDesignCategory,
|
||||||
|
PrivacyByDesignMeasure,
|
||||||
|
AIModuleReviewTrigger,
|
||||||
|
AIModuleReviewTriggerType,
|
||||||
|
checkArt22Applicability,
|
||||||
|
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
||||||
|
import { Art22AssessmentPanel } from './Art22AssessmentPanel'
|
||||||
|
import { AIRiskCriteriaChecklist } from './AIRiskCriteriaChecklist'
|
||||||
|
|
||||||
|
interface AIUseCaseModuleEditorProps {
|
||||||
|
module: AIUseCaseModule
|
||||||
|
onSave: (module: AIUseCaseModule) => void
|
||||||
|
onCancel: () => void
|
||||||
|
isSaving?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ id: 1, label: 'System', icon: '🖥️' },
|
||||||
|
{ id: 2, label: 'Daten', icon: '📊' },
|
||||||
|
{ id: 3, label: 'Zweck & Art. 22', icon: '⚖️' },
|
||||||
|
{ id: 4, label: 'KI-Kriterien', icon: '🔍' },
|
||||||
|
{ id: 5, label: 'Risiken', icon: '⚠️' },
|
||||||
|
{ id: 6, label: 'Maßnahmen', icon: '🛡️' },
|
||||||
|
{ id: 7, label: 'Review', icon: '🔄' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const REVIEW_TRIGGER_TYPES: { value: AIModuleReviewTriggerType; label: string; icon: string }[] = [
|
||||||
|
{ value: 'model_update', label: 'Modell-Update', icon: '🔄' },
|
||||||
|
{ value: 'data_drift', label: 'Datendrift', icon: '📉' },
|
||||||
|
{ value: 'accuracy_drop', label: 'Genauigkeitsabfall', icon: '📊' },
|
||||||
|
{ value: 'new_use_case', label: 'Neuer Anwendungsfall', icon: '🎯' },
|
||||||
|
{ value: 'regulatory_change', label: 'Regulatorische Änderung', icon: '📜' },
|
||||||
|
{ value: 'incident', label: 'Sicherheitsvorfall', icon: '🚨' },
|
||||||
|
{ value: 'periodic', label: 'Regelmäßig (zeitbasiert)', icon: '📅' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const LEGAL_BASES = [
|
||||||
|
'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)',
|
||||||
|
'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)',
|
||||||
|
'Art. 6 Abs. 1 lit. c DSGVO (Rechtliche Verpflichtung)',
|
||||||
|
'Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse)',
|
||||||
|
'Art. 9 Abs. 2 lit. a DSGVO (Ausdrückliche Einwilligung)',
|
||||||
|
]
|
||||||
|
|
||||||
|
export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel, isSaving }: AIUseCaseModuleEditorProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState(1)
|
||||||
|
const [module, setModule] = useState<AIUseCaseModule>(initialModule)
|
||||||
|
const [newCategory, setNewCategory] = useState('')
|
||||||
|
const [newOutputCategory, setNewOutputCategory] = useState('')
|
||||||
|
const [newSubject, setNewSubject] = useState('')
|
||||||
|
|
||||||
|
const typeInfo = AI_USE_CASE_TYPES[module.use_case_type]
|
||||||
|
const art22Required = checkArt22Applicability(module)
|
||||||
|
|
||||||
|
const update = (updates: Partial<AIUseCaseModule>) => {
|
||||||
|
setModule(prev => ({ ...prev, ...updates }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToList = (field: keyof AIUseCaseModule, value: string, setter: (v: string) => void) => {
|
||||||
|
if (!value.trim()) return
|
||||||
|
const current = (module[field] as string[]) || []
|
||||||
|
update({ [field]: [...current, value.trim()] } as Partial<AIUseCaseModule>)
|
||||||
|
setter('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFromList = (field: keyof AIUseCaseModule, idx: number) => {
|
||||||
|
const current = (module[field] as string[]) || []
|
||||||
|
update({ [field]: current.filter((_, i) => i !== idx) } as Partial<AIUseCaseModule>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePbdMeasure = (category: PrivacyByDesignCategory) => {
|
||||||
|
const existing = module.privacy_by_design_measures || []
|
||||||
|
const found = existing.find(m => m.category === category)
|
||||||
|
if (found) {
|
||||||
|
update({ privacy_by_design_measures: existing.map(m =>
|
||||||
|
m.category === category ? { ...m, implemented: !m.implemented } : m
|
||||||
|
)})
|
||||||
|
} else {
|
||||||
|
const newMeasure: PrivacyByDesignMeasure = {
|
||||||
|
category,
|
||||||
|
description: PRIVACY_BY_DESIGN_CATEGORIES[category].description,
|
||||||
|
implemented: true,
|
||||||
|
}
|
||||||
|
update({ privacy_by_design_measures: [...existing, newMeasure] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleReviewTrigger = (type: AIModuleReviewTriggerType) => {
|
||||||
|
const existing = module.review_triggers || []
|
||||||
|
const found = existing.find(t => t.type === type)
|
||||||
|
if (found) {
|
||||||
|
update({ review_triggers: existing.filter(t => t.type !== type) })
|
||||||
|
} else {
|
||||||
|
const label = REVIEW_TRIGGER_TYPES.find(rt => rt.value === type)?.label || type
|
||||||
|
const newTrigger: AIModuleReviewTrigger = { type, description: label }
|
||||||
|
update({ review_triggers: [...existing, newTrigger] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">{typeInfo.icon}</span>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">{module.name || typeInfo.label}</h2>
|
||||||
|
<p className="text-xs text-gray-500">{typeInfo.label} — KI-Anwendungsfall-Anhang</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Bar */}
|
||||||
|
<div className="flex gap-1 px-4 py-2 border-b border-gray-200 overflow-x-auto flex-shrink-0">
|
||||||
|
{TABS.map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-purple-100 text-purple-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{tab.icon}</span>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{/* Tab 1: Systembeschreibung */}
|
||||||
|
{activeTab === 1 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name des KI-Anwendungsfalls *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={module.name}
|
||||||
|
onChange={e => update({ name: e.target.value })}
|
||||||
|
placeholder={`z.B. ${typeInfo.label} für Kundenservice`}
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Systembeschreibung *</label>
|
||||||
|
<textarea
|
||||||
|
value={module.model_description}
|
||||||
|
onChange={e => update({ model_description: e.target.value })}
|
||||||
|
rows={4}
|
||||||
|
placeholder="Beschreiben Sie das KI-System: Funktionsweise, Input/Output, eingesetzte Algorithmen..."
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Modell-Typ</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={module.model_type || ''}
|
||||||
|
onChange={e => update({ model_type: e.target.value })}
|
||||||
|
placeholder="z.B. Random Forest, GPT-4, CNN"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Anbieter / Provider</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={module.provider || ''}
|
||||||
|
onChange={e => update({ provider: e.target.value })}
|
||||||
|
placeholder="z.B. Anthropic, OpenAI, intern"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Datenfluss-Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
value={module.data_flow_description || ''}
|
||||||
|
onChange={e => update({ data_flow_description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Wie fließen Daten in das KI-System ein und aus? Gibt es Drittland-Transfers?"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="third_country"
|
||||||
|
checked={module.third_country_transfer}
|
||||||
|
onChange={e => update({ third_country_transfer: e.target.checked })}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||||
|
/>
|
||||||
|
<label htmlFor="third_country" className="text-sm text-gray-700">
|
||||||
|
Drittland-Transfer (außerhalb EU/EWR)
|
||||||
|
</label>
|
||||||
|
{module.third_country_transfer && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={module.provider_country || ''}
|
||||||
|
onChange={e => update({ provider_country: e.target.value })}
|
||||||
|
placeholder="Land (z.B. USA)"
|
||||||
|
className="ml-2 px-2 py-1 text-sm border border-orange-300 rounded focus:ring-2 focus:ring-orange-500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 2: Daten & Betroffene */}
|
||||||
|
{activeTab === 2 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Input Data Categories */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Input-Datenkategorien *</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newCategory}
|
||||||
|
onChange={e => setNewCategory(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && addToList('input_data_categories', newCategory, setNewCategory)}
|
||||||
|
placeholder="Datenkategorie hinzufügen..."
|
||||||
|
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => addToList('input_data_categories', newCategory, setNewCategory)}
|
||||||
|
className="px-3 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(module.input_data_categories || []).map((cat, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs">
|
||||||
|
{cat}
|
||||||
|
<button onClick={() => removeFromList('input_data_categories', i)} className="hover:text-purple-900">×</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Output Data Categories */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Output-Datenkategorien</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newOutputCategory}
|
||||||
|
onChange={e => setNewOutputCategory(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && addToList('output_data_categories', newOutputCategory, setNewOutputCategory)}
|
||||||
|
placeholder="Output-Kategorie (z.B. Bewertung, Empfehlung)..."
|
||||||
|
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => addToList('output_data_categories', newOutputCategory, setNewOutputCategory)}
|
||||||
|
className="px-3 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(module.output_data_categories || []).map((cat, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
|
||||||
|
{cat}
|
||||||
|
<button onClick={() => removeFromList('output_data_categories', i)} className="hover:text-blue-900">×</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Special Categories */}
|
||||||
|
<div className="flex items-start gap-3 p-3 rounded-lg border border-gray-200">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="special_cats"
|
||||||
|
checked={module.involves_special_categories}
|
||||||
|
onChange={e => update({ involves_special_categories: e.target.checked })}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||||
|
/>
|
||||||
|
<label htmlFor="special_cats" className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-gray-900">Besondere Kategorien (Art. 9 DSGVO)</div>
|
||||||
|
<p className="text-xs text-gray-500">Gesundheit, Biometrie, Religion, politische Meinung etc.</p>
|
||||||
|
{module.involves_special_categories && (
|
||||||
|
<textarea
|
||||||
|
value={module.special_categories_justification || ''}
|
||||||
|
onChange={e => update({ special_categories_justification: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Begründung nach Art. 9 Abs. 2 DSGVO..."
|
||||||
|
className="mt-2 w-full px-3 py-2 text-xs border border-orange-300 rounded focus:ring-2 focus:ring-orange-400 resize-none"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Subjects */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Betroffenengruppen *</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newSubject}
|
||||||
|
onChange={e => setNewSubject(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && addToList('data_subjects', newSubject, setNewSubject)}
|
||||||
|
placeholder="z.B. Kunden, Mitarbeiter, Nutzer..."
|
||||||
|
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => addToList('data_subjects', newSubject, setNewSubject)}
|
||||||
|
className="px-3 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{(module.data_subjects || []).map((s, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
|
||||||
|
{s}
|
||||||
|
<button onClick={() => removeFromList('data_subjects', i)} className="hover:text-green-900">×</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Geschätztes Volumen</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={module.estimated_volume || ''}
|
||||||
|
onChange={e => update({ estimated_volume: e.target.value })}
|
||||||
|
placeholder="z.B. >10.000 Personen/Monat"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsdauer (Monate)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={module.data_retention_months || ''}
|
||||||
|
onChange={e => update({ data_retention_months: parseInt(e.target.value) || undefined })}
|
||||||
|
min={1}
|
||||||
|
placeholder="z.B. 24"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 3: Zweck & Art. 22 */}
|
||||||
|
{activeTab === 3 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Verarbeitungszweck *</label>
|
||||||
|
<textarea
|
||||||
|
value={module.processing_purpose}
|
||||||
|
onChange={e => update({ processing_purpose: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Welchem Zweck dient dieses KI-System? Was soll erreicht werden?"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsgrundlage *</label>
|
||||||
|
<select
|
||||||
|
value={module.legal_basis}
|
||||||
|
onChange={e => update({ legal_basis: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
<option value="">Bitte wählen...</option>
|
||||||
|
{LEGAL_BASES.map(lb => (
|
||||||
|
<option key={lb} value={lb}>{lb}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{module.legal_basis && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Details zur Rechtsgrundlage</label>
|
||||||
|
<textarea
|
||||||
|
value={module.legal_basis_details || ''}
|
||||||
|
onChange={e => update({ legal_basis_details: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Ergänzende Erläuterung zur Rechtsgrundlage..."
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Art. 22 Panel */}
|
||||||
|
<div className="border-t border-gray-200 pt-4">
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-3">
|
||||||
|
Art. 22 DSGVO – Automatisierte Einzelentscheidung
|
||||||
|
{art22Required && (
|
||||||
|
<span className="ml-2 text-xs px-1.5 py-0.5 bg-red-100 text-red-700 rounded">
|
||||||
|
Wahrscheinlich anwendbar
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h4>
|
||||||
|
<Art22AssessmentPanel
|
||||||
|
assessment={module.art22_assessment}
|
||||||
|
onChange={a22 => update({ art22_assessment: a22 })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 4: KI-Kriterien & AI Act */}
|
||||||
|
{activeTab === 4 && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-3">WP248-Risikokriterien</h4>
|
||||||
|
<AIRiskCriteriaChecklist
|
||||||
|
criteria={module.risk_criteria}
|
||||||
|
onChange={criteria => update({ risk_criteria: criteria })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-200 pt-4">
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-3">EU AI Act – Risikoklasse</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{(Object.entries(AI_ACT_RISK_CLASSES) as [AIActRiskClass, typeof AI_ACT_RISK_CLASSES[AIActRiskClass]][]).map(([cls, info]) => (
|
||||||
|
<button
|
||||||
|
key={cls}
|
||||||
|
onClick={() => update({ ai_act_risk_class: cls })}
|
||||||
|
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||||
|
module.ai_act_risk_class === cls
|
||||||
|
? cls === 'unacceptable' ? 'border-red-500 bg-red-50'
|
||||||
|
: cls === 'high_risk' ? 'border-orange-500 bg-orange-50'
|
||||||
|
: cls === 'limited' ? 'border-yellow-500 bg-yellow-50'
|
||||||
|
: 'border-green-500 bg-green-50'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm text-gray-900">{info.labelDE}</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{info.description}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{module.ai_act_risk_class && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Begründung der Klassifizierung</label>
|
||||||
|
<textarea
|
||||||
|
value={module.ai_act_justification || ''}
|
||||||
|
onChange={e => update({ ai_act_justification: e.target.value })}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Warum wurde diese Risikoklasse gewählt?"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{module.ai_act_risk_class && AI_ACT_RISK_CLASSES[module.ai_act_risk_class].requirements.length > 0 && (
|
||||||
|
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
||||||
|
<div className="text-xs font-medium text-gray-700 mb-2">Anforderungen dieser Klasse:</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{AI_ACT_RISK_CLASSES[module.ai_act_risk_class].requirements.map((req, i) => (
|
||||||
|
<li key={i} className="text-xs text-gray-600 flex items-start gap-1.5">
|
||||||
|
<span className="text-purple-500 flex-shrink-0">•</span>
|
||||||
|
{req}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 5: Risikoanalyse */}
|
||||||
|
{activeTab === 5 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Spezifische Risiken für diesen KI-Anwendungsfall. Typische Risiken basierend auf dem gewählten Typ:
|
||||||
|
</p>
|
||||||
|
{typeInfo.typical_risks.length > 0 && (
|
||||||
|
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<div className="text-xs font-medium text-yellow-800 mb-1">Typische Risiken für {typeInfo.label}:</div>
|
||||||
|
<ul className="space-y-0.5">
|
||||||
|
{typeInfo.typical_risks.map((r, i) => (
|
||||||
|
<li key={i} className="text-xs text-yellow-700 flex items-center gap-1.5">
|
||||||
|
<span>⚠️</span> {r}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(module.risks || []).map((risk, idx) => (
|
||||||
|
<div key={idx} className="p-3 border border-gray-200 rounded-lg bg-gray-50">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm text-gray-900">{risk.description}</p>
|
||||||
|
<div className="flex gap-2 mt-1">
|
||||||
|
<span className="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-600 rounded">W: {risk.likelihood}</span>
|
||||||
|
<span className="text-xs px-1.5 py-0.5 bg-purple-100 text-purple-600 rounded">S: {risk.impact}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => update({ risks: module.risks.filter((_, i) => i !== idx) })}
|
||||||
|
className="text-gray-400 hover:text-red-500 ml-2"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(module.risks || []).length === 0 && (
|
||||||
|
<p className="text-sm text-gray-400 text-center py-4">Noch keine Risiken dokumentiert</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Add Risk */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const desc = prompt('Risiko-Beschreibung:')
|
||||||
|
if (desc) {
|
||||||
|
update({
|
||||||
|
risks: [...(module.risks || []), {
|
||||||
|
risk_id: crypto.randomUUID(),
|
||||||
|
description: desc,
|
||||||
|
likelihood: 'medium',
|
||||||
|
impact: 'medium',
|
||||||
|
mitigation_ids: [],
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-purple-400 hover:text-purple-600 transition-colors"
|
||||||
|
>
|
||||||
|
+ Risiko hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 6: Maßnahmen & Privacy by Design */}
|
||||||
|
{activeTab === 6 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 mb-3">Privacy by Design Maßnahmen</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{(Object.entries(PRIVACY_BY_DESIGN_CATEGORIES) as [PrivacyByDesignCategory, typeof PRIVACY_BY_DESIGN_CATEGORIES[PrivacyByDesignCategory]][]).map(([cat, info]) => {
|
||||||
|
const measure = module.privacy_by_design_measures?.find(m => m.category === cat)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
onClick={() => togglePbdMeasure(cat)}
|
||||||
|
className={`flex items-start gap-2 p-3 rounded-lg border text-left transition-all ${
|
||||||
|
measure?.implemented
|
||||||
|
? 'border-green-400 bg-green-50'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-lg flex-shrink-0">{info.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className={`text-xs font-medium ${measure?.implemented ? 'text-green-800' : 'text-gray-700'}`}>
|
||||||
|
{info.label}
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-gray-500 mt-0.5">{info.description}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tab 7: Review-Trigger */}
|
||||||
|
{activeTab === 7 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Wählen Sie die Ereignisse, die eine erneute Bewertung dieses KI-Anwendungsfalls auslösen sollen.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{REVIEW_TRIGGER_TYPES.map(rt => {
|
||||||
|
const active = module.review_triggers?.some(t => t.type === rt.value)
|
||||||
|
const trigger = module.review_triggers?.find(t => t.type === rt.value)
|
||||||
|
return (
|
||||||
|
<div key={rt.value} className={`rounded-lg border p-3 transition-all ${active ? 'border-purple-300 bg-purple-50' : 'border-gray-200'}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={active || false}
|
||||||
|
onChange={() => toggleReviewTrigger(rt.value)}
|
||||||
|
className="h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||||
|
/>
|
||||||
|
<span className="text-base">{rt.icon}</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900">{rt.label}</span>
|
||||||
|
</div>
|
||||||
|
{active && (
|
||||||
|
<div className="mt-2 ml-7 space-y-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={trigger?.threshold || ''}
|
||||||
|
onChange={e => {
|
||||||
|
const updated = (module.review_triggers || []).map(t =>
|
||||||
|
t.type === rt.value ? { ...t, threshold: e.target.value } : t
|
||||||
|
)
|
||||||
|
update({ review_triggers: updated })
|
||||||
|
}}
|
||||||
|
placeholder="Schwellwert (z.B. Genauigkeit < 80%)"
|
||||||
|
className="w-full px-2 py-1 text-xs border border-purple-200 rounded focus:ring-2 focus:ring-purple-400"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={trigger?.monitoring_interval || ''}
|
||||||
|
onChange={e => {
|
||||||
|
const updated = (module.review_triggers || []).map(t =>
|
||||||
|
t.type === rt.value ? { ...t, monitoring_interval: e.target.value } : t
|
||||||
|
)
|
||||||
|
update({ review_triggers: updated })
|
||||||
|
}}
|
||||||
|
placeholder="Monitoring-Intervall (z.B. wöchentlich)"
|
||||||
|
className="w-full px-2 py-1 text-xs border border-purple-200 rounded focus:ring-2 focus:ring-purple-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Monitoring-Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
value={module.monitoring_description || ''}
|
||||||
|
onChange={e => update({ monitoring_description: e.target.value })}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Wie wird das KI-System kontinuierlich überwacht? Welche Metriken werden erfasst?"
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Nächstes Review-Datum</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={module.next_review_date || ''}
|
||||||
|
onChange={e => update({ next_review_date: e.target.value })}
|
||||||
|
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between flex-shrink-0">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{activeTab > 1 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab(activeTab - 1)}
|
||||||
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
← Zurück
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{activeTab < 7 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab(activeTab + 1)}
|
||||||
|
className="px-4 py-2 text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Weiter →
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onSave(module)}
|
||||||
|
disabled={isSaving || !module.name || !module.model_description}
|
||||||
|
className="px-6 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
{isSaving ? 'Speichert...' : 'Modul speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
264
admin-compliance/components/sdk/dsfa/AIUseCaseSection.tsx
Normal file
264
admin-compliance/components/sdk/dsfa/AIUseCaseSection.tsx
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { DSFA } from '@/lib/sdk/dsfa/types'
|
||||||
|
import {
|
||||||
|
AIUseCaseModule,
|
||||||
|
AIUseCaseType,
|
||||||
|
AI_USE_CASE_TYPES,
|
||||||
|
AI_ACT_RISK_CLASSES,
|
||||||
|
createEmptyModule,
|
||||||
|
calculateModuleRiskLevel,
|
||||||
|
getModuleCompletionPercentage,
|
||||||
|
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
||||||
|
import { AIUseCaseTypeSelector } from './AIUseCaseTypeSelector'
|
||||||
|
import { AIUseCaseModuleEditor } from './AIUseCaseModuleEditor'
|
||||||
|
|
||||||
|
interface AIUseCaseSectionProps {
|
||||||
|
dsfa: DSFA
|
||||||
|
onUpdate: (data: Record<string, unknown>) => Promise<void>
|
||||||
|
isSubmitting: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const RISK_LEVEL_LABELS: Record<string, string> = {
|
||||||
|
low: 'Niedrig',
|
||||||
|
medium: 'Mittel',
|
||||||
|
high: 'Hoch',
|
||||||
|
very_high: 'Sehr hoch',
|
||||||
|
}
|
||||||
|
|
||||||
|
const RISK_LEVEL_COLORS: Record<string, string> = {
|
||||||
|
low: 'bg-green-100 text-green-700 border-green-200',
|
||||||
|
medium: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||||
|
high: 'bg-orange-100 text-orange-700 border-orange-200',
|
||||||
|
very_high: 'bg-red-100 text-red-700 border-red-200',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIUseCaseSection({ dsfa, onUpdate, isSubmitting }: AIUseCaseSectionProps) {
|
||||||
|
const [showTypeSelector, setShowTypeSelector] = useState(false)
|
||||||
|
const [editingModule, setEditingModule] = useState<AIUseCaseModule | null>(null)
|
||||||
|
const [isSavingModule, setIsSavingModule] = useState(false)
|
||||||
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const modules = dsfa.ai_use_case_modules || []
|
||||||
|
|
||||||
|
// Aggregate AI risk overview
|
||||||
|
const highRiskCount = modules.filter(m => {
|
||||||
|
const level = calculateModuleRiskLevel(m)
|
||||||
|
return level === 'high' || level === 'very_high'
|
||||||
|
}).length
|
||||||
|
|
||||||
|
const art22Count = modules.filter(m => m.art22_assessment?.applies).length
|
||||||
|
|
||||||
|
const handleSelectType = (type: AIUseCaseType) => {
|
||||||
|
setShowTypeSelector(false)
|
||||||
|
const newModule = createEmptyModule(type)
|
||||||
|
setEditingModule(newModule)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveModule = async (savedModule: AIUseCaseModule) => {
|
||||||
|
setIsSavingModule(true)
|
||||||
|
try {
|
||||||
|
const existing = dsfa.ai_use_case_modules || []
|
||||||
|
const idx = existing.findIndex(m => m.id === savedModule.id)
|
||||||
|
const updated = idx >= 0
|
||||||
|
? existing.map(m => m.id === savedModule.id ? savedModule : m)
|
||||||
|
: [...existing, savedModule]
|
||||||
|
|
||||||
|
await onUpdate({ ai_use_case_modules: updated })
|
||||||
|
setEditingModule(null)
|
||||||
|
} finally {
|
||||||
|
setIsSavingModule(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteModule = async (moduleId: string) => {
|
||||||
|
const updated = modules.filter(m => m.id !== moduleId)
|
||||||
|
await onUpdate({ ai_use_case_modules: updated })
|
||||||
|
setConfirmDeleteId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header & Info */}
|
||||||
|
<div className="p-4 bg-purple-50 border border-purple-200 rounded-xl">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-2xl">🤖</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-purple-900">KI-Anwendungsfälle (Section 8)</h3>
|
||||||
|
<p className="text-sm text-purple-700 mt-1">
|
||||||
|
Dokumentieren Sie KI-spezifische Verarbeitungen mit modularen Anhängen nach Art. 35 DSGVO,
|
||||||
|
Art. 22 DSGVO und den Transparenzanforderungen des EU AI Act.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning: involves_ai but no modules */}
|
||||||
|
{dsfa.involves_ai && modules.length === 0 && (
|
||||||
|
<div className="flex items-start gap-3 p-4 bg-yellow-50 border border-yellow-300 rounded-xl">
|
||||||
|
<svg className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-yellow-800">KI-Einsatz dokumentiert, aber keine Module vorhanden</p>
|
||||||
|
<p className="text-xs text-yellow-700 mt-0.5">
|
||||||
|
In Section 3 wurde KI-Einsatz festgestellt. Fügen Sie mindestens ein KI-Modul hinzu.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Aggregated Risk Overview (only when modules exist) */}
|
||||||
|
{modules.length > 0 && (
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="p-3 bg-white border border-gray-200 rounded-xl text-center">
|
||||||
|
<div className="text-2xl font-bold text-purple-700">{modules.length}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">KI-Module</div>
|
||||||
|
</div>
|
||||||
|
<div className={`p-3 rounded-xl text-center border ${highRiskCount > 0 ? 'bg-red-50 border-red-200' : 'bg-green-50 border-green-200'}`}>
|
||||||
|
<div className={`text-2xl font-bold ${highRiskCount > 0 ? 'text-red-700' : 'text-green-700'}`}>{highRiskCount}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">Hoch-Risiko-Module</div>
|
||||||
|
</div>
|
||||||
|
<div className={`p-3 rounded-xl text-center border ${art22Count > 0 ? 'bg-orange-50 border-orange-200' : 'bg-gray-50 border-gray-200'}`}>
|
||||||
|
<div className={`text-2xl font-bold ${art22Count > 0 ? 'text-orange-700' : 'text-gray-400'}`}>{art22Count}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">Art. 22 DSGVO</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Module Cards */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{modules.map(module => {
|
||||||
|
const riskLevel = calculateModuleRiskLevel(module)
|
||||||
|
const completion = getModuleCompletionPercentage(module)
|
||||||
|
const typeInfo = AI_USE_CASE_TYPES[module.use_case_type]
|
||||||
|
const aiActInfo = AI_ACT_RISK_CLASSES[module.ai_act_risk_class]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={module.id}
|
||||||
|
className="bg-white border border-gray-200 rounded-xl p-4 hover:border-purple-300 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-2xl flex-shrink-0">{typeInfo.icon}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-medium text-gray-900">{module.name}</span>
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded border ${RISK_LEVEL_COLORS[riskLevel]}`}>
|
||||||
|
{RISK_LEVEL_LABELS[riskLevel]}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs px-1.5 py-0.5 bg-gray-100 text-gray-500 rounded">
|
||||||
|
{typeInfo.label}
|
||||||
|
</span>
|
||||||
|
{module.art22_assessment?.applies && (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 bg-orange-100 text-orange-700 rounded border border-orange-200">
|
||||||
|
Art. 22
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{module.ai_act_risk_class !== 'minimal' && (
|
||||||
|
<span className={`text-xs px-1.5 py-0.5 rounded border ${
|
||||||
|
module.ai_act_risk_class === 'unacceptable' ? 'bg-red-100 text-red-700 border-red-200' :
|
||||||
|
module.ai_act_risk_class === 'high_risk' ? 'bg-orange-100 text-orange-700 border-orange-200' :
|
||||||
|
'bg-yellow-100 text-yellow-700 border-yellow-200'
|
||||||
|
}`}>
|
||||||
|
AI Act: {aiActInfo.labelDE}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{module.model_description && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1 line-clamp-1">{module.model_description}</p>
|
||||||
|
)}
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-purple-500 transition-all"
|
||||||
|
style={{ width: `${completion}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-gray-400">{completion}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setEditingModule(module)}
|
||||||
|
className="px-3 py-1.5 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmDeleteId(module.id)}
|
||||||
|
className="px-2 py-1.5 text-xs text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete confirmation */}
|
||||||
|
{confirmDeleteId === module.id && (
|
||||||
|
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p className="text-sm text-red-700 mb-2">Modul wirklich löschen?</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteModule(module.id)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="px-3 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmDeleteId(null)}
|
||||||
|
className="px-3 py-1 text-xs bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{modules.length === 0 && !dsfa.involves_ai && (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
<div className="text-4xl mb-2">🤖</div>
|
||||||
|
<p className="text-sm">Keine KI-Anwendungsfälle dokumentiert</p>
|
||||||
|
<p className="text-xs mt-1">Optional: Fügen Sie KI-Module hinzu, wenn dieses DSFA KI-Einsatz umfasst</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Module Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTypeSelector(true)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="w-full py-3 border-2 border-dashed border-purple-300 rounded-xl text-sm text-purple-600 hover:border-purple-500 hover:bg-purple-50 transition-all font-medium"
|
||||||
|
>
|
||||||
|
+ KI-Anwendungsfall-Modul hinzufügen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Type Selector Modal */}
|
||||||
|
{showTypeSelector && (
|
||||||
|
<AIUseCaseTypeSelector
|
||||||
|
onSelect={handleSelectType}
|
||||||
|
onCancel={() => setShowTypeSelector(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Module Editor Modal */}
|
||||||
|
{editingModule && (
|
||||||
|
<AIUseCaseModuleEditor
|
||||||
|
module={editingModule}
|
||||||
|
onSave={handleSaveModule}
|
||||||
|
onCancel={() => setEditingModule(null)}
|
||||||
|
isSaving={isSavingModule}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { AI_USE_CASE_TYPES, AIUseCaseType } from '@/lib/sdk/dsfa/ai-use-case-types'
|
||||||
|
|
||||||
|
interface AIUseCaseTypeSelectorProps {
|
||||||
|
onSelect: (type: AIUseCaseType) => void
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIUseCaseTypeSelector({ onSelect, onCancel }: AIUseCaseTypeSelectorProps) {
|
||||||
|
const types = Object.entries(AI_USE_CASE_TYPES) as [AIUseCaseType, typeof AI_USE_CASE_TYPES[AIUseCaseType]][]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">KI-Anwendungsfall hinzufügen</h2>
|
||||||
|
<p className="text-sm text-gray-500 mt-0.5">Wählen Sie den Typ des KI-Systems für diesen Anhang</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type Grid */}
|
||||||
|
<div className="p-6 grid grid-cols-2 gap-3 max-h-[60vh] overflow-y-auto">
|
||||||
|
{types.map(([type, info]) => (
|
||||||
|
<button
|
||||||
|
key={type}
|
||||||
|
onClick={() => onSelect(type)}
|
||||||
|
className="flex items-start gap-3 p-4 rounded-xl border-2 border-gray-200 hover:border-purple-400 hover:bg-purple-50 text-left transition-all group"
|
||||||
|
>
|
||||||
|
<span className="text-2xl flex-shrink-0">{info.icon}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium text-gray-900 group-hover:text-purple-700">{info.label}</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{info.description}</p>
|
||||||
|
{info.typical_risks.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{info.typical_risks.slice(0, 2).map(risk => (
|
||||||
|
<span key={risk} className="text-[10px] px-1.5 py-0.5 bg-red-50 text-red-600 rounded">
|
||||||
|
{risk}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="px-6 py-4 border-t border-gray-200 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
149
admin-compliance/components/sdk/dsfa/Art22AssessmentPanel.tsx
Normal file
149
admin-compliance/components/sdk/dsfa/Art22AssessmentPanel.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { Art22Assessment, Art22ExceptionType } from '@/lib/sdk/dsfa/ai-use-case-types'
|
||||||
|
|
||||||
|
interface Art22AssessmentPanelProps {
|
||||||
|
assessment: Art22Assessment
|
||||||
|
onChange: (updated: Art22Assessment) => void
|
||||||
|
readonly?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const EXCEPTION_OPTIONS: { value: Art22ExceptionType; label: string; description: string }[] = [
|
||||||
|
{
|
||||||
|
value: 'contract',
|
||||||
|
label: 'Art. 22 Abs. 2 lit. a – Vertragserfüllung',
|
||||||
|
description: 'Die Entscheidung ist für den Abschluss oder die Erfüllung eines Vertrags erforderlich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'legal',
|
||||||
|
label: 'Art. 22 Abs. 2 lit. b – Rechtliche Verpflichtung',
|
||||||
|
description: 'Die Entscheidung ist durch Unionsrecht oder mitgliedstaatliches Recht zugelassen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'consent',
|
||||||
|
label: 'Art. 22 Abs. 2 lit. c – Ausdrückliche Einwilligung',
|
||||||
|
description: 'Die betroffene Person hat ausdrücklich eingewilligt',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Art22AssessmentPanel({ assessment, onChange, readonly }: Art22AssessmentPanelProps) {
|
||||||
|
const missingRequiredSafeguards = assessment.applies &&
|
||||||
|
!assessment.safeguards.some(s => s.id === 'human_review' && s.implemented)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Art. 22 Toggle */}
|
||||||
|
<div className="flex items-start gap-3 p-4 rounded-xl border border-gray-200 bg-gray-50">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="art22_applies"
|
||||||
|
checked={assessment.applies}
|
||||||
|
onChange={e => onChange({ ...assessment, applies: e.target.checked })}
|
||||||
|
disabled={readonly}
|
||||||
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<label htmlFor="art22_applies" className="flex-1">
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
Automatisierte Einzelentscheidung mit Rechtswirkung (Art. 22 DSGVO)
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
|
Das KI-System trifft Entscheidungen, die rechtliche Wirkung oder ähnlich erhebliche
|
||||||
|
Auswirkungen auf Personen haben, ohne maßgebliche menschliche Beteiligung.
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning if applies without safeguards */}
|
||||||
|
{missingRequiredSafeguards && (
|
||||||
|
<div className="flex items-start gap-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<svg className="w-4 h-4 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-sm text-red-700">
|
||||||
|
Art. 22 gilt als anwendbar, aber die erforderliche Schutzmaßnahme „Recht auf menschliche Überprüfung" ist nicht implementiert.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{assessment.applies && (
|
||||||
|
<>
|
||||||
|
{/* Justification */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Begründung der Ausnahme (Art. 22 Abs. 2)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={assessment.justification || ''}
|
||||||
|
onChange={e => onChange({ ...assessment, justification: e.target.value })}
|
||||||
|
disabled={readonly}
|
||||||
|
rows={3}
|
||||||
|
placeholder="Begründen Sie, auf welcher Rechtsgrundlage die automatisierte Entscheidung zulässig ist..."
|
||||||
|
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Exception Type */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">Ausnahmetatbestand</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{EXCEPTION_OPTIONS.map(opt => (
|
||||||
|
<label key={opt.value} className="flex items-start gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="art22_exception"
|
||||||
|
value={opt.value}
|
||||||
|
checked={assessment.exception_type === opt.value}
|
||||||
|
onChange={() => onChange({ ...assessment, exception_type: opt.value })}
|
||||||
|
disabled={readonly}
|
||||||
|
className="mt-0.5 h-4 w-4 text-purple-600"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-900">{opt.label}</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">{opt.description}</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Safeguards */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Schutzmaßnahmen (Art. 22 Abs. 3 DSGVO)
|
||||||
|
</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{assessment.safeguards.map((safeguard, idx) => (
|
||||||
|
<label key={safeguard.id} className="flex items-start gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={safeguard.implemented}
|
||||||
|
onChange={e => {
|
||||||
|
const updated = assessment.safeguards.map((s, i) =>
|
||||||
|
i === idx ? { ...s, implemented: e.target.checked } : s
|
||||||
|
)
|
||||||
|
onChange({ ...assessment, safeguards: updated })
|
||||||
|
}}
|
||||||
|
disabled={readonly}
|
||||||
|
className="mt-0.5 h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className={`text-sm font-medium ${safeguard.id === 'human_review' && !safeguard.implemented ? 'text-red-700' : 'text-gray-900'}`}>
|
||||||
|
{safeguard.label}
|
||||||
|
{safeguard.id === 'human_review' && (
|
||||||
|
<span className="ml-1 text-xs text-red-500">*Pflicht</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{safeguard.description && (
|
||||||
|
<p className="text-xs text-gray-500 mt-0.5">{safeguard.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -70,6 +70,15 @@ function calculateSectionProgress(dsfa: DSFA, sectionNumber: number): number {
|
|||||||
if (rs.next_review_date) return 50
|
if (rs.next_review_date) return 50
|
||||||
return 25
|
return 25
|
||||||
|
|
||||||
|
case 8: // KI-Anwendungsfälle (optional)
|
||||||
|
const aiModules = dsfa.ai_use_case_modules || []
|
||||||
|
if (aiModules.length === 0) return 0
|
||||||
|
const avgCompletion = aiModules.reduce((sum, m) => {
|
||||||
|
const checks = [!!m.name, !!m.model_description, m.input_data_categories?.length > 0, !!m.processing_purpose, !!m.legal_basis, !!m.ai_act_risk_class]
|
||||||
|
return sum + Math.round((checks.filter(Boolean).length / checks.length) * 100)
|
||||||
|
}, 0) / aiModules.length
|
||||||
|
return Math.round(avgCompletion)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -87,6 +96,7 @@ function isSectionComplete(dsfa: DSFA, sectionNumber: number): boolean {
|
|||||||
case 5: return progress.section_5_complete ?? false
|
case 5: return progress.section_5_complete ?? false
|
||||||
case 6: return progress.section_6_complete ?? false
|
case 6: return progress.section_6_complete ?? false
|
||||||
case 7: return progress.section_7_complete ?? false
|
case 7: return progress.section_7_complete ?? false
|
||||||
|
case 8: return progress.section_8_complete ?? false
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +122,7 @@ export function DSFASidebar({ dsfa, activeSection, onSectionChange }: DSFASideba
|
|||||||
const stakeholderSection = DSFA_SECTIONS.find(s => s.number === 5)
|
const stakeholderSection = DSFA_SECTIONS.find(s => s.number === 5)
|
||||||
const consultationSection = DSFA_SECTIONS.find(s => s.number === 6)
|
const consultationSection = DSFA_SECTIONS.find(s => s.number === 6)
|
||||||
const reviewSection = DSFA_SECTIONS.find(s => s.number === 7)
|
const reviewSection = DSFA_SECTIONS.find(s => s.number === 7)
|
||||||
|
const aiSection = DSFA_SECTIONS.find(s => s.number === 8)
|
||||||
|
|
||||||
const renderSectionItem = (section: typeof DSFA_SECTIONS[0]) => {
|
const renderSectionItem = (section: typeof DSFA_SECTIONS[0]) => {
|
||||||
const progress = calculateSectionProgress(dsfa, section.number)
|
const progress = calculateSectionProgress(dsfa, section.number)
|
||||||
@@ -230,13 +241,21 @@ export function DSFASidebar({ dsfa, activeSection, onSectionChange }: DSFASideba
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section 7: Review */}
|
{/* Section 7: Review */}
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
||||||
Fortschreibung
|
Fortschreibung
|
||||||
</div>
|
</div>
|
||||||
{reviewSection && renderSectionItem(reviewSection)}
|
{reviewSection && renderSectionItem(reviewSection)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Section 8: KI-Anwendungsfälle */}
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 px-3">
|
||||||
|
KI & AI Act
|
||||||
|
</div>
|
||||||
|
{aiSection && renderSectionItem(aiSection)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Status Footer */}
|
{/* Status Footer */}
|
||||||
<div className="mt-6 pt-4 border-t border-gray-200">
|
<div className="mt-6 pt-4 border-t border-gray-200">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const TRIGGER_TYPES = [
|
|||||||
{ value: 'new_purpose', label: 'Neuer Zweck', description: 'Aenderung oder Erweiterung des Verarbeitungszwecks', icon: '🎯' },
|
{ value: 'new_purpose', label: 'Neuer Zweck', description: 'Aenderung oder Erweiterung des Verarbeitungszwecks', icon: '🎯' },
|
||||||
{ value: 'incident', label: 'Sicherheitsvorfall', description: 'Datenschutzvorfall oder Sicherheitsproblem', icon: '🚨' },
|
{ value: 'incident', label: 'Sicherheitsvorfall', description: 'Datenschutzvorfall oder Sicherheitsproblem', icon: '🚨' },
|
||||||
{ value: 'regulatory', label: 'Regulatorisch', description: 'Gesetzes- oder Behoerden-Aenderung', icon: '📜' },
|
{ value: 'regulatory', label: 'Regulatorisch', description: 'Gesetzes- oder Behoerden-Aenderung', icon: '📜' },
|
||||||
|
{ value: 'ai_use_case_module', label: 'KI-Modul-Änderung', description: 'Änderung eines KI-Anwendungsfalls (Modell-Update, Datendrift)', icon: '🤖' },
|
||||||
{ value: 'other', label: 'Sonstiges', description: 'Anderer Ausloser', icon: '📋' },
|
{ value: 'other', label: 'Sonstiges', description: 'Anderer Ausloser', icon: '📋' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export { StakeholderConsultationSection } from './StakeholderConsultationSection
|
|||||||
export { Art36Warning } from './Art36Warning'
|
export { Art36Warning } from './Art36Warning'
|
||||||
export { ReviewScheduleSection } from './ReviewScheduleSection'
|
export { ReviewScheduleSection } from './ReviewScheduleSection'
|
||||||
export { SourceAttribution, InlineSourceRef, AttributionFooter } from './SourceAttribution'
|
export { SourceAttribution, InlineSourceRef, AttributionFooter } from './SourceAttribution'
|
||||||
|
export { AIUseCaseSection } from './AIUseCaseSection'
|
||||||
|
export { AIUseCaseModuleEditor } from './AIUseCaseModuleEditor'
|
||||||
|
export { AIUseCaseTypeSelector } from './AIUseCaseTypeSelector'
|
||||||
|
export { Art22AssessmentPanel } from './Art22AssessmentPanel'
|
||||||
|
export { AIRiskCriteriaChecklist } from './AIRiskCriteriaChecklist'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// DSFA Card Component
|
// DSFA Card Component
|
||||||
|
|||||||
448
admin-compliance/lib/sdk/dsfa/ai-use-case-types.ts
Normal file
448
admin-compliance/lib/sdk/dsfa/ai-use-case-types.ts
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
/**
|
||||||
|
* DSFA AI Use Case Types
|
||||||
|
*
|
||||||
|
* Type definitions, constants, and helpers for Section 8 "KI-Anwendungsfälle"
|
||||||
|
* of the DSFA module. Covers Art. 22 DSGVO, EU AI Act risk classification,
|
||||||
|
* and WP248-based risk criteria for AI/ML processing activities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { DSFARiskLevel } from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ENUMS & UNION TYPES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type AIUseCaseType =
|
||||||
|
| 'chatbot_nlp'
|
||||||
|
| 'recommendation'
|
||||||
|
| 'adm_scoring'
|
||||||
|
| 'video_image'
|
||||||
|
| 'biometrics'
|
||||||
|
| 'iot_sensors'
|
||||||
|
| 'generative_ai'
|
||||||
|
| 'custom'
|
||||||
|
|
||||||
|
export type AIActRiskClass =
|
||||||
|
| 'unacceptable'
|
||||||
|
| 'high_risk'
|
||||||
|
| 'limited'
|
||||||
|
| 'minimal'
|
||||||
|
|
||||||
|
export type Art22ExceptionType =
|
||||||
|
| 'contract' // Art. 22 Abs. 2 lit. a – Vertragserfüllung
|
||||||
|
| 'legal' // Art. 22 Abs. 2 lit. b – gesetzliche Verpflichtung
|
||||||
|
| 'consent' // Art. 22 Abs. 2 lit. c – explizite Einwilligung
|
||||||
|
|
||||||
|
export type PrivacyByDesignCategory =
|
||||||
|
| 'data_minimisation'
|
||||||
|
| 'pseudonymisation'
|
||||||
|
| 'encryption'
|
||||||
|
| 'purpose_limitation'
|
||||||
|
| 'access_control'
|
||||||
|
| 'audit_logging'
|
||||||
|
| 'explainability'
|
||||||
|
| 'human_oversight'
|
||||||
|
| 'fairness_testing'
|
||||||
|
| 'model_governance'
|
||||||
|
|
||||||
|
export type AIModuleReviewTriggerType =
|
||||||
|
| 'model_update'
|
||||||
|
| 'data_drift'
|
||||||
|
| 'accuracy_drop'
|
||||||
|
| 'new_use_case'
|
||||||
|
| 'regulatory_change'
|
||||||
|
| 'incident'
|
||||||
|
| 'periodic'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// INTERFACES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface Art22Assessment {
|
||||||
|
applies: boolean
|
||||||
|
justification?: string
|
||||||
|
exception_type?: Art22ExceptionType
|
||||||
|
safeguards: Art22Safeguard[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Art22Safeguard {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
implemented: boolean
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIUseCaseRiskCriterion {
|
||||||
|
id: string
|
||||||
|
applies: boolean
|
||||||
|
justification?: string
|
||||||
|
severity: 'low' | 'medium' | 'high'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrivacyByDesignMeasure {
|
||||||
|
category: PrivacyByDesignCategory
|
||||||
|
description: string
|
||||||
|
implemented: boolean
|
||||||
|
implementation_date?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIModuleReviewTrigger {
|
||||||
|
type: AIModuleReviewTriggerType
|
||||||
|
description: string
|
||||||
|
threshold?: string // e.g. "accuracy < 80%"
|
||||||
|
monitoring_interval?: string // e.g. "weekly"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIUseCaseModuleRisk {
|
||||||
|
risk_id: string // Reference to DSFA risk catalog entry
|
||||||
|
description: string
|
||||||
|
likelihood: 'low' | 'medium' | 'high'
|
||||||
|
impact: 'low' | 'medium' | 'high'
|
||||||
|
mitigation_ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIUseCaseModuleMitigation {
|
||||||
|
mitigation_id: string // Reference to DSFA mitigation library
|
||||||
|
description: string
|
||||||
|
status: 'planned' | 'in_progress' | 'implemented'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIUseCaseModule {
|
||||||
|
id: string
|
||||||
|
use_case_type: AIUseCaseType
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
|
||||||
|
// Tab 1: Systembeschreibung
|
||||||
|
name: string
|
||||||
|
model_description: string // Art und Weise des KI-Systems
|
||||||
|
model_type?: string // z.B. "Random Forest", "GPT-based LLM"
|
||||||
|
data_flow_description?: string // Datenfluss-Beschreibung
|
||||||
|
provider?: string // Anbieter / Dienstleister
|
||||||
|
provider_country?: string // Datenübermittlung Drittland?
|
||||||
|
third_country_transfer: boolean
|
||||||
|
|
||||||
|
// Tab 2: Daten & Betroffene
|
||||||
|
input_data_categories: string[]
|
||||||
|
output_data_categories: string[]
|
||||||
|
involves_special_categories: boolean
|
||||||
|
special_categories_justification?: string
|
||||||
|
data_subjects: string[]
|
||||||
|
estimated_volume?: string // z.B. ">10.000 Personen"
|
||||||
|
data_retention_months?: number
|
||||||
|
|
||||||
|
// Tab 3: Zweck & Rechtsgrundlage
|
||||||
|
processing_purpose: string
|
||||||
|
legal_basis: string
|
||||||
|
legal_basis_details?: string
|
||||||
|
art22_assessment: Art22Assessment
|
||||||
|
|
||||||
|
// Tab 4: KI-Kriterien & AI Act
|
||||||
|
risk_criteria: AIUseCaseRiskCriterion[]
|
||||||
|
ai_act_risk_class: AIActRiskClass
|
||||||
|
ai_act_justification?: string
|
||||||
|
wp248_criteria_met: string[] // IDs der relevanten WP248-Kriterien
|
||||||
|
|
||||||
|
// Tab 5: Risikoanalyse
|
||||||
|
risks: AIUseCaseModuleRisk[]
|
||||||
|
|
||||||
|
// Tab 6: Maßnahmen & Privacy by Design
|
||||||
|
mitigations: AIUseCaseModuleMitigation[]
|
||||||
|
privacy_by_design_measures: PrivacyByDesignMeasure[]
|
||||||
|
|
||||||
|
// Tab 7: Review-Trigger & Monitoring
|
||||||
|
review_triggers: AIModuleReviewTrigger[]
|
||||||
|
monitoring_description?: string
|
||||||
|
last_reviewed_at?: string
|
||||||
|
next_review_date?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONSTANTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const AI_USE_CASE_TYPES: Record<AIUseCaseType, {
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
description: string
|
||||||
|
typical_risks: string[]
|
||||||
|
wp248_criteria: string[]
|
||||||
|
}> = {
|
||||||
|
chatbot_nlp: {
|
||||||
|
label: 'Chatbot / NLP',
|
||||||
|
icon: '💬',
|
||||||
|
description: 'Natürliche Sprachverarbeitung, Konversationssysteme, automatisierte Kundenkommunikation',
|
||||||
|
typical_risks: ['Fehlinformation', 'Datenpersistenz im Verlauf', 'Unbeabsichtigte Datenoffenbarung'],
|
||||||
|
wp248_criteria: ['K1', 'K4'],
|
||||||
|
},
|
||||||
|
recommendation: {
|
||||||
|
label: 'Empfehlungssystem',
|
||||||
|
icon: '🎯',
|
||||||
|
description: 'Personalisierung, Inhaltsempfehlungen, Produktvorschläge basierend auf Nutzerverhalten',
|
||||||
|
typical_risks: ['Profiling', 'Verhaltensmanipulation', 'Filterblasen'],
|
||||||
|
wp248_criteria: ['K1', 'K3', 'K6'],
|
||||||
|
},
|
||||||
|
adm_scoring: {
|
||||||
|
label: 'ADM / Scoring',
|
||||||
|
icon: '⚖️',
|
||||||
|
description: 'Automatisierte Entscheidungsfindung mit Rechtswirkung, Bonitätsprüfung, HR-Scoring',
|
||||||
|
typical_risks: ['Diskriminierung', 'Fehlende Erklärbarkeit', 'Art. 22-Verletzung'],
|
||||||
|
wp248_criteria: ['K1', 'K2', 'K3', 'K5'],
|
||||||
|
},
|
||||||
|
video_image: {
|
||||||
|
label: 'Video / Bildanalyse',
|
||||||
|
icon: '📹',
|
||||||
|
description: 'Videoüberwachung, Bildklassifikation, Objekterkennung, Verhaltensanalyse',
|
||||||
|
typical_risks: ['Systematische Überwachung', 'Verdeckte Verarbeitung', 'Biometrische Ableitung'],
|
||||||
|
wp248_criteria: ['K2', 'K3', 'K6', 'K7'],
|
||||||
|
},
|
||||||
|
biometrics: {
|
||||||
|
label: 'Biometrische Daten',
|
||||||
|
icon: '👁️',
|
||||||
|
description: 'Gesichtserkennung, Fingerabdruck, Stimmerkennung, biometrische Authentifizierung',
|
||||||
|
typical_risks: ['Art. 9-Daten', 'Unwiderruflichkeit biometrischer Merkmale', 'Identitätsdiebstahl'],
|
||||||
|
wp248_criteria: ['K2', 'K3', 'K4', 'K7'],
|
||||||
|
},
|
||||||
|
iot_sensors: {
|
||||||
|
label: 'IoT / Sensordaten',
|
||||||
|
icon: '📡',
|
||||||
|
description: 'Standortverfolgung, Verhaltenssensoren, Smart Home, Wearables',
|
||||||
|
typical_risks: ['Lückenlose Überwachung', 'Kontextinferenz', 'Datenmenge'],
|
||||||
|
wp248_criteria: ['K2', 'K3', 'K5'],
|
||||||
|
},
|
||||||
|
generative_ai: {
|
||||||
|
label: 'Generative KI',
|
||||||
|
icon: '🤖',
|
||||||
|
description: 'LLMs, Bildgenerierung, Code-Generierung, synthetische Daten, RAG-Systeme',
|
||||||
|
typical_risks: ['Halluzinationen', 'Datenleckage im Prompt', 'Urheberrecht', 'Deepfakes'],
|
||||||
|
wp248_criteria: ['K1', 'K4', 'K6', 'K9'],
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
label: 'Sonstige KI',
|
||||||
|
icon: '⚙️',
|
||||||
|
description: 'Sonstige KI-gestützte Verarbeitung personenbezogener Daten',
|
||||||
|
typical_risks: ['Unbekannte Nebenwirkungen', 'Datenschutz durch Design fehlt'],
|
||||||
|
wp248_criteria: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AI_RISK_CRITERIA: Array<{
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
gdpr_ref: string
|
||||||
|
default_severity: 'low' | 'medium' | 'high'
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
id: 'adm_profiling',
|
||||||
|
label: 'ADM / Profiling',
|
||||||
|
description: 'Automatisierte Entscheidungen oder systematisches Profiling mit Auswirkungen auf Personen',
|
||||||
|
gdpr_ref: 'Art. 22 DSGVO, Erwägungsgrund 71, WP248 K1',
|
||||||
|
default_severity: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'systematic_monitoring',
|
||||||
|
label: 'Systematische Überwachung',
|
||||||
|
description: 'Überwachung öffentlicher Bereiche oder systematische Beobachtung von Personen',
|
||||||
|
gdpr_ref: 'Art. 35 Abs. 3 lit. c DSGVO, WP248 K2',
|
||||||
|
default_severity: 'high',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'large_scale',
|
||||||
|
label: 'Großflächige Verarbeitung',
|
||||||
|
description: 'Verarbeitung in großem Umfang nach Art. 9 oder Strafverfolgungsdaten',
|
||||||
|
gdpr_ref: 'Art. 35 Abs. 3 lit. b DSGVO, WP248 K3',
|
||||||
|
default_severity: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'innovative_tech',
|
||||||
|
label: 'Innovative Technologie',
|
||||||
|
description: 'Einsatz neuer Technologien mit unbekannten Risiken für die betroffenen Personen',
|
||||||
|
gdpr_ref: 'Art. 35 Abs. 1 DSGVO, Erwägungsgrund 89, WP248 K9',
|
||||||
|
default_severity: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change_trigger',
|
||||||
|
label: 'Wesentliche Änderung',
|
||||||
|
description: 'Änderung des Verarbeitungszwecks, Datenbasis oder Modells seit letzter DSFA',
|
||||||
|
gdpr_ref: 'Art. 35 Abs. 11 DSGVO',
|
||||||
|
default_severity: 'low',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dpo_consultation',
|
||||||
|
label: 'DSB-Konsultation empfohlen',
|
||||||
|
description: 'KI-System fällt in die Blacklist der Aufsichtsbehörde oder hat >= 2 Risikopunkte',
|
||||||
|
gdpr_ref: 'Art. 35 Abs. 4 DSGVO, DSK-Blacklist',
|
||||||
|
default_severity: 'high',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const AI_ACT_RISK_CLASSES: Record<AIActRiskClass, {
|
||||||
|
label: string
|
||||||
|
labelDE: string
|
||||||
|
color: string
|
||||||
|
description: string
|
||||||
|
requirements: string[]
|
||||||
|
}> = {
|
||||||
|
unacceptable: {
|
||||||
|
label: 'Unacceptable Risk',
|
||||||
|
labelDE: 'Unannehmbares Risiko',
|
||||||
|
color: 'red',
|
||||||
|
description: 'KI-Systeme mit unannehmbarem Risiko sind vollständig verboten (z.B. Social Scoring, subliminale Manipulation)',
|
||||||
|
requirements: ['VERBOTEN – Betrieb nicht gestattet'],
|
||||||
|
},
|
||||||
|
high_risk: {
|
||||||
|
label: 'High Risk',
|
||||||
|
labelDE: 'Hochrisiko-KI',
|
||||||
|
color: 'orange',
|
||||||
|
description: 'Hochrisiko-KI in Anhang III EU AI Act (z.B. Bildung, Beschäftigung, kritische Infrastruktur)',
|
||||||
|
requirements: [
|
||||||
|
'Risikomanagementsystem (Art. 9 AI Act)',
|
||||||
|
'Datenverwaltung und -qualität (Art. 10)',
|
||||||
|
'Technische Dokumentation (Art. 11)',
|
||||||
|
'Aufzeichnungspflicht / Logging (Art. 12)',
|
||||||
|
'Transparenz gegenüber Nutzern (Art. 13)',
|
||||||
|
'Menschliche Aufsicht (Art. 14)',
|
||||||
|
'Genauigkeit, Robustheit, Cybersicherheit (Art. 15)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limited: {
|
||||||
|
label: 'Limited Risk',
|
||||||
|
labelDE: 'Begrenztes Risiko',
|
||||||
|
color: 'yellow',
|
||||||
|
description: 'Transparenzpflichten: Nutzer müssen wissen, dass sie mit KI interagieren',
|
||||||
|
requirements: [
|
||||||
|
'Offenlegungspflicht bei KI-Interaktion (Art. 52 AI Act)',
|
||||||
|
'Kennzeichnung KI-generierter Inhalte',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
minimal: {
|
||||||
|
label: 'Minimal Risk',
|
||||||
|
labelDE: 'Minimales Risiko',
|
||||||
|
color: 'green',
|
||||||
|
description: 'KI mit minimalem Risiko (Spam-Filter, Empfehlungssysteme ohne wesentliche Auswirkung)',
|
||||||
|
requirements: ['Keine spezifischen Anforderungen aus AI Act'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PRIVACY_BY_DESIGN_CATEGORIES: Record<PrivacyByDesignCategory, {
|
||||||
|
label: string
|
||||||
|
icon: string
|
||||||
|
description: string
|
||||||
|
}> = {
|
||||||
|
data_minimisation: { label: 'Datenminimierung', icon: '✂️', description: 'Nur notwendige Daten verarbeiten' },
|
||||||
|
pseudonymisation: { label: 'Pseudonymisierung', icon: '🔒', description: 'Personenbezug auflösen' },
|
||||||
|
encryption: { label: 'Verschlüsselung', icon: '🔐', description: 'Daten in Ruhe und in Übertragung verschlüsseln' },
|
||||||
|
purpose_limitation: { label: 'Zweckbindung', icon: '🎯', description: 'Strikte Zweckbegrenzung durchsetzen' },
|
||||||
|
access_control: { label: 'Zugriffssteuerung', icon: '👥', description: 'Least Privilege für Modell und Daten' },
|
||||||
|
audit_logging: { label: 'Audit-Logging', icon: '📋', description: 'Alle Modellentscheidungen protokollieren' },
|
||||||
|
explainability: { label: 'Erklärbarkeit', icon: '💡', description: 'Entscheidungen nachvollziehbar machen' },
|
||||||
|
human_oversight: { label: 'Menschliche Kontrolle', icon: '👤', description: 'Human-in-the-Loop / Human-on-the-Loop' },
|
||||||
|
fairness_testing: { label: 'Fairness-Tests', icon: '⚖️', description: 'Diskriminierung erkennen und vermeiden' },
|
||||||
|
model_governance: { label: 'Modell-Governance', icon: '📊', description: 'Versionierung, Drift-Monitoring, Re-Training' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ART22_SAFEGUARDS: Art22Safeguard[] = [
|
||||||
|
{
|
||||||
|
id: 'human_review',
|
||||||
|
label: 'Recht auf menschliche Überprüfung',
|
||||||
|
implemented: false,
|
||||||
|
description: 'Betroffene können verlangen, dass die Entscheidung von einem Menschen überprüft wird',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'contestation_right',
|
||||||
|
label: 'Anfechtungsrecht',
|
||||||
|
implemented: false,
|
||||||
|
description: 'Betroffene können die automatisierte Entscheidung anfechten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'explanation_duty',
|
||||||
|
label: 'Erklärungspflicht',
|
||||||
|
implemented: false,
|
||||||
|
description: 'Betroffene werden über die Logik und Auswirkung der Entscheidung informiert',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prior_information',
|
||||||
|
label: 'Vorabinformation',
|
||||||
|
implemented: false,
|
||||||
|
description: 'Betroffene werden über ADM informiert, bevor es angewandt wird',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function createEmptyModule(type: AIUseCaseType): AIUseCaseModule {
|
||||||
|
const typeInfo = AI_USE_CASE_TYPES[type]
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
use_case_type: type,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
|
||||||
|
name: typeInfo.label,
|
||||||
|
model_description: '',
|
||||||
|
third_country_transfer: false,
|
||||||
|
|
||||||
|
input_data_categories: [],
|
||||||
|
output_data_categories: [],
|
||||||
|
involves_special_categories: false,
|
||||||
|
data_subjects: [],
|
||||||
|
|
||||||
|
processing_purpose: '',
|
||||||
|
legal_basis: '',
|
||||||
|
art22_assessment: {
|
||||||
|
applies: false,
|
||||||
|
safeguards: ART22_SAFEGUARDS.map(s => ({ ...s })),
|
||||||
|
},
|
||||||
|
|
||||||
|
risk_criteria: AI_RISK_CRITERIA.map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
applies: false,
|
||||||
|
severity: c.default_severity,
|
||||||
|
})),
|
||||||
|
ai_act_risk_class: 'minimal',
|
||||||
|
wp248_criteria_met: typeInfo.wp248_criteria,
|
||||||
|
|
||||||
|
risks: [],
|
||||||
|
mitigations: [],
|
||||||
|
privacy_by_design_measures: [],
|
||||||
|
review_triggers: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateModuleRiskLevel(module: AIUseCaseModule): DSFARiskLevel {
|
||||||
|
const appliedCriteria = module.risk_criteria.filter(c => c.applies)
|
||||||
|
const highCount = appliedCriteria.filter(c => c.severity === 'high').length
|
||||||
|
const mediumCount = appliedCriteria.filter(c => c.severity === 'medium').length
|
||||||
|
|
||||||
|
if (module.ai_act_risk_class === 'unacceptable') return 'very_high'
|
||||||
|
if (module.ai_act_risk_class === 'high_risk' || highCount >= 2) return 'high'
|
||||||
|
if (highCount === 1 || mediumCount >= 2) return 'medium'
|
||||||
|
if (mediumCount === 1 || appliedCriteria.length > 0) return 'low'
|
||||||
|
return 'low'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkArt22Applicability(module: AIUseCaseModule): boolean {
|
||||||
|
return module.use_case_type === 'adm_scoring' ||
|
||||||
|
module.risk_criteria.some(c => c.id === 'adm_profiling' && c.applies)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getModuleCompletionPercentage(module: AIUseCaseModule): number {
|
||||||
|
const checks = [
|
||||||
|
!!module.name,
|
||||||
|
!!module.model_description,
|
||||||
|
module.input_data_categories.length > 0,
|
||||||
|
module.data_subjects.length > 0,
|
||||||
|
!!module.processing_purpose,
|
||||||
|
!!module.legal_basis,
|
||||||
|
module.risk_criteria.some(c => c.applies !== undefined),
|
||||||
|
!!module.ai_act_risk_class,
|
||||||
|
module.risks.length > 0,
|
||||||
|
module.privacy_by_design_measures.length > 0,
|
||||||
|
]
|
||||||
|
return Math.round((checks.filter(Boolean).length / checks.length) * 100)
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
ApproveDSFARequest,
|
ApproveDSFARequest,
|
||||||
DSFATriggerInfo,
|
DSFATriggerInfo,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
import type { AIUseCaseModule } from './ai-use-case-types'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// CONFIGURATION
|
// CONFIGURATION
|
||||||
@@ -386,6 +387,44 @@ export async function updateDSFAMitigationStatus(
|
|||||||
// HELPER FUNCTIONS
|
// HELPER FUNCTIONS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AI USE CASE MODULE OPERATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new AI use case module to a DSFA (Section 8)
|
||||||
|
*/
|
||||||
|
export async function addAIUseCaseModule(dsfaId: string, module: AIUseCaseModule): Promise<DSFA> {
|
||||||
|
const dsfa = await getDSFA(dsfaId)
|
||||||
|
const existing = dsfa.ai_use_case_modules || []
|
||||||
|
return updateDSFA(dsfaId, { ai_use_case_modules: [...existing, module] } as Partial<DSFA>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing AI use case module in a DSFA
|
||||||
|
*/
|
||||||
|
export async function updateAIUseCaseModule(
|
||||||
|
dsfaId: string,
|
||||||
|
moduleId: string,
|
||||||
|
updates: Partial<AIUseCaseModule>
|
||||||
|
): Promise<DSFA> {
|
||||||
|
const dsfa = await getDSFA(dsfaId)
|
||||||
|
const existing = dsfa.ai_use_case_modules || []
|
||||||
|
const updated = existing.map(m =>
|
||||||
|
m.id === moduleId ? { ...m, ...updates, updated_at: new Date().toISOString() } : m
|
||||||
|
)
|
||||||
|
return updateDSFA(dsfaId, { ai_use_case_modules: updated } as Partial<DSFA>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an AI use case module from a DSFA
|
||||||
|
*/
|
||||||
|
export async function removeAIUseCaseModule(dsfaId: string, moduleId: string): Promise<DSFA> {
|
||||||
|
const dsfa = await getDSFA(dsfaId)
|
||||||
|
const updated = (dsfa.ai_use_case_modules || []).filter(m => m.id !== moduleId)
|
||||||
|
return updateDSFA(dsfaId, { ai_use_case_modules: updated } as Partial<DSFA>)
|
||||||
|
}
|
||||||
|
|
||||||
function calculateRiskLevelString(
|
function calculateRiskLevelString(
|
||||||
likelihood: 'low' | 'medium' | 'high',
|
likelihood: 'low' | 'medium' | 'high',
|
||||||
impact: 'low' | 'medium' | 'high'
|
impact: 'low' | 'medium' | 'high'
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ export * from './types'
|
|||||||
export * from './api'
|
export * from './api'
|
||||||
export * from './risk-catalog'
|
export * from './risk-catalog'
|
||||||
export * from './mitigation-library'
|
export * from './mitigation-library'
|
||||||
|
export * from './ai-use-case-types'
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
* aligned with the backend Go models.
|
* aligned with the backend Go models.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { AIUseCaseModule } from './ai-use-case-types'
|
||||||
|
export type { AIUseCaseModule } from './ai-use-case-types'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// SDM GEWAEHRLEISTUNGSZIELE (Standard-Datenschutzmodell V2.0)
|
// SDM GEWAEHRLEISTUNGSZIELE (Standard-Datenschutzmodell V2.0)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -503,6 +506,7 @@ export interface DSFASectionProgress {
|
|||||||
section_5_complete: boolean // Betroffenenperspektive (optional)
|
section_5_complete: boolean // Betroffenenperspektive (optional)
|
||||||
section_6_complete: boolean // DSB & Behördenkonsultation
|
section_6_complete: boolean // DSB & Behördenkonsultation
|
||||||
section_7_complete: boolean // Fortschreibung & Review
|
section_7_complete: boolean // Fortschreibung & Review
|
||||||
|
section_8_complete?: boolean // KI-Anwendungsfälle (optional)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -637,6 +641,9 @@ export interface DSFA {
|
|||||||
involves_ai?: boolean
|
involves_ai?: boolean
|
||||||
ai_trigger_ids?: string[] // IDs der ausgelösten KI-Trigger
|
ai_trigger_ids?: string[] // IDs der ausgelösten KI-Trigger
|
||||||
|
|
||||||
|
// Section 8: KI-Anwendungsfälle (NEU)
|
||||||
|
ai_use_case_modules?: AIUseCaseModule[]
|
||||||
|
|
||||||
// Section 4: Abhilfemaßnahmen (Art. 35 Abs. 7 lit. d)
|
// Section 4: Abhilfemaßnahmen (Art. 35 Abs. 7 lit. d)
|
||||||
mitigations: DSFAMitigation[]
|
mitigations: DSFAMitigation[]
|
||||||
tom_references?: string[]
|
tom_references?: string[]
|
||||||
@@ -873,6 +880,15 @@ export const DSFA_SECTIONS: DSFASectionConfig[] = [
|
|||||||
fields: ['review_schedule', 'review_triggers', 'version'],
|
fields: ['review_schedule', 'review_triggers', 'version'],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
number: 8,
|
||||||
|
title: 'AI Use Cases',
|
||||||
|
titleDE: 'KI-Anwendungsfälle',
|
||||||
|
description: 'Modulare Anhänge für KI-spezifische Risiken und Maßnahmen nach Art. 22 DSGVO und EU AI Act.',
|
||||||
|
gdprRef: 'Art. 35 DSGVO, Art. 22 DSGVO, EU AI Act',
|
||||||
|
fields: ['ai_use_case_modules'],
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
12
backend-compliance/migrations/028_dsfa_ai_use_cases.sql
Normal file
12
backend-compliance/migrations/028_dsfa_ai_use_cases.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- Migration 028: DSFA AI Use Cases
|
||||||
|
-- Adds ai_use_case_modules (JSONB) and section_8_complete (BOOLEAN) to compliance_dsfas
|
||||||
|
|
||||||
|
ALTER TABLE compliance.compliance_dsfas
|
||||||
|
ADD COLUMN IF NOT EXISTS ai_use_case_modules JSONB DEFAULT '[]'::jsonb,
|
||||||
|
ADD COLUMN IF NOT EXISTS section_8_complete BOOLEAN DEFAULT FALSE;
|
||||||
|
|
||||||
|
-- Extend section_progress JSONB for existing rows
|
||||||
|
UPDATE compliance.compliance_dsfas
|
||||||
|
SET section_progress = section_progress || '{"section_8_complete": false}'::jsonb
|
||||||
|
WHERE section_progress IS NOT NULL
|
||||||
|
AND NOT (section_progress ? 'section_8_complete');
|
||||||
@@ -274,9 +274,91 @@ export default function DsfaApiPage() {
|
|||||||
<ApiEndpoint method="GET" path="/dsfa/stats" description="Statistiken nach Status und Risiko" />
|
<ApiEndpoint method="GET" path="/dsfa/stats" description="Statistiken nach Status und Risiko" />
|
||||||
<ApiEndpoint method="GET" path="/dsfa/audit-log" description="Audit-Trail aller Aktionen" />
|
<ApiEndpoint method="GET" path="/dsfa/audit-log" description="Audit-Trail aller Aktionen" />
|
||||||
<ApiEndpoint method="GET" path="/dsfa/{id}" description="Einzelne DSFA abrufen" />
|
<ApiEndpoint method="GET" path="/dsfa/{id}" description="Einzelne DSFA abrufen" />
|
||||||
<ApiEndpoint method="PUT" path="/dsfa/{id}" description="DSFA aktualisieren (Partial Update)" />
|
<ApiEndpoint method="PUT" path="/dsfa/{id}" description="DSFA aktualisieren (inkl. ai_use_case_modules)" />
|
||||||
<ApiEndpoint method="DELETE" path="/dsfa/{id}" description="DSFA löschen (Art. 17 DSGVO)" />
|
<ApiEndpoint method="DELETE" path="/dsfa/{id}" description="DSFA löschen (Art. 17 DSGVO)" />
|
||||||
<ApiEndpoint method="PATCH" path="/dsfa/{id}/status" description="Schnell-Statuswechsel" />
|
<ApiEndpoint method="PATCH" path="/dsfa/{id}/status" description="Schnell-Statuswechsel" />
|
||||||
|
|
||||||
|
{/* Section 8: AI Use Case Modules */}
|
||||||
|
<h2>Section 8: KI-Anwendungsfälle</h2>
|
||||||
|
<p>
|
||||||
|
Section 8 ist ein optionaler modularer Anhang zur DSFA für KI-spezifische Verarbeitungen.
|
||||||
|
Die Module werden im Feld <code>ai_use_case_modules</code> (JSONB-Array) gespeichert
|
||||||
|
und über den normalen <code>PUT /dsfa/{'{id}'}</code> Endpoint aktualisiert.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<InfoBox type="info" title="KI-Modul-Typen">
|
||||||
|
Unterstützte Typen: <code>chatbot_nlp</code>, <code>recommendation</code>,{' '}
|
||||||
|
<code>adm_scoring</code>, <code>video_image</code>, <code>biometrics</code>,{' '}
|
||||||
|
<code>iot_sensors</code>, <code>generative_ai</code>, <code>custom</code>
|
||||||
|
</InfoBox>
|
||||||
|
|
||||||
|
<h3>KI-Modul hinzufügen</h3>
|
||||||
|
<CodeBlock language="bash">{`# KI-Modul zu bestehender DSFA hinzufügen
|
||||||
|
curl -sk -X PUT 'https://macmini:8002/api/v1/dsfa/{id}' \\
|
||||||
|
-H 'Content-Type: application/json' \\
|
||||||
|
-H 'X-Tenant-ID: default' \\
|
||||||
|
-d '{
|
||||||
|
"ai_use_case_modules": [
|
||||||
|
{
|
||||||
|
"id": "uuid-generated",
|
||||||
|
"use_case_type": "generative_ai",
|
||||||
|
"name": "GPT-Assistent Kundenservice",
|
||||||
|
"model_description": "LLM-basierter Chatbot mit RAG für FAQ-Beantwortung",
|
||||||
|
"model_type": "GPT-4o",
|
||||||
|
"provider": "OpenAI",
|
||||||
|
"third_country_transfer": true,
|
||||||
|
"provider_country": "USA",
|
||||||
|
"input_data_categories": ["Anfragetexte", "Kundennummer"],
|
||||||
|
"output_data_categories": ["Antworttext"],
|
||||||
|
"involves_special_categories": false,
|
||||||
|
"data_subjects": ["Kunden"],
|
||||||
|
"processing_purpose": "Automatisierte Beantwortung von Kundenanfragen",
|
||||||
|
"legal_basis": "Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)",
|
||||||
|
"art22_assessment": { "applies": false, "safeguards": [] },
|
||||||
|
"risk_criteria": [
|
||||||
|
{ "id": "adm_profiling", "applies": false, "severity": "high" }
|
||||||
|
],
|
||||||
|
"ai_act_risk_class": "limited",
|
||||||
|
"ai_act_justification": "Chatbot mit Transparenzpflicht nach Art. 52 AI Act",
|
||||||
|
"risks": [],
|
||||||
|
"mitigations": [],
|
||||||
|
"privacy_by_design_measures": [
|
||||||
|
{ "category": "data_minimisation", "description": "Nur notwendige Daten", "implemented": true }
|
||||||
|
],
|
||||||
|
"review_triggers": [
|
||||||
|
{ "type": "model_update", "description": "Bei Modell-Wechsel", "monitoring_interval": "monatlich" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'`}</CodeBlock>
|
||||||
|
|
||||||
|
<h3>TypeScript-Typen</h3>
|
||||||
|
<CodeBlock language="typescript">{`import type { AIUseCaseModule, AIUseCaseType, AIActRiskClass } from '@breakpilot/compliance-sdk'
|
||||||
|
|
||||||
|
// Modul erstellen
|
||||||
|
const module: AIUseCaseModule = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
use_case_type: 'generative_ai',
|
||||||
|
name: 'Mein KI-System',
|
||||||
|
model_description: 'Beschreibung...',
|
||||||
|
third_country_transfer: false,
|
||||||
|
input_data_categories: ['Nutzertexte'],
|
||||||
|
output_data_categories: ['Antwort'],
|
||||||
|
involves_special_categories: false,
|
||||||
|
data_subjects: ['Endnutzer'],
|
||||||
|
processing_purpose: 'Kundensupport',
|
||||||
|
legal_basis: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||||
|
art22_assessment: { applies: false, safeguards: [] },
|
||||||
|
risk_criteria: [],
|
||||||
|
ai_act_risk_class: 'limited',
|
||||||
|
wp248_criteria_met: ['K4', 'K8'],
|
||||||
|
risks: [],
|
||||||
|
mitigations: [],
|
||||||
|
privacy_by_design_measures: [],
|
||||||
|
review_triggers: [],
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
}`}</CodeBlock>
|
||||||
</DevPortalLayout>
|
</DevPortalLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,3 +260,146 @@ graph LR
|
|||||||
DSFA --> Audit["Audit Checklist"]
|
DSFA --> Audit["Audit Checklist"]
|
||||||
DSFA --> Obligation["Obligations"]
|
DSFA --> Obligation["Obligations"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 8: KI-Anwendungsfälle
|
||||||
|
|
||||||
|
Section 8 ist ein optionaler modularer Anhang zur DSFA für KI-spezifische Verarbeitungen.
|
||||||
|
Jedes KI-System erhält ein eigenes Modul mit 7 Tabs.
|
||||||
|
|
||||||
|
### KI-Modul-Typen
|
||||||
|
|
||||||
|
| Typ | Icon | Typische Risiken |
|
||||||
|
|-----|------|-----------------|
|
||||||
|
| `chatbot_nlp` | 💬 | Datenpersistenz, Fehlinformation |
|
||||||
|
| `recommendation` | 🎯 | Profiling, Verhaltensmanipulation |
|
||||||
|
| `adm_scoring` | ⚖️ | Art. 22 DSGVO, Diskriminierung |
|
||||||
|
| `video_image` | 📹 | Systematische Überwachung |
|
||||||
|
| `biometrics` | 👁️ | Art. 9 DSGVO, Unwiderruflichkeit |
|
||||||
|
| `iot_sensors` | 📡 | Lückenlose Überwachung |
|
||||||
|
| `generative_ai` | 🤖 | Halluzinationen, Datenleckage |
|
||||||
|
| `custom` | ⚙️ | Variabel |
|
||||||
|
|
||||||
|
### Art. 22 DSGVO Assessment
|
||||||
|
|
||||||
|
Für ADM/Scoring-Module wird automatisch ein Art.-22-Panel eingeblendet:
|
||||||
|
- Toggle: Automatisierte Entscheidung mit Rechtswirkung?
|
||||||
|
- Ausnahmetatbestand (Art. 22 Abs. 2 lit. a/b/c)
|
||||||
|
- Schutzmaßnahmen-Checklist (Menschliche Überprüfung Pflicht!)
|
||||||
|
|
||||||
|
### AI Act Risikoklassen
|
||||||
|
|
||||||
|
| Klasse | Label | Anforderungen |
|
||||||
|
|--------|-------|---------------|
|
||||||
|
| `unacceptable` | Unannehmbares Risiko | VERBOTEN |
|
||||||
|
| `high_risk` | Hochrisiko | Art. 9-15 AI Act |
|
||||||
|
| `limited` | Begrenztes Risiko | Transparenz Art. 52 AI Act |
|
||||||
|
| `minimal` | Minimales Risiko | Keine spezifischen AI-Act-Anforderungen |
|
||||||
|
|
||||||
|
### `ai_use_case_modules` JSONB-Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"use_case_type": "generative_ai",
|
||||||
|
"name": "GPT-Assistent Kundenservice",
|
||||||
|
"model_description": "LLM-basierter Chatbot mit RAG...",
|
||||||
|
"model_type": "GPT-4o",
|
||||||
|
"provider": "OpenAI",
|
||||||
|
"third_country_transfer": true,
|
||||||
|
"provider_country": "USA",
|
||||||
|
"input_data_categories": ["Anfragetexte", "Kundennummer"],
|
||||||
|
"output_data_categories": ["Antworttext", "Intent-Klassifikation"],
|
||||||
|
"involves_special_categories": false,
|
||||||
|
"data_subjects": ["Kunden", "Interessenten"],
|
||||||
|
"processing_purpose": "Automatisierte Beantwortung von Kundenanfragen",
|
||||||
|
"legal_basis": "Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)",
|
||||||
|
"art22_assessment": {
|
||||||
|
"applies": false,
|
||||||
|
"safeguards": []
|
||||||
|
},
|
||||||
|
"risk_criteria": [
|
||||||
|
{ "id": "adm_profiling", "applies": false, "severity": "high" },
|
||||||
|
{ "id": "systematic_monitoring", "applies": false, "severity": "high" }
|
||||||
|
],
|
||||||
|
"ai_act_risk_class": "limited",
|
||||||
|
"ai_act_justification": "Chatbot mit Transparenzpflicht nach Art. 52 AI Act",
|
||||||
|
"risks": [],
|
||||||
|
"mitigations": [],
|
||||||
|
"privacy_by_design_measures": [
|
||||||
|
{ "category": "data_minimisation", "description": "Nur notwendige Daten", "implemented": true }
|
||||||
|
],
|
||||||
|
"review_triggers": [
|
||||||
|
{ "type": "model_update", "description": "Modell-Update", "monitoring_interval": "monatlich" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migration 028 ausführen (auf Mac Mini)
|
||||||
|
ssh macmini "/usr/local/bin/docker exec bp-compliance-backend python3 -c \"
|
||||||
|
import sys; sys.path.insert(0, '/app')
|
||||||
|
from compliance.database import engine
|
||||||
|
from sqlalchemy import text
|
||||||
|
sql = '''
|
||||||
|
ALTER TABLE compliance.compliance_dsfas
|
||||||
|
ADD COLUMN IF NOT EXISTS ai_use_case_modules JSONB DEFAULT '[]'::jsonb,
|
||||||
|
ADD COLUMN IF NOT EXISTS section_8_complete BOOLEAN DEFAULT FALSE;
|
||||||
|
'''
|
||||||
|
with engine.connect() as conn:
|
||||||
|
conn.execute(text(sql))
|
||||||
|
conn.commit()
|
||||||
|
print('Migration 028 OK')
|
||||||
|
\""
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RAG-Corpus bp_dsfa_corpus
|
||||||
|
|
||||||
|
DSFA-spezifische Rechtsdokumente sind im Corpus `bp_dsfa_corpus` indexiert.
|
||||||
|
|
||||||
|
### Ingest-Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Auf Mac Mini ausführen
|
||||||
|
ssh macmini "~/Projekte/breakpilot-compliance/scripts/ingest-dsfa-bundesland.sh"
|
||||||
|
|
||||||
|
# Nur Text-Dokumente (ohne Download)
|
||||||
|
ssh macmini "~/Projekte/breakpilot-compliance/scripts/ingest-dsfa-bundesland.sh --only-text"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bundesland-Behörden (16 Bundesländer + BfDI)
|
||||||
|
|
||||||
|
| ID | Bundesland | Behörde |
|
||||||
|
|----|-----------|---------|
|
||||||
|
| `bfdi` | Bund | Bundesbeauftragte für den Datenschutz (BfDI) |
|
||||||
|
| `bw` | Baden-Württemberg | LfDI BW |
|
||||||
|
| `by` | Bayern | LDA Bayern |
|
||||||
|
| `be` | Berlin | BlnBDI |
|
||||||
|
| `bb` | Brandenburg | LDA Brandenburg |
|
||||||
|
| `hb` | Bremen | LfDI Bremen |
|
||||||
|
| `hh` | Hamburg | HmbBfDI |
|
||||||
|
| `he` | Hessen | HBDI |
|
||||||
|
| `mv` | Mecklenburg-Vorpommern | LfDI MV |
|
||||||
|
| `ni` | Niedersachsen | LfD Niedersachsen |
|
||||||
|
| `nw` | Nordrhein-Westfalen | LDI NRW |
|
||||||
|
| `rp` | Rheinland-Pfalz | LfDI RLP |
|
||||||
|
| `sl` | Saarland | UfD Saarland |
|
||||||
|
| `sn` | Sachsen | SDTB |
|
||||||
|
| `st` | Sachsen-Anhalt | LfD Sachsen-Anhalt |
|
||||||
|
| `sh` | Schleswig-Holstein | ULD |
|
||||||
|
| `th` | Thüringen | TLfDI |
|
||||||
|
|
||||||
|
### Suche im Corpus
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sk -X POST 'https://localhost:8097/api/v1/search' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"query":"DSFA Muss-Liste NRW","collection":"bp_dsfa_corpus","limit":5}'
|
||||||
|
```
|
||||||
|
|||||||
450
scripts/ingest-dsfa-bundesland.sh
Executable file
450
scripts/ingest-dsfa-bundesland.sh
Executable file
@@ -0,0 +1,450 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# ingest-dsfa-bundesland.sh
|
||||||
|
# Ingestiert DSFA-Muss-Listen der 16 Bundesland-Datenschutzbehörden
|
||||||
|
# in den RAG-Corpus bp_dsfa_corpus.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/ingest-dsfa-bundesland.sh [--skip-download] [--only-text]
|
||||||
|
#
|
||||||
|
# Voraussetzung: RAG-Service (document-crawler) erreichbar auf localhost:8097
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RAG_URL="${RAG_URL:-https://localhost:8097}"
|
||||||
|
COLLECTION="bp_dsfa_corpus"
|
||||||
|
DOWNLOAD_DIR="/tmp/dsfa-bundesland-pdfs"
|
||||||
|
SKIP_DOWNLOAD=false
|
||||||
|
ONLY_TEXT=false
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--skip-download) SKIP_DOWNLOAD=true ;;
|
||||||
|
--only-text) ONLY_TEXT=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$DOWNLOAD_DIR"
|
||||||
|
|
||||||
|
log() { echo "[$(date +'%H:%M:%S')] $*"; }
|
||||||
|
ok() { echo " ✓ $*"; }
|
||||||
|
warn() { echo " ⚠ $*"; }
|
||||||
|
fail() { echo " ✗ $*" >&2; }
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Phase 1: Behörden-Daten (URLs + Metadaten)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
declare -A AUTHORITY_LABELS=(
|
||||||
|
["bfdi"]="Bundesbeauftragte für den Datenschutz und die Informationsfreiheit"
|
||||||
|
["bw"]="Landesbeauftragte für den Datenschutz und die Informationsfreiheit Baden-Württemberg"
|
||||||
|
["by"]="Bayerisches Landesamt für Datenschutzaufsicht (LDA Bayern)"
|
||||||
|
["be"]="Berliner Beauftragte für Datenschutz und Informationsfreiheit"
|
||||||
|
["bb"]="Die Landesbeauftragte für den Datenschutz und für das Recht auf Akteneinsicht Brandenburg"
|
||||||
|
["hb"]="Die Landesbeauftragte für Datenschutz und Informationsfreiheit Bremen"
|
||||||
|
["hh"]="Der Hamburgische Beauftragte für Datenschutz und Informationsfreiheit"
|
||||||
|
["he"]="Der Hessische Beauftragte für Datenschutz und Informationsfreiheit"
|
||||||
|
["mv"]="Der Landesbeauftragte für Datenschutz und Informationsfreiheit Mecklenburg-Vorpommern"
|
||||||
|
["ni"]="Die Landesbeauftragte für den Datenschutz Niedersachsen"
|
||||||
|
["nw"]="Landesbeauftragte für Datenschutz und Informationsfreiheit NRW"
|
||||||
|
["rp"]="Der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz"
|
||||||
|
["sl"]="Unabhängiges Datenschutzzentrum Saarland"
|
||||||
|
["sn"]="Sächsischer Datenschutz- und Transparenzbeauftragter"
|
||||||
|
["st"]="Landesbeauftragter für den Datenschutz Sachsen-Anhalt"
|
||||||
|
["sh"]="Unabhängiges Landeszentrum für Datenschutz Schleswig-Holstein"
|
||||||
|
["th"]="Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit"
|
||||||
|
)
|
||||||
|
|
||||||
|
declare -A STATE_NAMES=(
|
||||||
|
["bfdi"]="Bund"
|
||||||
|
["bw"]="Baden-Württemberg"
|
||||||
|
["by"]="Bayern"
|
||||||
|
["be"]="Berlin"
|
||||||
|
["bb"]="Brandenburg"
|
||||||
|
["hb"]="Bremen"
|
||||||
|
["hh"]="Hamburg"
|
||||||
|
["he"]="Hessen"
|
||||||
|
["mv"]="Mecklenburg-Vorpommern"
|
||||||
|
["ni"]="Niedersachsen"
|
||||||
|
["nw"]="Nordrhein-Westfalen"
|
||||||
|
["rp"]="Rheinland-Pfalz"
|
||||||
|
["sl"]="Saarland"
|
||||||
|
["sn"]="Sachsen"
|
||||||
|
["st"]="Sachsen-Anhalt"
|
||||||
|
["sh"]="Schleswig-Holstein"
|
||||||
|
["th"]="Thüringen"
|
||||||
|
)
|
||||||
|
|
||||||
|
# PDF-URLs der Muss-Listen (direkte Download-Links)
|
||||||
|
declare -A PDF_URLS=(
|
||||||
|
["bw_privat"]="https://www.baden-wuerttemberg.datenschutz.de/dsfa-muss-liste/"
|
||||||
|
["hh_beide"]="https://datenschutz.hamburg.de/infothek/datenschutz-folgenabschaetzung"
|
||||||
|
["nw_oeffentlich"]="https://www.ldi.nrw.de/datenschutz/datenschutz-folgenabschaetzung"
|
||||||
|
["ni_beide"]="https://lfd.niedersachsen.de/startseite/themen/datenschutzfolgenabschaetzung/"
|
||||||
|
["be_beide"]="https://www.datenschutz-berlin.de/themen/verarbeitungen-mit-hohem-risiko/datenschutz-folgenabschaetzung/"
|
||||||
|
["bfdi_liste"]="https://www.bfdi.bund.de/DE/Fachthemen/Inhalte/Datenschutzbehoerden/DSFA.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Phase 2: Text-Zusammenfassungen (für Bundesländer ohne direkte PDFs)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
create_text_summaries() {
|
||||||
|
log "Erstelle Text-Zusammenfassungen für Bundesländer ohne direkte PDF-URLs..."
|
||||||
|
|
||||||
|
cat > "$DOWNLOAD_DIR/bfdi_muss_liste.txt" << 'EOF'
|
||||||
|
DSFA Muss-Liste - Bundesbeauftragte für den Datenschutz und die Informationsfreiheit (BfDI)
|
||||||
|
|
||||||
|
Gemäß Art. 35 Abs. 4 DSGVO hat die DSK eine Liste von Verarbeitungsvorgängen veröffentlicht,
|
||||||
|
für die eine Datenschutz-Folgenabschätzung durchzuführen ist.
|
||||||
|
|
||||||
|
Verarbeitungsvorgänge für die eine DSFA zwingend erforderlich ist:
|
||||||
|
|
||||||
|
1. Videoüberwachung öffentlich zugänglicher Bereiche
|
||||||
|
- Systematische umfangreiche Überwachung öffentlich zugänglicher Bereiche
|
||||||
|
- Betreiber: öffentliche und nicht-öffentliche Stellen
|
||||||
|
|
||||||
|
2. Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
|
||||||
|
- Gesundheitsdaten bei Versicherungen, Banken, Arbeitgebern
|
||||||
|
- Genetische Daten zu Diagnose- und Behandlungszwecken
|
||||||
|
- Biometrische Daten zur eindeutigen Identifizierung
|
||||||
|
|
||||||
|
3. Mobile personenbezogene Daten
|
||||||
|
- Standortdaten in Kombination mit anderen Daten zu Bewegungsprofilen
|
||||||
|
- Umfangreiche regelmäßige Verarbeitung von Standortdaten
|
||||||
|
|
||||||
|
4. Scoring und automatisierte Entscheidungen (Art. 22 DSGVO)
|
||||||
|
- Bonitätsbewertungen mit erheblichen Auswirkungen
|
||||||
|
- Scoring-Systeme im HR-Bereich
|
||||||
|
- KI-basierte Bewerbungsauswahlverfahren
|
||||||
|
|
||||||
|
5. IoT und Sensordaten
|
||||||
|
- Smarte Geräte mit dauerhafter Verhaltensaufzeichnung
|
||||||
|
- Wearables mit Gesundheits- oder Standortdaten
|
||||||
|
|
||||||
|
6. Datenhändler und Datenbroker
|
||||||
|
- Umfangreiche Zusammenführung personenbezogener Daten aus verschiedenen Quellen
|
||||||
|
- Verarbeitung zum Zweck des Profilings ohne direkte Beziehung zur betroffenen Person
|
||||||
|
|
||||||
|
Quelle: DSK-Positionspapier zur DSFA, BfDI-Orientierungshilfe Art. 35 DSGVO
|
||||||
|
Rechtsgrundlage: Art. 35 Abs. 4 DSGVO, Erwägungsgrund 90
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$DOWNLOAD_DIR/bw_dsfa_anforderungen.txt" << 'EOF'
|
||||||
|
DSFA Muss-Liste - Landesbeauftragte für den Datenschutz Baden-Württemberg (LfDI BW)
|
||||||
|
|
||||||
|
Verarbeitungen mit hohem Risiko - Pflicht zur DSFA:
|
||||||
|
|
||||||
|
Öffentlicher Bereich:
|
||||||
|
1. Verarbeitung von Sozialdaten in großem Umfang (SGB-Verarbeitungen)
|
||||||
|
2. Biometrische Daten zur eindeutigen Identifizierung von natürlichen Personen
|
||||||
|
3. Genetische Daten in der medizinischen Versorgung
|
||||||
|
4. Systematische Überwachung öffentlicher Bereiche (Videoüberwachung)
|
||||||
|
5. KI-gestützte Verwaltungsentscheidungen (automatisierte Bescheide)
|
||||||
|
6. Scoring-Systeme im Sozialbereich
|
||||||
|
|
||||||
|
Nicht-öffentlicher Bereich:
|
||||||
|
1. Verarbeitung besonderer Kategorien nach Art. 9 DSGVO in großem Umfang
|
||||||
|
2. Bonitätsprüfungen und Credit Scoring
|
||||||
|
3. Systematische Überwachung von Beschäftigten
|
||||||
|
4. KI-Systeme in Bewerbungsverfahren
|
||||||
|
5. Telematikdaten im Versicherungswesen
|
||||||
|
6. Location-based Services mit Bewegungsprofil
|
||||||
|
|
||||||
|
Schwellenwerte (WP248-Kriterien):
|
||||||
|
- 2 von 9 Kriterien erfüllt: DSFA in der Regel erforderlich
|
||||||
|
- 1 Kriterium: Einzelfallprüfung
|
||||||
|
|
||||||
|
Quelle: LfDI BW Orientierungshilfe DSFA, Stand 2023
|
||||||
|
Rechtsgrundlage: Art. 35 DSGVO i.V.m. § 67 BDSG
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$DOWNLOAD_DIR/nrw_dsfa_anforderungen.txt" << 'EOF'
|
||||||
|
DSFA Muss-Liste - Landesbeauftragte für Datenschutz und Informationsfreiheit NRW (LDI NRW)
|
||||||
|
|
||||||
|
Pflichtliste gem. Art. 35 Abs. 4 DSGVO für den öffentlichen Bereich NRW:
|
||||||
|
|
||||||
|
1. Verarbeitung von Sozialdaten
|
||||||
|
- Jobcenter und Sozialämter: Leistungsgewährung, Sachverhaltsermittlung
|
||||||
|
- Jugendhilfeverwaltung mit sensitiven Daten
|
||||||
|
|
||||||
|
2. Videoüberwachung
|
||||||
|
- Öffentliche Gebäude und Plätze mit dauerhafter Aufzeichnung
|
||||||
|
- Automatische Kennzeichenerfassung
|
||||||
|
|
||||||
|
3. Automatisierte Einzelentscheidungen
|
||||||
|
- Verwaltungsakte auf Basis von Algorithmen
|
||||||
|
- KI-gestützte Risikoanalysen in der Verwaltung
|
||||||
|
|
||||||
|
4. Biometrische und genetische Daten
|
||||||
|
- Verarbeitung zur Identifizierung in öffentlichen Bereichen
|
||||||
|
|
||||||
|
5. Gesundheitsdaten
|
||||||
|
- Umfangreiche Verarbeitung in Gesundheitsämtern
|
||||||
|
- Elektronische Patientenakten kommunaler Träger
|
||||||
|
|
||||||
|
Nicht-öffentlicher Bereich (Orientierung):
|
||||||
|
- Kreditwürdigkeit und Zuverlässigkeit
|
||||||
|
- Umfangreiche Tracking-Systeme
|
||||||
|
- Personalverwaltungs-Scoring
|
||||||
|
|
||||||
|
Quelle: LDI NRW Orientierungshilfe, Positionspapier zur DSFA
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$DOWNLOAD_DIR/by_dsfa_anforderungen.txt" << 'EOF'
|
||||||
|
DSFA Muss-Liste - Bayerisches Landesamt für Datenschutzaufsicht (LDA Bayern)
|
||||||
|
Zuständig für nicht-öffentliche Stellen in Bayern
|
||||||
|
|
||||||
|
Verarbeitungen für die eine DSFA erforderlich ist:
|
||||||
|
|
||||||
|
1. Scoring und Rating
|
||||||
|
- SCHUFA-ähnliche Bonitätsbewertungen
|
||||||
|
- Scoring für Versicherungstarife
|
||||||
|
- Mitarbeiter-Scoring und -bewertung
|
||||||
|
|
||||||
|
2. Targeting und Profiling
|
||||||
|
- Verhaltensbasierte Werbung in großem Umfang
|
||||||
|
- Psychographisches Profiling
|
||||||
|
- Cross-Device Tracking
|
||||||
|
|
||||||
|
3. Besondere Datenkategorien (Art. 9 DSGVO)
|
||||||
|
- Gesundheits-Apps mit sensitiven Daten
|
||||||
|
- Biometrische Zugangssysteme
|
||||||
|
- Religiöse und politische Zugehörigkeit
|
||||||
|
|
||||||
|
4. Innovative Technologien
|
||||||
|
- Smarte Geräte im Consumer-Bereich mit Verhaltensdaten
|
||||||
|
- AR/VR-Anwendungen mit Körperdaten
|
||||||
|
- Generative KI mit Nutzerdaten
|
||||||
|
|
||||||
|
5. Automatisierte Entscheidungen
|
||||||
|
- Kredit- und Versicherungsentscheidungen ohne menschliche Beteiligung
|
||||||
|
- KI-gestützte Personalauswahl
|
||||||
|
|
||||||
|
Quelle: LDA Bayern - Orientierungshilfe DSFA, Prüfliste für Unternehmen
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$DOWNLOAD_DIR/dsfa_wpk248_kriterien.txt" << 'EOF'
|
||||||
|
WP248 rev.01 - Leitlinien zur Datenschutz-Folgenabschätzung (DSFA)
|
||||||
|
Working Party 29 / EDPB
|
||||||
|
|
||||||
|
9 Risikokriterien (K1-K9):
|
||||||
|
|
||||||
|
K1 - Evaluation oder Scoring
|
||||||
|
Einschätzung oder Bewertung von Personen einschließlich Profiling und Vorhersagen.
|
||||||
|
Beispiele: Bonitätsbewertung, Gesundheitsrisiko, KI-Scoring, Personalbeurteilung
|
||||||
|
|
||||||
|
K2 - Automatisierte Entscheidungsfindung mit Rechtswirkung
|
||||||
|
Verarbeitung, die zu Entscheidungen führt, die Auswirkungen auf Personen haben.
|
||||||
|
Art. 22 DSGVO relevant. Beispiele: Kredit, Versicherung, automatisierte Verwaltungsakte
|
||||||
|
|
||||||
|
K3 - Systematische Überwachung
|
||||||
|
Verarbeitung zur Beobachtung, Überwachung oder Kontrolle von betroffenen Personen.
|
||||||
|
Beispiele: Videoüberwachung, IoT-Geräte, Mitarbeiterüberwachung
|
||||||
|
|
||||||
|
K4 - Sensible Daten oder Daten höchstpersönlichen Charakters
|
||||||
|
Art. 9 und 10 DSGVO: Gesundheit, Biometrie, Genetik, Religion, Strafverfolgung
|
||||||
|
|
||||||
|
K5 - Datenverarbeitung in großem Umfang
|
||||||
|
Mengenmäßig oder qualitativ umfangreiche Verarbeitung.
|
||||||
|
Faktoren: Zahl der Betroffenen, Datenmenge, Dauer, geografische Ausdehnung
|
||||||
|
|
||||||
|
K6 - Abgleich oder Zusammenführung von Datensätzen
|
||||||
|
Kombination aus zwei oder mehr Verarbeitungsvorgängen/Quellen.
|
||||||
|
Beispiele: Data Warehouses, 360°-Kundenprofile
|
||||||
|
|
||||||
|
K7 - Daten über schutzbedürftige Personen
|
||||||
|
Kinder, Ältere, Patienten, Asylsuchende, psychisch Kranke.
|
||||||
|
Besonderes Machtungleichgewicht.
|
||||||
|
|
||||||
|
K8 - Innovative Nutzung oder Anwendung neuer technologischer oder organisatorischer Lösungen
|
||||||
|
Neue Technologien mit unbekannten Risiken: KI, AR/VR, IoT, Blockchain
|
||||||
|
|
||||||
|
K9 - Wenn die Verarbeitung selbst die betroffenen Personen an der Ausübung eines Rechts
|
||||||
|
oder der Nutzung einer Dienstleistung bzw. Durchführung eines Vertrags hindert.
|
||||||
|
Beispiele: Fingerabdruckreader als einzige Zutrittsmöglichkeit
|
||||||
|
|
||||||
|
Empfehlung: Bei >= 2 Kriterien ist DSFA in der Regel erforderlich.
|
||||||
|
|
||||||
|
Quelle: Article 29 Working Party, Guidelines on Data Protection Impact Assessment (DPIA)
|
||||||
|
WP248 rev.01 (04/2017, revised 10/2017), endorsed by EDPB
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ok "Text-Zusammenfassungen erstellt in $DOWNLOAD_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Phase 3: Dokument-Ingest in RAG-Corpus
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
ingest_document() {
|
||||||
|
local file="$1"
|
||||||
|
local regulation_id="$2"
|
||||||
|
local state="$3"
|
||||||
|
local authority="$4"
|
||||||
|
local category="${5:-muss_liste}"
|
||||||
|
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
warn "Datei nicht gefunden: $file – überspringe"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local filename
|
||||||
|
filename=$(basename "$file")
|
||||||
|
local ext="${filename##*.}"
|
||||||
|
local mime_type="text/plain"
|
||||||
|
|
||||||
|
if [[ "$ext" == "pdf" ]]; then
|
||||||
|
mime_type="application/pdf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Ingestiere: $filename → $COLLECTION"
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sk -X POST "${RAG_URL}/api/v1/documents/upload" \
|
||||||
|
-H "X-Tenant-ID: default" \
|
||||||
|
-F "file=@${file};type=${mime_type}" \
|
||||||
|
-F "collection=${COLLECTION}" \
|
||||||
|
-F "data_type=compliance" \
|
||||||
|
-F "use_case=legal_reference" \
|
||||||
|
-F "metadata={\"regulation_id\":\"${regulation_id}\",\"state\":\"${state}\",\"category\":\"${category}\",\"authority\":\"${authority}\",\"license\":\"public_law\",\"source\":\"authority_publication\"}" \
|
||||||
|
--connect-timeout 30 \
|
||||||
|
-w "\n%{http_code}" 2>&1 || echo "CONNECTION_ERROR")
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(echo "$response" | tail -1)
|
||||||
|
|
||||||
|
if [[ "$http_code" == "200" || "$http_code" == "201" ]]; then
|
||||||
|
ok "Ingestiert: $filename (HTTP $http_code)"
|
||||||
|
else
|
||||||
|
warn "HTTP $http_code bei $filename – RAG-Service erreichbar?"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Phase 4: Alle Dateien ingestieren
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
ingest_all() {
|
||||||
|
log "Starte Ingest in Corpus: $COLLECTION"
|
||||||
|
log "RAG-URL: $RAG_URL"
|
||||||
|
|
||||||
|
# WP248-Dokument (für alle Bundesländer relevant)
|
||||||
|
ingest_document \
|
||||||
|
"$DOWNLOAD_DIR/dsfa_wpk248_kriterien.txt" \
|
||||||
|
"wp248_rev01" "EU" "Article 29 Working Party / EDPB" "leitlinie"
|
||||||
|
|
||||||
|
# BfDI
|
||||||
|
ingest_document \
|
||||||
|
"$DOWNLOAD_DIR/bfdi_muss_liste.txt" \
|
||||||
|
"muss_liste_bfdi" "Bund" "BfDI" "muss_liste"
|
||||||
|
|
||||||
|
# Baden-Württemberg
|
||||||
|
ingest_document \
|
||||||
|
"$DOWNLOAD_DIR/bw_dsfa_anforderungen.txt" \
|
||||||
|
"muss_liste_bw" "Baden-Württemberg" "LfDI BW" "muss_liste"
|
||||||
|
|
||||||
|
# Bayern
|
||||||
|
ingest_document \
|
||||||
|
"$DOWNLOAD_DIR/by_dsfa_anforderungen.txt" \
|
||||||
|
"muss_liste_by" "Bayern" "LDA Bayern" "muss_liste"
|
||||||
|
|
||||||
|
# NRW
|
||||||
|
ingest_document \
|
||||||
|
"$DOWNLOAD_DIR/nrw_dsfa_anforderungen.txt" \
|
||||||
|
"muss_liste_nw" "Nordrhein-Westfalen" "LDI NRW" "muss_liste"
|
||||||
|
|
||||||
|
# Weitere Bundesländer aus DSFA_AUTHORITY_RESOURCES-Daten (als Text)
|
||||||
|
for state_id in be bb hb hh he mv ni rp sl sn st sh th; do
|
||||||
|
local txt_file="$DOWNLOAD_DIR/${state_id}_dsfa_anforderungen.txt"
|
||||||
|
if [[ ! -f "$txt_file" ]]; then
|
||||||
|
# Generische Zusammenfassung erstellen
|
||||||
|
cat > "$txt_file" << EOF
|
||||||
|
DSFA Anforderungen - ${STATE_NAMES[$state_id]:-$state_id}
|
||||||
|
|
||||||
|
Behörde: ${AUTHORITY_LABELS[$state_id]:-Aufsichtsbehörde $state_id}
|
||||||
|
|
||||||
|
Gemäß Art. 35 DSGVO und den Orientierungshilfen der Datenschutzkonferenz (DSK)
|
||||||
|
ist für folgende Verarbeitungsvorgänge in ${STATE_NAMES[$state_id]:-$state_id} eine
|
||||||
|
Datenschutz-Folgenabschätzung durchzuführen:
|
||||||
|
|
||||||
|
Bundesweit geltende Pflichten:
|
||||||
|
- Verarbeitung besonderer Kategorien (Art. 9 DSGVO) in großem Umfang
|
||||||
|
- Systematische Überwachung öffentlich zugänglicher Bereiche
|
||||||
|
- Automatisierte Entscheidungen mit Rechtswirkung (Art. 22 DSGVO)
|
||||||
|
- Neue Technologien mit unbekannten Risiken
|
||||||
|
- Profiling in großem Umfang
|
||||||
|
|
||||||
|
Für länderspezifische Listen besuchen Sie bitte die Website der Behörde.
|
||||||
|
|
||||||
|
Quelle: DSK-Positionspapier, WP248, Art. 35 Abs. 4 DSGVO
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
ingest_document \
|
||||||
|
"$txt_file" \
|
||||||
|
"muss_liste_${state_id}" \
|
||||||
|
"${STATE_NAMES[$state_id]:-$state_id}" \
|
||||||
|
"${AUTHORITY_LABELS[$state_id]:-Datenschutzbehörde $state_id}" \
|
||||||
|
"muss_liste"
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Ingest abgeschlossen"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Phase 5: Verifikation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
verify_corpus() {
|
||||||
|
log "Verifiziere Corpus $COLLECTION..."
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sk -X POST "${RAG_URL}/api/v1/search" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Tenant-ID: default" \
|
||||||
|
-d "{\"query\":\"DSFA Muss-Liste Bundesland\",\"collection\":\"${COLLECTION}\",\"limit\":3}" \
|
||||||
|
--connect-timeout 15 2>&1 || echo "CONNECTION_ERROR")
|
||||||
|
|
||||||
|
if echo "$response" | grep -q "results"; then
|
||||||
|
local count
|
||||||
|
count=$(echo "$response" | grep -o '"total_results":[0-9]*' | grep -o '[0-9]*' || echo "?")
|
||||||
|
ok "Corpus $COLLECTION erreichbar – $count Ergebnisse für Testsuche"
|
||||||
|
else
|
||||||
|
warn "Konnte Corpus nicht verifizieren. Response: ${response:0:200}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Main
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
log "=== DSFA Bundesland RAG-Ingest ==="
|
||||||
|
log "Collection: $COLLECTION"
|
||||||
|
log "Download-Dir: $DOWNLOAD_DIR"
|
||||||
|
log "Skip-Download: $SKIP_DOWNLOAD"
|
||||||
|
|
||||||
|
# Schritt 1: Text-Zusammenfassungen erstellen (immer)
|
||||||
|
create_text_summaries
|
||||||
|
|
||||||
|
# Schritt 2: PDFs herunterladen (wenn nicht --skip-download)
|
||||||
|
if [[ "$SKIP_DOWNLOAD" == false && "$ONLY_TEXT" == false ]]; then
|
||||||
|
log "PDF-Downloads übersprungen (direkte URLs zu Behörden-PDFs variieren) – nutze Text-Dateien"
|
||||||
|
log "Tipp: Laden Sie PDFs manuell herunter und legen Sie sie in $DOWNLOAD_DIR ab"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Schritt 3: Ingest
|
||||||
|
ingest_all
|
||||||
|
|
||||||
|
# Schritt 4: Verifikation
|
||||||
|
verify_corpus
|
||||||
|
|
||||||
|
log "=== Fertig ==="
|
||||||
|
log "Corpus: $COLLECTION"
|
||||||
|
log "Um zu suchen: curl -sk -X POST '${RAG_URL}/api/v1/search' -H 'Content-Type: application/json' -d '{\"query\":\"DSFA\",\"collection\":\"${COLLECTION}\",\"limit\":5}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user