diff --git a/admin-compliance/app/api/sdk/v1/isms/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/isms/[[...path]]/route.ts new file mode 100644 index 0000000..84313ec --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/isms/[[...path]]/route.ts @@ -0,0 +1,111 @@ +/** + * ISMS (ISO 27001) API Proxy - Catch-all route + * Proxies all /api/sdk/v1/isms/* requests to backend-compliance /api/isms/* + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002' + +async function proxyRequest( + request: NextRequest, + pathSegments: string[] | undefined, + method: string +) { + const pathStr = pathSegments?.join('/') || '' + const searchParams = request.nextUrl.searchParams.toString() + const basePath = `${BACKEND_URL}/api/isms` + const url = pathStr + ? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}` + : `${basePath}${searchParams ? `?${searchParams}` : ''}` + + try { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + } + + const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug'] + for (const name of headerNames) { + const value = request.headers.get(name) + if (value) { + headers[name] = value + } + } + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + const clientUserId = request.headers.get('x-user-id') + const clientTenantId = request.headers.get('x-tenant-id') + headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId)) ? clientUserId : '00000000-0000-0000-0000-000000000001' + headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId)) ? clientTenantId : (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e') + + const fetchOptions: RequestInit = { + method, + headers, + signal: AbortSignal.timeout(60000), + } + + if (method === 'POST' || method === 'PUT' || method === 'PATCH') { + const body = await request.text() + if (body) { + fetchOptions.body = body + } + } + + const response = await fetch(url, fetchOptions) + + if (!response.ok) { + const errorText = await response.text() + let errorJson + try { + errorJson = JSON.parse(errorText) + } catch { + errorJson = { error: errorText } + } + return NextResponse.json( + { error: `Backend Error: ${response.status}`, ...errorJson }, + { status: response.status } + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + console.error('ISMS API proxy error:', error) + return NextResponse.json( + { error: 'Verbindung zum Compliance Backend fehlgeschlagen' }, + { status: 503 } + ) + } +} + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'GET') +} + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'POST') +} + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'PUT') +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ path?: string[] }> } +) { + const { path } = await params + return proxyRequest(request, path, 'DELETE') +} diff --git a/admin-compliance/app/sdk/architecture/architecture-data.ts b/admin-compliance/app/sdk/architecture/architecture-data.ts index 481bcc7..1256c89 100644 --- a/admin-compliance/app/sdk/architecture/architecture-data.ts +++ b/admin-compliance/app/sdk/architecture/architecture-data.ts @@ -160,6 +160,11 @@ export const ARCH_SERVICES: ArchService[] = [ 'security_backlog', 'quality_entries', 'notfallplan_incidents', 'notfallplan_templates', 'data_processing_agreement', + 'compliance_isms_scope', 'compliance_isms_context', 'compliance_isms_policy', + 'compliance_security_objectives', 'compliance_soa', + 'compliance_audit_findings', 'compliance_corrective_actions', + 'compliance_management_reviews', 'compliance_internal_audits', + 'compliance_audit_trail', 'compliance_isms_readiness_checks', ], ragCollections: [], apiEndpoints: [ @@ -173,6 +178,16 @@ export const ARCH_SERVICES: ArchService[] = [ 'CRUD /api/compliance/vvt', 'CRUD /api/compliance/loeschfristen', 'CRUD /api/compliance/obligations', + 'CRUD /api/isms/scope', + 'CRUD /api/isms/policies', + 'CRUD /api/isms/objectives', + 'CRUD /api/isms/soa', + 'CRUD /api/isms/findings', + 'CRUD /api/isms/capa', + 'CRUD /api/isms/management-reviews', + 'CRUD /api/isms/internal-audits', + 'GET /api/isms/overview', + 'POST /api/isms/readiness-check', 'CRUD /api/compliance/legal-documents', 'CRUD /api/compliance/legal-templates', ], diff --git a/admin-compliance/app/sdk/isms/page.tsx b/admin-compliance/app/sdk/isms/page.tsx new file mode 100644 index 0000000..a7e946c --- /dev/null +++ b/admin-compliance/app/sdk/isms/page.tsx @@ -0,0 +1,1267 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { useSDK } from '@/lib/sdk' + +// ============================================================================= +// TYPES +// ============================================================================= + +interface ISMSOverview { + overall_status: string + certification_readiness: number + chapters: ChapterStatus[] + scope_approved: boolean + soa_approved: boolean + last_management_review: string | null + last_internal_audit: string | null + open_major_findings: number + open_minor_findings: number + policies_count: number + policies_approved: number + objectives_count: number + objectives_achieved: number +} + +interface ChapterStatus { + chapter: string + title: string + status: string + completion_percentage: number + open_findings: number + key_documents: string[] + last_reviewed: string | null +} + +interface ISMSScope { + id: string + scope_statement: string + included_locations: string[] + included_processes: string[] + included_services: string[] + excluded_items: string[] + exclusion_justification: string + organizational_boundary: string + physical_boundary: string + technical_boundary: string + status: string + version: string + created_by: string + approved_by: string | null + approved_at: string | null + effective_date: string | null + review_date: string | null + created_at: string +} + +interface ISMSContext { + id: string + internal_issues: Record[] | null + external_issues: Record[] | null + interested_parties: Record[] | null + regulatory_requirements: string[] + contractual_requirements: string[] + swot_strengths: string[] + swot_weaknesses: string[] + swot_opportunities: string[] + swot_threats: string[] + status: string + created_at: string +} + +interface ISMSPolicy { + id: string + policy_id: string + title: string + policy_type: string + description: string + policy_text: string + applies_to: string[] + review_frequency_months: number + related_controls: string[] + status: string + version: string + authored_by: string + reviewed_by: string | null + approved_by: string | null + approved_at: string | null + effective_date: string | null + next_review_date: string | null + created_at: string +} + +interface SecurityObjective { + id: string + objective_id: string + title: string + description: string + category: string + kpi_name: string + kpi_target: number + kpi_unit: string + measurement_frequency: string + owner: string + target_date: string + progress_percentage: number + status: string + achieved_date: string | null +} + +interface SoAEntry { + id: string + annex_a_control: string + annex_a_title: string + annex_a_category: string + is_applicable: boolean + applicability_justification: string + implementation_status: string + implementation_notes: string + breakpilot_control_ids: string[] + coverage_level: string + evidence_description: string + version: string + approved_at: string | null +} + +interface InternalAudit { + id: string + audit_id: string + title: string + audit_type: string + scope_description: string + iso_chapters_covered: string[] + planned_date: string + actual_end_date: string | null + lead_auditor: string + audit_team: string[] + status: string + total_findings: number + major_findings: number + minor_findings: number + ofi_count: number + audit_conclusion: string | null + overall_assessment: string | null +} + +interface AuditFinding { + id: string + finding_id: string + finding_type: string + iso_chapter: string + annex_a_control: string | null + title: string + description: string + objective_evidence: string + owner: string + auditor: string + status: string + due_date: string | null + closed_date: string | null + internal_audit_id: string | null + is_blocking: boolean +} + +interface CAPA { + id: string + capa_id: string + finding_id: string + capa_type: string + title: string + description: string + assigned_to: string + status: string + planned_completion: string + actual_completion: string | null + effectiveness_verified: boolean | null +} + +interface ManagementReview { + id: string + review_id: string + title: string + review_date: string + review_period_start: string + review_period_end: string + chairperson: string + attendees: Record[] | null + status: string + approved_by: string | null + approved_at: string | null + next_review_date: string | null + action_items: Record[] | null +} + +interface ReadinessCheck { + id: string + check_date: string + overall_status: string + certification_possible: boolean + readiness_score: number + chapter_4_status: string + chapter_5_status: string + chapter_6_status: string + chapter_7_status: string + chapter_8_status: string + chapter_9_status: string + chapter_10_status: string + potential_majors: PotentialFinding[] + potential_minors: PotentialFinding[] + priority_actions: string[] +} + +interface PotentialFinding { + check: string + status: string + recommendation: string + iso_reference: string +} + +type TabId = 'overview' | 'policies' | 'soa' | 'objectives' | 'audits' | 'reviews' + +const API = '/api/sdk/v1/isms' + +// ============================================================================= +// HELPER COMPONENTS +// ============================================================================= + +function StatusBadge({ status, size = 'sm' }: { status: string; size?: 'sm' | 'md' }) { + const colors: Record = { + ready: 'bg-green-100 text-green-700', + compliant: 'bg-green-100 text-green-700', + approved: 'bg-green-100 text-green-700', + pass: 'bg-green-100 text-green-700', + implemented: 'bg-green-100 text-green-700', + completed: 'bg-green-100 text-green-700', + verified: 'bg-green-100 text-green-700', + achieved: 'bg-green-100 text-green-700', + closed: 'bg-green-100 text-green-700', + at_risk: 'bg-yellow-100 text-yellow-700', + partial: 'bg-yellow-100 text-yellow-700', + warning: 'bg-yellow-100 text-yellow-700', + planned: 'bg-blue-100 text-blue-700', + draft: 'bg-gray-100 text-gray-700', + active: 'bg-blue-100 text-blue-700', + in_progress: 'bg-blue-100 text-blue-700', + not_ready: 'bg-red-100 text-red-700', + non_compliant: 'bg-red-100 text-red-700', + fail: 'bg-red-100 text-red-700', + open: 'bg-red-100 text-red-700', + corrective_action_pending: 'bg-orange-100 text-orange-700', + verification_pending: 'bg-yellow-100 text-yellow-700', + } + const cls = colors[status] || 'bg-gray-100 text-gray-600' + const labels: Record = { + ready: 'Bereit', not_ready: 'Nicht bereit', at_risk: 'Risiko', + compliant: 'Konform', non_compliant: 'Nicht konform', partial: 'Teilweise', + approved: 'Genehmigt', draft: 'Entwurf', pass: 'Bestanden', fail: 'Fehlgeschlagen', + warning: 'Warnung', implemented: 'Implementiert', planned: 'Geplant', + active: 'Aktiv', achieved: 'Erreicht', completed: 'Abgeschlossen', + open: 'Offen', closed: 'Geschlossen', verified: 'Verifiziert', + corrective_action_pending: 'CAPA ausstehend', verification_pending: 'Verifizierung', + in_progress: 'In Bearbeitung', + not_applicable: 'N/A', + } + const pad = size === 'md' ? 'px-3 py-1 text-sm' : 'px-2 py-0.5 text-xs' + return {labels[status] || status} +} + +function StatCard({ label, value, sub, color = 'purple' }: { label: string; value: string | number; sub?: string; color?: string }) { + const colors: Record = { + purple: 'border-purple-200 bg-purple-50', + green: 'border-green-200 bg-green-50', + red: 'border-red-200 bg-red-50', + yellow: 'border-yellow-200 bg-yellow-50', + blue: 'border-blue-200 bg-blue-50', + } + return ( +
+
{value}
+
{label}
+ {sub &&
{sub}
} +
+ ) +} + +function LoadingSpinner() { + return ( +
+
+
+ ) +} + +function EmptyState({ text, action, onAction }: { text: string; action?: string; onAction?: () => void }) { + return ( +
+

{text}

+ {action && onAction && ( + + )} +
+ ) +} + +// ============================================================================= +// TAB: OVERVIEW +// ============================================================================= + +function OverviewTab() { + const [overview, setOverview] = useState(null) + const [readiness, setReadiness] = useState(null) + const [scope, setScope] = useState(null) + const [loading, setLoading] = useState(true) + const [running, setRunning] = useState(false) + + const load = useCallback(async () => { + setLoading(true) + try { + const [ovRes, rdRes, scRes] = await Promise.allSettled([ + fetch(`${API}/overview`), + fetch(`${API}/readiness-check/latest`), + fetch(`${API}/scope`), + ]) + if (ovRes.status === 'fulfilled' && ovRes.value.ok) setOverview(await ovRes.value.json()) + if (rdRes.status === 'fulfilled' && rdRes.value.ok) setReadiness(await rdRes.value.json()) + if (scRes.status === 'fulfilled' && scRes.value.ok) setScope(await scRes.value.json()) + } catch { /* ignore */ } + setLoading(false) + }, []) + + useEffect(() => { load() }, [load]) + + const runCheck = async () => { + setRunning(true) + try { + const res = await fetch(`${API}/readiness-check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ triggered_by: 'admin-ui' }), + }) + if (res.ok) { + setReadiness(await res.json()) + load() + } + } catch { /* ignore */ } + setRunning(false) + } + + if (loading) return + + return ( +
+ {/* Readiness Score */} +
+
+

ISO 27001 Zertifizierungsbereitschaft

+ +
+ + {overview && ( +
+ = 80 ? 'green' : overview.certification_readiness >= 50 ? 'yellow' : 'red'} + /> + + + 0 ? 'red' : 'green'} /> + 0 ? 'yellow' : 'green'} /> +
+ )} + + {/* Chapter Overview */} + {overview?.chapters && ( +
+

ISO 27001 Kapitel-Status

+
+ {overview.chapters.map(ch => ( +
+ Kap.{ch.chapter} + {ch.title} +
+
+
= 80 ? 'bg-green-500' : ch.completion_percentage >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`} + style={{ width: `${ch.completion_percentage}%` }} + /> +
+
+ {Math.round(ch.completion_percentage)}% + +
+ ))} +
+
+ )} +
+ + {/* Readiness Findings */} + {readiness && ( +
+
+

Readiness-Check Ergebnis

+
+ + Score: {Math.round(readiness.readiness_score)}% +
+
+ + {readiness.potential_majors.length > 0 && ( +
+

Potenzielle Major-Findings ({readiness.potential_majors.length})

+
+ {readiness.potential_majors.map((f, i) => ( +
+ +
+
{f.check}
+
{f.recommendation}
+
ISO Referenz: {f.iso_reference}
+
+
+ ))} +
+
+ )} + + {readiness.potential_minors.length > 0 && ( +
+

Potenzielle Minor-Findings ({readiness.potential_minors.length})

+
+ {readiness.potential_minors.map((f, i) => ( +
+ +
+
{f.check}
+
{f.recommendation}
+
+
+ ))} +
+
+ )} + + {readiness.priority_actions.length > 0 && ( +
+

Prioritaere Massnahmen

+
    + {readiness.priority_actions.map((a, i) =>
  1. {a}
  2. )} +
+
+ )} +
+ )} + + {/* Scope Summary */} + {scope && ( +
+
+

ISMS Scope (Kap. 4.3)

+ +
+

{scope.scope_statement}

+
+
+ Standorte: +
    + {scope.included_locations?.map((l, i) =>
  • {l}
  • )} +
+
+
+ Prozesse: +
    + {scope.included_processes?.map((p, i) =>
  • {p}
  • )} +
+
+
+ {scope.approved_by && ( +
+ Genehmigt von {scope.approved_by} am {new Date(scope.approved_at!).toLocaleDateString('de-DE')} +
+ )} +
+ )} +
+ ) +} + +// ============================================================================= +// TAB: POLICIES +// ============================================================================= + +function PoliciesTab() { + const [policies, setPolicies] = useState([]) + const [loading, setLoading] = useState(true) + const [showCreate, setShowCreate] = useState(false) + const [filter, setFilter] = useState('') + + const load = useCallback(async () => { + setLoading(true) + try { + const url = filter ? `${API}/policies?policy_type=${filter}` : `${API}/policies` + const res = await fetch(url) + if (res.ok) { + const data = await res.json() + setPolicies(data.policies || []) + } + } catch { /* ignore */ } + setLoading(false) + }, [filter]) + + useEffect(() => { load() }, [load]) + + const createPolicy = async (form: Record) => { + try { + const res = await fetch(`${API}/policies`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(form), + }) + if (res.ok) { setShowCreate(false); load() } + } catch { /* ignore */ } + } + + const approvePolicy = async (policyId: string) => { + try { + await fetch(`${API}/policies/${policyId}/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + reviewed_by: 'admin', + approved_by: 'admin', + effective_date: new Date().toISOString().split('T')[0], + }), + }) + load() + } catch { /* ignore */ } + } + + if (loading) return + + const policyTypes = ['master', 'topic', 'operational', 'standard'] + + return ( +
+
+
+ + {policyTypes.map(t => ( + + ))} +
+ +
+ + {policies.length === 0 ? ( + setShowCreate(true)} /> + ) : ( +
+ {policies.map(p => ( +
+
+
+
+ {p.policy_id} + {p.title} + + v{p.version} +
+

{p.description}

+
+ Typ: {p.policy_type} + Review: alle {p.review_frequency_months} Monate + {p.next_review_date && Naechste Review: {new Date(p.next_review_date).toLocaleDateString('de-DE')}} +
+
+ {p.status === 'draft' && ( + + )} +
+
+ ))} +
+ )} + + {/* Create Modal */} + {showCreate && ( + setShowCreate(false)} onSave={createPolicy} /> + )} +
+ ) +} + +function PolicyCreateModal({ onClose, onSave }: { onClose: () => void; onSave: (data: Record) => void }) { + const [form, setForm] = useState({ + policy_id: '', title: '', policy_type: 'topic', description: '', policy_text: '', + applies_to: ['Alle Mitarbeiter'], review_frequency_months: 12, related_controls: [] as string[], + authored_by: 'admin', + }) + + return ( +
+
+

Neue ISMS Policy

+
+
+
+ + setForm({ ...form, policy_id: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" placeholder="z.B. POL-SEC-001" /> +
+
+ + +
+
+
+ + setForm({ ...form, title: e.target.value })} className="w-full border rounded-lg px-3 py-2 text-sm" /> +
+
+ +