From 5da93c5d10f98034b6c116677d35426f56b660f4 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 10 Mar 2026 10:29:24 +0100 Subject: [PATCH] feat(regulations): Automatische Ableitung anwendbarer Gesetze & Aufsichtsbehoerden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach Abschluss von Profil + Scope werden jetzt automatisch die anwendbaren Regulierungen (DSGVO, NIS2, AI Act, DORA) ermittelt und die zustaendigen Aufsichtsbehoerden (Landes-DSB, BSI, BaFin) aus Bundesland + Branche abgeleitet. - Neues scope-to-facts.ts: Mapping CompanyProfile+Scope → Go SDK Payload - Neues supervisory-authority-resolver.ts: 16 Landes-DSB + nationale Behoerden - ScopeDecisionTab: Regulierungs-Report mit Aufsichtsbehoerden-Karten - Obligations-Seite: Echte Daten statt Dummy in handleAutoProfiling() - Neue Types: ApplicableRegulation, RegulationAssessmentResult, SupervisoryAuthorityInfo Co-Authored-By: Claude Opus 4.6 --- .../app/sdk/compliance-scope/page.tsx | 64 ++++- admin-compliance/app/sdk/obligations/page.tsx | 77 +++++- .../sdk/compliance-scope/ScopeDecisionTab.tsx | 107 ++++++++- .../lib/sdk/compliance-scope-types.ts | 60 +++++ admin-compliance/lib/sdk/scope-to-facts.ts | 220 ++++++++++++++++++ .../lib/sdk/supervisory-authority-resolver.ts | 153 ++++++++++++ 6 files changed, 665 insertions(+), 16 deletions(-) create mode 100644 admin-compliance/lib/sdk/scope-to-facts.ts create mode 100644 admin-compliance/lib/sdk/supervisory-authority-resolver.ts diff --git a/admin-compliance/app/sdk/compliance-scope/page.tsx b/admin-compliance/app/sdk/compliance-scope/page.tsx index 037d11e..322df33 100644 --- a/admin-compliance/app/sdk/compliance-scope/page.tsx +++ b/admin-compliance/app/sdk/compliance-scope/page.tsx @@ -12,7 +12,9 @@ import { import type { ComplianceScopeState, ScopeProfilingAnswer, - ScopeDecision + ScopeDecision, + ApplicableRegulation, + SupervisoryAuthorityInfo } from '@/lib/sdk/compliance-scope-types' import { createEmptyScopeState, @@ -20,6 +22,8 @@ import { } from '@/lib/sdk/compliance-scope-types' import { complianceScopeEngine } from '@/lib/sdk/compliance-scope-engine' import { getAllQuestions } from '@/lib/sdk/compliance-scope-profiling' +import { buildAssessmentPayload } from '@/lib/sdk/scope-to-facts' +import { resolveAuthorities } from '@/lib/sdk/supervisory-authority-resolver' type TabId = 'overview' | 'wizard' | 'decision' | 'export' @@ -49,6 +53,11 @@ export default function ComplianceScopePage() { const [isLoading, setIsLoading] = useState(true) const [isEvaluating, setIsEvaluating] = useState(false) + // Regulation assessment state + const [applicableRegulations, setApplicableRegulations] = useState([]) + const [supervisoryAuthorities, setSupervisoryAuthorities] = useState([]) + const [regulationAssessmentLoading, setRegulationAssessmentLoading] = useState(false) + // Load from SDK context first (persisted via State API), then localStorage as fallback. // Runs ONCE on mount only — empty deps breaks the dispatch→sdkState→setScopeState→dispatch loop. useEffect(() => { @@ -75,6 +84,14 @@ export default function ComplianceScopePage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // Fetch regulation assessment if decision exists on mount + useEffect(() => { + if (!isLoading && scopeState.decision && applicableRegulations.length === 0 && sdkState.companyProfile) { + fetchRegulationAssessment(scopeState.decision) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]) + // Save to localStorage and SDK context whenever state changes useEffect(() => { if (!isLoading) { @@ -96,6 +113,41 @@ export default function ComplianceScopePage() { })) }, []) + // Fetch regulation assessment from Go AI SDK + const fetchRegulationAssessment = useCallback(async (decision: ScopeDecision) => { + const profile = sdkState.companyProfile + if (!profile) return + + setRegulationAssessmentLoading(true) + try { + const payload = buildAssessmentPayload(profile, scopeState.answers, decision) + const res = await fetch('/api/sdk/v1/ucca/obligations/assess-from-scope', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + + // Set applicable regulations from response + const regs: ApplicableRegulation[] = data.overview?.applicable_regulations || data.applicable_regulations || [] + setApplicableRegulations(regs) + + // Derive supervisory authorities + const regIds = regs.map(r => r.id) + const authorities = resolveAuthorities( + profile.headquartersState, + profile.headquartersCountry || 'DE', + regIds + ) + setSupervisoryAuthorities(authorities) + } catch (error) { + console.error('Failed to fetch regulation assessment:', error) + } finally { + setRegulationAssessmentLoading(false) + } + }, [sdkState.companyProfile, scopeState.answers]) + // Handle evaluate button click const handleEvaluate = useCallback(async () => { setIsEvaluating(true) @@ -112,13 +164,15 @@ export default function ComplianceScopePage() { // Switch to decision tab to show results setActiveTab('decision') + + // Fetch regulation assessment in the background + fetchRegulationAssessment(decision) } catch (error) { console.error('Failed to evaluate compliance scope:', error) - // Optionally show error toast/notification } finally { setIsEvaluating(false) } - }, [scopeState.answers]) + }, [scopeState.answers, fetchRegulationAssessment]) // Handle start profiling from overview const handleStartProfiling = useCallback(() => { @@ -283,6 +337,10 @@ export default function ComplianceScopePage() { canEvaluate={canEvaluate} onEvaluate={handleEvaluate} isEvaluating={isEvaluating} + applicableRegulations={applicableRegulations} + supervisoryAuthorities={supervisoryAuthorities} + regulationAssessmentLoading={regulationAssessmentLoading} + onGoToObligations={() => { window.location.href = '/sdk/obligations' }} /> )} diff --git a/admin-compliance/app/sdk/obligations/page.tsx b/admin-compliance/app/sdk/obligations/page.tsx index 8345479..251a383 100644 --- a/admin-compliance/app/sdk/obligations/page.tsx +++ b/admin-compliance/app/sdk/obligations/page.tsx @@ -4,6 +4,9 @@ import React, { useState, useEffect, useCallback } from 'react' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import TOMControlPanel from '@/components/sdk/obligations/TOMControlPanel' import GapAnalysisView from '@/components/sdk/obligations/GapAnalysisView' +import { useSDK } from '@/lib/sdk' +import { buildAssessmentPayload } from '@/lib/sdk/scope-to-facts' +import type { ApplicableRegulation } from '@/lib/sdk/compliance-scope-types' // ============================================================================= // Types @@ -545,6 +548,7 @@ const REGULATION_CHIPS = [ const UCCA_API = '/api/sdk/v1/ucca/obligations' export default function ObligationsPage() { + const { state: sdkState } = useSDK() const [obligations, setObligations] = useState([]) const [stats, setStats] = useState(null) const [filter, setFilter] = useState('all') @@ -557,6 +561,7 @@ export default function ObligationsPage() { const [detailObligation, setDetailObligation] = useState(null) const [showGapAnalysis, setShowGapAnalysis] = useState(false) const [profiling, setProfiling] = useState(false) + const [applicableRegs, setApplicableRegs] = useState([]) const loadData = useCallback(async () => { setLoading(true) @@ -667,26 +672,44 @@ export default function ObligationsPage() { setProfiling(true) setError(null) try { - const res = await fetch(`${UCCA_API}/assess-from-scope`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ + // Build payload from real CompanyProfile + Scope data + const profile = sdkState.companyProfile + const scopeState = sdkState.complianceScope + const scopeAnswers = scopeState?.answers || [] + const scopeDecision = scopeState?.decision || null + + let payload: Record + if (profile) { + payload = buildAssessmentPayload(profile, scopeAnswers, scopeDecision) as unknown as Record + } else { + // Fallback: Minimaldaten wenn kein Profil vorhanden + payload = { employee_count: 50, industry: 'technology', + country: 'DE', processes_personal_data: true, is_controller: true, uses_processors: true, - processes_special_categories: false, - cross_border_transfer: true, - uses_ai: true, - determined_level: 'L2', - }), + determined_level: scopeDecision?.determinedLevel || 'L2', + } + } + + const res = await fetch(`${UCCA_API}/assess-from-scope`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() - if (data.obligations?.length > 0) { - // Merge auto-profiled obligations into the view - const autoObls: Obligation[] = data.obligations.map((o: Record) => ({ + + // Store applicable regulations for the info box + const regs: ApplicableRegulation[] = data.overview?.applicable_regulations || data.applicable_regulations || [] + setApplicableRegs(regs) + + // Extract obligations from response (can be nested under overview) + const rawObls = data.overview?.obligations || data.obligations || [] + if (rawObls.length > 0) { + const autoObls: Obligation[] = rawObls.map((o: Record) => ({ id: o.id as string, title: o.title as string, description: (o.description as string) || '', @@ -818,6 +841,36 @@ export default function ObligationsPage() {
{error}
)} + {/* Applicable Regulations Info */} + {applicableRegs.length > 0 && ( +
+

Anwendbare Regulierungen (aus Auto-Profiling)

+
+ {applicableRegs.map(reg => ( + + + + + {reg.name} + {reg.classification && ({reg.classification})} + {reg.obligation_count} Pflichten + + ))} +
+
+ )} + + {/* No Profile Warning */} + {!sdkState.companyProfile && ( +
+ Kein Unternehmensprofil vorhanden. Auto-Profiling verwendet Beispieldaten.{' '} + Profil anlegen → +
+ )} + {/* Stats */}
{[ diff --git a/admin-compliance/components/sdk/compliance-scope/ScopeDecisionTab.tsx b/admin-compliance/components/sdk/compliance-scope/ScopeDecisionTab.tsx index 43d716c..58c56ec 100644 --- a/admin-compliance/components/sdk/compliance-scope/ScopeDecisionTab.tsx +++ b/admin-compliance/components/sdk/compliance-scope/ScopeDecisionTab.tsx @@ -1,6 +1,6 @@ 'use client' import React, { useState } from 'react' -import type { ScopeDecision, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types' +import type { ScopeDecision, ComplianceDepthLevel, ApplicableRegulation, SupervisoryAuthorityInfo } from '@/lib/sdk/compliance-scope-types' import { DEPTH_LEVEL_LABELS, DEPTH_LEVEL_DESCRIPTIONS, DEPTH_LEVEL_COLORS, DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' interface ScopeDecisionTabProps { @@ -11,6 +11,10 @@ interface ScopeDecisionTabProps { canEvaluate?: boolean onEvaluate?: () => void isEvaluating?: boolean + applicableRegulations?: ApplicableRegulation[] + supervisoryAuthorities?: SupervisoryAuthorityInfo[] + regulationAssessmentLoading?: boolean + onGoToObligations?: () => void } export function ScopeDecisionTab({ @@ -20,6 +24,10 @@ export function ScopeDecisionTab({ canEvaluate, onEvaluate, isEvaluating, + applicableRegulations, + supervisoryAuthorities, + regulationAssessmentLoading, + onGoToObligations, }: ScopeDecisionTabProps) { const [expandedTrigger, setExpandedTrigger] = useState(null) const [showAuditTrail, setShowAuditTrail] = useState(false) @@ -125,6 +133,103 @@ export function ScopeDecisionTab({
)} + {/* Applicable Regulations */} + {(applicableRegulations || regulationAssessmentLoading) && ( +
+

Anwendbare Regulierungen

+ {regulationAssessmentLoading ? ( +
+ + + + + Regulierungen werden geprueft... +
+ ) : applicableRegulations && applicableRegulations.length > 0 ? ( +
+ {applicableRegulations.map((reg) => ( +
+
+
+ + + +
+
+ {reg.name} + {reg.classification && ( + + {reg.classification} + + )} +
+
+
+ {reg.obligation_count} Pflichten + {reg.control_count > 0 && ( + {reg.control_count} Controls + )} +
+
+ ))} + + {/* Supervisory Authorities */} + {supervisoryAuthorities && supervisoryAuthorities.length > 0 && ( +
+

Zustaendige Aufsichtsbehoerden

+
+ {supervisoryAuthorities.map((sa, idx) => ( +
+
+ + + +
+
+ {sa.authority.abbreviation} + ({sa.domain}) +

{sa.authority.name}

+ {sa.authority.url && ( + + Website → + + )} +
+
+ ))} +
+
+ )} + + {/* Link to Obligations */} + {onGoToObligations && ( +
+ +
+ )} +
+ ) : ( +

Keine anwendbaren Regulierungen ermittelt.

+ )} +
+ )} + {/* Hard Triggers */} {decision.hardTriggers && decision.hardTriggers.length > 0 && (
diff --git a/admin-compliance/lib/sdk/compliance-scope-types.ts b/admin-compliance/lib/sdk/compliance-scope-types.ts index 5cbd2d7..ef3b871 100644 --- a/admin-compliance/lib/sdk/compliance-scope-types.ts +++ b/admin-compliance/lib/sdk/compliance-scope-types.ts @@ -1406,3 +1406,63 @@ export function depthLevelFromNumeric(n: number): ComplianceDepthLevel { }; return map[Math.max(1, Math.min(4, Math.round(n)))] || 'L1'; } + +// ============================================================================ +// Regulation Assessment Types (from Go AI SDK /assess-from-scope) +// ============================================================================ + +/** + * Eine anwendbare Regulierung (aus Go SDK ApplicableRegulation) + */ +export interface ApplicableRegulation { + id: string + name: string + classification: string + reason: string + obligation_count: number + control_count: number +} + +/** + * Ergebnis der Regulierungs-Bewertung vom Go AI SDK + */ +export interface RegulationAssessmentResult { + applicable_regulations: ApplicableRegulation[] + obligations: RegulationObligation[] + executive_summary: { + total_regulations: number + total_obligations: number + critical_obligations: number + compliance_score: number + key_risks: string[] + recommended_actions: string[] + } +} + +/** + * Einzelne Pflicht aus dem Go SDK + */ +export interface RegulationObligation { + id: string + regulation_id: string + title: string + description: string + category: string + responsible: string + priority: string + legal_basis?: Array<{ article: string; name: string }> + how_to_implement?: string + breakpilot_feature?: string +} + +/** + * Aufsichtsbehoerden-Ergebnis + */ +export interface SupervisoryAuthorityInfo { + domain: string + authority: { + name: string + abbreviation: string + url: string + } +} diff --git a/admin-compliance/lib/sdk/scope-to-facts.ts b/admin-compliance/lib/sdk/scope-to-facts.ts new file mode 100644 index 0000000..afa6395 --- /dev/null +++ b/admin-compliance/lib/sdk/scope-to-facts.ts @@ -0,0 +1,220 @@ +/** + * Scope-to-Facts Mapper + * + * Konvertiert CompanyProfile + ScopeProfilingAnswer[] + ScopeDecision + * in das Go AI SDK ScopeDecision-Format fuer POST /assess-from-scope. + */ + +import type { CompanyProfile } from './types' +import type { ScopeProfilingAnswer, ScopeDecision } from './compliance-scope-types' + +/** + * Payload-Format fuer den Go AI SDK /assess-from-scope Endpoint. + * Muss mit ucca.ScopeDecision in scope_facts_mapper.go uebereinstimmen. + */ +export interface ScopeDecisionPayload { + employee_count: number + annual_revenue: number + country: string + industry: string + legal_form: string + processes_personal_data: boolean + is_controller: boolean + is_processor: boolean + data_art9: boolean + data_minors: boolean + large_scale: boolean + systematic_monitoring: boolean + cross_border_transfer: boolean + uses_processors: boolean + automated_decisions: boolean + processes_employee_data: boolean + processes_health_data: boolean + processes_financial_data: boolean + uses_cookies: boolean + uses_tracking: boolean + uses_video_surveillance: boolean + operates_platform: boolean + platform_user_count: number + proc_ai_usage: boolean + is_ai_provider: boolean + is_ai_deployer: boolean + high_risk_ai: boolean + limited_risk_ai: boolean + sector: string + special_services: string[] + is_kritis: boolean + is_financial_institution: boolean + determined_level: string + triggered_rules: string[] + required_documents: string[] + cert_target: string +} + +/** + * Konvertiert CompanyProfile + Scope-Daten in das Go SDK Payload-Format. + */ +export function buildAssessmentPayload( + profile: CompanyProfile, + scopeAnswers: ScopeProfilingAnswer[], + decision: ScopeDecision | null +): ScopeDecisionPayload { + const getBool = (questionId: string): boolean => getAnswerBool(scopeAnswers, questionId) + const getMulti = (questionId: string): string[] => getAnswerMulti(scopeAnswers, questionId) + + const aiCategories = getMulti('ai_categories') + const isAIProvider = aiCategories.includes('ai_provider') || aiCategories.includes('ai_developer') + const isAIDeployer = aiCategories.includes('ai_deployer') || aiCategories.includes('ai_operator') + const aiRisk = getAnswerString(scopeAnswers, 'ai_risk_assessment') + + const industry = Array.isArray(profile.industry) ? profile.industry.join(', ') : (profile.industry || '') + const isFinancial = industry.toLowerCase().includes('finanz') || + industry.toLowerCase().includes('bank') || + industry.toLowerCase().includes('versicherung') || + industry.toLowerCase().includes('financial') + + return { + employee_count: parseEmployeeRange(profile.employeeCount), + annual_revenue: parseRevenueRange(profile.annualRevenue), + country: profile.headquartersCountry || 'DE', + industry, + legal_form: profile.legalForm || '', + processes_personal_data: true, // Jedes Unternehmen im Tool verarbeitet personenbezogene Daten + is_controller: profile.isDataController ?? true, + is_processor: profile.isDataProcessor ?? false, + data_art9: getBool('data_art9'), + data_minors: getBool('data_minors'), + large_scale: parseEmployeeRange(profile.employeeCount) >= 250 || getBool('data_volume'), + systematic_monitoring: getBool('proc_employee_monitoring') || getBool('proc_video_surveillance'), + cross_border_transfer: getBool('tech_third_country'), + uses_processors: getBool('tech_subprocessors'), + automated_decisions: getBool('proc_adm_scoring'), + processes_employee_data: getBool('data_hr'), + processes_health_data: getBool('data_art9'), + processes_financial_data: getBool('data_financial'), + uses_cookies: getBool('prod_cookies_consent'), + uses_tracking: getBool('proc_tracking'), + uses_video_surveillance: getBool('proc_video_surveillance'), + operates_platform: (profile.offerings || []).some(o => + o === 'software_saas' || o === 'app_web' || o === 'app_mobile' + ), + platform_user_count: parseCustomerCount(scopeAnswers), + proc_ai_usage: getBool('ai_uses_ai'), + is_ai_provider: isAIProvider, + is_ai_deployer: isAIDeployer, + high_risk_ai: aiRisk === 'high' || aiRisk === 'unacceptable', + limited_risk_ai: aiRisk === 'limited', + sector: mapIndustryToSector(industry), + special_services: [], + is_kritis: false, // Kann spaeter aus Branche abgeleitet werden + is_financial_institution: isFinancial, + determined_level: decision?.determinedLevel || 'L2', + triggered_rules: decision?.triggeredHardTriggers?.map(t => t.rule.id) || [], + required_documents: decision?.requiredDocuments?.map(d => d.documentType) || [], + cert_target: getAnswerString(scopeAnswers, 'org_cert_target'), + } +} + +// ============================================================================= +// Hilfsfunktionen +// ============================================================================= + +/** Liest eine boolean-Antwort aus den Scope-Antworten */ +function getAnswerBool(answers: ScopeProfilingAnswer[], questionId: string): boolean { + const answer = answers.find(a => a.questionId === questionId) + if (!answer) return false + if (typeof answer.value === 'boolean') return answer.value + if (typeof answer.value === 'string') return answer.value === 'true' || answer.value === 'yes' || answer.value === 'ja' + if (Array.isArray(answer.value)) return answer.value.length > 0 + return false +} + +/** Liest eine Multi-Select-Antwort aus den Scope-Antworten */ +function getAnswerMulti(answers: ScopeProfilingAnswer[], questionId: string): string[] { + const answer = answers.find(a => a.questionId === questionId) + if (!answer) return [] + if (Array.isArray(answer.value)) return answer.value as string[] + if (typeof answer.value === 'string') return [answer.value] + return [] +} + +/** Liest einen String-Wert aus den Scope-Antworten */ +function getAnswerString(answers: ScopeProfilingAnswer[], questionId: string): string { + const answer = answers.find(a => a.questionId === questionId) + if (!answer) return '' + if (typeof answer.value === 'string') return answer.value + if (Array.isArray(answer.value)) return answer.value[0] || '' + return String(answer.value) +} + +/** + * Konvertiert Mitarbeiter-Range-String in einen numerischen Wert (Mittelwert). + * "1-9" → 5, "10-49" → 30, "50-249" → 150, "250-999" → 625, "1000+" → 1500 + */ +export function parseEmployeeRange(range: string | undefined | null): number { + if (!range) return 10 + const r = range.trim() + if (r.includes('1000') || r.includes('+')) return 1500 + if (r.includes('250')) return 625 + if (r.includes('50')) return 150 + if (r.includes('10')) return 30 + return 5 +} + +/** + * Konvertiert Umsatz-Range-String in einen numerischen Wert. + * "< 2 Mio" → 1000000, "2-10 Mio" → 6000000, "10-50 Mio" → 30000000, "> 50 Mio" → 75000000 + */ +export function parseRevenueRange(range: string | undefined | null): number { + if (!range) return 1000000 + const r = range.trim().toLowerCase() + if (r.includes('> 50') || r.includes('>50')) return 75000000 + if (r.includes('10-50') || r.includes('10 -')) return 30000000 + if (r.includes('2-10') || r.includes('2 -')) return 6000000 + return 1000000 +} + +/** Liest die Kundenzahl aus den Scope-Antworten */ +function parseCustomerCount(answers: ScopeProfilingAnswer[]): number { + const answer = answers.find(a => a.questionId === 'org_customer_count') + if (!answer) return 0 + if (typeof answer.value === 'number') return answer.value + const str = String(answer.value) + const match = str.match(/\d+/) + return match ? parseInt(match[0], 10) : 0 +} + +/** + * Mappt deutsche Branchenbezeichnungen auf Go SDK Sektor-IDs. + * Diese Sektoren werden fuer NIS2 Annex I/II Pruefung verwendet. + */ +function mapIndustryToSector(industry: string): string { + const lower = industry.toLowerCase() + + // NIS2 Annex I — "Essential" Sektoren + if (lower.includes('energie') || lower.includes('energy')) return 'energy' + if (lower.includes('transport') || lower.includes('logistik')) return 'transport' + if (lower.includes('bank') || lower.includes('finanz') || lower.includes('financial')) return 'banking' + if (lower.includes('versicherung') || lower.includes('insurance')) return 'financial_market' + if (lower.includes('gesundheit') || lower.includes('health') || lower.includes('pharma')) return 'health' + if (lower.includes('wasser') || lower.includes('water')) return 'drinking_water' + if (lower.includes('digital') || lower.includes('cloud') || lower.includes('hosting') || lower.includes('rechenzent')) return 'digital_infrastructure' + if (lower.includes('telekommunikation') || lower.includes('telecom')) return 'digital_infrastructure' + + // NIS2 Annex II — "Important" Sektoren + if (lower.includes('it') || lower.includes('software') || lower.includes('technologie')) return 'ict_service_management' + if (lower.includes('lebensmittel') || lower.includes('food')) return 'food' + if (lower.includes('chemie') || lower.includes('chemical')) return 'chemicals' + if (lower.includes('forschung') || lower.includes('research')) return 'research' + if (lower.includes('post') || lower.includes('kurier')) return 'postal' + if (lower.includes('abfall') || lower.includes('waste')) return 'waste_management' + if (lower.includes('fertigung') || lower.includes('manufactur') || lower.includes('produktion')) return 'manufacturing' + + // Fallback + if (lower.includes('handel') || lower.includes('retail') || lower.includes('e-commerce')) return 'digital_providers' + if (lower.includes('beratung') || lower.includes('consulting')) return 'ict_service_management' + if (lower.includes('bildung') || lower.includes('education')) return 'research' + if (lower.includes('medien') || lower.includes('media')) return 'digital_providers' + + return 'other' +} diff --git a/admin-compliance/lib/sdk/supervisory-authority-resolver.ts b/admin-compliance/lib/sdk/supervisory-authority-resolver.ts new file mode 100644 index 0000000..3f74161 --- /dev/null +++ b/admin-compliance/lib/sdk/supervisory-authority-resolver.ts @@ -0,0 +1,153 @@ +/** + * Supervisory Authority Resolver + * + * Ermittelt automatisch die zustaendigen Aufsichtsbehoerden basierend auf + * Bundesland/Land des Unternehmens und den anwendbaren Regulierungen. + */ + +/** + * Ergebnis der Aufsichtsbehoerden-Ermittlung + */ +export interface SupervisoryAuthorityResult { + domain: string + authority: { + name: string + abbreviation: string + url: string + } +} + +/** + * Mapping: Bundesland-Kuerzel → Landes-Datenschutzbehoerde + */ +const DATA_PROTECTION_AUTHORITIES_DE: Record = { + 'BW': { name: 'Landesbeauftragter fuer den Datenschutz und die Informationsfreiheit Baden-Wuerttemberg', abbreviation: 'LfDI BW', url: 'https://www.baden-wuerttemberg.datenschutz.de' }, + 'BY': { name: 'Bayerisches Landesamt fuer Datenschutzaufsicht', abbreviation: 'BayLDA', url: 'https://www.lda.bayern.de' }, + 'BE': { name: 'Berliner Beauftragte fuer Datenschutz und Informationsfreiheit', abbreviation: 'BlnBDI', url: 'https://www.datenschutz-berlin.de' }, + 'BB': { name: 'Landesbeauftragte fuer den Datenschutz und fuer das Recht auf Akteneinsicht Brandenburg', abbreviation: 'LDA BB', url: 'https://www.lda.brandenburg.de' }, + 'HB': { name: 'Landesbeauftragte fuer Datenschutz und Informationsfreiheit Bremen', abbreviation: 'LfDI HB', url: 'https://www.datenschutz.bremen.de' }, + 'HH': { name: 'Hamburgischer Beauftragter fuer Datenschutz und Informationsfreiheit', abbreviation: 'HmbBfDI', url: 'https://datenschutz-hamburg.de' }, + 'HE': { name: 'Hessischer Beauftragter fuer Datenschutz und Informationsfreiheit', abbreviation: 'HBDI', url: 'https://datenschutz.hessen.de' }, + 'MV': { name: 'Landesbeauftragter fuer Datenschutz und Informationsfreiheit Mecklenburg-Vorpommern', abbreviation: 'LfDI MV', url: 'https://www.datenschutz-mv.de' }, + 'NI': { name: 'Landesbeauftragte fuer den Datenschutz Niedersachsen', abbreviation: 'LfD NI', url: 'https://www.lfd.niedersachsen.de' }, + 'NW': { name: 'Landesbeauftragte fuer Datenschutz und Informationsfreiheit Nordrhein-Westfalen', abbreviation: 'LDI NRW', url: 'https://www.ldi.nrw.de' }, + 'RP': { name: 'Landesbeauftragter fuer den Datenschutz und die Informationsfreiheit Rheinland-Pfalz', abbreviation: 'LfDI RP', url: 'https://www.datenschutz.rlp.de' }, + 'SL': { name: 'Unabhaengiges Datenschutzzentrum Saarland', abbreviation: 'UDZ SL', url: 'https://www.datenschutz.saarland.de' }, + 'SN': { name: 'Saechsischer Datenschutz- und Transparenzbeauftragter', abbreviation: 'SDTB', url: 'https://www.saechsdsb.de' }, + 'ST': { name: 'Landesbeauftragter fuer den Datenschutz Sachsen-Anhalt', abbreviation: 'LfD ST', url: 'https://datenschutz.sachsen-anhalt.de' }, + 'SH': { name: 'Unabhaengiges Landeszentrum fuer Datenschutz Schleswig-Holstein', abbreviation: 'ULD SH', url: 'https://www.datenschutzzentrum.de' }, + 'TH': { name: 'Thueringer Landesbeauftragter fuer den Datenschutz und die Informationsfreiheit', abbreviation: 'TLfDI', url: 'https://www.tlfdi.de' }, +} + +/** + * Nationale Datenschutzbehoerden fuer Nicht-DE-Laender + */ +const DATA_PROTECTION_AUTHORITIES_NATIONAL: Record = { + 'DE': { name: 'Bundesbeauftragter fuer den Datenschutz und die Informationsfreiheit', abbreviation: 'BfDI', url: 'https://www.bfdi.bund.de' }, + 'AT': { name: 'Oesterreichische Datenschutzbehoerde', abbreviation: 'DSB AT', url: 'https://www.dsb.gv.at' }, + 'CH': { name: 'Eidgenoessischer Datenschutz- und Oeffentlichkeitsbeauftragter', abbreviation: 'EDOEB', url: 'https://www.edoeb.admin.ch' }, + 'FR': { name: 'Commission Nationale de l\'Informatique et des Libertes', abbreviation: 'CNIL', url: 'https://www.cnil.fr' }, + 'NL': { name: 'Autoriteit Persoonsgegevens', abbreviation: 'AP', url: 'https://www.autoriteitpersoonsgegevens.nl' }, + 'IT': { name: 'Garante per la protezione dei dati personali', abbreviation: 'Garante', url: 'https://www.garanteprivacy.it' }, + 'ES': { name: 'Agencia Espanola de Proteccion de Datos', abbreviation: 'AEPD', url: 'https://www.aepd.es' }, + 'GB': { name: 'Information Commissioner\'s Office', abbreviation: 'ICO', url: 'https://ico.org.uk' }, +} + +/** + * Ermittelt die Datenschutz-Aufsichtsbehoerde basierend auf Bundesland und Land. + */ +function resolveDataProtectionAuthority( + state: string | undefined, + country: string +): { name: string; abbreviation: string; url: string } { + // Fuer Deutschland: Landes-Datenschutzbehoerde verwenden + if (country === 'DE' && state) { + const stateUpper = state.toUpperCase() + const landesAuth = DATA_PROTECTION_AUTHORITIES_DE[stateUpper] + if (landesAuth) return landesAuth + } + + // Nationale Behoerde + const national = DATA_PROTECTION_AUTHORITIES_NATIONAL[country] + if (national) return national + + // Fallback fuer EU-Laender + return { name: 'Nationale Datenschutzbehoerde', abbreviation: 'DSB', url: '' } +} + +/** + * Ermittelt alle zustaendigen Aufsichtsbehoerden basierend auf + * CompanyProfile-Daten und den anwendbaren Regulierungen. + * + * @param headquartersState - Bundesland-Kuerzel (z.B. "BW", "BY") + * @param headquartersCountry - ISO-Laendercode (z.B. "DE", "AT") + * @param applicableRegulationIds - IDs der anwendbaren Regulierungen aus dem Go SDK + */ +export function resolveAuthorities( + headquartersState: string | undefined, + headquartersCountry: string, + applicableRegulationIds: string[] +): SupervisoryAuthorityResult[] { + const results: SupervisoryAuthorityResult[] = [] + + // Datenschutz-Aufsichtsbehoerde (DSGVO gilt fuer fast alle) + if (applicableRegulationIds.includes('dsgvo')) { + results.push({ + domain: 'Datenschutz', + authority: resolveDataProtectionAuthority(headquartersState, headquartersCountry), + }) + } + + // NIS2 → BSI (fuer Deutschland) + if (applicableRegulationIds.includes('nis2')) { + if (headquartersCountry === 'DE') { + results.push({ + domain: 'IT-Sicherheit (NIS2)', + authority: { + name: 'Bundesamt fuer Sicherheit in der Informationstechnik', + abbreviation: 'BSI', + url: 'https://www.bsi.bund.de', + }, + }) + } else { + results.push({ + domain: 'IT-Sicherheit (NIS2)', + authority: { + name: 'Nationale Cybersicherheitsbehoerde', + abbreviation: 'NCSA', + url: '', + }, + }) + } + } + + // Finanzaufsicht → BaFin + if (applicableRegulationIds.includes('financial_policy')) { + if (headquartersCountry === 'DE') { + results.push({ + domain: 'Finanzaufsicht (DORA/MaRisk)', + authority: { + name: 'Bundesanstalt fuer Finanzdienstleistungsaufsicht', + abbreviation: 'BaFin', + url: 'https://www.bafin.de', + }, + }) + } + } + + // AI Act → Marktueberwachung + if (applicableRegulationIds.includes('ai_act')) { + if (headquartersCountry === 'DE') { + results.push({ + domain: 'KI-Aufsicht (AI Act)', + authority: { + name: 'Bundesnetzagentur (voraussichtlich)', + abbreviation: 'BNetzA', + url: 'https://www.bundesnetzagentur.de', + }, + }) + } + } + + return results +}