feat(regulations): Automatische Ableitung anwendbarer Gesetze & Aufsichtsbehoerden
Some checks failed
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) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
Some checks failed
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) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<ApplicableRegulation[]>([])
|
||||
const [supervisoryAuthorities, setSupervisoryAuthorities] = useState<SupervisoryAuthorityInfo[]>([])
|
||||
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' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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<Obligation[]>([])
|
||||
const [stats, setStats] = useState<ObligationStats | null>(null)
|
||||
const [filter, setFilter] = useState('all')
|
||||
@@ -557,6 +561,7 @@ export default function ObligationsPage() {
|
||||
const [detailObligation, setDetailObligation] = useState<Obligation | null>(null)
|
||||
const [showGapAnalysis, setShowGapAnalysis] = useState(false)
|
||||
const [profiling, setProfiling] = useState(false)
|
||||
const [applicableRegs, setApplicableRegs] = useState<ApplicableRegulation[]>([])
|
||||
|
||||
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<string, unknown>
|
||||
if (profile) {
|
||||
payload = buildAssessmentPayload(profile, scopeAnswers, scopeDecision) as unknown as Record<string, unknown>
|
||||
} 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<string, unknown>) => ({
|
||||
|
||||
// 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<string, unknown>) => ({
|
||||
id: o.id as string,
|
||||
title: o.title as string,
|
||||
description: (o.description as string) || '',
|
||||
@@ -818,6 +841,36 @@ export default function ObligationsPage() {
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 text-sm text-amber-700">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Applicable Regulations Info */}
|
||||
{applicableRegs.length > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-blue-900 mb-2">Anwendbare Regulierungen (aus Auto-Profiling)</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{applicableRegs.map(reg => (
|
||||
<span
|
||||
key={reg.id}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-white border border-blue-300 text-blue-800"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{reg.name}
|
||||
{reg.classification && <span className="text-blue-500">({reg.classification})</span>}
|
||||
<span className="text-blue-400">{reg.obligation_count} Pflichten</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Profile Warning */}
|
||||
{!sdkState.companyProfile && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-700">
|
||||
Kein Unternehmensprofil vorhanden. Auto-Profiling verwendet Beispieldaten.{' '}
|
||||
<a href="/sdk/company-profile" className="underline font-medium">Profil anlegen →</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
|
||||
@@ -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<number | null>(null)
|
||||
const [showAuditTrail, setShowAuditTrail] = useState(false)
|
||||
@@ -125,6 +133,103 @@ export function ScopeDecisionTab({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Applicable Regulations */}
|
||||
{(applicableRegulations || regulationAssessmentLoading) && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Anwendbare Regulierungen</h3>
|
||||
{regulationAssessmentLoading ? (
|
||||
<div className="flex items-center gap-3 text-gray-500">
|
||||
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
<span>Regulierungen werden geprueft...</span>
|
||||
</div>
|
||||
) : applicableRegulations && applicableRegulations.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{applicableRegulations.map((reg) => (
|
||||
<div
|
||||
key={reg.id}
|
||||
className="flex items-center justify-between border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-900">{reg.name}</span>
|
||||
{reg.classification && (
|
||||
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||
{reg.classification}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right text-sm text-gray-600">
|
||||
<span>{reg.obligation_count} Pflichten</span>
|
||||
{reg.control_count > 0 && (
|
||||
<span className="ml-2">{reg.control_count} Controls</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Supervisory Authorities */}
|
||||
{supervisoryAuthorities && supervisoryAuthorities.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-3">Zustaendige Aufsichtsbehoerden</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{supervisoryAuthorities.map((sa, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 bg-gray-50 rounded-lg p-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 bg-blue-100 rounded flex items-center justify-center mt-0.5">
|
||||
<svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-900">{sa.authority.abbreviation}</span>
|
||||
<span className="text-xs text-gray-500 ml-1">({sa.domain})</span>
|
||||
<p className="text-xs text-gray-600 mt-0.5">{sa.authority.name}</p>
|
||||
{sa.authority.url && (
|
||||
<a
|
||||
href={sa.authority.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Website →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link to Obligations */}
|
||||
{onGoToObligations && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={onGoToObligations}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Pflichten anzeigen
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500 text-sm">Keine anwendbaren Regulierungen ermittelt.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hard Triggers */}
|
||||
{decision.hardTriggers && decision.hardTriggers.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
220
admin-compliance/lib/sdk/scope-to-facts.ts
Normal file
220
admin-compliance/lib/sdk/scope-to-facts.ts
Normal file
@@ -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'
|
||||
}
|
||||
153
admin-compliance/lib/sdk/supervisory-authority-resolver.ts
Normal file
153
admin-compliance/lib/sdk/supervisory-authority-resolver.ts
Normal file
@@ -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<string, { name: string; abbreviation: string; url: string }> = {
|
||||
'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<string, { name: string; abbreviation: string; url: string }> = {
|
||||
'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
|
||||
}
|
||||
Reference in New Issue
Block a user