diff --git a/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx b/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx index 79748ed..42be543 100644 --- a/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx +++ b/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx @@ -19,7 +19,21 @@ export function GeneratorWizard({ onClose, onSubmit, prefill }: GeneratorWizardP const [selectedCategories, setSelectedCategories] = useState(prefill?.dataCategories || []) const riskMap2: Record = { niedrig: 'low', mittel: 'medium', hoch: 'high', kritisch: 'critical' } const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>(riskMap2[prefill?.riskLevel || ''] || 'low') + const [residualRisk, setResidualRisk] = useState<'low' | 'medium' | 'high' | 'critical'>('low') const [selectedMeasures, setSelectedMeasures] = useState(prefill?.measures || []) + const [linkedVvtId, setLinkedVvtId] = useState('') + const [vvtActivities, setVvtActivities] = useState>([]) + + // Load VVT activities for linking + React.useEffect(() => { + fetch('/api/sdk/v1/compliance/vvt') + .then(r => r.ok ? r.json() : []) + .then(data => { + const items = Array.isArray(data) ? data : data.activities || [] + setVvtActivities(items.map((a: any) => ({ id: a.id, name: a.name || a.processing_name || a.title || 'Unbenannt' }))) + }) + .catch(() => {}) + }, []) const riskMap: Record = { Niedrig: 'low', Mittel: 'medium', Hoch: 'high', Kritisch: 'critical', @@ -39,6 +53,8 @@ export function GeneratorWizard({ onClose, onSubmit, prefill }: GeneratorWizardP ...(prefill?.federalState ? { federal_state: prefill.federalState } : {}), ...(prefill?.involvesAi ? { involves_ai: true } : {}), ...(prefill?.legalBasis ? { legal_basis: prefill.legalBasis } : {}), + ...(linkedVvtId ? { linked_vvt_id: linkedVvtId } : {}), + ...(residualRisk !== 'low' ? { residual_risk_level: residualRisk } : {}), } as Partial) onClose() } finally { @@ -59,7 +75,7 @@ export function GeneratorWizard({ onClose, onSubmit, prefill }: GeneratorWizardP {/* Progress Steps */}
- {[1, 2, 3, 4].map(s => ( + {[1, 2, 3, 4, 5].map(s => (
) : s}
- {s < 4 &&
} + {s < 5 &&
} ))}
@@ -100,6 +116,20 @@ export function GeneratorWizard({ onClose, onSubmit, prefill }: GeneratorWizardP className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" />
+ {vvtActivities.length > 0 && ( +
+ + +

Ordnen Sie diese DSFA einer VVT-Verarbeitungstaetigkeit zu.

+
+ )}
)} + + {step === 5 && ( +
+ +

+ Bewerten Sie das verbleibende Risiko NACH Umsetzung der Schutzmassnahmen. + Bei hohem Restrisiko → Art. 36 Vorabkonsultation der Aufsichtsbehoerde. +

+
+ {[ + { value: 'low' as const, label: 'Niedrig', desc: 'Risiko ausreichend gemindert', color: 'border-green-300 bg-green-50' }, + { value: 'medium' as const, label: 'Mittel', desc: 'Akzeptables Restrisiko', color: 'border-yellow-300 bg-yellow-50' }, + { value: 'high' as const, label: 'Hoch', desc: 'Art. 36 Konsultation pruefen', color: 'border-orange-300 bg-orange-50' }, + { value: 'critical' as const, label: 'Kritisch', desc: 'Art. 36 Konsultation PFLICHT', color: 'border-red-300 bg-red-50' }, + ].map(r => ( + + ))} +
+ {(residualRisk === 'high' || residualRisk === 'critical') && ( +
+

Vorabkonsultation erforderlich (Art. 36 DSGVO)

+

+ Bei hohem Restrisiko muss die Aufsichtsbehoerde VOR Beginn der Verarbeitung konsultiert werden. +

+
+ )} +
+ )}
{/* Navigation */} @@ -190,11 +257,11 @@ export function GeneratorWizard({ onClose, onSubmit, prefill }: GeneratorWizardP {step === 1 ? 'Abbrechen' : 'Zurueck'} diff --git a/admin-compliance/app/sdk/dsfa/page.tsx b/admin-compliance/app/sdk/dsfa/page.tsx index f31607e..79209f1 100644 --- a/admin-compliance/app/sdk/dsfa/page.tsx +++ b/admin-compliance/app/sdk/dsfa/page.tsx @@ -24,7 +24,10 @@ export default function DSFAPage() { () => prefillDSFAFromScope(state.companyProfile || null, scopeAnswers), [state.companyProfile, scopeAnswers] ) - const dsfaCheck = useMemo(() => isDSFARequired(scopeAnswers), [scopeAnswers]) + const dsfaCheck = useMemo( + () => isDSFARequired(scopeAnswers, state.companyProfile?.headquartersState), + [scopeAnswers, state.companyProfile?.headquartersState] + ) const loadDSFAs = useCallback(async () => { setIsLoading(true) @@ -142,6 +145,21 @@ export default function DSFAPage() { ))} + {dsfaCheck.blacklistMatches.length > 0 && ( +
+

+ Blacklist {dsfaCheck.authority || 'Aufsichtsbehoerde'} (Art. 35 Abs. 4): +

+
    + {dsfaCheck.blacklistMatches.map(m => ( +
  • + + {m} +
  • + ))} +
+
+ )} )} diff --git a/admin-compliance/lib/sdk/dsfa/bundesland-blacklists.ts b/admin-compliance/lib/sdk/dsfa/bundesland-blacklists.ts new file mode 100644 index 0000000..22a677d --- /dev/null +++ b/admin-compliance/lib/sdk/dsfa/bundesland-blacklists.ts @@ -0,0 +1,113 @@ +/** + * DSFA Bundesland-Blacklists — Verarbeitungen die IMMER eine DSFA erfordern. + * + * Jede Aufsichtsbehoerde fuehrt eine eigene Positiv-/Negativliste + * gemaess Art. 35 Abs. 4 DSGVO. Diese Listen definieren Verarbeitungen + * die UNABHAENGIG von der Schwellwertanalyse eine DSFA erfordern. + * + * Quellen: Offizielle Muss-Listen der LfDI/BfDI (oeffentlich zugaenglich). + * KEIN Normtext — eigene Zusammenfassung der Kriterien. + */ + +export interface BlacklistEntry { + id: string + description: string + triggerKeywords: string[] +} + +export interface BundeslandBlacklist { + state: string + stateCode: string + authority: string + authorityUrl: string + entries: BlacklistEntry[] +} + +// Gemeinsame Kriterien die in ALLEN Bundeslaendern gelten (DSK-Liste) +const DSK_COMMON: BlacklistEntry[] = [ + { id: 'dsk-01', description: 'Umfangreiche Verarbeitung besonderer Kategorien (Art. 9)', triggerKeywords: ['art9', 'gesundheit', 'biometrie', 'genetik', 'religion', 'gewerkschaft'] }, + { id: 'dsk-02', description: 'Systematische umfangreiche Ueberwachung oeffentlicher Bereiche', triggerKeywords: ['videoueberwachung', 'kamera', 'oeffentlich', 'ueberwachung'] }, + { id: 'dsk-03', description: 'Scoring/Profiling mit rechtlicher Wirkung', triggerKeywords: ['scoring', 'profiling', 'bonitaet', 'kredit', 'automatisiert'] }, + { id: 'dsk-04', description: 'Verarbeitung von Daten Minderjaehriger fuer Marketing/Profiling', triggerKeywords: ['minderjaehrig', 'kinder', 'schueler', 'marketing'] }, + { id: 'dsk-05', description: 'Zusammenfuehrung von Daten aus verschiedenen Quellen', triggerKeywords: ['zusammenfuehrung', 'matching', 'datenfusion', 'big data'] }, + { id: 'dsk-06', description: 'Einsatz neuer Technologien (KI, Biometrie, IoT)', triggerKeywords: ['ki', 'kuenstliche intelligenz', 'biometrie', 'iot', 'gesichtserkennung'] }, + { id: 'dsk-07', description: 'Umfangreiche Verarbeitung von Standortdaten', triggerKeywords: ['standort', 'gps', 'tracking', 'bewegungsprofil'] }, + { id: 'dsk-08', description: 'Verarbeitung von Beschaeftigtendaten mit Ueberwachungscharakter', triggerKeywords: ['mitarbeiterueberwachung', 'keylogger', 'bildschirmaufnahme', 'leistungskontrolle'] }, + { id: 'dsk-09', description: 'Anonymisierung besonderer Kategorien', triggerKeywords: ['anonymisierung', 'art9', 'pseudonymisierung'] }, + { id: 'dsk-10', description: 'Verarbeitung von Kommunikationsinhalten oder -metadaten', triggerKeywords: ['kommunikation', 'email', 'telefon', 'metadaten', 'inhaltsdaten'] }, +] + +// Bundesland-spezifische Ergaenzungen +const BAYERN_EXTRA: BlacklistEntry[] = [ + { id: 'by-01', description: 'Betrieb von Whistleblower-Systemen mit Identifizierungsrisiko', triggerKeywords: ['whistleblower', 'hinweisgeber', 'meldesystem'] }, + { id: 'by-02', description: 'Einsatz von Drohnen mit Kamera in oeffentlichen Bereichen', triggerKeywords: ['drohne', 'uav', 'luftaufnahme'] }, +] + +const NRW_EXTRA: BlacklistEntry[] = [ + { id: 'nw-01', description: 'Social-Media-Monitoring von Mitarbeitern oder Bewerbern', triggerKeywords: ['social media', 'monitoring', 'bewerber', 'hintergrundcheck'] }, +] + +const BERLIN_EXTRA: BlacklistEntry[] = [ + { id: 'be-01', description: 'Automatisierte Mieterbonitaetspruefung', triggerKeywords: ['mieter', 'bonitaet', 'wohnung', 'schufa'] }, +] + +export const BUNDESLAND_BLACKLISTS: Record = { + BW: { state: 'Baden-Wuerttemberg', stateCode: 'BW', authority: 'LfDI BW', authorityUrl: 'https://www.baden-wuerttemberg.datenschutz.de', entries: [...DSK_COMMON] }, + BY: { state: 'Bayern', stateCode: 'BY', authority: 'BayLDA', authorityUrl: 'https://www.lda.bayern.de', entries: [...DSK_COMMON, ...BAYERN_EXTRA] }, + BE: { state: 'Berlin', stateCode: 'BE', authority: 'BlnBDI', authorityUrl: 'https://www.datenschutz-berlin.de', entries: [...DSK_COMMON, ...BERLIN_EXTRA] }, + BB: { state: 'Brandenburg', stateCode: 'BB', authority: 'LDA BB', authorityUrl: 'https://www.lda.brandenburg.de', entries: [...DSK_COMMON] }, + HB: { state: 'Bremen', stateCode: 'HB', authority: 'LfDI HB', authorityUrl: 'https://www.datenschutz.bremen.de', entries: [...DSK_COMMON] }, + HH: { state: 'Hamburg', stateCode: 'HH', authority: 'HmbBfDI', authorityUrl: 'https://datenschutz-hamburg.de', entries: [...DSK_COMMON] }, + HE: { state: 'Hessen', stateCode: 'HE', authority: 'HBDI', authorityUrl: 'https://datenschutz.hessen.de', entries: [...DSK_COMMON] }, + MV: { state: 'Mecklenburg-Vorpommern', stateCode: 'MV', authority: 'LfDI MV', authorityUrl: 'https://www.datenschutz-mv.de', entries: [...DSK_COMMON] }, + NI: { state: 'Niedersachsen', stateCode: 'NI', authority: 'LfD NI', authorityUrl: 'https://lfd.niedersachsen.de', entries: [...DSK_COMMON] }, + NW: { state: 'Nordrhein-Westfalen', stateCode: 'NW', authority: 'LDI NRW', authorityUrl: 'https://www.ldi.nrw.de', entries: [...DSK_COMMON, ...NRW_EXTRA] }, + RP: { state: 'Rheinland-Pfalz', stateCode: 'RP', authority: 'LfDI RP', authorityUrl: 'https://www.datenschutz.rlp.de', entries: [...DSK_COMMON] }, + SL: { state: 'Saarland', stateCode: 'SL', authority: 'UDZ Saarland', authorityUrl: 'https://www.datenschutz.saarland.de', entries: [...DSK_COMMON] }, + SN: { state: 'Sachsen', stateCode: 'SN', authority: 'SDB', authorityUrl: 'https://www.saechsdsb.de', entries: [...DSK_COMMON] }, + ST: { state: 'Sachsen-Anhalt', stateCode: 'ST', authority: 'LfD LSA', authorityUrl: 'https://datenschutz.sachsen-anhalt.de', entries: [...DSK_COMMON] }, + SH: { state: 'Schleswig-Holstein', stateCode: 'SH', authority: 'ULD SH', authorityUrl: 'https://www.datenschutzzentrum.de', entries: [...DSK_COMMON] }, + TH: { state: 'Thueringen', stateCode: 'TH', authority: 'TLfDI', authorityUrl: 'https://www.tlfdi.de', entries: [...DSK_COMMON] }, +} + +/** + * Check scope answers against Bundesland blacklist. + * Returns matching entries that REQUIRE a DSFA. + */ +export function checkBlacklist( + stateCode: string, + scopeKeywords: string[], +): { matches: BlacklistEntry[]; authority: string; authorityUrl: string } { + const bl = BUNDESLAND_BLACKLISTS[stateCode] + if (!bl) return { matches: [], authority: 'BfDI', authorityUrl: 'https://www.bfdi.bund.de' } + + const lowerKeywords = scopeKeywords.map(k => k.toLowerCase()) + const matches = bl.entries.filter(entry => + entry.triggerKeywords.some(tk => lowerKeywords.some(kw => kw.includes(tk) || tk.includes(kw))) + ) + + return { matches, authority: bl.authority, authorityUrl: bl.authorityUrl } +} + +/** + * Derive keywords from scope answers for blacklist matching. + */ +export function scopeAnswersToKeywords(answers: Array<{ questionId: string; value: unknown }>): string[] { + const keywords: string[] = [] + for (const a of answers) { + if (a.value === true || a.value === 'yes') { + keywords.push(a.questionId.replace(/_/g, ' ')) + // Map specific question IDs to keywords + if (a.questionId === 'data_art9') keywords.push('art9', 'besondere kategorien') + if (a.questionId === 'data_minors') keywords.push('minderjaehrig', 'kinder') + if (a.questionId === 'proc_adm_scoring') keywords.push('scoring', 'profiling', 'automatisiert') + if (a.questionId === 'proc_video_surveillance') keywords.push('videoueberwachung', 'kamera') + if (a.questionId === 'proc_ai_usage') keywords.push('ki', 'kuenstliche intelligenz') + if (a.questionId === 'proc_employee_monitoring') keywords.push('mitarbeiterueberwachung') + } + if (Array.isArray(a.value)) { + for (const v of a.value) keywords.push(String(v).toLowerCase()) + } + } + return keywords +} diff --git a/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts b/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts index 03ae31d..3e0afeb 100644 --- a/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts +++ b/admin-compliance/lib/sdk/dsfa/prefill-from-scope.ts @@ -173,11 +173,17 @@ export function prefillDSFAFromScope( } /** - * Check if DSFA is required based on scope answers (Art. 35 Abs. 3 triggers). + * Check if DSFA is required based on scope answers (Art. 35 Abs. 3 triggers) + * AND Bundesland-specific blacklist (Art. 35 Abs. 4). */ -export function isDSFARequired(scopeAnswers: ScopeProfilingAnswer[]): { +export function isDSFARequired( + scopeAnswers: ScopeProfilingAnswer[], + headquartersState?: string, +): { required: boolean triggers: string[] + blacklistMatches: string[] + authority?: string } { const triggers: string[] = [] @@ -198,5 +204,21 @@ export function isDSFARequired(scopeAnswers: ScopeProfilingAnswer[]): { triggers.push('Umfangreiche Datenverarbeitung (Art. 35 Abs. 3 lit. b)') } - return { required: triggers.length > 0, triggers } + // Bundesland-Blacklist (Art. 35 Abs. 4) + let blacklistMatches: string[] = [] + let authority: string | undefined + if (headquartersState) { + const { checkBlacklist, scopeAnswersToKeywords } = require('./bundesland-blacklists') + const keywords = scopeAnswersToKeywords(scopeAnswers) + const result = checkBlacklist(headquartersState, keywords) + blacklistMatches = result.matches.map((m: { description: string }) => m.description) + authority = result.authority + } + + return { + required: triggers.length > 0 || blacklistMatches.length > 0, + triggers, + blacklistMatches, + authority, + } }