merge: sync with origin/main, take upstream on conflicts
# Conflicts: # admin-compliance/lib/sdk/types.ts # admin-compliance/lib/sdk/vendor-compliance/types.ts
This commit is contained in:
510
admin-compliance/lib/sdk/__tests__/vvt-scope-integration.test.ts
Normal file
510
admin-compliance/lib/sdk/__tests__/vvt-scope-integration.test.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Integration Tests: Company Profile → Compliance Scope → VVT Generator
|
||||
*
|
||||
* Tests the complete data pipeline from Company Profile master data
|
||||
* through the Compliance Scope Engine to VVT activity generation.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
prefillFromCompanyProfile,
|
||||
exportToVVTAnswers,
|
||||
getAutoFilledScoringAnswers,
|
||||
SCOPE_QUESTION_BLOCKS,
|
||||
} from '../compliance-scope-profiling'
|
||||
import {
|
||||
generateActivities,
|
||||
PROFILING_QUESTIONS,
|
||||
DEPARTMENT_DATA_CATEGORIES,
|
||||
SCOPE_PREFILLED_VVT_QUESTIONS,
|
||||
} from '../vvt-profiling'
|
||||
import type { ScopeProfilingAnswer } from '../compliance-scope-types'
|
||||
|
||||
// Helper
|
||||
function ans(questionId: string, value: unknown): ScopeProfilingAnswer {
|
||||
return { questionId, value } as ScopeProfilingAnswer
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 1. Company Profile → Scope Prefill
|
||||
// =============================================================================
|
||||
|
||||
describe('CompanyProfile → Scope prefill', () => {
|
||||
it('prefills org_has_dsb when dpoName is set', () => {
|
||||
const profile = { dpoName: 'Max Mustermann' } as any
|
||||
const answers = prefillFromCompanyProfile(profile)
|
||||
expect(answers.find((a) => a.questionId === 'org_has_dsb')?.value).toBe(true)
|
||||
})
|
||||
|
||||
it('does NOT prefill org_has_dsb when dpoName is empty', () => {
|
||||
const profile = { dpoName: '' } as any
|
||||
const answers = prefillFromCompanyProfile(profile)
|
||||
expect(answers.find((a) => a.questionId === 'org_has_dsb')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('maps offerings to prod_type correctly', () => {
|
||||
const profile = { offerings: ['WebApp', 'SaaS', 'API'] } as any
|
||||
const answers = prefillFromCompanyProfile(profile)
|
||||
const prodType = answers.find((a) => a.questionId === 'prod_type')
|
||||
expect(prodType?.value).toEqual(expect.arrayContaining(['webapp', 'saas', 'api']))
|
||||
})
|
||||
|
||||
it('detects webshop in offerings', () => {
|
||||
const profile = { offerings: ['Webshop'] } as any
|
||||
const answers = prefillFromCompanyProfile(profile)
|
||||
expect(answers.find((a) => a.questionId === 'prod_webshop')?.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns empty array when profile has no relevant data', () => {
|
||||
const profile = {} as any
|
||||
const answers = prefillFromCompanyProfile(profile)
|
||||
expect(answers).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('CompanyProfile → Scope scoring answers', () => {
|
||||
it('maps employeeCount to org_employee_count', () => {
|
||||
const profile = { employeeCount: '50-249' } as any
|
||||
const answers = getAutoFilledScoringAnswers(profile)
|
||||
expect(answers.find((a) => a.questionId === 'org_employee_count')?.value).toBe('50-249')
|
||||
})
|
||||
|
||||
it('maps industry to org_industry', () => {
|
||||
const profile = { industry: ['IT & Software', 'Finanzdienstleistungen'] } as any
|
||||
const answers = getAutoFilledScoringAnswers(profile)
|
||||
expect(answers.find((a) => a.questionId === 'org_industry')?.value).toBe(
|
||||
'IT & Software, Finanzdienstleistungen'
|
||||
)
|
||||
})
|
||||
|
||||
it('maps annualRevenue to org_annual_revenue', () => {
|
||||
const profile = { annualRevenue: '1-10M' } as any
|
||||
const answers = getAutoFilledScoringAnswers(profile)
|
||||
expect(answers.find((a) => a.questionId === 'org_annual_revenue')?.value).toBe('1-10M')
|
||||
})
|
||||
|
||||
it('maps businessModel to org_business_model', () => {
|
||||
const profile = { businessModel: 'B2B' } as any
|
||||
const answers = getAutoFilledScoringAnswers(profile)
|
||||
expect(answers.find((a) => a.questionId === 'org_business_model')?.value).toBe('B2B')
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 2. Scope → VVT Answer Mapping (exportToVVTAnswers)
|
||||
// =============================================================================
|
||||
|
||||
describe('Scope → VVT answer export', () => {
|
||||
it('maps scope questions with mapsToVVTQuestion property', () => {
|
||||
// Block 9: dk_dept_hr maps to dept_hr_categories
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [
|
||||
ans('dk_dept_hr', ['NAME', 'SALARY_DATA', 'HEALTH_DATA']),
|
||||
]
|
||||
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
|
||||
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'SALARY_DATA', 'HEALTH_DATA'])
|
||||
})
|
||||
|
||||
it('maps multiple department data categories', () => {
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [
|
||||
ans('dk_dept_hr', ['NAME', 'BANK_ACCOUNT']),
|
||||
ans('dk_dept_finance', ['INVOICE_DATA', 'TAX_ID']),
|
||||
ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA']),
|
||||
]
|
||||
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
|
||||
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'BANK_ACCOUNT'])
|
||||
expect(vvtAnswers.dept_finance_categories).toEqual(['INVOICE_DATA', 'TAX_ID'])
|
||||
expect(vvtAnswers.dept_marketing_categories).toEqual(['EMAIL', 'TRACKING_DATA'])
|
||||
})
|
||||
|
||||
it('ignores scope questions without mapsToVVTQuestion', () => {
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [
|
||||
ans('vvt_has_vvt', true), // No mapsToVVTQuestion property
|
||||
]
|
||||
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
|
||||
expect(Object.keys(vvtAnswers)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles empty scope answers', () => {
|
||||
const vvtAnswers = exportToVVTAnswers([])
|
||||
expect(vvtAnswers).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 3. Scope → VVT Profiling Prefill
|
||||
// Note: prefillFromScopeAnswers() uses dynamic require('./compliance-scope-profiling')
|
||||
// which doesn't resolve in vitest. We test the same pipeline by calling
|
||||
// exportToVVTAnswers() directly (which is what prefillFromScopeAnswers wraps).
|
||||
// =============================================================================
|
||||
|
||||
describe('Scope → VVT Profiling Prefill (via exportToVVTAnswers)', () => {
|
||||
it('converts scope answers to VVT ProfilingAnswers format', () => {
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [
|
||||
ans('dk_dept_hr', ['NAME', 'HEALTH_DATA']),
|
||||
ans('dk_dept_finance', ['BANK_ACCOUNT']),
|
||||
]
|
||||
const exported = exportToVVTAnswers(scopeAnswers)
|
||||
// Same transformation as prefillFromScopeAnswers
|
||||
const profiling: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(exported)) {
|
||||
if (value !== undefined && value !== null) profiling[key] = value
|
||||
}
|
||||
expect(profiling.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA'])
|
||||
expect(profiling.dept_finance_categories).toEqual(['BANK_ACCOUNT'])
|
||||
})
|
||||
|
||||
it('filters out null/undefined values', () => {
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', null)]
|
||||
const exported = exportToVVTAnswers(scopeAnswers)
|
||||
const profiling: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(exported)) {
|
||||
if (value !== undefined && value !== null) profiling[key] = value
|
||||
}
|
||||
expect(profiling.dept_hr_categories).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 4. VVT Generator — generateActivities
|
||||
// =============================================================================
|
||||
|
||||
describe('generateActivities', () => {
|
||||
it('always generates 4 IT baseline activities', () => {
|
||||
const result = generateActivities({})
|
||||
const names = result.generatedActivities.map((a) => a.name)
|
||||
expect(result.generatedActivities.length).toBeGreaterThanOrEqual(4)
|
||||
// IT baselines are always added
|
||||
const itTemplates = result.generatedActivities.filter(
|
||||
(a) => a.businessFunction === 'it_operations'
|
||||
)
|
||||
expect(itTemplates.length).toBeGreaterThanOrEqual(4)
|
||||
})
|
||||
|
||||
it('triggers HR templates when dept_hr=true', () => {
|
||||
const result = generateActivities({ dept_hr: true })
|
||||
const hrActivities = result.generatedActivities.filter(
|
||||
(a) => a.businessFunction === 'hr'
|
||||
)
|
||||
expect(hrActivities.length).toBeGreaterThanOrEqual(3) // mitarbeiter, gehalt, zeiterfassung
|
||||
})
|
||||
|
||||
it('triggers finance templates when dept_finance=true', () => {
|
||||
const result = generateActivities({ dept_finance: true })
|
||||
const financeActivities = result.generatedActivities.filter(
|
||||
(a) => a.businessFunction === 'finance'
|
||||
)
|
||||
expect(financeActivities.length).toBeGreaterThanOrEqual(2) // buchhaltung, zahlungsverkehr
|
||||
})
|
||||
|
||||
it('enriches activities with US cloud third-country transfer', () => {
|
||||
const result = generateActivities({ dept_hr: true, transfer_cloud_us: true })
|
||||
// Every activity should have a US third-country transfer
|
||||
for (const activity of result.generatedActivities) {
|
||||
expect(activity.thirdCountryTransfers.some((t) => t.country === 'US')).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('adds HEALTH_DATA to HR activities when data_health=true', () => {
|
||||
const result = generateActivities({ dept_hr: true, data_health: true })
|
||||
const hrActivities = result.generatedActivities.filter(
|
||||
(a) => a.businessFunction === 'hr'
|
||||
)
|
||||
expect(hrActivities.length).toBeGreaterThan(0)
|
||||
for (const hr of hrActivities) {
|
||||
expect(hr.personalDataCategories).toContain('HEALTH_DATA')
|
||||
}
|
||||
})
|
||||
|
||||
it('calculates Art. 30 Abs. 5 exemption correctly', () => {
|
||||
// < 250 employees, no special categories → exempt
|
||||
const result1 = generateActivities({ org_employees: 50 })
|
||||
expect(result1.art30Abs5Exempt).toBe(true)
|
||||
|
||||
// >= 250 employees → not exempt
|
||||
const result2 = generateActivities({ org_employees: 500 })
|
||||
expect(result2.art30Abs5Exempt).toBe(false)
|
||||
|
||||
// < 250 but with special categories → not exempt
|
||||
const result3 = generateActivities({ org_employees: 50, data_health: true })
|
||||
expect(result3.art30Abs5Exempt).toBe(false)
|
||||
})
|
||||
|
||||
it('generates unique VVT IDs for all activities', () => {
|
||||
const result = generateActivities({
|
||||
dept_hr: true,
|
||||
dept_finance: true,
|
||||
dept_sales: true,
|
||||
dept_marketing: true,
|
||||
})
|
||||
const ids = result.generatedActivities.map((a) => a.vvtId)
|
||||
const uniqueIds = new Set(ids)
|
||||
expect(uniqueIds.size).toBe(ids.length)
|
||||
})
|
||||
|
||||
it('calculates coverage score > 0 for template-generated activities', () => {
|
||||
const result = generateActivities({ dept_hr: true })
|
||||
expect(result.coverageScore).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 5. Full Pipeline: Company Profile → Scope → VVT
|
||||
// =============================================================================
|
||||
|
||||
describe('Full Pipeline: CompanyProfile → Scope → VVT Generation', () => {
|
||||
// Helper: replicate what prefillFromScopeAnswers does (avoiding dynamic require)
|
||||
function scopeToProfilingAnswers(
|
||||
scopeAnswers: ScopeProfilingAnswer[]
|
||||
): Record<string, string | string[] | number | boolean> {
|
||||
const exported = exportToVVTAnswers(scopeAnswers)
|
||||
const profiling: Record<string, string | string[] | number | boolean> = {}
|
||||
for (const [key, value] of Object.entries(exported)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
profiling[key] = value as string | string[] | number | boolean
|
||||
}
|
||||
}
|
||||
return profiling
|
||||
}
|
||||
|
||||
it('complete flow: profile with DSB → scope prefill → VVT generation', () => {
|
||||
// Step 1: Company Profile
|
||||
const profile = {
|
||||
dpoName: 'Dr. Datenschutz',
|
||||
employeeCount: '50-249',
|
||||
industry: ['IT & Software'],
|
||||
offerings: ['WebApp', 'SaaS'],
|
||||
} as any
|
||||
|
||||
// Step 2: Prefill scope from profile
|
||||
const profileAnswers = prefillFromCompanyProfile(profile)
|
||||
const scoringAnswers = getAutoFilledScoringAnswers(profile)
|
||||
|
||||
// Simulate user answering scope questions + auto-prefilled from profile
|
||||
const userAnswers: ScopeProfilingAnswer[] = [
|
||||
// Block 8: departments
|
||||
ans('vvt_departments', ['personal', 'finanzen', 'it']),
|
||||
// Block 9: data categories per department
|
||||
ans('dk_dept_hr', ['NAME', 'ADDRESS', 'SALARY_DATA', 'HEALTH_DATA']),
|
||||
ans('dk_dept_finance', ['NAME', 'BANK_ACCOUNT', 'INVOICE_DATA', 'TAX_ID']),
|
||||
ans('dk_dept_it', ['USER_ACCOUNTS', 'LOG_DATA', 'DEVICE_DATA']),
|
||||
// Block 2: data types
|
||||
ans('data_art9', true),
|
||||
ans('data_minors', false),
|
||||
]
|
||||
|
||||
const allScopeAnswers = [...profileAnswers, ...scoringAnswers, ...userAnswers]
|
||||
|
||||
// Step 3: Export to VVT format
|
||||
const vvtAnswers = exportToVVTAnswers(allScopeAnswers)
|
||||
expect(vvtAnswers.dept_hr_categories).toEqual([
|
||||
'NAME',
|
||||
'ADDRESS',
|
||||
'SALARY_DATA',
|
||||
'HEALTH_DATA',
|
||||
])
|
||||
expect(vvtAnswers.dept_finance_categories).toEqual([
|
||||
'NAME',
|
||||
'BANK_ACCOUNT',
|
||||
'INVOICE_DATA',
|
||||
'TAX_ID',
|
||||
])
|
||||
|
||||
// Step 4: Prefill VVT profiling from scope (via direct export)
|
||||
const profilingAnswers = scopeToProfilingAnswers(allScopeAnswers)
|
||||
|
||||
// Verify data survived the transformation
|
||||
expect(profilingAnswers.dept_hr_categories).toEqual([
|
||||
'NAME',
|
||||
'ADDRESS',
|
||||
'SALARY_DATA',
|
||||
'HEALTH_DATA',
|
||||
])
|
||||
|
||||
// Step 5: Generate VVT activities
|
||||
// Add department triggers that match Block 8 selections
|
||||
profilingAnswers.dept_hr = true
|
||||
profilingAnswers.dept_finance = true
|
||||
|
||||
const result = generateActivities(profilingAnswers)
|
||||
|
||||
// Verify activities were generated
|
||||
expect(result.generatedActivities.length).toBeGreaterThan(4) // 4 IT baseline + HR + Finance
|
||||
|
||||
// Verify HR activities exist
|
||||
const hrActivities = result.generatedActivities.filter(
|
||||
(a) => a.businessFunction === 'hr'
|
||||
)
|
||||
expect(hrActivities.length).toBeGreaterThanOrEqual(3)
|
||||
|
||||
// Verify finance activities exist
|
||||
const financeActivities = result.generatedActivities.filter(
|
||||
(a) => a.businessFunction === 'finance'
|
||||
)
|
||||
expect(financeActivities.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
it('end-to-end: departments selected in scope generate correct VVT activities', () => {
|
||||
// Simulate a complete scope session with department selections
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [
|
||||
// Block 2: data_art9 maps to data_health in VVT
|
||||
ans('data_art9', true),
|
||||
// Block 4: tech_third_country maps to transfer_cloud_us
|
||||
ans('tech_third_country', true),
|
||||
// Block 8: departments
|
||||
ans('vvt_departments', ['personal', 'marketing', 'kundenservice']),
|
||||
// Block 9: per-department data categories
|
||||
ans('dk_dept_hr', ['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS']),
|
||||
ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA', 'CONSENT_DATA']),
|
||||
ans('dk_dept_support', ['NAME', 'TICKET_DATA', 'COMMUNICATION_DATA']),
|
||||
]
|
||||
|
||||
// Transform to VVT answers
|
||||
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
|
||||
|
||||
// Verify Block 9 data categories are mapped correctly
|
||||
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS'])
|
||||
expect(vvtAnswers.dept_marketing_categories).toEqual([
|
||||
'EMAIL',
|
||||
'TRACKING_DATA',
|
||||
'CONSENT_DATA',
|
||||
])
|
||||
expect(vvtAnswers.dept_support_categories).toEqual([
|
||||
'NAME',
|
||||
'TICKET_DATA',
|
||||
'COMMUNICATION_DATA',
|
||||
])
|
||||
|
||||
// Verify the full pipeline using direct export
|
||||
const profilingAnswers = scopeToProfilingAnswers(scopeAnswers)
|
||||
expect(profilingAnswers.dept_hr_categories).toBeDefined()
|
||||
expect(profilingAnswers.dept_marketing_categories).toBeDefined()
|
||||
expect(profilingAnswers.dept_support_categories).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 6. DEPARTMENT_DATA_CATEGORIES Integrity
|
||||
// =============================================================================
|
||||
|
||||
describe('DEPARTMENT_DATA_CATEGORIES consistency', () => {
|
||||
it('all 12 departments are defined', () => {
|
||||
const expected = [
|
||||
'dept_hr',
|
||||
'dept_recruiting',
|
||||
'dept_finance',
|
||||
'dept_sales',
|
||||
'dept_marketing',
|
||||
'dept_support',
|
||||
'dept_it',
|
||||
'dept_recht',
|
||||
'dept_produktion',
|
||||
'dept_logistik',
|
||||
'dept_einkauf',
|
||||
'dept_facility',
|
||||
]
|
||||
for (const dept of expected) {
|
||||
expect(DEPARTMENT_DATA_CATEGORIES[dept]).toBeDefined()
|
||||
expect(DEPARTMENT_DATA_CATEGORIES[dept].categories.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('every department has a label and icon', () => {
|
||||
for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) {
|
||||
expect(dept.label).toBeTruthy()
|
||||
expect(dept.icon).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it('every category has id and label', () => {
|
||||
for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) {
|
||||
for (const cat of dept.categories) {
|
||||
expect(cat.id).toBeTruthy()
|
||||
expect(cat.label).toBeTruthy()
|
||||
expect(cat.info).toBeTruthy()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Art. 9 categories are correctly flagged', () => {
|
||||
const art9Categories = [
|
||||
{ dept: 'dept_hr', id: 'HEALTH_DATA' },
|
||||
{ dept: 'dept_hr', id: 'RELIGIOUS_BELIEFS' },
|
||||
{ dept: 'dept_recruiting', id: 'HEALTH_DATA' },
|
||||
{ dept: 'dept_recht', id: 'CRIMINAL_DATA' },
|
||||
{ dept: 'dept_produktion', id: 'HEALTH_DATA' },
|
||||
{ dept: 'dept_facility', id: 'HEALTH_DATA' },
|
||||
]
|
||||
|
||||
for (const { dept, id } of art9Categories) {
|
||||
const cat = DEPARTMENT_DATA_CATEGORIES[dept].categories.find((c) => c.id === id)
|
||||
expect(cat?.isArt9).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 7. Block 9 ↔ VVT Mapping Integrity
|
||||
// =============================================================================
|
||||
|
||||
describe('Block 9 Scope ↔ VVT question mapping', () => {
|
||||
it('every Block 9 question has mapsToVVTQuestion', () => {
|
||||
const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail')
|
||||
expect(block9).toBeDefined()
|
||||
|
||||
for (const q of block9!.questions) {
|
||||
expect(q.mapsToVVTQuestion).toBeTruthy()
|
||||
expect(q.mapsToVVTQuestion).toMatch(/^dept_\w+_categories$/)
|
||||
}
|
||||
})
|
||||
|
||||
it('Block 9 question options match DEPARTMENT_DATA_CATEGORIES', () => {
|
||||
const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail')
|
||||
expect(block9).toBeDefined()
|
||||
|
||||
// dk_dept_hr should have same options as DEPARTMENT_DATA_CATEGORIES.dept_hr
|
||||
const hrQuestion = block9!.questions.find((q) => q.id === 'dk_dept_hr')
|
||||
expect(hrQuestion).toBeDefined()
|
||||
|
||||
const expectedIds = DEPARTMENT_DATA_CATEGORIES.dept_hr.categories.map((c) => c.id)
|
||||
const actualIds = hrQuestion!.options!.map((o) => o.value)
|
||||
expect(actualIds).toEqual(expectedIds)
|
||||
})
|
||||
|
||||
it('SCOPE_PREFILLED_VVT_QUESTIONS lists all cross-module questions', () => {
|
||||
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('org_industry')
|
||||
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('dept_hr')
|
||||
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('data_health')
|
||||
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('transfer_cloud_us')
|
||||
expect(SCOPE_PREFILLED_VVT_QUESTIONS.length).toBeGreaterThanOrEqual(15)
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// 8. Edge Cases
|
||||
// =============================================================================
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('generateActivities with no answers still produces IT baselines', () => {
|
||||
const result = generateActivities({})
|
||||
expect(result.generatedActivities.length).toBe(4) // 4 IT baselines
|
||||
expect(result.art30Abs5Exempt).toBe(true) // 0 employees, no special categories
|
||||
})
|
||||
|
||||
it('same template triggered by multiple questions is only generated once', () => {
|
||||
const result = generateActivities({
|
||||
dept_sales: true, // triggers sales-kundenverwaltung
|
||||
sys_crm: true, // also triggers sales-kundenverwaltung
|
||||
})
|
||||
|
||||
const salesKunden = result.generatedActivities.filter((a) =>
|
||||
a.name.toLowerCase().includes('kundenverwaltung')
|
||||
)
|
||||
// Should be deduplicated (Set-based triggeredIds)
|
||||
expect(salesKunden.length).toBe(1)
|
||||
})
|
||||
|
||||
it('empty department category selections produce valid but empty mappings', () => {
|
||||
const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', [])]
|
||||
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
|
||||
expect(vvtAnswers.dept_hr_categories).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Loeschfristen Baseline-Katalog
|
||||
*
|
||||
* 18 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
|
||||
* 25 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
|
||||
* Datenobjekte in deutschen Unternehmen. Basierend auf AO, HGB,
|
||||
* UStG, BGB, ArbZG, AGG, BDSG und BSIG.
|
||||
* UStG, BGB, ArbZG, AGG, BDSG, BSIG und ArbMedVV.
|
||||
*
|
||||
* Werden genutzt, um neue Loeschfrist-Policies schnell aus
|
||||
* bewaehrten Vorlagen zu erstellen.
|
||||
@@ -48,7 +48,7 @@ export interface BaselineTemplate {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// BASELINE TEMPLATES (18 Vorlagen)
|
||||
// BASELINE TEMPLATES (25 Vorlagen)
|
||||
// =============================================================================
|
||||
|
||||
export const BASELINE_TEMPLATES: BaselineTemplate[] = [
|
||||
@@ -519,6 +519,188 @@ export const BASELINE_TEMPLATES: BaselineTemplate[] = [
|
||||
reviewInterval: 'ANNUAL',
|
||||
tags: ['datenschutz', 'consent'],
|
||||
},
|
||||
|
||||
// ==================== 19. E-Mail-Archivierung ====================
|
||||
{
|
||||
templateId: 'email-archivierung',
|
||||
dataObjectName: 'E-Mail-Archivierung',
|
||||
description:
|
||||
'Archivierte geschaeftliche E-Mails inkl. Anhaenge, die als Handelsbriefe oder steuerrelevante Korrespondenz einzustufen sind.',
|
||||
affectedGroups: ['Mitarbeiter', 'Kunden', 'Lieferanten'],
|
||||
dataCategories: ['E-Mail-Korrespondenz', 'Anhaenge', 'Metadaten'],
|
||||
primaryPurpose:
|
||||
'Erfuellung der handelsrechtlichen Aufbewahrungspflicht fuer geschaeftliche Korrespondenz, die als Handelsbrief einzuordnen ist.',
|
||||
deletionTrigger: 'RETENTION_DRIVER',
|
||||
retentionDriver: 'HGB_257',
|
||||
retentionDriverDetail:
|
||||
'Aufbewahrungspflicht gemaess 257 HGB fuer empfangene und versandte Handelsbriefe (6 Jahre) bzw. buchhalterisch relevante E-Mails (10 Jahre).',
|
||||
retentionDuration: 6,
|
||||
retentionUnit: 'YEARS',
|
||||
retentionDescription: '6 Jahre nach Versand/Empfang der E-Mail',
|
||||
startEvent: 'Versand- bzw. Empfangsdatum der E-Mail',
|
||||
deletionMethod: 'AUTO_DELETE',
|
||||
deletionMethodDetail:
|
||||
'Automatische Loeschung durch das E-Mail-Archivierungssystem nach Ablauf der konfigurierten Aufbewahrungsfrist. Vor Loeschung wird geprueft, ob die E-Mail in laufenden Verfahren benoetigt wird.',
|
||||
responsibleRole: 'IT-Abteilung',
|
||||
reviewInterval: 'ANNUAL',
|
||||
tags: ['kommunikation', 'hgb'],
|
||||
},
|
||||
|
||||
// ==================== 20. Zutrittsprotokolle ====================
|
||||
{
|
||||
templateId: 'zutrittsprotokolle',
|
||||
dataObjectName: 'Zutrittsprotokolle',
|
||||
description:
|
||||
'Protokolle des Zutrittskontrollsystems inkl. Zeitstempel, Kartennummer, Zutrittsort und Zugangsentscheidung (gewaehrt/verweigert).',
|
||||
affectedGroups: ['Mitarbeiter', 'Besucher'],
|
||||
dataCategories: ['Zutrittsdaten', 'Zeitstempel', 'Kartennummern', 'Standortdaten'],
|
||||
primaryPurpose:
|
||||
'Sicherstellung der physischen Sicherheit, Nachvollziehbarkeit von Zutritten und Unterstuetzung bei der Aufklaerung von Sicherheitsvorfaellen.',
|
||||
deletionTrigger: 'RETENTION_DRIVER',
|
||||
retentionDriver: 'BSIG',
|
||||
retentionDriverDetail:
|
||||
'Aufbewahrung gemaess BSI-Grundschutz-Empfehlung fuer Zutrittsprotokolle zur Analyse von Sicherheitsvorfaellen (90 Tage).',
|
||||
retentionDuration: 90,
|
||||
retentionUnit: 'DAYS',
|
||||
retentionDescription: '90 Tage nach Zeitpunkt des Zutritts',
|
||||
startEvent: 'Zeitpunkt des protokollierten Zutrittsereignisses',
|
||||
deletionMethod: 'AUTO_DELETE',
|
||||
deletionMethodDetail:
|
||||
'Automatische Rotation und Loeschung der Zutrittsprotokolle durch das Zutrittskontrollsystem nach Ablauf der 90-Tage-Frist.',
|
||||
responsibleRole: 'Facility Management',
|
||||
reviewInterval: 'QUARTERLY',
|
||||
tags: ['sicherheit', 'zutritt'],
|
||||
},
|
||||
|
||||
// ==================== 21. Schulungsnachweise ====================
|
||||
{
|
||||
templateId: 'schulungsnachweise',
|
||||
dataObjectName: 'Schulungsnachweise',
|
||||
description:
|
||||
'Teilnahmebestaetigungen, Zertifikate und Protokolle von Mitarbeiterschulungen (Datenschutz, Arbeitssicherheit, Compliance).',
|
||||
affectedGroups: ['Mitarbeiter'],
|
||||
dataCategories: ['Schulungsdaten', 'Zertifikate', 'Teilnahmelisten'],
|
||||
primaryPurpose:
|
||||
'Nachweis der Durchfuehrung gesetzlich vorgeschriebener Schulungen und Dokumentation der Mitarbeiterqualifikation.',
|
||||
deletionTrigger: 'RETENTION_DRIVER',
|
||||
retentionDriver: 'CUSTOM',
|
||||
retentionDriverDetail:
|
||||
'Aufbewahrung fuer 3 Jahre nach Ende des Beschaeftigungsverhaeltnisses als Nachweis der ordnungsgemaessen Schulungsdurchfuehrung.',
|
||||
retentionDuration: 3,
|
||||
retentionUnit: 'YEARS',
|
||||
retentionDescription: '3 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
|
||||
startEvent: 'Ende des Beschaeftigungsverhaeltnisses des geschulten Mitarbeiters',
|
||||
deletionMethod: 'MANUAL_REVIEW_DELETE',
|
||||
deletionMethodDetail:
|
||||
'Manuelle Pruefung durch die HR-Abteilung vor Loeschung, da Schulungsnachweise als Compliance-Nachweis in Audits relevant sein koennen.',
|
||||
responsibleRole: 'HR-Abteilung',
|
||||
reviewInterval: 'ANNUAL',
|
||||
tags: ['hr', 'schulung'],
|
||||
},
|
||||
|
||||
// ==================== 22. Betriebsarzt-Dokumentation ====================
|
||||
{
|
||||
templateId: 'betriebsarzt-doku',
|
||||
dataObjectName: 'Betriebsarzt-Dokumentation',
|
||||
description:
|
||||
'Ergebnisse arbeitsmedizinischer Vorsorgeuntersuchungen, Eignungsuntersuchungen und arbeitsmedizinische Empfehlungen.',
|
||||
affectedGroups: ['Mitarbeiter'],
|
||||
dataCategories: ['Gesundheitsdaten', 'Vorsorgeuntersuchungen', 'Eignungsbefunde'],
|
||||
primaryPurpose:
|
||||
'Erfuellung der Dokumentationspflicht fuer arbeitsmedizinische Vorsorge gemaess ArbMedVV und Nachweisfuehrung gegenueber Berufsgenossenschaften.',
|
||||
deletionTrigger: 'RETENTION_DRIVER',
|
||||
retentionDriver: 'CUSTOM',
|
||||
retentionDriverDetail:
|
||||
'Aufbewahrungspflicht gemaess ArbMedVV (Verordnung zur arbeitsmedizinischen Vorsorge) und Berufsgenossenschaftliche Grundsaetze: bis zu 40 Jahre bei Exposition gegenueber krebserzeugenden Gefahrstoffen.',
|
||||
retentionDuration: 40,
|
||||
retentionUnit: 'YEARS',
|
||||
retentionDescription: '40 Jahre nach letzter Exposition (bei Gefahrstoffen), sonst 10 Jahre nach Ende der Taetigkeit',
|
||||
startEvent: 'Ende der expositionsrelevanten Taetigkeit bzw. Ende des Beschaeftigungsverhaeltnisses',
|
||||
deletionMethod: 'PHYSICAL_DESTROY',
|
||||
deletionMethodDetail:
|
||||
'Physische Vernichtung der Papierunterlagen durch zertifizierten Aktenvernichtungsdienstleister (DIN 66399, Sicherheitsstufe P-5). Digitale Daten werden kryptographisch geloescht.',
|
||||
responsibleRole: 'Betriebsarzt / Arbeitsmedizinischer Dienst',
|
||||
reviewInterval: 'ANNUAL',
|
||||
tags: ['hr', 'gesundheit'],
|
||||
},
|
||||
|
||||
// ==================== 23. Kundenreklamationen ====================
|
||||
{
|
||||
templateId: 'kundenreklamationen',
|
||||
dataObjectName: 'Kundenreklamationen',
|
||||
description:
|
||||
'Reklamationsvorgaenge inkl. Beschwerdeinhalt, Kommunikationsverlauf, Massnahmen und Ergebnis der Reklamationsbearbeitung.',
|
||||
affectedGroups: ['Kunden'],
|
||||
dataCategories: ['Reklamationsdaten', 'Kommunikation', 'Massnahmenprotokolle'],
|
||||
primaryPurpose:
|
||||
'Dokumentation und Bearbeitung von Kundenreklamationen, Qualitaetssicherung und Absicherung gegen Gewaehrleistungsansprueche.',
|
||||
deletionTrigger: 'RETENTION_DRIVER',
|
||||
retentionDriver: 'BGB_195',
|
||||
retentionDriverDetail:
|
||||
'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB (3 Jahre) zur Absicherung gegen Gewaehrleistungs- und Schadensersatzansprueche.',
|
||||
retentionDuration: 3,
|
||||
retentionUnit: 'YEARS',
|
||||
retentionDescription: '3 Jahre nach Abschluss des Reklamationsvorgangs',
|
||||
startEvent: 'Abschluss des Reklamationsvorgangs (letzte Massnahme)',
|
||||
deletionMethod: 'ANONYMIZATION',
|
||||
deletionMethodDetail:
|
||||
'Anonymisierung der personenbezogenen Daten nach Ablauf der Frist. Anonymisierte Reklamationsstatistiken bleiben fuer die Qualitaetssicherung erhalten.',
|
||||
responsibleRole: 'Qualitaetsmanagement',
|
||||
reviewInterval: 'ANNUAL',
|
||||
tags: ['kunden', 'qualitaet'],
|
||||
},
|
||||
|
||||
// ==================== 24. Lieferantenbewertungen ====================
|
||||
{
|
||||
templateId: 'lieferantenbewertungen',
|
||||
dataObjectName: 'Lieferantenbewertungen',
|
||||
description:
|
||||
'Bewertungen und Auditergebnisse von Lieferanten und Auftragsverarbeitern inkl. Qualitaets-, Compliance- und Datenschutz-Bewertungen.',
|
||||
affectedGroups: ['Lieferanten', 'Auftragsverarbeiter'],
|
||||
dataCategories: ['Bewertungsdaten', 'Auditberichte', 'Vertragsdaten'],
|
||||
primaryPurpose:
|
||||
'Dokumentation der Sorgfaltspflicht bei der Auswahl und Ueberwachung von Auftragsverarbeitern gemaess Art. 28 DSGVO und Qualitaetssicherung in der Lieferkette.',
|
||||
deletionTrigger: 'RETENTION_DRIVER',
|
||||
retentionDriver: 'HGB_257',
|
||||
retentionDriverDetail:
|
||||
'Aufbewahrung gemaess 257 HGB als handelsrelevante Unterlagen sowie zur Nachweisfuehrung der Sorgfaltspflicht bei der Auftragsverarbeitung.',
|
||||
retentionDuration: 6,
|
||||
retentionUnit: 'YEARS',
|
||||
retentionDescription: '6 Jahre nach Ende der Geschaeftsbeziehung',
|
||||
startEvent: 'Ende der Geschaeftsbeziehung mit dem Lieferanten/Auftragsverarbeiter',
|
||||
deletionMethod: 'MANUAL_REVIEW_DELETE',
|
||||
deletionMethodDetail:
|
||||
'Manuelle Pruefung durch den Einkauf/Compliance-Abteilung vor Loeschung, um sicherzustellen, dass keine Nachweispflichten aus laufenden Vertraegen oder Audits bestehen.',
|
||||
responsibleRole: 'Einkauf / Compliance',
|
||||
reviewInterval: 'ANNUAL',
|
||||
tags: ['lieferanten', 'einkauf'],
|
||||
},
|
||||
|
||||
// ==================== 25. Social-Media-Marketingdaten ====================
|
||||
{
|
||||
templateId: 'social-media-daten',
|
||||
dataObjectName: 'Social-Media-Marketingdaten',
|
||||
description:
|
||||
'Personenbezogene Daten aus Social-Media-Kampagnen inkl. Nutzerinteraktionen, Custom Audiences, Retargeting-Listen und Kampagnen-Analytics.',
|
||||
affectedGroups: ['Kunden', 'Interessenten', 'Website-Besucher'],
|
||||
dataCategories: ['Interaktionsdaten', 'Zielgruppendaten', 'Tracking-Daten', 'Profilmerkmale'],
|
||||
primaryPurpose:
|
||||
'Durchfuehrung zielgerichteter Marketing-Kampagnen auf Social-Media-Plattformen und Analyse der Kampagneneffektivitaet.',
|
||||
deletionTrigger: 'PURPOSE_END',
|
||||
retentionDriver: null,
|
||||
retentionDriverDetail:
|
||||
'Keine gesetzliche Aufbewahrungspflicht. Daten werden bis zum Widerruf der Einwilligung bzw. bis zum Ende der Kampagne gespeichert (Art. 6 Abs. 1 lit. a DSGVO).',
|
||||
retentionDuration: null,
|
||||
retentionUnit: null,
|
||||
retentionDescription: 'Bis zum Widerruf der Einwilligung oder Ende des Kampagnenzwecks',
|
||||
startEvent: 'Widerruf der Einwilligung oder Ende der Marketing-Kampagne',
|
||||
deletionMethod: 'AUTO_DELETE',
|
||||
deletionMethodDetail:
|
||||
'Automatische Loeschung der personenbezogenen Daten in den Social-Media-Werbekonten und internen Systemen nach Zweckwegfall. Custom Audiences werden bei Plattformanbietern geloescht.',
|
||||
responsibleRole: 'Marketing',
|
||||
reviewInterval: 'SEMI_ANNUAL',
|
||||
tags: ['marketing', 'social'],
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
import {
|
||||
LoeschfristPolicy,
|
||||
PolicyStatus,
|
||||
RetentionDriverType,
|
||||
isPolicyOverdue,
|
||||
getActiveLegalHolds,
|
||||
RETENTION_DRIVER_META,
|
||||
} from './loeschfristen-types'
|
||||
|
||||
// =============================================================================
|
||||
@@ -22,6 +24,10 @@ export type ComplianceIssueType =
|
||||
| 'LEGAL_HOLD_CONFLICT'
|
||||
| 'STALE_DRAFT'
|
||||
| 'UNCOVERED_VVT_CATEGORY'
|
||||
| 'MISSING_DELETION_METHOD'
|
||||
| 'MISSING_STORAGE_LOCATIONS'
|
||||
| 'EXCESSIVE_RETENTION'
|
||||
| 'MISSING_DATA_CATEGORIES'
|
||||
|
||||
export type ComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
||||
|
||||
@@ -219,6 +225,108 @@ function checkStaleDraft(policy: LoeschfristPolicy): ComplianceIssue | null {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 8: MISSING_DELETION_METHOD (MEDIUM)
|
||||
* Active policy without a deletion method detail description.
|
||||
*/
|
||||
function checkMissingDeletionMethod(policy: LoeschfristPolicy): ComplianceIssue | null {
|
||||
if (policy.status === 'ACTIVE' && !policy.deletionMethodDetail.trim()) {
|
||||
return createIssue(
|
||||
policy,
|
||||
'MISSING_DELETION_METHOD',
|
||||
'MEDIUM',
|
||||
'Keine Loeschmethode beschrieben',
|
||||
`Die aktive Policy "${policy.dataObjectName}" hat keine detaillierte Beschreibung der Loeschmethode. Fuer ein auditfaehiges Loeschkonzept muss dokumentiert sein, wie die Loeschung technisch durchgefuehrt wird.`,
|
||||
'Ergaenzen Sie eine detaillierte Beschreibung der Loeschmethode (z.B. automatisches Loeschen durch Datenbank-Job, manuelle Pruefung durch Fachabteilung, kryptographische Loeschung).'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 9: MISSING_STORAGE_LOCATIONS (MEDIUM)
|
||||
* Active policy without any documented storage locations.
|
||||
*/
|
||||
function checkMissingStorageLocations(policy: LoeschfristPolicy): ComplianceIssue | null {
|
||||
if (policy.status === 'ACTIVE' && policy.storageLocations.length === 0) {
|
||||
return createIssue(
|
||||
policy,
|
||||
'MISSING_STORAGE_LOCATIONS',
|
||||
'MEDIUM',
|
||||
'Keine Speicherorte dokumentiert',
|
||||
`Die aktive Policy "${policy.dataObjectName}" hat keine Speicherorte hinterlegt. Ohne Speicherort-Dokumentation ist unklar, wo die Daten gespeichert sind und wo die Loeschung durchgefuehrt werden muss.`,
|
||||
'Dokumentieren Sie mindestens einen Speicherort (z.B. Datenbank, Cloud-Speicher, E-Mail-System, Papierarchiv).'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 10: EXCESSIVE_RETENTION (HIGH)
|
||||
* Retention duration exceeds 2x the legal default for the driver.
|
||||
*/
|
||||
function checkExcessiveRetention(policy: LoeschfristPolicy): ComplianceIssue | null {
|
||||
if (
|
||||
policy.retentionDriver &&
|
||||
policy.retentionDriver !== 'CUSTOM' &&
|
||||
policy.retentionDuration !== null &&
|
||||
policy.retentionUnit !== null
|
||||
) {
|
||||
const meta = RETENTION_DRIVER_META[policy.retentionDriver]
|
||||
if (meta.defaultDuration !== null && meta.defaultUnit !== null) {
|
||||
// Normalize both to days for comparison
|
||||
const policyDays = toDays(policy.retentionDuration, policy.retentionUnit)
|
||||
const legalDays = toDays(meta.defaultDuration, meta.defaultUnit)
|
||||
|
||||
if (legalDays > 0 && policyDays > legalDays * 2) {
|
||||
return createIssue(
|
||||
policy,
|
||||
'EXCESSIVE_RETENTION',
|
||||
'HIGH',
|
||||
'Ueberschreitung der gesetzlichen Aufbewahrungsfrist',
|
||||
`Die Policy "${policy.dataObjectName}" hat eine Aufbewahrungsdauer von ${policy.retentionDuration} ${policy.retentionUnit === 'YEARS' ? 'Jahren' : policy.retentionUnit === 'MONTHS' ? 'Monaten' : 'Tagen'}, die mehr als das Doppelte der gesetzlichen Frist (${meta.defaultDuration} ${meta.defaultUnit === 'YEARS' ? 'Jahre' : meta.defaultUnit === 'MONTHS' ? 'Monate' : 'Tage'} nach ${meta.statute}) betraegt. Ueberlange Speicherung widerspricht dem Grundsatz der Speicherbegrenzung (Art. 5 Abs. 1 lit. e DSGVO).`,
|
||||
'Pruefen Sie, ob die verlaengerte Aufbewahrungsdauer gerechtfertigt ist. Falls nicht, reduzieren Sie sie auf die gesetzliche Mindestfrist.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 11: MISSING_DATA_CATEGORIES (LOW)
|
||||
* Non-draft policy without any data categories assigned.
|
||||
*/
|
||||
function checkMissingDataCategories(policy: LoeschfristPolicy): ComplianceIssue | null {
|
||||
if (policy.status !== 'DRAFT' && policy.dataCategories.length === 0) {
|
||||
return createIssue(
|
||||
policy,
|
||||
'MISSING_DATA_CATEGORIES',
|
||||
'LOW',
|
||||
'Keine Datenkategorien zugeordnet',
|
||||
`Die Policy "${policy.dataObjectName}" (Status: ${policy.status}) hat keine Datenkategorien zugeordnet. Ohne Datenkategorien ist unklar, welche personenbezogenen Daten von dieser Loeschregel betroffen sind.`,
|
||||
'Ordnen Sie mindestens eine Datenkategorie zu (z.B. Stammdaten, Kontaktdaten, Finanzdaten, Gesundheitsdaten).'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: convert retention duration to days for comparison.
|
||||
*/
|
||||
function toDays(duration: number, unit: string): number {
|
||||
switch (unit) {
|
||||
case 'DAYS': return duration
|
||||
case 'MONTHS': return duration * 30
|
||||
case 'YEARS': return duration * 365
|
||||
default: return duration
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN COMPLIANCE CHECK
|
||||
// =============================================================================
|
||||
@@ -248,6 +356,10 @@ export function runComplianceCheck(
|
||||
checkNoResponsible(policy),
|
||||
checkLegalHoldConflict(policy),
|
||||
checkStaleDraft(policy),
|
||||
checkMissingDeletionMethod(policy),
|
||||
checkMissingStorageLocations(policy),
|
||||
checkExcessiveRetention(policy),
|
||||
checkMissingDataCategories(policy),
|
||||
]
|
||||
|
||||
for (const issue of checks) {
|
||||
|
||||
879
admin-compliance/lib/sdk/loeschfristen-document.ts
Normal file
879
admin-compliance/lib/sdk/loeschfristen-document.ts
Normal file
@@ -0,0 +1,879 @@
|
||||
// =============================================================================
|
||||
// Loeschfristen Module - Loeschkonzept Document Generator
|
||||
// Generates a printable, audit-ready HTML document according to DSGVO Art. 5/17/30
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
LoeschfristPolicy,
|
||||
RetentionDriverType,
|
||||
} from './loeschfristen-types'
|
||||
|
||||
import {
|
||||
RETENTION_DRIVER_META,
|
||||
DELETION_METHOD_LABELS,
|
||||
STATUS_LABELS,
|
||||
TRIGGER_LABELS,
|
||||
REVIEW_INTERVAL_LABELS,
|
||||
formatRetentionDuration,
|
||||
getEffectiveDeletionTrigger,
|
||||
getActiveLegalHolds,
|
||||
} from './loeschfristen-types'
|
||||
|
||||
import type { ComplianceCheckResult, ComplianceIssueSeverity } from './loeschfristen-compliance'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface LoeschkonzeptOrgHeader {
|
||||
organizationName: string
|
||||
industry: string
|
||||
dpoName: string
|
||||
dpoContact: string
|
||||
responsiblePerson: string
|
||||
locations: string[]
|
||||
employeeCount: string
|
||||
loeschkonzeptVersion: string
|
||||
lastReviewDate: string
|
||||
nextReviewDate: string
|
||||
reviewInterval: string
|
||||
}
|
||||
|
||||
export interface LoeschkonzeptRevision {
|
||||
version: string
|
||||
date: string
|
||||
author: string
|
||||
changes: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEFAULTS
|
||||
// =============================================================================
|
||||
|
||||
export function createDefaultLoeschkonzeptOrgHeader(): LoeschkonzeptOrgHeader {
|
||||
const now = new Date()
|
||||
const nextYear = new Date()
|
||||
nextYear.setFullYear(nextYear.getFullYear() + 1)
|
||||
|
||||
return {
|
||||
organizationName: '',
|
||||
industry: '',
|
||||
dpoName: '',
|
||||
dpoContact: '',
|
||||
responsiblePerson: '',
|
||||
locations: [],
|
||||
employeeCount: '',
|
||||
loeschkonzeptVersion: '1.0',
|
||||
lastReviewDate: now.toISOString().split('T')[0],
|
||||
nextReviewDate: nextYear.toISOString().split('T')[0],
|
||||
reviewInterval: 'Jaehrlich',
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEVERITY LABELS (for Compliance Status section)
|
||||
// =============================================================================
|
||||
|
||||
const SEVERITY_LABELS_DE: Record<ComplianceIssueSeverity, string> = {
|
||||
CRITICAL: 'Kritisch',
|
||||
HIGH: 'Hoch',
|
||||
MEDIUM: 'Mittel',
|
||||
LOW: 'Niedrig',
|
||||
}
|
||||
|
||||
const SEVERITY_COLORS: Record<ComplianceIssueSeverity, string> = {
|
||||
CRITICAL: '#dc2626',
|
||||
HIGH: '#ea580c',
|
||||
MEDIUM: '#d97706',
|
||||
LOW: '#6b7280',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTML DOCUMENT BUILDER
|
||||
// =============================================================================
|
||||
|
||||
export function buildLoeschkonzeptHtml(
|
||||
policies: LoeschfristPolicy[],
|
||||
orgHeader: LoeschkonzeptOrgHeader,
|
||||
vvtActivities: Array<{ id: string; vvt_id?: string; vvtId?: string; name?: string; activity_name?: string }>,
|
||||
complianceResult: ComplianceCheckResult | null,
|
||||
revisions: LoeschkonzeptRevision[]
|
||||
): string {
|
||||
const today = new Date().toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
|
||||
const orgName = orgHeader.organizationName || 'Organisation'
|
||||
|
||||
// Collect unique storage locations across all policies
|
||||
const allStorageLocations = new Set<string>()
|
||||
for (const p of activePolicies) {
|
||||
for (const loc of p.storageLocations) {
|
||||
allStorageLocations.add(loc.name || loc.type)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect unique responsible roles
|
||||
const roleMap = new Map<string, string[]>()
|
||||
for (const p of activePolicies) {
|
||||
const role = p.responsibleRole || p.responsiblePerson || 'Nicht zugewiesen'
|
||||
if (!roleMap.has(role)) roleMap.set(role, [])
|
||||
roleMap.get(role)!.push(p.dataObjectName || p.policyId)
|
||||
}
|
||||
|
||||
// Collect active legal holds
|
||||
const allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }> = []
|
||||
for (const p of activePolicies) {
|
||||
for (const h of getActiveLegalHolds(p)) {
|
||||
allActiveLegalHolds.push({ policy: p.dataObjectName || p.policyId, hold: h })
|
||||
}
|
||||
}
|
||||
|
||||
// Build VVT cross-reference data
|
||||
const vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }> = []
|
||||
for (const p of activePolicies) {
|
||||
for (const linkedId of p.linkedVVTActivityIds) {
|
||||
const activity = vvtActivities.find(a => a.id === linkedId)
|
||||
if (activity) {
|
||||
vvtRefs.push({
|
||||
policyName: p.dataObjectName || p.policyId,
|
||||
policyId: p.policyId,
|
||||
vvtId: activity.vvt_id || activity.vvtId || linkedId.substring(0, 8),
|
||||
vvtName: activity.activity_name || activity.name || 'Unbenannte Verarbeitungstaetigkeit',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build vendor cross-reference data
|
||||
const vendorRefs: Array<{ policyName: string; policyId: string; vendorId: string; duration: string }> = []
|
||||
for (const p of activePolicies) {
|
||||
if (p.linkedVendorIds && p.linkedVendorIds.length > 0) {
|
||||
for (const vendorId of p.linkedVendorIds) {
|
||||
vendorRefs.push({
|
||||
policyName: p.dataObjectName || p.policyId,
|
||||
policyId: p.policyId,
|
||||
vendorId,
|
||||
duration: formatRetentionDuration(p.retentionDuration, p.retentionUnit),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HTML Template
|
||||
// =========================================================================
|
||||
|
||||
let html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Loeschkonzept — ${escHtml(orgName)}</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 10pt;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Cover */
|
||||
.cover {
|
||||
min-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
page-break-after: always;
|
||||
}
|
||||
.cover h1 {
|
||||
font-size: 28pt;
|
||||
color: #5b21b6;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cover .subtitle {
|
||||
font-size: 14pt;
|
||||
color: #7c3aed;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.cover .org-info {
|
||||
background: #f5f3ff;
|
||||
border: 1px solid #ddd6fe;
|
||||
border-radius: 8px;
|
||||
padding: 24px 40px;
|
||||
text-align: left;
|
||||
width: 400px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.cover .org-info div { margin-bottom: 6px; }
|
||||
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
|
||||
.cover .legal-ref {
|
||||
font-size: 9pt;
|
||||
color: #64748b;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* TOC */
|
||||
.toc {
|
||||
page-break-after: always;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.toc h2 {
|
||||
font-size: 18pt;
|
||||
color: #5b21b6;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #5b21b6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.toc-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dotted #cbd5e1;
|
||||
font-size: 10pt;
|
||||
}
|
||||
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.section-header {
|
||||
font-size: 14pt;
|
||||
color: #5b21b6;
|
||||
font-weight: 700;
|
||||
margin: 30px 0 12px 0;
|
||||
border-bottom: 2px solid #ddd6fe;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.section-body { margin-bottom: 16px; }
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 16px 0;
|
||||
font-size: 9pt;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f5f3ff;
|
||||
color: #5b21b6;
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
tr:nth-child(even) td { background: #faf5ff; }
|
||||
|
||||
/* Detail cards */
|
||||
.policy-detail {
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.policy-detail-header {
|
||||
background: #f5f3ff;
|
||||
padding: 8px 12px;
|
||||
font-weight: 700;
|
||||
color: #5b21b6;
|
||||
border-bottom: 1px solid #ddd6fe;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.policy-detail-body { padding: 0; }
|
||||
.policy-detail-body table { margin: 0; }
|
||||
.policy-detail-body th { width: 200px; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-active { background: #dcfce7; color: #166534; }
|
||||
.badge-draft { background: #f3f4f6; color: #374151; }
|
||||
.badge-review { background: #fef9c3; color: #854d0e; }
|
||||
.badge-critical { background: #fecaca; color: #991b1b; }
|
||||
.badge-high { background: #fed7aa; color: #9a3412; }
|
||||
.badge-medium { background: #fef3c7; color: #92400e; }
|
||||
.badge-low { background: #f3f4f6; color: #4b5563; }
|
||||
|
||||
/* Principles */
|
||||
.principle {
|
||||
margin-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.principle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #7c3aed;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.principle strong { color: #5b21b6; }
|
||||
|
||||
/* Score */
|
||||
.score-box {
|
||||
display: inline-block;
|
||||
padding: 4px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.score-excellent { background: #dcfce7; color: #166534; }
|
||||
.score-good { background: #dbeafe; color: #1e40af; }
|
||||
.score-needs-work { background: #fef3c7; color: #92400e; }
|
||||
.score-poor { background: #fecaca; color: #991b1b; }
|
||||
|
||||
/* Footer */
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px 18mm;
|
||||
font-size: 7.5pt;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.no-print { display: none !important; }
|
||||
.page-break { page-break-after: always; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 0: Cover Page
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="cover">
|
||||
<h1>Loeschkonzept</h1>
|
||||
<div class="subtitle">gemaess Art. 5 Abs. 1 lit. e, Art. 17, Art. 30 DSGVO</div>
|
||||
<div class="org-info">
|
||||
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
|
||||
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
|
||||
${orgHeader.dpoName ? `<div><span class="label">Datenschutzbeauftragter:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
|
||||
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
|
||||
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
|
||||
${orgHeader.employeeCount ? `<div><span class="label">Mitarbeiter:</span> ${escHtml(orgHeader.employeeCount)}</div>` : ''}
|
||||
${orgHeader.locations.length > 0 ? `<div><span class="label">Standorte:</span> ${escHtml(orgHeader.locations.join(', '))}</div>` : ''}
|
||||
</div>
|
||||
<div class="legal-ref">
|
||||
Version ${escHtml(orgHeader.loeschkonzeptVersion)} | Stand: ${today}<br/>
|
||||
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
|
||||
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Table of Contents
|
||||
// =========================================================================
|
||||
const sections = [
|
||||
'Ziel und Zweck',
|
||||
'Geltungsbereich',
|
||||
'Grundprinzipien der Datenspeicherung',
|
||||
'Loeschregeln-Uebersicht',
|
||||
'Detaillierte Loeschregeln',
|
||||
'VVT-Verknuepfung',
|
||||
'Auftragsverarbeiter mit Loeschpflichten',
|
||||
'Legal Hold Verfahren',
|
||||
'Verantwortlichkeiten',
|
||||
'Pruef- und Revisionszyklus',
|
||||
'Compliance-Status',
|
||||
'Aenderungshistorie',
|
||||
]
|
||||
|
||||
html += `
|
||||
<div class="toc">
|
||||
<h2>Inhaltsverzeichnis</h2>
|
||||
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 1: Ziel und Zweck
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">1. Ziel und Zweck</div>
|
||||
<div class="section-body">
|
||||
<p>Dieses Loeschkonzept definiert die systematischen Regeln und Verfahren fuer die Loeschung
|
||||
personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Es dient der Umsetzung
|
||||
folgender DSGVO-Anforderungen:</p>
|
||||
<table>
|
||||
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
|
||||
<tr><td><strong>Art. 5 Abs. 1 lit. e DSGVO</strong></td><td>Grundsatz der Speicherbegrenzung — personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke der Verarbeitung erforderlich ist.</td></tr>
|
||||
<tr><td><strong>Art. 17 DSGVO</strong></td><td>Recht auf Loeschung („Recht auf Vergessenwerden“) — Betroffene haben das Recht, die Loeschung ihrer Daten zu verlangen.</td></tr>
|
||||
<tr><td><strong>Art. 30 DSGVO</strong></td><td>Verzeichnis von Verarbeitungstaetigkeiten — vorgesehene Fristen fuer die Loeschung der verschiedenen Datenkategorien muessen dokumentiert werden.</td></tr>
|
||||
</table>
|
||||
<p>Das Loeschkonzept ist fester Bestandteil des Datenschutz-Managementsystems und wird
|
||||
regelmaessig ueberprueft und aktualisiert.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 2: Geltungsbereich
|
||||
// =========================================================================
|
||||
const storageListHtml = allStorageLocations.size > 0
|
||||
? Array.from(allStorageLocations).map(s => `<li>${escHtml(s)}</li>`).join('')
|
||||
: '<li><em>Keine Speicherorte dokumentiert</em></li>'
|
||||
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">2. Geltungsbereich</div>
|
||||
<div class="section-body">
|
||||
<p>Dieses Loeschkonzept gilt fuer alle personenbezogenen Daten, die von <strong>${escHtml(orgName)}</strong>
|
||||
verarbeitet werden. Es umfasst <strong>${activePolicies.length}</strong> Loeschregeln fuer folgende Systeme und Speicherorte:</p>
|
||||
<ul style="margin: 8px 0 8px 24px;">
|
||||
${storageListHtml}
|
||||
</ul>
|
||||
<p>Saemtliche Verarbeitungstaetigkeiten, die im Verzeichnis von Verarbeitungstaetigkeiten (VVT)
|
||||
erfasst sind, werden durch dieses Loeschkonzept abgedeckt.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 3: Grundprinzipien
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">3. Grundprinzipien der Datenspeicherung</div>
|
||||
<div class="section-body">
|
||||
<div class="principle"><strong>Speicherbegrenzung:</strong> Personenbezogene Daten werden nur so lange gespeichert, wie es fuer den jeweiligen Verarbeitungszweck erforderlich ist (Art. 5 Abs. 1 lit. e DSGVO).</div>
|
||||
<div class="principle"><strong>3-Level-Loeschlogik:</strong> Die Loeschung folgt einer dreistufigen Priorisierung: (1) Zweckende, (2) gesetzliche Aufbewahrungspflichten, (3) Legal Hold — jeweils mit der laengsten Frist als massgeblich.</div>
|
||||
<div class="principle"><strong>Dokumentationspflicht:</strong> Jede Loeschregel ist dokumentiert mit Rechtsgrundlage, Frist, Loeschmethode und Verantwortlichkeit.</div>
|
||||
<div class="principle"><strong>Regelmaessige Ueberpruefung:</strong> Alle Loeschregeln werden im definierten Intervall ueberprueft und bei Bedarf angepasst.</div>
|
||||
<div class="principle"><strong>Datenschutz durch Technikgestaltung:</strong> Loeschmechanismen werden moeglichst automatisiert, um menschliche Fehler zu minimieren (Art. 25 DSGVO).</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 4: Loeschregeln-Uebersicht
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">4. Loeschregeln-Uebersicht</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${activePolicies.length} aktiven Loeschregeln:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>LF-Nr.</th>
|
||||
<th>Datenobjekt</th>
|
||||
<th>Loeschtrigger</th>
|
||||
<th>Aufbewahrungsfrist</th>
|
||||
<th>Loeschmethode</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
`
|
||||
for (const p of activePolicies) {
|
||||
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
|
||||
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
|
||||
const method = DELETION_METHOD_LABELS[p.deletionMethod]
|
||||
const statusLabel = STATUS_LABELS[p.status]
|
||||
const statusClass = p.status === 'ACTIVE' ? 'badge-active' : p.status === 'REVIEW_NEEDED' ? 'badge-review' : 'badge-draft'
|
||||
|
||||
html += ` <tr>
|
||||
<td>${escHtml(p.policyId)}</td>
|
||||
<td>${escHtml(p.dataObjectName)}</td>
|
||||
<td>${escHtml(trigger)}</td>
|
||||
<td>${escHtml(duration)}</td>
|
||||
<td>${escHtml(method)}</td>
|
||||
<td><span class="badge ${statusClass}">${escHtml(statusLabel)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 5: Detaillierte Loeschregeln
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">5. Detaillierte Loeschregeln</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
for (const p of activePolicies) {
|
||||
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
|
||||
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
|
||||
const method = DELETION_METHOD_LABELS[p.deletionMethod]
|
||||
const statusLabel = STATUS_LABELS[p.status]
|
||||
const driverLabel = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.label || p.retentionDriver : '-'
|
||||
const driverStatute = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.statute || '' : ''
|
||||
const locations = p.storageLocations.map(l => l.name || l.type).join(', ') || '-'
|
||||
const responsible = [p.responsiblePerson, p.responsibleRole].filter(s => s.trim()).join(' / ') || '-'
|
||||
const activeHolds = getActiveLegalHolds(p)
|
||||
|
||||
html += `
|
||||
<div class="policy-detail">
|
||||
<div class="policy-detail-header">
|
||||
<span>${escHtml(p.policyId)} — ${escHtml(p.dataObjectName)}</span>
|
||||
<span class="badge ${p.status === 'ACTIVE' ? 'badge-active' : 'badge-draft'}">${escHtml(statusLabel)}</span>
|
||||
</div>
|
||||
<div class="policy-detail-body">
|
||||
<table>
|
||||
<tr><th>Beschreibung</th><td>${escHtml(p.description || '-')}</td></tr>
|
||||
<tr><th>Betroffenengruppen</th><td>${escHtml(p.affectedGroups.join(', ') || '-')}</td></tr>
|
||||
<tr><th>Datenkategorien</th><td>${escHtml(p.dataCategories.join(', ') || '-')}</td></tr>
|
||||
<tr><th>Verarbeitungszweck</th><td>${escHtml(p.primaryPurpose || '-')}</td></tr>
|
||||
<tr><th>Loeschtrigger</th><td>${escHtml(trigger)}</td></tr>
|
||||
<tr><th>Aufbewahrungstreiber</th><td>${escHtml(driverLabel)}${driverStatute ? ` (${escHtml(driverStatute)})` : ''}</td></tr>
|
||||
<tr><th>Aufbewahrungsfrist</th><td>${escHtml(duration)}</td></tr>
|
||||
<tr><th>Startereignis</th><td>${escHtml(p.startEvent || '-')}</td></tr>
|
||||
<tr><th>Loeschmethode</th><td>${escHtml(method)}</td></tr>
|
||||
<tr><th>Loeschmethode (Detail)</th><td>${escHtml(p.deletionMethodDetail || '-')}</td></tr>
|
||||
<tr><th>Speicherorte</th><td>${escHtml(locations)}</td></tr>
|
||||
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
|
||||
<tr><th>Pruefintervall</th><td>${escHtml(REVIEW_INTERVAL_LABELS[p.reviewInterval] || p.reviewInterval)}</td></tr>
|
||||
${activeHolds.length > 0 ? `<tr><th>Aktive Legal Holds</th><td>${activeHolds.map(h => `${escHtml(h.reason)} (seit ${formatDateDE(h.startDate)})`).join('<br/>')}</td></tr>` : ''}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 6: VVT-Verknuepfung
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">6. VVT-Verknuepfung</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Tabelle zeigt die Verknuepfung zwischen Loeschregeln und Verarbeitungstaetigkeiten
|
||||
im VVT (Art. 30 DSGVO):</p>
|
||||
`
|
||||
if (vvtRefs.length > 0) {
|
||||
html += ` <table>
|
||||
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>VVT-Nr.</th><th>Verarbeitungstaetigkeit</th></tr>
|
||||
`
|
||||
for (const ref of vvtRefs) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(ref.policyName)}</td>
|
||||
<td>${escHtml(ref.policyId)}</td>
|
||||
<td>${escHtml(ref.vvtId)}</td>
|
||||
<td>${escHtml(ref.vvtName)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p><em>Noch keine VVT-Verknuepfungen dokumentiert. Verknuepfen Sie Ihre Loeschregeln
|
||||
mit den entsprechenden Verarbeitungstaetigkeiten im Editor-Tab.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 7: Auftragsverarbeiter mit Loeschpflichten
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">7. Auftragsverarbeiter mit Loeschpflichten</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Tabelle zeigt Loeschregeln, die mit Auftragsverarbeitern verknuepft sind.
|
||||
Diese Verknuepfungen stellen sicher, dass auch bei extern verarbeiteten Daten die Loeschpflichten
|
||||
eingehalten werden (Art. 28 DSGVO).</p>
|
||||
`
|
||||
if (vendorRefs.length > 0) {
|
||||
html += ` <table>
|
||||
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>Auftragsverarbeiter (ID)</th><th>Aufbewahrungsfrist</th></tr>
|
||||
`
|
||||
for (const ref of vendorRefs) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(ref.policyName)}</td>
|
||||
<td>${escHtml(ref.policyId)}</td>
|
||||
<td>${escHtml(ref.vendorId)}</td>
|
||||
<td>${escHtml(ref.duration)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p><em>Noch keine Auftragsverarbeiter mit Loeschregeln verknuepft. Verknuepfen Sie Ihre
|
||||
Loeschregeln mit den entsprechenden Auftragsverarbeitern im Editor-Tab.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 8: Legal Hold Verfahren
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">8. Legal Hold Verfahren</div>
|
||||
<div class="section-body">
|
||||
<p>Ein Legal Hold (Aufbewahrungspflicht aufgrund rechtlicher Verfahren) setzt die regulaere
|
||||
Loeschung aus. Betroffene Daten duerfen trotz abgelaufener Loeschfrist nicht geloescht werden,
|
||||
bis der Legal Hold aufgehoben wird.</p>
|
||||
<p><strong>Verfahrensschritte:</strong></p>
|
||||
<ol style="margin: 8px 0 8px 24px;">
|
||||
<li>Rechtsabteilung/DSB identifiziert betroffene Datenkategorien</li>
|
||||
<li>Legal Hold wird im System aktiviert (Status: Aktiv)</li>
|
||||
<li>Automatische Loeschung wird fuer betroffene Policies ausgesetzt</li>
|
||||
<li>Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist</li>
|
||||
<li>Nach Aufhebung: Regulaere Loeschfristen greifen wieder</li>
|
||||
</ol>
|
||||
`
|
||||
if (allActiveLegalHolds.length > 0) {
|
||||
html += ` <p><strong>Aktuell aktive Legal Holds (${allActiveLegalHolds.length}):</strong></p>
|
||||
<table>
|
||||
<tr><th>Datenobjekt</th><th>Grund</th><th>Rechtsgrundlage</th><th>Seit</th><th>Voraussichtlich bis</th></tr>
|
||||
`
|
||||
for (const { policy, hold } of allActiveLegalHolds) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(policy)}</td>
|
||||
<td>${escHtml(hold.reason)}</td>
|
||||
<td>${escHtml(hold.legalBasis)}</td>
|
||||
<td>${formatDateDE(hold.startDate)}</td>
|
||||
<td>${hold.expectedEndDate ? formatDateDE(hold.expectedEndDate) : 'Unbefristet'}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p><em>Derzeit sind keine aktiven Legal Holds vorhanden.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 9: Verantwortlichkeiten
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">9. Verantwortlichkeiten</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Rollenmatrix zeigt, welche Organisationseinheiten fuer welche Datenobjekte
|
||||
die Loeschverantwortung tragen:</p>
|
||||
<table>
|
||||
<tr><th>Rolle / Verantwortlich</th><th>Datenobjekte</th><th>Anzahl</th></tr>
|
||||
`
|
||||
for (const [role, objects] of roleMap.entries()) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(role)}</td>
|
||||
<td>${objects.map(o => escHtml(o)).join(', ')}</td>
|
||||
<td>${objects.length}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 10: Pruef- und Revisionszyklus
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">10. Pruef- und Revisionszyklus</div>
|
||||
<div class="section-body">
|
||||
<table>
|
||||
<tr><th>Eigenschaft</th><th>Wert</th></tr>
|
||||
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
|
||||
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
|
||||
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
|
||||
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.loeschkonzeptVersion)}</td></tr>
|
||||
</table>
|
||||
<p style="margin-top: 8px;">Bei jeder Pruefung wird das Loeschkonzept auf folgende Punkte ueberprueft:</p>
|
||||
<ul style="margin: 8px 0 8px 24px;">
|
||||
<li>Vollstaendigkeit aller Loeschregeln (neue Verarbeitungen erfasst?)</li>
|
||||
<li>Aktualitaet der gesetzlichen Aufbewahrungsfristen</li>
|
||||
<li>Wirksamkeit der technischen Loeschmechanismen</li>
|
||||
<li>Einhaltung der definierten Loeschfristen</li>
|
||||
<li>Angemessenheit der Verantwortlichkeiten</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 11: Compliance-Status
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">11. Compliance-Status</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (complianceResult) {
|
||||
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
|
||||
: complianceResult.score >= 75 ? 'score-good'
|
||||
: complianceResult.score >= 50 ? 'score-needs-work'
|
||||
: 'score-poor'
|
||||
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
|
||||
: complianceResult.score >= 75 ? 'Gut'
|
||||
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
|
||||
: 'Mangelhaft'
|
||||
|
||||
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
|
||||
<table style="margin-top: 12px;">
|
||||
<tr><th>Kennzahl</th><th>Wert</th></tr>
|
||||
<tr><td>Gepruefte Policies</td><td>${complianceResult.stats.total}</td></tr>
|
||||
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
|
||||
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
|
||||
</table>
|
||||
`
|
||||
if (complianceResult.issues.length > 0) {
|
||||
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
|
||||
<table>
|
||||
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
|
||||
`
|
||||
const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
|
||||
for (const sev of severityOrder) {
|
||||
const count = complianceResult.stats.bySeverity[sev]
|
||||
if (count === 0) continue
|
||||
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
|
||||
html += ` <tr>
|
||||
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
|
||||
<td>${count}</td>
|
||||
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Policies sind konform.</em></p>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
|
||||
Export-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 12: Aenderungshistorie
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">12. Aenderungshistorie</div>
|
||||
<div class="section-body">
|
||||
<table>
|
||||
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
|
||||
`
|
||||
if (revisions.length > 0) {
|
||||
for (const rev of revisions) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(rev.version)}</td>
|
||||
<td>${formatDateDE(rev.date)}</td>
|
||||
<td>${escHtml(rev.author)}</td>
|
||||
<td>${escHtml(rev.changes)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(orgHeader.loeschkonzeptVersion)}</td>
|
||||
<td>${today}</td>
|
||||
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
|
||||
<td>Erstversion des Loeschkonzepts</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Footer
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="page-footer">
|
||||
<span>Loeschkonzept — ${escHtml(orgName)}</span>
|
||||
<span>Stand: ${today} | Version ${escHtml(orgHeader.loeschkonzeptVersion)}</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERNAL HELPERS
|
||||
// =============================================================================
|
||||
|
||||
function escHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function formatDateDE(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '-'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return '-'
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Loeschfristen Profiling — utility functions
|
||||
// Data (types + PROFILING_STEPS) lives in loeschfristen-profiling-data.ts
|
||||
// =============================================================================
|
||||
// Loeschfristen Module - Profiling Wizard
|
||||
// 4-Step Profiling (16 Fragen) zur Generierung von Baseline-Loeschrichtlinien
|
||||
// =============================================================================
|
||||
|
||||
import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types'
|
||||
import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog'
|
||||
@@ -8,6 +10,244 @@ import type { ProfilingAnswer, ProfilingStepId, ProfilingResult } from './loesch
|
||||
// Re-export types + data so existing imports work unchanged
|
||||
export { type ProfilingStepId, type ProfilingQuestion, type ProfilingAnswer, type ProfilingStep, type ProfilingResult, PROFILING_STEPS } from './loeschfristen-profiling-data'
|
||||
|
||||
export type ProfilingStepId = 'organization' | 'data-categories' | 'systems' | 'special'
|
||||
|
||||
export interface ProfilingQuestion {
|
||||
id: string
|
||||
step: ProfilingStepId
|
||||
question: string // German
|
||||
helpText?: string
|
||||
type: 'single' | 'multi' | 'boolean' | 'number'
|
||||
options?: { value: string; label: string }[]
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export interface ProfilingAnswer {
|
||||
questionId: string
|
||||
value: string | string[] | boolean | number
|
||||
}
|
||||
|
||||
export interface ProfilingStep {
|
||||
id: ProfilingStepId
|
||||
title: string
|
||||
description: string
|
||||
questions: ProfilingQuestion[]
|
||||
}
|
||||
|
||||
export interface ProfilingResult {
|
||||
matchedTemplates: BaselineTemplate[]
|
||||
generatedPolicies: LoeschfristPolicy[]
|
||||
additionalStorageLocations: StorageLocation[]
|
||||
hasLegalHoldRequirement: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROFILING STEPS (4 Steps, 16 Questions)
|
||||
// =============================================================================
|
||||
|
||||
export const PROFILING_STEPS: ProfilingStep[] = [
|
||||
// =========================================================================
|
||||
// Step 1: Organisation (4 Fragen)
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Organisation',
|
||||
description: 'Allgemeine Informationen zu Ihrem Unternehmen, um branchenspezifische Loeschfristen zu ermitteln.',
|
||||
questions: [
|
||||
{
|
||||
id: 'org-branche',
|
||||
step: 'organization',
|
||||
question: 'In welcher Branche ist Ihr Unternehmen taetig?',
|
||||
helpText: 'Die Branche bestimmt, welche branchenspezifischen Aufbewahrungspflichten relevant sind.',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'it-software', label: 'IT / Software' },
|
||||
{ value: 'handel', label: 'Handel' },
|
||||
{ value: 'dienstleistung', label: 'Dienstleistung' },
|
||||
{ value: 'gesundheitswesen', label: 'Gesundheitswesen' },
|
||||
{ value: 'bildung', label: 'Bildung' },
|
||||
{ value: 'fertigung-industrie', label: 'Fertigung / Industrie' },
|
||||
{ value: 'finanzwesen', label: 'Finanzwesen' },
|
||||
{ value: 'oeffentlicher-sektor', label: 'Oeffentlicher Sektor' },
|
||||
{ value: 'sonstige', label: 'Sonstige' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'org-mitarbeiter',
|
||||
step: 'organization',
|
||||
question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?',
|
||||
helpText: 'Die Unternehmensgroesse beeinflusst den Umfang der erforderlichen Loeschkonzepte.',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: '<10', label: 'Weniger als 10' },
|
||||
{ value: '10-49', label: '10 bis 49' },
|
||||
{ value: '50-249', label: '50 bis 249' },
|
||||
{ value: '250+', label: '250 und mehr' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'org-geschaeftsmodell',
|
||||
step: 'organization',
|
||||
question: 'Welches Geschaeftsmodell verfolgen Sie?',
|
||||
helpText: 'B2B und B2C haben unterschiedliche Anforderungen an die Datenhaltung.',
|
||||
type: 'single',
|
||||
options: [
|
||||
{ value: 'b2b', label: 'B2B (Geschaeftskunden)' },
|
||||
{ value: 'b2c', label: 'B2C (Endkunden)' },
|
||||
{ value: 'beides', label: 'Beides (B2B und B2C)' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'org-website',
|
||||
step: 'organization',
|
||||
question: 'Betreiben Sie eine Website oder Online-Praesenz?',
|
||||
helpText: 'Websites erzeugen Webserver-Logs und erfordern Cookie-Consent-Verwaltung.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Step 2: Datenkategorien (5 Fragen)
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'data-categories',
|
||||
title: 'Datenkategorien',
|
||||
description: 'Welche Arten personenbezogener Daten verarbeiten Sie? Dies bestimmt die relevanten Aufbewahrungsfristen.',
|
||||
questions: [
|
||||
{
|
||||
id: 'data-hr',
|
||||
step: 'data-categories',
|
||||
question: 'Verarbeiten Sie HR-/Personaldaten (Personalakten, Gehaltsabrechnungen, Zeiterfassung)?',
|
||||
helpText: 'Personalakten unterliegen umfangreichen gesetzlichen Aufbewahrungspflichten (bis zu 10 Jahre).',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'data-buchhaltung',
|
||||
step: 'data-categories',
|
||||
question: 'Fuehren Sie eine Buchhaltung mit Finanzdaten (Rechnungen, Belege, Steuererklarungen)?',
|
||||
helpText: 'Buchhaltungsunterlagen muessen gemaess HGB und AO bis zu 10 Jahre aufbewahrt werden.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'data-vertraege',
|
||||
step: 'data-categories',
|
||||
question: 'Verwalten Sie Vertraege mit Kunden oder Lieferanten?',
|
||||
helpText: 'Vertragsunterlagen und Geschaeftsbriefe haben spezifische Aufbewahrungspflichten.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'data-marketing',
|
||||
step: 'data-categories',
|
||||
question: 'Betreiben Sie Marketing-Aktivitaeten (Newsletter, CRM-Kampagnen)?',
|
||||
helpText: 'Marketing-Einwilligungen und Kontakthistorien muessen dokumentiert und verwaltet werden.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'data-video',
|
||||
step: 'data-categories',
|
||||
question: 'Setzen Sie Videoueberwachung ein?',
|
||||
helpText: 'Videoueberwachungsdaten haben besonders kurze Loeschfristen (in der Regel 72 Stunden).',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Step 3: Systeme (4 Fragen)
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'systems',
|
||||
title: 'Systeme & Infrastruktur',
|
||||
description: 'Welche IT-Systeme und Infrastruktur nutzen Sie? Dies beeinflusst die Speicherorte in Ihrem Loeschkonzept.',
|
||||
questions: [
|
||||
{
|
||||
id: 'sys-cloud',
|
||||
step: 'systems',
|
||||
question: 'Nutzen Sie Cloud-Dienste zur Datenspeicherung oder -verarbeitung?',
|
||||
helpText: 'Cloud-Speicherorte muessen in den Loeschrichtlinien als separate Speicherorte dokumentiert werden.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'sys-backup',
|
||||
step: 'systems',
|
||||
question: 'Haben Sie Backup-Systeme im Einsatz?',
|
||||
helpText: 'Backups erfordern eine eigene Loeschstrategie, da Daten dort nach der primaeren Loeschung weiter existieren koennen.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'sys-erp',
|
||||
step: 'systems',
|
||||
question: 'Setzen Sie ein ERP- oder CRM-System ein?',
|
||||
helpText: 'ERP-/CRM-Systeme sind haeufig zentrale Speicherorte fuer Kunden- und Geschaeftsdaten.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'sys-zutritt',
|
||||
step: 'systems',
|
||||
question: 'Nutzen Sie ein Zutrittskontrollsystem?',
|
||||
helpText: 'Zutrittskontrollsysteme erzeugen Protokolle, die personenbezogene Daten enthalten und einer Loeschfrist unterliegen.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// Step 4: Spezielle Anforderungen (3 Fragen)
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'special',
|
||||
title: 'Spezielle Anforderungen',
|
||||
description: 'Gibt es besondere rechtliche oder organisatorische Anforderungen, die Ihr Loeschkonzept beeinflussen?',
|
||||
questions: [
|
||||
{
|
||||
id: 'special-legal-hold',
|
||||
step: 'special',
|
||||
question: 'Gibt es Legal-Hold-Anforderungen (z.B. laufende Rechtsstreitigkeiten, behoerdliche Untersuchungen)?',
|
||||
helpText: 'Bei einem Legal Hold muessen betroffene Daten trotz abgelaufener Loeschfristen aufbewahrt werden.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'special-archivierung',
|
||||
step: 'special',
|
||||
question: 'Benoetigen Sie eine Langzeitarchivierung von Dokumenten?',
|
||||
helpText: 'Langzeitarchivierung kann ueber die gesetzlichen Mindestfristen hinausgehen und erfordert eine gesonderte Rechtfertigung.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'special-gesundheit',
|
||||
step: 'special',
|
||||
question: 'Verarbeiten Sie Gesundheitsdaten (z.B. Krankmeldungen, Arbeitsmedizin)?',
|
||||
helpText: 'Gesundheitsdaten sind besonders schuetzenswerte Daten nach Art. 9 DSGVO und unterliegen strengeren Anforderungen.',
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Retrieve the value of a specific answer by question ID.
|
||||
*/
|
||||
export function getAnswerValue(answers: ProfilingAnswer[], questionId: string): unknown {
|
||||
const answer = answers.find(a => a.questionId === questionId)
|
||||
return answer?.value ?? undefined
|
||||
@@ -108,6 +348,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
|
||||
matchedTemplateIds.add('zeiterfassung')
|
||||
matchedTemplateIds.add('bewerbungsunterlagen')
|
||||
matchedTemplateIds.add('krankmeldungen')
|
||||
matchedTemplateIds.add('schulungsnachweise')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -126,6 +367,8 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
|
||||
matchedTemplateIds.add('vertraege')
|
||||
matchedTemplateIds.add('geschaeftsbriefe')
|
||||
matchedTemplateIds.add('kundenstammdaten')
|
||||
matchedTemplateIds.add('kundenreklamationen')
|
||||
matchedTemplateIds.add('lieferantenbewertungen')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -135,6 +378,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
|
||||
matchedTemplateIds.add('newsletter-einwilligungen')
|
||||
matchedTemplateIds.add('crm-kontakthistorie')
|
||||
matchedTemplateIds.add('cookie-consent-logs')
|
||||
matchedTemplateIds.add('social-media-daten')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -152,6 +396,20 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
|
||||
matchedTemplateIds.add('cookie-consent-logs')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Cloud (sys-cloud = true) → E-Mail-Archivierung
|
||||
// -------------------------------------------------------------------------
|
||||
if (getBool('sys-cloud')) {
|
||||
matchedTemplateIds.add('email-archivierung')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Zutritt (sys-zutritt = true)
|
||||
// -------------------------------------------------------------------------
|
||||
if (getBool('sys-zutritt')) {
|
||||
matchedTemplateIds.add('zutrittsprotokolle')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ERP/CRM (sys-erp = true)
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -173,6 +431,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
|
||||
if (getBool('special-gesundheit')) {
|
||||
// Ensure krankmeldungen is included even without full HR data
|
||||
matchedTemplateIds.add('krankmeldungen')
|
||||
matchedTemplateIds.add('betriebsarzt-doku')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -91,6 +91,7 @@ export interface LoeschfristPolicy {
|
||||
responsiblePerson: string
|
||||
releaseProcess: string
|
||||
linkedVVTActivityIds: string[]
|
||||
linkedVendorIds: string[]
|
||||
// Status & Review
|
||||
status: PolicyStatus
|
||||
lastReviewDate: string
|
||||
@@ -272,6 +273,7 @@ export function createEmptyPolicy(): LoeschfristPolicy {
|
||||
responsiblePerson: '',
|
||||
releaseProcess: '',
|
||||
linkedVVTActivityIds: [],
|
||||
linkedVendorIds: [],
|
||||
status: 'DRAFT',
|
||||
lastReviewDate: now,
|
||||
nextReviewDate: nextYear.toISOString(),
|
||||
|
||||
395
admin-compliance/lib/sdk/obligations-compliance.ts
Normal file
395
admin-compliance/lib/sdk/obligations-compliance.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
// =============================================================================
|
||||
// Obligations Module - Compliance Check Engine
|
||||
// Prueft Pflichten auf Vollstaendigkeit, Konsistenz und Auditfaehigkeit
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface Obligation {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
source: string
|
||||
source_article: string
|
||||
deadline: string | null
|
||||
status: 'pending' | 'in-progress' | 'completed' | 'overdue'
|
||||
priority: 'critical' | 'high' | 'medium' | 'low'
|
||||
responsible: string
|
||||
linked_systems: string[]
|
||||
linked_vendor_ids?: string[]
|
||||
assessment_id?: string
|
||||
rule_code?: string
|
||||
notes?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
evidence?: string[]
|
||||
review_date?: string
|
||||
category?: string
|
||||
}
|
||||
|
||||
export type ObligationComplianceIssueType =
|
||||
| 'MISSING_RESPONSIBLE'
|
||||
| 'OVERDUE_DEADLINE'
|
||||
| 'MISSING_EVIDENCE'
|
||||
| 'MISSING_DESCRIPTION'
|
||||
| 'NO_LEGAL_REFERENCE'
|
||||
| 'INCOMPLETE_REGULATION'
|
||||
| 'HIGH_PRIORITY_NOT_STARTED'
|
||||
| 'STALE_PENDING'
|
||||
| 'MISSING_LINKED_SYSTEMS'
|
||||
| 'NO_REVIEW_PROCESS'
|
||||
| 'CRITICAL_WITHOUT_EVIDENCE'
|
||||
| 'MISSING_VENDOR_LINK'
|
||||
|
||||
export type ObligationComplianceIssueSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
|
||||
|
||||
export interface ObligationComplianceIssue {
|
||||
type: ObligationComplianceIssueType
|
||||
severity: ObligationComplianceIssueSeverity
|
||||
message: string
|
||||
affectedObligations: string[]
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
export interface ObligationComplianceCheckResult {
|
||||
score: number
|
||||
issues: ObligationComplianceIssue[]
|
||||
summary: { total: number; critical: number; high: number; medium: number; low: number }
|
||||
checkedAt: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export const OBLIGATION_SEVERITY_LABELS_DE: Record<ObligationComplianceIssueSeverity, string> = {
|
||||
CRITICAL: 'Kritisch',
|
||||
HIGH: 'Hoch',
|
||||
MEDIUM: 'Mittel',
|
||||
LOW: 'Niedrig',
|
||||
}
|
||||
|
||||
export const OBLIGATION_SEVERITY_COLORS: Record<ObligationComplianceIssueSeverity, string> = {
|
||||
CRITICAL: '#dc2626',
|
||||
HIGH: '#ea580c',
|
||||
MEDIUM: '#d97706',
|
||||
LOW: '#6b7280',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPERS
|
||||
// =============================================================================
|
||||
|
||||
function daysBetween(date: Date, now: Date): number {
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PER-OBLIGATION CHECKS (1-5, 9, 11)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check 1: MISSING_RESPONSIBLE (MEDIUM)
|
||||
* Pflicht ohne verantwortliche Person/Abteilung.
|
||||
*/
|
||||
function checkMissingResponsible(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o => !o.responsible || o.responsible.trim() === '')
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'MISSING_RESPONSIBLE',
|
||||
severity: 'MEDIUM',
|
||||
message: `${affected.length} Pflicht(en) ohne verantwortliche Person oder Abteilung. Ohne klare Zustaendigkeit koennen Pflichten nicht zuverlaessig umgesetzt werden.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Weisen Sie jeder Pflicht eine verantwortliche Person oder Abteilung zu.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 2: OVERDUE_DEADLINE (HIGH)
|
||||
* Pflicht mit Deadline in der Vergangenheit + Status != completed.
|
||||
*/
|
||||
function checkOverdueDeadline(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const now = new Date()
|
||||
const affected = obligations.filter(o => {
|
||||
if (!o.deadline || o.status === 'completed') return false
|
||||
return new Date(o.deadline) < now
|
||||
})
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'OVERDUE_DEADLINE',
|
||||
severity: 'HIGH',
|
||||
message: `${affected.length} Pflicht(en) mit ueberschrittener Frist. Ueberfaellige Pflichten stellen ein Compliance-Risiko dar und koennen zu Bussgeldern fuehren.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Bearbeiten Sie ueberfaellige Pflichten umgehend oder passen Sie die Fristen an.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 3: MISSING_EVIDENCE (HIGH)
|
||||
* Completed-Pflicht ohne Evidence.
|
||||
*/
|
||||
function checkMissingEvidence(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o =>
|
||||
o.status === 'completed' && (!o.evidence || o.evidence.length === 0)
|
||||
)
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'MISSING_EVIDENCE',
|
||||
severity: 'HIGH',
|
||||
message: `${affected.length} abgeschlossene Pflicht(en) ohne Nachweis. Ohne Nachweise ist die Erfuellung im Audit nicht belegbar.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Hinterlegen Sie Nachweisdokumente fuer alle abgeschlossenen Pflichten.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 4: MISSING_DESCRIPTION (MEDIUM)
|
||||
* Pflicht ohne Beschreibung.
|
||||
*/
|
||||
function checkMissingDescription(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o => !o.description || o.description.trim() === '')
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'MISSING_DESCRIPTION',
|
||||
severity: 'MEDIUM',
|
||||
message: `${affected.length} Pflicht(en) ohne Beschreibung. Eine fehlende Beschreibung erschwert die Nachvollziehbarkeit und Umsetzung.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Ergaenzen Sie eine Beschreibung fuer jede Pflicht, die den Inhalt und die Anforderungen erlaeutert.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 5: NO_LEGAL_REFERENCE (HIGH)
|
||||
* Pflicht ohne source_article (kein Artikel-Bezug).
|
||||
*/
|
||||
function checkNoLegalReference(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o => !o.source_article || o.source_article.trim() === '')
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'NO_LEGAL_REFERENCE',
|
||||
severity: 'HIGH',
|
||||
message: `${affected.length} Pflicht(en) ohne Artikel-/Paragraphen-Referenz. Ohne Rechtsbezug ist die Pflicht im Audit nicht nachvollziehbar.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Ergaenzen Sie die Rechtsgrundlage (z.B. Art. 32 DSGVO) fuer jede Pflicht.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 9: MISSING_LINKED_SYSTEMS (MEDIUM)
|
||||
* Pflicht ohne verknuepfte Systeme/Verarbeitungen.
|
||||
*/
|
||||
function checkMissingLinkedSystems(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o => !o.linked_systems || o.linked_systems.length === 0)
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'MISSING_LINKED_SYSTEMS',
|
||||
severity: 'MEDIUM',
|
||||
message: `${affected.length} Pflicht(en) ohne verknuepfte Systeme oder Verarbeitungstaetigkeiten. Ohne Systemzuordnung fehlt der operative Bezug.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Ordnen Sie jeder Pflicht die betroffenen IT-Systeme oder Verarbeitungstaetigkeiten zu.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 11: CRITICAL_WITHOUT_EVIDENCE (CRITICAL)
|
||||
* Critical-Pflicht ohne Evidence.
|
||||
*/
|
||||
function checkCriticalWithoutEvidence(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o =>
|
||||
o.priority === 'critical' && (!o.evidence || o.evidence.length === 0)
|
||||
)
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'CRITICAL_WITHOUT_EVIDENCE',
|
||||
severity: 'CRITICAL',
|
||||
message: `${affected.length} kritische Pflicht(en) ohne Nachweis. Kritische Pflichten erfordern zwingend eine Dokumentation der Erfuellung.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Hinterlegen Sie umgehend Nachweise fuer alle kritischen Pflichten.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 12: MISSING_VENDOR_LINK (MEDIUM)
|
||||
* Art.-28-Pflicht ohne verknuepften Auftragsverarbeiter.
|
||||
*/
|
||||
function checkMissingVendorLink(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const affected = obligations.filter(o =>
|
||||
o.source_article?.includes('Art. 28') &&
|
||||
(!o.linked_vendor_ids || o.linked_vendor_ids.length === 0)
|
||||
)
|
||||
if (affected.length === 0) return null
|
||||
return {
|
||||
type: 'MISSING_VENDOR_LINK',
|
||||
severity: 'MEDIUM',
|
||||
message: `${affected.length} Art.-28-Pflicht(en) ohne verknuepften Auftragsverarbeiter.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Verknuepfen Sie Art.-28-Pflichten mit den betroffenen Auftragsverarbeitern im Vendor Register.',
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AGGREGATE CHECKS (6-8, 10)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check 6: INCOMPLETE_REGULATION (HIGH)
|
||||
* Regulierung, bei der alle Pflichten pending/overdue sind.
|
||||
*/
|
||||
function checkIncompleteRegulation(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const bySource = new Map<string, Obligation[]>()
|
||||
for (const o of obligations) {
|
||||
const src = o.source || 'Unbekannt'
|
||||
if (!bySource.has(src)) bySource.set(src, [])
|
||||
bySource.get(src)!.push(o)
|
||||
}
|
||||
|
||||
const incompleteRegs: string[] = []
|
||||
const affectedIds: string[] = []
|
||||
|
||||
for (const [source, obls] of bySource.entries()) {
|
||||
if (obls.length < 2) continue // Skip single-obligation regulations
|
||||
const allStalled = obls.every(o => o.status === 'pending' || o.status === 'overdue')
|
||||
if (allStalled) {
|
||||
incompleteRegs.push(source)
|
||||
affectedIds.push(...obls.map(o => o.id))
|
||||
}
|
||||
}
|
||||
|
||||
if (incompleteRegs.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'INCOMPLETE_REGULATION',
|
||||
severity: 'HIGH',
|
||||
message: `${incompleteRegs.length} Regulierung(en) vollstaendig ohne Umsetzung: ${incompleteRegs.join(', ')}. Alle Pflichten sind ausstehend oder ueberfaellig.`,
|
||||
affectedObligations: affectedIds,
|
||||
recommendation: 'Beginnen Sie mit der Umsetzung der wichtigsten Pflichten in den betroffenen Regulierungen.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 7: HIGH_PRIORITY_NOT_STARTED (CRITICAL)
|
||||
* Critical/High-Pflicht seit > 30 Tagen pending.
|
||||
*/
|
||||
function checkHighPriorityNotStarted(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const now = new Date()
|
||||
const affected = obligations.filter(o => {
|
||||
if (o.status !== 'pending') return false
|
||||
if (o.priority !== 'critical' && o.priority !== 'high') return false
|
||||
if (!o.created_at) return false
|
||||
return daysBetween(new Date(o.created_at), now) > 30
|
||||
})
|
||||
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'HIGH_PRIORITY_NOT_STARTED',
|
||||
severity: 'CRITICAL',
|
||||
message: `${affected.length} hochprioritaere Pflicht(en) seit ueber 30 Tagen nicht begonnen. Dies deutet auf organisatorische Blockaden oder fehlende Priorisierung hin.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Starten Sie umgehend mit der Bearbeitung dieser kritischen/hohen Pflichten und erstellen Sie einen Umsetzungsplan.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 8: STALE_PENDING (LOW)
|
||||
* Pflicht seit > 90 Tagen pending.
|
||||
*/
|
||||
function checkStalePending(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
const now = new Date()
|
||||
const affected = obligations.filter(o => {
|
||||
if (o.status !== 'pending') return false
|
||||
if (!o.created_at) return false
|
||||
return daysBetween(new Date(o.created_at), now) > 90
|
||||
})
|
||||
|
||||
if (affected.length === 0) return null
|
||||
|
||||
return {
|
||||
type: 'STALE_PENDING',
|
||||
severity: 'LOW',
|
||||
message: `${affected.length} Pflicht(en) seit ueber 90 Tagen ausstehend. Langfristig unbearbeitete Pflichten sollten priorisiert oder als nicht relevant markiert werden.`,
|
||||
affectedObligations: affected.map(o => o.id),
|
||||
recommendation: 'Pruefen Sie, ob die Pflichten weiterhin relevant sind, und setzen Sie Prioritaeten fuer die Umsetzung.',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 10: NO_REVIEW_PROCESS (MEDIUM)
|
||||
* Keine einzige Pflicht hat review_date.
|
||||
*/
|
||||
function checkNoReviewProcess(obligations: Obligation[]): ObligationComplianceIssue | null {
|
||||
if (obligations.length === 0) return null
|
||||
const hasAnyReview = obligations.some(o => o.review_date)
|
||||
if (hasAnyReview) return null
|
||||
|
||||
return {
|
||||
type: 'NO_REVIEW_PROCESS',
|
||||
severity: 'MEDIUM',
|
||||
message: 'Keine Pflicht hat ein Pruefungsdatum (review_date). Ohne regelmaessige Ueberpruefung ist die Aktualitaet des Pflichtenregisters nicht gewaehrleistet.',
|
||||
affectedObligations: [],
|
||||
recommendation: 'Fuehren Sie ein Pruefintervall ein und setzen Sie review_date fuer alle Pflichten.',
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN COMPLIANCE CHECK
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fuehrt einen vollstaendigen Compliance-Check ueber alle Pflichten durch.
|
||||
*/
|
||||
export function runObligationComplianceCheck(obligations: Obligation[]): ObligationComplianceCheckResult {
|
||||
const issues: ObligationComplianceIssue[] = []
|
||||
|
||||
const checks = [
|
||||
checkMissingResponsible(obligations),
|
||||
checkOverdueDeadline(obligations),
|
||||
checkMissingEvidence(obligations),
|
||||
checkMissingDescription(obligations),
|
||||
checkNoLegalReference(obligations),
|
||||
checkIncompleteRegulation(obligations),
|
||||
checkHighPriorityNotStarted(obligations),
|
||||
checkStalePending(obligations),
|
||||
checkMissingLinkedSystems(obligations),
|
||||
checkNoReviewProcess(obligations),
|
||||
checkCriticalWithoutEvidence(obligations),
|
||||
checkMissingVendorLink(obligations),
|
||||
]
|
||||
|
||||
for (const issue of checks) {
|
||||
if (issue !== null) {
|
||||
issues.push(issue)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate score
|
||||
const summary = { total: issues.length, critical: 0, high: 0, medium: 0, low: 0 }
|
||||
for (const issue of issues) {
|
||||
switch (issue.severity) {
|
||||
case 'CRITICAL': summary.critical++; break
|
||||
case 'HIGH': summary.high++; break
|
||||
case 'MEDIUM': summary.medium++; break
|
||||
case 'LOW': summary.low++; break
|
||||
}
|
||||
}
|
||||
|
||||
const rawScore = 100 - (summary.critical * 15 + summary.high * 10 + summary.medium * 5 + summary.low * 2)
|
||||
const score = Math.max(0, rawScore)
|
||||
|
||||
return {
|
||||
score,
|
||||
issues,
|
||||
summary,
|
||||
checkedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
915
admin-compliance/lib/sdk/obligations-document.ts
Normal file
915
admin-compliance/lib/sdk/obligations-document.ts
Normal file
@@ -0,0 +1,915 @@
|
||||
// =============================================================================
|
||||
// Obligations Module - Pflichtenregister Document Generator
|
||||
// Generates a printable, audit-ready HTML document for the obligation register
|
||||
// =============================================================================
|
||||
|
||||
import type { Obligation, ObligationComplianceCheckResult, ObligationComplianceIssueSeverity } from './obligations-compliance'
|
||||
import { OBLIGATION_SEVERITY_LABELS_DE, OBLIGATION_SEVERITY_COLORS } from './obligations-compliance'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ObligationDocumentOrgHeader {
|
||||
organizationName: string
|
||||
industry: string
|
||||
dpoName: string
|
||||
dpoContact: string
|
||||
responsiblePerson: string
|
||||
legalDepartment: string
|
||||
documentVersion: string
|
||||
lastReviewDate: string
|
||||
nextReviewDate: string
|
||||
reviewInterval: string
|
||||
}
|
||||
|
||||
export interface ObligationDocumentRevision {
|
||||
version: string
|
||||
date: string
|
||||
author: string
|
||||
changes: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEFAULTS
|
||||
// =============================================================================
|
||||
|
||||
export function createDefaultObligationDocumentOrgHeader(): ObligationDocumentOrgHeader {
|
||||
const now = new Date()
|
||||
const nextYear = new Date()
|
||||
nextYear.setFullYear(nextYear.getFullYear() + 1)
|
||||
|
||||
return {
|
||||
organizationName: '',
|
||||
industry: '',
|
||||
dpoName: '',
|
||||
dpoContact: '',
|
||||
responsiblePerson: '',
|
||||
legalDepartment: '',
|
||||
documentVersion: '1.0',
|
||||
lastReviewDate: now.toISOString().split('T')[0],
|
||||
nextReviewDate: nextYear.toISOString().split('T')[0],
|
||||
reviewInterval: 'Jaehrlich',
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATUS & PRIORITY LABELS
|
||||
// =============================================================================
|
||||
|
||||
const STATUS_LABELS_DE: Record<string, string> = {
|
||||
'pending': 'Ausstehend',
|
||||
'in-progress': 'In Bearbeitung',
|
||||
'completed': 'Abgeschlossen',
|
||||
'overdue': 'Ueberfaellig',
|
||||
}
|
||||
|
||||
const STATUS_BADGE_CLASSES: Record<string, string> = {
|
||||
'pending': 'badge-draft',
|
||||
'in-progress': 'badge-review',
|
||||
'completed': 'badge-active',
|
||||
'overdue': 'badge-critical',
|
||||
}
|
||||
|
||||
const PRIORITY_LABELS_DE: Record<string, string> = {
|
||||
critical: 'Kritisch',
|
||||
high: 'Hoch',
|
||||
medium: 'Mittel',
|
||||
low: 'Niedrig',
|
||||
}
|
||||
|
||||
const PRIORITY_BADGE_CLASSES: Record<string, string> = {
|
||||
critical: 'badge-critical',
|
||||
high: 'badge-high',
|
||||
medium: 'badge-medium',
|
||||
low: 'badge-low',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTML DOCUMENT BUILDER
|
||||
// =============================================================================
|
||||
|
||||
export function buildObligationDocumentHtml(
|
||||
obligations: Obligation[],
|
||||
orgHeader: ObligationDocumentOrgHeader,
|
||||
complianceResult: ObligationComplianceCheckResult | null,
|
||||
revisions: ObligationDocumentRevision[]
|
||||
): string {
|
||||
const today = new Date().toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
const orgName = orgHeader.organizationName || 'Organisation'
|
||||
|
||||
// Group obligations by source (regulation)
|
||||
const bySource = new Map<string, Obligation[]>()
|
||||
for (const o of obligations) {
|
||||
const src = o.source || 'Sonstig'
|
||||
if (!bySource.has(src)) bySource.set(src, [])
|
||||
bySource.get(src)!.push(o)
|
||||
}
|
||||
|
||||
// Build role map
|
||||
const roleMap = new Map<string, Obligation[]>()
|
||||
for (const o of obligations) {
|
||||
const role = o.responsible || 'Nicht zugewiesen'
|
||||
if (!roleMap.has(role)) roleMap.set(role, [])
|
||||
roleMap.get(role)!.push(o)
|
||||
}
|
||||
|
||||
// Distinct sources
|
||||
const distinctSources = Array.from(bySource.keys()).sort()
|
||||
|
||||
// =========================================================================
|
||||
// HTML Template
|
||||
// =========================================================================
|
||||
|
||||
let html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Pflichtenregister — ${escHtml(orgName)}</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 10pt;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Cover */
|
||||
.cover {
|
||||
min-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
page-break-after: always;
|
||||
}
|
||||
.cover h1 {
|
||||
font-size: 28pt;
|
||||
color: #5b21b6;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cover .subtitle {
|
||||
font-size: 14pt;
|
||||
color: #7c3aed;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.cover .org-info {
|
||||
background: #f5f3ff;
|
||||
border: 1px solid #ddd6fe;
|
||||
border-radius: 8px;
|
||||
padding: 24px 40px;
|
||||
text-align: left;
|
||||
width: 400px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.cover .org-info div { margin-bottom: 6px; }
|
||||
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
|
||||
.cover .legal-ref {
|
||||
font-size: 9pt;
|
||||
color: #64748b;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* TOC */
|
||||
.toc {
|
||||
page-break-after: always;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.toc h2 {
|
||||
font-size: 18pt;
|
||||
color: #5b21b6;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #5b21b6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.toc-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dotted #cbd5e1;
|
||||
font-size: 10pt;
|
||||
}
|
||||
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.section-header {
|
||||
font-size: 14pt;
|
||||
color: #5b21b6;
|
||||
font-weight: 700;
|
||||
margin: 30px 0 12px 0;
|
||||
border-bottom: 2px solid #ddd6fe;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.section-body { margin-bottom: 16px; }
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 16px 0;
|
||||
font-size: 9pt;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f5f3ff;
|
||||
color: #5b21b6;
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
tr:nth-child(even) td { background: #faf5ff; }
|
||||
|
||||
/* Detail cards */
|
||||
.policy-detail {
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.policy-detail-header {
|
||||
background: #f5f3ff;
|
||||
padding: 8px 12px;
|
||||
font-weight: 700;
|
||||
color: #5b21b6;
|
||||
border-bottom: 1px solid #ddd6fe;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.policy-detail-body { padding: 0; }
|
||||
.policy-detail-body table { margin: 0; }
|
||||
.policy-detail-body th { width: 200px; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-active { background: #dcfce7; color: #166534; }
|
||||
.badge-draft { background: #f3f4f6; color: #374151; }
|
||||
.badge-review { background: #fef9c3; color: #854d0e; }
|
||||
.badge-critical { background: #fecaca; color: #991b1b; }
|
||||
.badge-high { background: #fed7aa; color: #9a3412; }
|
||||
.badge-medium { background: #fef3c7; color: #92400e; }
|
||||
.badge-low { background: #f3f4f6; color: #4b5563; }
|
||||
|
||||
/* Principles */
|
||||
.principle {
|
||||
margin-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.principle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #7c3aed;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.principle strong { color: #5b21b6; }
|
||||
|
||||
/* Score */
|
||||
.score-box {
|
||||
display: inline-block;
|
||||
padding: 4px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.score-excellent { background: #dcfce7; color: #166534; }
|
||||
.score-good { background: #dbeafe; color: #1e40af; }
|
||||
.score-needs-work { background: #fef3c7; color: #92400e; }
|
||||
.score-poor { background: #fecaca; color: #991b1b; }
|
||||
|
||||
/* Footer */
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px 18mm;
|
||||
font-size: 7.5pt;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.no-print { display: none !important; }
|
||||
.page-break { page-break-after: always; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 0: Cover Page
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="cover">
|
||||
<h1>Pflichtenregister</h1>
|
||||
<div class="subtitle">Regulatorische Pflichten — DSGVO, AI Act, NIS2 und weitere</div>
|
||||
<div class="org-info">
|
||||
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
|
||||
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
|
||||
${orgHeader.dpoName ? `<div><span class="label">DSB:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
|
||||
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
|
||||
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
|
||||
${orgHeader.legalDepartment ? `<div><span class="label">Rechtsabteilung:</span> ${escHtml(orgHeader.legalDepartment)}</div>` : ''}
|
||||
</div>
|
||||
<div class="legal-ref">
|
||||
Version ${escHtml(orgHeader.documentVersion)} | Stand: ${today}<br/>
|
||||
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
|
||||
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Table of Contents
|
||||
// =========================================================================
|
||||
const sections = [
|
||||
'Ziel und Zweck',
|
||||
'Geltungsbereich',
|
||||
'Methodik',
|
||||
'Regulatorische Grundlagen',
|
||||
'Pflichtenuebersicht',
|
||||
'Detaillierte Pflichten',
|
||||
'Verantwortlichkeiten',
|
||||
'Fristen und Termine',
|
||||
'Nachweisverzeichnis',
|
||||
'Compliance-Status',
|
||||
'Aenderungshistorie',
|
||||
]
|
||||
|
||||
html += `
|
||||
<div class="toc">
|
||||
<h2>Inhaltsverzeichnis</h2>
|
||||
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 1: Ziel und Zweck
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">1. Ziel und Zweck</div>
|
||||
<div class="section-body">
|
||||
<p>Dieses Pflichtenregister dokumentiert alle regulatorischen Pflichten, denen
|
||||
<strong>${escHtml(orgName)}</strong> unterliegt. Es dient der systematischen Erfassung,
|
||||
Ueberwachung und Nachverfolgung aller Compliance-Anforderungen aus den anwendbaren
|
||||
Regulierungen.</p>
|
||||
<p style="margin-top: 8px;">Das Register erfuellt folgende Zwecke:</p>
|
||||
<ul style="margin: 8px 0 8px 24px;">
|
||||
<li>Vollstaendige Erfassung aller anwendbaren regulatorischen Pflichten</li>
|
||||
<li>Zuordnung von Verantwortlichkeiten und Fristen</li>
|
||||
<li>Nachverfolgung des Umsetzungsstatus</li>
|
||||
<li>Dokumentation von Nachweisen fuer Audits</li>
|
||||
<li>Identifikation von Compliance-Luecken und Handlungsbedarf</li>
|
||||
</ul>
|
||||
<table>
|
||||
<tr><th>Rechtsrahmen</th><th>Relevanz</th></tr>
|
||||
<tr><td><strong>DSGVO (EU) 2016/679</strong></td><td>Datenschutz-Grundverordnung — Kernregulierung fuer personenbezogene Daten</td></tr>
|
||||
<tr><td><strong>AI Act (EU) 2024/1689</strong></td><td>KI-Verordnung — Anforderungen an KI-Systeme nach Risikoklasse</td></tr>
|
||||
<tr><td><strong>NIS2 (EU) 2022/2555</strong></td><td>Netzwerk- und Informationssicherheit — Cybersicherheitspflichten</td></tr>
|
||||
<tr><td><strong>BDSG</strong></td><td>Bundesdatenschutzgesetz — Nationale Ergaenzung zur DSGVO</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 2: Geltungsbereich
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">2. Geltungsbereich</div>
|
||||
<div class="section-body">
|
||||
<p>Dieses Pflichtenregister gilt fuer alle Geschaeftsprozesse und IT-Systeme von
|
||||
<strong>${escHtml(orgName)}</strong>${orgHeader.industry ? ` (Branche: ${escHtml(orgHeader.industry)})` : ''}.</p>
|
||||
<p style="margin-top: 8px;">Anwendbare Regulierungen:</p>
|
||||
<table>
|
||||
<tr><th>Regulierung</th><th>Anzahl Pflichten</th><th>Status</th></tr>
|
||||
`
|
||||
for (const [source, obls] of bySource.entries()) {
|
||||
const completed = obls.filter(o => o.status === 'completed').length
|
||||
const pct = obls.length > 0 ? Math.round((completed / obls.length) * 100) : 0
|
||||
html += ` <tr>
|
||||
<td>${escHtml(source)}</td>
|
||||
<td>${obls.length}</td>
|
||||
<td>${completed}/${obls.length} abgeschlossen (${pct}%)</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
<p>Insgesamt umfasst dieses Register <strong>${obligations.length}</strong> Pflichten aus
|
||||
<strong>${distinctSources.length}</strong> Regulierungen.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 3: Methodik
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">3. Methodik</div>
|
||||
<div class="section-body">
|
||||
<p>Die Identifikation und Bewertung der Pflichten erfolgt in drei Schritten:</p>
|
||||
<div class="principle"><strong>Pflicht-Identifikation:</strong> Systematische Analyse aller anwendbaren Regulierungen und Extraktion der einzelnen Pflichten mit Artikel-Referenz, Beschreibung und Zielgruppe.</div>
|
||||
<div class="principle"><strong>Bewertung und Priorisierung:</strong> Jede Pflicht wird nach Prioritaet (kritisch, hoch, mittel, niedrig) und Dringlichkeit (Frist) bewertet. Die Bewertung basiert auf dem Risikopotenzial bei Nichterfuellung.</div>
|
||||
<div class="principle"><strong>Ueberwachung und Nachverfolgung:</strong> Regelmaessige Pruefung des Umsetzungsstatus, Aktualisierung der Fristen und Dokumentation von Nachweisen.</div>
|
||||
<p style="margin-top: 12px;">Die Pflichten werden ueber einen automatisierten Compliance-Check geprueft, der
|
||||
11 Kriterien umfasst (siehe Abschnitt 10: Compliance-Status).</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 4: Regulatorische Grundlagen
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">4. Regulatorische Grundlagen</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Tabelle zeigt die regulatorischen Grundlagen mit Artikelzahl und Umsetzungsstatus:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Regulierung</th>
|
||||
<th>Pflichten</th>
|
||||
<th>Kritisch</th>
|
||||
<th>Hoch</th>
|
||||
<th>Mittel</th>
|
||||
<th>Niedrig</th>
|
||||
<th>Abgeschlossen</th>
|
||||
</tr>
|
||||
`
|
||||
for (const [source, obls] of bySource.entries()) {
|
||||
const critical = obls.filter(o => o.priority === 'critical').length
|
||||
const high = obls.filter(o => o.priority === 'high').length
|
||||
const medium = obls.filter(o => o.priority === 'medium').length
|
||||
const low = obls.filter(o => o.priority === 'low').length
|
||||
const completed = obls.filter(o => o.status === 'completed').length
|
||||
|
||||
html += ` <tr>
|
||||
<td><strong>${escHtml(source)}</strong></td>
|
||||
<td>${obls.length}</td>
|
||||
<td>${critical}</td>
|
||||
<td>${high}</td>
|
||||
<td>${medium}</td>
|
||||
<td>${low}</td>
|
||||
<td>${completed}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
// Totals row
|
||||
const totalCritical = obligations.filter(o => o.priority === 'critical').length
|
||||
const totalHigh = obligations.filter(o => o.priority === 'high').length
|
||||
const totalMedium = obligations.filter(o => o.priority === 'medium').length
|
||||
const totalLow = obligations.filter(o => o.priority === 'low').length
|
||||
const totalCompleted = obligations.filter(o => o.status === 'completed').length
|
||||
|
||||
html += ` <tr style="font-weight: 700; background: #f5f3ff;">
|
||||
<td>Gesamt</td>
|
||||
<td>${obligations.length}</td>
|
||||
<td>${totalCritical}</td>
|
||||
<td>${totalHigh}</td>
|
||||
<td>${totalMedium}</td>
|
||||
<td>${totalLow}</td>
|
||||
<td>${totalCompleted}</td>
|
||||
</tr>
|
||||
`
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 5: Pflichtenuebersicht
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">5. Pflichtenuebersicht</div>
|
||||
<div class="section-body">
|
||||
<p>Uebersicht aller ${obligations.length} Pflichten nach Regulierung und Status:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Regulierung</th>
|
||||
<th>Gesamt</th>
|
||||
<th>Ausstehend</th>
|
||||
<th>In Bearbeitung</th>
|
||||
<th>Abgeschlossen</th>
|
||||
<th>Ueberfaellig</th>
|
||||
</tr>
|
||||
`
|
||||
for (const [source, obls] of bySource.entries()) {
|
||||
const pending = obls.filter(o => o.status === 'pending').length
|
||||
const inProgress = obls.filter(o => o.status === 'in-progress').length
|
||||
const completed = obls.filter(o => o.status === 'completed').length
|
||||
const overdue = obls.filter(o => o.status === 'overdue').length
|
||||
|
||||
html += ` <tr>
|
||||
<td>${escHtml(source)}</td>
|
||||
<td>${obls.length}</td>
|
||||
<td>${pending}</td>
|
||||
<td>${inProgress}</td>
|
||||
<td>${completed}</td>
|
||||
<td>${overdue > 0 ? `<span class="badge badge-critical">${overdue}</span>` : '0'}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 6: Detaillierte Pflichten
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">6. Detaillierte Pflichten</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
|
||||
for (const [source, obls] of bySource.entries()) {
|
||||
// Sort by priority (critical first) then by title
|
||||
const priorityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 }
|
||||
const sorted = [...obls].sort((a, b) => {
|
||||
const pa = priorityOrder[a.priority] ?? 2
|
||||
const pb = priorityOrder[b.priority] ?? 2
|
||||
if (pa !== pb) return pa - pb
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
|
||||
html += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(source)} <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${sorted.length} Pflichten)</span></h3>
|
||||
`
|
||||
|
||||
for (const o of sorted) {
|
||||
const statusLabel = STATUS_LABELS_DE[o.status] || o.status
|
||||
const statusBadge = STATUS_BADGE_CLASSES[o.status] || 'badge-draft'
|
||||
const priorityLabel = PRIORITY_LABELS_DE[o.priority] || o.priority
|
||||
const priorityBadge = PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'
|
||||
const deadlineStr = o.deadline ? formatDateDE(o.deadline) : '—'
|
||||
const evidenceStr = o.evidence && o.evidence.length > 0
|
||||
? o.evidence.map(e => escHtml(e)).join(', ')
|
||||
: '<em style="color: #d97706;">Kein Nachweis</em>'
|
||||
const systemsStr = o.linked_systems && o.linked_systems.length > 0
|
||||
? o.linked_systems.map(s => escHtml(s)).join(', ')
|
||||
: '—'
|
||||
|
||||
html += `
|
||||
<div class="policy-detail">
|
||||
<div class="policy-detail-header">
|
||||
<span>${escHtml(o.title)}</span>
|
||||
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
|
||||
</div>
|
||||
<div class="policy-detail-body">
|
||||
<table>
|
||||
<tr><th>Rechtsquelle</th><td>${escHtml(o.source)} ${escHtml(o.source_article || '')}</td></tr>
|
||||
<tr><th>Beschreibung</th><td>${escHtml(o.description || '—')}</td></tr>
|
||||
<tr><th>Prioritaet</th><td><span class="badge ${priorityBadge}">${escHtml(priorityLabel)}</span></td></tr>
|
||||
<tr><th>Status</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
|
||||
<tr><th>Verantwortlich</th><td>${escHtml(o.responsible || '—')}</td></tr>
|
||||
<tr><th>Frist</th><td>${deadlineStr}</td></tr>
|
||||
<tr><th>Nachweise</th><td>${evidenceStr}</td></tr>
|
||||
<tr><th>Betroffene Systeme</th><td>${systemsStr}</td></tr>
|
||||
${o.linked_vendor_ids && o.linked_vendor_ids.length > 0 ? `<tr><th>Auftragsverarbeiter</th><td>${o.linked_vendor_ids.map(id => escHtml(id)).join(', ')}</td></tr>` : ''}
|
||||
${o.notes ? `<tr><th>Notizen</th><td>${escHtml(o.notes)}</td></tr>` : ''}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 7: Verantwortlichkeiten
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">7. Verantwortlichkeiten</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Pflichten
|
||||
die Umsetzungsverantwortung tragen:</p>
|
||||
<table>
|
||||
<tr><th>Verantwortlich</th><th>Pflichten</th><th>Anzahl</th><th>Davon offen</th></tr>
|
||||
`
|
||||
for (const [role, obls] of roleMap.entries()) {
|
||||
const openCount = obls.filter(o => o.status !== 'completed').length
|
||||
const titles = obls.slice(0, 5).map(o => escHtml(o.title))
|
||||
const suffix = obls.length > 5 ? `, ... (+${obls.length - 5})` : ''
|
||||
html += ` <tr>
|
||||
<td>${escHtml(role)}</td>
|
||||
<td>${titles.join('; ')}${suffix}</td>
|
||||
<td>${obls.length}</td>
|
||||
<td>${openCount}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 8: Fristen und Termine
|
||||
// =========================================================================
|
||||
const now = new Date()
|
||||
const withDeadline = obligations
|
||||
.filter(o => o.deadline && o.status !== 'completed')
|
||||
.sort((a, b) => new Date(a.deadline!).getTime() - new Date(b.deadline!).getTime())
|
||||
|
||||
const overdue = withDeadline.filter(o => new Date(o.deadline!) < now)
|
||||
const upcoming = withDeadline.filter(o => new Date(o.deadline!) >= now)
|
||||
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">8. Fristen und Termine</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (overdue.length > 0) {
|
||||
html += ` <h4 style="color: #dc2626; margin-bottom: 8px;">Ueberfaellige Pflichten (${overdue.length})</h4>
|
||||
<table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Tage ueberfaellig</th><th>Prioritaet</th></tr>
|
||||
`
|
||||
for (const o of overdue) {
|
||||
const days = daysBetween(new Date(o.deadline!), now)
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${formatDateDE(o.deadline)}</td>
|
||||
<td><span class="badge badge-critical">${days} Tage</span></td>
|
||||
<td><span class="badge ${PRIORITY_BADGE_CLASSES[o.priority] || 'badge-draft'}">${escHtml(PRIORITY_LABELS_DE[o.priority] || o.priority)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (upcoming.length > 0) {
|
||||
html += ` <h4 style="color: #5b21b6; margin: 16px 0 8px 0;">Anstehende Fristen (${upcoming.length})</h4>
|
||||
<table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Frist</th><th>Verbleibend</th><th>Verantwortlich</th></tr>
|
||||
`
|
||||
for (const o of upcoming.slice(0, 20)) {
|
||||
const days = daysBetween(now, new Date(o.deadline!))
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${formatDateDE(o.deadline)}</td>
|
||||
<td>${days} Tage</td>
|
||||
<td>${escHtml(o.responsible || '—')}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
if (upcoming.length > 20) {
|
||||
html += ` <tr><td colspan="5" style="text-align: center; color: #64748b;">... und ${upcoming.length - 20} weitere</td></tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (withDeadline.length === 0) {
|
||||
html += ` <p><em>Keine offenen Pflichten mit Fristen vorhanden.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 9: Nachweisverzeichnis
|
||||
// =========================================================================
|
||||
const withEvidence = obligations.filter(o => o.evidence && o.evidence.length > 0)
|
||||
const withoutEvidence = obligations.filter(o => !o.evidence || o.evidence.length === 0)
|
||||
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">9. Nachweisverzeichnis</div>
|
||||
<div class="section-body">
|
||||
<p>${withEvidence.length} von ${obligations.length} Pflichten haben Nachweise hinterlegt.</p>
|
||||
`
|
||||
if (withEvidence.length > 0) {
|
||||
html += ` <table>
|
||||
<tr><th>Pflicht</th><th>Regulierung</th><th>Nachweise</th><th>Status</th></tr>
|
||||
`
|
||||
for (const o of withEvidence) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(o.title)}</td>
|
||||
<td>${escHtml(o.source)}</td>
|
||||
<td>${o.evidence!.map(e => escHtml(e)).join(', ')}</td>
|
||||
<td><span class="badge ${STATUS_BADGE_CLASSES[o.status] || 'badge-draft'}">${escHtml(STATUS_LABELS_DE[o.status] || o.status)}</span></td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
}
|
||||
|
||||
if (withoutEvidence.length > 0) {
|
||||
html += ` <p style="margin-top: 12px;"><strong>Pflichten ohne Nachweise (${withoutEvidence.length}):</strong></p>
|
||||
<ul style="margin: 4px 0 8px 24px; font-size: 9pt; color: #d97706;">
|
||||
`
|
||||
for (const o of withoutEvidence.slice(0, 15)) {
|
||||
html += ` <li>${escHtml(o.title)} (${escHtml(o.source)})</li>
|
||||
`
|
||||
}
|
||||
if (withoutEvidence.length > 15) {
|
||||
html += ` <li>... und ${withoutEvidence.length - 15} weitere</li>
|
||||
`
|
||||
}
|
||||
html += ` </ul>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 10: Compliance-Status
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">10. Compliance-Status</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (complianceResult) {
|
||||
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
|
||||
: complianceResult.score >= 75 ? 'score-good'
|
||||
: complianceResult.score >= 50 ? 'score-needs-work'
|
||||
: 'score-poor'
|
||||
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
|
||||
: complianceResult.score >= 75 ? 'Gut'
|
||||
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
|
||||
: 'Mangelhaft'
|
||||
|
||||
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
|
||||
<table style="margin-top: 12px;">
|
||||
<tr><th>Kennzahl</th><th>Wert</th></tr>
|
||||
<tr><td>Geprueft am</td><td>${formatDateDE(complianceResult.checkedAt)}</td></tr>
|
||||
<tr><td>Befunde gesamt</td><td>${complianceResult.summary.total}</td></tr>
|
||||
<tr><td>Kritisch</td><td>${complianceResult.summary.critical}</td></tr>
|
||||
<tr><td>Hoch</td><td>${complianceResult.summary.high}</td></tr>
|
||||
<tr><td>Mittel</td><td>${complianceResult.summary.medium}</td></tr>
|
||||
<tr><td>Niedrig</td><td>${complianceResult.summary.low}</td></tr>
|
||||
</table>
|
||||
`
|
||||
if (complianceResult.issues.length > 0) {
|
||||
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
|
||||
<table>
|
||||
<tr><th>Schweregrad</th><th>Befund</th><th>Betroffene Pflichten</th><th>Empfehlung</th></tr>
|
||||
`
|
||||
const severityOrder: ObligationComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
|
||||
for (const sev of severityOrder) {
|
||||
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
|
||||
for (const issue of issuesForSev) {
|
||||
html += ` <tr>
|
||||
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${OBLIGATION_SEVERITY_COLORS[sev]}">${OBLIGATION_SEVERITY_LABELS_DE[sev]}</span></td>
|
||||
<td>${escHtml(issue.message)}</td>
|
||||
<td>${issue.affectedObligations.length > 0 ? issue.affectedObligations.length + ' Pflicht(en)' : '—'}</td>
|
||||
<td>${escHtml(issue.recommendation)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Pflichten sind konform.</em></p>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
|
||||
Pflichtenregister-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 11: Aenderungshistorie
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">11. Aenderungshistorie</div>
|
||||
<div class="section-body">
|
||||
<table>
|
||||
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
|
||||
`
|
||||
if (revisions.length > 0) {
|
||||
for (const rev of revisions) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(rev.version)}</td>
|
||||
<td>${formatDateDE(rev.date)}</td>
|
||||
<td>${escHtml(rev.author)}</td>
|
||||
<td>${escHtml(rev.changes)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(orgHeader.documentVersion)}</td>
|
||||
<td>${today}</td>
|
||||
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '—')}</td>
|
||||
<td>Erstversion des Pflichtenregisters</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Footer
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="page-footer">
|
||||
<span>Pflichtenregister — ${escHtml(orgName)}</span>
|
||||
<span>Stand: ${today} | Version ${escHtml(orgHeader.documentVersion)}</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERNAL HELPERS
|
||||
// =============================================================================
|
||||
|
||||
function escHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function formatDateDE(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '—'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return '—'
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
return '—'
|
||||
}
|
||||
}
|
||||
|
||||
function daysBetween(earlier: Date, later: Date): number {
|
||||
const diffMs = later.getTime() - earlier.getTime()
|
||||
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
553
admin-compliance/lib/sdk/tom-compliance.ts
Normal file
553
admin-compliance/lib/sdk/tom-compliance.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
// =============================================================================
|
||||
// TOM Module - Compliance Check Engine
|
||||
// Prueft Technische und Organisatorische Massnahmen auf Vollstaendigkeit,
|
||||
// Konsistenz und DSGVO-Konformitaet (Art. 32 DSGVO)
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
TOMGeneratorState,
|
||||
DerivedTOM,
|
||||
RiskProfile,
|
||||
DataProfile,
|
||||
ControlCategory,
|
||||
ImplementationStatus,
|
||||
} from './tom-generator/types'
|
||||
|
||||
import { getControlById, getControlsByCategory, getAllCategories } from './tom-generator/controls/loader'
|
||||
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export type TOMComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
||||
|
||||
export type TOMComplianceIssueType =
|
||||
| 'MISSING_RESPONSIBLE'
|
||||
| 'OVERDUE_REVIEW'
|
||||
| 'MISSING_EVIDENCE'
|
||||
| 'INCOMPLETE_CATEGORY'
|
||||
| 'NO_ENCRYPTION_MEASURES'
|
||||
| 'NO_PSEUDONYMIZATION'
|
||||
| 'MISSING_AVAILABILITY'
|
||||
| 'NO_REVIEW_PROCESS'
|
||||
| 'UNCOVERED_SDM_GOAL'
|
||||
| 'HIGH_RISK_WITHOUT_MEASURES'
|
||||
| 'STALE_NOT_IMPLEMENTED'
|
||||
|
||||
export interface TOMComplianceIssue {
|
||||
id: string
|
||||
controlId: string
|
||||
controlName: string
|
||||
type: TOMComplianceIssueType
|
||||
severity: TOMComplianceIssueSeverity
|
||||
title: string
|
||||
description: string
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
export interface TOMComplianceCheckResult {
|
||||
issues: TOMComplianceIssue[]
|
||||
score: number // 0-100
|
||||
stats: {
|
||||
total: number
|
||||
passed: number
|
||||
failed: number
|
||||
bySeverity: Record<TOMComplianceIssueSeverity, number>
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export const TOM_SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
|
||||
CRITICAL: 'Kritisch',
|
||||
HIGH: 'Hoch',
|
||||
MEDIUM: 'Mittel',
|
||||
LOW: 'Niedrig',
|
||||
}
|
||||
|
||||
export const TOM_SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
|
||||
CRITICAL: '#dc2626',
|
||||
HIGH: '#ea580c',
|
||||
MEDIUM: '#d97706',
|
||||
LOW: '#6b7280',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPERS
|
||||
// =============================================================================
|
||||
|
||||
let issueCounter = 0
|
||||
|
||||
function createIssueId(): string {
|
||||
issueCounter++
|
||||
return `TCI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
|
||||
}
|
||||
|
||||
function createIssue(
|
||||
controlId: string,
|
||||
controlName: string,
|
||||
type: TOMComplianceIssueType,
|
||||
severity: TOMComplianceIssueSeverity,
|
||||
title: string,
|
||||
description: string,
|
||||
recommendation: string
|
||||
): TOMComplianceIssue {
|
||||
return { id: createIssueId(), controlId, controlName, type, severity, title, description, recommendation }
|
||||
}
|
||||
|
||||
function daysBetween(date: Date, now: Date): number {
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PER-TOM CHECKS (1-3, 11)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check 1: MISSING_RESPONSIBLE (MEDIUM)
|
||||
* REQUIRED TOM without responsiblePerson AND responsibleDepartment.
|
||||
*/
|
||||
function checkMissingResponsible(tom: DerivedTOM): TOMComplianceIssue | null {
|
||||
if (tom.applicability !== 'REQUIRED') return null
|
||||
|
||||
if (!tom.responsiblePerson && !tom.responsibleDepartment) {
|
||||
return createIssue(
|
||||
tom.controlId,
|
||||
tom.name,
|
||||
'MISSING_RESPONSIBLE',
|
||||
'MEDIUM',
|
||||
'Keine verantwortliche Person/Abteilung',
|
||||
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, hat aber weder eine verantwortliche Person noch eine verantwortliche Abteilung zugewiesen. Ohne klare Verantwortlichkeit kann die Massnahme nicht zuverlaessig umgesetzt und gepflegt werden.`,
|
||||
'Weisen Sie eine verantwortliche Person oder Abteilung zu, die fuer die Umsetzung und regelmaessige Pruefung dieser Massnahme zustaendig ist.'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 2: OVERDUE_REVIEW (MEDIUM)
|
||||
* TOM with reviewDate in the past.
|
||||
*/
|
||||
function checkOverdueReview(tom: DerivedTOM): TOMComplianceIssue | null {
|
||||
if (!tom.reviewDate) return null
|
||||
|
||||
const reviewDate = new Date(tom.reviewDate)
|
||||
const now = new Date()
|
||||
|
||||
if (reviewDate < now) {
|
||||
const overdueDays = daysBetween(reviewDate, now)
|
||||
return createIssue(
|
||||
tom.controlId,
|
||||
tom.name,
|
||||
'OVERDUE_REVIEW',
|
||||
'MEDIUM',
|
||||
'Ueberfaellige Pruefung',
|
||||
`Die TOM "${tom.name}" haette am ${reviewDate.toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig. Gemaess Art. 32 Abs. 1 lit. d DSGVO ist eine regelmaessige Ueberpruefung der Wirksamkeit von TOMs erforderlich.`,
|
||||
'Fuehren Sie umgehend eine Wirksamkeitspruefung dieser Massnahme durch und aktualisieren Sie das naechste Pruefungsdatum.'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 3: MISSING_EVIDENCE (HIGH)
|
||||
* IMPLEMENTED TOM where linkedEvidence is empty but the control has evidenceRequirements.
|
||||
*/
|
||||
function checkMissingEvidence(tom: DerivedTOM): TOMComplianceIssue | null {
|
||||
if (tom.implementationStatus !== 'IMPLEMENTED') return null
|
||||
if (tom.linkedEvidence.length > 0) return null
|
||||
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control || control.evidenceRequirements.length === 0) return null
|
||||
|
||||
return createIssue(
|
||||
tom.controlId,
|
||||
tom.name,
|
||||
'MISSING_EVIDENCE',
|
||||
'HIGH',
|
||||
'Kein Nachweis hinterlegt',
|
||||
`Die TOM "${tom.name}" ist als IMPLEMENTED markiert, hat aber keine verknuepften Nachweisdokumente. Der Control erfordert ${control.evidenceRequirements.length} Nachweis(e): ${control.evidenceRequirements.join(', ')}. Ohne Nachweise ist die Umsetzung nicht auditfaehig.`,
|
||||
'Laden Sie die erforderlichen Nachweisdokumente hoch und verknuepfen Sie sie mit dieser Massnahme.'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 11: STALE_NOT_IMPLEMENTED (LOW)
|
||||
* REQUIRED TOM that has been NOT_IMPLEMENTED for >90 days.
|
||||
* Uses implementationDate === null and state.createdAt / state.updatedAt as reference.
|
||||
*/
|
||||
function checkStaleNotImplemented(tom: DerivedTOM, state: TOMGeneratorState): TOMComplianceIssue | null {
|
||||
if (tom.applicability !== 'REQUIRED') return null
|
||||
if (tom.implementationStatus !== 'NOT_IMPLEMENTED') return null
|
||||
if (tom.implementationDate !== null) return null
|
||||
|
||||
const referenceDate = state.createdAt ? new Date(state.createdAt) : (state.updatedAt ? new Date(state.updatedAt) : null)
|
||||
if (!referenceDate) return null
|
||||
|
||||
const ageInDays = daysBetween(referenceDate, new Date())
|
||||
if (ageInDays <= 90) return null
|
||||
|
||||
return createIssue(
|
||||
tom.controlId,
|
||||
tom.name,
|
||||
'STALE_NOT_IMPLEMENTED',
|
||||
'LOW',
|
||||
'Langfristig nicht umgesetzte Pflichtmassnahme',
|
||||
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, aber seit ${ageInDays} Tagen nicht umgesetzt. Pflichtmassnahmen, die laenger als 90 Tage nicht implementiert werden, deuten auf organisatorische Blockaden oder unzureichende Priorisierung hin.`,
|
||||
'Pruefen Sie, ob die Massnahme weiterhin erforderlich ist, und erstellen Sie einen konkreten Umsetzungsplan mit Verantwortlichkeiten und Fristen.'
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AGGREGATE CHECKS (4-10)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check 4: INCOMPLETE_CATEGORY (HIGH)
|
||||
* Category where ALL applicable (REQUIRED) controls are NOT_IMPLEMENTED.
|
||||
*/
|
||||
function checkIncompleteCategory(toms: DerivedTOM[]): TOMComplianceIssue[] {
|
||||
const issues: TOMComplianceIssue[] = []
|
||||
|
||||
// Group applicable TOMs by category
|
||||
const categoryMap = new Map<ControlCategory, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
if (!categoryMap.has(category)) {
|
||||
categoryMap.set(category, [])
|
||||
}
|
||||
categoryMap.get(category)!.push(tom)
|
||||
}
|
||||
|
||||
for (const [category, categoryToms] of Array.from(categoryMap.entries())) {
|
||||
// Only check categories that have at least one REQUIRED control
|
||||
const requiredToms = categoryToms.filter((t: DerivedTOM) => t.applicability === 'REQUIRED')
|
||||
if (requiredToms.length === 0) continue
|
||||
|
||||
const allNotImplemented = requiredToms.every((t: DerivedTOM) => t.implementationStatus === 'NOT_IMPLEMENTED')
|
||||
if (allNotImplemented) {
|
||||
issues.push(
|
||||
createIssue(
|
||||
category,
|
||||
category,
|
||||
'INCOMPLETE_CATEGORY',
|
||||
'HIGH',
|
||||
`Kategorie "${category}" vollstaendig ohne Umsetzung`,
|
||||
`Alle ${requiredToms.length} Pflichtmassnahme(n) in der Kategorie "${category}" sind nicht umgesetzt. Eine vollstaendig unabgedeckte Kategorie stellt eine erhebliche Luecke im TOM-Konzept dar.`,
|
||||
`Setzen Sie mindestens die wichtigsten Massnahmen in der Kategorie "${category}" um, um eine Grundabdeckung sicherzustellen.`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 5: NO_ENCRYPTION_MEASURES (CRITICAL)
|
||||
* No ENCRYPTION control with status IMPLEMENTED.
|
||||
*/
|
||||
function checkNoEncryption(toms: DerivedTOM[]): TOMComplianceIssue | null {
|
||||
const hasImplementedEncryption = toms.some((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === 'ENCRYPTION' && tom.implementationStatus === 'IMPLEMENTED'
|
||||
})
|
||||
|
||||
if (!hasImplementedEncryption) {
|
||||
return createIssue(
|
||||
'ENCRYPTION',
|
||||
'Verschluesselung',
|
||||
'NO_ENCRYPTION_MEASURES',
|
||||
'CRITICAL',
|
||||
'Keine Verschluesselungsmassnahmen umgesetzt',
|
||||
'Es ist keine einzige Verschluesselungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. a DSGVO nennt Verschluesselung explizit als geeignete technische Massnahme. Ohne Verschluesselung sind personenbezogene Daten bei Zugriff oder Verlust ungeschuetzt.',
|
||||
'Implementieren Sie umgehend Verschluesselungsmassnahmen fuer Daten im Ruhezustand (Encryption at Rest) und waehrend der Uebertragung (Encryption in Transit).'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 6: NO_PSEUDONYMIZATION (MEDIUM)
|
||||
* DataProfile has special categories (Art. 9) but no PSEUDONYMIZATION control implemented.
|
||||
*/
|
||||
function checkNoPseudonymization(toms: DerivedTOM[], dataProfile: DataProfile | null): TOMComplianceIssue | null {
|
||||
if (!dataProfile || !dataProfile.hasSpecialCategories) return null
|
||||
|
||||
const hasImplementedPseudonymization = toms.some((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === 'PSEUDONYMIZATION' && tom.implementationStatus === 'IMPLEMENTED'
|
||||
})
|
||||
|
||||
if (!hasImplementedPseudonymization) {
|
||||
return createIssue(
|
||||
'PSEUDONYMIZATION',
|
||||
'Pseudonymisierung',
|
||||
'NO_PSEUDONYMIZATION',
|
||||
'MEDIUM',
|
||||
'Keine Pseudonymisierung bei besonderen Datenkategorien',
|
||||
'Das Datenprofil enthaelt besondere Kategorien personenbezogener Daten (Art. 9 DSGVO), aber keine Pseudonymisierungsmassnahme ist umgesetzt. Art. 32 Abs. 1 lit. a DSGVO empfiehlt Pseudonymisierung ausdruecklich als Schutzmassnahme.',
|
||||
'Implementieren Sie Pseudonymisierungsmassnahmen fuer die Verarbeitung besonderer Datenkategorien, um das Risiko fuer betroffene Personen zu minimieren.'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 7: MISSING_AVAILABILITY (HIGH)
|
||||
* No AVAILABILITY or RECOVERY control implemented AND no DR plan in securityProfile.
|
||||
*/
|
||||
function checkMissingAvailability(toms: DerivedTOM[], state: TOMGeneratorState): TOMComplianceIssue | null {
|
||||
const hasAvailabilityOrRecovery = toms.some((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return (
|
||||
(control?.category === 'AVAILABILITY' || control?.category === 'RECOVERY') &&
|
||||
tom.implementationStatus === 'IMPLEMENTED'
|
||||
)
|
||||
})
|
||||
|
||||
const hasDRPlan = state.securityProfile?.hasDRPlan ?? false
|
||||
|
||||
if (!hasAvailabilityOrRecovery && !hasDRPlan) {
|
||||
return createIssue(
|
||||
'AVAILABILITY',
|
||||
'Verfuegbarkeit / Wiederherstellbarkeit',
|
||||
'MISSING_AVAILABILITY',
|
||||
'HIGH',
|
||||
'Keine Verfuegbarkeits- oder Wiederherstellungsmassnahmen',
|
||||
'Weder Verfuegbarkeits- noch Wiederherstellungsmassnahmen sind umgesetzt, und es existiert kein Disaster-Recovery-Plan im Security-Profil. Art. 32 Abs. 1 lit. b und c DSGVO verlangen die Faehigkeit zur raschen Wiederherstellung der Verfuegbarkeit personenbezogener Daten.',
|
||||
'Implementieren Sie Backup-Konzepte, Redundanzloesungen und einen Disaster-Recovery-Plan, um die Verfuegbarkeit und Wiederherstellbarkeit sicherzustellen.'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 8: NO_REVIEW_PROCESS (MEDIUM)
|
||||
* No REVIEW control implemented (Art. 32 Abs. 1 lit. d requires periodic review).
|
||||
*/
|
||||
function checkNoReviewProcess(toms: DerivedTOM[]): TOMComplianceIssue | null {
|
||||
const hasImplementedReview = toms.some((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === 'REVIEW' && tom.implementationStatus === 'IMPLEMENTED'
|
||||
})
|
||||
|
||||
if (!hasImplementedReview) {
|
||||
return createIssue(
|
||||
'REVIEW',
|
||||
'Ueberpruefung & Bewertung',
|
||||
'NO_REVIEW_PROCESS',
|
||||
'MEDIUM',
|
||||
'Kein Verfahren zur regelmaessigen Ueberpruefung',
|
||||
'Es ist keine Ueberpruefungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. d DSGVO verlangt ein Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen.',
|
||||
'Implementieren Sie einen regelmaessigen Review-Prozess (z.B. quartalsweise TOM-Audits, jaehrliche Wirksamkeitspruefung) und dokumentieren Sie die Ergebnisse.'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 9: UNCOVERED_SDM_GOAL (HIGH)
|
||||
* SDM goal with 0% coverage — no implemented control maps to it via SDM_CATEGORY_MAPPING.
|
||||
*/
|
||||
function checkUncoveredSDMGoal(toms: DerivedTOM[]): TOMComplianceIssue[] {
|
||||
const issues: TOMComplianceIssue[] = []
|
||||
|
||||
// Build reverse mapping: SDM goal -> ControlCategories that cover it
|
||||
const sdmGoals = [
|
||||
'Verfuegbarkeit',
|
||||
'Integritaet',
|
||||
'Vertraulichkeit',
|
||||
'Nichtverkettung',
|
||||
'Intervenierbarkeit',
|
||||
'Transparenz',
|
||||
'Datenminimierung',
|
||||
] as const
|
||||
|
||||
const goalToCategoriesMap = new Map<string, ControlCategory[]>()
|
||||
for (const goal of sdmGoals) {
|
||||
goalToCategoriesMap.set(goal, [])
|
||||
}
|
||||
|
||||
// Build reverse lookup from SDM_CATEGORY_MAPPING
|
||||
for (const [category, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
|
||||
for (const goal of goals) {
|
||||
const existing = goalToCategoriesMap.get(goal)
|
||||
if (existing) {
|
||||
existing.push(category as ControlCategory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect implemented categories
|
||||
const implementedCategories = new Set<ControlCategory>()
|
||||
for (const tom of toms) {
|
||||
if (tom.implementationStatus !== 'IMPLEMENTED') continue
|
||||
const control = getControlById(tom.controlId)
|
||||
if (control) {
|
||||
implementedCategories.add(control.category)
|
||||
}
|
||||
}
|
||||
|
||||
// Check each SDM goal
|
||||
for (const goal of sdmGoals) {
|
||||
const coveringCategories = goalToCategoriesMap.get(goal) ?? []
|
||||
const hasCoverage = coveringCategories.some((cat) => implementedCategories.has(cat))
|
||||
|
||||
if (!hasCoverage) {
|
||||
issues.push(
|
||||
createIssue(
|
||||
`SDM-${goal}`,
|
||||
goal,
|
||||
'UNCOVERED_SDM_GOAL',
|
||||
'HIGH',
|
||||
`SDM-Gewaehrleistungsziel "${goal}" nicht abgedeckt`,
|
||||
`Das Gewaehrleistungsziel "${goal}" des Standard-Datenschutzmodells (SDM) ist durch keine umgesetzte Massnahme abgedeckt. Zugehoerige Kategorien (${coveringCategories.join(', ')}) haben keine IMPLEMENTED Controls. Das SDM ist die anerkannte Methodik zur Umsetzung der DSGVO-Anforderungen.`,
|
||||
`Setzen Sie mindestens eine Massnahme aus den Kategorien ${coveringCategories.join(', ')} um, um das SDM-Ziel "${goal}" abzudecken.`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
/**
|
||||
* Check 10: HIGH_RISK_WITHOUT_MEASURES (CRITICAL)
|
||||
* Protection level VERY_HIGH but < 50% of REQUIRED controls implemented.
|
||||
*/
|
||||
function checkHighRiskWithoutMeasures(toms: DerivedTOM[], riskProfile: RiskProfile | null): TOMComplianceIssue | null {
|
||||
if (!riskProfile || riskProfile.protectionLevel !== 'VERY_HIGH') return null
|
||||
|
||||
const requiredToms = toms.filter((t) => t.applicability === 'REQUIRED')
|
||||
if (requiredToms.length === 0) return null
|
||||
|
||||
const implementedCount = requiredToms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
|
||||
const implementationRate = implementedCount / requiredToms.length
|
||||
|
||||
if (implementationRate < 0.5) {
|
||||
const percentage = Math.round(implementationRate * 100)
|
||||
return createIssue(
|
||||
'RISK-PROFILE',
|
||||
'Risikoprofil VERY_HIGH',
|
||||
'HIGH_RISK_WITHOUT_MEASURES',
|
||||
'CRITICAL',
|
||||
'Sehr hoher Schutzbedarf bei niedriger Umsetzungsrate',
|
||||
`Der Schutzbedarf ist als VERY_HIGH eingestuft, aber nur ${implementedCount} von ${requiredToms.length} Pflichtmassnahmen (${percentage}%) sind umgesetzt. Bei sehr hohem Schutzbedarf muessen mindestens 50% der Pflichtmassnahmen implementiert sein, um ein angemessenes Schutzniveau gemaess Art. 32 DSGVO zu gewaehrleisten.`,
|
||||
'Priorisieren Sie die Umsetzung der verbleibenden Pflichtmassnahmen. Beginnen Sie mit CRITICAL- und HIGH-Priority Controls. Erwaeegen Sie einen Umsetzungsplan mit klaren Meilensteinen.'
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN COMPLIANCE CHECK
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fuehrt einen vollstaendigen Compliance-Check ueber alle TOMs durch.
|
||||
*
|
||||
* @param state - Der vollstaendige TOMGeneratorState
|
||||
* @returns TOMComplianceCheckResult mit Issues, Score und Statistiken
|
||||
*/
|
||||
export function runTOMComplianceCheck(state: TOMGeneratorState): TOMComplianceCheckResult {
|
||||
// Reset counter for deterministic IDs within a single check run
|
||||
issueCounter = 0
|
||||
|
||||
const issues: TOMComplianceIssue[] = []
|
||||
|
||||
// Filter to applicable TOMs only (REQUIRED or RECOMMENDED, exclude NOT_APPLICABLE)
|
||||
const applicableTOMs = state.derivedTOMs.filter(
|
||||
(tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
|
||||
)
|
||||
|
||||
// Run per-TOM checks (1-3, 11) on each applicable TOM
|
||||
for (const tom of applicableTOMs) {
|
||||
const perTomChecks = [
|
||||
checkMissingResponsible(tom),
|
||||
checkOverdueReview(tom),
|
||||
checkMissingEvidence(tom),
|
||||
checkStaleNotImplemented(tom, state),
|
||||
]
|
||||
|
||||
for (const issue of perTomChecks) {
|
||||
if (issue !== null) {
|
||||
issues.push(issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run aggregate checks (4-10)
|
||||
issues.push(...checkIncompleteCategory(applicableTOMs))
|
||||
|
||||
const aggregateChecks = [
|
||||
checkNoEncryption(applicableTOMs),
|
||||
checkNoPseudonymization(applicableTOMs, state.dataProfile),
|
||||
checkMissingAvailability(applicableTOMs, state),
|
||||
checkNoReviewProcess(applicableTOMs),
|
||||
checkHighRiskWithoutMeasures(applicableTOMs, state.riskProfile),
|
||||
]
|
||||
|
||||
for (const issue of aggregateChecks) {
|
||||
if (issue !== null) {
|
||||
issues.push(issue)
|
||||
}
|
||||
}
|
||||
|
||||
issues.push(...checkUncoveredSDMGoal(applicableTOMs))
|
||||
|
||||
// Calculate score
|
||||
const bySeverity: Record<TOMComplianceIssueSeverity, number> = {
|
||||
LOW: 0,
|
||||
MEDIUM: 0,
|
||||
HIGH: 0,
|
||||
CRITICAL: 0,
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
bySeverity[issue.severity]++
|
||||
}
|
||||
|
||||
const rawScore =
|
||||
100 -
|
||||
(bySeverity.CRITICAL * 15 +
|
||||
bySeverity.HIGH * 10 +
|
||||
bySeverity.MEDIUM * 5 +
|
||||
bySeverity.LOW * 2)
|
||||
|
||||
const score = Math.max(0, rawScore)
|
||||
|
||||
// Calculate pass/fail per TOM
|
||||
const failedControlIds = new Set(
|
||||
issues.filter((i) => !i.controlId.startsWith('SDM-') && i.controlId !== 'RISK-PROFILE').map((i) => i.controlId)
|
||||
)
|
||||
const totalTOMs = applicableTOMs.length
|
||||
const failedCount = failedControlIds.size
|
||||
const passedCount = Math.max(0, totalTOMs - failedCount)
|
||||
|
||||
return {
|
||||
issues,
|
||||
score,
|
||||
stats: {
|
||||
total: totalTOMs,
|
||||
passed: passedCount,
|
||||
failed: failedCount,
|
||||
bySeverity,
|
||||
},
|
||||
}
|
||||
}
|
||||
906
admin-compliance/lib/sdk/tom-document.ts
Normal file
906
admin-compliance/lib/sdk/tom-document.ts
Normal file
@@ -0,0 +1,906 @@
|
||||
// =============================================================================
|
||||
// TOM Module - TOM-Dokumentation Document Generator
|
||||
// Generates a printable, audit-ready HTML document according to DSGVO Art. 32
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
TOMGeneratorState,
|
||||
DerivedTOM,
|
||||
CompanyProfile,
|
||||
RiskProfile,
|
||||
ControlCategory,
|
||||
} from './tom-generator/types'
|
||||
|
||||
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
|
||||
|
||||
import {
|
||||
getControlById,
|
||||
getControlsByCategory,
|
||||
getAllCategories,
|
||||
getCategoryMetadata,
|
||||
} from './tom-generator/controls/loader'
|
||||
|
||||
import type { TOMComplianceCheckResult, TOMComplianceIssueSeverity } from './tom-compliance'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface TOMDocumentOrgHeader {
|
||||
organizationName: string
|
||||
industry: string
|
||||
dpoName: string
|
||||
dpoContact: string
|
||||
responsiblePerson: string
|
||||
itSecurityContact: string
|
||||
locations: string[]
|
||||
employeeCount: string
|
||||
documentVersion: string
|
||||
lastReviewDate: string
|
||||
nextReviewDate: string
|
||||
reviewInterval: string
|
||||
}
|
||||
|
||||
export interface TOMDocumentRevision {
|
||||
version: string
|
||||
date: string
|
||||
author: string
|
||||
changes: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEFAULTS
|
||||
// =============================================================================
|
||||
|
||||
export function createDefaultTOMDocumentOrgHeader(): TOMDocumentOrgHeader {
|
||||
const now = new Date()
|
||||
const nextYear = new Date()
|
||||
nextYear.setFullYear(nextYear.getFullYear() + 1)
|
||||
|
||||
return {
|
||||
organizationName: '',
|
||||
industry: '',
|
||||
dpoName: '',
|
||||
dpoContact: '',
|
||||
responsiblePerson: '',
|
||||
itSecurityContact: '',
|
||||
locations: [],
|
||||
employeeCount: '',
|
||||
documentVersion: '1.0',
|
||||
lastReviewDate: now.toISOString().split('T')[0],
|
||||
nextReviewDate: nextYear.toISOString().split('T')[0],
|
||||
reviewInterval: 'Jaehrlich',
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SEVERITY LABELS (for Compliance Status section)
|
||||
// =============================================================================
|
||||
|
||||
const SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
|
||||
CRITICAL: 'Kritisch',
|
||||
HIGH: 'Hoch',
|
||||
MEDIUM: 'Mittel',
|
||||
LOW: 'Niedrig',
|
||||
}
|
||||
|
||||
const SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
|
||||
CRITICAL: '#dc2626',
|
||||
HIGH: '#ea580c',
|
||||
MEDIUM: '#d97706',
|
||||
LOW: '#6b7280',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CATEGORY LABELS (German)
|
||||
// =============================================================================
|
||||
|
||||
const CATEGORY_LABELS_DE: Record<ControlCategory, string> = {
|
||||
ACCESS_CONTROL: 'Zutrittskontrolle',
|
||||
ADMISSION_CONTROL: 'Zugangskontrolle',
|
||||
ACCESS_AUTHORIZATION: 'Zugriffskontrolle',
|
||||
TRANSFER_CONTROL: 'Weitergabekontrolle',
|
||||
INPUT_CONTROL: 'Eingabekontrolle',
|
||||
ORDER_CONTROL: 'Auftragskontrolle',
|
||||
AVAILABILITY: 'Verfuegbarkeit',
|
||||
SEPARATION: 'Trennbarkeit',
|
||||
ENCRYPTION: 'Verschluesselung',
|
||||
PSEUDONYMIZATION: 'Pseudonymisierung',
|
||||
RESILIENCE: 'Belastbarkeit',
|
||||
RECOVERY: 'Wiederherstellbarkeit',
|
||||
REVIEW: 'Ueberpruefung & Bewertung',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATUS & APPLICABILITY LABELS
|
||||
// =============================================================================
|
||||
|
||||
const STATUS_LABELS_DE: Record<string, string> = {
|
||||
IMPLEMENTED: 'Umgesetzt',
|
||||
PARTIAL: 'Teilweise umgesetzt',
|
||||
NOT_IMPLEMENTED: 'Nicht umgesetzt',
|
||||
}
|
||||
|
||||
const STATUS_BADGE_CLASSES: Record<string, string> = {
|
||||
IMPLEMENTED: 'badge-active',
|
||||
PARTIAL: 'badge-review',
|
||||
NOT_IMPLEMENTED: 'badge-critical',
|
||||
}
|
||||
|
||||
const APPLICABILITY_LABELS_DE: Record<string, string> = {
|
||||
REQUIRED: 'Erforderlich',
|
||||
RECOMMENDED: 'Empfohlen',
|
||||
OPTIONAL: 'Optional',
|
||||
NOT_APPLICABLE: 'Nicht anwendbar',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HTML DOCUMENT BUILDER
|
||||
// =============================================================================
|
||||
|
||||
export function buildTOMDocumentHtml(
|
||||
derivedTOMs: DerivedTOM[],
|
||||
orgHeader: TOMDocumentOrgHeader,
|
||||
companyProfile: CompanyProfile | null,
|
||||
riskProfile: RiskProfile | null,
|
||||
complianceResult: TOMComplianceCheckResult | null,
|
||||
revisions: TOMDocumentRevision[]
|
||||
): string {
|
||||
const today = new Date().toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
|
||||
const orgName = orgHeader.organizationName || 'Organisation'
|
||||
|
||||
// Filter out NOT_APPLICABLE TOMs for display
|
||||
const applicableTOMs = derivedTOMs.filter(t => t.applicability !== 'NOT_APPLICABLE')
|
||||
|
||||
// Group TOMs by category via control library lookup
|
||||
const tomsByCategory = new Map<ControlCategory, DerivedTOM[]>()
|
||||
for (const tom of applicableTOMs) {
|
||||
const control = getControlById(tom.controlId)
|
||||
const cat = control?.category || 'REVIEW'
|
||||
if (!tomsByCategory.has(cat)) tomsByCategory.set(cat, [])
|
||||
tomsByCategory.get(cat)!.push(tom)
|
||||
}
|
||||
|
||||
// Build role map: role/department → list of control codes
|
||||
const roleMap = new Map<string, string[]>()
|
||||
for (const tom of applicableTOMs) {
|
||||
const role = tom.responsiblePerson || tom.responsibleDepartment || 'Nicht zugewiesen'
|
||||
if (!roleMap.has(role)) roleMap.set(role, [])
|
||||
const control = getControlById(tom.controlId)
|
||||
roleMap.get(role)!.push(control?.code || tom.controlId)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HTML Template
|
||||
// =========================================================================
|
||||
|
||||
let html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TOM-Dokumentation — ${escHtml(orgName)}</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 10pt;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Cover */
|
||||
.cover {
|
||||
min-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
page-break-after: always;
|
||||
}
|
||||
.cover h1 {
|
||||
font-size: 28pt;
|
||||
color: #5b21b6;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cover .subtitle {
|
||||
font-size: 14pt;
|
||||
color: #7c3aed;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.cover .org-info {
|
||||
background: #f5f3ff;
|
||||
border: 1px solid #ddd6fe;
|
||||
border-radius: 8px;
|
||||
padding: 24px 40px;
|
||||
text-align: left;
|
||||
width: 400px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.cover .org-info div { margin-bottom: 6px; }
|
||||
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
|
||||
.cover .legal-ref {
|
||||
font-size: 9pt;
|
||||
color: #64748b;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* TOC */
|
||||
.toc {
|
||||
page-break-after: always;
|
||||
padding-top: 40px;
|
||||
}
|
||||
.toc h2 {
|
||||
font-size: 18pt;
|
||||
color: #5b21b6;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #5b21b6;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.toc-entry {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dotted #cbd5e1;
|
||||
font-size: 10pt;
|
||||
}
|
||||
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
|
||||
|
||||
/* Sections */
|
||||
.section {
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.section-header {
|
||||
font-size: 14pt;
|
||||
color: #5b21b6;
|
||||
font-weight: 700;
|
||||
margin: 30px 0 12px 0;
|
||||
border-bottom: 2px solid #ddd6fe;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.section-body { margin-bottom: 16px; }
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0 16px 0;
|
||||
font-size: 9pt;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f5f3ff;
|
||||
color: #5b21b6;
|
||||
font-weight: 600;
|
||||
font-size: 8.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
tr:nth-child(even) td { background: #faf5ff; }
|
||||
|
||||
/* Detail cards */
|
||||
.policy-detail {
|
||||
page-break-inside: avoid;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.policy-detail-header {
|
||||
background: #f5f3ff;
|
||||
padding: 8px 12px;
|
||||
font-weight: 700;
|
||||
color: #5b21b6;
|
||||
border-bottom: 1px solid #ddd6fe;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.policy-detail-body { padding: 0; }
|
||||
.policy-detail-body table { margin: 0; }
|
||||
.policy-detail-body th { width: 200px; }
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 9999px;
|
||||
font-size: 8pt;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-active { background: #dcfce7; color: #166534; }
|
||||
.badge-draft { background: #f3f4f6; color: #374151; }
|
||||
.badge-review { background: #fef9c3; color: #854d0e; }
|
||||
.badge-critical { background: #fecaca; color: #991b1b; }
|
||||
.badge-high { background: #fed7aa; color: #9a3412; }
|
||||
.badge-medium { background: #fef3c7; color: #92400e; }
|
||||
.badge-low { background: #f3f4f6; color: #4b5563; }
|
||||
|
||||
/* Principles */
|
||||
.principle {
|
||||
margin-bottom: 10px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
.principle::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #7c3aed;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.principle strong { color: #5b21b6; }
|
||||
|
||||
/* Score */
|
||||
.score-box {
|
||||
display: inline-block;
|
||||
padding: 4px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.score-excellent { background: #dcfce7; color: #166534; }
|
||||
.score-good { background: #dbeafe; color: #1e40af; }
|
||||
.score-needs-work { background: #fef3c7; color: #92400e; }
|
||||
.score-poor { background: #fecaca; color: #991b1b; }
|
||||
|
||||
/* Footer */
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px 18mm;
|
||||
font-size: 7.5pt;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Print */
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
.no-print { display: none !important; }
|
||||
.page-break { page-break-after: always; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 0: Cover Page
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="cover">
|
||||
<h1>TOM-Dokumentation</h1>
|
||||
<div class="subtitle">Technische und Organisatorische Massnahmen gemaess Art. 32 DSGVO</div>
|
||||
<div class="org-info">
|
||||
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
|
||||
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
|
||||
${orgHeader.dpoName ? `<div><span class="label">DSB:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
|
||||
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
|
||||
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
|
||||
${orgHeader.itSecurityContact ? `<div><span class="label">IT-Sicherheit:</span> ${escHtml(orgHeader.itSecurityContact)}</div>` : ''}
|
||||
${orgHeader.employeeCount ? `<div><span class="label">Mitarbeiter:</span> ${escHtml(orgHeader.employeeCount)}</div>` : ''}
|
||||
${orgHeader.locations.length > 0 ? `<div><span class="label">Standorte:</span> ${escHtml(orgHeader.locations.join(', '))}</div>` : ''}
|
||||
</div>
|
||||
<div class="legal-ref">
|
||||
Version ${escHtml(orgHeader.documentVersion)} | Stand: ${today}<br/>
|
||||
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
|
||||
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Table of Contents
|
||||
// =========================================================================
|
||||
const sections = [
|
||||
'Ziel und Zweck',
|
||||
'Geltungsbereich',
|
||||
'Grundprinzipien Art. 32',
|
||||
'Schutzbedarf und Risikoanalyse',
|
||||
'Massnahmen-Uebersicht',
|
||||
'Detaillierte Massnahmen',
|
||||
'SDM Gewaehrleistungsziele',
|
||||
'Verantwortlichkeiten',
|
||||
'Pruef- und Revisionszyklus',
|
||||
'Compliance-Status',
|
||||
'Aenderungshistorie',
|
||||
]
|
||||
|
||||
html += `
|
||||
<div class="toc">
|
||||
<h2>Inhaltsverzeichnis</h2>
|
||||
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 1: Ziel und Zweck
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">1. Ziel und Zweck</div>
|
||||
<div class="section-body">
|
||||
<p>Diese TOM-Dokumentation beschreibt die technischen und organisatorischen Massnahmen
|
||||
zum Schutz personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Sie dient
|
||||
der Umsetzung folgender DSGVO-Anforderungen:</p>
|
||||
<table>
|
||||
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
|
||||
<tr><td><strong>Art. 32 Abs. 1 lit. a DSGVO</strong></td><td>Pseudonymisierung und Verschluesselung personenbezogener Daten</td></tr>
|
||||
<tr><td><strong>Art. 32 Abs. 1 lit. b DSGVO</strong></td><td>Faehigkeit, die Vertraulichkeit, Integritaet, Verfuegbarkeit und Belastbarkeit der Systeme und Dienste im Zusammenhang mit der Verarbeitung auf Dauer sicherzustellen</td></tr>
|
||||
<tr><td><strong>Art. 32 Abs. 1 lit. c DSGVO</strong></td><td>Faehigkeit, die Verfuegbarkeit der personenbezogenen Daten und den Zugang zu ihnen bei einem physischen oder technischen Zwischenfall rasch wiederherzustellen</td></tr>
|
||||
<tr><td><strong>Art. 32 Abs. 1 lit. d DSGVO</strong></td><td>Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen</td></tr>
|
||||
</table>
|
||||
<p>Die TOM-Dokumentation ist fester Bestandteil des Datenschutz-Managementsystems und wird
|
||||
regelmaessig ueberprueft und aktualisiert.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 2: Geltungsbereich
|
||||
// =========================================================================
|
||||
const industryInfo = companyProfile?.industry || orgHeader.industry || ''
|
||||
const hostingInfo = companyProfile ? `Unternehmen: ${escHtml(companyProfile.name || orgName)}, Groesse: ${escHtml(companyProfile.size || '-')}` : ''
|
||||
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">2. Geltungsbereich</div>
|
||||
<div class="section-body">
|
||||
<p>Diese TOM-Dokumentation gilt fuer alle IT-Systeme, Anwendungen und Verarbeitungsprozesse
|
||||
von <strong>${escHtml(orgName)}</strong>${industryInfo ? ` (Branche: ${escHtml(industryInfo)})` : ''}.</p>
|
||||
${hostingInfo ? `<p>${hostingInfo}</p>` : ''}
|
||||
${orgHeader.locations.length > 0 ? `<p>Standorte: ${escHtml(orgHeader.locations.join(', '))}</p>` : ''}
|
||||
<p>Die dokumentierten Massnahmen stammen aus zwei Quellen:</p>
|
||||
<ul style="margin: 8px 0 8px 24px;">
|
||||
<li><strong>Embedded Library (TOM-xxx):</strong> Integrierte Kontrollbibliothek mit spezifischen Massnahmen fuer Art. 32 DSGVO</li>
|
||||
<li><strong>Canonical Control Library (CP-CLIB):</strong> Uebergreifende Kontrollbibliothek mit framework-uebergreifenden Massnahmen</li>
|
||||
</ul>
|
||||
<p>Insgesamt umfasst dieses Dokument <strong>${applicableTOMs.length}</strong> anwendbare Massnahmen
|
||||
in <strong>${tomsByCategory.size}</strong> Kategorien.</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 3: Grundprinzipien Art. 32
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">3. Grundprinzipien Art. 32</div>
|
||||
<div class="section-body">
|
||||
<div class="principle"><strong>Vertraulichkeit:</strong> Schutz personenbezogener Daten vor unbefugter Kenntnisnahme durch Zutrittskontrolle, Zugangskontrolle, Zugriffskontrolle und Verschluesselung (Art. 32 Abs. 1 lit. b DSGVO).</div>
|
||||
<div class="principle"><strong>Integritaet:</strong> Sicherstellung, dass personenbezogene Daten nicht unbefugt oder unbeabsichtigt veraendert werden koennen, durch Eingabekontrolle, Weitergabekontrolle und Protokollierung (Art. 32 Abs. 1 lit. b DSGVO).</div>
|
||||
<div class="principle"><strong>Verfuegbarkeit und Belastbarkeit:</strong> Gewaehrleistung, dass Systeme und Dienste bei Lastspitzen und Stoerungen zuverlaessig funktionieren, durch Backup, Redundanz und Disaster Recovery (Art. 32 Abs. 1 lit. b DSGVO).</div>
|
||||
<div class="principle"><strong>Rasche Wiederherstellbarkeit:</strong> Faehigkeit, nach einem physischen oder technischen Zwischenfall Daten und Systeme schnell wiederherzustellen, durch getestete Recovery-Prozesse (Art. 32 Abs. 1 lit. c DSGVO).</div>
|
||||
<div class="principle"><strong>Regelmaessige Wirksamkeitspruefung:</strong> Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit aller technischen und organisatorischen Massnahmen (Art. 32 Abs. 1 lit. d DSGVO).</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 4: Schutzbedarf und Risikoanalyse
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">4. Schutzbedarf und Risikoanalyse</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (riskProfile) {
|
||||
html += ` <p>Die folgende Schutzbedarfsanalyse bildet die Grundlage fuer die Auswahl und Priorisierung
|
||||
der technischen und organisatorischen Massnahmen:</p>
|
||||
<table>
|
||||
<tr><th>Kriterium</th><th>Bewertung</th></tr>
|
||||
<tr><td>Vertraulichkeit</td><td>${riskProfile.ciaAssessment.confidentiality}/5</td></tr>
|
||||
<tr><td>Integritaet</td><td>${riskProfile.ciaAssessment.integrity}/5</td></tr>
|
||||
<tr><td>Verfuegbarkeit</td><td>${riskProfile.ciaAssessment.availability}/5</td></tr>
|
||||
<tr><td>Schutzniveau</td><td><strong>${escHtml(riskProfile.protectionLevel)}</strong></td></tr>
|
||||
<tr><td>DSFA-Pflicht</td><td>${riskProfile.dsfaRequired ? 'Ja' : 'Nein'}</td></tr>
|
||||
${riskProfile.specialRisks.length > 0 ? `<tr><td>Spezialrisiken</td><td>${escHtml(riskProfile.specialRisks.join(', '))}</td></tr>` : ''}
|
||||
${riskProfile.regulatoryRequirements.length > 0 ? `<tr><td>Regulatorische Anforderungen</td><td>${escHtml(riskProfile.regulatoryRequirements.join(', '))}</td></tr>` : ''}
|
||||
</table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p><em>Die Schutzbedarfsanalyse wurde noch nicht durchgefuehrt. Fuehren Sie den
|
||||
Risiko-Wizard im TOM-Generator durch, um den Schutzbedarf zu ermitteln.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 5: Massnahmen-Uebersicht
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">5. Massnahmen-Uebersicht</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${applicableTOMs.length} anwendbaren Massnahmen
|
||||
nach Kategorie:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Kategorie</th>
|
||||
<th>Gesamt</th>
|
||||
<th>Umgesetzt</th>
|
||||
<th>Teilweise</th>
|
||||
<th>Offen</th>
|
||||
</tr>
|
||||
`
|
||||
const allCategories = getAllCategories()
|
||||
for (const cat of allCategories) {
|
||||
const tomsInCat = tomsByCategory.get(cat)
|
||||
if (!tomsInCat || tomsInCat.length === 0) continue
|
||||
|
||||
const implemented = tomsInCat.filter(t => t.implementationStatus === 'IMPLEMENTED').length
|
||||
const partial = tomsInCat.filter(t => t.implementationStatus === 'PARTIAL').length
|
||||
const notImpl = tomsInCat.filter(t => t.implementationStatus === 'NOT_IMPLEMENTED').length
|
||||
const catLabel = CATEGORY_LABELS_DE[cat] || cat
|
||||
|
||||
html += ` <tr>
|
||||
<td>${escHtml(catLabel)}</td>
|
||||
<td>${tomsInCat.length}</td>
|
||||
<td>${implemented}</td>
|
||||
<td>${partial}</td>
|
||||
<td>${notImpl}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 6: Detaillierte Massnahmen
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">6. Detaillierte Massnahmen</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
|
||||
for (const cat of allCategories) {
|
||||
const tomsInCat = tomsByCategory.get(cat)
|
||||
if (!tomsInCat || tomsInCat.length === 0) continue
|
||||
|
||||
const catLabel = CATEGORY_LABELS_DE[cat] || cat
|
||||
const catMeta = getCategoryMetadata(cat)
|
||||
const gdprRef = catMeta?.gdprReference || ''
|
||||
|
||||
html += ` <h3 style="color: #5b21b6; margin: 20px 0 10px 0; font-size: 11pt;">${escHtml(catLabel)}${gdprRef ? ` <span style="font-weight: 400; font-size: 9pt; color: #64748b;">(${escHtml(gdprRef)})</span>` : ''}</h3>
|
||||
`
|
||||
|
||||
// Sort TOMs by control code
|
||||
const sortedTOMs = [...tomsInCat].sort((a, b) => {
|
||||
const codeA = getControlById(a.controlId)?.code || a.controlId
|
||||
const codeB = getControlById(b.controlId)?.code || b.controlId
|
||||
return codeA.localeCompare(codeB)
|
||||
})
|
||||
|
||||
for (const tom of sortedTOMs) {
|
||||
const control = getControlById(tom.controlId)
|
||||
const code = control?.code || tom.controlId
|
||||
const nameDE = control?.name?.de || tom.name
|
||||
const descDE = control?.description?.de || tom.description
|
||||
const typeLabel = control?.type === 'TECHNICAL' ? 'Technisch' : control?.type === 'ORGANIZATIONAL' ? 'Organisatorisch' : '-'
|
||||
const statusLabel = STATUS_LABELS_DE[tom.implementationStatus] || tom.implementationStatus
|
||||
const statusBadge = STATUS_BADGE_CLASSES[tom.implementationStatus] || 'badge-draft'
|
||||
const applicabilityLabel = APPLICABILITY_LABELS_DE[tom.applicability] || tom.applicability
|
||||
const responsible = [tom.responsiblePerson, tom.responsibleDepartment].filter(s => s && s.trim()).join(' / ') || '-'
|
||||
const implDate = tom.implementationDate ? formatDateDE(typeof tom.implementationDate === 'string' ? tom.implementationDate : tom.implementationDate.toISOString()) : '-'
|
||||
const reviewDate = tom.reviewDate ? formatDateDE(typeof tom.reviewDate === 'string' ? tom.reviewDate : tom.reviewDate.toISOString()) : '-'
|
||||
|
||||
// Evidence
|
||||
const evidenceInfo = tom.linkedEvidence.length > 0
|
||||
? tom.linkedEvidence.join(', ')
|
||||
: tom.evidenceGaps.length > 0
|
||||
? `<em style="color: #d97706;">Fehlend: ${escHtml(tom.evidenceGaps.join(', '))}</em>`
|
||||
: '-'
|
||||
|
||||
// Framework mappings
|
||||
let mappingsHtml = '-'
|
||||
if (control?.mappings && control.mappings.length > 0) {
|
||||
mappingsHtml = control.mappings.map(m => `${escHtml(m.framework)}: ${escHtml(m.reference)}`).join('<br/>')
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="policy-detail">
|
||||
<div class="policy-detail-header">
|
||||
<span>${escHtml(code)} — ${escHtml(nameDE)}</span>
|
||||
<span class="badge ${statusBadge}">${escHtml(statusLabel)}</span>
|
||||
</div>
|
||||
<div class="policy-detail-body">
|
||||
<table>
|
||||
<tr><th>Beschreibung</th><td>${escHtml(descDE)}</td></tr>
|
||||
<tr><th>Massnahmentyp</th><td>${escHtml(typeLabel)}</td></tr>
|
||||
<tr><th>Anwendbarkeit</th><td>${escHtml(applicabilityLabel)}${tom.applicabilityReason ? ` — ${escHtml(tom.applicabilityReason)}` : ''}</td></tr>
|
||||
<tr><th>Umsetzungsstatus</th><td><span class="badge ${statusBadge}">${escHtml(statusLabel)}</span></td></tr>
|
||||
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
|
||||
<tr><th>Umsetzungsdatum</th><td>${implDate}</td></tr>
|
||||
<tr><th>Naechste Pruefung</th><td>${reviewDate}</td></tr>
|
||||
<tr><th>Evidence</th><td>${evidenceInfo}</td></tr>
|
||||
<tr><th>Framework-Mappings</th><td>${mappingsHtml}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 7: SDM Gewaehrleistungsziele
|
||||
// =========================================================================
|
||||
const sdmGoals: Array<{ goal: string; categories: ControlCategory[] }> = []
|
||||
const allSDMGoals = [
|
||||
'Verfuegbarkeit',
|
||||
'Integritaet',
|
||||
'Vertraulichkeit',
|
||||
'Nichtverkettung',
|
||||
'Intervenierbarkeit',
|
||||
'Transparenz',
|
||||
'Datenminimierung',
|
||||
] as const
|
||||
|
||||
for (const goal of allSDMGoals) {
|
||||
const cats: ControlCategory[] = []
|
||||
for (const [cat, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
|
||||
if (goals.includes(goal)) {
|
||||
cats.push(cat as ControlCategory)
|
||||
}
|
||||
}
|
||||
sdmGoals.push({ goal, categories: cats })
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">7. SDM Gewaehrleistungsziele</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Tabelle zeigt die Abdeckung der sieben Gewaehrleistungsziele des
|
||||
Standard-Datenschutzmodells (SDM) durch die implementierten Massnahmen:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Gewaehrleistungsziel</th>
|
||||
<th>Abgedeckt</th>
|
||||
<th>Gesamt</th>
|
||||
<th>Abdeckung (%)</th>
|
||||
</tr>
|
||||
`
|
||||
for (const { goal, categories } of sdmGoals) {
|
||||
let totalInGoal = 0
|
||||
let implementedInGoal = 0
|
||||
for (const cat of categories) {
|
||||
const tomsInCat = tomsByCategory.get(cat) || []
|
||||
totalInGoal += tomsInCat.length
|
||||
implementedInGoal += tomsInCat.filter(t => t.implementationStatus === 'IMPLEMENTED').length
|
||||
}
|
||||
const percentage = totalInGoal > 0 ? Math.round((implementedInGoal / totalInGoal) * 100) : 0
|
||||
|
||||
html += ` <tr>
|
||||
<td>${escHtml(goal)}</td>
|
||||
<td>${implementedInGoal}</td>
|
||||
<td>${totalInGoal}</td>
|
||||
<td>${percentage}%</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 8: Verantwortlichkeiten
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">8. Verantwortlichkeiten</div>
|
||||
<div class="section-body">
|
||||
<p>Die folgende Rollenmatrix zeigt, welche Personen oder Abteilungen fuer welche Massnahmen
|
||||
die Umsetzungsverantwortung tragen:</p>
|
||||
<table>
|
||||
<tr><th>Rolle / Verantwortlich</th><th>Massnahmen</th><th>Anzahl</th></tr>
|
||||
`
|
||||
for (const [role, controls] of roleMap.entries()) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(role)}</td>
|
||||
<td>${controls.map(c => escHtml(c)).join(', ')}</td>
|
||||
<td>${controls.length}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 9: Pruef- und Revisionszyklus
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">9. Pruef- und Revisionszyklus</div>
|
||||
<div class="section-body">
|
||||
<table>
|
||||
<tr><th>Eigenschaft</th><th>Wert</th></tr>
|
||||
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
|
||||
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
|
||||
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
|
||||
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.documentVersion)}</td></tr>
|
||||
</table>
|
||||
<p style="margin-top: 8px;">Bei jeder Pruefung wird die TOM-Dokumentation auf folgende Punkte ueberprueft:</p>
|
||||
<ul style="margin: 8px 0 8px 24px;">
|
||||
<li>Vollstaendigkeit aller Massnahmen (neue Systeme oder Verarbeitungen erfasst?)</li>
|
||||
<li>Aktualitaet des Umsetzungsstatus (Aenderungen seit letzter Pruefung?)</li>
|
||||
<li>Wirksamkeit der technischen Massnahmen (Penetration-Tests, Audit-Ergebnisse)</li>
|
||||
<li>Angemessenheit der organisatorischen Massnahmen (Schulungen, Richtlinien aktuell?)</li>
|
||||
<li>Abdeckung aller SDM-Gewaehrleistungsziele</li>
|
||||
<li>Zuordnung von Verantwortlichkeiten zu allen Massnahmen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 10: Compliance-Status
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section page-break">
|
||||
<div class="section-header">10. Compliance-Status</div>
|
||||
<div class="section-body">
|
||||
`
|
||||
if (complianceResult) {
|
||||
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
|
||||
: complianceResult.score >= 75 ? 'score-good'
|
||||
: complianceResult.score >= 50 ? 'score-needs-work'
|
||||
: 'score-poor'
|
||||
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
|
||||
: complianceResult.score >= 75 ? 'Gut'
|
||||
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
|
||||
: 'Mangelhaft'
|
||||
|
||||
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
|
||||
<table style="margin-top: 12px;">
|
||||
<tr><th>Kennzahl</th><th>Wert</th></tr>
|
||||
<tr><td>Gepruefte Massnahmen</td><td>${complianceResult.stats.total}</td></tr>
|
||||
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
|
||||
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
|
||||
</table>
|
||||
`
|
||||
if (complianceResult.issues.length > 0) {
|
||||
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
|
||||
<table>
|
||||
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
|
||||
`
|
||||
const severityOrder: TOMComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
|
||||
for (const sev of severityOrder) {
|
||||
const count = complianceResult.stats.bySeverity[sev]
|
||||
if (count === 0) continue
|
||||
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
|
||||
html += ` <tr>
|
||||
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
|
||||
<td>${count}</td>
|
||||
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
html += ` </table>
|
||||
`
|
||||
} else {
|
||||
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Massnahmen sind konform.</em></p>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
|
||||
Export-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Section 11: Aenderungshistorie
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="section">
|
||||
<div class="section-header">11. Aenderungshistorie</div>
|
||||
<div class="section-body">
|
||||
<table>
|
||||
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
|
||||
`
|
||||
if (revisions.length > 0) {
|
||||
for (const rev of revisions) {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(rev.version)}</td>
|
||||
<td>${formatDateDE(rev.date)}</td>
|
||||
<td>${escHtml(rev.author)}</td>
|
||||
<td>${escHtml(rev.changes)}</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
html += ` <tr>
|
||||
<td>${escHtml(orgHeader.documentVersion)}</td>
|
||||
<td>${today}</td>
|
||||
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
|
||||
<td>Erstversion der TOM-Dokumentation</td>
|
||||
</tr>
|
||||
`
|
||||
}
|
||||
|
||||
html += ` </table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// =========================================================================
|
||||
// Footer
|
||||
// =========================================================================
|
||||
html += `
|
||||
<div class="page-footer">
|
||||
<span>TOM-Dokumentation — ${escHtml(orgName)}</span>
|
||||
<span>Stand: ${today} | Version ${escHtml(orgHeader.documentVersion)}</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERNAL HELPERS
|
||||
// =============================================================================
|
||||
|
||||
function escHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function formatDateDE(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '-'
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
if (isNaN(date.getTime())) return '-'
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
})
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
@@ -89,9 +89,9 @@ export interface ControlLibrary {
|
||||
|
||||
const CONTROL_LIBRARY_DATA: ControlLibrary = {
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
lastUpdated: '2026-02-04',
|
||||
totalControls: 60,
|
||||
version: '1.1.0',
|
||||
lastUpdated: '2026-03-19',
|
||||
totalControls: 88,
|
||||
},
|
||||
categories: new Map([
|
||||
[
|
||||
@@ -2353,6 +2353,648 @@ const CONTROL_LIBRARY_DATA: ControlLibrary = {
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['training', 'security-awareness', 'phishing', 'social-engineering'],
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// NEW CONTROLS (v1.1.0) — 25 additional measures
|
||||
// =========================================================================
|
||||
|
||||
// ENCRYPTION — 2 new
|
||||
{
|
||||
id: 'TOM-ENC-04',
|
||||
code: 'TOM-ENC-04',
|
||||
category: 'ENCRYPTION',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Zertifikatsmanagement (TLS/SSL)', en: 'Certificate Management (TLS/SSL)' },
|
||||
description: {
|
||||
de: 'Systematische Verwaltung, Ueberwachung und rechtzeitige Erneuerung aller TLS/SSL-Zertifikate zur Vermeidung von Sicherheitsluecken durch abgelaufene Zertifikate.',
|
||||
en: 'Systematic management, monitoring and timely renewal of all TLS/SSL certificates to prevent security gaps from expired certificates.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.10.1.2' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.encryptionInTransit', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Zertifikatsinventar', 'Monitoring-Konfiguration', 'Erneuerungsprotokolle'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['encryption', 'certificates', 'tls'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-ENC-05',
|
||||
code: 'TOM-ENC-05',
|
||||
category: 'ENCRYPTION',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Schluesselmanagement-Policy', en: 'Key Management Policy' },
|
||||
description: {
|
||||
de: 'Dokumentierte Richtlinie fuer den gesamten Lebenszyklus kryptografischer Schluessel inkl. Erzeugung, Verteilung, Speicherung, Rotation und Vernichtung.',
|
||||
en: 'Documented policy for the full lifecycle of cryptographic keys including generation, distribution, storage, rotation and destruction.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.10.1.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.encryptionAtRest', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 30 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Schluesselmanagement-Richtlinie', 'Schluesselrotationsplan'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'LOW',
|
||||
tags: ['encryption', 'key-management', 'policy'],
|
||||
},
|
||||
|
||||
// PSEUDONYMIZATION — 2 new
|
||||
{
|
||||
id: 'TOM-PS-03',
|
||||
code: 'TOM-PS-03',
|
||||
category: 'PSEUDONYMIZATION',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Anonymisierung fuer Analysezwecke', en: 'Anonymization for Analytics' },
|
||||
description: {
|
||||
de: 'Technische Verfahren zur irreversiblen Anonymisierung personenbezogener Daten fuer statistische Auswertungen und Analysen.',
|
||||
en: 'Technical procedures for irreversible anonymization of personal data for statistical evaluations and analyses.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
|
||||
{ framework: 'GDPR_ART25', reference: 'Art. 25 Abs. 1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'dataProfile.dataVolume', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['Anonymisierungsverfahren-Dokumentation', 'Re-Identifizierungs-Risikoanalyse'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'HIGH',
|
||||
tags: ['pseudonymization', 'anonymization', 'analytics'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-PS-04',
|
||||
code: 'TOM-PS-04',
|
||||
category: 'PSEUDONYMIZATION',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Pseudonymisierungskonzept', en: 'Pseudonymization Concept' },
|
||||
description: {
|
||||
de: 'Dokumentiertes Konzept fuer die Pseudonymisierung personenbezogener Daten mit Definition der Verfahren, Zustaendigkeiten und Zuordnungsregeln.',
|
||||
en: 'Documented concept for pseudonymization of personal data with definition of procedures, responsibilities and mapping rules.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
|
||||
{ framework: 'GDPR_ART25', reference: 'Art. 25 Abs. 1' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Pseudonymisierungskonzept', 'Verfahrensdokumentation'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['pseudonymization', 'concept', 'documentation'],
|
||||
},
|
||||
|
||||
// INPUT_CONTROL — 1 new
|
||||
{
|
||||
id: 'TOM-IN-05',
|
||||
code: 'TOM-IN-05',
|
||||
category: 'INPUT_CONTROL',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Automatisierte Eingabevalidierung', en: 'Automated Input Validation' },
|
||||
description: {
|
||||
de: 'Technische Validierung aller Benutzereingaben zur Verhinderung von Injection-Angriffen und Sicherstellung der Datenintegritaet.',
|
||||
en: 'Technical validation of all user inputs to prevent injection attacks and ensure data integrity.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.14.2.5' },
|
||||
],
|
||||
applicabilityConditions: [],
|
||||
defaultApplicability: 'REQUIRED',
|
||||
evidenceRequirements: ['Validierungsregeln-Dokumentation', 'Penetrationstest-Berichte'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['input-validation', 'security', 'injection-prevention'],
|
||||
},
|
||||
|
||||
// ORDER_CONTROL — 2 new
|
||||
{
|
||||
id: 'TOM-OR-05',
|
||||
code: 'TOM-OR-05',
|
||||
category: 'ORDER_CONTROL',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Auftragsverarbeiter-Monitoring', en: 'Processor Monitoring' },
|
||||
description: {
|
||||
de: 'Regelmaessige Ueberpruefung und Bewertung der Datenschutz-Massnahmen bei Auftragsverarbeitern gemaess Art. 28 Abs. 3 lit. h DSGVO.',
|
||||
en: 'Regular review and assessment of data protection measures at processors according to Art. 28(3)(h) GDPR.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 28 Abs. 3 lit. h' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.2.1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Audit-Berichte der Auftragsverarbeiter', 'Monitoring-Checklisten'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['order-control', 'processor', 'monitoring'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-OR-06',
|
||||
code: 'TOM-OR-06',
|
||||
category: 'ORDER_CONTROL',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Sub-Processor Management', en: 'Sub-Processor Management' },
|
||||
description: {
|
||||
de: 'Dokumentiertes Verfahren zur Genehmigung, Ueberwachung und Dokumentation von Unterauftragsverarbeitern.',
|
||||
en: 'Documented process for approval, monitoring and documentation of sub-processors.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 28 Abs. 2, 4' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.1.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
{ field: 'architectureProfile.subprocessorCount', operator: 'GREATER_THAN', value: 3, result: 'REQUIRED', priority: 20 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Sub-Processor-Register', 'Genehmigungsverfahren', 'Vertragsdokumentation'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['order-control', 'sub-processor'],
|
||||
},
|
||||
|
||||
// RESILIENCE — 2 new
|
||||
{
|
||||
id: 'TOM-RE-04',
|
||||
code: 'TOM-RE-04',
|
||||
category: 'RESILIENCE',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'DDoS-Abwehr (erweitert)', en: 'DDoS Mitigation (Advanced)' },
|
||||
description: {
|
||||
de: 'Erweiterte DDoS-Schutzmassnahmen inkl. Traffic-Analyse, automatischer Mitigation und Incident-Response-Integration.',
|
||||
en: 'Advanced DDoS protection measures including traffic analysis, automatic mitigation and incident response integration.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'EQUALS', value: 'VERY_HIGH', result: 'REQUIRED', priority: 25 },
|
||||
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'RECOMMENDED', priority: 15 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['DDoS-Schutzkonzept (erweitert)', 'Mitigation-Berichte', 'Incident-Playbooks'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'HIGH',
|
||||
complexity: 'HIGH',
|
||||
tags: ['resilience', 'ddos', 'advanced'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-RE-05',
|
||||
code: 'TOM-RE-05',
|
||||
category: 'RESILIENCE',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Kapazitaetsplanung', en: 'Capacity Planning' },
|
||||
description: {
|
||||
de: 'Systematische Planung und Ueberwachung von IT-Kapazitaeten zur Sicherstellung der Systemverfuegbarkeit bei wachsender Nutzung.',
|
||||
en: 'Systematic planning and monitoring of IT capacities to ensure system availability with growing usage.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.1.3' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'dataProfile.dataVolume', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 20 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Kapazitaetsplan', 'Trend-Analysen', 'Skalierungskonzept'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['resilience', 'capacity', 'planning'],
|
||||
},
|
||||
|
||||
// RECOVERY — 2 new
|
||||
{
|
||||
id: 'TOM-RC-04',
|
||||
code: 'TOM-RC-04',
|
||||
category: 'RECOVERY',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Georedundantes Backup', en: 'Geo-Redundant Backup' },
|
||||
description: {
|
||||
de: 'Speicherung von Backup-Kopien an geografisch getrennten Standorten zum Schutz vor standortbezogenen Katastrophen.',
|
||||
en: 'Storage of backup copies at geographically separated locations to protect against site-specific disasters.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. c' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.3.1' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'CON.3' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
|
||||
{ field: 'riskProfile.ciaAssessment.availability', operator: 'GREATER_THAN', value: 3, result: 'REQUIRED', priority: 20 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Georedundanz-Konzept', 'Backup-Standort-Dokumentation', 'Wiederherstellungstests'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'HIGH',
|
||||
tags: ['recovery', 'backup', 'geo-redundancy'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-RC-05',
|
||||
code: 'TOM-RC-05',
|
||||
category: 'RECOVERY',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Notfallwiederherstellungs-Tests', en: 'Disaster Recovery Testing' },
|
||||
description: {
|
||||
de: 'Regelmaessige Durchfuehrung und Dokumentation von Notfallwiederherstellungstests zur Validierung der RTO/RPO-Ziele.',
|
||||
en: 'Regular execution and documentation of disaster recovery tests to validate RTO/RPO targets.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. c, d' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.17.1.3' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'securityProfile.hasDRPlan', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['DR-Testberichte', 'RTO/RPO-Messungen', 'Verbesserungsmassnahmen'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['recovery', 'dr-testing', 'rto', 'rpo'],
|
||||
},
|
||||
|
||||
// SEPARATION — 2 new
|
||||
{
|
||||
id: 'TOM-SE-05',
|
||||
code: 'TOM-SE-05',
|
||||
category: 'SEPARATION',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Netzwerksegmentierung', en: 'Network Segmentation' },
|
||||
description: {
|
||||
de: 'Aufteilung des Netzwerks in separate Sicherheitszonen mit kontrollierten Uebergaengen zur Begrenzung der Ausbreitung von Sicherheitsvorfaellen.',
|
||||
en: 'Division of the network into separate security zones with controlled transitions to limit the spread of security incidents.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.3' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'NET.1.1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['ON_PREMISE', 'PRIVATE_CLOUD', 'HYBRID'], result: 'REQUIRED', priority: 20 },
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Netzwerkplan', 'Firewall-Regeln', 'Segmentierungskonzept'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'HIGH',
|
||||
tags: ['separation', 'network', 'segmentation'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-SE-06',
|
||||
code: 'TOM-SE-06',
|
||||
category: 'SEPARATION',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Mandantenisolierung in Cloud', en: 'Tenant Isolation in Cloud' },
|
||||
description: {
|
||||
de: 'Technische Sicherstellung der vollstaendigen Datentrennung zwischen verschiedenen Mandanten in Multi-Tenant-Cloud-Umgebungen.',
|
||||
en: 'Technical assurance of complete data separation between different tenants in multi-tenant cloud environments.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.1.3' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.multiTenancy', operator: 'EQUALS', value: 'MULTI_TENANT', result: 'REQUIRED', priority: 30 },
|
||||
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'RECOMMENDED', priority: 15 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['Mandantentrennungskonzept', 'Isolierungstests', 'Cloud-Security-Assessment'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'CRITICAL',
|
||||
complexity: 'HIGH',
|
||||
tags: ['separation', 'multi-tenant', 'cloud'],
|
||||
},
|
||||
|
||||
// ACCESS_CONTROL — 1 new
|
||||
{
|
||||
id: 'TOM-AC-06',
|
||||
code: 'TOM-AC-06',
|
||||
category: 'ACCESS_CONTROL',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Besuchermanagement (erweitert)', en: 'Visitor Management (Extended)' },
|
||||
description: {
|
||||
de: 'Erweitertes Besuchermanagement mit Voranmeldung, Identitaetspruefung, Begleitpflicht und zeitlich begrenztem Zugang zu sicherheitsrelevanten Bereichen.',
|
||||
en: 'Extended visitor management with pre-registration, identity verification, escort requirement and time-limited access to security-relevant areas.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.7.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 20 },
|
||||
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'RECOMMENDED', priority: 15 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['Besuchermanagement-Richtlinie', 'Besucherprotokolle', 'Zonenkonzept'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'LOW',
|
||||
tags: ['physical-security', 'visitors', 'extended'],
|
||||
},
|
||||
|
||||
// ADMISSION_CONTROL — 1 new
|
||||
{
|
||||
id: 'TOM-ADM-06',
|
||||
code: 'TOM-ADM-06',
|
||||
category: 'ADMISSION_CONTROL',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Endpoint Detection & Response (EDR)', en: 'Endpoint Detection & Response (EDR)' },
|
||||
description: {
|
||||
de: 'Einsatz von EDR-Loesungen zur Erkennung und Abwehr von Bedrohungen auf Endgeraeten in Echtzeit.',
|
||||
en: 'Deployment of EDR solutions for real-time threat detection and response on endpoints.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.2.1' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'OPS.1.1.4' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'REQUIRED', priority: 25 },
|
||||
{ field: 'companyProfile.size', operator: 'IN', value: ['LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['EDR-Konfiguration', 'Bedrohungsberichte', 'Incident-Response-Statistiken'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'HIGH',
|
||||
complexity: 'HIGH',
|
||||
tags: ['endpoint', 'edr', 'threat-detection'],
|
||||
},
|
||||
|
||||
// ACCESS_AUTHORIZATION — 2 new
|
||||
{
|
||||
id: 'TOM-AZ-06',
|
||||
code: 'TOM-AZ-06',
|
||||
category: 'ACCESS_AUTHORIZATION',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'API-Zugriffskontrolle', en: 'API Access Control' },
|
||||
description: {
|
||||
de: 'Implementierung von Authentifizierungs- und Autorisierungsmechanismen fuer APIs (OAuth 2.0, API-Keys, Rate Limiting).',
|
||||
en: 'Implementation of authentication and authorization mechanisms for APIs (OAuth 2.0, API keys, rate limiting).',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.9.4.1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'architectureProfile.hostingModel', operator: 'IN', value: ['PUBLIC_CLOUD', 'HYBRID'], result: 'REQUIRED', priority: 20 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['API-Security-Konzept', 'OAuth-Konfiguration', 'Rate-Limiting-Regeln'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['authorization', 'api', 'oauth'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-AZ-07',
|
||||
code: 'TOM-AZ-07',
|
||||
category: 'ACCESS_AUTHORIZATION',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Regelmaessiger Berechtigungsreview', en: 'Regular Permission Review' },
|
||||
description: {
|
||||
de: 'Systematische Ueberpruefung und Bereinigung von Zugriffsberechtigungen in regelmaessigen Abstaenden durch die jeweiligen Fachverantwortlichen.',
|
||||
en: 'Systematic review and cleanup of access permissions at regular intervals by the respective department heads.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.9.2.5' },
|
||||
],
|
||||
applicabilityConditions: [],
|
||||
defaultApplicability: 'REQUIRED',
|
||||
evidenceRequirements: ['Review-Protokolle', 'Berechtigungsaenderungslog', 'Freigabedokumentation'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'LOW',
|
||||
tags: ['authorization', 'review', 'permissions'],
|
||||
},
|
||||
|
||||
// TRANSFER_CONTROL — 2 new
|
||||
{
|
||||
id: 'TOM-TR-06',
|
||||
code: 'TOM-TR-06',
|
||||
category: 'TRANSFER_CONTROL',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'E-Mail-Verschluesselung (erweitert)', en: 'Email Encryption (Extended)' },
|
||||
description: {
|
||||
de: 'Erweiterte E-Mail-Verschluesselung mit automatischer Erkennung sensibler Inhalte und erzwungener Gateway-Verschluesselung.',
|
||||
en: 'Extended email encryption with automatic detection of sensitive content and enforced gateway encryption.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. a' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.13.2.3' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['E-Mail-Verschluesselungs-Policy', 'Gateway-Konfiguration', 'DLP-Regeln'],
|
||||
reviewFrequency: 'SEMI_ANNUAL',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['transfer', 'email', 'encryption'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-TR-07',
|
||||
code: 'TOM-TR-07',
|
||||
category: 'TRANSFER_CONTROL',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Drittstaat-Transferbewertung', en: 'Third Country Transfer Assessment' },
|
||||
description: {
|
||||
de: 'Dokumentierte Bewertung und Absicherung von Datenuebermittlungen in Drittstaaten gemaess Art. 44-49 DSGVO (Standardvertragsklauseln, TIA).',
|
||||
en: 'Documented assessment and safeguarding of data transfers to third countries according to Art. 44-49 GDPR (Standard Contractual Clauses, TIA).',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 44-49' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.1.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'dataProfile.thirdCountryTransfers', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 30 },
|
||||
{ field: 'architectureProfile.hostingLocation', operator: 'IN', value: ['THIRD_COUNTRY_ADEQUATE', 'THIRD_COUNTRY'], result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['Transfer Impact Assessment', 'Standardvertragsklauseln', 'Angemessenheitsbeschluss-Pruefung'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'CRITICAL',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['transfer', 'third-country', 'schrems-ii'],
|
||||
},
|
||||
|
||||
// AVAILABILITY — 2 new
|
||||
{
|
||||
id: 'TOM-AV-06',
|
||||
code: 'TOM-AV-06',
|
||||
category: 'AVAILABILITY',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Monitoring und Alerting', en: 'Monitoring and Alerting' },
|
||||
description: {
|
||||
de: 'Implementierung einer umfassenden Ueberwachung aller IT-Systeme mit automatischen Benachrichtigungen bei Stoerungen oder Schwellenwert-Ueberschreitungen.',
|
||||
en: 'Implementation of comprehensive monitoring of all IT systems with automatic notifications for disruptions or threshold violations.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.12.4.1' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'OPS.1.1.2' },
|
||||
],
|
||||
applicabilityConditions: [],
|
||||
defaultApplicability: 'REQUIRED',
|
||||
evidenceRequirements: ['Monitoring-Konzept', 'Alerting-Konfiguration', 'Eskalationsmatrix'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['availability', 'monitoring', 'alerting'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-AV-07',
|
||||
code: 'TOM-AV-07',
|
||||
category: 'AVAILABILITY',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Service Level Management', en: 'Service Level Management' },
|
||||
description: {
|
||||
de: 'Definition und Ueberwachung von Service Level Agreements (SLAs) fuer alle kritischen IT-Services mit klaren Verfuegbarkeitszielen.',
|
||||
en: 'Definition and monitoring of Service Level Agreements (SLAs) for all critical IT services with clear availability targets.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. b' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.15.2.1' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'companyProfile.size', operator: 'IN', value: ['MEDIUM', 'LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
|
||||
{ field: 'architectureProfile.hasSubprocessors', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 20 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['SLA-Dokumentation', 'Verfuegbarkeitsberichte', 'Eskalationsverfahren'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'LOW',
|
||||
tags: ['availability', 'sla', 'service-management'],
|
||||
},
|
||||
|
||||
// SEPARATION — 1 more new (TOM-DL-05)
|
||||
{
|
||||
id: 'TOM-DL-05',
|
||||
code: 'TOM-DL-05',
|
||||
category: 'SEPARATION',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Datenloesch-Audit', en: 'Data Deletion Audit' },
|
||||
description: {
|
||||
de: 'Regelmaessige Ueberpruefung der Wirksamkeit und Vollstaendigkeit von Datenloeschvorgaengen durch unabhaengige Stellen.',
|
||||
en: 'Regular review of the effectiveness and completeness of data deletion processes by independent parties.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 5 Abs. 1 lit. e' },
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 17' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.8.3.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'dataProfile.hasSpecialCategories', operator: 'EQUALS', value: true, result: 'REQUIRED', priority: 25 },
|
||||
],
|
||||
defaultApplicability: 'RECOMMENDED',
|
||||
evidenceRequirements: ['Audit-Berichte', 'Loeschprotokolle', 'Stichproben-Ergebnisse'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['separation', 'deletion', 'audit'],
|
||||
},
|
||||
|
||||
// REVIEW — 3 new
|
||||
{
|
||||
id: 'TOM-RV-09',
|
||||
code: 'TOM-RV-09',
|
||||
category: 'REVIEW',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Datenschutz-Audit-Programm', en: 'Data Protection Audit Program' },
|
||||
description: {
|
||||
de: 'Systematisches Programm zur regelmaessigen internen Ueberpruefung aller Datenschutzmassnahmen mit dokumentierten Ergebnissen und Massnahmenverfolgung.',
|
||||
en: 'Systematic program for regular internal review of all data protection measures with documented results and action tracking.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.1' },
|
||||
{ framework: 'BSI_IT_GRUNDSCHUTZ', reference: 'DER.3.1' },
|
||||
],
|
||||
applicabilityConditions: [],
|
||||
defaultApplicability: 'REQUIRED',
|
||||
evidenceRequirements: ['Audit-Programm', 'Audit-Berichte', 'Massnahmenplan'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'MEDIUM',
|
||||
tags: ['review', 'audit', 'data-protection'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-RV-10',
|
||||
code: 'TOM-RV-10',
|
||||
category: 'REVIEW',
|
||||
type: 'TECHNICAL',
|
||||
name: { de: 'Automatisierte Compliance-Pruefung', en: 'Automated Compliance Checking' },
|
||||
description: {
|
||||
de: 'Einsatz automatisierter Tools zur kontinuierlichen Ueberpruefung der Einhaltung von Sicherheits- und Datenschutzrichtlinien.',
|
||||
en: 'Use of automated tools for continuous monitoring of compliance with security and data protection policies.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.2' },
|
||||
],
|
||||
applicabilityConditions: [
|
||||
{ field: 'companyProfile.size', operator: 'IN', value: ['MEDIUM', 'LARGE', 'ENTERPRISE'], result: 'RECOMMENDED', priority: 10 },
|
||||
{ field: 'riskProfile.protectionLevel', operator: 'IN', value: ['HIGH', 'VERY_HIGH'], result: 'RECOMMENDED', priority: 15 },
|
||||
],
|
||||
defaultApplicability: 'OPTIONAL',
|
||||
evidenceRequirements: ['Tool-Konfiguration', 'Compliance-Dashboard', 'Automatisierte Berichte'],
|
||||
reviewFrequency: 'QUARTERLY',
|
||||
priority: 'MEDIUM',
|
||||
complexity: 'HIGH',
|
||||
tags: ['review', 'automation', 'compliance'],
|
||||
},
|
||||
{
|
||||
id: 'TOM-RV-11',
|
||||
code: 'TOM-RV-11',
|
||||
category: 'REVIEW',
|
||||
type: 'ORGANIZATIONAL',
|
||||
name: { de: 'Management Review (Art. 32 Abs. 1 lit. d)', en: 'Management Review (Art. 32(1)(d))' },
|
||||
description: {
|
||||
de: 'Regelmaessige Ueberpruefung der Wirksamkeit aller technischen und organisatorischen Massnahmen durch die Geschaeftsfuehrung mit dokumentierten Ergebnissen.',
|
||||
en: 'Regular review of the effectiveness of all technical and organizational measures by management with documented results.',
|
||||
},
|
||||
mappings: [
|
||||
{ framework: 'GDPR_ART32', reference: 'Art. 32 Abs. 1 lit. d' },
|
||||
{ framework: 'ISO27001_ANNEX_A', reference: 'A.18.2.1' },
|
||||
],
|
||||
applicabilityConditions: [],
|
||||
defaultApplicability: 'REQUIRED',
|
||||
evidenceRequirements: ['Management-Review-Protokolle', 'Massnahmenplan', 'Wirksamkeitsbewertung'],
|
||||
reviewFrequency: 'ANNUAL',
|
||||
priority: 'HIGH',
|
||||
complexity: 'LOW',
|
||||
tags: ['review', 'management', 'effectiveness'],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -320,3 +320,158 @@ export async function generateVideo(moduleId: string): Promise<TrainingMedia> {
|
||||
export async function previewVideoScript(moduleId: string): Promise<{ title: string; sections: Array<{ heading: string; text: string; bullet_points: string[] }> }> {
|
||||
return apiFetch(`/content/${moduleId}/preview-script`, { method: 'POST' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRAINING BLOCKS (Controls → Schulungsmodule)
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
TrainingBlockConfig,
|
||||
CanonicalControlSummary,
|
||||
CanonicalControlMeta,
|
||||
BlockPreview,
|
||||
BlockGenerateResult,
|
||||
TrainingBlockControlLink,
|
||||
} from './types'
|
||||
|
||||
export async function listBlockConfigs(): Promise<{ blocks: TrainingBlockConfig[]; total: number }> {
|
||||
return apiFetch('/blocks')
|
||||
}
|
||||
|
||||
export async function createBlockConfig(data: {
|
||||
name: string
|
||||
description?: string
|
||||
domain_filter?: string
|
||||
category_filter?: string
|
||||
severity_filter?: string
|
||||
target_audience_filter?: string
|
||||
regulation_area: string
|
||||
module_code_prefix: string
|
||||
frequency_type?: string
|
||||
duration_minutes?: number
|
||||
pass_threshold?: number
|
||||
max_controls_per_module?: number
|
||||
}): Promise<TrainingBlockConfig> {
|
||||
return apiFetch<TrainingBlockConfig>('/blocks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getBlockConfig(id: string): Promise<TrainingBlockConfig> {
|
||||
return apiFetch<TrainingBlockConfig>(`/blocks/${id}`)
|
||||
}
|
||||
|
||||
export async function updateBlockConfig(id: string, data: Record<string, unknown>): Promise<TrainingBlockConfig> {
|
||||
return apiFetch<TrainingBlockConfig>(`/blocks/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteBlockConfig(id: string): Promise<void> {
|
||||
return apiFetch(`/blocks/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export async function previewBlock(id: string): Promise<BlockPreview> {
|
||||
return apiFetch<BlockPreview>(`/blocks/${id}/preview`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function generateBlock(id: string, data?: {
|
||||
language?: string
|
||||
auto_matrix?: boolean
|
||||
}): Promise<BlockGenerateResult> {
|
||||
return apiFetch<BlockGenerateResult>(`/blocks/${id}/generate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data || { language: 'de', auto_matrix: true }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getBlockControls(id: string): Promise<{ controls: TrainingBlockControlLink[]; total: number }> {
|
||||
return apiFetch(`/blocks/${id}/controls`)
|
||||
}
|
||||
|
||||
export async function listCanonicalControls(filters?: {
|
||||
domain?: string
|
||||
category?: string
|
||||
severity?: string
|
||||
target_audience?: string
|
||||
}): Promise<{ controls: CanonicalControlSummary[]; total: number }> {
|
||||
const params = new URLSearchParams()
|
||||
if (filters?.domain) params.set('domain', filters.domain)
|
||||
if (filters?.category) params.set('category', filters.category)
|
||||
if (filters?.severity) params.set('severity', filters.severity)
|
||||
if (filters?.target_audience) params.set('target_audience', filters.target_audience)
|
||||
const qs = params.toString()
|
||||
return apiFetch(`/canonical/controls${qs ? `?${qs}` : ''}`)
|
||||
}
|
||||
|
||||
export async function getCanonicalMeta(): Promise<CanonicalControlMeta> {
|
||||
return apiFetch<CanonicalControlMeta>('/canonical/meta')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CERTIFICATES
|
||||
// =============================================================================
|
||||
|
||||
export async function generateCertificate(assignmentId: string): Promise<{ certificate_id: string; assignment: TrainingAssignment }> {
|
||||
return apiFetch(`/certificates/generate/${assignmentId}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function listCertificates(): Promise<{ certificates: TrainingAssignment[]; total: number }> {
|
||||
return apiFetch('/certificates')
|
||||
}
|
||||
|
||||
export async function downloadCertificatePDF(certificateId: string): Promise<Blob> {
|
||||
const res = await fetch(`${BASE_URL}/certificates/${certificateId}/pdf`, {
|
||||
headers: {
|
||||
'X-Tenant-ID': typeof window !== 'undefined'
|
||||
? (localStorage.getItem('bp-tenant-id') || 'default')
|
||||
: 'default',
|
||||
},
|
||||
})
|
||||
if (!res.ok) throw new Error(`PDF download failed: ${res.status}`)
|
||||
return res.blob()
|
||||
}
|
||||
|
||||
export async function verifyCertificate(certificateId: string): Promise<{ valid: boolean; assignment: TrainingAssignment }> {
|
||||
return apiFetch(`/certificates/${certificateId}/verify`)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MEDIA STREAMING
|
||||
// =============================================================================
|
||||
|
||||
export function getMediaStreamURL(mediaId: string): string {
|
||||
return `${BASE_URL}/media/${mediaId}/stream`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERACTIVE VIDEO
|
||||
// =============================================================================
|
||||
|
||||
import type {
|
||||
InteractiveVideoManifest,
|
||||
CheckpointQuizResult,
|
||||
CheckpointProgress,
|
||||
} from './types'
|
||||
|
||||
export async function generateInteractiveVideo(moduleId: string): Promise<TrainingMedia> {
|
||||
return apiFetch<TrainingMedia>(`/content/${moduleId}/generate-interactive`, { method: 'POST' })
|
||||
}
|
||||
|
||||
export async function getInteractiveManifest(moduleId: string, assignmentId?: string): Promise<InteractiveVideoManifest> {
|
||||
const qs = assignmentId ? `?assignment_id=${assignmentId}` : ''
|
||||
return apiFetch<InteractiveVideoManifest>(`/content/${moduleId}/interactive-manifest${qs}`)
|
||||
}
|
||||
|
||||
export async function submitCheckpointQuiz(checkpointId: string, assignmentId: string, answers: number[]): Promise<CheckpointQuizResult> {
|
||||
return apiFetch<CheckpointQuizResult>(`/checkpoints/${checkpointId}/submit`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ assignment_id: assignmentId, answers }),
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCheckpointProgress(assignmentId: string): Promise<{ progress: CheckpointProgress[]; total: number }> {
|
||||
return apiFetch(`/checkpoints/progress/${assignmentId}`)
|
||||
}
|
||||
|
||||
@@ -65,9 +65,17 @@ export const ROLE_LABELS: Record<string, string> = {
|
||||
R7: 'Fachabteilung',
|
||||
R8: 'IT-Administration',
|
||||
R9: 'Alle Mitarbeiter',
|
||||
R10: 'Behoerden / Oeffentlicher Dienst',
|
||||
}
|
||||
|
||||
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9'] as const
|
||||
export const ALL_ROLES = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9', 'R10'] as const
|
||||
|
||||
export const TARGET_AUDIENCE_LABELS: Record<string, string> = {
|
||||
enterprise: 'Unternehmen',
|
||||
authority: 'Behoerden',
|
||||
provider: 'IT-Dienstleister',
|
||||
all: 'Alle',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN ENTITIES
|
||||
@@ -273,7 +281,7 @@ export interface QuizSubmitResponse {
|
||||
// MEDIA (Audio/Video)
|
||||
// =============================================================================
|
||||
|
||||
export type MediaType = 'audio' | 'video'
|
||||
export type MediaType = 'audio' | 'video' | 'interactive_video'
|
||||
export type MediaStatus = 'processing' | 'completed' | 'failed'
|
||||
|
||||
export interface TrainingMedia {
|
||||
@@ -307,3 +315,121 @@ export interface VideoScriptSection {
|
||||
text: string
|
||||
bullet_points: string[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TRAINING BLOCKS (Controls → Schulungsmodule)
|
||||
// =============================================================================
|
||||
|
||||
export interface TrainingBlockConfig {
|
||||
id: string
|
||||
tenant_id: string
|
||||
name: string
|
||||
description?: string
|
||||
domain_filter?: string
|
||||
category_filter?: string
|
||||
severity_filter?: string
|
||||
target_audience_filter?: string
|
||||
regulation_area: RegulationArea
|
||||
module_code_prefix: string
|
||||
frequency_type: FrequencyType
|
||||
duration_minutes: number
|
||||
pass_threshold: number
|
||||
max_controls_per_module: number
|
||||
is_active: boolean
|
||||
last_generated_at?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CanonicalControlSummary {
|
||||
control_id: string
|
||||
title: string
|
||||
objective: string
|
||||
rationale: string
|
||||
requirements: string[]
|
||||
severity: string
|
||||
category: string
|
||||
target_audience: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface CanonicalControlMeta {
|
||||
domains: { domain: string; count: number }[]
|
||||
categories: { category: string; count: number }[]
|
||||
audiences: { audience: string; count: number }[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface BlockPreview {
|
||||
control_count: number
|
||||
module_count: number
|
||||
controls: CanonicalControlSummary[]
|
||||
proposed_roles: string[]
|
||||
}
|
||||
|
||||
export interface BlockGenerateResult {
|
||||
modules_created: number
|
||||
controls_linked: number
|
||||
matrix_entries_created: number
|
||||
content_generated: number
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export interface TrainingBlockControlLink {
|
||||
id: string
|
||||
block_config_id: string
|
||||
module_id: string
|
||||
control_id: string
|
||||
control_title: string
|
||||
control_objective: string
|
||||
control_requirements: string[]
|
||||
sort_order: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INTERACTIVE VIDEO / CHECKPOINTS
|
||||
// =============================================================================
|
||||
|
||||
export interface InteractiveVideoManifest {
|
||||
media_id: string
|
||||
stream_url: string
|
||||
checkpoints: CheckpointEntry[]
|
||||
}
|
||||
|
||||
export interface CheckpointEntry {
|
||||
checkpoint_id: string
|
||||
index: number
|
||||
title: string
|
||||
timestamp_seconds: number
|
||||
questions: CheckpointQuestion[]
|
||||
progress?: CheckpointProgress
|
||||
}
|
||||
|
||||
export interface CheckpointQuestion {
|
||||
question: string
|
||||
options: string[]
|
||||
correct_index: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
export interface CheckpointProgress {
|
||||
id: string
|
||||
assignment_id: string
|
||||
checkpoint_id: string
|
||||
passed: boolean
|
||||
attempts: number
|
||||
last_attempt_at?: string
|
||||
}
|
||||
|
||||
export interface CheckpointQuizResult {
|
||||
passed: boolean
|
||||
score: number
|
||||
feedback: CheckpointQuizFeedback[]
|
||||
}
|
||||
|
||||
export interface CheckpointQuizFeedback {
|
||||
question: string
|
||||
correct: boolean
|
||||
explanation: string
|
||||
}
|
||||
|
||||
2624
admin-compliance/lib/sdk/types.ts
Normal file
2624
admin-compliance/lib/sdk/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import React, {
|
||||
|
||||
import {
|
||||
VendorComplianceContextValue,
|
||||
ProcessingActivity,
|
||||
VendorStatistics,
|
||||
ComplianceStatistics,
|
||||
RiskOverview,
|
||||
@@ -199,6 +200,245 @@ export function VendorComplianceProvider({
|
||||
}
|
||||
}, [state.vendors, state.findings])
|
||||
|
||||
// ==========================================
|
||||
// API CALLS
|
||||
// ==========================================
|
||||
|
||||
const apiBase = '/api/sdk/v1/vendor-compliance'
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const [
|
||||
activitiesRes,
|
||||
vendorsRes,
|
||||
contractsRes,
|
||||
findingsRes,
|
||||
controlsRes,
|
||||
controlInstancesRes,
|
||||
] = await Promise.all([
|
||||
fetch(`${apiBase}/processing-activities`),
|
||||
fetch(`${apiBase}/vendors`),
|
||||
fetch(`${apiBase}/contracts`),
|
||||
fetch(`${apiBase}/findings`),
|
||||
fetch(`${apiBase}/controls`),
|
||||
fetch(`${apiBase}/control-instances`),
|
||||
])
|
||||
|
||||
if (activitiesRes.ok) {
|
||||
const data = await activitiesRes.json()
|
||||
dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (vendorsRes.ok) {
|
||||
const data = await vendorsRes.json()
|
||||
dispatch({ type: 'SET_VENDORS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (contractsRes.ok) {
|
||||
const data = await contractsRes.json()
|
||||
dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (findingsRes.ok) {
|
||||
const data = await findingsRes.json()
|
||||
dispatch({ type: 'SET_FINDINGS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (controlsRes.ok) {
|
||||
const data = await controlsRes.json()
|
||||
dispatch({ type: 'SET_CONTROLS', payload: data.data || [] })
|
||||
}
|
||||
|
||||
if (controlInstancesRes.ok) {
|
||||
const data = await controlInstancesRes.json()
|
||||
dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vendor compliance data:', error)
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: 'Fehler beim Laden der Daten',
|
||||
})
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
}, [apiBase])
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await loadData()
|
||||
}, [loadData])
|
||||
|
||||
// ==========================================
|
||||
// PROCESSING ACTIVITIES ACTIONS
|
||||
// ==========================================
|
||||
|
||||
const createProcessingActivity = useCallback(
|
||||
async (
|
||||
data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
||||
): Promise<ProcessingActivity> => {
|
||||
const response = await fetch(`${apiBase}/processing-activities`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
const activity = result.data
|
||||
|
||||
dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: activity })
|
||||
|
||||
return activity
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
const deleteProcessingActivity = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const response = await fetch(`${apiBase}/processing-activities/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id })
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
const duplicateProcessingActivity = useCallback(
|
||||
async (id: string): Promise<ProcessingActivity> => {
|
||||
const original = state.processingActivities.find((a) => a.id === id)
|
||||
if (!original) {
|
||||
throw new Error('Verarbeitungstätigkeit nicht gefunden')
|
||||
}
|
||||
|
||||
const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original
|
||||
|
||||
const newActivity = await createProcessingActivity({
|
||||
...rest,
|
||||
vvtId: '', // Will be generated by backend
|
||||
name: {
|
||||
de: `${original.name.de} (Kopie)`,
|
||||
en: `${original.name.en} (Copy)`,
|
||||
},
|
||||
status: 'DRAFT',
|
||||
})
|
||||
|
||||
return newActivity
|
||||
},
|
||||
[state.processingActivities, createProcessingActivity]
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// VENDOR ACTIONS
|
||||
// ==========================================
|
||||
|
||||
const deleteVendor = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const response = await fetch(`${apiBase}/vendors/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen des Vendors')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_VENDOR', payload: id })
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// CONTRACT ACTIONS
|
||||
// ==========================================
|
||||
|
||||
const deleteContract = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
const contract = state.contracts.find((c) => c.id === id)
|
||||
|
||||
const response = await fetch(`${apiBase}/contracts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Löschen des Vertrags')
|
||||
}
|
||||
|
||||
dispatch({ type: 'DELETE_CONTRACT', payload: id })
|
||||
|
||||
// Update vendor's contracts list
|
||||
if (contract) {
|
||||
const vendor = state.vendors.find((v) => v.id === contract.vendorId)
|
||||
if (vendor) {
|
||||
dispatch({
|
||||
type: 'UPDATE_VENDOR',
|
||||
payload: {
|
||||
id: vendor.id,
|
||||
data: { contracts: vendor.contracts.filter((cId) => cId !== id) },
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[apiBase, state.contracts, state.vendors]
|
||||
)
|
||||
|
||||
const startContractReview = useCallback(
|
||||
async (contractId: string): Promise<void> => {
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } },
|
||||
})
|
||||
|
||||
const response = await fetch(`${apiBase}/contracts/${contractId}/review`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: { id: contractId, data: { reviewStatus: 'FAILED' } },
|
||||
})
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Update contract with review results
|
||||
dispatch({
|
||||
type: 'UPDATE_CONTRACT',
|
||||
payload: {
|
||||
id: contractId,
|
||||
data: {
|
||||
reviewStatus: 'COMPLETED',
|
||||
reviewCompletedAt: new Date(),
|
||||
complianceScore: result.data.complianceScore,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Add findings
|
||||
if (result.data.findings && result.data.findings.length > 0) {
|
||||
dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings })
|
||||
}
|
||||
},
|
||||
[apiBase]
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// INITIALIZATION
|
||||
// ==========================================
|
||||
@@ -221,9 +461,27 @@ export function VendorComplianceProvider({
|
||||
vendorStats,
|
||||
complianceStats,
|
||||
riskOverview,
|
||||
...actions,
|
||||
deleteProcessingActivity,
|
||||
duplicateProcessingActivity,
|
||||
deleteVendor,
|
||||
deleteContract,
|
||||
startContractReview,
|
||||
loadData,
|
||||
refresh,
|
||||
}),
|
||||
[state, vendorStats, complianceStats, riskOverview, actions]
|
||||
[
|
||||
state,
|
||||
vendorStats,
|
||||
complianceStats,
|
||||
riskOverview,
|
||||
deleteProcessingActivity,
|
||||
duplicateProcessingActivity,
|
||||
deleteVendor,
|
||||
deleteContract,
|
||||
startContractReview,
|
||||
loadData,
|
||||
refresh,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -232,3 +490,20 @@ export function VendorComplianceProvider({
|
||||
</VendorComplianceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HOOK
|
||||
// ==========================================
|
||||
|
||||
export function useVendorCompliance(): VendorComplianceContextValue {
|
||||
const context = useContext(VendorComplianceContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useVendorCompliance must be used within a VendorComplianceProvider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,6 @@ export * from './types'
|
||||
export {
|
||||
VendorComplianceProvider,
|
||||
useVendorCompliance,
|
||||
useVendor,
|
||||
useProcessingActivity,
|
||||
useVendorContracts,
|
||||
useVendorFindings,
|
||||
useContractFindings,
|
||||
useControlInstancesForEntity,
|
||||
} from './context'
|
||||
|
||||
// ==========================================
|
||||
|
||||
1190
admin-compliance/lib/sdk/vendor-compliance/types.ts
Normal file
1190
admin-compliance/lib/sdk/vendor-compliance/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -103,25 +103,21 @@ export interface VVTActivity {
|
||||
owner: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Processor-Record (Art. 30 Abs. 2)
|
||||
export interface VVTProcessorActivity {
|
||||
id: string
|
||||
vvtId: string
|
||||
controllerReference: string
|
||||
processingCategories: string[]
|
||||
subProcessorChain: SubProcessor[]
|
||||
thirdCountryTransfers: { country: string; recipient: string; transferMechanism: string }[]
|
||||
tomDescription: string
|
||||
status: 'DRAFT' | 'REVIEW' | 'APPROVED' | 'ARCHIVED'
|
||||
}
|
||||
|
||||
export interface SubProcessor {
|
||||
name: string
|
||||
purpose: string
|
||||
country: string
|
||||
isThirdCountry: boolean
|
||||
// Library refs (optional, parallel to freetext)
|
||||
purposeRefs?: string[]
|
||||
legalBasisRefs?: string[]
|
||||
dataSubjectRefs?: string[]
|
||||
dataCategoryRefs?: string[]
|
||||
recipientRefs?: string[]
|
||||
retentionRuleRef?: string
|
||||
transferMechanismRefs?: string[]
|
||||
tomRefs?: string[]
|
||||
linkedLoeschfristenIds?: string[]
|
||||
linkedTomMeasureIds?: string[]
|
||||
sourceTemplateId?: string
|
||||
riskScore?: number
|
||||
art30Completeness?: VVTCompleteness
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -186,6 +182,14 @@ export const ART9_CATEGORIES: string[] = [
|
||||
'CRIMINAL_DATA',
|
||||
]
|
||||
|
||||
export interface VVTCompleteness {
|
||||
score: number
|
||||
missing: string[]
|
||||
warnings: string[]
|
||||
passed: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER: Create empty activity
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user