Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
545 lines
17 KiB
TypeScript
545 lines
17 KiB
TypeScript
// =============================================================================
|
|
// 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
|