// ============================================================================= // 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 = {} ): 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 = { 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 = { 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 { const grouped = new Map() 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 { 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 { const grouped = new Map() 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> = { 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 = {} ): Promise { 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