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
- 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>
227 lines
8.9 KiB
TypeScript
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>
|
|
)
|
|
}
|