Files
breakpilot-compliance/admin-compliance/components/sdk/obligations/GapAnalysisView.tsx
Benjamin Admin 38e278ee3c
All checks were successful
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) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 18s
feat(ucca): Pflichtendatenbank v2 (325 Obligations), Trigger-Engine, TOM-Control-Mapping
- 9 Regulation-JSON-Dateien (DSGVO 80, AI Act 60, NIS2 40, BDSG 30, TTDSG 20, DSA 35, Data Act 25, EU-Maschinen 15, DORA 20)
- Condition-Tree-Engine fuer automatische Pflichtenselektion (all_of/any_of, 80+ Field-Paths)
- Generischer JSONRegulationModule-Loader mit YAML-Fallback
- Bidirektionales TOM-Control-Mapping (291 Obligation→Control, 92 Control→Obligation)
- Gap-Analyse-Engine (Compliance-%, Priority Actions, Domain Breakdown)
- ScopeDecision→UnifiedFacts Bridge fuer Auto-Profiling
- 4 neue API-Endpoints (assess-from-scope, tom-controls, gap-analysis, reverse-lookup)
- Frontend: Auto-Profiling Button, Regulation-Filter Chips, TOM-Panel, Gap-Analyse-View
- 18 Unit Tests (Condition Engine, v2 Loader, TOM Mapper)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:51:44 +01:00

227 lines
8.9 KiB
TypeScript

'use client'
import React, { useState } from 'react'
interface GapItem {
control_id: string
control_title: string
control_domain: string
status: string
priority: string
obligation_ids: string[]
required_by_count: number
}
interface PriorityAction {
rank: number
action: string
control_ids: string[]
impact: string
effort: string
}
interface DomainGap {
domain_id: string
domain_name: string
total_controls: number
implemented_controls: number
compliance_percent: number
}
interface GapAnalysisResult {
compliance_percent: number
total_controls: number
implemented_controls: number
partial_controls: number
missing_controls: number
gaps: GapItem[]
priority_actions: PriorityAction[]
by_domain: Record<string, DomainGap>
}
const UCCA_API = '/api/sdk/v1/ucca/obligations'
const DOMAIN_LABELS: Record<string, string> = {
GOV: 'Governance', HR: 'Human Resources', IAM: 'Identity & Access',
AC: 'Access Control', CRYPTO: 'Kryptographie', LOG: 'Logging & Monitoring',
SDLC: 'Softwareentwicklung', OPS: 'Betrieb', NET: 'Netzwerk',
BCP: 'Business Continuity', VENDOR: 'Lieferanten', DATA: 'Datenschutz',
}
const IMPACT_COLORS: Record<string, string> = {
critical: 'text-red-700 bg-red-50',
high: 'text-orange-700 bg-orange-50',
medium: 'text-yellow-700 bg-yellow-50',
low: 'text-green-700 bg-green-50',
}
export default function GapAnalysisView() {
const [result, setResult] = useState<GapAnalysisResult | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const runAnalysis = async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(`${UCCA_API}/gap-analysis`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ control_status_map: {} }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
setResult(await res.json())
} catch (e) {
setError(e instanceof Error ? e.message : 'Analyse fehlgeschlagen')
} finally {
setLoading(false)
}
}
if (!result) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 text-center">
<div className="w-12 h-12 mx-auto bg-purple-50 rounded-full flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h3 className="text-sm font-semibold text-gray-900">TOM Gap-Analyse</h3>
<p className="text-xs text-gray-500 mt-1 mb-4">
Vergleicht erforderliche TOM Controls mit dem aktuellen Implementierungsstatus.
</p>
{error && <p className="text-xs text-red-600 mb-3">{error}</p>}
<button
onClick={runAnalysis}
disabled={loading}
className="px-5 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{loading ? 'Analysiere...' : 'Gap-Analyse starten'}
</button>
</div>
)
}
const domains = Object.values(result.by_domain).sort((a, b) => a.compliance_percent - b.compliance_percent)
return (
<div className="space-y-4">
{/* Compliance Score */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-900">Compliance-Status</h3>
<button
onClick={runAnalysis}
disabled={loading}
className="text-xs text-purple-600 hover:text-purple-700"
>
{loading ? 'Aktualisiere...' : 'Aktualisieren'}
</button>
</div>
<div className="flex items-center gap-6">
<div className="flex-shrink-0">
<div className={`text-4xl font-bold ${result.compliance_percent >= 80 ? 'text-green-600' : result.compliance_percent >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
{Math.round(result.compliance_percent)}%
</div>
<p className="text-xs text-gray-500">Compliance</p>
</div>
<div className="flex-1">
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className={`h-3 rounded-full transition-all ${result.compliance_percent >= 80 ? 'bg-green-500' : result.compliance_percent >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${Math.min(result.compliance_percent, 100)}%` }}
/>
</div>
<div className="flex justify-between mt-2 text-xs text-gray-500">
<span>{result.implemented_controls} implementiert</span>
<span>{result.partial_controls} teilweise</span>
<span>{result.missing_controls} fehlend</span>
</div>
</div>
</div>
</div>
{/* Domain Breakdown */}
{domains.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Nach Domaene</h4>
<div className="space-y-2">
{domains.map(d => (
<div key={d.domain_id} className="flex items-center gap-3">
<span className="text-xs text-gray-600 w-32 flex-shrink-0 truncate">
{DOMAIN_LABELS[d.domain_id] || d.domain_id}
</span>
<div className="flex-1 bg-gray-100 rounded-full h-2">
<div
className={`h-2 rounded-full ${d.compliance_percent >= 80 ? 'bg-green-400' : d.compliance_percent >= 50 ? 'bg-yellow-400' : 'bg-red-400'}`}
style={{ width: `${Math.min(d.compliance_percent, 100)}%` }}
/>
</div>
<span className="text-xs text-gray-500 w-14 text-right">
{d.implemented_controls}/{d.total_controls}
</span>
</div>
))}
</div>
</div>
)}
{/* Priority Actions */}
{result.priority_actions.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h4 className="text-sm font-semibold text-gray-900 mb-3">Prioritaere Massnahmen</h4>
<div className="space-y-2">
{result.priority_actions.slice(0, 5).map(a => (
<div key={a.rank} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
<span className="text-xs font-bold text-purple-600 bg-purple-50 rounded-full w-6 h-6 flex items-center justify-center flex-shrink-0">
{a.rank}
</span>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900">{a.action}</p>
<div className="flex items-center gap-2 mt-1">
<span className={`text-[10px] px-1.5 py-0.5 rounded ${IMPACT_COLORS[a.impact] || 'bg-gray-100 text-gray-600'}`}>
{a.impact}
</span>
<span className="text-[10px] text-gray-400">
Aufwand: {a.effort}
</span>
<span className="text-[10px] text-gray-400">
{a.control_ids.length} Controls
</span>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Gap List */}
{result.gaps.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h4 className="text-sm font-semibold text-gray-900 mb-3">
Offene Gaps ({result.gaps.length})
</h4>
<div className="space-y-1 max-h-64 overflow-y-auto">
{result.gaps.map(g => (
<div key={g.control_id} className="flex items-center justify-between py-2 px-3 hover:bg-gray-50 rounded text-xs">
<div className="flex items-center gap-2 min-w-0">
<span className="font-mono text-purple-600 flex-shrink-0">{g.control_id}</span>
<span className="text-gray-700 truncate">{g.control_title}</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className={`px-1.5 py-0.5 rounded ${g.status === 'NOT_IMPLEMENTED' ? 'bg-red-50 text-red-600' : 'bg-yellow-50 text-yellow-600'}`}>
{g.status === 'NOT_IMPLEMENTED' ? 'Fehlend' : 'Teilweise'}
</span>
<span className="text-gray-400">{g.required_by_count}x</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}