From e6d666b89beb432eed85f696df1df9049b3f2f1d Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 2 Mar 2026 11:04:31 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Vorbereitung-Module=20auf=20100%=20?= =?UTF-8?q?=E2=80=94=20Persistenz,=20Backend-Services,=20UCCA=20Frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase A: PostgreSQL State Store (sdk_states Tabelle, InMemory-Fallback) Phase B: Modules dynamisch vom Backend, Scope DB-Persistenz, Source Policy State Phase C: UCCA Frontend (3 Seiten, Wizard, RiskScoreGauge), Obligations Live-Daten Phase D: Document Import (PDF/LLM/Gap-Analyse), System Screening (SBOM/OSV.dev) Phase E: Company Profile CRUD mit Audit-Logging Phase F: Tests (Python + TypeScript), flow-data.ts DB-Tabellen aktualisiert Co-Authored-By: Claude Opus 4.6 --- .../(admin)/development/sdk-flow/flow-data.ts | 30 +- .../app/(sdk)/sdk/compliance-scope/page.tsx | 26 +- .../app/(sdk)/sdk/import/page.tsx | 175 +++-- .../app/(sdk)/sdk/modules/page.tsx | 103 ++- .../app/(sdk)/sdk/obligations/page.tsx | 200 +++--- .../app/(sdk)/sdk/screening/page.tsx | 268 ++++---- .../app/(sdk)/sdk/use-cases/[id]/page.tsx | 179 ++++++ .../app/(sdk)/sdk/use-cases/new/page.tsx | 464 +++++++++++++ .../app/(sdk)/sdk/use-cases/page.tsx | 210 ++++++ .../app/api/sdk/v1/company-profile/route.ts | 81 +++ .../app/api/sdk/v1/import/analyze/route.ts | 36 ++ .../app/api/sdk/v1/modules/route.ts | 56 ++ .../app/api/sdk/v1/screening/scan/route.ts | 36 ++ .../app/api/sdk/v1/state/route.ts | 172 ++--- .../app/api/sdk/v1/ucca/assess/route.ts | 41 ++ .../app/api/sdk/v1/ucca/assessments/route.ts | 48 ++ .../components/layout/Sidebar.tsx | 2 +- .../AssessmentResultCard.tsx | 154 +++++ .../use-case-assessment/RiskScoreGauge.tsx | 85 +++ .../lib/sdk/__tests__/api-client.test.ts | 110 ++++ admin-compliance/lib/sdk/api-client.ts | 147 +++++ admin-compliance/lib/sdk/context.tsx | 3 + .../__tests__/state-projector.test.ts | 1 + admin-compliance/lib/sdk/types.ts | 8 + admin-compliance/package.json | 1 + .../compliance/api/company_profile_routes.py | 344 ++++++++++ .../compliance/api/import_routes.py | 380 +++++++++++ .../compliance/api/screening_routes.py | 608 ++++++++++++++++++ backend-compliance/main.py | 16 + .../migrations/002_sdk_states.sql | 19 + .../migrations/003_document_import.sql | 41 ++ .../migrations/004_screening.sql | 45 ++ .../migrations/005_company_profile.sql | 74 +++ backend-compliance/requirements.txt | 3 + .../tests/test_company_profile_routes.py | 134 ++++ .../tests/test_import_routes.py | 123 ++++ .../tests/test_screening_routes.py | 191 ++++++ docker-compose.yml | 1 + 38 files changed, 4195 insertions(+), 420 deletions(-) create mode 100644 admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx create mode 100644 admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx create mode 100644 admin-compliance/app/(sdk)/sdk/use-cases/page.tsx create mode 100644 admin-compliance/app/api/sdk/v1/company-profile/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/import/analyze/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/modules/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/screening/scan/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/ucca/assess/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/ucca/assessments/route.ts create mode 100644 admin-compliance/components/sdk/use-case-assessment/AssessmentResultCard.tsx create mode 100644 admin-compliance/components/sdk/use-case-assessment/RiskScoreGauge.tsx create mode 100644 admin-compliance/lib/sdk/__tests__/api-client.test.ts create mode 100644 backend-compliance/compliance/api/company_profile_routes.py create mode 100644 backend-compliance/compliance/api/import_routes.py create mode 100644 backend-compliance/compliance/api/screening_routes.py create mode 100644 backend-compliance/migrations/002_sdk_states.sql create mode 100644 backend-compliance/migrations/003_document_import.sql create mode 100644 backend-compliance/migrations/004_screening.sql create mode 100644 backend-compliance/migrations/005_company_profile.sql create mode 100644 backend-compliance/tests/test_company_profile_routes.py create mode 100644 backend-compliance/tests/test_import_routes.py create mode 100644 backend-compliance/tests/test_screening_routes.py diff --git a/admin-compliance/app/(admin)/development/sdk-flow/flow-data.ts b/admin-compliance/app/(admin)/development/sdk-flow/flow-data.ts index 91e2013..67f5bc1 100644 --- a/admin-compliance/app/(admin)/development/sdk-flow/flow-data.ts +++ b/admin-compliance/app/(admin)/development/sdk-flow/flow-data.ts @@ -100,8 +100,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: [], outputs: ['companyProfile', 'complianceScope'], prerequisiteSteps: [], - dbTables: [], - dbMode: 'none', + dbTables: ['sdk_states', 'compliance_company_profiles', 'compliance_company_profile_audit'], + dbMode: 'read/write', ragCollections: [], isOptional: false, url: '/sdk/company-profile', @@ -120,8 +120,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: ['companyProfile'], outputs: ['complianceDepthLevel'], prerequisiteSteps: ['company-profile'], - dbTables: [], - dbMode: 'none', + dbTables: ['sdk_states'], + dbMode: 'read/write', ragCollections: [], isOptional: false, url: '/sdk/compliance-scope', @@ -141,12 +141,12 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: ['companyProfile'], outputs: ['useCases'], prerequisiteSteps: ['company-profile'], - dbTables: [], - dbMode: 'none', + dbTables: ['ucca_assessments', 'ucca_findings', 'ucca_controls'], + dbMode: 'read/write', ragCollections: ['bp_compliance_ce'], ragPurpose: 'CE-Regulierungen fuer Use-Case Matching', isOptional: false, - url: '/sdk/advisory-board', + url: '/sdk/use-cases', }, { id: 'import', @@ -159,8 +159,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: ['useCases'], outputs: ['importedDocuments'], prerequisiteSteps: ['use-case-assessment'], - dbTables: [], - dbMode: 'none', + dbTables: ['compliance_imported_documents', 'compliance_gap_analyses'], + dbMode: 'read/write', ragCollections: [], isOptional: true, url: '/sdk/import', @@ -179,8 +179,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: ['useCases'], outputs: ['screening', 'sbom'], prerequisiteSteps: ['use-case-assessment'], - dbTables: [], - dbMode: 'none', + dbTables: ['compliance_screenings', 'compliance_security_issues'], + dbMode: 'read/write', ragCollections: [], isOptional: false, url: '/sdk/screening', @@ -199,8 +199,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: ['companyProfile', 'screening'], outputs: ['modules'], prerequisiteSteps: ['screening'], - dbTables: [], - dbMode: 'none', + dbTables: ['compliance_service_modules', 'sdk_states'], + dbMode: 'read/write', ragCollections: ['bp_compliance_gesetze'], ragPurpose: 'Regulierungen den Modulen zuordnen', isOptional: false, @@ -220,8 +220,8 @@ export const SDK_FLOW_STEPS: SDKFlowStep[] = [ inputs: ['modules'], outputs: ['sourcePolicy'], prerequisiteSteps: ['modules'], - dbTables: [], - dbMode: 'none', + dbTables: ['compliance_source_policies', 'compliance_allowed_sources', 'compliance_pii_field_rules', 'compliance_source_policy_audit'], + dbMode: 'read/write', ragCollections: [], isOptional: false, url: '/sdk/source-policy', diff --git a/admin-compliance/app/(sdk)/sdk/compliance-scope/page.tsx b/admin-compliance/app/(sdk)/sdk/compliance-scope/page.tsx index 73c8872..9dd35a1 100644 --- a/admin-compliance/app/(sdk)/sdk/compliance-scope/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/compliance-scope/page.tsx @@ -49,22 +49,30 @@ export default function ComplianceScopePage() { const [isLoading, setIsLoading] = useState(true) const [isEvaluating, setIsEvaluating] = useState(false) - // Load from localStorage on mount + // Load from SDK context first (persisted via State API), then localStorage as fallback useEffect(() => { try { - const stored = localStorage.getItem(STORAGE_KEY) - if (stored) { - const parsed = JSON.parse(stored) as ComplianceScopeState - setScopeState(parsed) - // Also sync to SDK context - dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed }) + // Priority 1: SDK context (loaded from PostgreSQL via State API) + if (sdkState.complianceScope && sdkState.complianceScope.answers?.length > 0) { + setScopeState(sdkState.complianceScope) + // Also update localStorage for offline fallback + localStorage.setItem(STORAGE_KEY, JSON.stringify(sdkState.complianceScope)) + } else { + // Priority 2: localStorage fallback + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + const parsed = JSON.parse(stored) as ComplianceScopeState + setScopeState(parsed) + // Sync to SDK context for backend persistence + dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed }) + } } } catch (error) { - console.error('Failed to load compliance scope state from localStorage:', error) + console.error('Failed to load compliance scope state:', error) } finally { setIsLoading(false) } - }, [dispatch]) + }, [dispatch, sdkState.complianceScope]) // Save to localStorage and SDK context whenever state changes useEffect(() => { diff --git a/admin-compliance/app/(sdk)/sdk/import/page.tsx b/admin-compliance/app/(sdk)/sdk/import/page.tsx index fc33df2..c8848de 100644 --- a/admin-compliance/app/(sdk)/sdk/import/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/import/page.tsx @@ -358,108 +358,105 @@ export default function ImportPage() { if (files.length === 0) return setIsAnalyzing(true) + const allGaps: GapItem[] = [] - // Simulate upload and analysis for (let i = 0; i < files.length; i++) { const file = files[i] // Update to uploading setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, status: 'uploading' as const } : f))) - // Simulate upload progress - for (let p = 0; p <= 100; p += 20) { - await new Promise(resolve => setTimeout(resolve, 100)) - setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: p } : f))) + // Upload progress + setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 30 } : f))) + + // Prepare form data for backend + const formData = new FormData() + formData.append('file', file.file) + formData.append('document_type', file.type) + formData.append('tenant_id', 'default') + + setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 60, status: 'analyzing' as const } : f))) + + try { + const response = await fetch('/api/sdk/v1/import/analyze', { + method: 'POST', + body: formData, + }) + + if (response.ok) { + const result = await response.json() + + // Create imported document from backend response + const doc: ImportedDocument = { + id: result.document_id || file.id, + name: file.file.name, + type: result.detected_type || file.type, + fileUrl: URL.createObjectURL(file.file), + uploadedAt: new Date(), + analyzedAt: new Date(), + analysisResult: { + detectedType: result.detected_type || file.type, + confidence: result.confidence || 0.85, + extractedEntities: result.extracted_entities || [], + gaps: result.gap_analysis?.gaps || [], + recommendations: result.recommendations || [], + }, + } + + addImportedDocument(doc) + + // Collect gaps + if (result.gap_analysis?.gaps) { + for (const gap of result.gap_analysis.gaps) { + allGaps.push({ + id: gap.id, + category: gap.category, + description: gap.description, + severity: gap.severity, + regulation: gap.regulation, + requiredAction: gap.required_action, + relatedStepId: gap.related_step_id || '', + }) + } + } + + setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 100, status: 'complete' as const } : f))) + } else { + setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, status: 'error' as const, error: 'Analyse fehlgeschlagen' } : f))) + } + } catch { + // Fallback: create basic document without backend analysis + const doc: ImportedDocument = { + id: file.id, + name: file.file.name, + type: file.type, + fileUrl: URL.createObjectURL(file.file), + uploadedAt: new Date(), + analyzedAt: new Date(), + analysisResult: { + detectedType: file.type, + confidence: 0.5, + extractedEntities: [], + gaps: [], + recommendations: ['Backend nicht erreichbar — manuelle Pruefung empfohlen'], + }, + } + addImportedDocument(doc) + setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, progress: 100, status: 'complete' as const } : f))) } - - // Update to analyzing - setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, status: 'analyzing' as const } : f))) - - // Simulate analysis - await new Promise(resolve => setTimeout(resolve, 1000)) - - // Create imported document - const doc: ImportedDocument = { - id: file.id, - name: file.file.name, - type: file.type, - fileUrl: URL.createObjectURL(file.file), - uploadedAt: new Date(), - analyzedAt: new Date(), - analysisResult: { - detectedType: file.type, - confidence: 0.85 + Math.random() * 0.15, - extractedEntities: ['DSGVO', 'AI Act', 'Personenbezogene Daten'], - gaps: [], - recommendations: ['KI-spezifische Klauseln ergaenzen', 'AI Act Anforderungen pruefen'], - }, - } - - addImportedDocument(doc) - - // Update to complete - setFiles(prev => prev.map(f => (f.id === file.id ? { ...f, status: 'complete' as const } : f))) } - // Generate mock gap analysis - const gaps: GapItem[] = [ - { - id: 'gap-1', - category: 'AI Act Compliance', - description: 'Keine Risikoklassifizierung fuer KI-Systeme vorhanden', - severity: 'CRITICAL', - regulation: 'EU AI Act Art. 6', - requiredAction: 'Risikoklassifizierung durchfuehren', - relatedStepId: 'ai-act', - }, - { - id: 'gap-2', - category: 'Transparenz', - description: 'Informationspflichten bei automatisierten Entscheidungen fehlen', - severity: 'HIGH', - regulation: 'DSGVO Art. 13, 14, 22', - requiredAction: 'Datenschutzerklaerung erweitern', - relatedStepId: 'einwilligungen', - }, - { - id: 'gap-3', - category: 'TOMs', - description: 'KI-spezifische technische Massnahmen nicht dokumentiert', - severity: 'MEDIUM', - regulation: 'DSGVO Art. 32', - requiredAction: 'TOMs um KI-Aspekte erweitern', - relatedStepId: 'tom', - }, - { - id: 'gap-4', - category: 'VVT', - description: 'KI-basierte Verarbeitungstaetigkeiten nicht erfasst', - severity: 'HIGH', - regulation: 'DSGVO Art. 30', - requiredAction: 'VVT aktualisieren', - relatedStepId: 'vvt', - }, - { - id: 'gap-5', - category: 'Aufsicht', - description: 'Menschliche Aufsicht nicht definiert', - severity: 'MEDIUM', - regulation: 'EU AI Act Art. 14', - requiredAction: 'Aufsichtsprozesse definieren', - relatedStepId: 'controls', - }, - ] - + // Build gap analysis summary const gapAnalysis: GapAnalysis = { id: `analysis-${Date.now()}`, createdAt: new Date(), - totalGaps: gaps.length, - criticalGaps: gaps.filter(g => g.severity === 'CRITICAL').length, - highGaps: gaps.filter(g => g.severity === 'HIGH').length, - mediumGaps: gaps.filter(g => g.severity === 'MEDIUM').length, - lowGaps: gaps.filter(g => g.severity === 'LOW').length, - gaps, - recommendedPackages: ['analyse', 'dokumentation'], + totalGaps: allGaps.length, + criticalGaps: allGaps.filter(g => g.severity === 'CRITICAL').length, + highGaps: allGaps.filter(g => g.severity === 'HIGH').length, + mediumGaps: allGaps.filter(g => g.severity === 'MEDIUM').length, + lowGaps: allGaps.filter(g => g.severity === 'LOW').length, + gaps: allGaps, + recommendedPackages: allGaps.length > 0 ? ['analyse', 'dokumentation'] : [], } setAnalysisResult(gapAnalysis) diff --git a/admin-compliance/app/(sdk)/sdk/modules/page.tsx b/admin-compliance/app/(sdk)/sdk/modules/page.tsx index 6ce1e3d..58fa6af 100644 --- a/admin-compliance/app/(sdk)/sdk/modules/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/modules/page.tsx @@ -19,11 +19,26 @@ interface DisplayModule extends ServiceModule { completionPercent: number } +interface BackendModule { + id: string + name: string + display_name: string + description: string + service_type: string | null + processes_pii: boolean + ai_components: boolean + criticality: string + is_active: boolean + compliance_score: number | null + regulation_count: number + risk_count: number +} + // ============================================================================= -// AVAILABLE MODULES (Templates) +// FALLBACK MODULES (used when backend is unavailable) // ============================================================================= -const availableModules: Omit[] = [ +const fallbackModules: Omit[] = [ { id: 'mod-gdpr', name: 'DSGVO Compliance', @@ -74,6 +89,34 @@ const availableModules: Omit[] = }, ] +// ============================================================================= +// HELPERS +// ============================================================================= + +function categorizeModule(name: string): ModuleCategory { + const lower = name.toLowerCase() + if (lower.includes('dsgvo') || lower.includes('gdpr') || lower.includes('datenschutz')) return 'gdpr' + if (lower.includes('ai act') || lower.includes('ki-verordnung')) return 'ai-act' + if (lower.includes('iso 27001') || lower.includes('iso27001') || lower.includes('isms')) return 'iso27001' + if (lower.includes('nis2') || lower.includes('netz- und informations')) return 'nis2' + return 'custom' +} + +function mapBackendToDisplay(m: BackendModule): Omit { + return { + id: m.id, + name: m.display_name || m.name, + description: m.description || '', + category: categorizeModule(m.display_name || m.name), + regulations: [], + criticality: (m.criticality || 'MEDIUM').toUpperCase(), + processesPersonalData: m.processes_pii, + hasAIComponents: m.ai_components, + requirementsCount: m.regulation_count || 0, + controlsCount: m.risk_count || 0, + } +} + // ============================================================================= // COMPONENTS // ============================================================================= @@ -124,13 +167,15 @@ function ModuleCard({

{module.name}

{module.description}

-
- {module.regulations.map(reg => ( - - {reg} - - ))} -
+ {module.regulations.length > 0 && ( +
+ {module.regulations.map(reg => ( + + {reg} + + ))} +
+ )} @@ -193,6 +238,33 @@ function ModuleCard({ export default function ModulesPage() { const { state, dispatch } = useSDK() const [filter, setFilter] = useState('all') + const [availableModules, setAvailableModules] = useState[]>(fallbackModules) + const [isLoadingModules, setIsLoadingModules] = useState(true) + const [backendError, setBackendError] = useState(null) + + // Load modules from backend + useEffect(() => { + async function loadModules() { + try { + const response = await fetch('/api/sdk/v1/modules') + if (response.ok) { + const data = await response.json() + if (data.modules && data.modules.length > 0) { + const mapped = data.modules.map(mapBackendToDisplay) + setAvailableModules(mapped) + setBackendError(null) + } + } else { + setBackendError('Backend nicht erreichbar — zeige Standard-Module') + } + } catch { + setBackendError('Backend nicht erreichbar — zeige Standard-Module') + } finally { + setIsLoadingModules(false) + } + } + loadModules() + }, []) // Convert SDK modules to display modules with additional UI properties const displayModules: DisplayModule[] = availableModules.map(template => { @@ -243,7 +315,6 @@ export default function ModulesPage() { } const handleDeactivateModule = (moduleId: string) => { - // Remove module by updating state without it const updatedModules = state.modules.filter(m => m.id !== moduleId) dispatch({ type: 'SET_STATE', payload: { modules: updatedModules } }) } @@ -268,6 +339,18 @@ export default function ModulesPage() { + {/* Backend Status */} + {backendError && ( +
+ {backendError} +
+ )} + + {/* Loading */} + {isLoadingModules && ( +
Lade Module vom Backend...
+ )} + {/* Stats */}
diff --git a/admin-compliance/app/(sdk)/sdk/obligations/page.tsx b/admin-compliance/app/(sdk)/sdk/obligations/page.tsx index 8cd70e0..4e061bb 100644 --- a/admin-compliance/app/(sdk)/sdk/obligations/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/obligations/page.tsx @@ -1,6 +1,6 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' @@ -21,73 +21,6 @@ interface Obligation { linkedSystems: string[] } -// ============================================================================= -// MOCK DATA -// ============================================================================= - -const mockObligations: Obligation[] = [ - { - id: 'obl-1', - title: 'Risikomanagementsystem implementieren', - description: 'Ein Risikomanagementsystem fuer das Hochrisiko-KI-System muss implementiert werden.', - source: 'AI Act', - sourceArticle: 'Art. 9', - deadline: new Date('2024-06-01'), - status: 'in-progress', - priority: 'critical', - responsible: 'IT Security', - linkedSystems: ['Bewerber-Screening'], - }, - { - id: 'obl-2', - title: 'Technische Dokumentation erstellen', - description: 'Umfassende technische Dokumentation fuer alle Hochrisiko-KI-Systeme.', - source: 'AI Act', - sourceArticle: 'Art. 11', - deadline: new Date('2024-05-15'), - status: 'pending', - priority: 'high', - responsible: 'Entwicklung', - linkedSystems: ['Bewerber-Screening'], - }, - { - id: 'obl-3', - title: 'Datenschutzerklaerung aktualisieren', - description: 'Die Datenschutzerklaerung muss an die neuen KI-Verarbeitungen angepasst werden.', - source: 'DSGVO', - sourceArticle: 'Art. 13/14', - deadline: new Date('2024-02-01'), - status: 'overdue', - priority: 'high', - responsible: 'Datenschutz', - linkedSystems: ['Kundenservice Chatbot', 'Empfehlungsalgorithmus'], - }, - { - id: 'obl-4', - title: 'KI-Kennzeichnung implementieren', - description: 'Nutzer muessen informiert werden, dass sie mit einem KI-System interagieren.', - source: 'AI Act', - sourceArticle: 'Art. 52', - deadline: new Date('2024-03-01'), - status: 'completed', - priority: 'medium', - responsible: 'UX Team', - linkedSystems: ['Kundenservice Chatbot'], - }, - { - id: 'obl-5', - title: 'Menschliche Aufsicht sicherstellen', - description: 'Prozesse fuer menschliche Aufsicht bei automatisierten Entscheidungen.', - source: 'AI Act', - sourceArticle: 'Art. 14', - deadline: new Date('2024-04-01'), - status: 'pending', - priority: 'critical', - responsible: 'Operations', - linkedSystems: ['Bewerber-Screening'], - }, -] - // ============================================================================= // COMPONENTS // ============================================================================= @@ -188,14 +121,124 @@ function ObligationCard({ obligation }: { obligation: Obligation }) { ) } +// ============================================================================= +// HELPERS +// ============================================================================= + +function mapControlsToObligations(assessments: Array<{ + id: string + title?: string + domain?: string + result?: { + required_controls?: Array<{ + id: string + title: string + description: string + gdpr_ref?: string + effort?: string + }> + triggered_rules?: Array<{ + rule_code: string + title: string + severity: string + gdpr_ref: string + }> + risk_level?: string + } +}>): Obligation[] { + const obligations: Obligation[] = [] + + for (const assessment of assessments) { + // Map triggered rules to obligations + const rules = assessment.result?.triggered_rules || [] + for (const rule of rules) { + const severity = rule.severity + obligations.push({ + id: `${assessment.id}-${rule.rule_code}`, + title: rule.title, + description: `Aus Assessment: ${assessment.title || assessment.id.slice(0, 8)}`, + source: rule.gdpr_ref?.includes('AI Act') ? 'AI Act' : 'DSGVO', + sourceArticle: rule.gdpr_ref || '', + deadline: null, + status: 'pending', + priority: severity === 'BLOCK' ? 'critical' : severity === 'WARN' ? 'high' : 'medium', + responsible: 'Compliance Team', + linkedSystems: assessment.title ? [assessment.title] : [], + }) + } + + // Map required controls to obligations + const controls = assessment.result?.required_controls || [] + for (const control of controls) { + obligations.push({ + id: `${assessment.id}-ctrl-${control.id}`, + title: control.title, + description: control.description, + source: control.gdpr_ref?.includes('AI Act') ? 'AI Act' : 'DSGVO', + sourceArticle: control.gdpr_ref || '', + deadline: null, + status: 'pending', + priority: assessment.result?.risk_level === 'HIGH' || assessment.result?.risk_level === 'UNACCEPTABLE' ? 'high' : 'medium', + responsible: 'IT / Compliance', + linkedSystems: assessment.title ? [assessment.title] : [], + }) + } + } + + return obligations +} + // ============================================================================= // MAIN PAGE // ============================================================================= export default function ObligationsPage() { const { state } = useSDK() - const [obligations] = useState(mockObligations) + const [obligations, setObligations] = useState([]) const [filter, setFilter] = useState('all') + const [loading, setLoading] = useState(true) + const [backendAvailable, setBackendAvailable] = useState(false) + + useEffect(() => { + async function loadObligations() { + try { + const response = await fetch('/api/sdk/v1/ucca/assessments') + if (response.ok) { + const data = await response.json() + const assessments = data.assessments || [] + if (assessments.length > 0) { + const mapped = mapControlsToObligations(assessments) + setObligations(mapped) + setBackendAvailable(true) + setLoading(false) + return + } + } + } catch { + // Backend unavailable, use SDK state obligations + } + + // Fallback: use obligations from SDK state + if (state.obligations && state.obligations.length > 0) { + setObligations(state.obligations.map(o => ({ + id: o.id, + title: o.title, + description: o.description || '', + source: o.source || 'DSGVO', + sourceArticle: o.sourceArticle || '', + deadline: o.deadline ? new Date(o.deadline) : null, + status: (o.status as Obligation['status']) || 'pending', + priority: (o.priority as Obligation['priority']) || 'medium', + responsible: o.responsible || 'Compliance Team', + linkedSystems: o.linkedSystems || [], + }))) + } + + setLoading(false) + } + + loadObligations() + }, [state.obligations]) const filteredObligations = filter === 'all' ? obligations @@ -226,6 +269,18 @@ export default function ObligationsPage() { + {/* Backend Status */} + {backendAvailable && ( +
+ Pflichten aus UCCA-Assessments geladen (Live-Daten) +
+ )} + + {/* Loading */} + {loading && ( +
Lade Pflichten...
+ )} + {/* Stats */}
@@ -288,7 +343,6 @@ export default function ObligationsPage() {
{filteredObligations .sort((a, b) => { - // Sort by status priority: overdue > in-progress > pending > completed const statusOrder = { overdue: 0, 'in-progress': 1, pending: 2, completed: 3 } return statusOrder[a.status] - statusOrder[b.status] }) @@ -297,7 +351,7 @@ export default function ObligationsPage() { ))}
- {filteredObligations.length === 0 && ( + {filteredObligations.length === 0 && !loading && (
@@ -305,7 +359,9 @@ export default function ObligationsPage() {

Keine Pflichten gefunden

-

Passen Sie den Filter an oder fuegen Sie neue Pflichten hinzu.

+

+ Erstellen Sie zuerst ein Use Case Assessment, um automatisch Pflichten abzuleiten. +

)}
diff --git a/admin-compliance/app/(sdk)/sdk/screening/page.tsx b/admin-compliance/app/(sdk)/sdk/screening/page.tsx index c7b1972..0877023 100644 --- a/admin-compliance/app/(sdk)/sdk/screening/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/screening/page.tsx @@ -1,85 +1,8 @@ 'use client' -import React, { useState } from 'react' +import React, { useState, useRef } from 'react' import { useSDK, ScreeningResult, SecurityIssue, SBOMComponent } from '@/lib/sdk' -// ============================================================================= -// MOCK DATA -// ============================================================================= - -const mockSBOMComponents: SBOMComponent[] = [ - { - name: 'react', - version: '18.3.0', - type: 'library', - purl: 'pkg:npm/react@18.3.0', - licenses: ['MIT'], - vulnerabilities: [], - }, - { - name: 'next', - version: '15.1.0', - type: 'framework', - purl: 'pkg:npm/next@15.1.0', - licenses: ['MIT'], - vulnerabilities: [], - }, - { - name: 'lodash', - version: '4.17.21', - type: 'library', - purl: 'pkg:npm/lodash@4.17.21', - licenses: ['MIT'], - vulnerabilities: [ - { - id: 'CVE-2021-23337', - cve: 'CVE-2021-23337', - severity: 'HIGH', - title: 'Prototype Pollution', - description: 'Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.', - cvss: 7.2, - fixedIn: '4.17.21', - }, - ], - }, -] - -const mockSecurityIssues: SecurityIssue[] = [ - { - id: 'issue-1', - severity: 'CRITICAL', - title: 'SQL Injection Vulnerability', - description: 'Unvalidated user input in database queries', - cve: 'CVE-2024-12345', - cvss: 9.8, - affectedComponent: 'database-connector', - remediation: 'Use parameterized queries', - status: 'OPEN', - }, - { - id: 'issue-2', - severity: 'HIGH', - title: 'Cross-Site Scripting (XSS)', - description: 'Reflected XSS in search functionality', - cve: 'CVE-2024-12346', - cvss: 7.5, - affectedComponent: 'search-module', - remediation: 'Sanitize and encode user input', - status: 'IN_PROGRESS', - }, - { - id: 'issue-3', - severity: 'MEDIUM', - title: 'Insecure Cookie Configuration', - description: 'Session cookies missing Secure and HttpOnly flags', - cve: null, - cvss: 5.3, - affectedComponent: 'auth-service', - remediation: 'Set Secure and HttpOnly flags on cookies', - status: 'OPEN', - }, -] - // ============================================================================= // COMPONENTS // ============================================================================= @@ -243,62 +166,120 @@ export default function ScreeningPage() { const [isScanning, setIsScanning] = useState(false) const [scanProgress, setScanProgress] = useState(0) const [scanStatus, setScanStatus] = useState('') - const [repositoryUrl, setRepositoryUrl] = useState('') - - const startScan = async () => { - if (!repositoryUrl) return + const [scanError, setScanError] = useState(null) + const fileInputRef = useRef(null) + const startScan = async (file: File) => { setIsScanning(true) setScanProgress(0) setScanStatus('Initialisierung...') + setScanError(null) - // Simulate scan progress - const steps = [ - { progress: 10, status: 'Repository wird geklont...' }, - { progress: 25, status: 'Abhängigkeiten werden analysiert...' }, - { progress: 40, status: 'SBOM wird generiert...' }, - { progress: 60, status: 'Schwachstellenscan läuft...' }, - { progress: 80, status: 'Lizenzprüfung...' }, - { progress: 95, status: 'Bericht wird erstellt...' }, - { progress: 100, status: 'Abgeschlossen!' }, - ] + // Show progress steps while API processes + const progressInterval = setInterval(() => { + setScanProgress(prev => { + if (prev >= 90) return prev + const step = Math.random() * 15 + 5 + const next = Math.min(prev + step, 90) + const statuses = [ + 'Abhaengigkeiten werden analysiert...', + 'SBOM wird generiert...', + 'Schwachstellenscan laeuft...', + 'OSV.dev Datenbank wird abgefragt...', + 'Lizenzpruefung...', + ] + setScanStatus(statuses[Math.min(Math.floor(next / 20), statuses.length - 1)]) + return next + }) + }, 600) - for (const step of steps) { - await new Promise(r => setTimeout(r, 800)) - setScanProgress(step.progress) - setScanStatus(step.status) + try { + const formData = new FormData() + formData.append('file', file) + formData.append('tenant_id', 'default') + + const response = await fetch('/api/sdk/v1/screening/scan', { + method: 'POST', + body: formData, + }) + + clearInterval(progressInterval) + + if (!response.ok) { + const err = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(err.details || err.error || `HTTP ${response.status}`) + } + + const data = await response.json() + + setScanProgress(100) + setScanStatus('Abgeschlossen!') + + // Map backend response to ScreeningResult + const issues: SecurityIssue[] = (data.issues || []).map((i: any) => ({ + id: i.id, + severity: i.severity, + title: i.title, + description: i.description, + cve: i.cve || null, + cvss: i.cvss || null, + affectedComponent: i.affected_component, + remediation: i.remediation, + status: i.status || 'OPEN', + })) + + const components: SBOMComponent[] = (data.components || []).map((c: any) => ({ + name: c.name, + version: c.version, + type: c.type, + purl: c.purl, + licenses: c.licenses || [], + vulnerabilities: c.vulnerabilities || [], + })) + + const result: ScreeningResult = { + id: data.id, + status: 'COMPLETED', + startedAt: data.started_at ? new Date(data.started_at) : new Date(), + completedAt: data.completed_at ? new Date(data.completed_at) : new Date(), + sbom: { + format: data.sbom_format || 'CycloneDX', + version: data.sbom_version || '1.5', + components, + dependencies: [], + generatedAt: new Date(), + }, + securityScan: { + totalIssues: data.total_issues || issues.length, + critical: data.critical_issues || 0, + high: data.high_issues || 0, + medium: data.medium_issues || 0, + low: data.low_issues || 0, + issues, + }, + error: null, + } + + dispatch({ type: 'SET_SCREENING', payload: result }) + issues.forEach(issue => { + dispatch({ type: 'ADD_SECURITY_ISSUE', payload: issue }) + }) + } catch (error: any) { + clearInterval(progressInterval) + console.error('Screening scan failed:', error) + setScanError(error.message || 'Scan fehlgeschlagen') + setScanProgress(0) + setScanStatus('') + } finally { + setIsScanning(false) } + } - // Set mock results - const result: ScreeningResult = { - id: `scan-${Date.now()}`, - status: 'COMPLETED', - startedAt: new Date(Date.now() - 30000), - completedAt: new Date(), - sbom: { - format: 'CycloneDX', - version: '1.5', - components: mockSBOMComponents, - dependencies: [], - generatedAt: new Date(), - }, - securityScan: { - totalIssues: mockSecurityIssues.length, - critical: mockSecurityIssues.filter(i => i.severity === 'CRITICAL').length, - high: mockSecurityIssues.filter(i => i.severity === 'HIGH').length, - medium: mockSecurityIssues.filter(i => i.severity === 'MEDIUM').length, - low: mockSecurityIssues.filter(i => i.severity === 'LOW').length, - issues: mockSecurityIssues, - }, - error: null, + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + startScan(file) } - - dispatch({ type: 'SET_SCREENING', payload: result }) - mockSecurityIssues.forEach(issue => { - dispatch({ type: 'ADD_SECURITY_ISSUE', payload: issue }) - }) - - setIsScanning(false) } return ( @@ -314,30 +295,33 @@ export default function ScreeningPage() { {/* Scan Input */} {!state.screening && !isScanning && (
-

Repository scannen

+

Abhaengigkeiten scannen

+

+ Laden Sie eine Abhaengigkeitsdatei hoch, um ein SBOM zu generieren und Schwachstellen zu erkennen. +

+
- setRepositoryUrl(e.target.value)} - placeholder="https://github.com/organization/repository" - className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" - />

- Unterstützte Formate: Git URL, GitHub, GitLab, Bitbucket + Unterstuetzte Formate: package-lock.json, requirements.txt, yarn.lock

+ {scanError && ( +
+ {scanError} +
+ )}
)} diff --git a/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx b/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx new file mode 100644 index 0000000..5053412 --- /dev/null +++ b/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx @@ -0,0 +1,179 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useParams, useRouter } from 'next/navigation' +import Link from 'next/link' +import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard' + +interface FullAssessment { + id: string + title: string + tenant_id: string + domain: string + created_at: string + use_case_text?: string + intake?: Record + result?: { + feasibility: string + risk_level: string + risk_score: number + complexity: string + dsfa_recommended: boolean + art22_risk: boolean + training_allowed: string + summary: string + recommendation: string + alternative_approach?: string + triggered_rules?: Array<{ + rule_code: string + title: string + severity: string + gdpr_ref: string + }> + required_controls?: Array<{ + id: string + title: string + description: string + effort: string + }> + recommended_architecture?: Array<{ + id: string + title: string + description: string + benefit: string + }> + } +} + +export default function AssessmentDetailPage() { + const params = useParams() + const router = useRouter() + const assessmentId = params.id as string + + const [assessment, setAssessment] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + async function load() { + try { + const response = await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}`) + if (!response.ok) { + throw new Error('Assessment nicht gefunden') + } + const data = await response.json() + setAssessment(data) + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + } + + if (assessmentId) { + // Try the direct endpoint first; if it fails, try the list endpoint and filter + load().catch(() => { + // Fallback: fetch from list + fetch('/api/sdk/v1/ucca/assessments') + .then(r => r.json()) + .then(data => { + const found = (data.assessments || []).find((a: FullAssessment) => a.id === assessmentId) + if (found) { + setAssessment(found) + setError(null) + } + }) + .catch(() => {}) + .finally(() => setLoading(false)) + }) + } + }, [assessmentId]) + + const handleDelete = async () => { + if (!confirm('Assessment wirklich loeschen?')) return + try { + await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}`, { method: 'DELETE' }) + router.push('/sdk/use-cases') + } catch { + // Ignore delete errors + } + } + + if (loading) { + return ( +
+
Lade Assessment...
+
+ ) + } + + if (error || !assessment) { + return ( +
+
+

Fehler

+

{error || 'Assessment nicht gefunden'}

+
+ + Zurueck zur Uebersicht + +
+ ) + } + + return ( +
+ {/* Breadcrumb */} +
+ Use Cases + / + {assessment.title || assessmentId.slice(0, 8)} +
+ + {/* Header */} +
+
+

{assessment.title || 'Assessment Detail'}

+
+ Domain: {assessment.domain} + Erstellt: {new Date(assessment.created_at).toLocaleDateString('de-DE')} +
+
+
+ + + Zurueck + +
+
+ + {/* Use Case Text */} + {assessment.use_case_text && ( +
+

Beschreibung des Anwendungsfalls

+

{assessment.use_case_text}

+
+ )} + + {/* Result */} + {assessment.result && ( + + )} + + {/* No Result */} + {!assessment.result && ( +
+

Dieses Assessment hat noch kein Ergebnis.

+
+ )} +
+ ) +} diff --git a/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx b/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx new file mode 100644 index 0000000..5b01760 --- /dev/null +++ b/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx @@ -0,0 +1,464 @@ +'use client' + +import React, { useState } from 'react' +import { useRouter } from 'next/navigation' +import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard' + +// ============================================================================= +// WIZARD STEPS CONFIG +// ============================================================================= + +const WIZARD_STEPS = [ + { id: 1, title: 'Grundlegendes', description: 'Titel und Beschreibung' }, + { id: 2, title: 'Datenkategorien', description: 'Welche Daten werden verarbeitet?' }, + { id: 3, title: 'Automatisierung', description: 'Grad der Automatisierung' }, + { id: 4, title: 'Hosting & Modell', description: 'Technische Details' }, + { id: 5, title: 'Datenhaltung', description: 'Aufbewahrung und Speicherung' }, +] + +const DOMAINS = [ + { value: 'healthcare', label: 'Gesundheit' }, + { value: 'finance', label: 'Finanzen' }, + { value: 'education', label: 'Bildung' }, + { value: 'retail', label: 'Handel' }, + { value: 'it_services', label: 'IT-Dienstleistungen' }, + { value: 'consulting', label: 'Beratung' }, + { value: 'manufacturing', label: 'Produktion' }, + { value: 'hr', label: 'Personalwesen' }, + { value: 'marketing', label: 'Marketing' }, + { value: 'legal', label: 'Recht' }, + { value: 'public', label: 'Oeffentlicher Sektor' }, + { value: 'general', label: 'Allgemein' }, +] + +// ============================================================================= +// MAIN COMPONENT +// ============================================================================= + +export default function NewUseCasePage() { + const router = useRouter() + const [currentStep, setCurrentStep] = useState(1) + const [isSubmitting, setIsSubmitting] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + + // Form state + const [form, setForm] = useState({ + title: '', + use_case_text: '', + domain: 'general', + // Data Types + personal_data: false, + special_categories: false, + minors_data: false, + health_data: false, + biometric_data: false, + financial_data: false, + // Purpose + purpose_profiling: false, + purpose_automated_decision: false, + purpose_marketing: false, + purpose_analytics: false, + purpose_service_delivery: false, + // Automation + automation: 'assistive' as 'assistive' | 'semi_automated' | 'fully_automated', + // Hosting + hosting_provider: 'self_hosted', + hosting_region: 'eu', + // Model Usage + model_rag: false, + model_finetune: false, + model_training: false, + model_inference: true, + // Retention + retention_days: 90, + retention_purpose: '', + }) + + const updateForm = (updates: Partial) => { + setForm(prev => ({ ...prev, ...updates })) + } + + const handleSubmit = async () => { + setIsSubmitting(true) + setError(null) + try { + const intake = { + title: form.title, + use_case_text: form.use_case_text, + domain: form.domain, + data_types: { + personal_data: form.personal_data, + special_categories: form.special_categories, + minors_data: form.minors_data, + health_data: form.health_data, + biometric_data: form.biometric_data, + financial_data: form.financial_data, + }, + purpose: { + profiling: form.purpose_profiling, + automated_decision: form.purpose_automated_decision, + marketing: form.purpose_marketing, + analytics: form.purpose_analytics, + service_delivery: form.purpose_service_delivery, + }, + automation: form.automation, + hosting: { + provider: form.hosting_provider, + region: form.hosting_region, + }, + model_usage: { + rag: form.model_rag, + finetune: form.model_finetune, + training: form.model_training, + inference: form.model_inference, + }, + retention: { + days: form.retention_days, + purpose: form.retention_purpose, + }, + store_raw_text: true, + } + + const response = await fetch('/api/sdk/v1/ucca/assess', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(intake), + }) + + if (!response.ok) { + const errData = await response.json().catch(() => null) + throw new Error(errData?.error || `HTTP ${response.status}`) + } + + const data = await response.json() + setResult(data) + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler bei der Bewertung') + } finally { + setIsSubmitting(false) + } + } + + // If we have a result, show it + if (result) { + const r = result as { assessment?: { id: string }; result?: Record } + return ( +
+
+

Assessment Ergebnis

+
+ {r.assessment?.id && ( + + )} + +
+
+ {r.result && ( + [0]['result']} /> + )} +
+ ) + } + + return ( +
+ {/* Header */} +
+

Neues Use Case Assessment

+

+ Beschreiben Sie Ihren KI-Anwendungsfall Schritt fuer Schritt +

+
+ + {/* Step Indicator */} +
+ {WIZARD_STEPS.map((step, idx) => ( + + + {idx < WIZARD_STEPS.length - 1 &&
} + + ))} +
+ + {/* Error */} + {error && ( +
{error}
+ )} + + {/* Step Content */} +
+ {/* Step 1: Grundlegendes */} + {currentStep === 1 && ( +
+

Grundlegende Informationen

+
+ + updateForm({ title: e.target.value })} + placeholder="z.B. Chatbot fuer Kundenservice" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" + /> +
+
+ +