fix(admin-v2): Restore complete admin-v2 application

The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-08 23:40:15 -08:00
parent f28244753f
commit 660295e218
385 changed files with 138126 additions and 3079 deletions

View File

@@ -0,0 +1,414 @@
// =============================================================================
// TOM Generator Document Analyzer
// AI-powered analysis of uploaded evidence documents
// =============================================================================
import {
EvidenceDocument,
AIDocumentAnalysis,
ExtractedClause,
DocumentType,
} from '../types'
import {
getDocumentAnalysisPrompt,
getDocumentTypeDetectionPrompt,
DocumentAnalysisPromptContext,
} from './prompts'
import { getAllControls } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface AnalysisResult {
success: boolean
analysis: AIDocumentAnalysis | null
error?: string
}
export interface DocumentTypeDetectionResult {
documentType: DocumentType
confidence: number
reasoning: string
}
// =============================================================================
// DOCUMENT ANALYZER CLASS
// =============================================================================
export class TOMDocumentAnalyzer {
private apiEndpoint: string
private apiKey: string | null
constructor(options?: { apiEndpoint?: string; apiKey?: string }) {
this.apiEndpoint = options?.apiEndpoint || '/api/sdk/v1/tom-generator/evidence/analyze'
this.apiKey = options?.apiKey || null
}
/**
* Analyze a document and extract relevant TOM information
*/
async analyzeDocument(
document: EvidenceDocument,
documentText: string,
language: 'de' | 'en' = 'de'
): Promise<AnalysisResult> {
try {
// Get all control IDs for context
const allControls = getAllControls()
const controlIds = allControls.map((c) => c.id)
// Build the prompt context
const promptContext: DocumentAnalysisPromptContext = {
documentType: document.documentType,
documentText,
controlIds,
language,
}
const prompt = getDocumentAnalysisPrompt(promptContext)
// Call the AI API
const response = await this.callAI(prompt)
if (!response.success || !response.data) {
return {
success: false,
analysis: null,
error: response.error || 'Failed to analyze document',
}
}
// Parse the AI response
const parsedResponse = this.parseAnalysisResponse(response.data)
const analysis: AIDocumentAnalysis = {
summary: parsedResponse.summary,
extractedClauses: parsedResponse.extractedClauses,
applicableControls: parsedResponse.applicableControls,
gaps: parsedResponse.gaps,
confidence: parsedResponse.confidence,
analyzedAt: new Date(),
}
return {
success: true,
analysis,
}
} catch (error) {
return {
success: false,
analysis: null,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Detect the document type from content
*/
async detectDocumentType(
documentText: string,
filename: string
): Promise<DocumentTypeDetectionResult> {
try {
const prompt = getDocumentTypeDetectionPrompt(documentText, filename)
const response = await this.callAI(prompt)
if (!response.success || !response.data) {
return {
documentType: 'OTHER',
confidence: 0,
reasoning: 'Could not detect document type',
}
}
const parsed = this.parseJSONResponse(response.data)
return {
documentType: this.mapDocumentType(String(parsed.documentType || 'OTHER')),
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0,
reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : '',
}
} catch (error) {
return {
documentType: 'OTHER',
confidence: 0,
reasoning: error instanceof Error ? error.message : 'Detection failed',
}
}
}
/**
* Link document to applicable controls based on analysis
*/
async suggestControlLinks(
analysis: AIDocumentAnalysis
): Promise<string[]> {
// Use the applicable controls from the analysis
const suggestedControls = [...analysis.applicableControls]
// Also check extracted clauses for related controls
for (const clause of analysis.extractedClauses) {
if (clause.relatedControlId && !suggestedControls.includes(clause.relatedControlId)) {
suggestedControls.push(clause.relatedControlId)
}
}
return suggestedControls
}
/**
* Calculate evidence coverage for a control
*/
calculateEvidenceCoverage(
controlId: string,
documents: EvidenceDocument[]
): {
coverage: number
linkedDocuments: string[]
missingEvidence: string[]
} {
const control = getAllControls().find((c) => c.id === controlId)
if (!control) {
return { coverage: 0, linkedDocuments: [], missingEvidence: [] }
}
const linkedDocuments: string[] = []
const coveredRequirements = new Set<string>()
for (const doc of documents) {
// Check if document is explicitly linked
if (doc.linkedControlIds.includes(controlId)) {
linkedDocuments.push(doc.id)
}
// Check if AI analysis suggests this document covers the control
if (doc.aiAnalysis?.applicableControls.includes(controlId)) {
if (!linkedDocuments.includes(doc.id)) {
linkedDocuments.push(doc.id)
}
}
// Check which evidence requirements are covered
if (doc.aiAnalysis) {
for (const requirement of control.evidenceRequirements) {
const reqLower = requirement.toLowerCase()
if (
doc.aiAnalysis.summary.toLowerCase().includes(reqLower) ||
doc.aiAnalysis.extractedClauses.some((c) =>
c.text.toLowerCase().includes(reqLower)
)
) {
coveredRequirements.add(requirement)
}
}
}
}
const missingEvidence = control.evidenceRequirements.filter(
(req) => !coveredRequirements.has(req)
)
const coverage =
control.evidenceRequirements.length > 0
? Math.round(
(coveredRequirements.size / control.evidenceRequirements.length) * 100
)
: 100
return {
coverage,
linkedDocuments,
missingEvidence,
}
}
/**
* Call the AI API
*/
private async callAI(
prompt: string
): Promise<{ success: boolean; data?: string; error?: string }> {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
}
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`
}
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers,
body: JSON.stringify({ prompt }),
})
if (!response.ok) {
return {
success: false,
error: `API error: ${response.status} ${response.statusText}`,
}
}
const data = await response.json()
return {
success: true,
data: data.response || data.content || JSON.stringify(data),
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'API call failed',
}
}
}
/**
* Parse the AI analysis response
*/
private parseAnalysisResponse(response: string): {
summary: string
extractedClauses: ExtractedClause[]
applicableControls: string[]
gaps: string[]
confidence: number
} {
const parsed = this.parseJSONResponse(response)
return {
summary: typeof parsed.summary === 'string' ? parsed.summary : '',
extractedClauses: (Array.isArray(parsed.extractedClauses) ? parsed.extractedClauses : []).map(
(clause: Record<string, unknown>) => ({
id: String(clause.id || ''),
text: String(clause.text || ''),
type: String(clause.type || ''),
relatedControlId: clause.relatedControlId
? String(clause.relatedControlId)
: null,
})
),
applicableControls: Array.isArray(parsed.applicableControls)
? parsed.applicableControls.map(String)
: [],
gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [],
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0,
}
}
/**
* Parse JSON from AI response (handles markdown code blocks)
*/
private parseJSONResponse(response: string): Record<string, unknown> {
let jsonStr = response.trim()
// Remove markdown code blocks if present
if (jsonStr.startsWith('```json')) {
jsonStr = jsonStr.slice(7)
} else if (jsonStr.startsWith('```')) {
jsonStr = jsonStr.slice(3)
}
if (jsonStr.endsWith('```')) {
jsonStr = jsonStr.slice(0, -3)
}
jsonStr = jsonStr.trim()
try {
return JSON.parse(jsonStr)
} catch {
// Try to extract JSON from the response
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/)
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0])
} catch {
return {}
}
}
return {}
}
}
/**
* Map string to DocumentType
*/
private mapDocumentType(type: string): DocumentType {
const typeMap: Record<string, DocumentType> = {
AVV: 'AVV',
DPA: 'DPA',
SLA: 'SLA',
NDA: 'NDA',
POLICY: 'POLICY',
CERTIFICATE: 'CERTIFICATE',
AUDIT_REPORT: 'AUDIT_REPORT',
OTHER: 'OTHER',
}
return typeMap[type.toUpperCase()] || 'OTHER'
}
}
// =============================================================================
// SINGLETON INSTANCE
// =============================================================================
let analyzerInstance: TOMDocumentAnalyzer | null = null
export function getDocumentAnalyzer(
options?: { apiEndpoint?: string; apiKey?: string }
): TOMDocumentAnalyzer {
if (!analyzerInstance) {
analyzerInstance = new TOMDocumentAnalyzer(options)
}
return analyzerInstance
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Quick document analysis
*/
export async function analyzeEvidenceDocument(
document: EvidenceDocument,
documentText: string,
language: 'de' | 'en' = 'de'
): Promise<AnalysisResult> {
return getDocumentAnalyzer().analyzeDocument(document, documentText, language)
}
/**
* Quick document type detection
*/
export async function detectEvidenceDocumentType(
documentText: string,
filename: string
): Promise<DocumentTypeDetectionResult> {
return getDocumentAnalyzer().detectDocumentType(documentText, filename)
}
/**
* Get evidence gaps for all controls
*/
export function getEvidenceGapsForAllControls(
documents: EvidenceDocument[]
): Map<string, { coverage: number; missing: string[] }> {
const analyzer = getDocumentAnalyzer()
const allControls = getAllControls()
const gaps = new Map<string, { coverage: number; missing: string[] }>()
for (const control of allControls) {
const result = analyzer.calculateEvidenceCoverage(control.id, documents)
gaps.set(control.id, {
coverage: result.coverage,
missing: result.missingEvidence,
})
}
return gaps
}

View File

@@ -0,0 +1,427 @@
// =============================================================================
// TOM Generator AI Prompts
// Prompts for document analysis and TOM description generation
// =============================================================================
import {
CompanyProfile,
DataProfile,
ArchitectureProfile,
RiskProfile,
DocumentType,
ControlLibraryEntry,
} from '../types'
// =============================================================================
// DOCUMENT ANALYSIS PROMPT
// =============================================================================
export interface DocumentAnalysisPromptContext {
documentType: DocumentType
documentText: string
controlIds?: string[]
language?: 'de' | 'en'
}
export function getDocumentAnalysisPrompt(
context: DocumentAnalysisPromptContext
): string {
const { documentType, documentText, controlIds, language = 'de' } = context
const controlContext = controlIds?.length
? `\nRELEVANT CONTROL IDS: ${controlIds.join(', ')}`
: ''
if (language === 'de') {
return `Du bist ein Experte für Datenschutz-Compliance und analysierst ein Dokument für die TOM-Dokumentation nach DSGVO Art. 32.
DOKUMENTTYP: ${documentType}
${controlContext}
DOKUMENTTEXT:
${documentText}
AUFGABE: Analysiere das Dokument und extrahiere die folgenden Informationen:
1. SUMMARY: Eine Zusammenfassung in 2-3 Sätzen, die die Relevanz für den Datenschutz beschreibt.
2. EXTRACTED_CLAUSES: Alle Klauseln, die sich auf technische und organisatorische Sicherheitsmaßnahmen beziehen. Für jede Klausel:
- id: Eindeutige ID (z.B. "clause-1")
- text: Der extrahierte Text
- type: Art der Maßnahme (z.B. "encryption", "access-control", "backup", "training")
- relatedControlId: Falls zutreffend, die TOM-Control-ID (z.B. "TOM-ENC-01")
3. APPLICABLE_CONTROLS: Liste der TOM-Control-IDs, die durch dieses Dokument belegt werden könnten.
4. GAPS: Identifizierte Lücken oder fehlende Maßnahmen, die im Dokument nicht adressiert werden.
5. CONFIDENCE: Dein Vertrauenswert für die Analyse (0.0 bis 1.0).
Antworte im JSON-Format:
{
"summary": "...",
"extractedClauses": [
{ "id": "...", "text": "...", "type": "...", "relatedControlId": "..." }
],
"applicableControls": ["TOM-..."],
"gaps": ["..."],
"confidence": 0.85
}`
}
return `You are a data protection compliance expert analyzing a document for TOM documentation according to GDPR Art. 32.
DOCUMENT TYPE: ${documentType}
${controlContext}
DOCUMENT TEXT:
${documentText}
TASK: Analyze the document and extract the following information:
1. SUMMARY: A 2-3 sentence summary describing the relevance for data protection.
2. EXTRACTED_CLAUSES: All clauses related to technical and organizational security measures. For each clause:
- id: Unique ID (e.g., "clause-1")
- text: The extracted text
- type: Type of measure (e.g., "encryption", "access-control", "backup", "training")
- relatedControlId: If applicable, the TOM control ID (e.g., "TOM-ENC-01")
3. APPLICABLE_CONTROLS: List of TOM control IDs that could be evidenced by this document.
4. GAPS: Identified gaps or missing measures not addressed in the document.
5. CONFIDENCE: Your confidence score for the analysis (0.0 to 1.0).
Respond in JSON format:
{
"summary": "...",
"extractedClauses": [
{ "id": "...", "text": "...", "type": "...", "relatedControlId": "..." }
],
"applicableControls": ["TOM-..."],
"gaps": ["..."],
"confidence": 0.85
}`
}
// =============================================================================
// TOM DESCRIPTION GENERATION PROMPT
// =============================================================================
export interface TOMDescriptionPromptContext {
control: ControlLibraryEntry
companyProfile: CompanyProfile
dataProfile: DataProfile
architectureProfile: ArchitectureProfile
riskProfile: RiskProfile
language?: 'de' | 'en'
}
export function getTOMDescriptionPrompt(
context: TOMDescriptionPromptContext
): string {
const {
control,
companyProfile,
dataProfile,
architectureProfile,
riskProfile,
language = 'de',
} = context
if (language === 'de') {
return `Du bist ein Experte für Datenschutz-Compliance und erstellst eine unternehmensspezifische TOM-Beschreibung.
KONTROLLE:
- Name: ${control.name.de}
- Beschreibung: ${control.description.de}
- Kategorie: ${control.category}
- Typ: ${control.type}
UNTERNEHMENSPROFIL:
- Branche: ${companyProfile.industry}
- Größe: ${companyProfile.size}
- Rolle: ${companyProfile.role}
- Produkte/Services: ${companyProfile.products.join(', ')}
DATENPROFIL:
- Datenkategorien: ${dataProfile.categories.join(', ')}
- Besondere Kategorien: ${dataProfile.hasSpecialCategories ? 'Ja' : 'Nein'}
- Betroffene: ${dataProfile.subjects.join(', ')}
- Datenvolumen: ${dataProfile.dataVolume}
ARCHITEKTUR:
- Hosting-Modell: ${architectureProfile.hostingModel}
- Standort: ${architectureProfile.hostingLocation}
- Mandantentrennung: ${architectureProfile.multiTenancy}
SCHUTZBEDARF: ${riskProfile.protectionLevel}
AUFGABE: Erstelle eine unternehmensspezifische Beschreibung dieser TOM in 3-5 Sätzen.
Die Beschreibung soll:
- Auf das spezifische Unternehmensprofil zugeschnitten sein
- Konkrete Maßnahmen beschreiben, die für dieses Unternehmen relevant sind
- In formeller Geschäftssprache verfasst sein
- Keine Platzhalter oder generischen Formulierungen enthalten
Antworte nur mit der Beschreibung, ohne zusätzliche Erklärungen.`
}
return `You are a data protection compliance expert creating a company-specific TOM description.
CONTROL:
- Name: ${control.name.en}
- Description: ${control.description.en}
- Category: ${control.category}
- Type: ${control.type}
COMPANY PROFILE:
- Industry: ${companyProfile.industry}
- Size: ${companyProfile.size}
- Role: ${companyProfile.role}
- Products/Services: ${companyProfile.products.join(', ')}
DATA PROFILE:
- Data Categories: ${dataProfile.categories.join(', ')}
- Special Categories: ${dataProfile.hasSpecialCategories ? 'Yes' : 'No'}
- Data Subjects: ${dataProfile.subjects.join(', ')}
- Data Volume: ${dataProfile.dataVolume}
ARCHITECTURE:
- Hosting Model: ${architectureProfile.hostingModel}
- Location: ${architectureProfile.hostingLocation}
- Multi-tenancy: ${architectureProfile.multiTenancy}
PROTECTION LEVEL: ${riskProfile.protectionLevel}
TASK: Create a company-specific description of this TOM in 3-5 sentences.
The description should:
- Be tailored to the specific company profile
- Describe concrete measures relevant to this company
- Be written in formal business language
- Contain no placeholders or generic formulations
Respond only with the description, without additional explanations.`
}
// =============================================================================
// GAP RECOMMENDATIONS PROMPT
// =============================================================================
export interface GapRecommendationsPromptContext {
missingControls: Array<{ controlId: string; name: string; priority: string }>
partialControls: Array<{ controlId: string; name: string; missingAspects: string[] }>
companyProfile: CompanyProfile
riskProfile: RiskProfile
language?: 'de' | 'en'
}
export function getGapRecommendationsPrompt(
context: GapRecommendationsPromptContext
): string {
const {
missingControls,
partialControls,
companyProfile,
riskProfile,
language = 'de',
} = context
const missingList = missingControls
.map((c) => `- ${c.name} (${c.controlId}, Priorität: ${c.priority})`)
.join('\n')
const partialList = partialControls
.map((c) => `- ${c.name} (${c.controlId}): Fehlend: ${c.missingAspects.join(', ')}`)
.join('\n')
if (language === 'de') {
return `Du bist ein Experte für Datenschutz-Compliance und erstellst Handlungsempfehlungen für TOM-Lücken.
UNTERNEHMEN:
- Branche: ${companyProfile.industry}
- Größe: ${companyProfile.size}
- Rolle: ${companyProfile.role}
SCHUTZBEDARF: ${riskProfile.protectionLevel}
FEHLENDE KONTROLLEN:
${missingList || 'Keine'}
TEILWEISE IMPLEMENTIERTE KONTROLLEN:
${partialList || 'Keine'}
AUFGABE: Erstelle konkrete Handlungsempfehlungen, um die Lücken zu schließen.
Für jede Empfehlung:
1. Priorisiere nach Schutzbedarf und DSGVO-Relevanz
2. Berücksichtige die Unternehmensgröße und Branche
3. Gib konkrete, umsetzbare Schritte an
4. Schätze den Aufwand ein (niedrig/mittel/hoch)
Antworte im JSON-Format:
{
"recommendations": [
{
"priority": "HIGH",
"title": "...",
"description": "...",
"steps": ["..."],
"effort": "MEDIUM",
"relatedControls": ["TOM-..."]
}
],
"summary": "Kurze Zusammenfassung der wichtigsten Maßnahmen"
}`
}
return `You are a data protection compliance expert creating recommendations for TOM gaps.
COMPANY:
- Industry: ${companyProfile.industry}
- Size: ${companyProfile.size}
- Role: ${companyProfile.role}
PROTECTION LEVEL: ${riskProfile.protectionLevel}
MISSING CONTROLS:
${missingList || 'None'}
PARTIALLY IMPLEMENTED CONTROLS:
${partialList || 'None'}
TASK: Create concrete recommendations to close the gaps.
For each recommendation:
1. Prioritize by protection level and GDPR relevance
2. Consider company size and industry
3. Provide concrete, actionable steps
4. Estimate effort (low/medium/high)
Respond in JSON format:
{
"recommendations": [
{
"priority": "HIGH",
"title": "...",
"description": "...",
"steps": ["..."],
"effort": "MEDIUM",
"relatedControls": ["TOM-..."]
}
],
"summary": "Brief summary of the most important measures"
}`
}
// =============================================================================
// DOCUMENT TYPE DETECTION PROMPT
// =============================================================================
export function getDocumentTypeDetectionPrompt(
documentText: string,
filename: string
): string {
return `Du bist ein Experte für Datenschutz-Dokumente und sollst den Dokumenttyp erkennen.
DATEINAME: ${filename}
DOKUMENTTEXT (Auszug):
${documentText.substring(0, 2000)}
MÖGLICHE DOKUMENTTYPEN:
- AVV: Auftragsverarbeitungsvertrag
- DPA: Data Processing Agreement (englisch)
- SLA: Service Level Agreement
- NDA: Geheimhaltungsvereinbarung
- POLICY: Interne Richtlinie (z.B. Passwortrichtlinie, IT-Sicherheitsrichtlinie)
- CERTIFICATE: Zertifikat (z.B. ISO 27001, SOC 2)
- AUDIT_REPORT: Audit-Bericht oder Prüfbericht
- OTHER: Sonstiges Dokument
Antworte im JSON-Format:
{
"documentType": "...",
"confidence": 0.85,
"reasoning": "Kurze Begründung"
}`
}
// =============================================================================
// CLAUSE EXTRACTION PROMPT
// =============================================================================
export function getClauseExtractionPrompt(
documentText: string,
controlCategory: string
): string {
return `Du bist ein Experte für Datenschutz-Compliance und extrahierst Klauseln aus einem Dokument.
GESUCHTE KATEGORIE: ${controlCategory}
DOKUMENTTEXT:
${documentText}
AUFGABE: Extrahiere alle Klauseln, die sich auf die Kategorie "${controlCategory}" beziehen.
Antworte im JSON-Format:
{
"clauses": [
{
"id": "clause-1",
"text": "Der extrahierte Text der Klausel",
"section": "Abschnittsnummer oder -name falls vorhanden",
"relevance": "Kurze Erklärung der Relevanz",
"matchScore": 0.9
}
],
"totalFound": 3
}`
}
// =============================================================================
// COMPLIANCE ASSESSMENT PROMPT
// =============================================================================
export function getComplianceAssessmentPrompt(
tomDescription: string,
evidenceDescriptions: string[],
controlRequirements: string[]
): string {
return `Du bist ein Experte für Datenschutz-Compliance und bewertest die Umsetzung einer TOM.
TOM-BESCHREIBUNG:
${tomDescription}
ANFORDERUNGEN AN NACHWEISE:
${controlRequirements.map((r, i) => `${i + 1}. ${r}`).join('\n')}
VORHANDENE NACHWEISE:
${evidenceDescriptions.map((e, i) => `${i + 1}. ${e}`).join('\n') || 'Keine Nachweise vorhanden'}
AUFGABE: Bewerte den Umsetzungsgrad dieser TOM.
Antworte im JSON-Format:
{
"implementationStatus": "NOT_IMPLEMENTED" | "PARTIAL" | "IMPLEMENTED",
"score": 0-100,
"coveredRequirements": ["..."],
"missingRequirements": ["..."],
"recommendations": ["..."],
"reasoning": "Begründung der Bewertung"
}`
}
// =============================================================================
// EXPORT FUNCTIONS
// =============================================================================
export const AI_PROMPTS = {
documentAnalysis: getDocumentAnalysisPrompt,
tomDescription: getTOMDescriptionPrompt,
gapRecommendations: getGapRecommendationsPrompt,
documentTypeDetection: getDocumentTypeDetectionPrompt,
clauseExtraction: getClauseExtractionPrompt,
complianceAssessment: getComplianceAssessmentPrompt,
}

View File

@@ -0,0 +1,698 @@
'use client'
// =============================================================================
// TOM Generator Context
// State management for the TOM Generator Wizard
// =============================================================================
import React, {
createContext,
useContext,
useReducer,
useCallback,
useEffect,
useRef,
ReactNode,
} from 'react'
import {
TOMGeneratorState,
TOMGeneratorStepId,
CompanyProfile,
DataProfile,
ArchitectureProfile,
SecurityProfile,
RiskProfile,
EvidenceDocument,
DerivedTOM,
GapAnalysisResult,
ExportRecord,
WizardStep,
createInitialTOMGeneratorState,
TOM_GENERATOR_STEPS,
getStepIndex,
calculateProtectionLevel,
isDSFARequired,
hasSpecialCategories,
} from './types'
import { TOMRulesEngine } from './rules-engine'
// =============================================================================
// ACTION TYPES
// =============================================================================
type TOMGeneratorAction =
| { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } }
| { type: 'RESET'; payload: { tenantId: string } }
| { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId }
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial<CompanyProfile> }
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
| { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } }
| { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId }
| { type: 'ADD_EVIDENCE'; payload: EvidenceDocument }
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<EvidenceDocument> } }
| { type: 'DELETE_EVIDENCE'; payload: string }
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
| { type: 'ADD_EXPORT'; payload: ExportRecord }
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
// =============================================================================
// REDUCER
// =============================================================================
function tomGeneratorReducer(
state: TOMGeneratorState,
action: TOMGeneratorAction
): TOMGeneratorState {
const updateState = (updates: Partial<TOMGeneratorState>): TOMGeneratorState => ({
...state,
...updates,
updatedAt: new Date(),
})
switch (action.type) {
case 'INITIALIZE': {
if (action.payload.state) {
return action.payload.state
}
return createInitialTOMGeneratorState(action.payload.tenantId)
}
case 'RESET': {
return createInitialTOMGeneratorState(action.payload.tenantId)
}
case 'SET_CURRENT_STEP': {
return updateState({ currentStep: action.payload })
}
case 'SET_COMPANY_PROFILE': {
return updateState({ companyProfile: action.payload })
}
case 'UPDATE_COMPANY_PROFILE': {
if (!state.companyProfile) return state
return updateState({
companyProfile: { ...state.companyProfile, ...action.payload },
})
}
case 'SET_DATA_PROFILE': {
// Automatically set hasSpecialCategories based on categories
const profile: DataProfile = {
...action.payload,
hasSpecialCategories: hasSpecialCategories(action.payload.categories),
}
return updateState({ dataProfile: profile })
}
case 'UPDATE_DATA_PROFILE': {
if (!state.dataProfile) return state
const updatedProfile = { ...state.dataProfile, ...action.payload }
// Recalculate hasSpecialCategories if categories changed
if (action.payload.categories) {
updatedProfile.hasSpecialCategories = hasSpecialCategories(
action.payload.categories
)
}
return updateState({ dataProfile: updatedProfile })
}
case 'SET_ARCHITECTURE_PROFILE': {
return updateState({ architectureProfile: action.payload })
}
case 'UPDATE_ARCHITECTURE_PROFILE': {
if (!state.architectureProfile) return state
return updateState({
architectureProfile: { ...state.architectureProfile, ...action.payload },
})
}
case 'SET_SECURITY_PROFILE': {
return updateState({ securityProfile: action.payload })
}
case 'UPDATE_SECURITY_PROFILE': {
if (!state.securityProfile) return state
return updateState({
securityProfile: { ...state.securityProfile, ...action.payload },
})
}
case 'SET_RISK_PROFILE': {
// Automatically calculate protection level and DSFA requirement
const profile: RiskProfile = {
...action.payload,
protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment),
dsfaRequired: isDSFARequired(state.dataProfile, action.payload),
}
return updateState({ riskProfile: profile })
}
case 'UPDATE_RISK_PROFILE': {
if (!state.riskProfile) return state
const updatedProfile = { ...state.riskProfile, ...action.payload }
// Recalculate protection level if CIA assessment changed
if (action.payload.ciaAssessment) {
updatedProfile.protectionLevel = calculateProtectionLevel(
action.payload.ciaAssessment
)
}
// Recalculate DSFA requirement
updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile)
return updateState({ riskProfile: updatedProfile })
}
case 'COMPLETE_STEP': {
const updatedSteps = state.steps.map((step) =>
step.id === action.payload.stepId
? {
...step,
completed: true,
data: action.payload.data,
validatedAt: new Date(),
}
: step
)
return updateState({ steps: updatedSteps })
}
case 'UNCOMPLETE_STEP': {
const updatedSteps = state.steps.map((step) =>
step.id === action.payload
? { ...step, completed: false, validatedAt: null }
: step
)
return updateState({ steps: updatedSteps })
}
case 'ADD_EVIDENCE': {
return updateState({
documents: [...state.documents, action.payload],
})
}
case 'UPDATE_EVIDENCE': {
const updatedDocuments = state.documents.map((doc) =>
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
)
return updateState({ documents: updatedDocuments })
}
case 'DELETE_EVIDENCE': {
return updateState({
documents: state.documents.filter((doc) => doc.id !== action.payload),
})
}
case 'SET_DERIVED_TOMS': {
return updateState({ derivedTOMs: action.payload })
}
case 'UPDATE_DERIVED_TOM': {
const updatedTOMs = state.derivedTOMs.map((tom) =>
tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom
)
return updateState({ derivedTOMs: updatedTOMs })
}
case 'SET_GAP_ANALYSIS': {
return updateState({ gapAnalysis: action.payload })
}
case 'ADD_EXPORT': {
return updateState({
exports: [...state.exports, action.payload],
})
}
case 'LOAD_STATE': {
return action.payload
}
default:
return state
}
}
// =============================================================================
// CONTEXT VALUE INTERFACE
// =============================================================================
interface TOMGeneratorContextValue {
state: TOMGeneratorState
dispatch: React.Dispatch<TOMGeneratorAction>
// Navigation
currentStepIndex: number
totalSteps: number
canGoNext: boolean
canGoPrevious: boolean
goToStep: (stepId: TOMGeneratorStepId) => void
goToNextStep: () => void
goToPreviousStep: () => void
completeCurrentStep: (data: unknown) => void
// Profile setters
setCompanyProfile: (profile: CompanyProfile) => void
updateCompanyProfile: (data: Partial<CompanyProfile>) => void
setDataProfile: (profile: DataProfile) => void
updateDataProfile: (data: Partial<DataProfile>) => void
setArchitectureProfile: (profile: ArchitectureProfile) => void
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
setSecurityProfile: (profile: SecurityProfile) => void
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
setRiskProfile: (profile: RiskProfile) => void
updateRiskProfile: (data: Partial<RiskProfile>) => void
// Evidence management
addEvidence: (document: EvidenceDocument) => void
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
deleteEvidence: (id: string) => void
// TOM derivation
deriveTOMs: () => void
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
// Gap analysis
runGapAnalysis: () => void
// Export
addExport: (record: ExportRecord) => void
// Persistence
saveState: () => Promise<void>
loadState: () => Promise<void>
resetState: () => void
// Status
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
getCompletionPercentage: () => number
isLoading: boolean
error: string | null
}
// =============================================================================
// CONTEXT
// =============================================================================
const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(null)
// =============================================================================
// STORAGE KEYS
// =============================================================================
const STORAGE_KEY_PREFIX = 'tom-generator-state-'
function getStorageKey(tenantId: string): string {
return `${STORAGE_KEY_PREFIX}${tenantId}`
}
// =============================================================================
// PROVIDER COMPONENT
// =============================================================================
interface TOMGeneratorProviderProps {
children: ReactNode
tenantId: string
initialState?: TOMGeneratorState
enablePersistence?: boolean
}
export function TOMGeneratorProvider({
children,
tenantId,
initialState,
enablePersistence = true,
}: TOMGeneratorProviderProps) {
const [state, dispatch] = useReducer(
tomGeneratorReducer,
initialState ?? createInitialTOMGeneratorState(tenantId)
)
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const rulesEngineRef = useRef<TOMRulesEngine | null>(null)
// Initialize rules engine
useEffect(() => {
if (!rulesEngineRef.current) {
rulesEngineRef.current = new TOMRulesEngine()
}
}, [])
// Load state from localStorage on mount
useEffect(() => {
if (enablePersistence && typeof window !== 'undefined') {
try {
const stored = localStorage.getItem(getStorageKey(tenantId))
if (stored) {
const parsed = JSON.parse(stored)
// Convert date strings back to Date objects
if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt)
if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt)
if (parsed.steps) {
parsed.steps = parsed.steps.map((step: WizardStep) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
}))
}
if (parsed.documents) {
parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({
...doc,
uploadedAt: new Date(doc.uploadedAt),
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
aiAnalysis: doc.aiAnalysis
? {
...doc.aiAnalysis,
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
}
: null,
}))
}
if (parsed.derivedTOMs) {
parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({
...tom,
implementationDate: tom.implementationDate
? new Date(tom.implementationDate)
: null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
}))
}
if (parsed.gapAnalysis?.generatedAt) {
parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt)
}
if (parsed.exports) {
parsed.exports = parsed.exports.map((exp: ExportRecord) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
}))
}
dispatch({ type: 'LOAD_STATE', payload: parsed })
}
} catch (e) {
console.error('Failed to load TOM Generator state from localStorage:', e)
}
}
}, [tenantId, enablePersistence])
// Save state to localStorage on changes
useEffect(() => {
if (enablePersistence && typeof window !== 'undefined') {
try {
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state))
} catch (e) {
console.error('Failed to save TOM Generator state to localStorage:', e)
}
}
}, [state, tenantId, enablePersistence])
// Navigation helpers
const currentStepIndex = getStepIndex(state.currentStep)
const totalSteps = TOM_GENERATOR_STEPS.length
const canGoNext = currentStepIndex < totalSteps - 1
const canGoPrevious = currentStepIndex > 0
const goToStep = useCallback((stepId: TOMGeneratorStepId) => {
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
}, [])
const goToNextStep = useCallback(() => {
if (canGoNext) {
const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1]
dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id })
}
}, [canGoNext, currentStepIndex])
const goToPreviousStep = useCallback(() => {
if (canGoPrevious) {
const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1]
dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id })
}
}, [canGoPrevious, currentStepIndex])
const completeCurrentStep = useCallback(
(data: unknown) => {
dispatch({
type: 'COMPLETE_STEP',
payload: { stepId: state.currentStep, data },
})
},
[state.currentStep]
)
// Profile setters
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
}, [])
const updateCompanyProfile = useCallback((data: Partial<CompanyProfile>) => {
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
}, [])
const setDataProfile = useCallback((profile: DataProfile) => {
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
}, [])
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
}, [])
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
}, [])
const updateArchitectureProfile = useCallback(
(data: Partial<ArchitectureProfile>) => {
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
},
[]
)
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
}, [])
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
}, [])
const setRiskProfile = useCallback((profile: RiskProfile) => {
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
}, [])
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data })
}, [])
// Evidence management
const addEvidence = useCallback((document: EvidenceDocument) => {
dispatch({ type: 'ADD_EVIDENCE', payload: document })
}, [])
const updateEvidence = useCallback(
(id: string, data: Partial<EvidenceDocument>) => {
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
},
[]
)
const deleteEvidence = useCallback((id: string) => {
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
}, [])
// TOM derivation
const deriveTOMs = useCallback(() => {
if (!rulesEngineRef.current) return
const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({
companyProfile: state.companyProfile,
dataProfile: state.dataProfile,
architectureProfile: state.architectureProfile,
securityProfile: state.securityProfile,
riskProfile: state.riskProfile,
})
dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs })
}, [
state.companyProfile,
state.dataProfile,
state.architectureProfile,
state.securityProfile,
state.riskProfile,
])
const updateDerivedTOM = useCallback(
(id: string, data: Partial<DerivedTOM>) => {
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
},
[]
)
// Gap analysis
const runGapAnalysis = useCallback(() => {
if (!rulesEngineRef.current) return
const result = rulesEngineRef.current.performGapAnalysis(
state.derivedTOMs,
state.documents
)
dispatch({ type: 'SET_GAP_ANALYSIS', payload: result })
}, [state.derivedTOMs, state.documents])
// Export
const addExport = useCallback((record: ExportRecord) => {
dispatch({ type: 'ADD_EXPORT', payload: record })
}, [])
// Persistence
const saveState = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
// API call to save state
const response = await fetch('/api/sdk/v1/tom-generator/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenantId, state }),
})
if (!response.ok) {
throw new Error('Failed to save state')
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error')
throw e
} finally {
setIsLoading(false)
}
}, [tenantId, state])
const loadState = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(
`/api/sdk/v1/tom-generator/state?tenantId=${tenantId}`
)
if (!response.ok) {
throw new Error('Failed to load state')
}
const data = await response.json()
if (data.state) {
dispatch({ type: 'LOAD_STATE', payload: data.state })
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error')
throw e
} finally {
setIsLoading(false)
}
}, [tenantId])
const resetState = useCallback(() => {
dispatch({ type: 'RESET', payload: { tenantId } })
}, [tenantId])
// Status helpers
const isStepCompleted = useCallback(
(stepId: TOMGeneratorStepId) => {
const step = state.steps.find((s) => s.id === stepId)
return step?.completed ?? false
},
[state.steps]
)
const getCompletionPercentage = useCallback(() => {
const completedSteps = state.steps.filter((s) => s.completed).length
return Math.round((completedSteps / totalSteps) * 100)
}, [state.steps, totalSteps])
const contextValue: TOMGeneratorContextValue = {
state,
dispatch,
currentStepIndex,
totalSteps,
canGoNext,
canGoPrevious,
goToStep,
goToNextStep,
goToPreviousStep,
completeCurrentStep,
setCompanyProfile,
updateCompanyProfile,
setDataProfile,
updateDataProfile,
setArchitectureProfile,
updateArchitectureProfile,
setSecurityProfile,
updateSecurityProfile,
setRiskProfile,
updateRiskProfile,
addEvidence,
updateEvidence,
deleteEvidence,
deriveTOMs,
updateDerivedTOM,
runGapAnalysis,
addExport,
saveState,
loadState,
resetState,
isStepCompleted,
getCompletionPercentage,
isLoading,
error,
}
return (
<TOMGeneratorContext.Provider value={contextValue}>
{children}
</TOMGeneratorContext.Provider>
)
}
// =============================================================================
// HOOK
// =============================================================================
export function useTOMGenerator(): TOMGeneratorContextValue {
const context = useContext(TOMGeneratorContext)
if (!context) {
throw new Error(
'useTOMGenerator must be used within a TOMGeneratorProvider'
)
}
return context
}
// =============================================================================
// EXPORTS
// =============================================================================
export { TOMGeneratorContext }
export type { TOMGeneratorAction, TOMGeneratorContextValue }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,518 @@
// =============================================================================
// TOM Generator Demo Data
// Sample data for demonstration and testing
// =============================================================================
import {
TOMGeneratorState,
CompanyProfile,
DataProfile,
ArchitectureProfile,
SecurityProfile,
RiskProfile,
EvidenceDocument,
DerivedTOM,
GapAnalysisResult,
TOM_GENERATOR_STEPS,
} from '../types'
import { getTOMRulesEngine } from '../rules-engine'
// =============================================================================
// DEMO COMPANY PROFILES
// =============================================================================
export const DEMO_COMPANY_PROFILES: Record<string, CompanyProfile> = {
saas: {
id: 'demo-company-saas',
name: 'CloudTech Solutions GmbH',
industry: 'Software / SaaS',
size: 'MEDIUM',
role: 'PROCESSOR',
products: ['Cloud CRM', 'Analytics Platform', 'API Services'],
dpoPerson: 'Dr. Maria Schmidt',
dpoEmail: 'dpo@cloudtech.de',
itSecurityContact: 'Thomas Müller',
},
healthcare: {
id: 'demo-company-health',
name: 'MediCare Digital GmbH',
industry: 'Gesundheitswesen / HealthTech',
size: 'SMALL',
role: 'CONTROLLER',
products: ['Patientenportal', 'Telemedizin-App', 'Terminbuchung'],
dpoPerson: 'Dr. Klaus Weber',
dpoEmail: 'datenschutz@medicare.de',
itSecurityContact: 'Anna Bauer',
},
enterprise: {
id: 'demo-company-enterprise',
name: 'GlobalCorp AG',
industry: 'Finanzdienstleistungen',
size: 'ENTERPRISE',
role: 'CONTROLLER',
products: ['Online Banking', 'Investment Platform', 'Payment Services'],
dpoPerson: 'Prof. Dr. Hans Meyer',
dpoEmail: 'privacy@globalcorp.de',
itSecurityContact: 'Security Team',
},
}
// =============================================================================
// DEMO DATA PROFILES
// =============================================================================
export const DEMO_DATA_PROFILES: Record<string, DataProfile> = {
saas: {
categories: ['IDENTIFICATION', 'CONTACT', 'PROFESSIONAL', 'BEHAVIORAL'],
subjects: ['CUSTOMERS', 'EMPLOYEES'],
hasSpecialCategories: false,
processesMinors: false,
dataVolume: 'HIGH',
thirdCountryTransfers: true,
thirdCountryList: ['USA'],
},
healthcare: {
categories: ['IDENTIFICATION', 'CONTACT', 'HEALTH', 'BIOMETRIC'],
subjects: ['PATIENTS', 'EMPLOYEES'],
hasSpecialCategories: true,
processesMinors: true,
dataVolume: 'MEDIUM',
thirdCountryTransfers: false,
thirdCountryList: [],
},
enterprise: {
categories: ['IDENTIFICATION', 'CONTACT', 'FINANCIAL', 'BEHAVIORAL'],
subjects: ['CUSTOMERS', 'EMPLOYEES', 'PROSPECTS'],
hasSpecialCategories: false,
processesMinors: false,
dataVolume: 'VERY_HIGH',
thirdCountryTransfers: true,
thirdCountryList: ['USA', 'UK', 'Schweiz'],
},
}
// =============================================================================
// DEMO ARCHITECTURE PROFILES
// =============================================================================
export const DEMO_ARCHITECTURE_PROFILES: Record<string, ArchitectureProfile> = {
saas: {
hostingModel: 'PUBLIC_CLOUD',
hostingLocation: 'EU',
providers: [
{ name: 'AWS', location: 'EU', certifications: ['ISO 27001', 'SOC 2', 'C5'] },
{ name: 'Cloudflare', location: 'EU', certifications: ['ISO 27001'] },
],
multiTenancy: 'MULTI_TENANT',
hasSubprocessors: true,
subprocessorCount: 5,
encryptionAtRest: true,
encryptionInTransit: true,
},
healthcare: {
hostingModel: 'PRIVATE_CLOUD',
hostingLocation: 'DE',
providers: [
{ name: 'Telekom Cloud', location: 'DE', certifications: ['ISO 27001', 'C5', 'TISAX'] },
],
multiTenancy: 'SINGLE_TENANT',
hasSubprocessors: true,
subprocessorCount: 2,
encryptionAtRest: true,
encryptionInTransit: true,
},
enterprise: {
hostingModel: 'HYBRID',
hostingLocation: 'DE',
providers: [
{ name: 'Private Datacenter', location: 'DE', certifications: ['ISO 27001', 'SOC 2'] },
{ name: 'Azure', location: 'EU', certifications: ['ISO 27001', 'C5', 'SOC 2'] },
],
multiTenancy: 'DEDICATED',
hasSubprocessors: true,
subprocessorCount: 10,
encryptionAtRest: true,
encryptionInTransit: true,
},
}
// =============================================================================
// DEMO SECURITY PROFILES
// =============================================================================
export const DEMO_SECURITY_PROFILES: Record<string, SecurityProfile> = {
saas: {
authMethods: [
{ type: 'PASSWORD', provider: null },
{ type: 'MFA', provider: 'Auth0' },
{ type: 'SSO', provider: 'Auth0' },
],
hasMFA: true,
hasSSO: true,
hasIAM: true,
hasPAM: false,
hasEncryptionAtRest: true,
hasEncryptionInTransit: true,
hasLogging: true,
logRetentionDays: 90,
hasBackup: true,
backupFrequency: 'DAILY',
backupRetentionDays: 30,
hasDRPlan: true,
rtoHours: 4,
rpoHours: 1,
hasVulnerabilityManagement: true,
hasPenetrationTests: true,
hasSecurityTraining: true,
},
healthcare: {
authMethods: [
{ type: 'PASSWORD', provider: null },
{ type: 'MFA', provider: 'Microsoft Authenticator' },
{ type: 'CERTIFICATE', provider: 'Internal PKI' },
],
hasMFA: true,
hasSSO: false,
hasIAM: true,
hasPAM: true,
hasEncryptionAtRest: true,
hasEncryptionInTransit: true,
hasLogging: true,
logRetentionDays: 365,
hasBackup: true,
backupFrequency: 'HOURLY',
backupRetentionDays: 90,
hasDRPlan: true,
rtoHours: 2,
rpoHours: 0.5,
hasVulnerabilityManagement: true,
hasPenetrationTests: true,
hasSecurityTraining: true,
},
enterprise: {
authMethods: [
{ type: 'PASSWORD', provider: null },
{ type: 'MFA', provider: 'Okta' },
{ type: 'SSO', provider: 'Okta' },
{ type: 'BIOMETRIC', provider: 'Windows Hello' },
],
hasMFA: true,
hasSSO: true,
hasIAM: true,
hasPAM: true,
hasEncryptionAtRest: true,
hasEncryptionInTransit: true,
hasLogging: true,
logRetentionDays: 730,
hasBackup: true,
backupFrequency: 'HOURLY',
backupRetentionDays: 365,
hasDRPlan: true,
rtoHours: 1,
rpoHours: 0.25,
hasVulnerabilityManagement: true,
hasPenetrationTests: true,
hasSecurityTraining: true,
},
}
// =============================================================================
// DEMO RISK PROFILES
// =============================================================================
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
saas: {
ciaAssessment: {
confidentiality: 3,
integrity: 3,
availability: 4,
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
},
protectionLevel: 'HIGH',
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
hasHighRiskProcessing: false,
dsfaRequired: false,
},
healthcare: {
ciaAssessment: {
confidentiality: 5,
integrity: 5,
availability: 4,
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
enterprise: {
ciaAssessment: {
confidentiality: 4,
integrity: 5,
availability: 5,
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
},
protectionLevel: 'VERY_HIGH',
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
hasHighRiskProcessing: true,
dsfaRequired: true,
},
}
// =============================================================================
// DEMO EVIDENCE DOCUMENTS
// =============================================================================
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
{
id: 'demo-evidence-1',
filename: 'iso27001-certificate.pdf',
originalName: 'ISO 27001 Zertifikat.pdf',
mimeType: 'application/pdf',
size: 245678,
uploadedAt: new Date('2025-01-15'),
uploadedBy: 'admin@company.de',
documentType: 'CERTIFICATE',
detectedType: 'CERTIFICATE',
hash: 'sha256:abc123def456',
validFrom: new Date('2024-06-01'),
validUntil: new Date('2027-05-31'),
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
aiAnalysis: {
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
extractedClauses: [
{
id: 'clause-1',
text: 'Zertifiziert nach ISO/IEC 27001:2022',
type: 'certification',
relatedControlId: 'TOM-RV-04',
},
],
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
gaps: [],
confidence: 0.95,
analyzedAt: new Date('2025-01-15'),
},
status: 'VERIFIED',
},
{
id: 'demo-evidence-2',
filename: 'passwort-richtlinie.pdf',
originalName: 'Passwortrichtlinie v2.1.pdf',
mimeType: 'application/pdf',
size: 128456,
uploadedAt: new Date('2025-01-10'),
uploadedBy: 'admin@company.de',
documentType: 'POLICY',
detectedType: 'POLICY',
hash: 'sha256:xyz789abc012',
validFrom: new Date('2024-09-01'),
validUntil: null,
linkedControlIds: ['TOM-ADM-02'],
aiAnalysis: {
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
extractedClauses: [
{
id: 'clause-1',
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
{
id: 'clause-2',
text: 'Passwörter müssen alle 90 Tage geändert werden',
type: 'password-policy',
relatedControlId: 'TOM-ADM-02',
},
],
applicableControls: ['TOM-ADM-02'],
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
confidence: 0.85,
analyzedAt: new Date('2025-01-10'),
},
status: 'ANALYZED',
},
{
id: 'demo-evidence-3',
filename: 'aws-avv.pdf',
originalName: 'AWS Data Processing Addendum.pdf',
mimeType: 'application/pdf',
size: 456789,
uploadedAt: new Date('2025-01-05'),
uploadedBy: 'admin@company.de',
documentType: 'AVV',
detectedType: 'DPA',
hash: 'sha256:qwe123rty456',
validFrom: new Date('2024-01-01'),
validUntil: null,
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
aiAnalysis: {
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
extractedClauses: [
{
id: 'clause-1',
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
type: 'data-processing',
relatedControlId: 'TOM-OR-01',
},
{
id: 'clause-2',
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
type: 'audit',
relatedControlId: 'TOM-OR-02',
},
],
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
gaps: [],
confidence: 0.9,
analyzedAt: new Date('2025-01-05'),
},
status: 'VERIFIED',
},
]
// =============================================================================
// DEMO STATE GENERATOR
// =============================================================================
export type DemoScenario = 'saas' | 'healthcare' | 'enterprise'
/**
* Generate a complete demo state for a given scenario
*/
export function generateDemoState(
tenantId: string,
scenario: DemoScenario = 'saas'
): TOMGeneratorState {
const companyProfile = DEMO_COMPANY_PROFILES[scenario]
const dataProfile = DEMO_DATA_PROFILES[scenario]
const architectureProfile = DEMO_ARCHITECTURE_PROFILES[scenario]
const securityProfile = DEMO_SECURITY_PROFILES[scenario]
const riskProfile = DEMO_RISK_PROFILES[scenario]
// Generate derived TOMs using the rules engine
const rulesEngine = getTOMRulesEngine()
const derivedTOMs = rulesEngine.deriveAllTOMs({
companyProfile,
dataProfile,
architectureProfile,
securityProfile,
riskProfile,
})
// Set some TOMs as implemented for demo
const implementedTOMs = derivedTOMs.map((tom, index) => ({
...tom,
implementationStatus:
index % 3 === 0
? 'IMPLEMENTED' as const
: index % 3 === 1
? 'PARTIAL' as const
: 'NOT_IMPLEMENTED' as const,
responsiblePerson:
index % 2 === 0 ? 'IT Security Team' : 'Datenschutzbeauftragter',
implementationDate:
index % 3 === 0 ? new Date('2024-06-15') : null,
}))
// Generate gap analysis
const gapAnalysis = rulesEngine.performGapAnalysis(
implementedTOMs,
DEMO_EVIDENCE_DOCUMENTS
)
const now = new Date()
return {
id: `demo-state-${scenario}-${Date.now()}`,
tenantId,
companyProfile,
dataProfile,
architectureProfile,
securityProfile,
riskProfile,
currentStep: 'review-export',
steps: TOM_GENERATOR_STEPS.map((step) => ({
id: step.id,
completed: true,
data: null,
validatedAt: now,
})),
documents: DEMO_EVIDENCE_DOCUMENTS,
derivedTOMs: implementedTOMs,
gapAnalysis,
exports: [],
createdAt: now,
updatedAt: now,
}
}
/**
* Generate an empty starter state
*/
export function generateEmptyState(tenantId: string): TOMGeneratorState {
const now = new Date()
return {
id: `new-state-${Date.now()}`,
tenantId,
companyProfile: null,
dataProfile: null,
architectureProfile: null,
securityProfile: null,
riskProfile: null,
currentStep: 'scope-roles',
steps: TOM_GENERATOR_STEPS.map((step) => ({
id: step.id,
completed: false,
data: null,
validatedAt: null,
})),
documents: [],
derivedTOMs: [],
gapAnalysis: null,
exports: [],
createdAt: now,
updatedAt: now,
}
}
/**
* Generate partial state (first 3 steps completed)
*/
export function generatePartialState(
tenantId: string,
scenario: DemoScenario = 'saas'
): TOMGeneratorState {
const state = generateEmptyState(tenantId)
const now = new Date()
state.companyProfile = DEMO_COMPANY_PROFILES[scenario]
state.dataProfile = DEMO_DATA_PROFILES[scenario]
state.architectureProfile = DEMO_ARCHITECTURE_PROFILES[scenario]
state.currentStep = 'security-profile'
state.steps = state.steps.map((step, index) => ({
...step,
completed: index < 3,
validatedAt: index < 3 ? now : null,
}))
return state
}
// =============================================================================
// EXPORTS
// =============================================================================
export {
DEMO_COMPANY_PROFILES as demoCompanyProfiles,
DEMO_DATA_PROFILES as demoDataProfiles,
DEMO_ARCHITECTURE_PROFILES as demoArchitectureProfiles,
DEMO_SECURITY_PROFILES as demoSecurityProfiles,
DEMO_RISK_PROFILES as demoRiskProfiles,
DEMO_EVIDENCE_DOCUMENTS as demoEvidenceDocuments,
}

View File

@@ -0,0 +1,67 @@
// =============================================================================
// TOM Generator Evidence Store
// Shared in-memory storage for evidence documents
// =============================================================================
import { EvidenceDocument, DocumentType } from './types'
interface StoredEvidence {
tenantId: string
documents: EvidenceDocument[]
}
class InMemoryEvidenceStore {
private store: Map<string, StoredEvidence> = new Map()
async getAll(tenantId: string): Promise<EvidenceDocument[]> {
const stored = this.store.get(tenantId)
return stored?.documents || []
}
async getById(tenantId: string, documentId: string): Promise<EvidenceDocument | null> {
const stored = this.store.get(tenantId)
return stored?.documents.find((d) => d.id === documentId) || null
}
async add(tenantId: string, document: EvidenceDocument): Promise<EvidenceDocument> {
const stored = this.store.get(tenantId) || { tenantId, documents: [] }
stored.documents.push(document)
this.store.set(tenantId, stored)
return document
}
async update(tenantId: string, documentId: string, updates: Partial<EvidenceDocument>): Promise<EvidenceDocument | null> {
const stored = this.store.get(tenantId)
if (!stored) return null
const index = stored.documents.findIndex((d) => d.id === documentId)
if (index === -1) return null
stored.documents[index] = { ...stored.documents[index], ...updates }
this.store.set(tenantId, stored)
return stored.documents[index]
}
async delete(tenantId: string, documentId: string): Promise<boolean> {
const stored = this.store.get(tenantId)
if (!stored) return false
const initialLength = stored.documents.length
stored.documents = stored.documents.filter((d) => d.id !== documentId)
this.store.set(tenantId, stored)
return stored.documents.length < initialLength
}
async getByType(tenantId: string, type: DocumentType): Promise<EvidenceDocument[]> {
const stored = this.store.get(tenantId)
return stored?.documents.filter((d) => d.documentType === type) || []
}
async getByStatus(tenantId: string, status: string): Promise<EvidenceDocument[]> {
const stored = this.store.get(tenantId)
return stored?.documents.filter((d) => d.status === status) || []
}
}
// Singleton instance for the application
export const evidenceStore = new InMemoryEvidenceStore()

View File

@@ -0,0 +1,525 @@
// =============================================================================
// TOM Generator DOCX Export
// Export TOMs to Microsoft Word format
// =============================================================================
import {
TOMGeneratorState,
DerivedTOM,
ControlCategory,
CONTROL_CATEGORIES,
} from '../types'
import { getControlById, getCategoryMetadata } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface DOCXExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
companyLogo?: string
primaryColor?: string
}
const DEFAULT_OPTIONS: DOCXExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
primaryColor: '#1a56db',
}
// =============================================================================
// DOCX CONTENT GENERATION
// =============================================================================
export interface DocxParagraph {
type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet'
content: string
style?: Record<string, string>
}
export interface DocxTableRow {
cells: string[]
isHeader?: boolean
}
export interface DocxTable {
type: 'table'
headers: string[]
rows: DocxTableRow[]
}
export type DocxElement = DocxParagraph | DocxTable
/**
* Generate DOCX content structure for TOMs
*/
export function generateDOCXContent(
state: TOMGeneratorState,
options: Partial<DOCXExportOptions> = {}
): DocxElement[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const elements: DocxElement[] = []
// Title page
elements.push({
type: 'heading1',
content: opts.language === 'de'
? 'Technische und Organisatorische Maßnahmen (TOMs)'
: 'Technical and Organizational Measures (TOMs)',
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `gemäß Art. 32 DSGVO`
: 'according to Art. 32 GDPR',
})
// Company info
if (state.companyProfile) {
elements.push({
type: 'heading2',
content: opts.language === 'de' ? 'Unternehmen' : 'Company',
})
elements.push({
type: 'paragraph',
content: `${state.companyProfile.name}`,
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Branche: ${state.companyProfile.industry}`
: `Industry: ${state.companyProfile.industry}`,
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Rolle: ${formatRole(state.companyProfile.role, opts.language)}`
: `Role: ${formatRole(state.companyProfile.role, opts.language)}`,
})
if (state.companyProfile.dpoPerson) {
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Datenschutzbeauftragter: ${state.companyProfile.dpoPerson}`
: `Data Protection Officer: ${state.companyProfile.dpoPerson}`,
})
}
}
// Document metadata
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Stand: ${new Date().toLocaleDateString('de-DE')}`
: `Date: ${new Date().toLocaleDateString('en-US')}`,
})
// Protection level summary
if (state.riskProfile) {
elements.push({
type: 'heading2',
content: opts.language === 'de' ? 'Schutzbedarf' : 'Protection Level',
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Ermittelter Schutzbedarf: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`
: `Determined Protection Level: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`,
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `CIA-Bewertung: Vertraulichkeit ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrität ${state.riskProfile.ciaAssessment.integrity}/5, Verfügbarkeit ${state.riskProfile.ciaAssessment.availability}/5`
: `CIA Assessment: Confidentiality ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrity ${state.riskProfile.ciaAssessment.integrity}/5, Availability ${state.riskProfile.ciaAssessment.availability}/5`,
})
if (state.riskProfile.dsfaRequired) {
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? '⚠️ Eine Datenschutz-Folgenabschätzung (DSFA) ist erforderlich.'
: '⚠️ A Data Protection Impact Assessment (DPIA) is required.',
})
}
}
// TOMs by category
elements.push({
type: 'heading2',
content: opts.language === 'de'
? 'Übersicht der Maßnahmen'
: 'Measures Overview',
})
// Group TOMs by category
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs, opts.includeNotApplicable)
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = tomsByCategory.get(category.id)
if (!categoryTOMs || categoryTOMs.length === 0) continue
const categoryName = category.name[opts.language]
elements.push({
type: 'heading3',
content: `${categoryName} (${category.gdprReference})`,
})
// Create table for this category
const tableHeaders = opts.language === 'de'
? ['ID', 'Maßnahme', 'Typ', 'Status', 'Anwendbarkeit']
: ['ID', 'Measure', 'Type', 'Status', 'Applicability']
const tableRows: DocxTableRow[] = categoryTOMs.map((tom) => ({
cells: [
tom.controlId,
tom.name,
formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language),
formatImplementationStatus(tom.implementationStatus, opts.language),
formatApplicability(tom.applicability, opts.language),
],
}))
elements.push({
type: 'table',
headers: tableHeaders,
rows: tableRows,
})
// Add detailed descriptions
for (const tom of categoryTOMs) {
if (tom.applicability === 'NOT_APPLICABLE' && !opts.includeNotApplicable) {
continue
}
elements.push({
type: 'paragraph',
content: `**${tom.controlId}: ${tom.name}**`,
})
elements.push({
type: 'paragraph',
content: tom.aiGeneratedDescription || tom.description,
})
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Anwendbarkeit: ${formatApplicability(tom.applicability, opts.language)}`
: `Applicability: ${formatApplicability(tom.applicability, opts.language)}`,
})
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Begründung: ${tom.applicabilityReason}`
: `Reason: ${tom.applicabilityReason}`,
})
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Umsetzungsstatus: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`
: `Implementation Status: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`,
})
if (tom.responsiblePerson) {
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Verantwortlich: ${tom.responsiblePerson}`
: `Responsible: ${tom.responsiblePerson}`,
})
}
if (opts.includeEvidence && tom.linkedEvidence.length > 0) {
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Nachweise: ${tom.linkedEvidence.length} Dokument(e) verknüpft`
: `Evidence: ${tom.linkedEvidence.length} document(s) linked`,
})
}
if (tom.evidenceGaps.length > 0) {
elements.push({
type: 'bullet',
content: opts.language === 'de'
? `Fehlende Nachweise: ${tom.evidenceGaps.join(', ')}`
: `Missing Evidence: ${tom.evidenceGaps.join(', ')}`,
})
}
}
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
elements.push({
type: 'heading2',
content: opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis',
})
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Gesamtscore: ${state.gapAnalysis.overallScore}%`
: `Overall Score: ${state.gapAnalysis.overallScore}%`,
})
if (state.gapAnalysis.missingControls.length > 0) {
elements.push({
type: 'heading3',
content: opts.language === 'de'
? 'Fehlende Maßnahmen'
: 'Missing Measures',
})
for (const missing of state.gapAnalysis.missingControls) {
const control = getControlById(missing.controlId)
elements.push({
type: 'bullet',
content: `${missing.controlId}: ${control?.name[opts.language] || 'Unknown'} (${missing.priority})`,
})
}
}
if (state.gapAnalysis.recommendations.length > 0) {
elements.push({
type: 'heading3',
content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations',
})
for (const rec of state.gapAnalysis.recommendations) {
elements.push({
type: 'bullet',
content: rec,
})
}
}
}
// Footer
elements.push({
type: 'paragraph',
content: opts.language === 'de'
? `Dieses Dokument wurde automatisch generiert mit dem TOM Generator am ${new Date().toLocaleDateString('de-DE')}.`
: `This document was automatically generated with the TOM Generator on ${new Date().toLocaleDateString('en-US')}.`,
})
return elements
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function groupTOMsByCategory(
toms: DerivedTOM[],
includeNotApplicable: boolean
): Map<ControlCategory, DerivedTOM[]> {
const grouped = new Map<ControlCategory, DerivedTOM[]>()
for (const tom of toms) {
if (!includeNotApplicable && tom.applicability === 'NOT_APPLICABLE') {
continue
}
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
const existing = grouped.get(category) || []
existing.push(tom)
grouped.set(category, existing)
}
return grouped
}
function formatRole(role: string, language: 'de' | 'en'): string {
const roles: Record<string, Record<'de' | 'en', string>> = {
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
}
return roles[role]?.[language] || role
}
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
const levels: Record<string, Record<'de' | 'en', string>> = {
NORMAL: { de: 'Normal', en: 'Normal' },
HIGH: { de: 'Hoch', en: 'High' },
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
}
return levels[level]?.[language] || level
}
function formatType(type: string, language: 'de' | 'en'): string {
const types: Record<string, Record<'de' | 'en', string>> = {
TECHNICAL: { de: 'Technisch', en: 'Technical' },
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
}
return types[type]?.[language] || type
}
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
const statuses: Record<string, Record<'de' | 'en', string>> = {
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
PARTIAL: { de: 'Teilweise umgesetzt', en: 'Partially Implemented' },
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
}
return statuses[status]?.[language] || status
}
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
const apps: Record<string, Record<'de' | 'en', string>> = {
REQUIRED: { de: 'Erforderlich', en: 'Required' },
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
OPTIONAL: { de: 'Optional', en: 'Optional' },
NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' },
}
return apps[applicability]?.[language] || applicability
}
// =============================================================================
// DOCX BLOB GENERATION
// Uses simple XML structure compatible with docx libraries
// =============================================================================
/**
* Generate a DOCX file as a Blob
* Note: For production, use docx library (npm install docx)
* This is a simplified version that generates XML-based content
*/
export async function generateDOCXBlob(
state: TOMGeneratorState,
options: Partial<DOCXExportOptions> = {}
): Promise<Blob> {
const content = generateDOCXContent(state, options)
// Generate simple HTML that can be converted to DOCX
// In production, use the docx library for proper DOCX generation
const html = generateHTMLFromContent(content, options)
// Return as a Word-compatible HTML blob
// The proper way would be to use the docx library
const blob = new Blob([html], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
return blob
}
function generateHTMLFromContent(
content: DocxElement[],
options: Partial<DOCXExportOptions>
): string {
const opts = { ...DEFAULT_OPTIONS, ...options }
let html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.5; }
h1 { font-size: 24pt; color: ${opts.primaryColor}; border-bottom: 2px solid ${opts.primaryColor}; }
h2 { font-size: 18pt; color: ${opts.primaryColor}; margin-top: 24pt; }
h3 { font-size: 14pt; color: #333; margin-top: 18pt; }
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
th, td { border: 1px solid #ddd; padding: 8pt; text-align: left; }
th { background-color: ${opts.primaryColor}; color: white; }
tr:nth-child(even) { background-color: #f9f9f9; }
ul { margin: 6pt 0; }
li { margin: 3pt 0; }
.warning { color: #dc2626; font-weight: bold; }
</style>
</head>
<body>
`
for (const element of content) {
if (element.type === 'table') {
html += '<table>'
html += '<tr>'
for (const header of element.headers) {
html += `<th>${escapeHtml(header)}</th>`
}
html += '</tr>'
for (const row of element.rows) {
html += '<tr>'
for (const cell of row.cells) {
html += `<td>${escapeHtml(cell)}</td>`
}
html += '</tr>'
}
html += '</table>'
} else {
const tag = getHtmlTag(element.type)
const processedContent = processContent(element.content)
html += `<${tag}>${processedContent}</${tag}>\n`
}
}
html += '</body></html>'
return html
}
function getHtmlTag(type: string): string {
switch (type) {
case 'heading1':
return 'h1'
case 'heading2':
return 'h2'
case 'heading3':
return 'h3'
case 'bullet':
return 'li'
default:
return 'p'
}
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
function processContent(content: string): string {
// Convert markdown-style bold to HTML
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the DOCX export
*/
export function generateDOCXFilename(
state: TOMGeneratorState,
language: 'de' | 'en' = 'de'
): string {
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'TOMs' : 'TOMs'
return `${prefix}-${companyName}-${date}.docx`
}
// Types are exported at their definition site above

View File

@@ -0,0 +1,517 @@
// =============================================================================
// TOM Generator PDF Export
// Export TOMs to PDF format
// =============================================================================
import {
TOMGeneratorState,
DerivedTOM,
CONTROL_CATEGORIES,
} from '../types'
import { getControlById } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface PDFExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
companyLogo?: string
primaryColor?: string
pageSize?: 'A4' | 'LETTER'
orientation?: 'portrait' | 'landscape'
}
const DEFAULT_OPTIONS: PDFExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
primaryColor: '#1a56db',
pageSize: 'A4',
orientation: 'portrait',
}
// =============================================================================
// PDF CONTENT STRUCTURE
// =============================================================================
export interface PDFSection {
type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak'
content?: string
items?: string[]
table?: {
headers: string[]
rows: string[][]
}
style?: {
color?: string
fontSize?: number
bold?: boolean
italic?: boolean
align?: 'left' | 'center' | 'right'
}
}
/**
* Generate PDF content structure for TOMs
*/
export function generatePDFContent(
state: TOMGeneratorState,
options: Partial<PDFExportOptions> = {}
): PDFSection[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const sections: PDFSection[] = []
// Title page
sections.push({
type: 'title',
content: opts.language === 'de'
? 'Technische und Organisatorische Maßnahmen (TOMs)'
: 'Technical and Organizational Measures (TOMs)',
style: { color: opts.primaryColor, fontSize: 24, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? 'gemäß Art. 32 DSGVO'
: 'according to Art. 32 GDPR',
style: { fontSize: 14, align: 'center' },
})
// Company information
if (state.companyProfile) {
sections.push({
type: 'paragraph',
content: state.companyProfile.name,
style: { fontSize: 16, bold: true, align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${opts.language === 'de' ? 'Branche' : 'Industry'}: ${state.companyProfile.industry}`,
style: { align: 'center' },
})
sections.push({
type: 'paragraph',
content: `${opts.language === 'de' ? 'Stand' : 'Date'}: ${new Date().toLocaleDateString(opts.language === 'de' ? 'de-DE' : 'en-US')}`,
style: { align: 'center' },
})
}
sections.push({ type: 'pagebreak' })
// Table of Contents
sections.push({
type: 'heading',
content: opts.language === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
style: { color: opts.primaryColor },
})
const tocItems = [
opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary',
opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level',
opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview',
]
let sectionNum = 4
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
return control?.category === category.id &&
(opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
})
if (categoryTOMs.length > 0) {
tocItems.push(`${sectionNum}. ${category.name[opts.language]}`)
sectionNum++
}
}
if (opts.includeGapAnalysis && state.gapAnalysis) {
tocItems.push(`${sectionNum}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`)
}
sections.push({
type: 'list',
items: tocItems,
})
sections.push({ type: 'pagebreak' })
// Executive Summary
sections.push({
type: 'heading',
content: opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary',
style: { color: opts.primaryColor },
})
const totalTOMs = state.derivedTOMs.length
const requiredTOMs = state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length
const implementedTOMs = state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Dieses Dokument beschreibt die technischen und organisatorischen Maßnahmen (TOMs) gemäß Art. 32 DSGVO. Insgesamt wurden ${totalTOMs} Kontrollen bewertet, davon ${requiredTOMs} als erforderlich eingestuft. Aktuell sind ${implementedTOMs} Maßnahmen vollständig umgesetzt.`
: `This document describes the technical and organizational measures (TOMs) according to Art. 32 GDPR. A total of ${totalTOMs} controls were evaluated, of which ${requiredTOMs} are classified as required. Currently, ${implementedTOMs} measures are fully implemented.`,
})
// Summary statistics table
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['Kategorie', 'Anzahl', 'Erforderlich', 'Umgesetzt']
: ['Category', 'Count', 'Required', 'Implemented'],
rows: generateCategorySummary(state.derivedTOMs, opts),
},
})
// Protection Level
sections.push({
type: 'heading',
content: opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level',
style: { color: opts.primaryColor },
})
if (state.riskProfile) {
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Der ermittelte Schutzbedarf beträgt: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`
: `The determined protection level is: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`,
})
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['Schutzziel', 'Bewertung (1-5)', 'Bedeutung']
: ['Protection Goal', 'Rating (1-5)', 'Meaning'],
rows: [
[
opts.language === 'de' ? 'Vertraulichkeit' : 'Confidentiality',
String(state.riskProfile.ciaAssessment.confidentiality),
getCIAMeaning(state.riskProfile.ciaAssessment.confidentiality, opts.language),
],
[
opts.language === 'de' ? 'Integrität' : 'Integrity',
String(state.riskProfile.ciaAssessment.integrity),
getCIAMeaning(state.riskProfile.ciaAssessment.integrity, opts.language),
],
[
opts.language === 'de' ? 'Verfügbarkeit' : 'Availability',
String(state.riskProfile.ciaAssessment.availability),
getCIAMeaning(state.riskProfile.ciaAssessment.availability, opts.language),
],
],
},
})
if (state.riskProfile.dsfaRequired) {
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? '⚠️ HINWEIS: Aufgrund der Verarbeitung ist eine Datenschutz-Folgenabschätzung (DSFA) nach Art. 35 DSGVO erforderlich.'
: '⚠️ NOTE: Due to the processing, a Data Protection Impact Assessment (DPIA) according to Art. 35 GDPR is required.',
style: { bold: true, color: '#dc2626' },
})
}
}
// Measures Overview
sections.push({
type: 'heading',
content: opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview',
style: { color: opts.primaryColor },
})
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['ID', 'Maßnahme', 'Anwendbarkeit', 'Status']
: ['ID', 'Measure', 'Applicability', 'Status'],
rows: state.derivedTOMs
.filter((tom) => opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
.map((tom) => [
tom.controlId,
tom.name,
formatApplicability(tom.applicability, opts.language),
formatImplementationStatus(tom.implementationStatus, opts.language),
]),
},
})
// Detailed sections by category
let currentSection = 4
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = state.derivedTOMs.filter((tom) => {
const control = getControlById(tom.controlId)
return control?.category === category.id &&
(opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
})
if (categoryTOMs.length === 0) continue
sections.push({ type: 'pagebreak' })
sections.push({
type: 'heading',
content: `${currentSection}. ${category.name[opts.language]}`,
style: { color: opts.primaryColor },
})
sections.push({
type: 'paragraph',
content: `${opts.language === 'de' ? 'Rechtsgrundlage' : 'Legal Basis'}: ${category.gdprReference}`,
style: { italic: true },
})
for (const tom of categoryTOMs) {
sections.push({
type: 'subheading',
content: `${tom.controlId}: ${tom.name}`,
})
sections.push({
type: 'paragraph',
content: tom.aiGeneratedDescription || tom.description,
})
sections.push({
type: 'list',
items: [
`${opts.language === 'de' ? 'Typ' : 'Type'}: ${formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language)}`,
`${opts.language === 'de' ? 'Anwendbarkeit' : 'Applicability'}: ${formatApplicability(tom.applicability, opts.language)}`,
`${opts.language === 'de' ? 'Begründung' : 'Reason'}: ${tom.applicabilityReason}`,
`${opts.language === 'de' ? 'Umsetzungsstatus' : 'Implementation Status'}: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`,
...(tom.responsiblePerson ? [`${opts.language === 'de' ? 'Verantwortlich' : 'Responsible'}: ${tom.responsiblePerson}`] : []),
...(opts.includeEvidence && tom.linkedEvidence.length > 0
? [`${opts.language === 'de' ? 'Verknüpfte Nachweise' : 'Linked Evidence'}: ${tom.linkedEvidence.length}`]
: []),
],
})
}
currentSection++
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
sections.push({ type: 'pagebreak' })
sections.push({
type: 'heading',
content: `${currentSection}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`,
style: { color: opts.primaryColor },
})
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Gesamtscore: ${state.gapAnalysis.overallScore}%`
: `Overall Score: ${state.gapAnalysis.overallScore}%`,
style: { fontSize: 16, bold: true },
})
if (state.gapAnalysis.missingControls.length > 0) {
sections.push({
type: 'subheading',
content: opts.language === 'de' ? 'Fehlende Maßnahmen' : 'Missing Measures',
})
sections.push({
type: 'table',
table: {
headers: opts.language === 'de'
? ['ID', 'Maßnahme', 'Priorität']
: ['ID', 'Measure', 'Priority'],
rows: state.gapAnalysis.missingControls.map((mc) => {
const control = getControlById(mc.controlId)
return [
mc.controlId,
control?.name[opts.language] || 'Unknown',
mc.priority,
]
}),
},
})
}
if (state.gapAnalysis.recommendations.length > 0) {
sections.push({
type: 'subheading',
content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations',
})
sections.push({
type: 'list',
items: state.gapAnalysis.recommendations,
})
}
}
// Footer
sections.push({
type: 'paragraph',
content: opts.language === 'de'
? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem TOM Generator`
: `Generated on ${new Date().toLocaleDateString('en-US')} with the TOM Generator`,
style: { italic: true, align: 'center', fontSize: 10 },
})
return sections
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateCategorySummary(
toms: DerivedTOM[],
opts: PDFExportOptions
): string[][] {
const summary: string[][] = []
for (const category of CONTROL_CATEGORIES) {
const categoryTOMs = toms.filter((tom) => {
const control = getControlById(tom.controlId)
return control?.category === category.id
})
if (categoryTOMs.length === 0) continue
const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length
const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
summary.push([
category.name[opts.language],
String(categoryTOMs.length),
String(required),
String(implemented),
])
}
return summary
}
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
const levels: Record<string, Record<'de' | 'en', string>> = {
NORMAL: { de: 'Normal', en: 'Normal' },
HIGH: { de: 'Hoch', en: 'High' },
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
}
return levels[level]?.[language] || level
}
function formatType(type: string, language: 'de' | 'en'): string {
const types: Record<string, Record<'de' | 'en', string>> = {
TECHNICAL: { de: 'Technisch', en: 'Technical' },
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
}
return types[type]?.[language] || type
}
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
const statuses: Record<string, Record<'de' | 'en', string>> = {
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
PARTIAL: { de: 'Teilweise', en: 'Partial' },
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
}
return statuses[status]?.[language] || status
}
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
const apps: Record<string, Record<'de' | 'en', string>> = {
REQUIRED: { de: 'Erforderlich', en: 'Required' },
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
OPTIONAL: { de: 'Optional', en: 'Optional' },
NOT_APPLICABLE: { de: 'N/A', en: 'N/A' },
}
return apps[applicability]?.[language] || applicability
}
function getCIAMeaning(rating: number, language: 'de' | 'en'): string {
const meanings: Record<number, Record<'de' | 'en', string>> = {
1: { de: 'Sehr gering', en: 'Very Low' },
2: { de: 'Gering', en: 'Low' },
3: { de: 'Mittel', en: 'Medium' },
4: { de: 'Hoch', en: 'High' },
5: { de: 'Sehr hoch', en: 'Very High' },
}
return meanings[rating]?.[language] || String(rating)
}
// =============================================================================
// PDF BLOB GENERATION
// Note: For production, use jspdf or pdfmake library
// =============================================================================
/**
* Generate a PDF file as a Blob
* This is a placeholder - in production, use jspdf or similar library
*/
export async function generatePDFBlob(
state: TOMGeneratorState,
options: Partial<PDFExportOptions> = {}
): Promise<Blob> {
const content = generatePDFContent(state, options)
// Convert to simple text-based content for now
// In production, use jspdf library
const textContent = content
.map((section) => {
switch (section.type) {
case 'title':
return `\n\n${'='.repeat(60)}\n${section.content}\n${'='.repeat(60)}\n`
case 'heading':
return `\n\n${section.content}\n${'-'.repeat(40)}\n`
case 'subheading':
return `\n${section.content}\n`
case 'paragraph':
return `${section.content}\n`
case 'list':
return section.items?.map((item) => `${item}`).join('\n') + '\n'
case 'table':
if (section.table) {
const headerLine = section.table.headers.join(' | ')
const separator = '-'.repeat(headerLine.length)
const rows = section.table.rows.map((row) => row.join(' | ')).join('\n')
return `\n${headerLine}\n${separator}\n${rows}\n`
}
return ''
case 'pagebreak':
return '\n\n' + '='.repeat(60) + '\n\n'
default:
return ''
}
})
.join('')
return new Blob([textContent], { type: 'application/pdf' })
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for the PDF export
*/
export function generatePDFFilename(
state: TOMGeneratorState,
language: 'de' | 'en' = 'de'
): string {
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'TOMs' : 'TOMs'
return `${prefix}-${companyName}-${date}.pdf`
}
// Types are exported at their definition site above

View File

@@ -0,0 +1,544 @@
// =============================================================================
// TOM Generator ZIP Export
// Export complete TOM package as ZIP archive
// =============================================================================
import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
import { generateDOCXContent, DOCXExportOptions } from './docx'
import { generatePDFContent, PDFExportOptions } from './pdf'
import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader'
// =============================================================================
// TYPES
// =============================================================================
export interface ZIPExportOptions {
language: 'de' | 'en'
includeNotApplicable: boolean
includeEvidence: boolean
includeGapAnalysis: boolean
includeControlLibrary: boolean
includeRawData: boolean
formats: Array<'json' | 'docx' | 'pdf'>
}
const DEFAULT_OPTIONS: ZIPExportOptions = {
language: 'de',
includeNotApplicable: false,
includeEvidence: true,
includeGapAnalysis: true,
includeControlLibrary: true,
includeRawData: true,
formats: ['json', 'docx'],
}
// =============================================================================
// ZIP CONTENT STRUCTURE
// =============================================================================
export interface ZIPFileEntry {
path: string
content: string | Blob
mimeType: string
}
/**
* Generate all files for the ZIP archive
*/
export function generateZIPFiles(
state: TOMGeneratorState,
options: Partial<ZIPExportOptions> = {}
): ZIPFileEntry[] {
const opts = { ...DEFAULT_OPTIONS, ...options }
const files: ZIPFileEntry[] = []
// README
files.push({
path: 'README.md',
content: generateReadme(state, opts),
mimeType: 'text/markdown',
})
// State JSON
if (opts.includeRawData) {
files.push({
path: 'data/state.json',
content: JSON.stringify(state, null, 2),
mimeType: 'application/json',
})
}
// Profile data
files.push({
path: 'data/profiles/company-profile.json',
content: JSON.stringify(state.companyProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/data-profile.json',
content: JSON.stringify(state.dataProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/architecture-profile.json',
content: JSON.stringify(state.architectureProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/security-profile.json',
content: JSON.stringify(state.securityProfile, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'data/profiles/risk-profile.json',
content: JSON.stringify(state.riskProfile, null, 2),
mimeType: 'application/json',
})
// Derived TOMs
files.push({
path: 'data/toms/derived-toms.json',
content: JSON.stringify(state.derivedTOMs, null, 2),
mimeType: 'application/json',
})
// TOMs by category
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs)
for (const [category, toms] of tomsByCategory.entries()) {
files.push({
path: `data/toms/by-category/${category.toLowerCase()}.json`,
content: JSON.stringify(toms, null, 2),
mimeType: 'application/json',
})
}
// Required TOMs summary
const requiredTOMs = state.derivedTOMs.filter(
(tom) => tom.applicability === 'REQUIRED'
)
files.push({
path: 'data/toms/required-toms.json',
content: JSON.stringify(requiredTOMs, null, 2),
mimeType: 'application/json',
})
// Implementation status summary
const implementationSummary = generateImplementationSummary(state.derivedTOMs)
files.push({
path: 'data/toms/implementation-summary.json',
content: JSON.stringify(implementationSummary, null, 2),
mimeType: 'application/json',
})
// Evidence documents
if (opts.includeEvidence && state.documents.length > 0) {
files.push({
path: 'data/evidence/documents.json',
content: JSON.stringify(state.documents, null, 2),
mimeType: 'application/json',
})
// Evidence by control
const evidenceByControl = groupEvidenceByControl(state.documents)
files.push({
path: 'data/evidence/by-control.json',
content: JSON.stringify(Object.fromEntries(evidenceByControl), null, 2),
mimeType: 'application/json',
})
}
// Gap Analysis
if (opts.includeGapAnalysis && state.gapAnalysis) {
files.push({
path: 'data/gap-analysis/analysis.json',
content: JSON.stringify(state.gapAnalysis, null, 2),
mimeType: 'application/json',
})
// Missing controls details
if (state.gapAnalysis.missingControls.length > 0) {
files.push({
path: 'data/gap-analysis/missing-controls.json',
content: JSON.stringify(state.gapAnalysis.missingControls, null, 2),
mimeType: 'application/json',
})
}
// Recommendations
if (state.gapAnalysis.recommendations.length > 0) {
files.push({
path: 'data/gap-analysis/recommendations.md',
content: generateRecommendationsMarkdown(
state.gapAnalysis.recommendations,
opts.language
),
mimeType: 'text/markdown',
})
}
}
// Control Library
if (opts.includeControlLibrary) {
const controls = getAllControls()
const metadata = getLibraryMetadata()
files.push({
path: 'reference/control-library/metadata.json',
content: JSON.stringify(metadata, null, 2),
mimeType: 'application/json',
})
files.push({
path: 'reference/control-library/all-controls.json',
content: JSON.stringify(controls, null, 2),
mimeType: 'application/json',
})
// Controls by category
for (const category of new Set(controls.map((c) => c.category))) {
const categoryControls = controls.filter((c) => c.category === category)
files.push({
path: `reference/control-library/by-category/${category.toLowerCase()}.json`,
content: JSON.stringify(categoryControls, null, 2),
mimeType: 'application/json',
})
}
}
// Export history
if (state.exports.length > 0) {
files.push({
path: 'data/exports/history.json',
content: JSON.stringify(state.exports, null, 2),
mimeType: 'application/json',
})
}
// DOCX content structure (if requested)
if (opts.formats.includes('docx')) {
const docxOptions: Partial<DOCXExportOptions> = {
language: opts.language,
includeNotApplicable: opts.includeNotApplicable,
includeEvidence: opts.includeEvidence,
includeGapAnalysis: opts.includeGapAnalysis,
}
const docxContent = generateDOCXContent(state, docxOptions)
files.push({
path: 'documents/tom-document-structure.json',
content: JSON.stringify(docxContent, null, 2),
mimeType: 'application/json',
})
}
// PDF content structure (if requested)
if (opts.formats.includes('pdf')) {
const pdfOptions: Partial<PDFExportOptions> = {
language: opts.language,
includeNotApplicable: opts.includeNotApplicable,
includeEvidence: opts.includeEvidence,
includeGapAnalysis: opts.includeGapAnalysis,
}
const pdfContent = generatePDFContent(state, pdfOptions)
files.push({
path: 'documents/tom-document-structure-pdf.json',
content: JSON.stringify(pdfContent, null, 2),
mimeType: 'application/json',
})
}
// Markdown summary
files.push({
path: 'documents/tom-summary.md',
content: generateMarkdownSummary(state, opts),
mimeType: 'text/markdown',
})
// CSV export for spreadsheet import
files.push({
path: 'documents/toms.csv',
content: generateCSV(state.derivedTOMs, opts),
mimeType: 'text/csv',
})
return files
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateReadme(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const date = new Date().toISOString().split('T')[0]
const lang = opts.language
return `# TOM Export Package
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
### /data
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
### /reference
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
### /documents
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
---
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
`
}
function groupTOMsByCategory(
toms: DerivedTOM[]
): Map<string, DerivedTOM[]> {
const grouped = new Map<string, DerivedTOM[]>()
for (const tom of toms) {
const control = getControlById(tom.controlId)
if (!control) continue
const category = control.category
const existing: DerivedTOM[] = grouped.get(category) || []
existing.push(tom)
grouped.set(category, existing)
}
return grouped
}
function generateImplementationSummary(
toms: Array<{ implementationStatus: string; applicability: string }>
): Record<string, number> {
return {
total: toms.length,
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
}
}
function groupEvidenceByControl(
documents: Array<{ id: string; linkedControlIds: string[] }>
): Map<string, string[]> {
const grouped = new Map<string, string[]>()
for (const doc of documents) {
for (const controlId of doc.linkedControlIds) {
const existing = grouped.get(controlId) || []
existing.push(doc.id)
grouped.set(controlId, existing)
}
}
return grouped
}
function generateRecommendationsMarkdown(
recommendations: string[],
language: 'de' | 'en'
): string {
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
return `# ${title}
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
---
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
`
}
function generateMarkdownSummary(
state: TOMGeneratorState,
opts: ZIPExportOptions
): string {
const lang = opts.language
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|--------|-------|
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
`
if (state.gapAnalysis) {
md += `
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
**${state.gapAnalysis.overallScore}%**
`
}
// Add required TOMs table
const requiredTOMs = state.derivedTOMs.filter(
(t) => t.applicability === 'REQUIRED'
)
if (requiredTOMs.length > 0) {
md += `
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|----|----------|--------|
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
`
}
return md
}
function generateCSV(
toms: Array<{
controlId: string
name: string
description: string
applicability: string
implementationStatus: string
responsiblePerson: string | null
}>,
opts: ZIPExportOptions
): string {
const lang = opts.language
const headers = lang === 'de'
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
const rows = toms.map((tom) => [
tom.controlId,
escapeCSV(tom.name),
escapeCSV(tom.description),
tom.applicability,
tom.implementationStatus,
tom.responsiblePerson || '',
])
return [
headers.join(','),
...rows.map((row) => row.join(',')),
].join('\n')
}
function escapeCSV(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
function formatStatus(status: string, lang: 'de' | 'en'): string {
const statuses: Record<string, Record<'de' | 'en', string>> = {
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
PARTIAL: { de: 'Teilweise', en: 'Partial' },
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
}
return statuses[status]?.[lang] || status
}
// =============================================================================
// ZIP BLOB GENERATION
// Note: For production, use jszip library
// =============================================================================
/**
* Generate a ZIP file as a Blob
* This is a placeholder - in production, use jszip library
*/
export async function generateZIPBlob(
state: TOMGeneratorState,
options: Partial<ZIPExportOptions> = {}
): Promise<Blob> {
const files = generateZIPFiles(state, options)
// Create a simple JSON representation for now
// In production, use JSZip library
const manifest = {
generated: new Date().toISOString(),
files: files.map((f) => ({
path: f.path,
mimeType: f.mimeType,
size: typeof f.content === 'string' ? f.content.length : 0,
})),
}
const allContent = files
.filter((f) => typeof f.content === 'string')
.map((f) => `\n\n=== ${f.path} ===\n\n${f.content}`)
.join('\n')
const output = `TOM Export Package
Generated: ${manifest.generated}
Files:
${manifest.files.map((f) => ` - ${f.path} (${f.mimeType})`).join('\n')}
${allContent}`
return new Blob([output], { type: 'application/zip' })
}
// =============================================================================
// FILENAME GENERATION
// =============================================================================
/**
* Generate a filename for ZIP export
*/
export function generateZIPFilename(
state: TOMGeneratorState,
language: 'de' | 'en' = 'de'
): string {
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
const date = new Date().toISOString().split('T')[0]
const prefix = language === 'de' ? 'TOMs-Export' : 'TOMs-Export'
return `${prefix}-${companyName}-${date}.zip`
}
// =============================================================================
// EXPORT
// =============================================================================
// Types are exported at their definition site above

View File

@@ -0,0 +1,206 @@
// =============================================================================
// TOM Generator Module - Public API
// =============================================================================
// Types
export * from './types'
// Context and Hooks
export {
TOMGeneratorProvider,
useTOMGenerator,
TOMGeneratorContext,
} from './context'
export type {
TOMGeneratorAction,
TOMGeneratorContextValue,
} from './context'
// Rules Engine
export {
TOMRulesEngine,
getTOMRulesEngine,
evaluateControlsForContext,
deriveTOMsForContext,
performQuickGapAnalysis,
} from './rules-engine'
// Control Library
export {
getControlLibrary,
getAllControls,
getControlById,
getControlsByCategory,
getControlsByType,
getControlsByPriority,
getControlsByTag,
getAllTags,
getCategoryMetadata,
getAllCategories,
getLibraryMetadata,
searchControls,
getControlsByFramework,
getControlsCountByCategory,
} from './controls/loader'
export type { ControlLibrary } from './controls/loader'
// AI Integration
export {
AI_PROMPTS,
getDocumentAnalysisPrompt,
getTOMDescriptionPrompt,
getGapRecommendationsPrompt,
getDocumentTypeDetectionPrompt,
getClauseExtractionPrompt,
getComplianceAssessmentPrompt,
} from './ai/prompts'
export type {
DocumentAnalysisPromptContext,
TOMDescriptionPromptContext,
GapRecommendationsPromptContext,
} from './ai/prompts'
export {
TOMDocumentAnalyzer,
getDocumentAnalyzer,
analyzeEvidenceDocument,
detectEvidenceDocumentType,
getEvidenceGapsForAllControls,
} from './ai/document-analyzer'
export type {
AnalysisResult,
DocumentTypeDetectionResult,
} from './ai/document-analyzer'
// Export Functions
export {
generateDOCXContent,
generateDOCXBlob,
} from './export/docx'
export type {
DOCXExportOptions,
DocxElement,
DocxParagraph,
DocxTable,
DocxTableRow,
} from './export/docx'
export {
generatePDFContent,
generatePDFBlob,
} from './export/pdf'
export type {
PDFExportOptions,
PDFSection,
} from './export/pdf'
export {
generateZIPFiles,
generateZIPBlob,
} from './export/zip'
export type {
ZIPExportOptions,
ZIPFileEntry,
} from './export/zip'
// Demo Data
export {
generateDemoState,
generateEmptyState,
generatePartialState,
DEMO_COMPANY_PROFILES,
DEMO_DATA_PROFILES,
DEMO_ARCHITECTURE_PROFILES,
DEMO_SECURITY_PROFILES,
DEMO_RISK_PROFILES,
DEMO_EVIDENCE_DOCUMENTS,
} from './demo-data'
export type { DemoScenario } from './demo-data'
// =============================================================================
// CONVENIENCE EXPORTS
// =============================================================================
import { TOMRulesEngine } from './rules-engine'
import {
TOMGeneratorState,
RulesEngineEvaluationContext,
DerivedTOM,
GapAnalysisResult,
EvidenceDocument,
} from './types'
/**
* Create a new TOM Rules Engine instance
*/
export function createRulesEngine(): TOMRulesEngine {
return new TOMRulesEngine()
}
/**
* Derive TOMs for a given state
*/
export function deriveTOMsFromState(state: TOMGeneratorState): DerivedTOM[] {
const engine = new TOMRulesEngine()
return engine.deriveAllTOMs({
companyProfile: state.companyProfile,
dataProfile: state.dataProfile,
architectureProfile: state.architectureProfile,
securityProfile: state.securityProfile,
riskProfile: state.riskProfile,
})
}
/**
* Perform gap analysis on a state
*/
export function analyzeGapsFromState(
state: TOMGeneratorState
): GapAnalysisResult {
const engine = new TOMRulesEngine()
return engine.performGapAnalysis(state.derivedTOMs, state.documents)
}
/**
* Get completion statistics for a state
*/
export function getStateStatistics(state: TOMGeneratorState): {
totalControls: number
requiredControls: number
implementedControls: number
partialControls: number
notImplementedControls: number
complianceScore: number
stepsCompleted: number
totalSteps: number
documentsUploaded: number
} {
const totalControls = state.derivedTOMs.length
const requiredControls = state.derivedTOMs.filter(
(t) => t.applicability === 'REQUIRED'
).length
const implementedControls = state.derivedTOMs.filter(
(t) => t.implementationStatus === 'IMPLEMENTED'
).length
const partialControls = state.derivedTOMs.filter(
(t) => t.implementationStatus === 'PARTIAL'
).length
const notImplementedControls = state.derivedTOMs.filter(
(t) => t.implementationStatus === 'NOT_IMPLEMENTED'
).length
const stepsCompleted = state.steps.filter((s) => s.completed).length
const totalSteps = state.steps.length
return {
totalControls,
requiredControls,
implementedControls,
partialControls,
notImplementedControls,
complianceScore: state.gapAnalysis?.overallScore ?? 0,
stepsCompleted,
totalSteps,
documentsUploaded: state.documents.length,
}
}

View File

@@ -0,0 +1,560 @@
// =============================================================================
// TOM Rules Engine
// Evaluates control applicability based on company context
// =============================================================================
import {
ControlLibraryEntry,
ApplicabilityCondition,
ControlApplicability,
RulesEngineResult,
RulesEngineEvaluationContext,
DerivedTOM,
EvidenceDocument,
GapAnalysisResult,
MissingControl,
PartialControl,
MissingEvidence,
ConditionOperator,
} from './types'
import { getAllControls, getControlById } from './controls/loader'
// =============================================================================
// RULES ENGINE CLASS
// =============================================================================
export class TOMRulesEngine {
private controls: ControlLibraryEntry[]
constructor() {
this.controls = getAllControls()
}
/**
* Evaluate all controls against the current context
*/
evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] {
return this.controls.map((control) => this.evaluateControl(control, context))
}
/**
* Evaluate a single control against the context
*/
evaluateControl(
control: ControlLibraryEntry,
context: RulesEngineEvaluationContext
): RulesEngineResult {
// Sort conditions by priority (highest first)
const sortedConditions = [...control.applicabilityConditions].sort(
(a, b) => b.priority - a.priority
)
// Evaluate conditions in priority order
for (const condition of sortedConditions) {
const matches = this.evaluateCondition(condition, context)
if (matches) {
return {
controlId: control.id,
applicability: condition.result,
reason: this.formatConditionReason(condition, context),
matchedCondition: condition,
}
}
}
// No condition matched, use default applicability
return {
controlId: control.id,
applicability: control.defaultApplicability,
reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfüllt)',
}
}
/**
* Evaluate a single condition
*/
private evaluateCondition(
condition: ApplicabilityCondition,
context: RulesEngineEvaluationContext
): boolean {
const value = this.getFieldValue(condition.field, context)
if (value === undefined || value === null) {
return false
}
return this.evaluateOperator(condition.operator, value, condition.value)
}
/**
* Get a nested field value from the context
*/
private getFieldValue(
fieldPath: string,
context: RulesEngineEvaluationContext
): unknown {
const parts = fieldPath.split('.')
let current: unknown = context
for (const part of parts) {
if (current === null || current === undefined) {
return undefined
}
if (typeof current === 'object') {
current = (current as Record<string, unknown>)[part]
} else {
return undefined
}
}
return current
}
/**
* Evaluate an operator with given values
*/
private evaluateOperator(
operator: ConditionOperator,
actualValue: unknown,
expectedValue: unknown
): boolean {
switch (operator) {
case 'EQUALS':
return actualValue === expectedValue
case 'NOT_EQUALS':
return actualValue !== expectedValue
case 'CONTAINS':
if (Array.isArray(actualValue)) {
return actualValue.includes(expectedValue)
}
if (typeof actualValue === 'string' && typeof expectedValue === 'string') {
return actualValue.includes(expectedValue)
}
return false
case 'GREATER_THAN':
if (typeof actualValue === 'number' && typeof expectedValue === 'number') {
return actualValue > expectedValue
}
return false
case 'IN':
if (Array.isArray(expectedValue)) {
return expectedValue.includes(actualValue)
}
return false
default:
return false
}
}
/**
* Format a human-readable reason for the condition match
*/
private formatConditionReason(
condition: ApplicabilityCondition,
context: RulesEngineEvaluationContext
): string {
const fieldValue = this.getFieldValue(condition.field, context)
const fieldLabel = this.getFieldLabel(condition.field)
switch (condition.operator) {
case 'EQUALS':
return `${fieldLabel} ist "${this.formatValue(fieldValue)}"`
case 'NOT_EQUALS':
return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"`
case 'CONTAINS':
return `${fieldLabel} enthält "${this.formatValue(condition.value)}"`
case 'GREATER_THAN':
return `${fieldLabel} ist größer als ${this.formatValue(condition.value)}`
case 'IN':
return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]`
default:
return `Bedingung erfüllt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}`
}
}
/**
* Get a human-readable label for a field path
*/
private getFieldLabel(fieldPath: string): string {
const labels: Record<string, string> = {
'companyProfile.role': 'Unternehmensrolle',
'companyProfile.size': 'Unternehmensgröße',
'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien',
'dataProfile.processesMinors': 'Verarbeitung von Minderjährigen-Daten',
'dataProfile.dataVolume': 'Datenvolumen',
'dataProfile.thirdCountryTransfers': 'Drittlandübermittlungen',
'architectureProfile.hostingModel': 'Hosting-Modell',
'architectureProfile.hostingLocation': 'Hosting-Standort',
'architectureProfile.multiTenancy': 'Mandantentrennung',
'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter',
'architectureProfile.encryptionAtRest': 'Verschlüsselung ruhender Daten',
'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung',
'securityProfile.hasSSO': 'Single Sign-On',
'securityProfile.hasPAM': 'Privileged Access Management',
'riskProfile.protectionLevel': 'Schutzbedarf',
'riskProfile.dsfaRequired': 'DSFA erforderlich',
'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit',
'riskProfile.ciaAssessment.integrity': 'Integrität',
'riskProfile.ciaAssessment.availability': 'Verfügbarkeit',
}
return labels[fieldPath] || fieldPath
}
/**
* Format a value for display
*/
private formatValue(value: unknown): string {
if (value === true) return 'Ja'
if (value === false) return 'Nein'
if (value === null || value === undefined) return 'nicht gesetzt'
if (Array.isArray(value)) return value.join(', ')
return String(value)
}
/**
* Derive all TOMs based on the current context
*/
deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
const results = this.evaluateControls(context)
return results.map((result) => {
const control = getControlById(result.controlId)
if (!control) {
throw new Error(`Control not found: ${result.controlId}`)
}
return {
id: `derived-${result.controlId}`,
controlId: result.controlId,
name: control.name.de,
description: control.description.de,
applicability: result.applicability,
applicabilityReason: result.reason,
implementationStatus: 'NOT_IMPLEMENTED',
responsiblePerson: null,
responsibleDepartment: null,
implementationDate: null,
reviewDate: null,
linkedEvidence: [],
evidenceGaps: [...control.evidenceRequirements],
aiGeneratedDescription: null,
aiRecommendations: [],
}
})
}
/**
* Get only required and recommended TOMs
*/
getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
const allTOMs = this.deriveAllTOMs(context)
return allTOMs.filter(
(tom) =>
tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
)
}
/**
* Get only required TOMs
*/
getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
const allTOMs = this.deriveAllTOMs(context)
return allTOMs.filter((tom) => tom.applicability === 'REQUIRED')
}
/**
* Perform gap analysis on derived TOMs and evidence
*/
performGapAnalysis(
derivedTOMs: DerivedTOM[],
documents: EvidenceDocument[]
): GapAnalysisResult {
const missingControls: MissingControl[] = []
const partialControls: PartialControl[] = []
const missingEvidence: MissingEvidence[] = []
const recommendations: string[] = []
let totalScore = 0
let totalWeight = 0
// Analyze each required/recommended TOM
const applicableTOMs = derivedTOMs.filter(
(tom) =>
tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
)
for (const tom of applicableTOMs) {
const control = getControlById(tom.controlId)
if (!control) continue
const weight = tom.applicability === 'REQUIRED' ? 3 : 1
totalWeight += weight
// Check implementation status
if (tom.implementationStatus === 'NOT_IMPLEMENTED') {
missingControls.push({
controlId: tom.controlId,
reason: `${control.name.de} ist nicht implementiert`,
priority: control.priority,
})
// Score: 0 for not implemented
} else if (tom.implementationStatus === 'PARTIAL') {
partialControls.push({
controlId: tom.controlId,
missingAspects: tom.evidenceGaps,
})
// Score: 50% for partial
totalScore += weight * 0.5
} else {
// Fully implemented
totalScore += weight
}
// Check evidence
const linkedEvidenceIds = tom.linkedEvidence
const requiredEvidence = control.evidenceRequirements
const providedEvidence = documents.filter((doc) =>
linkedEvidenceIds.includes(doc.id)
)
if (providedEvidence.length < requiredEvidence.length) {
const missing = requiredEvidence.filter(
(req) =>
!providedEvidence.some(
(doc) =>
doc.documentType === 'POLICY' ||
doc.documentType === 'CERTIFICATE' ||
doc.originalName.toLowerCase().includes(req.toLowerCase())
)
)
if (missing.length > 0) {
missingEvidence.push({
controlId: tom.controlId,
requiredEvidence: missing,
})
}
}
}
// Calculate overall score as percentage
const overallScore =
totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0
// Generate recommendations
if (missingControls.length > 0) {
const criticalMissing = missingControls.filter(
(mc) => mc.priority === 'CRITICAL'
)
if (criticalMissing.length > 0) {
recommendations.push(
`${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.`
)
}
}
if (partialControls.length > 0) {
recommendations.push(
`${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollständigen Sie die Implementierung.`
)
}
if (missingEvidence.length > 0) {
recommendations.push(
`Für ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.`
)
}
if (overallScore >= 80) {
recommendations.push(
'Ihr TOM-Compliance-Score ist gut. Führen Sie regelmäßige Überprüfungen durch.'
)
} else if (overallScore >= 50) {
recommendations.push(
'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Lücken.'
)
} else {
recommendations.push(
'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Überarbeitung der Maßnahmen wird empfohlen.'
)
}
return {
overallScore,
missingControls,
partialControls,
missingEvidence,
recommendations,
generatedAt: new Date(),
}
}
/**
* Get controls by applicability level
*/
getControlsByApplicability(
context: RulesEngineEvaluationContext,
applicability: ControlApplicability
): ControlLibraryEntry[] {
const results = this.evaluateControls(context)
return results
.filter((r) => r.applicability === applicability)
.map((r) => getControlById(r.controlId))
.filter((c): c is ControlLibraryEntry => c !== undefined)
}
/**
* Get summary statistics for the evaluation
*/
getSummaryStatistics(context: RulesEngineEvaluationContext): {
total: number
required: number
recommended: number
optional: number
notApplicable: number
byCategory: Map<string, { required: number; recommended: number }>
} {
const results = this.evaluateControls(context)
const stats = {
total: results.length,
required: 0,
recommended: 0,
optional: 0,
notApplicable: 0,
byCategory: new Map<string, { required: number; recommended: number }>(),
}
for (const result of results) {
switch (result.applicability) {
case 'REQUIRED':
stats.required++
break
case 'RECOMMENDED':
stats.recommended++
break
case 'OPTIONAL':
stats.optional++
break
case 'NOT_APPLICABLE':
stats.notApplicable++
break
}
// Count by category
const control = getControlById(result.controlId)
if (control) {
const category = control.category
const existing = stats.byCategory.get(category) || {
required: 0,
recommended: 0,
}
if (result.applicability === 'REQUIRED') {
existing.required++
} else if (result.applicability === 'RECOMMENDED') {
existing.recommended++
}
stats.byCategory.set(category, existing)
}
}
return stats
}
/**
* Check if a specific control is applicable
*/
isControlApplicable(
controlId: string,
context: RulesEngineEvaluationContext
): boolean {
const control = getControlById(controlId)
if (!control) return false
const result = this.evaluateControl(control, context)
return (
result.applicability === 'REQUIRED' ||
result.applicability === 'RECOMMENDED'
)
}
/**
* Get all controls that match a specific tag
*/
getControlsByTagWithApplicability(
tag: string,
context: RulesEngineEvaluationContext
): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> {
return this.controls
.filter((control) => control.tags.includes(tag))
.map((control) => ({
control,
result: this.evaluateControl(control, context),
}))
}
/**
* Reload controls (useful if the control library is updated)
*/
reloadControls(): void {
this.controls = getAllControls()
}
}
// =============================================================================
// SINGLETON INSTANCE
// =============================================================================
let rulesEngineInstance: TOMRulesEngine | null = null
export function getTOMRulesEngine(): TOMRulesEngine {
if (!rulesEngineInstance) {
rulesEngineInstance = new TOMRulesEngine()
}
return rulesEngineInstance
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Quick evaluation of controls for a context
*/
export function evaluateControlsForContext(
context: RulesEngineEvaluationContext
): RulesEngineResult[] {
return getTOMRulesEngine().evaluateControls(context)
}
/**
* Quick derivation of TOMs for a context
*/
export function deriveTOMsForContext(
context: RulesEngineEvaluationContext
): DerivedTOM[] {
return getTOMRulesEngine().deriveAllTOMs(context)
}
/**
* Quick gap analysis
*/
export function performQuickGapAnalysis(
derivedTOMs: DerivedTOM[],
documents: EvidenceDocument[]
): GapAnalysisResult {
return getTOMRulesEngine().performGapAnalysis(derivedTOMs, documents)
}

View File

@@ -0,0 +1,901 @@
// =============================================================================
// TOM Generator Module - TypeScript Types
// DSGVO Art. 32 Technical and Organizational Measures
// =============================================================================
// =============================================================================
// ENUMS & LITERAL TYPES
// =============================================================================
export type TOMGeneratorStepId =
| 'scope-roles'
| 'data-categories'
| 'architecture-hosting'
| 'security-profile'
| 'risk-protection'
| 'review-export'
export type CompanyRole = 'CONTROLLER' | 'PROCESSOR' | 'JOINT_CONTROLLER'
export type DataCategory =
| 'IDENTIFICATION'
| 'CONTACT'
| 'FINANCIAL'
| 'PROFESSIONAL'
| 'LOCATION'
| 'BEHAVIORAL'
| 'BIOMETRIC'
| 'HEALTH'
| 'GENETIC'
| 'POLITICAL'
| 'RELIGIOUS'
| 'SEXUAL_ORIENTATION'
| 'CRIMINAL'
export type DataSubject =
| 'EMPLOYEES'
| 'CUSTOMERS'
| 'PROSPECTS'
| 'SUPPLIERS'
| 'MINORS'
| 'PATIENTS'
| 'STUDENTS'
| 'GENERAL_PUBLIC'
export type HostingLocation =
| 'DE'
| 'EU'
| 'EEA'
| 'THIRD_COUNTRY_ADEQUATE'
| 'THIRD_COUNTRY'
export type HostingModel = 'ON_PREMISE' | 'PRIVATE_CLOUD' | 'PUBLIC_CLOUD' | 'HYBRID'
export type MultiTenancy = 'SINGLE_TENANT' | 'MULTI_TENANT' | 'DEDICATED'
export type ControlApplicability =
| 'REQUIRED'
| 'RECOMMENDED'
| 'OPTIONAL'
| 'NOT_APPLICABLE'
export type DocumentType =
| 'AVV'
| 'DPA'
| 'SLA'
| 'NDA'
| 'POLICY'
| 'CERTIFICATE'
| 'AUDIT_REPORT'
| 'OTHER'
export type ProtectionLevel = 'NORMAL' | 'HIGH' | 'VERY_HIGH'
export type CIARating = 1 | 2 | 3 | 4 | 5
export type ControlCategory =
| 'ACCESS_CONTROL'
| 'ADMISSION_CONTROL'
| 'ACCESS_AUTHORIZATION'
| 'TRANSFER_CONTROL'
| 'INPUT_CONTROL'
| 'ORDER_CONTROL'
| 'AVAILABILITY'
| 'SEPARATION'
| 'ENCRYPTION'
| 'PSEUDONYMIZATION'
| 'RESILIENCE'
| 'RECOVERY'
| 'REVIEW'
export type CompanySize = 'MICRO' | 'SMALL' | 'MEDIUM' | 'LARGE' | 'ENTERPRISE'
export type DataVolume = 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
export type AuthMethodType =
| 'PASSWORD'
| 'MFA'
| 'SSO'
| 'CERTIFICATE'
| 'BIOMETRIC'
export type BackupFrequency = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY'
export type ReviewFrequency = 'MONTHLY' | 'QUARTERLY' | 'SEMI_ANNUAL' | 'ANNUAL'
export type ControlPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
export type ControlComplexity = 'LOW' | 'MEDIUM' | 'HIGH'
export type ImplementationStatus = 'NOT_IMPLEMENTED' | 'PARTIAL' | 'IMPLEMENTED'
export type EvidenceStatus = 'PENDING' | 'ANALYZED' | 'VERIFIED' | 'REJECTED'
export type ConditionOperator =
| 'EQUALS'
| 'NOT_EQUALS'
| 'CONTAINS'
| 'GREATER_THAN'
| 'IN'
// =============================================================================
// PROFILE INTERFACES (Wizard Steps 1-5)
// =============================================================================
export interface CompanyProfile {
id: string
name: string
industry: string
size: CompanySize
role: CompanyRole
products: string[]
dpoPerson: string | null
dpoEmail: string | null
itSecurityContact: string | null
}
export interface DataProfile {
categories: DataCategory[]
subjects: DataSubject[]
hasSpecialCategories: boolean
processesMinors: boolean
dataVolume: DataVolume
thirdCountryTransfers: boolean
thirdCountryList: string[]
}
export interface CloudProvider {
name: string
location: HostingLocation
certifications: string[]
}
export interface ArchitectureProfile {
hostingModel: HostingModel
hostingLocation: HostingLocation
providers: CloudProvider[]
multiTenancy: MultiTenancy
hasSubprocessors: boolean
subprocessorCount: number
encryptionAtRest: boolean
encryptionInTransit: boolean
}
export interface AuthMethod {
type: AuthMethodType
provider: string | null
}
export interface SecurityProfile {
authMethods: AuthMethod[]
hasMFA: boolean
hasSSO: boolean
hasIAM: boolean
hasPAM: boolean
hasEncryptionAtRest: boolean
hasEncryptionInTransit: boolean
hasLogging: boolean
logRetentionDays: number
hasBackup: boolean
backupFrequency: BackupFrequency
backupRetentionDays: number
hasDRPlan: boolean
rtoHours: number | null
rpoHours: number | null
hasVulnerabilityManagement: boolean
hasPenetrationTests: boolean
hasSecurityTraining: boolean
}
export interface CIAAssessment {
confidentiality: CIARating
integrity: CIARating
availability: CIARating
justification: string
}
export interface RiskProfile {
ciaAssessment: CIAAssessment
protectionLevel: ProtectionLevel
specialRisks: string[]
regulatoryRequirements: string[]
hasHighRiskProcessing: boolean
dsfaRequired: boolean
}
// =============================================================================
// EVIDENCE DOCUMENT
// =============================================================================
export interface ExtractedClause {
id: string
text: string
type: string
relatedControlId: string | null
}
export interface AIDocumentAnalysis {
summary: string
extractedClauses: ExtractedClause[]
applicableControls: string[]
gaps: string[]
confidence: number
analyzedAt: Date
}
export interface EvidenceDocument {
id: string
filename: string
originalName: string
mimeType: string
size: number
uploadedAt: Date
uploadedBy: string
documentType: DocumentType
detectedType: DocumentType | null
hash: string
validFrom: Date | null
validUntil: Date | null
linkedControlIds: string[]
aiAnalysis: AIDocumentAnalysis | null
status: EvidenceStatus
}
// =============================================================================
// CONTROL LIBRARY
// =============================================================================
export interface LocalizedString {
de: string
en: string
}
export interface FrameworkMapping {
framework: string
reference: string
}
export interface ApplicabilityCondition {
field: string
operator: ConditionOperator
value: unknown
result: ControlApplicability
priority: number
}
export interface ControlLibraryEntry {
id: string
code: string
category: ControlCategory
type: 'TECHNICAL' | 'ORGANIZATIONAL'
name: LocalizedString
description: LocalizedString
mappings: FrameworkMapping[]
applicabilityConditions: ApplicabilityCondition[]
defaultApplicability: ControlApplicability
evidenceRequirements: string[]
reviewFrequency: ReviewFrequency
priority: ControlPriority
complexity: ControlComplexity
tags: string[]
}
// =============================================================================
// DERIVED TOM
// =============================================================================
export interface DerivedTOM {
id: string
controlId: string
name: string
description: string
applicability: ControlApplicability
applicabilityReason: string
implementationStatus: ImplementationStatus
responsiblePerson: string | null
responsibleDepartment: string | null
implementationDate: Date | null
reviewDate: Date | null
linkedEvidence: string[]
evidenceGaps: string[]
aiGeneratedDescription: string | null
aiRecommendations: string[]
}
// =============================================================================
// GAP ANALYSIS
// =============================================================================
export interface MissingControl {
controlId: string
reason: string
priority: string
}
export interface PartialControl {
controlId: string
missingAspects: string[]
}
export interface MissingEvidence {
controlId: string
requiredEvidence: string[]
}
export interface GapAnalysisResult {
overallScore: number
missingControls: MissingControl[]
partialControls: PartialControl[]
missingEvidence: MissingEvidence[]
recommendations: string[]
generatedAt: Date
}
// =============================================================================
// WIZARD STEP
// =============================================================================
export interface WizardStep {
id: TOMGeneratorStepId
completed: boolean
data: unknown
validatedAt: Date | null
}
// =============================================================================
// EXPORT RECORD
// =============================================================================
export interface ExportRecord {
id: string
format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
generatedAt: Date
filename: string
}
// =============================================================================
// TOM GENERATOR STATE
// =============================================================================
export interface TOMGeneratorState {
id: string
tenantId: string
companyProfile: CompanyProfile | null
dataProfile: DataProfile | null
architectureProfile: ArchitectureProfile | null
securityProfile: SecurityProfile | null
riskProfile: RiskProfile | null
currentStep: TOMGeneratorStepId
steps: WizardStep[]
documents: EvidenceDocument[]
derivedTOMs: DerivedTOM[]
gapAnalysis: GapAnalysisResult | null
exports: ExportRecord[]
createdAt: Date
updatedAt: Date
}
// =============================================================================
// RULES ENGINE TYPES
// =============================================================================
export interface RulesEngineResult {
controlId: string
applicability: ControlApplicability
reason: string
matchedCondition?: ApplicabilityCondition
}
export interface RulesEngineEvaluationContext {
companyProfile: CompanyProfile | null
dataProfile: DataProfile | null
architectureProfile: ArchitectureProfile | null
securityProfile: SecurityProfile | null
riskProfile: RiskProfile | null
}
// =============================================================================
// API TYPES
// =============================================================================
export interface TOMGeneratorStateRequest {
tenantId: string
}
export interface TOMGeneratorStateResponse {
success: boolean
state: TOMGeneratorState | null
error?: string
}
export interface ControlsEvaluationRequest {
tenantId: string
context: RulesEngineEvaluationContext
}
export interface ControlsEvaluationResponse {
success: boolean
results: RulesEngineResult[]
evaluatedAt: string
}
export interface EvidenceUploadRequest {
tenantId: string
documentType: DocumentType
validFrom?: string
validUntil?: string
}
export interface EvidenceUploadResponse {
success: boolean
document: EvidenceDocument | null
error?: string
}
export interface EvidenceAnalyzeRequest {
documentId: string
tenantId: string
}
export interface EvidenceAnalyzeResponse {
success: boolean
analysis: AIDocumentAnalysis | null
error?: string
}
export interface ExportRequest {
tenantId: string
format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
language: 'de' | 'en'
}
export interface ExportResponse {
success: boolean
exportId: string
filename: string
downloadUrl?: string
error?: string
}
export interface GapAnalysisRequest {
tenantId: string
}
export interface GapAnalysisResponse {
success: boolean
result: GapAnalysisResult | null
error?: string
}
// =============================================================================
// STEP CONFIGURATION
// =============================================================================
export interface StepConfig {
id: TOMGeneratorStepId
title: LocalizedString
description: LocalizedString
checkpointId: string
path: string
/** Alias for path (for convenience) */
url: string
/** German title for display (for convenience) */
name: string
}
export const TOM_GENERATOR_STEPS: StepConfig[] = [
{
id: 'scope-roles',
title: { de: 'Scope & Rollen', en: 'Scope & Roles' },
description: {
de: 'Unternehmensname, Branche, Größe und Rolle definieren',
en: 'Define company name, industry, size and role',
},
checkpointId: 'CP-TOM-SCOPE',
path: '/sdk/tom-generator/scope',
url: '/sdk/tom-generator/scope',
name: 'Scope & Rollen',
},
{
id: 'data-categories',
title: { de: 'Datenkategorien', en: 'Data Categories' },
description: {
de: 'Datenkategorien und betroffene Personen erfassen',
en: 'Capture data categories and data subjects',
},
checkpointId: 'CP-TOM-DATA',
path: '/sdk/tom-generator/data',
url: '/sdk/tom-generator/data',
name: 'Datenkategorien',
},
{
id: 'architecture-hosting',
title: { de: 'Architektur & Hosting', en: 'Architecture & Hosting' },
description: {
de: 'Hosting-Modell, Standort und Provider definieren',
en: 'Define hosting model, location and providers',
},
checkpointId: 'CP-TOM-ARCH',
path: '/sdk/tom-generator/architecture',
url: '/sdk/tom-generator/architecture',
name: 'Architektur & Hosting',
},
{
id: 'security-profile',
title: { de: 'Security-Profil', en: 'Security Profile' },
description: {
de: 'Authentifizierung, Verschlüsselung und Backup konfigurieren',
en: 'Configure authentication, encryption and backup',
},
checkpointId: 'CP-TOM-SEC',
path: '/sdk/tom-generator/security',
url: '/sdk/tom-generator/security',
name: 'Security-Profil',
},
{
id: 'risk-protection',
title: { de: 'Risiko & Schutzbedarf', en: 'Risk & Protection Level' },
description: {
de: 'CIA-Bewertung und Schutzbedarf ermitteln',
en: 'Determine CIA assessment and protection level',
},
checkpointId: 'CP-TOM-RISK',
path: '/sdk/tom-generator/risk',
url: '/sdk/tom-generator/risk',
name: 'Risiko & Schutzbedarf',
},
{
id: 'review-export',
title: { de: 'Review & Export', en: 'Review & Export' },
description: {
de: 'Zusammenfassung prüfen und TOMs exportieren',
en: 'Review summary and export TOMs',
},
checkpointId: 'CP-TOM-REVIEW',
path: '/sdk/tom-generator/review',
url: '/sdk/tom-generator/review',
name: 'Review & Export',
},
]
// =============================================================================
// CATEGORY METADATA
// =============================================================================
export interface CategoryMetadata {
id: ControlCategory
name: LocalizedString
gdprReference: string
icon?: string
}
export const CONTROL_CATEGORIES: CategoryMetadata[] = [
{
id: 'ACCESS_CONTROL',
name: { de: 'Zutrittskontrolle', en: 'Physical Access Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ADMISSION_CONTROL',
name: { de: 'Zugangskontrolle', en: 'System Access Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ACCESS_AUTHORIZATION',
name: { de: 'Zugriffskontrolle', en: 'Access Authorization' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'TRANSFER_CONTROL',
name: { de: 'Weitergabekontrolle', en: 'Transfer Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'INPUT_CONTROL',
name: { de: 'Eingabekontrolle', en: 'Input Control' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ORDER_CONTROL',
name: { de: 'Auftragskontrolle', en: 'Order Control' },
gdprReference: 'Art. 28',
},
{
id: 'AVAILABILITY',
name: { de: 'Verfügbarkeit', en: 'Availability' },
gdprReference: 'Art. 32 Abs. 1 lit. b, c',
},
{
id: 'SEPARATION',
name: { de: 'Trennbarkeit', en: 'Separation' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'ENCRYPTION',
name: { de: 'Verschlüsselung', en: 'Encryption' },
gdprReference: 'Art. 32 Abs. 1 lit. a',
},
{
id: 'PSEUDONYMIZATION',
name: { de: 'Pseudonymisierung', en: 'Pseudonymization' },
gdprReference: 'Art. 32 Abs. 1 lit. a',
},
{
id: 'RESILIENCE',
name: { de: 'Belastbarkeit', en: 'Resilience' },
gdprReference: 'Art. 32 Abs. 1 lit. b',
},
{
id: 'RECOVERY',
name: { de: 'Wiederherstellbarkeit', en: 'Recovery' },
gdprReference: 'Art. 32 Abs. 1 lit. c',
},
{
id: 'REVIEW',
name: { de: 'Überprüfung & Bewertung', en: 'Review & Assessment' },
gdprReference: 'Art. 32 Abs. 1 lit. d',
},
]
// =============================================================================
// DATA CATEGORY METADATA
// =============================================================================
export interface DataCategoryMetadata {
id: DataCategory
name: LocalizedString
isSpecialCategory: boolean
gdprReference?: string
}
export const DATA_CATEGORIES_METADATA: DataCategoryMetadata[] = [
{
id: 'IDENTIFICATION',
name: { de: 'Identifikationsdaten', en: 'Identification Data' },
isSpecialCategory: false,
},
{
id: 'CONTACT',
name: { de: 'Kontaktdaten', en: 'Contact Data' },
isSpecialCategory: false,
},
{
id: 'FINANCIAL',
name: { de: 'Finanzdaten', en: 'Financial Data' },
isSpecialCategory: false,
},
{
id: 'PROFESSIONAL',
name: { de: 'Berufliche Daten', en: 'Professional Data' },
isSpecialCategory: false,
},
{
id: 'LOCATION',
name: { de: 'Standortdaten', en: 'Location Data' },
isSpecialCategory: false,
},
{
id: 'BEHAVIORAL',
name: { de: 'Verhaltensdaten', en: 'Behavioral Data' },
isSpecialCategory: false,
},
{
id: 'BIOMETRIC',
name: { de: 'Biometrische Daten', en: 'Biometric Data' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'HEALTH',
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'GENETIC',
name: { de: 'Genetische Daten', en: 'Genetic Data' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'POLITICAL',
name: { de: 'Politische Meinungen', en: 'Political Opinions' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'RELIGIOUS',
name: { de: 'Religiöse Überzeugungen', en: 'Religious Beliefs' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'SEXUAL_ORIENTATION',
name: { de: 'Sexuelle Orientierung', en: 'Sexual Orientation' },
isSpecialCategory: true,
gdprReference: 'Art. 9 Abs. 1',
},
{
id: 'CRIMINAL',
name: { de: 'Strafrechtliche Daten', en: 'Criminal Data' },
isSpecialCategory: true,
gdprReference: 'Art. 10',
},
]
// =============================================================================
// DATA SUBJECT METADATA
// =============================================================================
export interface DataSubjectMetadata {
id: DataSubject
name: LocalizedString
isVulnerable: boolean
}
export const DATA_SUBJECTS_METADATA: DataSubjectMetadata[] = [
{
id: 'EMPLOYEES',
name: { de: 'Mitarbeiter', en: 'Employees' },
isVulnerable: false,
},
{
id: 'CUSTOMERS',
name: { de: 'Kunden', en: 'Customers' },
isVulnerable: false,
},
{
id: 'PROSPECTS',
name: { de: 'Interessenten', en: 'Prospects' },
isVulnerable: false,
},
{
id: 'SUPPLIERS',
name: { de: 'Lieferanten', en: 'Suppliers' },
isVulnerable: false,
},
{
id: 'MINORS',
name: { de: 'Minderjährige', en: 'Minors' },
isVulnerable: true,
},
{
id: 'PATIENTS',
name: { de: 'Patienten', en: 'Patients' },
isVulnerable: true,
},
{
id: 'STUDENTS',
name: { de: 'Schüler/Studenten', en: 'Students' },
isVulnerable: false,
},
{
id: 'GENERAL_PUBLIC',
name: { de: 'Allgemeine Öffentlichkeit', en: 'General Public' },
isVulnerable: false,
},
]
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
export function getStepByIndex(index: number): StepConfig | undefined {
return TOM_GENERATOR_STEPS[index]
}
export function getStepById(id: TOMGeneratorStepId): StepConfig | undefined {
return TOM_GENERATOR_STEPS.find((step) => step.id === id)
}
export function getStepIndex(id: TOMGeneratorStepId): number {
return TOM_GENERATOR_STEPS.findIndex((step) => step.id === id)
}
export function getNextStep(
currentId: TOMGeneratorStepId
): StepConfig | undefined {
const currentIndex = getStepIndex(currentId)
return TOM_GENERATOR_STEPS[currentIndex + 1]
}
export function getPreviousStep(
currentId: TOMGeneratorStepId
): StepConfig | undefined {
const currentIndex = getStepIndex(currentId)
return currentIndex > 0 ? TOM_GENERATOR_STEPS[currentIndex - 1] : undefined
}
export function isSpecialCategory(category: DataCategory): boolean {
const meta = DATA_CATEGORIES_METADATA.find((c) => c.id === category)
return meta?.isSpecialCategory ?? false
}
export function hasSpecialCategories(categories: DataCategory[]): boolean {
return categories.some(isSpecialCategory)
}
export function isVulnerableSubject(subject: DataSubject): boolean {
const meta = DATA_SUBJECTS_METADATA.find((s) => s.id === subject)
return meta?.isVulnerable ?? false
}
export function hasVulnerableSubjects(subjects: DataSubject[]): boolean {
return subjects.some(isVulnerableSubject)
}
export function calculateProtectionLevel(
ciaAssessment: CIAAssessment
): ProtectionLevel {
const maxRating = Math.max(
ciaAssessment.confidentiality,
ciaAssessment.integrity,
ciaAssessment.availability
)
if (maxRating >= 4) return 'VERY_HIGH'
if (maxRating >= 3) return 'HIGH'
return 'NORMAL'
}
export function isDSFARequired(
dataProfile: DataProfile | null,
riskProfile: RiskProfile | null
): boolean {
if (!dataProfile) return false
// DSFA required if:
// 1. Special categories are processed
if (dataProfile.hasSpecialCategories) return true
// 2. Minors data is processed
if (dataProfile.processesMinors) return true
// 3. Large scale processing
if (dataProfile.dataVolume === 'VERY_HIGH') return true
// 4. High risk processing indicated
if (riskProfile?.hasHighRiskProcessing) return true
// 5. Very high protection level
if (riskProfile?.protectionLevel === 'VERY_HIGH') return true
return false
}
// =============================================================================
// INITIAL STATE FACTORY
// =============================================================================
export function createInitialTOMGeneratorState(
tenantId: string
): TOMGeneratorState {
const now = new Date()
return {
id: crypto.randomUUID(),
tenantId,
companyProfile: null,
dataProfile: null,
architectureProfile: null,
securityProfile: null,
riskProfile: null,
currentStep: 'scope-roles',
steps: TOM_GENERATOR_STEPS.map((step) => ({
id: step.id,
completed: false,
data: null,
validatedAt: null,
})),
documents: [],
derivedTOMs: [],
gapAnalysis: null,
exports: [],
createdAt: now,
updatedAt: now,
}
}
/**
* Alias for createInitialTOMGeneratorState (for API compatibility)
*/
export const createEmptyTOMGeneratorState = createInitialTOMGeneratorState