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:
414
admin-v2/lib/sdk/tom-generator/ai/document-analyzer.ts
Normal file
414
admin-v2/lib/sdk/tom-generator/ai/document-analyzer.ts
Normal 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
|
||||
}
|
||||
427
admin-v2/lib/sdk/tom-generator/ai/prompts.ts
Normal file
427
admin-v2/lib/sdk/tom-generator/ai/prompts.ts
Normal 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,
|
||||
}
|
||||
698
admin-v2/lib/sdk/tom-generator/context.tsx
Normal file
698
admin-v2/lib/sdk/tom-generator/context.tsx
Normal 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 }
|
||||
1848
admin-v2/lib/sdk/tom-generator/controls/controls.yml
Normal file
1848
admin-v2/lib/sdk/tom-generator/controls/controls.yml
Normal file
File diff suppressed because it is too large
Load Diff
2240
admin-v2/lib/sdk/tom-generator/controls/loader.ts
Normal file
2240
admin-v2/lib/sdk/tom-generator/controls/loader.ts
Normal file
File diff suppressed because it is too large
Load Diff
518
admin-v2/lib/sdk/tom-generator/demo-data/index.ts
Normal file
518
admin-v2/lib/sdk/tom-generator/demo-data/index.ts
Normal 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,
|
||||
}
|
||||
67
admin-v2/lib/sdk/tom-generator/evidence-store.ts
Normal file
67
admin-v2/lib/sdk/tom-generator/evidence-store.ts
Normal 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()
|
||||
525
admin-v2/lib/sdk/tom-generator/export/docx.ts
Normal file
525
admin-v2/lib/sdk/tom-generator/export/docx.ts
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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
|
||||
517
admin-v2/lib/sdk/tom-generator/export/pdf.ts
Normal file
517
admin-v2/lib/sdk/tom-generator/export/pdf.ts
Normal 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
|
||||
544
admin-v2/lib/sdk/tom-generator/export/zip.ts
Normal file
544
admin-v2/lib/sdk/tom-generator/export/zip.ts
Normal 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
|
||||
206
admin-v2/lib/sdk/tom-generator/index.ts
Normal file
206
admin-v2/lib/sdk/tom-generator/index.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
560
admin-v2/lib/sdk/tom-generator/rules-engine.ts
Normal file
560
admin-v2/lib/sdk/tom-generator/rules-engine.ts
Normal 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)
|
||||
}
|
||||
901
admin-v2/lib/sdk/tom-generator/types.ts
Normal file
901
admin-v2/lib/sdk/tom-generator/types.ts
Normal 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
|
||||
Reference in New Issue
Block a user