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'}
|
isActive={pathname === '/sdk/reporting'}
|
||||||
collapsed={collapsed}
|
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
|
<AdditionalModuleItem
|
||||||
href="/sdk/industry-templates"
|
href="/sdk/industry-templates"
|
||||||
icon={
|
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/vendor"
|
||||||
"github.com/breakpilot/ai-compliance-sdk/internal/workshop"
|
"github.com/breakpilot/ai-compliance-sdk/internal/workshop"
|
||||||
"github.com/breakpilot/ai-compliance-sdk/internal/portfolio"
|
"github.com/breakpilot/ai-compliance-sdk/internal/portfolio"
|
||||||
|
"github.com/breakpilot/ai-compliance-sdk/internal/gci"
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
@@ -124,6 +125,10 @@ func main() {
|
|||||||
industryHandlers := handlers.NewIndustryHandlers()
|
industryHandlers := handlers.NewIndustryHandlers()
|
||||||
dsbHandlers := handlers.NewDSBHandlers(dsbStore)
|
dsbHandlers := handlers.NewDSBHandlers(dsbStore)
|
||||||
|
|
||||||
|
// Initialize GCI engine and handlers
|
||||||
|
gciEngine := gci.NewEngine()
|
||||||
|
gciHandlers := handlers.NewGCIHandlers(gciEngine)
|
||||||
|
|
||||||
// Initialize middleware
|
// Initialize middleware
|
||||||
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
||||||
|
|
||||||
@@ -652,6 +657,29 @@ func main() {
|
|||||||
dsbRoutes.POST("/assignments/:id/communications", dsbHandlers.CreateCommunication)
|
dsbRoutes.POST("/assignments/:id/communications", dsbHandlers.CreateCommunication)
|
||||||
dsbRoutes.GET("/assignments/:id/communications", dsbHandlers.ListCommunications)
|
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
|
// 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