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:
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
|
||||
Reference in New Issue
Block a user