feat(gci): add Gesamt-Compliance-Index scoring engine and dashboard
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 34s
CI / test-python-backend-compliance (push) Successful in 28s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 17s
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 34s
CI / test-python-backend-compliance (push) Successful in 28s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 17s
Implements the 4-level GCI scoring model (Module -> Risk-Weighted -> Regulation Area -> Final GCI) with DSGVO, NIS2, ISO 27001, and EU AI Act integration. Backend: - 9 Go files: engine, models, weights, validity, NIS2 roles/scoring, ISO mapping/gap-analysis, mock data - GCI handlers with 13 API endpoints under /sdk/v1/gci/ - Routes registered in main.go Frontend: - TypeScript types, API client, Next.js API proxy - Dashboard page with 6 tabs (Overview, Breakdown, NIS2, ISO 27001, Matrix, Audit Trail) - Sidebar navigation entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
693
admin-compliance/app/(sdk)/sdk/gci/page.tsx
Normal file
693
admin-compliance/app/(sdk)/sdk/gci/page.tsx
Normal file
@@ -0,0 +1,693 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import {
|
||||
GCIResult,
|
||||
GCIBreakdown,
|
||||
GCIHistoryResponse,
|
||||
GCIMatrixResponse,
|
||||
NIS2Score,
|
||||
ISOGapAnalysis,
|
||||
WeightProfile,
|
||||
MaturityLevel,
|
||||
MATURITY_INFO,
|
||||
getScoreColor,
|
||||
getScoreRingColor,
|
||||
} from '@/lib/sdk/gci/types'
|
||||
import {
|
||||
getGCIScore,
|
||||
getGCIBreakdown,
|
||||
getGCIHistory,
|
||||
getGCIMatrix,
|
||||
getNIS2Score,
|
||||
getISOGapAnalysis,
|
||||
getWeightProfiles,
|
||||
} from '@/lib/sdk/gci/api'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
type TabId = 'overview' | 'breakdown' | 'nis2' | 'iso' | 'matrix' | 'audit'
|
||||
|
||||
interface Tab {
|
||||
id: TabId
|
||||
label: string
|
||||
}
|
||||
|
||||
const TABS: Tab[] = [
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'breakdown', label: 'Breakdown' },
|
||||
{ id: 'nis2', label: 'NIS2' },
|
||||
{ id: 'iso', label: 'ISO 27001' },
|
||||
{ id: 'matrix', label: 'Matrix' },
|
||||
{ id: 'audit', label: 'Audit Trail' },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER COMPONENTS
|
||||
// =============================================================================
|
||||
|
||||
function TabNavigation({ tabs, activeTab, onTabChange }: { tabs: Tab[]; activeTab: TabId; onTabChange: (tab: TabId) => void }) {
|
||||
return (
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex gap-1 -mb-px overflow-x-auto" aria-label="Tabs">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
|
||||
activeTab === tab.id
|
||||
? 'border-purple-600 text-purple-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScoreCircle({ score, size = 144, label }: { score: number; size?: number; label?: string }) {
|
||||
const radius = (size / 2) - 12
|
||||
const circumference = 2 * Math.PI * radius
|
||||
const strokeDashoffset = circumference - (score / 100) * circumference
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center">
|
||||
<svg className="-rotate-90" width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
<circle cx={size/2} cy={size/2} r={radius} stroke="#e5e7eb" strokeWidth="8" fill="none" />
|
||||
<circle
|
||||
cx={size/2} cy={size/2} r={radius}
|
||||
stroke={getScoreRingColor(score)}
|
||||
strokeWidth="8" fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
className="transition-all duration-1000"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className={`text-3xl font-bold ${getScoreColor(score)}`}>{score.toFixed(1)}</span>
|
||||
{label && <span className="text-xs text-gray-500 mt-1">{label}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MaturityBadge({ level }: { level: MaturityLevel }) {
|
||||
const info = MATURITY_INFO[level] || MATURITY_INFO.HIGH_RISK
|
||||
return (
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${info.bgColor} ${info.color} border ${info.borderColor}`}>
|
||||
{info.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function AreaScoreBar({ name, score, weight }: { name: string; score: number; weight: number }) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-gray-700">{name}</span>
|
||||
<span className={`font-semibold ${getScoreColor(score)}`}>{score.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
||||
<div
|
||||
className="h-3 rounded-full transition-all duration-700"
|
||||
style={{ width: `${Math.min(score, 100)}%`, backgroundColor: getScoreRingColor(score) }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Gewichtung: {(weight * 100).toFixed(0)}%</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
|
||||
<p>{message}</p>
|
||||
{onRetry && (
|
||||
<button onClick={onRetry} className="mt-2 text-sm underline hover:no-underline">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAB: OVERVIEW
|
||||
// =============================================================================
|
||||
|
||||
function OverviewTab({ gci, history, profiles, selectedProfile, onProfileChange }: {
|
||||
gci: GCIResult
|
||||
history: GCIHistoryResponse | null
|
||||
profiles: WeightProfile[]
|
||||
selectedProfile: string
|
||||
onProfileChange: (p: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Profile Selector */}
|
||||
{profiles.length > 0 && (
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-sm font-medium text-gray-700">Gewichtungsprofil:</label>
|
||||
<select
|
||||
value={selectedProfile}
|
||||
onChange={e => onProfileChange(e.target.value)}
|
||||
className="rounded-md border-gray-300 shadow-sm text-sm focus:border-purple-500 focus:ring-purple-500"
|
||||
>
|
||||
{profiles.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Score */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex flex-col md:flex-row items-center gap-8">
|
||||
<ScoreCircle score={gci.gci_score} label="GCI Score" />
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Gesamt-Compliance-Index</h3>
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<MaturityBadge level={gci.maturity_level} />
|
||||
<span className="text-sm text-gray-500">
|
||||
Berechnet: {new Date(gci.calculated_at).toLocaleString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">
|
||||
{MATURITY_INFO[gci.maturity_level]?.description || ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Area Scores */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Regulierungsbereiche</h3>
|
||||
<div className="space-y-4">
|
||||
{gci.area_scores.map(area => (
|
||||
<AreaScoreBar
|
||||
key={area.regulation_id}
|
||||
name={area.regulation_name}
|
||||
score={area.score}
|
||||
weight={area.weight}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History Chart (simplified) */}
|
||||
{history && history.snapshots.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Verlauf</h3>
|
||||
<div className="flex items-end gap-2 h-32">
|
||||
{history.snapshots.map((snap, i) => (
|
||||
<div key={i} className="flex-1 flex flex-col items-center gap-1">
|
||||
<span className="text-xs text-gray-500">{snap.score.toFixed(0)}</span>
|
||||
<div
|
||||
className="w-full rounded-t transition-all duration-500"
|
||||
style={{
|
||||
height: `${(snap.score / 100) * 100}%`,
|
||||
backgroundColor: getScoreRingColor(snap.score),
|
||||
minHeight: '4px',
|
||||
}}
|
||||
/>
|
||||
<span className="text-[10px] text-gray-400">
|
||||
{new Date(snap.calculated_at).toLocaleDateString('de-DE', { month: 'short' })}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Adjustments */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4">
|
||||
<div className="text-sm text-gray-500">Kritikalitaets-Multiplikator</div>
|
||||
<div className="text-2xl font-bold text-gray-900">{gci.criticality_multiplier.toFixed(2)}x</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-4">
|
||||
<div className="text-sm text-gray-500">Incident-Korrektur</div>
|
||||
<div className={`text-2xl font-bold ${gci.incident_adjustment < 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{gci.incident_adjustment > 0 ? '+' : ''}{gci.incident_adjustment.toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAB: BREAKDOWN
|
||||
// =============================================================================
|
||||
|
||||
function BreakdownTab({ breakdown }: { breakdown: GCIBreakdown | null; loading: boolean }) {
|
||||
if (!breakdown) return <LoadingSpinner />
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Level 1: Modules */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Level 1: Modul-Scores</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-2 pr-4 font-medium text-gray-600">Modul</th>
|
||||
<th className="text-left py-2 pr-4 font-medium text-gray-600">Kategorie</th>
|
||||
<th className="text-right py-2 pr-4 font-medium text-gray-600">Zugewiesen</th>
|
||||
<th className="text-right py-2 pr-4 font-medium text-gray-600">Abgeschlossen</th>
|
||||
<th className="text-right py-2 pr-4 font-medium text-gray-600">Raw Score</th>
|
||||
<th className="text-right py-2 pr-4 font-medium text-gray-600">Validitaet</th>
|
||||
<th className="text-right py-2 font-medium text-gray-600">Final</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{breakdown.level1_modules.map(m => (
|
||||
<tr key={m.module_id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-2 pr-4 font-medium text-gray-900">{m.module_name}</td>
|
||||
<td className="py-2 pr-4">
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
|
||||
{m.category}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-right text-gray-600">{m.assigned}</td>
|
||||
<td className="py-2 pr-4 text-right text-gray-600">{m.completed}</td>
|
||||
<td className="py-2 pr-4 text-right text-gray-600">{(m.raw_score * 100).toFixed(1)}%</td>
|
||||
<td className="py-2 pr-4 text-right text-gray-600">{(m.validity_factor * 100).toFixed(0)}%</td>
|
||||
<td className={`py-2 text-right font-semibold ${getScoreColor(m.final_score * 100)}`}>
|
||||
{(m.final_score * 100).toFixed(1)}%
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Level 2: Areas */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Level 2: Regulierungsbereiche (risikogewichtet)</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{breakdown.level2_areas.map(area => (
|
||||
<div key={area.area_id} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-medium text-gray-900">{area.area_name}</h4>
|
||||
<span className={`text-lg font-bold ${getScoreColor(area.area_score)}`}>
|
||||
{area.area_score.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{area.modules.map(m => (
|
||||
<div key={m.module_id} className="flex justify-between text-xs text-gray-500">
|
||||
<span>{m.module_name}</span>
|
||||
<span>{(m.final_score * 100).toFixed(0)}% (w:{m.risk_weight.toFixed(1)})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAB: NIS2
|
||||
// =============================================================================
|
||||
|
||||
function NIS2Tab({ nis2 }: { nis2: NIS2Score | null }) {
|
||||
if (!nis2) return <LoadingSpinner />
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* NIS2 Overall */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<ScoreCircle score={nis2.overall_score} size={120} label="NIS2" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">NIS2 Compliance Score</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Network and Information Security Directive 2 (EU 2022/2555)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NIS2 Areas */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">NIS2 Bereiche</h3>
|
||||
<div className="space-y-3">
|
||||
{nis2.areas.map(area => (
|
||||
<AreaScoreBar key={area.area_id} name={area.area_name} score={area.score} weight={area.weight} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NIS2 Roles */}
|
||||
{nis2.role_scores && nis2.role_scores.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Rollen-Compliance</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{nis2.role_scores.map(role => (
|
||||
<div key={role.role_id} className="border border-gray-200 rounded-lg p-3">
|
||||
<div className="font-medium text-gray-900 text-sm">{role.role_name}</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className={`text-lg font-bold ${getScoreColor(role.completion_rate * 100)}`}>
|
||||
{(role.completion_rate * 100).toFixed(0)}%
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{role.modules_completed}/{role.modules_required} Module
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-2">
|
||||
<div
|
||||
className="h-1.5 rounded-full"
|
||||
style={{
|
||||
width: `${Math.min(role.completion_rate * 100, 100)}%`,
|
||||
backgroundColor: getScoreRingColor(role.completion_rate * 100),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAB: ISO 27001
|
||||
// =============================================================================
|
||||
|
||||
function ISOTab({ iso }: { iso: ISOGapAnalysis | null }) {
|
||||
if (!iso) return <LoadingSpinner />
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Coverage Overview */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<ScoreCircle score={iso.coverage_percent} size={120} label="Abdeckung" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900">ISO 27001:2022 Gap-Analyse</h3>
|
||||
<div className="grid grid-cols-3 gap-4 mt-3">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{iso.covered_full}</div>
|
||||
<div className="text-xs text-gray-500">Voll abgedeckt</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-yellow-600">{iso.covered_partial}</div>
|
||||
<div className="text-xs text-gray-500">Teilweise</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-red-600">{iso.not_covered}</div>
|
||||
<div className="text-xs text-gray-500">Nicht abgedeckt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Summaries */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Kategorien</h3>
|
||||
<div className="space-y-3">
|
||||
{iso.category_summaries.map(cat => {
|
||||
const coveragePercent = cat.total_controls > 0
|
||||
? ((cat.covered_full + cat.covered_partial * 0.5) / cat.total_controls) * 100
|
||||
: 0
|
||||
return (
|
||||
<div key={cat.category_id} className="space-y-1">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium text-gray-700">{cat.category_id}: {cat.category_name}</span>
|
||||
<span className="text-gray-500">
|
||||
{cat.covered_full}/{cat.total_controls} Controls
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 flex overflow-hidden">
|
||||
<div className="h-3 bg-green-500" style={{ width: `${(cat.covered_full / cat.total_controls) * 100}%` }} />
|
||||
<div className="h-3 bg-yellow-500" style={{ width: `${(cat.covered_partial / cat.total_controls) * 100}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gaps */}
|
||||
{iso.gaps && iso.gaps.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">
|
||||
Offene Gaps ({iso.gaps.length})
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{iso.gaps.map(gap => (
|
||||
<div key={gap.control_id} className="flex items-start gap-3 p-3 border border-gray-100 rounded-lg hover:bg-gray-50">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
|
||||
gap.priority === 'high' ? 'bg-red-100 text-red-700' :
|
||||
gap.priority === 'medium' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{gap.priority}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900">{gap.control_id}: {gap.control_name}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{gap.recommendation}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAB: MATRIX
|
||||
// =============================================================================
|
||||
|
||||
function MatrixTab({ matrix }: { matrix: GCIMatrixResponse | null }) {
|
||||
if (!matrix || !matrix.matrix) return <LoadingSpinner />
|
||||
|
||||
const regulations = matrix.matrix.length > 0 ? Object.keys(matrix.matrix[0].regulations) : []
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">Compliance-Matrix (Rollen x Regulierungen)</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-2 pr-4 font-medium text-gray-600">Rolle</th>
|
||||
{regulations.map(r => (
|
||||
<th key={r} className="text-center py-2 px-3 font-medium text-gray-600 uppercase">{r}</th>
|
||||
))}
|
||||
<th className="text-center py-2 px-3 font-medium text-gray-600">Gesamt</th>
|
||||
<th className="text-center py-2 px-3 font-medium text-gray-600">Module</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{matrix.matrix.map(entry => (
|
||||
<tr key={entry.role} className="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td className="py-2 pr-4 font-medium text-gray-900">{entry.role_name}</td>
|
||||
{regulations.map(r => (
|
||||
<td key={r} className="py-2 px-3 text-center">
|
||||
<span className={`font-semibold ${getScoreColor(entry.regulations[r])}`}>
|
||||
{entry.regulations[r].toFixed(0)}%
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
<td className="py-2 px-3 text-center">
|
||||
<span className={`font-bold ${getScoreColor(entry.overall_score)}`}>
|
||||
{entry.overall_score.toFixed(0)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-center text-gray-500">
|
||||
{entry.completed_modules}/{entry.required_modules}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TAB: AUDIT TRAIL
|
||||
// =============================================================================
|
||||
|
||||
function AuditTab({ gci }: { gci: GCIResult }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-base font-semibold text-gray-900 mb-4">
|
||||
Audit Trail - Berechnung GCI {gci.gci_score.toFixed(1)}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Jeder Schritt der GCI-Berechnung ist nachvollziehbar und prueffaehig dokumentiert.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{gci.audit_trail.map((entry, i) => (
|
||||
<div key={i} className="flex items-start gap-3 p-3 border border-gray-100 rounded-lg">
|
||||
<div className={`flex-shrink-0 w-2 h-2 rounded-full mt-1.5 ${
|
||||
entry.impact === 'positive' ? 'bg-green-500' :
|
||||
entry.impact === 'negative' ? 'bg-red-500' :
|
||||
'bg-gray-400'
|
||||
}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-900">{entry.factor}</span>
|
||||
<span className={`text-sm font-mono ${
|
||||
entry.impact === 'positive' ? 'text-green-600' :
|
||||
entry.impact === 'negative' ? 'text-red-600' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{entry.value > 0 ? '+' : ''}{entry.value.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{entry.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN PAGE
|
||||
// =============================================================================
|
||||
|
||||
export default function GCIPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [gci, setGCI] = useState<GCIResult | null>(null)
|
||||
const [breakdown, setBreakdown] = useState<GCIBreakdown | null>(null)
|
||||
const [history, setHistory] = useState<GCIHistoryResponse | null>(null)
|
||||
const [matrix, setMatrix] = useState<GCIMatrixResponse | null>(null)
|
||||
const [nis2, setNIS2] = useState<NIS2Score | null>(null)
|
||||
const [iso, setISO] = useState<ISOGapAnalysis | null>(null)
|
||||
const [profiles, setProfiles] = useState<WeightProfile[]>([])
|
||||
const [selectedProfile, setSelectedProfile] = useState('default')
|
||||
|
||||
const loadData = useCallback(async (profile?: string) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const [gciRes, historyRes, profilesRes] = await Promise.all([
|
||||
getGCIScore(profile),
|
||||
getGCIHistory(),
|
||||
getWeightProfiles(),
|
||||
])
|
||||
setGCI(gciRes)
|
||||
setHistory(historyRes)
|
||||
setProfiles(profilesRes.profiles || [])
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Fehler beim Laden der GCI-Daten')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadData(selectedProfile)
|
||||
}, [selectedProfile, loadData])
|
||||
|
||||
// Lazy-load tab data
|
||||
useEffect(() => {
|
||||
if (activeTab === 'breakdown' && !breakdown && gci) {
|
||||
getGCIBreakdown(selectedProfile).then(setBreakdown).catch(() => {})
|
||||
}
|
||||
if (activeTab === 'nis2' && !nis2) {
|
||||
getNIS2Score().then(setNIS2).catch(() => {})
|
||||
}
|
||||
if (activeTab === 'iso' && !iso) {
|
||||
getISOGapAnalysis().then(setISO).catch(() => {})
|
||||
}
|
||||
if (activeTab === 'matrix' && !matrix) {
|
||||
getGCIMatrix().then(setMatrix).catch(() => {})
|
||||
}
|
||||
}, [activeTab, breakdown, nis2, iso, matrix, gci, selectedProfile])
|
||||
|
||||
const handleProfileChange = (profile: string) => {
|
||||
setSelectedProfile(profile)
|
||||
setBreakdown(null) // reset breakdown to reload
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Gesamt-Compliance-Index (GCI)</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
4-stufiges, mathematisch fundiertes Compliance-Scoring
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => loadData(selectedProfile)}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? 'Lade...' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<TabNavigation tabs={TABS} activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
||||
{/* Content */}
|
||||
{error && <ErrorMessage message={error} onRetry={() => loadData(selectedProfile)} />}
|
||||
|
||||
{loading && !gci ? (
|
||||
<LoadingSpinner />
|
||||
) : gci ? (
|
||||
<div className="pb-8">
|
||||
{activeTab === 'overview' && (
|
||||
<OverviewTab
|
||||
gci={gci}
|
||||
history={history}
|
||||
profiles={profiles}
|
||||
selectedProfile={selectedProfile}
|
||||
onProfileChange={handleProfileChange}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'breakdown' && (
|
||||
<BreakdownTab breakdown={breakdown} loading={!breakdown} />
|
||||
)}
|
||||
{activeTab === 'nis2' && <NIS2Tab nis2={nis2} />}
|
||||
{activeTab === 'iso' && <ISOTab iso={iso} />}
|
||||
{activeTab === 'matrix' && <MatrixTab matrix={matrix} />}
|
||||
{activeTab === 'audit' && <AuditTab gci={gci} />}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
admin-compliance/app/api/sdk/v1/gci/[[...path]]/route.ts
Normal file
89
admin-compliance/app/api/sdk/v1/gci/[[...path]]/route.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* GCI API Proxy - Catch-all route
|
||||
* Proxies all /api/sdk/v1/gci/* requests to ai-compliance-sdk backend
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
||||
|
||||
async function proxyRequest(
|
||||
request: NextRequest,
|
||||
pathSegments: string[] | undefined,
|
||||
method: string
|
||||
) {
|
||||
const pathStr = pathSegments?.join('/') || ''
|
||||
const searchParams = request.nextUrl.searchParams.toString()
|
||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/gci`
|
||||
const url = pathStr
|
||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
||||
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
||||
for (const name of headerNames) {
|
||||
const value = request.headers.get(name)
|
||||
if (value) {
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
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('GCI API proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Verbindung zum SDK 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')
|
||||
}
|
||||
@@ -561,6 +561,20 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
|
||||
isActive={pathname === '/sdk/reporting'}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
<AdditionalModuleItem
|
||||
href="/sdk/gci"
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
|
||||
</svg>
|
||||
}
|
||||
label="GCI Score"
|
||||
isActive={pathname === '/sdk/gci'}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
<AdditionalModuleItem
|
||||
href="/sdk/industry-templates"
|
||||
icon={
|
||||
|
||||
99
admin-compliance/lib/sdk/gci/api.ts
Normal file
99
admin-compliance/lib/sdk/gci/api.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* GCI API Client
|
||||
* Communicates with the Go backend via Next.js API proxy at /api/sdk/v1/gci/*
|
||||
*/
|
||||
|
||||
import type {
|
||||
GCIResult,
|
||||
GCIBreakdown,
|
||||
GCIHistoryResponse,
|
||||
GCIMatrixResponse,
|
||||
NIS2Score,
|
||||
NIS2Role,
|
||||
ISOGapAnalysis,
|
||||
WeightProfile,
|
||||
} from './types'
|
||||
|
||||
const BASE_URL = '/api/sdk/v1/gci'
|
||||
|
||||
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': typeof window !== 'undefined'
|
||||
? (localStorage.getItem('bp-tenant-id') || 'default')
|
||||
: 'default',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ error: res.statusText }))
|
||||
throw new Error(error.error || `API Error: ${res.status}`)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/** GCI Score abrufen */
|
||||
export async function getGCIScore(profile?: string): Promise<GCIResult> {
|
||||
const params = profile ? `?profile=${profile}` : ''
|
||||
return apiFetch<GCIResult>(`/score${params}`)
|
||||
}
|
||||
|
||||
/** Detailliertes 4-Level Breakdown abrufen */
|
||||
export async function getGCIBreakdown(profile?: string): Promise<GCIBreakdown> {
|
||||
const params = profile ? `?profile=${profile}` : ''
|
||||
return apiFetch<GCIBreakdown>(`/score/breakdown${params}`)
|
||||
}
|
||||
|
||||
/** GCI History abrufen */
|
||||
export async function getGCIHistory(): Promise<GCIHistoryResponse> {
|
||||
return apiFetch<GCIHistoryResponse>('/score/history')
|
||||
}
|
||||
|
||||
/** Compliance Matrix abrufen */
|
||||
export async function getGCIMatrix(): Promise<GCIMatrixResponse> {
|
||||
return apiFetch<GCIMatrixResponse>('/matrix')
|
||||
}
|
||||
|
||||
/** Audit Trail abrufen */
|
||||
export async function getGCIAuditTrail(profile?: string): Promise<{ tenant_id: string; gci_score: number; audit_trail: any[] }> {
|
||||
const params = profile ? `?profile=${profile}` : ''
|
||||
return apiFetch(`/audit-trail${params}`)
|
||||
}
|
||||
|
||||
/** Gewichtungsprofile abrufen */
|
||||
export async function getWeightProfiles(): Promise<{ profiles: WeightProfile[] }> {
|
||||
return apiFetch<{ profiles: WeightProfile[] }>('/profiles')
|
||||
}
|
||||
|
||||
/** NIS2 Score abrufen */
|
||||
export async function getNIS2Score(): Promise<NIS2Score> {
|
||||
return apiFetch<NIS2Score>('/nis2/score')
|
||||
}
|
||||
|
||||
/** NIS2 Rollen auflisten */
|
||||
export async function getNIS2Roles(): Promise<{ roles: NIS2Role[]; total: number }> {
|
||||
return apiFetch<{ roles: NIS2Role[]; total: number }>('/nis2/roles')
|
||||
}
|
||||
|
||||
/** NIS2 Rolle zuweisen */
|
||||
export async function assignNIS2Role(roleId: string, userId: string): Promise<any> {
|
||||
return apiFetch('/nis2/roles/assign', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ role_id: roleId, user_id: userId }),
|
||||
})
|
||||
}
|
||||
|
||||
/** ISO Gap-Analyse abrufen */
|
||||
export async function getISOGapAnalysis(): Promise<ISOGapAnalysis> {
|
||||
return apiFetch<ISOGapAnalysis>('/iso/gap-analysis')
|
||||
}
|
||||
|
||||
/** ISO Mappings abrufen */
|
||||
export async function getISOMappings(category?: string): Promise<any> {
|
||||
const params = category ? `?category=${category}` : ''
|
||||
return apiFetch(`/iso/mappings${params}`)
|
||||
}
|
||||
246
admin-compliance/lib/sdk/gci/types.ts
Normal file
246
admin-compliance/lib/sdk/gci/types.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* GCI (Gesamt-Compliance-Index) Types
|
||||
* TypeScript definitions for the 4-level compliance scoring model
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// MATURITY LEVELS
|
||||
// =============================================================================
|
||||
|
||||
export type MaturityLevel = 'OPTIMIZED' | 'MANAGED' | 'DEFINED' | 'REACTIVE' | 'HIGH_RISK'
|
||||
|
||||
export const MATURITY_INFO: Record<MaturityLevel, { label: string; color: string; bgColor: string; borderColor: string; description: string }> = {
|
||||
OPTIMIZED: { label: 'Optimiert', color: 'text-green-700', bgColor: 'bg-green-100', borderColor: 'border-green-300', description: 'Kontinuierliche Verbesserung, proaktive Compliance' },
|
||||
MANAGED: { label: 'Gesteuert', color: 'text-blue-700', bgColor: 'bg-blue-100', borderColor: 'border-blue-300', description: 'Messbare Prozesse, regelmaessige Reviews' },
|
||||
DEFINED: { label: 'Definiert', color: 'text-yellow-700', bgColor: 'bg-yellow-100', borderColor: 'border-yellow-300', description: 'Dokumentierte Prozesse, erste Strukturen' },
|
||||
REACTIVE: { label: 'Reaktiv', color: 'text-orange-700', bgColor: 'bg-orange-100', borderColor: 'border-orange-300', description: 'Ad-hoc Massnahmen, wenig Struktur' },
|
||||
HIGH_RISK: { label: 'Hohes Risiko', color: 'text-red-700', bgColor: 'bg-red-100', borderColor: 'border-red-300', description: 'Erheblicher Handlungsbedarf, Compliance-Luecken' },
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEVEL 1: MODULE SCORE
|
||||
// =============================================================================
|
||||
|
||||
export interface ModuleScore {
|
||||
module_id: string
|
||||
module_name: string
|
||||
assigned: number
|
||||
completed: number
|
||||
raw_score: number
|
||||
validity_factor: number
|
||||
final_score: number
|
||||
risk_weight: number
|
||||
category: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEVEL 2: RISK-WEIGHTED AREA SCORE
|
||||
// =============================================================================
|
||||
|
||||
export interface RiskWeightedScore {
|
||||
area_id: string
|
||||
area_name: string
|
||||
modules: ModuleScore[]
|
||||
weighted_sum: number
|
||||
total_weight: number
|
||||
area_score: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEVEL 3: REGULATION AREA SCORE
|
||||
// =============================================================================
|
||||
|
||||
export interface RegulationAreaScore {
|
||||
regulation_id: string
|
||||
regulation_name: string
|
||||
score: number
|
||||
weight: number
|
||||
weighted_score: number
|
||||
module_count: number
|
||||
completed_count: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEVEL 4: GCI RESULT
|
||||
// =============================================================================
|
||||
|
||||
export interface AuditEntry {
|
||||
timestamp: string
|
||||
factor: string
|
||||
description: string
|
||||
value: number
|
||||
impact: 'positive' | 'negative' | 'neutral'
|
||||
}
|
||||
|
||||
export interface GCIResult {
|
||||
tenant_id: string
|
||||
gci_score: number
|
||||
maturity_level: MaturityLevel
|
||||
maturity_label: string
|
||||
calculated_at: string
|
||||
profile: string
|
||||
area_scores: RegulationAreaScore[]
|
||||
criticality_multiplier: number
|
||||
incident_adjustment: number
|
||||
audit_trail: AuditEntry[]
|
||||
}
|
||||
|
||||
export interface GCIBreakdown extends GCIResult {
|
||||
level1_modules: ModuleScore[]
|
||||
level2_areas: RiskWeightedScore[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GCI HISTORY
|
||||
// =============================================================================
|
||||
|
||||
export interface GCISnapshot {
|
||||
tenant_id: string
|
||||
score: number
|
||||
maturity_level: MaturityLevel
|
||||
area_scores: Record<string, number>
|
||||
calculated_at: string
|
||||
}
|
||||
|
||||
export interface GCIHistoryResponse {
|
||||
tenant_id: string
|
||||
snapshots: GCISnapshot[]
|
||||
total: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPLIANCE MATRIX
|
||||
// =============================================================================
|
||||
|
||||
export interface ComplianceMatrixEntry {
|
||||
role: string
|
||||
role_name: string
|
||||
regulations: Record<string, number>
|
||||
overall_score: number
|
||||
required_modules: number
|
||||
completed_modules: number
|
||||
}
|
||||
|
||||
export interface GCIMatrixResponse {
|
||||
tenant_id: string
|
||||
matrix: ComplianceMatrixEntry[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NIS2
|
||||
// =============================================================================
|
||||
|
||||
export interface NIS2Role {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
mandatory_modules: string[]
|
||||
priority: number
|
||||
}
|
||||
|
||||
export interface NIS2AreaScore {
|
||||
area_id: string
|
||||
area_name: string
|
||||
weight: number
|
||||
score: number
|
||||
weighted_score: number
|
||||
}
|
||||
|
||||
export interface NIS2RoleScore {
|
||||
role_id: string
|
||||
role_name: string
|
||||
assigned_users: number
|
||||
completion_rate: number
|
||||
modules_completed: number
|
||||
modules_required: number
|
||||
}
|
||||
|
||||
export interface NIS2Score {
|
||||
tenant_id: string
|
||||
overall_score: number
|
||||
maturity_level: string
|
||||
areas: NIS2AreaScore[]
|
||||
role_scores: NIS2RoleScore[]
|
||||
calculated_at: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ISO 27001
|
||||
// =============================================================================
|
||||
|
||||
export interface ISOControl {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category_id: string
|
||||
category_name: string
|
||||
control_type: string
|
||||
is_critical: boolean
|
||||
sdk_modules: string[]
|
||||
}
|
||||
|
||||
export interface ISOGap {
|
||||
control_id: string
|
||||
control_name: string
|
||||
category: string
|
||||
status: string
|
||||
priority: string
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
export interface ISOCategorySummary {
|
||||
category_id: string
|
||||
category_name: string
|
||||
total_controls: number
|
||||
covered_full: number
|
||||
covered_partial: number
|
||||
not_covered: number
|
||||
}
|
||||
|
||||
export interface ISOGapAnalysis {
|
||||
tenant_id: string
|
||||
total_controls: number
|
||||
covered_full: number
|
||||
covered_partial: number
|
||||
not_covered: number
|
||||
coverage_percent: number
|
||||
category_summaries: ISOCategorySummary[]
|
||||
gaps: ISOGap[]
|
||||
calculated_at: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WEIGHT PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export interface WeightProfile {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
weights: Record<string, number>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function getScoreColor(score: number): string {
|
||||
if (score >= 80) return 'text-green-600'
|
||||
if (score >= 60) return 'text-yellow-600'
|
||||
if (score >= 40) return 'text-orange-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
export function getScoreBgColor(score: number): string {
|
||||
if (score >= 80) return 'bg-green-500'
|
||||
if (score >= 60) return 'bg-yellow-500'
|
||||
if (score >= 40) return 'bg-orange-500'
|
||||
return 'bg-red-500'
|
||||
}
|
||||
|
||||
export function getScoreRingColor(score: number): string {
|
||||
if (score >= 80) return '#22c55e'
|
||||
if (score >= 60) return '#eab308'
|
||||
if (score >= 40) return '#f97316'
|
||||
return '#ef4444'
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/vendor"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/workshop"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/portfolio"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/gci"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
@@ -124,6 +125,10 @@ func main() {
|
||||
industryHandlers := handlers.NewIndustryHandlers()
|
||||
dsbHandlers := handlers.NewDSBHandlers(dsbStore)
|
||||
|
||||
// Initialize GCI engine and handlers
|
||||
gciEngine := gci.NewEngine()
|
||||
gciHandlers := handlers.NewGCIHandlers(gciEngine)
|
||||
|
||||
// Initialize middleware
|
||||
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
||||
|
||||
@@ -652,6 +657,29 @@ func main() {
|
||||
dsbRoutes.POST("/assignments/:id/communications", dsbHandlers.CreateCommunication)
|
||||
dsbRoutes.GET("/assignments/:id/communications", dsbHandlers.ListCommunications)
|
||||
}
|
||||
|
||||
// GCI routes - Gesamt-Compliance-Index
|
||||
gciRoutes := v1.Group("/gci")
|
||||
{
|
||||
// Core GCI endpoints
|
||||
gciRoutes.GET("/score", gciHandlers.GetScore)
|
||||
gciRoutes.GET("/score/breakdown", gciHandlers.GetScoreBreakdown)
|
||||
gciRoutes.GET("/score/history", gciHandlers.GetHistory)
|
||||
gciRoutes.GET("/matrix", gciHandlers.GetMatrix)
|
||||
gciRoutes.GET("/audit-trail", gciHandlers.GetAuditTrail)
|
||||
gciRoutes.GET("/profiles", gciHandlers.GetWeightProfiles)
|
||||
|
||||
// NIS2 sub-routes
|
||||
gciRoutes.GET("/nis2/score", gciHandlers.GetNIS2Score)
|
||||
gciRoutes.GET("/nis2/roles", gciHandlers.ListNIS2Roles)
|
||||
gciRoutes.POST("/nis2/roles/assign", gciHandlers.AssignNIS2Role)
|
||||
|
||||
// ISO 27001 sub-routes
|
||||
gciRoutes.GET("/iso/gap-analysis", gciHandlers.GetISOGapAnalysis)
|
||||
gciRoutes.GET("/iso/mappings", gciHandlers.ListISOMappings)
|
||||
gciRoutes.GET("/iso/mappings/:controlId", gciHandlers.GetISOMapping)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Create HTTP server
|
||||
|
||||
188
ai-compliance-sdk/internal/api/handlers/gci_handlers.go
Normal file
188
ai-compliance-sdk/internal/api/handlers/gci_handlers.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/gci"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type GCIHandlers struct {
|
||||
engine *gci.Engine
|
||||
}
|
||||
|
||||
func NewGCIHandlers(engine *gci.Engine) *GCIHandlers {
|
||||
return &GCIHandlers{engine: engine}
|
||||
}
|
||||
|
||||
// GetScore returns the GCI score for the current tenant
|
||||
// GET /sdk/v1/gci/score
|
||||
func (h *GCIHandlers) GetScore(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
profile := c.DefaultQuery("profile", "default")
|
||||
|
||||
result := h.engine.Calculate(tenantID, profile)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetScoreBreakdown returns the detailed 4-level GCI breakdown
|
||||
// GET /sdk/v1/gci/score/breakdown
|
||||
func (h *GCIHandlers) GetScoreBreakdown(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
profile := c.DefaultQuery("profile", "default")
|
||||
|
||||
breakdown := h.engine.CalculateBreakdown(tenantID, profile)
|
||||
c.JSON(http.StatusOK, breakdown)
|
||||
}
|
||||
|
||||
// GetHistory returns historical GCI snapshots for trend analysis
|
||||
// GET /sdk/v1/gci/score/history
|
||||
func (h *GCIHandlers) GetHistory(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
|
||||
history := h.engine.GetHistory(tenantID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"tenant_id": tenantID,
|
||||
"snapshots": history,
|
||||
"total": len(history),
|
||||
})
|
||||
}
|
||||
|
||||
// GetMatrix returns the compliance matrix (roles x regulations)
|
||||
// GET /sdk/v1/gci/matrix
|
||||
func (h *GCIHandlers) GetMatrix(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
|
||||
matrix := h.engine.GetMatrix(tenantID)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"tenant_id": tenantID,
|
||||
"matrix": matrix,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAuditTrail returns the audit trail for the latest GCI calculation
|
||||
// GET /sdk/v1/gci/audit-trail
|
||||
func (h *GCIHandlers) GetAuditTrail(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
profile := c.DefaultQuery("profile", "default")
|
||||
|
||||
result := h.engine.Calculate(tenantID, profile)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"tenant_id": tenantID,
|
||||
"gci_score": result.GCIScore,
|
||||
"audit_trail": result.AuditTrail,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNIS2Score returns the NIS2-specific compliance score
|
||||
// GET /sdk/v1/gci/nis2/score
|
||||
func (h *GCIHandlers) GetNIS2Score(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
|
||||
score := gci.CalculateNIS2Score(tenantID)
|
||||
c.JSON(http.StatusOK, score)
|
||||
}
|
||||
|
||||
// ListNIS2Roles returns available NIS2 responsibility roles
|
||||
// GET /sdk/v1/gci/nis2/roles
|
||||
func (h *GCIHandlers) ListNIS2Roles(c *gin.Context) {
|
||||
roles := gci.ListNIS2Roles()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"roles": roles,
|
||||
"total": len(roles),
|
||||
})
|
||||
}
|
||||
|
||||
// AssignNIS2Role assigns a NIS2 role to a user (stub - returns mock)
|
||||
// POST /sdk/v1/gci/nis2/roles/assign
|
||||
func (h *GCIHandlers) AssignNIS2Role(c *gin.Context) {
|
||||
var req struct {
|
||||
RoleID string `json:"role_id" binding:"required"`
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
role, found := gci.GetNIS2Role(req.RoleID)
|
||||
if !found {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "NIS2 role not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "assigned",
|
||||
"role": role,
|
||||
"user_id": req.UserID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetISOGapAnalysis returns the ISO 27001 gap analysis
|
||||
// GET /sdk/v1/gci/iso/gap-analysis
|
||||
func (h *GCIHandlers) GetISOGapAnalysis(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c).String()
|
||||
|
||||
analysis := gci.CalculateISOGapAnalysis(tenantID)
|
||||
c.JSON(http.StatusOK, analysis)
|
||||
}
|
||||
|
||||
// ListISOMappings returns all ISO 27001 control mappings
|
||||
// GET /sdk/v1/gci/iso/mappings
|
||||
func (h *GCIHandlers) ListISOMappings(c *gin.Context) {
|
||||
category := c.Query("category")
|
||||
|
||||
if category != "" {
|
||||
controls := gci.GetISOControlsByCategory(category)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"controls": controls,
|
||||
"total": len(controls),
|
||||
"category": category,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
categories := []string{"A.5", "A.6", "A.7", "A.8"}
|
||||
result := make(map[string][]gci.ISOControl)
|
||||
total := 0
|
||||
for _, cat := range categories {
|
||||
controls := gci.GetISOControlsByCategory(cat)
|
||||
if len(controls) > 0 {
|
||||
result[cat] = controls
|
||||
total += len(controls)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"categories": result,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
|
||||
// GetISOMapping returns a single ISO control by ID
|
||||
// GET /sdk/v1/gci/iso/mappings/:controlId
|
||||
func (h *GCIHandlers) GetISOMapping(c *gin.Context) {
|
||||
controlID := c.Param("controlId")
|
||||
|
||||
control, found := gci.GetISOControlByID(controlID)
|
||||
if !found {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "ISO control not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, control)
|
||||
}
|
||||
|
||||
// GetWeightProfiles returns available weighting profiles
|
||||
// GET /sdk/v1/gci/profiles
|
||||
func (h *GCIHandlers) GetWeightProfiles(c *gin.Context) {
|
||||
profiles := []string{"default", "nis2_relevant", "ki_nutzer"}
|
||||
result := make([]gci.WeightProfile, 0, len(profiles))
|
||||
for _, id := range profiles {
|
||||
result = append(result, gci.GetProfile(id))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"profiles": result,
|
||||
})
|
||||
}
|
||||
371
ai-compliance-sdk/internal/gci/engine.go
Normal file
371
ai-compliance-sdk/internal/gci/engine.go
Normal file
@@ -0,0 +1,371 @@
|
||||
package gci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Engine calculates the GCI score
|
||||
type Engine struct{}
|
||||
|
||||
// NewEngine creates a new GCI calculation engine
|
||||
func NewEngine() *Engine {
|
||||
return &Engine{}
|
||||
}
|
||||
|
||||
// Calculate computes the full GCI result for a tenant
|
||||
func (e *Engine) Calculate(tenantID string, profileID string) *GCIResult {
|
||||
now := time.Now()
|
||||
profile := GetProfile(profileID)
|
||||
auditTrail := []AuditEntry{}
|
||||
|
||||
// Step 1: Get module data (mock for now)
|
||||
modules := MockModuleData(tenantID)
|
||||
certDates := MockCertificateData()
|
||||
|
||||
// Step 2: Calculate Level 1 - Module Scores with validity
|
||||
for i := range modules {
|
||||
m := &modules[i]
|
||||
if m.Assigned > 0 {
|
||||
m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0
|
||||
}
|
||||
// Apply validity factor
|
||||
if validUntil, ok := certDates[m.ModuleID]; ok {
|
||||
m.ValidityFactor = CalculateValidityFactor(validUntil, now)
|
||||
} else {
|
||||
m.ValidityFactor = 1.0 // No certificate tracking = assume valid
|
||||
}
|
||||
m.FinalScore = m.RawScore * m.ValidityFactor
|
||||
|
||||
if m.ValidityFactor < 1.0 {
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "validity_decay",
|
||||
Description: fmt.Sprintf("Modul '%s': Gueltigkeitsfaktor %.2f (Zertifikat laeuft ab/abgelaufen)", m.ModuleName, m.ValidityFactor),
|
||||
Value: m.ValidityFactor,
|
||||
Impact: "negative",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Calculate Level 2 - Risk-Weighted Scores per area
|
||||
areaModules := map[string][]ModuleScore{
|
||||
"dsgvo": {},
|
||||
"nis2": {},
|
||||
"iso27001": {},
|
||||
"ai_act": {},
|
||||
}
|
||||
for _, m := range modules {
|
||||
if _, ok := areaModules[m.Category]; ok {
|
||||
areaModules[m.Category] = append(areaModules[m.Category], m)
|
||||
}
|
||||
}
|
||||
|
||||
level2Areas := []RiskWeightedScore{}
|
||||
areaNames := map[string]string{
|
||||
"dsgvo": "DSGVO",
|
||||
"nis2": "NIS2",
|
||||
"iso27001": "ISO 27001",
|
||||
"ai_act": "EU AI Act",
|
||||
}
|
||||
|
||||
for areaID, mods := range areaModules {
|
||||
rws := RiskWeightedScore{
|
||||
AreaID: areaID,
|
||||
AreaName: areaNames[areaID],
|
||||
Modules: mods,
|
||||
}
|
||||
for _, m := range mods {
|
||||
rws.WeightedSum += m.FinalScore * m.RiskWeight
|
||||
rws.TotalWeight += m.RiskWeight
|
||||
}
|
||||
if rws.TotalWeight > 0 {
|
||||
rws.AreaScore = rws.WeightedSum / rws.TotalWeight
|
||||
}
|
||||
level2Areas = append(level2Areas, rws)
|
||||
}
|
||||
|
||||
// Step 4: Calculate Level 3 - Regulation Area Scores
|
||||
areaScores := []RegulationAreaScore{}
|
||||
for _, rws := range level2Areas {
|
||||
weight := profile.Weights[rws.AreaID]
|
||||
completedCount := 0
|
||||
for _, m := range rws.Modules {
|
||||
if m.Completed >= m.Assigned && m.Assigned > 0 {
|
||||
completedCount++
|
||||
}
|
||||
}
|
||||
ras := RegulationAreaScore{
|
||||
RegulationID: rws.AreaID,
|
||||
RegulationName: rws.AreaName,
|
||||
Score: math.Round(rws.AreaScore*100) / 100,
|
||||
Weight: weight,
|
||||
WeightedScore: rws.AreaScore * weight,
|
||||
ModuleCount: len(rws.Modules),
|
||||
CompletedCount: completedCount,
|
||||
}
|
||||
areaScores = append(areaScores, ras)
|
||||
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "area_score",
|
||||
Description: fmt.Sprintf("Bereich '%s': Score %.1f, Gewicht %.0f%%", rws.AreaName, rws.AreaScore, weight*100),
|
||||
Value: rws.AreaScore,
|
||||
Impact: "neutral",
|
||||
})
|
||||
}
|
||||
|
||||
// Step 5: Calculate raw GCI
|
||||
rawGCI := 0.0
|
||||
totalWeight := 0.0
|
||||
for _, ras := range areaScores {
|
||||
rawGCI += ras.WeightedScore
|
||||
totalWeight += ras.Weight
|
||||
}
|
||||
if totalWeight > 0 {
|
||||
rawGCI = rawGCI / totalWeight
|
||||
}
|
||||
|
||||
// Step 6: Apply Criticality Multiplier
|
||||
criticalityMult := calculateCriticalityMultiplier(modules)
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "criticality_multiplier",
|
||||
Description: fmt.Sprintf("Kritikalitaetsmultiplikator: %.3f", criticalityMult),
|
||||
Value: criticalityMult,
|
||||
Impact: func() string {
|
||||
if criticalityMult < 1.0 {
|
||||
return "negative"
|
||||
}
|
||||
return "neutral"
|
||||
}(),
|
||||
})
|
||||
|
||||
// Step 7: Apply Incident Adjustment
|
||||
openInc, critInc := MockIncidentData()
|
||||
incidentAdj := calculateIncidentAdjustment(openInc, critInc)
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "incident_adjustment",
|
||||
Description: fmt.Sprintf("Vorfallsanpassung: %.3f (%d offen, %d kritisch)", incidentAdj, openInc, critInc),
|
||||
Value: incidentAdj,
|
||||
Impact: "negative",
|
||||
})
|
||||
|
||||
// Step 8: Final GCI
|
||||
finalGCI := rawGCI * criticalityMult * incidentAdj
|
||||
finalGCI = math.Max(0, math.Min(100, math.Round(finalGCI*10)/10))
|
||||
|
||||
// Step 9: Determine Maturity Level
|
||||
maturity := determineMaturityLevel(finalGCI)
|
||||
|
||||
auditTrail = append(auditTrail, AuditEntry{
|
||||
Timestamp: now,
|
||||
Factor: "final_gci",
|
||||
Description: fmt.Sprintf("GCI-Endergebnis: %.1f → Reifegrad: %s", finalGCI, MaturityLabels[maturity]),
|
||||
Value: finalGCI,
|
||||
Impact: "neutral",
|
||||
})
|
||||
|
||||
return &GCIResult{
|
||||
TenantID: tenantID,
|
||||
GCIScore: finalGCI,
|
||||
MaturityLevel: maturity,
|
||||
MaturityLabel: MaturityLabels[maturity],
|
||||
CalculatedAt: now,
|
||||
Profile: profileID,
|
||||
AreaScores: areaScores,
|
||||
CriticalityMult: criticalityMult,
|
||||
IncidentAdj: incidentAdj,
|
||||
AuditTrail: auditTrail,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateBreakdown returns the full 4-level breakdown
|
||||
func (e *Engine) CalculateBreakdown(tenantID string, profileID string) *GCIBreakdown {
|
||||
result := e.Calculate(tenantID, profileID)
|
||||
modules := MockModuleData(tenantID)
|
||||
certDates := MockCertificateData()
|
||||
now := time.Now()
|
||||
|
||||
// Recalculate module scores for the breakdown
|
||||
for i := range modules {
|
||||
m := &modules[i]
|
||||
if m.Assigned > 0 {
|
||||
m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0
|
||||
}
|
||||
if validUntil, ok := certDates[m.ModuleID]; ok {
|
||||
m.ValidityFactor = CalculateValidityFactor(validUntil, now)
|
||||
} else {
|
||||
m.ValidityFactor = 1.0
|
||||
}
|
||||
m.FinalScore = m.RawScore * m.ValidityFactor
|
||||
}
|
||||
|
||||
// Build Level 2 areas
|
||||
areaModules := map[string][]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
areaModules[m.Category] = append(areaModules[m.Category], m)
|
||||
}
|
||||
|
||||
areaNames := map[string]string{"dsgvo": "DSGVO", "nis2": "NIS2", "iso27001": "ISO 27001", "ai_act": "EU AI Act"}
|
||||
level2 := []RiskWeightedScore{}
|
||||
for areaID, mods := range areaModules {
|
||||
rws := RiskWeightedScore{AreaID: areaID, AreaName: areaNames[areaID], Modules: mods}
|
||||
for _, m := range mods {
|
||||
rws.WeightedSum += m.FinalScore * m.RiskWeight
|
||||
rws.TotalWeight += m.RiskWeight
|
||||
}
|
||||
if rws.TotalWeight > 0 {
|
||||
rws.AreaScore = rws.WeightedSum / rws.TotalWeight
|
||||
}
|
||||
level2 = append(level2, rws)
|
||||
}
|
||||
|
||||
return &GCIBreakdown{
|
||||
GCIResult: *result,
|
||||
Level1Modules: modules,
|
||||
Level2Areas: level2,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHistory returns historical GCI snapshots
|
||||
func (e *Engine) GetHistory(tenantID string) []GCISnapshot {
|
||||
// Add current score to history
|
||||
result := e.Calculate(tenantID, "default")
|
||||
history := MockGCIHistory(tenantID)
|
||||
current := GCISnapshot{
|
||||
TenantID: tenantID,
|
||||
Score: result.GCIScore,
|
||||
MaturityLevel: result.MaturityLevel,
|
||||
AreaScores: make(map[string]float64),
|
||||
CalculatedAt: result.CalculatedAt,
|
||||
}
|
||||
for _, as := range result.AreaScores {
|
||||
current.AreaScores[as.RegulationID] = as.Score
|
||||
}
|
||||
history = append(history, current)
|
||||
return history
|
||||
}
|
||||
|
||||
// GetMatrix returns the compliance matrix (roles x regulations)
|
||||
func (e *Engine) GetMatrix(tenantID string) []ComplianceMatrixEntry {
|
||||
modules := MockModuleData(tenantID)
|
||||
|
||||
roles := []struct {
|
||||
ID string
|
||||
Name string
|
||||
}{
|
||||
{"management", "Geschaeftsfuehrung"},
|
||||
{"it_security", "IT-Sicherheit / CISO"},
|
||||
{"data_protection", "Datenschutz / DSB"},
|
||||
{"hr", "Personalwesen"},
|
||||
{"general", "Allgemeine Mitarbeiter"},
|
||||
}
|
||||
|
||||
// Define which modules are relevant per role
|
||||
roleModules := map[string][]string{
|
||||
"management": {"dsgvo-grundlagen", "nis2-management", "ai-governance", "iso-isms"},
|
||||
"it_security": {"nis2-risikomanagement", "nis2-incident-response", "iso-zugangssteuerung", "iso-kryptografie", "ai-hochrisiko"},
|
||||
"data_protection": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "dsgvo-tom", "dsgvo-dsfa", "dsgvo-auftragsverarbeitung"},
|
||||
"hr": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "nis2-management"},
|
||||
"general": {"dsgvo-grundlagen", "nis2-risikomanagement", "ai-risikokategorien", "ai-transparenz"},
|
||||
}
|
||||
|
||||
moduleMap := map[string]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
moduleMap[m.ModuleID] = m
|
||||
}
|
||||
|
||||
entries := []ComplianceMatrixEntry{}
|
||||
for _, role := range roles {
|
||||
entry := ComplianceMatrixEntry{
|
||||
Role: role.ID,
|
||||
RoleName: role.Name,
|
||||
Regulations: map[string]float64{},
|
||||
}
|
||||
|
||||
regScores := map[string][]float64{}
|
||||
requiredModuleIDs := roleModules[role.ID]
|
||||
entry.RequiredModules = len(requiredModuleIDs)
|
||||
|
||||
for _, modID := range requiredModuleIDs {
|
||||
if m, ok := moduleMap[modID]; ok {
|
||||
score := 0.0
|
||||
if m.Assigned > 0 {
|
||||
score = float64(m.Completed) / float64(m.Assigned) * 100
|
||||
}
|
||||
regScores[m.Category] = append(regScores[m.Category], score)
|
||||
if m.Completed >= m.Assigned && m.Assigned > 0 {
|
||||
entry.CompletedModules++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalScore := 0.0
|
||||
count := 0
|
||||
for reg, scores := range regScores {
|
||||
sum := 0.0
|
||||
for _, s := range scores {
|
||||
sum += s
|
||||
}
|
||||
avg := sum / float64(len(scores))
|
||||
entry.Regulations[reg] = math.Round(avg*10) / 10
|
||||
totalScore += avg
|
||||
count++
|
||||
}
|
||||
if count > 0 {
|
||||
entry.OverallScore = math.Round(totalScore/float64(count)*10) / 10
|
||||
}
|
||||
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func calculateCriticalityMultiplier(modules []ModuleScore) float64 {
|
||||
criticalModules := 0
|
||||
criticalLow := 0
|
||||
for _, m := range modules {
|
||||
if m.RiskWeight >= 2.5 {
|
||||
criticalModules++
|
||||
if m.FinalScore < 50 {
|
||||
criticalLow++
|
||||
}
|
||||
}
|
||||
}
|
||||
if criticalModules == 0 {
|
||||
return 1.0
|
||||
}
|
||||
// Reduce score if critical modules have low completion
|
||||
ratio := float64(criticalLow) / float64(criticalModules)
|
||||
return 1.0 - (ratio * 0.15) // max 15% reduction
|
||||
}
|
||||
|
||||
func calculateIncidentAdjustment(openIncidents, criticalIncidents int) float64 {
|
||||
adj := 1.0
|
||||
// Each open incident reduces by 1%
|
||||
adj -= float64(openIncidents) * 0.01
|
||||
// Each critical incident reduces by additional 3%
|
||||
adj -= float64(criticalIncidents) * 0.03
|
||||
return math.Max(0.8, adj) // minimum 80% (max 20% reduction)
|
||||
}
|
||||
|
||||
func determineMaturityLevel(score float64) string {
|
||||
switch {
|
||||
case score >= 90:
|
||||
return MaturityOptimized
|
||||
case score >= 75:
|
||||
return MaturityManaged
|
||||
case score >= 60:
|
||||
return MaturityDefined
|
||||
case score >= 40:
|
||||
return MaturityReactive
|
||||
default:
|
||||
return MaturityHighRisk
|
||||
}
|
||||
}
|
||||
188
ai-compliance-sdk/internal/gci/iso_gap_analysis.go
Normal file
188
ai-compliance-sdk/internal/gci/iso_gap_analysis.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package gci
|
||||
|
||||
import "math"
|
||||
|
||||
// ISOGapAnalysis represents the complete ISO 27001 gap analysis
|
||||
type ISOGapAnalysis struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
TotalControls int `json:"total_controls"`
|
||||
CoveredFull int `json:"covered_full"`
|
||||
CoveredPartial int `json:"covered_partial"`
|
||||
NotCovered int `json:"not_covered"`
|
||||
CoveragePercent float64 `json:"coverage_percent"`
|
||||
CategorySummaries []ISOCategorySummary `json:"category_summaries"`
|
||||
ControlDetails []ISOControlDetail `json:"control_details"`
|
||||
Gaps []ISOGap `json:"gaps"`
|
||||
}
|
||||
|
||||
// ISOControlDetail shows coverage status for a single control
|
||||
type ISOControlDetail struct {
|
||||
Control ISOControl `json:"control"`
|
||||
CoverageLevel string `json:"coverage_level"` // full, partial, none
|
||||
CoveredBy []string `json:"covered_by"` // module IDs
|
||||
Score float64 `json:"score"` // 0-100
|
||||
}
|
||||
|
||||
// ISOGap represents an identified gap in ISO coverage
|
||||
type ISOGap struct {
|
||||
ControlID string `json:"control_id"`
|
||||
ControlName string `json:"control_name"`
|
||||
Category string `json:"category"`
|
||||
Priority string `json:"priority"` // high, medium, low
|
||||
Recommendation string `json:"recommendation"`
|
||||
}
|
||||
|
||||
// CalculateISOGapAnalysis performs the ISO 27001 gap analysis
|
||||
func CalculateISOGapAnalysis(tenantID string) *ISOGapAnalysis {
|
||||
modules := MockModuleData(tenantID)
|
||||
moduleMap := map[string]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
moduleMap[m.ModuleID] = m
|
||||
}
|
||||
|
||||
// Build reverse mapping: control -> modules covering it
|
||||
controlCoverage := map[string][]string{}
|
||||
controlCoverageLevel := map[string]string{}
|
||||
for _, mapping := range DefaultISOModuleMappings {
|
||||
for _, controlID := range mapping.ISOControls {
|
||||
controlCoverage[controlID] = append(controlCoverage[controlID], mapping.ModuleID)
|
||||
// Use the highest coverage level
|
||||
existingLevel := controlCoverageLevel[controlID]
|
||||
if mapping.CoverageLevel == "full" || existingLevel == "" {
|
||||
controlCoverageLevel[controlID] = mapping.CoverageLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze each control
|
||||
details := []ISOControlDetail{}
|
||||
gaps := []ISOGap{}
|
||||
coveredFull := 0
|
||||
coveredPartial := 0
|
||||
notCovered := 0
|
||||
|
||||
categoryCounts := map[string]*ISOCategorySummary{
|
||||
"A.5": {CategoryID: "A.5", CategoryName: "Organisatorische Massnahmen"},
|
||||
"A.6": {CategoryID: "A.6", CategoryName: "Personelle Massnahmen"},
|
||||
"A.7": {CategoryID: "A.7", CategoryName: "Physische Massnahmen"},
|
||||
"A.8": {CategoryID: "A.8", CategoryName: "Technologische Massnahmen"},
|
||||
}
|
||||
|
||||
for _, control := range ISOControls {
|
||||
coveredBy := controlCoverage[control.ID]
|
||||
level := controlCoverageLevel[control.ID]
|
||||
|
||||
if len(coveredBy) == 0 {
|
||||
level = "none"
|
||||
}
|
||||
|
||||
// Calculate score based on module completion
|
||||
score := 0.0
|
||||
if len(coveredBy) > 0 {
|
||||
scoreSum := 0.0
|
||||
count := 0
|
||||
for _, modID := range coveredBy {
|
||||
if m, ok := moduleMap[modID]; ok && m.Assigned > 0 {
|
||||
scoreSum += float64(m.Completed) / float64(m.Assigned) * 100
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
score = scoreSum / float64(count)
|
||||
}
|
||||
// Adjust for coverage level
|
||||
if level == "partial" {
|
||||
score *= 0.7 // partial coverage reduces effective score
|
||||
}
|
||||
}
|
||||
|
||||
detail := ISOControlDetail{
|
||||
Control: control,
|
||||
CoverageLevel: level,
|
||||
CoveredBy: coveredBy,
|
||||
Score: math.Round(score*10) / 10,
|
||||
}
|
||||
details = append(details, detail)
|
||||
|
||||
// Count by category
|
||||
cat := categoryCounts[control.CategoryID]
|
||||
if cat != nil {
|
||||
cat.TotalControls++
|
||||
switch level {
|
||||
case "full":
|
||||
coveredFull++
|
||||
cat.CoveredFull++
|
||||
case "partial":
|
||||
coveredPartial++
|
||||
cat.CoveredPartial++
|
||||
default:
|
||||
notCovered++
|
||||
cat.NotCovered++
|
||||
// Generate gap recommendation
|
||||
gap := ISOGap{
|
||||
ControlID: control.ID,
|
||||
ControlName: control.Name,
|
||||
Category: control.Category,
|
||||
Priority: determineGapPriority(control),
|
||||
Recommendation: generateGapRecommendation(control),
|
||||
}
|
||||
gaps = append(gaps, gap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalControls := len(ISOControls)
|
||||
coveragePercent := 0.0
|
||||
if totalControls > 0 {
|
||||
coveragePercent = math.Round(float64(coveredFull+coveredPartial)/float64(totalControls)*100*10) / 10
|
||||
}
|
||||
|
||||
summaries := []ISOCategorySummary{}
|
||||
for _, catID := range []string{"A.5", "A.6", "A.7", "A.8"} {
|
||||
if cat, ok := categoryCounts[catID]; ok {
|
||||
summaries = append(summaries, *cat)
|
||||
}
|
||||
}
|
||||
|
||||
return &ISOGapAnalysis{
|
||||
TenantID: tenantID,
|
||||
TotalControls: totalControls,
|
||||
CoveredFull: coveredFull,
|
||||
CoveredPartial: coveredPartial,
|
||||
NotCovered: notCovered,
|
||||
CoveragePercent: coveragePercent,
|
||||
CategorySummaries: summaries,
|
||||
ControlDetails: details,
|
||||
Gaps: gaps,
|
||||
}
|
||||
}
|
||||
|
||||
func determineGapPriority(control ISOControl) string {
|
||||
// High priority for access, incident, and data protection controls
|
||||
highPriority := map[string]bool{
|
||||
"A.5.15": true, "A.5.17": true, "A.5.24": true, "A.5.26": true,
|
||||
"A.5.34": true, "A.8.2": true, "A.8.5": true, "A.8.7": true,
|
||||
"A.8.10": true, "A.8.20": true,
|
||||
}
|
||||
if highPriority[control.ID] {
|
||||
return "high"
|
||||
}
|
||||
// Medium for organizational and people controls
|
||||
if control.CategoryID == "A.5" || control.CategoryID == "A.6" {
|
||||
return "medium"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
func generateGapRecommendation(control ISOControl) string {
|
||||
recommendations := map[string]string{
|
||||
"organizational": "Erstellen Sie eine Richtlinie und weisen Sie Verantwortlichkeiten zu fuer: " + control.Name,
|
||||
"people": "Implementieren Sie Schulungen und Prozesse fuer: " + control.Name,
|
||||
"physical": "Definieren Sie physische Sicherheitsmassnahmen fuer: " + control.Name,
|
||||
"technological": "Implementieren Sie technische Kontrollen fuer: " + control.Name,
|
||||
}
|
||||
if rec, ok := recommendations[control.Category]; ok {
|
||||
return rec
|
||||
}
|
||||
return "Massnahmen implementieren fuer: " + control.Name
|
||||
}
|
||||
207
ai-compliance-sdk/internal/gci/iso_mapping.go
Normal file
207
ai-compliance-sdk/internal/gci/iso_mapping.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package gci
|
||||
|
||||
// ISOControl represents an ISO 27001:2022 Annex A control
|
||||
type ISOControl struct {
|
||||
ID string `json:"id"` // e.g. "A.5.1"
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"` // organizational, people, physical, technological
|
||||
CategoryID string `json:"category_id"` // A.5, A.6, A.7, A.8
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// ISOModuleMapping maps a course/module to ISO controls
|
||||
type ISOModuleMapping struct {
|
||||
ModuleID string `json:"module_id"`
|
||||
ModuleName string `json:"module_name"`
|
||||
ISOControls []string `json:"iso_controls"` // control IDs
|
||||
CoverageLevel string `json:"coverage_level"` // full, partial, none
|
||||
}
|
||||
|
||||
// ISO 27001:2022 Annex A controls (representative selection)
|
||||
var ISOControls = []ISOControl{
|
||||
// A.5 Organizational Controls (37 controls, showing key ones)
|
||||
{ID: "A.5.1", Name: "Informationssicherheitsrichtlinien", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheitsleitlinie und themenspezifische Richtlinien"},
|
||||
{ID: "A.5.2", Name: "Rollen und Verantwortlichkeiten", Category: "organizational", CategoryID: "A.5", Description: "Definition und Zuweisung von Informationssicherheitsrollen"},
|
||||
{ID: "A.5.3", Name: "Aufgabentrennung", Category: "organizational", CategoryID: "A.5", Description: "Trennung von konfligierenden Aufgaben und Verantwortlichkeiten"},
|
||||
{ID: "A.5.4", Name: "Managementverantwortung", Category: "organizational", CategoryID: "A.5", Description: "Fuehrungskraefte muessen Sicherheitsrichtlinien einhalten und durchsetzen"},
|
||||
{ID: "A.5.5", Name: "Kontakt mit Behoerden", Category: "organizational", CategoryID: "A.5", Description: "Pflege von Kontakten zu relevanten Aufsichtsbehoerden"},
|
||||
{ID: "A.5.6", Name: "Kontakt mit Interessengruppen", Category: "organizational", CategoryID: "A.5", Description: "Kontakt zu Fachgruppen und Sicherheitsforen"},
|
||||
{ID: "A.5.7", Name: "Bedrohungsintelligenz", Category: "organizational", CategoryID: "A.5", Description: "Sammlung und Analyse von Bedrohungsinformationen"},
|
||||
{ID: "A.5.8", Name: "Informationssicherheit im Projektmanagement", Category: "organizational", CategoryID: "A.5", Description: "Integration von Sicherheit in Projektmanagement"},
|
||||
{ID: "A.5.9", Name: "Inventar der Informationswerte", Category: "organizational", CategoryID: "A.5", Description: "Inventarisierung und Verwaltung von Informationswerten"},
|
||||
{ID: "A.5.10", Name: "Zuleassige Nutzung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer die zuleassige Nutzung von Informationswerten"},
|
||||
{ID: "A.5.11", Name: "Rueckgabe von Werten", Category: "organizational", CategoryID: "A.5", Description: "Rueckgabe von Werten bei Beendigung"},
|
||||
{ID: "A.5.12", Name: "Klassifizierung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Klassifizierungsschema fuer Informationen"},
|
||||
{ID: "A.5.13", Name: "Kennzeichnung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Kennzeichnung gemaess Klassifizierung"},
|
||||
{ID: "A.5.14", Name: "Informationsuebertragung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer sichere Informationsuebertragung"},
|
||||
{ID: "A.5.15", Name: "Zugangssteuerung", Category: "organizational", CategoryID: "A.5", Description: "Zugangssteuerungsrichtlinie"},
|
||||
{ID: "A.5.16", Name: "Identitaetsmanagement", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung des Lebenszyklus von Identitaeten"},
|
||||
{ID: "A.5.17", Name: "Authentifizierungsinformationen", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung von Authentifizierungsinformationen"},
|
||||
{ID: "A.5.18", Name: "Zugriffsrechte", Category: "organizational", CategoryID: "A.5", Description: "Vergabe, Pruefung und Entzug von Zugriffsrechten"},
|
||||
{ID: "A.5.19", Name: "Informationssicherheit in Lieferantenbeziehungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsanforderungen an Lieferanten"},
|
||||
{ID: "A.5.20", Name: "Informationssicherheit in Lieferantenvereinbarungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsklauseln in Vertraegen"},
|
||||
{ID: "A.5.21", Name: "IKT-Lieferkette", Category: "organizational", CategoryID: "A.5", Description: "Management der IKT-Lieferkette"},
|
||||
{ID: "A.5.22", Name: "Ueberwachung von Lieferantenservices", Category: "organizational", CategoryID: "A.5", Description: "Ueberwachung und Pruefung von Lieferantenservices"},
|
||||
{ID: "A.5.23", Name: "Cloud-Sicherheit", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheit fuer Cloud-Dienste"},
|
||||
{ID: "A.5.24", Name: "Vorfallsmanagement - Planung", Category: "organizational", CategoryID: "A.5", Description: "Planung und Vorbereitung des Vorfallsmanagements"},
|
||||
{ID: "A.5.25", Name: "Vorfallsbeurteilung", Category: "organizational", CategoryID: "A.5", Description: "Beurteilung und Entscheidung ueber Sicherheitsereignisse"},
|
||||
{ID: "A.5.26", Name: "Vorfallsreaktion", Category: "organizational", CategoryID: "A.5", Description: "Reaktion auf Sicherheitsvorfaelle"},
|
||||
{ID: "A.5.27", Name: "Aus Vorfaellen lernen", Category: "organizational", CategoryID: "A.5", Description: "Lessons Learned aus Sicherheitsvorfaellen"},
|
||||
{ID: "A.5.28", Name: "Beweissicherung", Category: "organizational", CategoryID: "A.5", Description: "Identifikation und Sicherung von Beweisen"},
|
||||
{ID: "A.5.29", Name: "Informationssicherheit bei Stoerungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheit waehrend Stoerungen und Krisen"},
|
||||
{ID: "A.5.30", Name: "IKT-Bereitschaft fuer Business Continuity", Category: "organizational", CategoryID: "A.5", Description: "IKT-Bereitschaft zur Unterstuetzung der Geschaeftskontinuitaet"},
|
||||
{ID: "A.5.31", Name: "Rechtliche Anforderungen", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung rechtlicher und vertraglicher Anforderungen"},
|
||||
{ID: "A.5.32", Name: "Geistige Eigentumsrechte", Category: "organizational", CategoryID: "A.5", Description: "Schutz geistigen Eigentums"},
|
||||
{ID: "A.5.33", Name: "Schutz von Aufzeichnungen", Category: "organizational", CategoryID: "A.5", Description: "Schutz von Aufzeichnungen vor Verlust und Manipulation"},
|
||||
{ID: "A.5.34", Name: "Datenschutz und PII", Category: "organizational", CategoryID: "A.5", Description: "Datenschutz und Schutz personenbezogener Daten"},
|
||||
{ID: "A.5.35", Name: "Unabhaengige Ueberpruefung", Category: "organizational", CategoryID: "A.5", Description: "Unabhaengige Ueberpruefung der Informationssicherheit"},
|
||||
{ID: "A.5.36", Name: "Richtlinienkonformitaet", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung von Richtlinien und Standards"},
|
||||
{ID: "A.5.37", Name: "Dokumentierte Betriebsverfahren", Category: "organizational", CategoryID: "A.5", Description: "Dokumentation von Betriebsverfahren"},
|
||||
|
||||
// A.6 People Controls (8 controls)
|
||||
{ID: "A.6.1", Name: "Ueberpruefen", Category: "people", CategoryID: "A.6", Description: "Hintergrundpruefungen vor der Einstellung"},
|
||||
{ID: "A.6.2", Name: "Beschaeftigungsbedingungen", Category: "people", CategoryID: "A.6", Description: "Sicherheitsanforderungen in Arbeitsvertraegen"},
|
||||
{ID: "A.6.3", Name: "Sensibilisierung und Schulung", Category: "people", CategoryID: "A.6", Description: "Awareness-Programme und Schulungen"},
|
||||
{ID: "A.6.4", Name: "Disziplinarverfahren", Category: "people", CategoryID: "A.6", Description: "Formales Disziplinarverfahren"},
|
||||
{ID: "A.6.5", Name: "Verantwortlichkeiten nach Beendigung", Category: "people", CategoryID: "A.6", Description: "Sicherheitspflichten nach Beendigung des Beschaeftigungsverhaeltnisses"},
|
||||
{ID: "A.6.6", Name: "Vertraulichkeitsvereinbarungen", Category: "people", CategoryID: "A.6", Description: "Vertraulichkeits- und Geheimhaltungsvereinbarungen"},
|
||||
{ID: "A.6.7", Name: "Remote-Arbeit", Category: "people", CategoryID: "A.6", Description: "Sicherheitsmassnahmen fuer Remote-Arbeit"},
|
||||
{ID: "A.6.8", Name: "Meldung von Sicherheitsereignissen", Category: "people", CategoryID: "A.6", Description: "Mechanismen zur Meldung von Sicherheitsereignissen"},
|
||||
|
||||
// A.7 Physical Controls (14 controls, showing key ones)
|
||||
{ID: "A.7.1", Name: "Physische Sicherheitsperimeter", Category: "physical", CategoryID: "A.7", Description: "Definition physischer Sicherheitszonen"},
|
||||
{ID: "A.7.2", Name: "Physischer Zutritt", Category: "physical", CategoryID: "A.7", Description: "Zutrittskontrolle zu Sicherheitszonen"},
|
||||
{ID: "A.7.3", Name: "Sicherung von Bueros und Raeumen", Category: "physical", CategoryID: "A.7", Description: "Physische Sicherheit fuer Bueros und Raeume"},
|
||||
{ID: "A.7.4", Name: "Physische Sicherheitsueberwachung", Category: "physical", CategoryID: "A.7", Description: "Ueberwachung physischer Sicherheit"},
|
||||
{ID: "A.7.5", Name: "Schutz vor Umweltgefahren", Category: "physical", CategoryID: "A.7", Description: "Schutz gegen natuerliche und menschgemachte Gefahren"},
|
||||
{ID: "A.7.6", Name: "Arbeit in Sicherheitszonen", Category: "physical", CategoryID: "A.7", Description: "Regeln fuer das Arbeiten in Sicherheitszonen"},
|
||||
{ID: "A.7.7", Name: "Aufgeraemter Schreibtisch", Category: "physical", CategoryID: "A.7", Description: "Clean-Desk und Clear-Screen Richtlinie"},
|
||||
{ID: "A.7.8", Name: "Geraeteplatzierung", Category: "physical", CategoryID: "A.7", Description: "Platzierung und Schutz von Geraeten"},
|
||||
{ID: "A.7.9", Name: "Sicherheit von Geraeten ausserhalb", Category: "physical", CategoryID: "A.7", Description: "Sicherheit von Geraeten ausserhalb der Raeumlichkeiten"},
|
||||
{ID: "A.7.10", Name: "Speichermedien", Category: "physical", CategoryID: "A.7", Description: "Verwaltung von Speichermedien"},
|
||||
{ID: "A.7.11", Name: "Versorgungseinrichtungen", Category: "physical", CategoryID: "A.7", Description: "Schutz vor Ausfaellen der Versorgungseinrichtungen"},
|
||||
{ID: "A.7.12", Name: "Verkabelungssicherheit", Category: "physical", CategoryID: "A.7", Description: "Schutz der Verkabelung"},
|
||||
{ID: "A.7.13", Name: "Instandhaltung von Geraeten", Category: "physical", CategoryID: "A.7", Description: "Korrekte Instandhaltung von Geraeten"},
|
||||
{ID: "A.7.14", Name: "Sichere Entsorgung", Category: "physical", CategoryID: "A.7", Description: "Sichere Entsorgung oder Wiederverwendung"},
|
||||
|
||||
// A.8 Technological Controls (34 controls, showing key ones)
|
||||
{ID: "A.8.1", Name: "Endbenutzergeraete", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Endbenutzergeraeten"},
|
||||
{ID: "A.8.2", Name: "Privilegierte Zugriffsrechte", Category: "technological", CategoryID: "A.8", Description: "Verwaltung privilegierter Zugriffsrechte"},
|
||||
{ID: "A.8.3", Name: "Informationszugangsbeschraenkung", Category: "technological", CategoryID: "A.8", Description: "Beschraenkung des Zugangs zu Informationen"},
|
||||
{ID: "A.8.4", Name: "Zugang zu Quellcode", Category: "technological", CategoryID: "A.8", Description: "Sicherer Zugang zu Quellcode"},
|
||||
{ID: "A.8.5", Name: "Sichere Authentifizierung", Category: "technological", CategoryID: "A.8", Description: "Sichere Authentifizierungstechnologien"},
|
||||
{ID: "A.8.6", Name: "Kapazitaetsmanagement", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung und Anpassung der Kapazitaet"},
|
||||
{ID: "A.8.7", Name: "Schutz gegen Malware", Category: "technological", CategoryID: "A.8", Description: "Schutz vor Schadprogrammen"},
|
||||
{ID: "A.8.8", Name: "Management technischer Schwachstellen", Category: "technological", CategoryID: "A.8", Description: "Identifikation und Behebung von Schwachstellen"},
|
||||
{ID: "A.8.9", Name: "Konfigurationsmanagement", Category: "technological", CategoryID: "A.8", Description: "Sichere Konfiguration von Systemen"},
|
||||
{ID: "A.8.10", Name: "Datensicherung", Category: "technological", CategoryID: "A.8", Description: "Erstellen und Testen von Datensicherungen"},
|
||||
{ID: "A.8.11", Name: "Datenredundanz", Category: "technological", CategoryID: "A.8", Description: "Redundanz von Informationsverarbeitungseinrichtungen"},
|
||||
{ID: "A.8.12", Name: "Protokollierung", Category: "technological", CategoryID: "A.8", Description: "Aufzeichnung und Ueberwachung von Aktivitaeten"},
|
||||
{ID: "A.8.13", Name: "Ueberwachung von Aktivitaeten", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung von Netzwerken und Systemen"},
|
||||
{ID: "A.8.14", Name: "Zeitsynchronisation", Category: "technological", CategoryID: "A.8", Description: "Synchronisation von Uhren"},
|
||||
{ID: "A.8.15", Name: "Nutzung privilegierter Hilfsprogramme", Category: "technological", CategoryID: "A.8", Description: "Einschraenkung privilegierter Hilfsprogramme"},
|
||||
{ID: "A.8.16", Name: "Softwareinstallation", Category: "technological", CategoryID: "A.8", Description: "Kontrolle der Softwareinstallation"},
|
||||
{ID: "A.8.17", Name: "Netzwerksicherheit", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Netzwerken"},
|
||||
{ID: "A.8.18", Name: "Netzwerksegmentierung", Category: "technological", CategoryID: "A.8", Description: "Segmentierung von Netzwerken"},
|
||||
{ID: "A.8.19", Name: "Webfilterung", Category: "technological", CategoryID: "A.8", Description: "Filterung des Webzugangs"},
|
||||
{ID: "A.8.20", Name: "Kryptografie", Category: "technological", CategoryID: "A.8", Description: "Einsatz kryptografischer Massnahmen"},
|
||||
{ID: "A.8.21", Name: "Sichere Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Sichere Entwicklungslebenszyklus"},
|
||||
{ID: "A.8.22", Name: "Sicherheitsanforderungen bei Applikationen", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsanforderungen bei Anwendungen"},
|
||||
{ID: "A.8.23", Name: "Sichere Systemarchitektur", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsprinzipien in der Systemarchitektur"},
|
||||
{ID: "A.8.24", Name: "Sicheres Programmieren", Category: "technological", CategoryID: "A.8", Description: "Sichere Programmierpraktiken"},
|
||||
{ID: "A.8.25", Name: "Sicherheitstests", Category: "technological", CategoryID: "A.8", Description: "Sicherheitstests in der Entwicklung und Abnahme"},
|
||||
{ID: "A.8.26", Name: "Auslagerung der Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung ausgelagerter Entwicklung"},
|
||||
{ID: "A.8.27", Name: "Trennung von Umgebungen", Category: "technological", CategoryID: "A.8", Description: "Trennung von Entwicklungs-, Test- und Produktionsumgebungen"},
|
||||
{ID: "A.8.28", Name: "Aenderungsmanagement", Category: "technological", CategoryID: "A.8", Description: "Formales Aenderungsmanagement"},
|
||||
{ID: "A.8.29", Name: "Sicherheitstests in der Abnahme", Category: "technological", CategoryID: "A.8", Description: "Durchfuehrung von Sicherheitstests vor Abnahme"},
|
||||
{ID: "A.8.30", Name: "Datenloeschung", Category: "technological", CategoryID: "A.8", Description: "Sichere Datenloeschung"},
|
||||
{ID: "A.8.31", Name: "Datenmaskierung", Category: "technological", CategoryID: "A.8", Description: "Techniken zur Datenmaskierung"},
|
||||
{ID: "A.8.32", Name: "Verhinderung von Datenverlust", Category: "technological", CategoryID: "A.8", Description: "DLP-Massnahmen"},
|
||||
{ID: "A.8.33", Name: "Testinformationen", Category: "technological", CategoryID: "A.8", Description: "Schutz von Testinformationen"},
|
||||
{ID: "A.8.34", Name: "Audit-Informationssysteme", Category: "technological", CategoryID: "A.8", Description: "Schutz von Audit-Tools und -systemen"},
|
||||
}
|
||||
|
||||
// Default mappings: which modules cover which ISO controls
|
||||
var DefaultISOModuleMappings = []ISOModuleMapping{
|
||||
{
|
||||
ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen",
|
||||
ISOControls: []string{"A.5.1", "A.5.2", "A.5.3", "A.5.4", "A.5.35", "A.5.36"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung",
|
||||
ISOControls: []string{"A.5.7", "A.5.8", "A.5.9", "A.5.10", "A.5.12", "A.5.13"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung",
|
||||
ISOControls: []string{"A.5.15", "A.5.16", "A.5.17", "A.5.18", "A.8.2", "A.8.3", "A.8.5"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-kryptografie", ModuleName: "Kryptografie",
|
||||
ISOControls: []string{"A.8.20", "A.8.21", "A.8.24"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
{
|
||||
ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit",
|
||||
ISOControls: []string{"A.7.1", "A.7.2", "A.7.3", "A.7.4", "A.7.5", "A.7.7", "A.7.8"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen",
|
||||
ISOControls: []string{"A.5.34", "A.8.10", "A.8.12", "A.8.30", "A.8.31"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
{
|
||||
ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response",
|
||||
ISOControls: []string{"A.5.24", "A.5.25", "A.5.26", "A.5.27", "A.5.28", "A.6.8"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit",
|
||||
ISOControls: []string{"A.5.19", "A.5.20", "A.5.21", "A.5.22", "A.5.23"},
|
||||
CoverageLevel: "full",
|
||||
},
|
||||
{
|
||||
ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement",
|
||||
ISOControls: []string{"A.5.29", "A.5.30", "A.8.6", "A.8.7", "A.8.8", "A.8.9"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
{
|
||||
ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen",
|
||||
ISOControls: []string{"A.5.31", "A.5.34", "A.6.2", "A.6.3"},
|
||||
CoverageLevel: "partial",
|
||||
},
|
||||
}
|
||||
|
||||
// GetISOControlByID returns a control by its ID
|
||||
func GetISOControlByID(id string) (ISOControl, bool) {
|
||||
for _, c := range ISOControls {
|
||||
if c.ID == id {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return ISOControl{}, false
|
||||
}
|
||||
|
||||
// GetISOControlsByCategory returns all controls in a category
|
||||
func GetISOControlsByCategory(categoryID string) []ISOControl {
|
||||
var result []ISOControl
|
||||
for _, c := range ISOControls {
|
||||
if c.CategoryID == categoryID {
|
||||
result = append(result, c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ISOCategorySummary provides a summary per ISO category
|
||||
type ISOCategorySummary struct {
|
||||
CategoryID string `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
TotalControls int `json:"total_controls"`
|
||||
CoveredFull int `json:"covered_full"`
|
||||
CoveredPartial int `json:"covered_partial"`
|
||||
NotCovered int `json:"not_covered"`
|
||||
}
|
||||
74
ai-compliance-sdk/internal/gci/mock_data.go
Normal file
74
ai-compliance-sdk/internal/gci/mock_data.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package gci
|
||||
|
||||
import "time"
|
||||
|
||||
// MockModuleData provides fallback data when academy store is empty
|
||||
func MockModuleData(tenantID string) []ModuleScore {
|
||||
return []ModuleScore{
|
||||
// DSGVO modules
|
||||
{ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen", Assigned: 25, Completed: 22, Category: "dsgvo", RiskWeight: 2.0},
|
||||
{ModuleID: "dsgvo-betroffenenrechte", ModuleName: "Betroffenenrechte", Assigned: 25, Completed: 18, Category: "dsgvo", RiskWeight: 2.5},
|
||||
{ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen", Assigned: 20, Completed: 17, Category: "dsgvo", RiskWeight: 2.5},
|
||||
{ModuleID: "dsgvo-dsfa", ModuleName: "Datenschutz-Folgenabschaetzung", Assigned: 15, Completed: 10, Category: "dsgvo", RiskWeight: 2.0},
|
||||
{ModuleID: "dsgvo-auftragsverarbeitung", ModuleName: "Auftragsverarbeitung", Assigned: 20, Completed: 16, Category: "dsgvo", RiskWeight: 2.0},
|
||||
|
||||
// NIS2 modules
|
||||
{ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement", Assigned: 15, Completed: 11, Category: "nis2", RiskWeight: 3.0},
|
||||
{ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response", Assigned: 15, Completed: 9, Category: "nis2", RiskWeight: 3.0},
|
||||
{ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit", Assigned: 10, Completed: 6, Category: "nis2", RiskWeight: 2.0},
|
||||
{ModuleID: "nis2-management", ModuleName: "NIS2 Geschaeftsleitungspflicht", Assigned: 10, Completed: 8, Category: "nis2", RiskWeight: 3.0},
|
||||
|
||||
// ISO 27001 modules
|
||||
{ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen", Assigned: 20, Completed: 16, Category: "iso27001", RiskWeight: 2.0},
|
||||
{ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung", Assigned: 15, Completed: 12, Category: "iso27001", RiskWeight: 2.0},
|
||||
{ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung", Assigned: 20, Completed: 18, Category: "iso27001", RiskWeight: 2.0},
|
||||
{ModuleID: "iso-kryptografie", ModuleName: "Kryptografie", Assigned: 10, Completed: 7, Category: "iso27001", RiskWeight: 1.5},
|
||||
{ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit", Assigned: 10, Completed: 9, Category: "iso27001", RiskWeight: 1.0},
|
||||
|
||||
// AI Act modules
|
||||
{ModuleID: "ai-risikokategorien", ModuleName: "KI-Risikokategorien", Assigned: 15, Completed: 12, Category: "ai_act", RiskWeight: 2.5},
|
||||
{ModuleID: "ai-transparenz", ModuleName: "KI-Transparenzpflichten", Assigned: 15, Completed: 10, Category: "ai_act", RiskWeight: 2.0},
|
||||
{ModuleID: "ai-hochrisiko", ModuleName: "Hochrisiko-KI-Systeme", Assigned: 10, Completed: 6, Category: "ai_act", RiskWeight: 2.5},
|
||||
{ModuleID: "ai-governance", ModuleName: "KI-Governance", Assigned: 10, Completed: 7, Category: "ai_act", RiskWeight: 2.0},
|
||||
}
|
||||
}
|
||||
|
||||
// MockCertificateData provides mock certificate validity dates
|
||||
func MockCertificateData() map[string]time.Time {
|
||||
now := time.Now()
|
||||
return map[string]time.Time{
|
||||
"dsgvo-grundlagen": now.AddDate(0, 8, 0), // valid 8 months
|
||||
"dsgvo-betroffenenrechte": now.AddDate(0, 3, 0), // expiring in 3 months
|
||||
"dsgvo-tom": now.AddDate(0, 10, 0), // valid
|
||||
"dsgvo-dsfa": now.AddDate(0, -1, 0), // expired 1 month ago
|
||||
"dsgvo-auftragsverarbeitung": now.AddDate(0, 6, 0),
|
||||
"nis2-risikomanagement": now.AddDate(0, 5, 0),
|
||||
"nis2-incident-response": now.AddDate(0, 2, 0), // expiring soon
|
||||
"nis2-supply-chain": now.AddDate(0, -2, 0), // expired 2 months
|
||||
"nis2-management": now.AddDate(0, 9, 0),
|
||||
"iso-isms": now.AddDate(1, 0, 0),
|
||||
"iso-risikobewertung": now.AddDate(0, 4, 0),
|
||||
"iso-zugangssteuerung": now.AddDate(0, 11, 0),
|
||||
"iso-kryptografie": now.AddDate(0, 1, 0), // expiring in 1 month
|
||||
"iso-physisch": now.AddDate(0, 7, 0),
|
||||
"ai-risikokategorien": now.AddDate(0, 6, 0),
|
||||
"ai-transparenz": now.AddDate(0, 3, 0),
|
||||
"ai-hochrisiko": now.AddDate(0, -3, 0), // expired 3 months
|
||||
"ai-governance": now.AddDate(0, 5, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// MockIncidentData returns mock incident counts for adjustment
|
||||
func MockIncidentData() (openIncidents int, criticalIncidents int) {
|
||||
return 3, 1
|
||||
}
|
||||
|
||||
// MockGCIHistory returns mock historical GCI snapshots
|
||||
func MockGCIHistory(tenantID string) []GCISnapshot {
|
||||
now := time.Now()
|
||||
return []GCISnapshot{
|
||||
{TenantID: tenantID, Score: 58.2, MaturityLevel: MaturityReactive, AreaScores: map[string]float64{"dsgvo": 62, "nis2": 48, "iso27001": 60, "ai_act": 55}, CalculatedAt: now.AddDate(0, -3, 0)},
|
||||
{TenantID: tenantID, Score: 62.5, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 65, "nis2": 55, "iso27001": 63, "ai_act": 58}, CalculatedAt: now.AddDate(0, -2, 0)},
|
||||
{TenantID: tenantID, Score: 67.8, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 70, "nis2": 60, "iso27001": 68, "ai_act": 62}, CalculatedAt: now.AddDate(0, -1, 0)},
|
||||
}
|
||||
}
|
||||
104
ai-compliance-sdk/internal/gci/models.go
Normal file
104
ai-compliance-sdk/internal/gci/models.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package gci
|
||||
|
||||
import "time"
|
||||
|
||||
// Level 1: Module Score
|
||||
type ModuleScore struct {
|
||||
ModuleID string `json:"module_id"`
|
||||
ModuleName string `json:"module_name"`
|
||||
Assigned int `json:"assigned"`
|
||||
Completed int `json:"completed"`
|
||||
RawScore float64 `json:"raw_score"` // completions/assigned
|
||||
ValidityFactor float64 `json:"validity_factor"` // 0.0-1.0
|
||||
FinalScore float64 `json:"final_score"` // RawScore * ValidityFactor
|
||||
RiskWeight float64 `json:"risk_weight"` // module criticality weight
|
||||
Category string `json:"category"` // dsgvo, nis2, iso27001, ai_act
|
||||
}
|
||||
|
||||
// Level 2: Risk-weighted Module Score per regulation area
|
||||
type RiskWeightedScore struct {
|
||||
AreaID string `json:"area_id"`
|
||||
AreaName string `json:"area_name"`
|
||||
Modules []ModuleScore `json:"modules"`
|
||||
WeightedSum float64 `json:"weighted_sum"`
|
||||
TotalWeight float64 `json:"total_weight"`
|
||||
AreaScore float64 `json:"area_score"` // WeightedSum / TotalWeight
|
||||
}
|
||||
|
||||
// Level 3: Regulation Area Score
|
||||
type RegulationAreaScore struct {
|
||||
RegulationID string `json:"regulation_id"` // dsgvo, nis2, iso27001, ai_act
|
||||
RegulationName string `json:"regulation_name"` // Display name
|
||||
Score float64 `json:"score"` // 0-100
|
||||
Weight float64 `json:"weight"` // regulation weight in GCI
|
||||
WeightedScore float64 `json:"weighted_score"` // Score * Weight
|
||||
ModuleCount int `json:"module_count"`
|
||||
CompletedCount int `json:"completed_count"`
|
||||
}
|
||||
|
||||
// Level 4: GCI Result
|
||||
type GCIResult struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
GCIScore float64 `json:"gci_score"` // 0-100
|
||||
MaturityLevel string `json:"maturity_level"` // Optimized, Managed, Defined, Reactive, HighRisk
|
||||
MaturityLabel string `json:"maturity_label"` // German label
|
||||
CalculatedAt time.Time `json:"calculated_at"`
|
||||
Profile string `json:"profile"` // default, nis2_relevant, ki_nutzer
|
||||
AreaScores []RegulationAreaScore `json:"area_scores"`
|
||||
CriticalityMult float64 `json:"criticality_multiplier"`
|
||||
IncidentAdj float64 `json:"incident_adjustment"`
|
||||
AuditTrail []AuditEntry `json:"audit_trail"`
|
||||
}
|
||||
|
||||
// GCI Breakdown with all 4 levels
|
||||
type GCIBreakdown struct {
|
||||
GCIResult
|
||||
Level1Modules []ModuleScore `json:"level1_modules"`
|
||||
Level2Areas []RiskWeightedScore `json:"level2_areas"`
|
||||
}
|
||||
|
||||
// MaturityLevel constants
|
||||
const (
|
||||
MaturityOptimized = "OPTIMIZED"
|
||||
MaturityManaged = "MANAGED"
|
||||
MaturityDefined = "DEFINED"
|
||||
MaturityReactive = "REACTIVE"
|
||||
MaturityHighRisk = "HIGH_RISK"
|
||||
)
|
||||
|
||||
// Maturity level labels (German)
|
||||
var MaturityLabels = map[string]string{
|
||||
MaturityOptimized: "Optimiert",
|
||||
MaturityManaged: "Gesteuert",
|
||||
MaturityDefined: "Definiert",
|
||||
MaturityReactive: "Reaktiv",
|
||||
MaturityHighRisk: "Hohes Risiko",
|
||||
}
|
||||
|
||||
// AuditEntry for score transparency
|
||||
type AuditEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Factor string `json:"factor"`
|
||||
Description string `json:"description"`
|
||||
Value float64 `json:"value"`
|
||||
Impact string `json:"impact"` // positive, negative, neutral
|
||||
}
|
||||
|
||||
// ComplianceMatrixEntry maps roles to regulations
|
||||
type ComplianceMatrixEntry struct {
|
||||
Role string `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Regulations map[string]float64 `json:"regulations"` // regulation_id -> score
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
RequiredModules int `json:"required_modules"`
|
||||
CompletedModules int `json:"completed_modules"`
|
||||
}
|
||||
|
||||
// GCI History snapshot
|
||||
type GCISnapshot struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
Score float64 `json:"score"`
|
||||
MaturityLevel string `json:"maturity_level"`
|
||||
AreaScores map[string]float64 `json:"area_scores"`
|
||||
CalculatedAt time.Time `json:"calculated_at"`
|
||||
}
|
||||
118
ai-compliance-sdk/internal/gci/nis2_roles.go
Normal file
118
ai-compliance-sdk/internal/gci/nis2_roles.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package gci
|
||||
|
||||
// NIS2Role defines a NIS2 role classification
|
||||
type NIS2Role struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
MandatoryModules []string `json:"mandatory_modules"`
|
||||
Priority int `json:"priority"` // 1=highest
|
||||
}
|
||||
|
||||
// NIS2RoleAssignment represents a user's NIS2 role
|
||||
type NIS2RoleAssignment struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
RoleID string `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AssignedAt string `json:"assigned_at"`
|
||||
}
|
||||
|
||||
// NIS2 role definitions
|
||||
var NIS2Roles = map[string]NIS2Role{
|
||||
"N1": {
|
||||
ID: "N1",
|
||||
Name: "Geschaeftsleitung",
|
||||
Description: "Leitungsorgane mit persoenlicher Haftung gemaess NIS2 Art. 20",
|
||||
Priority: 1,
|
||||
MandatoryModules: []string{
|
||||
"nis2-management",
|
||||
"nis2-risikomanagement",
|
||||
"dsgvo-grundlagen",
|
||||
"iso-isms",
|
||||
},
|
||||
},
|
||||
"N2": {
|
||||
ID: "N2",
|
||||
Name: "IT-Sicherheit / CISO",
|
||||
Description: "Verantwortliche fuer IT-Sicherheit und Cybersecurity",
|
||||
Priority: 2,
|
||||
MandatoryModules: []string{
|
||||
"nis2-risikomanagement",
|
||||
"nis2-incident-response",
|
||||
"nis2-supply-chain",
|
||||
"iso-zugangssteuerung",
|
||||
"iso-kryptografie",
|
||||
},
|
||||
},
|
||||
"N3": {
|
||||
ID: "N3",
|
||||
Name: "Kritische Funktionen",
|
||||
Description: "Mitarbeiter in kritischen Geschaeftsprozessen",
|
||||
Priority: 3,
|
||||
MandatoryModules: []string{
|
||||
"nis2-risikomanagement",
|
||||
"nis2-incident-response",
|
||||
"dsgvo-tom",
|
||||
"iso-zugangssteuerung",
|
||||
},
|
||||
},
|
||||
"N4": {
|
||||
ID: "N4",
|
||||
Name: "Allgemeine Mitarbeiter",
|
||||
Description: "Alle Mitarbeiter mit IT-Zugang",
|
||||
Priority: 4,
|
||||
MandatoryModules: []string{
|
||||
"nis2-risikomanagement",
|
||||
"dsgvo-grundlagen",
|
||||
"iso-isms",
|
||||
},
|
||||
},
|
||||
"N5": {
|
||||
ID: "N5",
|
||||
Name: "Incident Response Team",
|
||||
Description: "Mitglieder des IRT/CSIRT gemaess NIS2 Art. 21",
|
||||
Priority: 2,
|
||||
MandatoryModules: []string{
|
||||
"nis2-incident-response",
|
||||
"nis2-risikomanagement",
|
||||
"nis2-supply-chain",
|
||||
"iso-zugangssteuerung",
|
||||
"iso-kryptografie",
|
||||
"iso-isms",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GetNIS2Role returns a NIS2 role by ID
|
||||
func GetNIS2Role(roleID string) (NIS2Role, bool) {
|
||||
r, ok := NIS2Roles[roleID]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// ListNIS2Roles returns all NIS2 roles sorted by priority
|
||||
func ListNIS2Roles() []NIS2Role {
|
||||
roles := []NIS2Role{}
|
||||
// Return in priority order
|
||||
order := []string{"N1", "N2", "N5", "N3", "N4"}
|
||||
for _, id := range order {
|
||||
if r, ok := NIS2Roles[id]; ok {
|
||||
roles = append(roles, r)
|
||||
}
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
// MockNIS2RoleAssignments returns mock role assignments
|
||||
func MockNIS2RoleAssignments(tenantID string) []NIS2RoleAssignment {
|
||||
return []NIS2RoleAssignment{
|
||||
{TenantID: tenantID, UserID: "user-001", UserName: "Dr. Schmidt", RoleID: "N1", RoleName: "Geschaeftsleitung", AssignedAt: "2025-06-01"},
|
||||
{TenantID: tenantID, UserID: "user-002", UserName: "M. Weber", RoleID: "N2", RoleName: "IT-Sicherheit / CISO", AssignedAt: "2025-06-01"},
|
||||
{TenantID: tenantID, UserID: "user-003", UserName: "S. Mueller", RoleID: "N5", RoleName: "Incident Response Team", AssignedAt: "2025-07-15"},
|
||||
{TenantID: tenantID, UserID: "user-004", UserName: "K. Fischer", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"},
|
||||
{TenantID: tenantID, UserID: "user-005", UserName: "L. Braun", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"},
|
||||
{TenantID: tenantID, UserID: "user-006", UserName: "A. Schwarz", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"},
|
||||
{TenantID: tenantID, UserID: "user-007", UserName: "T. Wagner", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"},
|
||||
}
|
||||
}
|
||||
147
ai-compliance-sdk/internal/gci/nis2_scoring.go
Normal file
147
ai-compliance-sdk/internal/gci/nis2_scoring.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package gci
|
||||
|
||||
import "math"
|
||||
|
||||
// NIS2Score represents the NIS2-specific compliance score
|
||||
type NIS2Score struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
OverallScore float64 `json:"overall_score"`
|
||||
MaturityLevel string `json:"maturity_level"`
|
||||
MaturityLabel string `json:"maturity_label"`
|
||||
AreaScores []NIS2AreaScore `json:"area_scores"`
|
||||
RoleCompliance []NIS2RoleScore `json:"role_compliance"`
|
||||
}
|
||||
|
||||
// NIS2AreaScore represents a NIS2 compliance area
|
||||
type NIS2AreaScore struct {
|
||||
AreaID string `json:"area_id"`
|
||||
AreaName string `json:"area_name"`
|
||||
Score float64 `json:"score"`
|
||||
Weight float64 `json:"weight"`
|
||||
ModuleIDs []string `json:"module_ids"`
|
||||
}
|
||||
|
||||
// NIS2RoleScore represents completion per NIS2 role
|
||||
type NIS2RoleScore struct {
|
||||
RoleID string `json:"role_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AssignedUsers int `json:"assigned_users"`
|
||||
CompletionRate float64 `json:"completion_rate"`
|
||||
MandatoryTotal int `json:"mandatory_total"`
|
||||
MandatoryDone int `json:"mandatory_done"`
|
||||
}
|
||||
|
||||
// NIS2 scoring areas with weights
|
||||
// NIS2Score = 25% Management + 25% Incident + 30% IT Security + 20% Supply Chain
|
||||
var nis2Areas = []struct {
|
||||
ID string
|
||||
Name string
|
||||
Weight float64
|
||||
ModuleIDs []string
|
||||
}{
|
||||
{
|
||||
ID: "management", Name: "Management & Governance", Weight: 0.25,
|
||||
ModuleIDs: []string{"nis2-management", "dsgvo-grundlagen", "iso-isms"},
|
||||
},
|
||||
{
|
||||
ID: "incident", Name: "Vorfallsbehandlung", Weight: 0.25,
|
||||
ModuleIDs: []string{"nis2-incident-response"},
|
||||
},
|
||||
{
|
||||
ID: "it_security", Name: "IT-Sicherheit", Weight: 0.30,
|
||||
ModuleIDs: []string{"nis2-risikomanagement", "iso-zugangssteuerung", "iso-kryptografie"},
|
||||
},
|
||||
{
|
||||
ID: "supply_chain", Name: "Lieferkettensicherheit", Weight: 0.20,
|
||||
ModuleIDs: []string{"nis2-supply-chain", "dsgvo-auftragsverarbeitung"},
|
||||
},
|
||||
}
|
||||
|
||||
// CalculateNIS2Score computes the NIS2-specific compliance score
|
||||
func CalculateNIS2Score(tenantID string) *NIS2Score {
|
||||
modules := MockModuleData(tenantID)
|
||||
moduleMap := map[string]ModuleScore{}
|
||||
for _, m := range modules {
|
||||
moduleMap[m.ModuleID] = m
|
||||
}
|
||||
|
||||
areaScores := []NIS2AreaScore{}
|
||||
totalWeighted := 0.0
|
||||
|
||||
for _, area := range nis2Areas {
|
||||
areaScore := NIS2AreaScore{
|
||||
AreaID: area.ID,
|
||||
AreaName: area.Name,
|
||||
Weight: area.Weight,
|
||||
ModuleIDs: area.ModuleIDs,
|
||||
}
|
||||
|
||||
scoreSum := 0.0
|
||||
count := 0
|
||||
for _, modID := range area.ModuleIDs {
|
||||
if m, ok := moduleMap[modID]; ok {
|
||||
if m.Assigned > 0 {
|
||||
scoreSum += float64(m.Completed) / float64(m.Assigned) * 100
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count > 0 {
|
||||
areaScore.Score = math.Round(scoreSum/float64(count)*10) / 10
|
||||
}
|
||||
totalWeighted += areaScore.Score * areaScore.Weight
|
||||
areaScores = append(areaScores, areaScore)
|
||||
}
|
||||
|
||||
overallScore := math.Round(totalWeighted*10) / 10
|
||||
|
||||
// Calculate role compliance
|
||||
roleAssignments := MockNIS2RoleAssignments(tenantID)
|
||||
roleScores := calculateNIS2RoleScores(roleAssignments, moduleMap)
|
||||
|
||||
return &NIS2Score{
|
||||
TenantID: tenantID,
|
||||
OverallScore: overallScore,
|
||||
MaturityLevel: determineMaturityLevel(overallScore),
|
||||
MaturityLabel: MaturityLabels[determineMaturityLevel(overallScore)],
|
||||
AreaScores: areaScores,
|
||||
RoleCompliance: roleScores,
|
||||
}
|
||||
}
|
||||
|
||||
func calculateNIS2RoleScores(assignments []NIS2RoleAssignment, moduleMap map[string]ModuleScore) []NIS2RoleScore {
|
||||
// Count users per role
|
||||
roleCounts := map[string]int{}
|
||||
for _, a := range assignments {
|
||||
roleCounts[a.RoleID]++
|
||||
}
|
||||
|
||||
scores := []NIS2RoleScore{}
|
||||
for roleID, role := range NIS2Roles {
|
||||
rs := NIS2RoleScore{
|
||||
RoleID: roleID,
|
||||
RoleName: role.Name,
|
||||
AssignedUsers: roleCounts[roleID],
|
||||
MandatoryTotal: len(role.MandatoryModules),
|
||||
}
|
||||
|
||||
completionSum := 0.0
|
||||
for _, modID := range role.MandatoryModules {
|
||||
if m, ok := moduleMap[modID]; ok {
|
||||
if m.Assigned > 0 {
|
||||
rate := float64(m.Completed) / float64(m.Assigned)
|
||||
completionSum += rate
|
||||
if rate >= 0.8 { // 80%+ = considered done
|
||||
rs.MandatoryDone++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if rs.MandatoryTotal > 0 {
|
||||
rs.CompletionRate = math.Round(completionSum/float64(rs.MandatoryTotal)*100*10) / 10
|
||||
}
|
||||
scores = append(scores, rs)
|
||||
}
|
||||
|
||||
return scores
|
||||
}
|
||||
59
ai-compliance-sdk/internal/gci/validity.go
Normal file
59
ai-compliance-sdk/internal/gci/validity.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package gci
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// GracePeriodDays is the number of days after expiry during which
|
||||
// the certificate still contributes (with declining factor)
|
||||
GracePeriodDays = 180
|
||||
|
||||
// DecayStartDays is how many days before expiry the linear decay begins
|
||||
DecayStartDays = 180
|
||||
)
|
||||
|
||||
// CalculateValidityFactor computes the validity factor for a certificate
|
||||
// based on its expiry date.
|
||||
//
|
||||
// Rules:
|
||||
// - Certificate not yet expiring (>6 months): factor = 1.0
|
||||
// - Certificate expiring within 6 months: linear decay from 1.0 to 0.5
|
||||
// - Certificate expired: linear decay from 0.5 to 0.0 over grace period
|
||||
// - Certificate expired beyond grace period: factor = 0.0
|
||||
func CalculateValidityFactor(validUntil time.Time, now time.Time) float64 {
|
||||
daysUntilExpiry := validUntil.Sub(now).Hours() / 24.0
|
||||
|
||||
if daysUntilExpiry > float64(DecayStartDays) {
|
||||
// Not yet in decay window
|
||||
return 1.0
|
||||
}
|
||||
|
||||
if daysUntilExpiry > 0 {
|
||||
// In pre-expiry decay window: linear from 1.0 to 0.5
|
||||
fraction := daysUntilExpiry / float64(DecayStartDays)
|
||||
return 0.5 + 0.5*fraction
|
||||
}
|
||||
|
||||
// Certificate is expired
|
||||
daysExpired := -daysUntilExpiry
|
||||
if daysExpired > float64(GracePeriodDays) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// In grace period: linear from 0.5 to 0.0
|
||||
fraction := 1.0 - (daysExpired / float64(GracePeriodDays))
|
||||
return math.Max(0, 0.5*fraction)
|
||||
}
|
||||
|
||||
// IsExpired returns true if the certificate is past its validity date
|
||||
func IsExpired(validUntil time.Time, now time.Time) bool {
|
||||
return now.After(validUntil)
|
||||
}
|
||||
|
||||
// IsExpiringSoon returns true if the certificate expires within the decay window
|
||||
func IsExpiringSoon(validUntil time.Time, now time.Time) bool {
|
||||
daysUntil := validUntil.Sub(now).Hours() / 24.0
|
||||
return daysUntil > 0 && daysUntil <= float64(DecayStartDays)
|
||||
}
|
||||
78
ai-compliance-sdk/internal/gci/weights.go
Normal file
78
ai-compliance-sdk/internal/gci/weights.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package gci
|
||||
|
||||
// WeightProfile defines regulation weights for different compliance profiles
|
||||
type WeightProfile struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Weights map[string]float64 `json:"weights"` // regulation_id -> weight (0.0-1.0)
|
||||
}
|
||||
|
||||
// Default weight profiles
|
||||
var DefaultProfiles = map[string]WeightProfile{
|
||||
"default": {
|
||||
ID: "default",
|
||||
Name: "Standard",
|
||||
Description: "Ausgewogenes Profil fuer allgemeine Compliance",
|
||||
Weights: map[string]float64{
|
||||
"dsgvo": 0.30,
|
||||
"nis2": 0.25,
|
||||
"iso27001": 0.25,
|
||||
"ai_act": 0.20,
|
||||
},
|
||||
},
|
||||
"nis2_relevant": {
|
||||
ID: "nis2_relevant",
|
||||
Name: "NIS2-relevant",
|
||||
Description: "Fuer Betreiber kritischer Infrastrukturen",
|
||||
Weights: map[string]float64{
|
||||
"dsgvo": 0.25,
|
||||
"nis2": 0.35,
|
||||
"iso27001": 0.25,
|
||||
"ai_act": 0.15,
|
||||
},
|
||||
},
|
||||
"ki_nutzer": {
|
||||
ID: "ki_nutzer",
|
||||
Name: "KI-Nutzer",
|
||||
Description: "Fuer Organisationen mit KI-Einsatz",
|
||||
Weights: map[string]float64{
|
||||
"dsgvo": 0.25,
|
||||
"nis2": 0.25,
|
||||
"iso27001": 0.20,
|
||||
"ai_act": 0.30,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ModuleRiskWeights defines risk criticality per module type
|
||||
var ModuleRiskWeights = map[string]float64{
|
||||
"incident_response": 3.0,
|
||||
"management_awareness": 3.0,
|
||||
"data_protection": 2.5,
|
||||
"it_security": 2.5,
|
||||
"supply_chain": 2.0,
|
||||
"risk_assessment": 2.0,
|
||||
"access_control": 2.0,
|
||||
"business_continuity": 2.0,
|
||||
"employee_training": 1.5,
|
||||
"documentation": 1.5,
|
||||
"physical_security": 1.0,
|
||||
"general": 1.0,
|
||||
}
|
||||
|
||||
// GetProfile returns a weight profile by ID, defaulting to "default"
|
||||
func GetProfile(profileID string) WeightProfile {
|
||||
if p, ok := DefaultProfiles[profileID]; ok {
|
||||
return p
|
||||
}
|
||||
return DefaultProfiles["default"]
|
||||
}
|
||||
|
||||
// GetModuleRiskWeight returns the risk weight for a module category
|
||||
func GetModuleRiskWeight(category string) float64 {
|
||||
if w, ok := ModuleRiskWeights[category]; ok {
|
||||
return w
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
Reference in New Issue
Block a user