feat(tom): audit document, compliance checks, 25 controls, canonical control mapping

Phase A: TOM document HTML generator (12 sections, inline CSS, A4 print)
Phase B: TOMDocumentTab component (org-header form, revisions, print/download)
Phase C: 11 compliance checks with severity-weighted scoring
Phase D: MkDocs documentation for TOM module
Phase E: 25 new controls (63 → 88) in 13 categories

Canonical Control Mapping (three-layer architecture):
- Migration 068: tom_control_mappings + tom_control_sync_state tables
- 6 API endpoints: sync, list, by-tom, stats, manual add, delete
- Category mapping: 13 TOM categories → 17 canonical categories
- Frontend: sync button + coverage card (Overview), drill-down (Editor),
  belegende Controls count (Document)
- 20 tests (unit + API with mocked DB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-19 11:56:53 +01:00
parent 2a70441eaa
commit 4b1eede45b
14 changed files with 3910 additions and 8 deletions

View File

@@ -1,9 +1,18 @@
'use client'
import { useMemo, useState, useEffect } from 'react'
import { useMemo, useState, useEffect, useCallback } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
interface CanonicalMapping {
id: string
canonical_control_code: string
canonical_title: string | null
canonical_severity: string | null
canonical_objective: string | null
mapping_type: string
}
interface TOMEditorTabProps {
state: TOMGeneratorState
selectedTOMId: string | null
@@ -46,6 +55,17 @@ export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOME
const [notes, setNotes] = useState('')
const [linkedEvidence, setLinkedEvidence] = useState<string[]>([])
const [selectedEvidenceId, setSelectedEvidenceId] = useState('')
const [canonicalMappings, setCanonicalMappings] = useState<CanonicalMapping[]>([])
const [showCanonical, setShowCanonical] = useState(false)
// Load canonical controls for this TOM's category
useEffect(() => {
if (!control?.category) { setCanonicalMappings([]); return }
fetch(`/api/sdk/v1/compliance/tom-mappings/by-tom/${encodeURIComponent(control.category)}`)
.then(r => r.ok ? r.json() : null)
.then(data => { if (data?.mappings) setCanonicalMappings(data.mappings) })
.catch(() => setCanonicalMappings([]))
}, [control?.category])
useEffect(() => {
if (tom) {
@@ -341,6 +361,62 @@ export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOME
</div>
)}
{/* Canonical Controls (Belegende Security-Controls) */}
{canonicalMappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-gray-700">
Belegende Security-Controls ({canonicalMappings.length})
</h3>
<button
onClick={() => setShowCanonical(!showCanonical)}
className="text-xs text-purple-600 hover:text-purple-700 font-medium"
>
{showCanonical ? 'Einklappen' : 'Alle anzeigen'}
</button>
</div>
<div className="space-y-2">
{(showCanonical ? canonicalMappings : canonicalMappings.slice(0, 5)).map(m => (
<div key={m.id} className="flex items-start gap-3 bg-gray-50 rounded-lg px-3 py-2">
<div className="flex-shrink-0">
<span className="text-xs font-mono bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
{m.canonical_control_code}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 font-medium truncate">{m.canonical_title || m.canonical_control_code}</p>
{m.canonical_objective && showCanonical && (
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{m.canonical_objective}</p>
)}
</div>
<div className="flex-shrink-0 flex items-center gap-1.5">
{m.canonical_severity && (
<span className={`text-xs px-1.5 py-0.5 rounded-full font-medium ${
m.canonical_severity === 'critical' ? 'bg-red-100 text-red-700' :
m.canonical_severity === 'high' ? 'bg-orange-100 text-orange-700' :
m.canonical_severity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-600'
}`}>
{m.canonical_severity}
</span>
)}
<span className={`text-xs px-1.5 py-0.5 rounded-full ${
m.mapping_type === 'manual' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-500'
}`}>
{m.mapping_type === 'manual' ? 'manuell' : 'auto'}
</span>
</div>
</div>
))}
</div>
{!showCanonical && canonicalMappings.length > 5 && (
<p className="text-xs text-gray-400 mt-2">
+ {canonicalMappings.length - 5} weitere Controls
</p>
)}
</div>
)}
{/* Framework Mappings */}
{control?.mappings && control.mappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">