fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
324
admin-v2/lib/sdk/__tests__/export.test.ts
Normal file
324
admin-v2/lib/sdk/__tests__/export.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { exportToPDF, exportToZIP, downloadExport } from '../export'
|
||||
import type { SDKState } from '../types'
|
||||
|
||||
// Mock jsPDF as a class
|
||||
vi.mock('jspdf', () => {
|
||||
return {
|
||||
default: class MockJsPDF {
|
||||
internal = {
|
||||
pageSize: { getWidth: () => 210, getHeight: () => 297 },
|
||||
}
|
||||
setFillColor = vi.fn().mockReturnThis()
|
||||
setDrawColor = vi.fn().mockReturnThis()
|
||||
setTextColor = vi.fn().mockReturnThis()
|
||||
setFontSize = vi.fn().mockReturnThis()
|
||||
setFont = vi.fn().mockReturnThis()
|
||||
setLineWidth = vi.fn().mockReturnThis()
|
||||
text = vi.fn().mockReturnThis()
|
||||
line = vi.fn().mockReturnThis()
|
||||
rect = vi.fn().mockReturnThis()
|
||||
roundedRect = vi.fn().mockReturnThis()
|
||||
circle = vi.fn().mockReturnThis()
|
||||
addPage = vi.fn().mockReturnThis()
|
||||
setPage = vi.fn().mockReturnThis()
|
||||
getNumberOfPages = vi.fn(() => 5)
|
||||
splitTextToSize = vi.fn((text: string) => [text])
|
||||
output = vi.fn(() => new Blob(['mock-pdf'], { type: 'application/pdf' }))
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// Mock JSZip as a class
|
||||
vi.mock('jszip', () => {
|
||||
return {
|
||||
default: class MockJSZip {
|
||||
private mockFolder = {
|
||||
file: vi.fn().mockReturnThis(),
|
||||
folder: vi.fn(() => this.mockFolder),
|
||||
}
|
||||
folder = vi.fn(() => this.mockFolder)
|
||||
generateAsync = vi.fn(() => Promise.resolve(new Blob(['mock-zip'], { type: 'application/zip' })))
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const createMockState = (overrides: Partial<SDKState> = {}): SDKState => ({
|
||||
version: '1.0.0',
|
||||
lastModified: new Date('2024-01-15'),
|
||||
tenantId: 'test-tenant',
|
||||
userId: 'test-user',
|
||||
subscription: 'PROFESSIONAL',
|
||||
currentPhase: 1,
|
||||
currentStep: 'use-case-workshop',
|
||||
completedSteps: ['use-case-workshop', 'screening'],
|
||||
checkpoints: {
|
||||
'CP-UC': {
|
||||
checkpointId: 'CP-UC',
|
||||
passed: true,
|
||||
validatedAt: new Date(),
|
||||
validatedBy: 'SYSTEM',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
},
|
||||
useCases: [
|
||||
{
|
||||
id: 'uc-1',
|
||||
name: 'Test Use Case',
|
||||
description: 'A test use case for testing',
|
||||
category: 'Marketing',
|
||||
stepsCompleted: 3,
|
||||
steps: [
|
||||
{ id: 's1', name: 'Step 1', completed: true, data: {} },
|
||||
{ id: 's2', name: 'Step 2', completed: true, data: {} },
|
||||
{ id: 's3', name: 'Step 3', completed: true, data: {} },
|
||||
],
|
||||
assessmentResult: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
activeUseCase: 'uc-1',
|
||||
screening: null,
|
||||
modules: [],
|
||||
requirements: [],
|
||||
controls: [
|
||||
{
|
||||
id: 'ctrl-1',
|
||||
name: 'Test Control',
|
||||
description: 'A test control',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Access Control',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: [],
|
||||
owner: 'Test Owner',
|
||||
dueDate: null,
|
||||
},
|
||||
],
|
||||
evidence: [],
|
||||
checklist: [],
|
||||
risks: [
|
||||
{
|
||||
id: 'risk-1',
|
||||
title: 'Test Risk',
|
||||
description: 'A test risk',
|
||||
category: 'Security',
|
||||
likelihood: 3,
|
||||
impact: 4,
|
||||
severity: 'HIGH',
|
||||
inherentRiskScore: 12,
|
||||
residualRiskScore: 6,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'mit-1',
|
||||
description: 'Test mitigation',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 80,
|
||||
controlId: 'ctrl-1',
|
||||
},
|
||||
],
|
||||
owner: 'Risk Owner',
|
||||
relatedControls: ['ctrl-1'],
|
||||
relatedRequirements: [],
|
||||
},
|
||||
],
|
||||
aiActClassification: null,
|
||||
obligations: [],
|
||||
dsfa: null,
|
||||
toms: [],
|
||||
retentionPolicies: [],
|
||||
vvt: [],
|
||||
documents: [],
|
||||
cookieBanner: null,
|
||||
consents: [],
|
||||
dsrConfig: null,
|
||||
escalationWorkflows: [],
|
||||
sbom: null,
|
||||
securityIssues: [],
|
||||
securityBacklog: [],
|
||||
commandBarHistory: [],
|
||||
recentSearches: [],
|
||||
preferences: {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true,
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('exportToPDF', () => {
|
||||
it('should return a Blob', async () => {
|
||||
const state = createMockState()
|
||||
const result = await exportToPDF(state)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should create a PDF with the correct type', async () => {
|
||||
const state = createMockState()
|
||||
const result = await exportToPDF(state)
|
||||
|
||||
expect(result.type).toBe('application/pdf')
|
||||
})
|
||||
|
||||
it('should handle empty state', async () => {
|
||||
const emptyState = createMockState({
|
||||
useCases: [],
|
||||
risks: [],
|
||||
controls: [],
|
||||
completedSteps: [],
|
||||
})
|
||||
|
||||
const result = await exportToPDF(emptyState)
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should handle state with multiple risks of different severities', async () => {
|
||||
const state = createMockState({
|
||||
risks: [
|
||||
{
|
||||
id: 'risk-1',
|
||||
title: 'Critical Risk',
|
||||
description: 'Critical',
|
||||
category: 'Security',
|
||||
likelihood: 5,
|
||||
impact: 5,
|
||||
severity: 'CRITICAL',
|
||||
inherentRiskScore: 25,
|
||||
residualRiskScore: 15,
|
||||
status: 'IDENTIFIED',
|
||||
mitigation: [],
|
||||
owner: null,
|
||||
relatedControls: [],
|
||||
relatedRequirements: [],
|
||||
},
|
||||
{
|
||||
id: 'risk-2',
|
||||
title: 'Low Risk',
|
||||
description: 'Low',
|
||||
category: 'Operational',
|
||||
likelihood: 1,
|
||||
impact: 1,
|
||||
severity: 'LOW',
|
||||
inherentRiskScore: 1,
|
||||
residualRiskScore: 1,
|
||||
status: 'ACCEPTED',
|
||||
mitigation: [],
|
||||
owner: null,
|
||||
relatedControls: [],
|
||||
relatedRequirements: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const result = await exportToPDF(state)
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
})
|
||||
|
||||
describe('exportToZIP', () => {
|
||||
it('should return a Blob', async () => {
|
||||
const state = createMockState()
|
||||
const result = await exportToZIP(state)
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should create a ZIP with the correct type', async () => {
|
||||
const state = createMockState()
|
||||
const result = await exportToZIP(state)
|
||||
|
||||
expect(result.type).toBe('application/zip')
|
||||
})
|
||||
|
||||
it('should handle empty state', async () => {
|
||||
const emptyState = createMockState({
|
||||
useCases: [],
|
||||
risks: [],
|
||||
controls: [],
|
||||
completedSteps: [],
|
||||
})
|
||||
|
||||
const result = await exportToZIP(emptyState)
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should respect includeEvidence option', async () => {
|
||||
const state = createMockState()
|
||||
const result = await exportToZIP(state, { includeEvidence: false })
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
|
||||
it('should respect includeDocuments option', async () => {
|
||||
const state = createMockState()
|
||||
const result = await exportToZIP(state, { includeDocuments: false })
|
||||
|
||||
expect(result).toBeInstanceOf(Blob)
|
||||
})
|
||||
})
|
||||
|
||||
describe('downloadExport', () => {
|
||||
let mockCreateElement: ReturnType<typeof vi.spyOn>
|
||||
let mockAppendChild: ReturnType<typeof vi.spyOn>
|
||||
let mockRemoveChild: ReturnType<typeof vi.spyOn>
|
||||
let mockLink: { href: string; download: string; click: ReturnType<typeof vi.fn> }
|
||||
|
||||
beforeEach(() => {
|
||||
mockLink = {
|
||||
href: '',
|
||||
download: '',
|
||||
click: vi.fn(),
|
||||
}
|
||||
|
||||
mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as unknown as HTMLElement)
|
||||
mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockLink as unknown as HTMLElement)
|
||||
mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockLink as unknown as HTMLElement)
|
||||
})
|
||||
|
||||
it('should download JSON format', async () => {
|
||||
const state = createMockState()
|
||||
await downloadExport(state, 'json')
|
||||
|
||||
expect(mockLink.download).toContain('.json')
|
||||
expect(mockLink.click).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should download PDF format', async () => {
|
||||
const state = createMockState()
|
||||
await downloadExport(state, 'pdf')
|
||||
|
||||
expect(mockLink.download).toContain('.pdf')
|
||||
expect(mockLink.click).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should download ZIP format', async () => {
|
||||
const state = createMockState()
|
||||
await downloadExport(state, 'zip')
|
||||
|
||||
expect(mockLink.download).toContain('.zip')
|
||||
expect(mockLink.click).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should include date in filename', async () => {
|
||||
const state = createMockState()
|
||||
await downloadExport(state, 'json')
|
||||
|
||||
// Check that filename contains a date pattern
|
||||
expect(mockLink.download).toMatch(/ai-compliance-sdk-\d{4}-\d{2}-\d{2}\.json/)
|
||||
})
|
||||
|
||||
it('should throw error for unknown format', async () => {
|
||||
const state = createMockState()
|
||||
|
||||
await expect(downloadExport(state, 'unknown' as any)).rejects.toThrow('Unknown export format')
|
||||
})
|
||||
})
|
||||
250
admin-v2/lib/sdk/__tests__/types.test.ts
Normal file
250
admin-v2/lib/sdk/__tests__/types.test.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
SDK_STEPS,
|
||||
getStepById,
|
||||
getStepByUrl,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
getCompletionPercentage,
|
||||
getPhaseCompletionPercentage,
|
||||
type SDKState,
|
||||
} from '../types'
|
||||
|
||||
describe('SDK_STEPS', () => {
|
||||
it('should have steps defined for both phases', () => {
|
||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
||||
|
||||
expect(phase1Steps.length).toBeGreaterThan(0)
|
||||
expect(phase2Steps.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should have unique IDs for all steps', () => {
|
||||
const ids = SDK_STEPS.map(s => s.id)
|
||||
const uniqueIds = new Set(ids)
|
||||
expect(uniqueIds.size).toBe(ids.length)
|
||||
})
|
||||
|
||||
it('should have unique URLs for all steps', () => {
|
||||
const urls = SDK_STEPS.map(s => s.url)
|
||||
const uniqueUrls = new Set(urls)
|
||||
expect(uniqueUrls.size).toBe(urls.length)
|
||||
})
|
||||
|
||||
it('should have checkpoint IDs for all steps', () => {
|
||||
SDK_STEPS.forEach(step => {
|
||||
expect(step.checkpointId).toBeDefined()
|
||||
expect(step.checkpointId.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getStepById', () => {
|
||||
it('should return the correct step for a valid ID', () => {
|
||||
const step = getStepById('use-case-workshop')
|
||||
expect(step).toBeDefined()
|
||||
expect(step?.name).toBe('Use Case Workshop')
|
||||
})
|
||||
|
||||
it('should return undefined for an invalid ID', () => {
|
||||
const step = getStepById('invalid-step-id')
|
||||
expect(step).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should find steps in Phase 2', () => {
|
||||
const step = getStepById('dsfa')
|
||||
expect(step).toBeDefined()
|
||||
expect(step?.phase).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getStepByUrl', () => {
|
||||
it('should return the correct step for a valid URL', () => {
|
||||
const step = getStepByUrl('/sdk/advisory-board')
|
||||
expect(step).toBeDefined()
|
||||
expect(step?.id).toBe('use-case-workshop')
|
||||
})
|
||||
|
||||
it('should return undefined for an invalid URL', () => {
|
||||
const step = getStepByUrl('/invalid/url')
|
||||
expect(step).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should find Phase 2 steps by URL', () => {
|
||||
const step = getStepByUrl('/sdk/dsfa')
|
||||
expect(step).toBeDefined()
|
||||
expect(step?.id).toBe('dsfa')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextStep', () => {
|
||||
it('should return the next step in sequence', () => {
|
||||
const nextStep = getNextStep('use-case-workshop')
|
||||
expect(nextStep).toBeDefined()
|
||||
expect(nextStep?.id).toBe('screening')
|
||||
})
|
||||
|
||||
it('should return undefined for the last step', () => {
|
||||
const lastStep = SDK_STEPS[SDK_STEPS.length - 1]
|
||||
const nextStep = getNextStep(lastStep.id)
|
||||
expect(nextStep).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should handle transition between phases', () => {
|
||||
const lastPhase1Step = SDK_STEPS.filter(s => s.phase === 1).pop()
|
||||
expect(lastPhase1Step).toBeDefined()
|
||||
|
||||
const nextStep = getNextStep(lastPhase1Step!.id)
|
||||
expect(nextStep?.phase).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPreviousStep', () => {
|
||||
it('should return the previous step in sequence', () => {
|
||||
const prevStep = getPreviousStep('screening')
|
||||
expect(prevStep).toBeDefined()
|
||||
expect(prevStep?.id).toBe('use-case-workshop')
|
||||
})
|
||||
|
||||
it('should return undefined for the first step', () => {
|
||||
const prevStep = getPreviousStep('use-case-workshop')
|
||||
expect(prevStep).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCompletionPercentage', () => {
|
||||
const createMockState = (completedSteps: string[]): SDKState => ({
|
||||
version: '1.0.0',
|
||||
lastModified: new Date(),
|
||||
tenantId: 'test',
|
||||
userId: 'test',
|
||||
subscription: 'PROFESSIONAL',
|
||||
currentPhase: 1,
|
||||
currentStep: 'use-case-workshop',
|
||||
completedSteps,
|
||||
checkpoints: {},
|
||||
useCases: [],
|
||||
activeUseCase: null,
|
||||
screening: null,
|
||||
modules: [],
|
||||
requirements: [],
|
||||
controls: [],
|
||||
evidence: [],
|
||||
checklist: [],
|
||||
risks: [],
|
||||
aiActClassification: null,
|
||||
obligations: [],
|
||||
dsfa: null,
|
||||
toms: [],
|
||||
retentionPolicies: [],
|
||||
vvt: [],
|
||||
documents: [],
|
||||
cookieBanner: null,
|
||||
consents: [],
|
||||
dsrConfig: null,
|
||||
escalationWorkflows: [],
|
||||
sbom: null,
|
||||
securityIssues: [],
|
||||
securityBacklog: [],
|
||||
commandBarHistory: [],
|
||||
recentSearches: [],
|
||||
preferences: {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true,
|
||||
},
|
||||
})
|
||||
|
||||
it('should return 0 for no completed steps', () => {
|
||||
const state = createMockState([])
|
||||
const percentage = getCompletionPercentage(state)
|
||||
expect(percentage).toBe(0)
|
||||
})
|
||||
|
||||
it('should return 100 for all completed steps', () => {
|
||||
const allStepIds = SDK_STEPS.map(s => s.id)
|
||||
const state = createMockState(allStepIds)
|
||||
const percentage = getCompletionPercentage(state)
|
||||
expect(percentage).toBe(100)
|
||||
})
|
||||
|
||||
it('should calculate correct percentage for partial completion', () => {
|
||||
const halfSteps = SDK_STEPS.slice(0, Math.floor(SDK_STEPS.length / 2)).map(s => s.id)
|
||||
const state = createMockState(halfSteps)
|
||||
const percentage = getCompletionPercentage(state)
|
||||
expect(percentage).toBeGreaterThan(40)
|
||||
expect(percentage).toBeLessThan(60)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPhaseCompletionPercentage', () => {
|
||||
const createMockState = (completedSteps: string[]): SDKState => ({
|
||||
version: '1.0.0',
|
||||
lastModified: new Date(),
|
||||
tenantId: 'test',
|
||||
userId: 'test',
|
||||
subscription: 'PROFESSIONAL',
|
||||
currentPhase: 1,
|
||||
currentStep: 'use-case-workshop',
|
||||
completedSteps,
|
||||
checkpoints: {},
|
||||
useCases: [],
|
||||
activeUseCase: null,
|
||||
screening: null,
|
||||
modules: [],
|
||||
requirements: [],
|
||||
controls: [],
|
||||
evidence: [],
|
||||
checklist: [],
|
||||
risks: [],
|
||||
aiActClassification: null,
|
||||
obligations: [],
|
||||
dsfa: null,
|
||||
toms: [],
|
||||
retentionPolicies: [],
|
||||
vvt: [],
|
||||
documents: [],
|
||||
cookieBanner: null,
|
||||
consents: [],
|
||||
dsrConfig: null,
|
||||
escalationWorkflows: [],
|
||||
sbom: null,
|
||||
securityIssues: [],
|
||||
securityBacklog: [],
|
||||
commandBarHistory: [],
|
||||
recentSearches: [],
|
||||
preferences: {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true,
|
||||
},
|
||||
})
|
||||
|
||||
it('should return 0 for Phase 1 with no completed steps', () => {
|
||||
const state = createMockState([])
|
||||
const percentage = getPhaseCompletionPercentage(state, 1)
|
||||
expect(percentage).toBe(0)
|
||||
})
|
||||
|
||||
it('should return 100 for Phase 1 when all Phase 1 steps are complete', () => {
|
||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1).map(s => s.id)
|
||||
const state = createMockState(phase1Steps)
|
||||
const percentage = getPhaseCompletionPercentage(state, 1)
|
||||
expect(percentage).toBe(100)
|
||||
})
|
||||
|
||||
it('should not count Phase 2 steps in Phase 1 percentage', () => {
|
||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2).map(s => s.id)
|
||||
const state = createMockState(phase2Steps)
|
||||
const percentage = getPhaseCompletionPercentage(state, 1)
|
||||
expect(percentage).toBe(0)
|
||||
})
|
||||
})
|
||||
471
admin-v2/lib/sdk/api-client.ts
Normal file
471
admin-v2/lib/sdk/api-client.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* SDK API Client
|
||||
*
|
||||
* Centralized API client for SDK state management with error handling,
|
||||
* retry logic, and optimistic locking support.
|
||||
*/
|
||||
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface APIResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
version?: number
|
||||
lastModified?: string
|
||||
}
|
||||
|
||||
export interface StateResponse {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version: number
|
||||
lastModified: string
|
||||
}
|
||||
|
||||
export interface SaveStateRequest {
|
||||
tenantId: string
|
||||
state: SDKState
|
||||
version?: number // For optimistic locking
|
||||
}
|
||||
|
||||
export interface CheckpointValidationResult {
|
||||
checkpointId: string
|
||||
passed: boolean
|
||||
errors: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
warnings: Array<{
|
||||
ruleId: string
|
||||
field: string
|
||||
message: string
|
||||
severity: 'ERROR' | 'WARNING' | 'INFO'
|
||||
}>
|
||||
validatedAt: string
|
||||
validatedBy: string
|
||||
}
|
||||
|
||||
export interface APIError extends Error {
|
||||
status?: number
|
||||
code?: string
|
||||
retryable: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const DEFAULT_BASE_URL = '/api/sdk/v1'
|
||||
const DEFAULT_TIMEOUT = 30000 // 30 seconds
|
||||
const MAX_RETRIES = 3
|
||||
const RETRY_DELAYS = [1000, 2000, 4000] // Exponential backoff
|
||||
|
||||
// =============================================================================
|
||||
// API CLIENT
|
||||
// =============================================================================
|
||||
|
||||
export class SDKApiClient {
|
||||
private baseUrl: string
|
||||
private tenantId: string
|
||||
private timeout: number
|
||||
private abortControllers: Map<string, AbortController> = new Map()
|
||||
|
||||
constructor(options: {
|
||||
baseUrl?: string
|
||||
tenantId: string
|
||||
timeout?: number
|
||||
}) {
|
||||
this.baseUrl = options.baseUrl || DEFAULT_BASE_URL
|
||||
this.tenantId = options.tenantId
|
||||
this.timeout = options.timeout || DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private createError(message: string, status?: number, retryable = false): APIError {
|
||||
const error = new Error(message) as APIError
|
||||
error.status = status
|
||||
error.retryable = retryable
|
||||
return error
|
||||
}
|
||||
|
||||
private async fetchWithTimeout(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
requestId: string
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController()
|
||||
this.abortControllers.set(requestId, controller)
|
||||
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
})
|
||||
return response
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
this.abortControllers.delete(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWithRetry<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
retries = MAX_RETRIES
|
||||
): Promise<T> {
|
||||
const requestId = `${Date.now()}-${Math.random()}`
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(url, options, requestId)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}`
|
||||
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep the HTTP status message
|
||||
}
|
||||
|
||||
// Don't retry client errors (4xx) except for 429 (rate limit)
|
||||
const retryable = response.status >= 500 || response.status === 429
|
||||
|
||||
if (!retryable || attempt === retries) {
|
||||
throw this.createError(errorMessage, response.status, retryable)
|
||||
}
|
||||
} else {
|
||||
const data = await response.json()
|
||||
return data as T
|
||||
}
|
||||
} catch (error) {
|
||||
lastError = error as Error
|
||||
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw this.createError('Request timeout', 408, true)
|
||||
}
|
||||
|
||||
// Check if it's a retryable error
|
||||
const apiError = error as APIError
|
||||
if (!apiError.retryable || attempt === retries) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Wait before retrying (exponential backoff)
|
||||
if (attempt < retries) {
|
||||
await this.sleep(RETRY_DELAYS[attempt] || RETRY_DELAYS[RETRY_DELAYS.length - 1])
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || this.createError('Unknown error', 500, false)
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - State Management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Load SDK state for the current tenant
|
||||
*/
|
||||
async getState(): Promise<StateResponse | null> {
|
||||
try {
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (error) {
|
||||
const apiError = error as APIError
|
||||
// 404 means no state exists yet - that's okay
|
||||
if (apiError.status === 404) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save SDK state for the current tenant
|
||||
* Supports optimistic locking via version parameter
|
||||
*/
|
||||
async saveState(state: SDKState, version?: number): Promise<StateResponse> {
|
||||
const response = await this.fetchWithRetry<APIResponse<StateResponse>>(
|
||||
`${this.baseUrl}/state`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(version !== undefined && { 'If-Match': String(version) }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
state,
|
||||
version,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success) {
|
||||
throw this.createError(response.error || 'Failed to save state', 500, true)
|
||||
}
|
||||
|
||||
return response.data!
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete SDK state for the current tenant
|
||||
*/
|
||||
async deleteState(): Promise<void> {
|
||||
await this.fetchWithRetry<APIResponse<void>>(
|
||||
`${this.baseUrl}/state?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Checkpoint Validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate a specific checkpoint
|
||||
*/
|
||||
async validateCheckpoint(
|
||||
checkpointId: string,
|
||||
data?: unknown
|
||||
): Promise<CheckpointValidationResult> {
|
||||
const response = await this.fetchWithRetry<APIResponse<CheckpointValidationResult>>(
|
||||
`${this.baseUrl}/checkpoints/validate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
checkpointId,
|
||||
data,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw this.createError(response.error || 'Checkpoint validation failed', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all checkpoint statuses
|
||||
*/
|
||||
async getCheckpoints(): Promise<Record<string, CheckpointStatus>> {
|
||||
const response = await this.fetchWithRetry<APIResponse<Record<string, CheckpointStatus>>>(
|
||||
`${this.baseUrl}/checkpoints?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response.data || {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Flow Navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get current flow state
|
||||
*/
|
||||
async getFlowState(): Promise<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}> {
|
||||
const response = await this.fetchWithRetry<APIResponse<{
|
||||
currentStep: string
|
||||
currentPhase: 1 | 2
|
||||
completedSteps: string[]
|
||||
suggestions: Array<{ stepId: string; reason: string }>
|
||||
}>>(
|
||||
`${this.baseUrl}/flow?tenantId=${encodeURIComponent(this.tenantId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw this.createError('Failed to get flow state', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to next/previous step
|
||||
*/
|
||||
async navigateFlow(direction: 'next' | 'previous'): Promise<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}> {
|
||||
const response = await this.fetchWithRetry<APIResponse<{
|
||||
stepId: string
|
||||
phase: 1 | 2
|
||||
}>>(
|
||||
`${this.baseUrl}/flow`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId: this.tenantId,
|
||||
direction,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw this.createError('Failed to navigate flow', 500, true)
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Export SDK state in various formats
|
||||
*/
|
||||
async exportState(format: 'json' | 'pdf' | 'zip'): Promise<Blob> {
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/export?tenantId=${encodeURIComponent(this.tenantId)}&format=${format}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': format === 'json' ? 'application/json' : format === 'pdf' ? 'application/pdf' : 'application/zip',
|
||||
},
|
||||
},
|
||||
`export-${Date.now()}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw this.createError(`Export failed: ${response.statusText}`, response.status, true)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public Methods - Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cancel all pending requests
|
||||
*/
|
||||
cancelAllRequests(): void {
|
||||
this.abortControllers.forEach(controller => controller.abort())
|
||||
this.abortControllers.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tenant ID (useful when switching contexts)
|
||||
*/
|
||||
setTenantId(tenantId: string): void {
|
||||
this.tenantId = tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current tenant ID
|
||||
*/
|
||||
getTenantId(): string {
|
||||
return this.tenantId
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.fetchWithTimeout(
|
||||
`${this.baseUrl}/health`,
|
||||
{ method: 'GET' },
|
||||
`health-${Date.now()}`
|
||||
)
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SINGLETON FACTORY
|
||||
// =============================================================================
|
||||
|
||||
let clientInstance: SDKApiClient | null = null
|
||||
|
||||
export function getSDKApiClient(tenantId?: string): SDKApiClient {
|
||||
if (!clientInstance && !tenantId) {
|
||||
throw new Error('SDKApiClient not initialized. Provide tenantId on first call.')
|
||||
}
|
||||
|
||||
if (!clientInstance && tenantId) {
|
||||
clientInstance = new SDKApiClient({ tenantId })
|
||||
}
|
||||
|
||||
if (tenantId && clientInstance && clientInstance.getTenantId() !== tenantId) {
|
||||
clientInstance.setTenantId(tenantId)
|
||||
}
|
||||
|
||||
return clientInstance!
|
||||
}
|
||||
|
||||
export function resetSDKApiClient(): void {
|
||||
if (clientInstance) {
|
||||
clientInstance.cancelAllRequests()
|
||||
}
|
||||
clientInstance = null
|
||||
}
|
||||
1084
admin-v2/lib/sdk/context.tsx
Normal file
1084
admin-v2/lib/sdk/context.tsx
Normal file
File diff suppressed because it is too large
Load Diff
210
admin-v2/lib/sdk/demo-data/controls.ts
Normal file
210
admin-v2/lib/sdk/demo-data/controls.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Demo Controls for AI Compliance SDK
|
||||
*/
|
||||
|
||||
import { Control } from '../types'
|
||||
|
||||
export const DEMO_CONTROLS: Control[] = [
|
||||
// Zugangskontrolle
|
||||
{
|
||||
id: 'demo-ctrl-1',
|
||||
name: 'Multi-Faktor-Authentifizierung',
|
||||
description: 'Alle Systemzugriffe erfordern mindestens zwei unabhängige Authentifizierungsfaktoren (Wissen + Besitz).',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Zugangskontrolle',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-1'],
|
||||
owner: 'IT-Sicherheit',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-2',
|
||||
name: 'Rollenbasiertes Berechtigungskonzept',
|
||||
description: 'Zugriffsrechte werden nach dem Least-Privilege-Prinzip anhand definierter Rollen vergeben und regelmäßig überprüft.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
category: 'Zugangskontrolle',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-2'],
|
||||
owner: 'IT-Sicherheit',
|
||||
dueDate: null,
|
||||
},
|
||||
|
||||
// Verfügbarkeit
|
||||
{
|
||||
id: 'demo-ctrl-3',
|
||||
name: 'Automatisiertes Backup-System',
|
||||
description: 'Tägliche inkrementelle Backups und wöchentliche Vollbackups aller kritischen Daten mit Verschlüsselung.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Verfügbarkeit',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-3'],
|
||||
owner: 'IT-Betrieb',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-4',
|
||||
name: 'Georedundante Datenspeicherung',
|
||||
description: 'Kritische Daten werden synchron in zwei geographisch getrennten Rechenzentren gespeichert.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Verfügbarkeit',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-4'],
|
||||
owner: 'IT-Betrieb',
|
||||
dueDate: null,
|
||||
},
|
||||
|
||||
// KI-Fairness
|
||||
{
|
||||
id: 'demo-ctrl-5',
|
||||
name: 'Bias-Monitoring',
|
||||
description: 'Kontinuierliche Überwachung der KI-Modelle auf systematische Verzerrungen anhand definierter Fairness-Metriken.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'KI-Governance',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'MEDIUM',
|
||||
evidence: ['demo-evi-5'],
|
||||
owner: 'Data Science Lead',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-6',
|
||||
name: 'Human-in-the-Loop',
|
||||
description: 'Kritische automatisierte Entscheidungen werden vor Umsetzung durch qualifizierte Mitarbeiter überprüft.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
category: 'KI-Governance',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-6'],
|
||||
owner: 'Fachbereich HR',
|
||||
dueDate: null,
|
||||
},
|
||||
|
||||
// Transparenz
|
||||
{
|
||||
id: 'demo-ctrl-7',
|
||||
name: 'Explainable AI Komponenten',
|
||||
description: 'Einsatz von SHAP/LIME zur Erklärung von KI-Entscheidungen für nachvollziehbare Begründungen.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Transparenz',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'MEDIUM',
|
||||
evidence: ['demo-evi-7'],
|
||||
owner: 'Data Science Lead',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-8',
|
||||
name: 'Verständliche Datenschutzinformationen',
|
||||
description: 'Betroffene erhalten klare, verständliche Informationen über die Verarbeitung ihrer Daten gemäß Art. 13-14 DSGVO.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
category: 'Transparenz',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-8'],
|
||||
owner: 'DSB',
|
||||
dueDate: null,
|
||||
},
|
||||
|
||||
// Datensparsamkeit
|
||||
{
|
||||
id: 'demo-ctrl-9',
|
||||
name: 'Zweckbindungskontrollen',
|
||||
description: 'Technische Maßnahmen stellen sicher, dass Daten nur für definierte Zwecke verarbeitet werden.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Datensparsamkeit',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'MEDIUM',
|
||||
evidence: ['demo-evi-9'],
|
||||
owner: 'IT-Sicherheit',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-10',
|
||||
name: 'Anonymisierungs-Pipeline',
|
||||
description: 'Automatisierte Anonymisierung von Daten für Analysen, wo keine Personenbezug erforderlich ist.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Datensparsamkeit',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-10'],
|
||||
owner: 'Data Engineering',
|
||||
dueDate: null,
|
||||
},
|
||||
|
||||
// KI-Sicherheit
|
||||
{
|
||||
id: 'demo-ctrl-11',
|
||||
name: 'Input-Validierung',
|
||||
description: 'Strenge Validierung aller Eingabedaten zur Verhinderung von Adversarial Attacks auf KI-Modelle.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'KI-Sicherheit',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'MEDIUM',
|
||||
evidence: ['demo-evi-11'],
|
||||
owner: 'Data Science Lead',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-12',
|
||||
name: 'Model Performance Monitoring',
|
||||
description: 'Kontinuierliche Überwachung der Modell-Performance mit automatischen Alerts bei Abweichungen.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'KI-Sicherheit',
|
||||
implementationStatus: 'PARTIAL',
|
||||
effectiveness: 'MEDIUM',
|
||||
evidence: [],
|
||||
owner: 'Data Science Lead',
|
||||
dueDate: new Date('2026-03-31'),
|
||||
},
|
||||
|
||||
// Datenlebenszyklus
|
||||
{
|
||||
id: 'demo-ctrl-13',
|
||||
name: 'Automatisierte Löschroutinen',
|
||||
description: 'Technische Umsetzung der Aufbewahrungsfristen mit automatischer Löschung nach Fristablauf.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Datenlebenszyklus',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-13'],
|
||||
owner: 'IT-Betrieb',
|
||||
dueDate: null,
|
||||
},
|
||||
{
|
||||
id: 'demo-ctrl-14',
|
||||
name: 'Löschprotokoll-Review',
|
||||
description: 'Quartalsweise Überprüfung der Löschprotokolle durch den DSB.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
category: 'Datenlebenszyklus',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'MEDIUM',
|
||||
evidence: ['demo-evi-14'],
|
||||
owner: 'DSB',
|
||||
dueDate: null,
|
||||
},
|
||||
|
||||
// Audit
|
||||
{
|
||||
id: 'demo-ctrl-15',
|
||||
name: 'Umfassendes Audit-Logging',
|
||||
description: 'Alle sicherheitsrelevanten Ereignisse werden manipulationssicher protokolliert und 10 Jahre aufbewahrt.',
|
||||
type: 'TECHNICAL',
|
||||
category: 'Audit',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
effectiveness: 'HIGH',
|
||||
evidence: ['demo-evi-15'],
|
||||
owner: 'IT-Sicherheit',
|
||||
dueDate: null,
|
||||
},
|
||||
]
|
||||
|
||||
export function getDemoControls(): Control[] {
|
||||
return DEMO_CONTROLS.map(ctrl => ({
|
||||
...ctrl,
|
||||
dueDate: ctrl.dueDate ? new Date(ctrl.dueDate) : null,
|
||||
}))
|
||||
}
|
||||
224
admin-v2/lib/sdk/demo-data/dsfa.ts
Normal file
224
admin-v2/lib/sdk/demo-data/dsfa.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Demo DSFA for AI Compliance SDK
|
||||
*/
|
||||
|
||||
import { DSFA, DSFASection, DSFAApproval } from '../types'
|
||||
|
||||
export const DEMO_DSFA: DSFA = {
|
||||
id: 'demo-dsfa-1',
|
||||
status: 'IN_REVIEW',
|
||||
version: 2,
|
||||
sections: [
|
||||
{
|
||||
id: 'dsfa-sec-1',
|
||||
title: 'Systematische Beschreibung der Verarbeitungsvorgänge',
|
||||
content: `## 1. Verarbeitungsbeschreibung
|
||||
|
||||
### 1.1 Gegenstand der Verarbeitung
|
||||
Die geplante KI-gestützte Kundenanalyse verarbeitet personenbezogene Daten von Kunden und Interessenten zur Optimierung von Marketingmaßnahmen und Personalisierung von Angeboten.
|
||||
|
||||
### 1.2 Verarbeitungszwecke
|
||||
- Kundensegmentierung basierend auf Kaufverhalten
|
||||
- Churn-Prediction zur Kundenbindung
|
||||
- Personalisierte Produktempfehlungen
|
||||
- Optimierung von Marketing-Kampagnen
|
||||
|
||||
### 1.3 Kategorien personenbezogener Daten
|
||||
- **Stammdaten**: Name, Adresse, E-Mail, Telefon
|
||||
- **Transaktionsdaten**: Käufe, Bestellungen, Retouren
|
||||
- **Nutzungsdaten**: Clickstreams, Seitenaufrufe, Verweildauer
|
||||
- **Demographische Daten**: Alter, Geschlecht, PLZ-Region
|
||||
|
||||
### 1.4 Kategorien betroffener Personen
|
||||
- Bestandskunden (ca. 250.000 aktive Kunden)
|
||||
- Registrierte Interessenten (ca. 100.000)
|
||||
- Newsletter-Abonnenten (ca. 180.000)
|
||||
|
||||
### 1.5 Rechtsgrundlage
|
||||
**Primär**: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
|
||||
**Sekundär**: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweiterte Profiling-Maßnahmen)
|
||||
|
||||
Das berechtigte Interesse liegt in der Verbesserung des Kundenerlebnisses und der Effizienzsteigerung des Marketings.`,
|
||||
status: 'COMPLETED',
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
id: 'dsfa-sec-2',
|
||||
title: 'Bewertung der Notwendigkeit und Verhältnismäßigkeit',
|
||||
content: `## 2. Notwendigkeit und Verhältnismäßigkeit
|
||||
|
||||
### 2.1 Notwendigkeit der Verarbeitung
|
||||
Die Verarbeitung ist notwendig, um:
|
||||
- Kunden individuell relevante Angebote zu unterbreiten
|
||||
- Abwanderungsgefährdete Kunden frühzeitig zu identifizieren
|
||||
- Marketing-Budget effizienter einzusetzen
|
||||
- Wettbewerbsfähigkeit zu erhalten
|
||||
|
||||
### 2.2 Verhältnismäßigkeitsprüfung
|
||||
|
||||
**Alternative Methoden geprüft:**
|
||||
1. **Manuelle Analyse**: Nicht praktikabel bei 250.000+ Kunden
|
||||
2. **Regelbasierte Systeme**: Zu ungenau, führt zu höherem Datenverbrauch
|
||||
3. **Aggregierte Analysen**: Keine ausreichende Personalisierung möglich
|
||||
|
||||
**Ergebnis**: Die KI-gestützte Analyse stellt die mildeste effektive Maßnahme dar.
|
||||
|
||||
### 2.3 Datensparsamkeit
|
||||
- Nur für den Zweck notwendige Daten werden verarbeitet
|
||||
- Sensitive Kategorien (Art. 9 DSGVO) werden ausgeschlossen
|
||||
- Automatische Löschung nach definierten Fristen
|
||||
|
||||
### 2.4 Interessenabwägung
|
||||
| Interesse des Verantwortlichen | Interesse der Betroffenen |
|
||||
|-------------------------------|---------------------------|
|
||||
| Effizientes Marketing | Privatsphäre |
|
||||
| Kundenbindung | Keine unerwünschte Profilbildung |
|
||||
| Umsatzsteigerung | Transparenz über Verarbeitung |
|
||||
|
||||
**Ausgleichende Maßnahmen:**
|
||||
- Umfassende Informationen nach Art. 13/14 DSGVO
|
||||
- Einfacher Opt-out für Profiling
|
||||
- Human-Review bei kritischen Entscheidungen`,
|
||||
status: 'COMPLETED',
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
id: 'dsfa-sec-3',
|
||||
title: 'Risikobewertung',
|
||||
content: `## 3. Risiken für Rechte und Freiheiten
|
||||
|
||||
### 3.1 Identifizierte Risiken
|
||||
|
||||
| # | Risiko | Eintritt | Schwere | Gesamt |
|
||||
|---|--------|----------|---------|--------|
|
||||
| R1 | Unbefugter Zugriff auf Profildaten | Mittel | Hoch | HOCH |
|
||||
| R2 | Diskriminierende Entscheidungen durch Bias | Mittel | Hoch | HOCH |
|
||||
| R3 | Unzulässige Profilbildung | Mittel | Mittel | MITTEL |
|
||||
| R4 | Fehlende Nachvollziehbarkeit | Hoch | Mittel | MITTEL |
|
||||
| R5 | Übermäßige Datensammlung | Niedrig | Mittel | NIEDRIG |
|
||||
|
||||
### 3.2 Detailanalyse kritischer Risiken
|
||||
|
||||
**R1 - Unbefugter Zugriff**
|
||||
- Quelle: Externe Angreifer, Insider-Bedrohung
|
||||
- Auswirkung: Identitätsdiebstahl, Reputationsschaden
|
||||
- Betroffene: Alle Kunden
|
||||
|
||||
**R2 - Diskriminierende Entscheidungen**
|
||||
- Quelle: Historische Verzerrungen in Trainingsdaten
|
||||
- Auswirkung: Benachteiligung bestimmter Gruppen
|
||||
- Betroffene: Potentiell alle, besonders geschützte Gruppen`,
|
||||
status: 'COMPLETED',
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
id: 'dsfa-sec-4',
|
||||
title: 'Maßnahmen zur Risikominderung',
|
||||
content: `## 4. Abhilfemaßnahmen
|
||||
|
||||
### 4.1 Technische Maßnahmen
|
||||
|
||||
| Maßnahme | Risiko | Status | Wirksamkeit |
|
||||
|----------|--------|--------|-------------|
|
||||
| Multi-Faktor-Authentifizierung | R1 | ✅ Umgesetzt | Hoch |
|
||||
| Verschlüsselung (AES-256) | R1 | ✅ Umgesetzt | Hoch |
|
||||
| Bias-Monitoring | R2 | ✅ Umgesetzt | Mittel |
|
||||
| Explainable AI | R4 | ✅ Umgesetzt | Mittel |
|
||||
| Zweckbindungskontrollen | R3 | ✅ Umgesetzt | Hoch |
|
||||
| Audit-Logging | R1, R4 | ✅ Umgesetzt | Hoch |
|
||||
|
||||
### 4.2 Organisatorische Maßnahmen
|
||||
|
||||
| Maßnahme | Risiko | Status | Wirksamkeit |
|
||||
|----------|--------|--------|-------------|
|
||||
| Rollenbasierte Zugriffskontrolle | R1 | ✅ Umgesetzt | Hoch |
|
||||
| Human-in-the-Loop | R2 | ✅ Umgesetzt | Hoch |
|
||||
| Datenschutz-Schulungen | R1, R3 | ✅ Umgesetzt | Mittel |
|
||||
| Regelmäßige Audits | Alle | ⏳ Geplant | Hoch |
|
||||
|
||||
### 4.3 Restrisikobewertung
|
||||
|
||||
Nach Implementierung aller Maßnahmen:
|
||||
- **R1**: HOCH → MITTEL (akzeptabel)
|
||||
- **R2**: HOCH → MITTEL (akzeptabel)
|
||||
- **R3**: MITTEL → NIEDRIG (akzeptabel)
|
||||
- **R4**: MITTEL → NIEDRIG (akzeptabel)
|
||||
- **R5**: NIEDRIG → NIEDRIG (akzeptabel)`,
|
||||
status: 'COMPLETED',
|
||||
order: 4,
|
||||
},
|
||||
{
|
||||
id: 'dsfa-sec-5',
|
||||
title: 'Stellungnahme des Datenschutzbeauftragten',
|
||||
content: `## 5. Stellungnahme DSB
|
||||
|
||||
### 5.1 Bewertung
|
||||
|
||||
Der Datenschutzbeauftragte hat die DSFA geprüft und kommt zu folgender Einschätzung:
|
||||
|
||||
**Positiv:**
|
||||
- Umfassende Risikoanalyse durchgeführt
|
||||
- Technische Schutzmaßnahmen dem Stand der Technik entsprechend
|
||||
- Transparenzpflichten angemessen berücksichtigt
|
||||
- Interessenabwägung nachvollziehbar dokumentiert
|
||||
|
||||
**Verbesserungspotenzial:**
|
||||
- Regelmäßige Überprüfung der Bias-Metriken sollte quartalsweise erfolgen
|
||||
- Informationen für Betroffene könnten noch verständlicher formuliert werden
|
||||
- Löschkonzept sollte um automatische Überprüfungsmechanismen ergänzt werden
|
||||
|
||||
### 5.2 Empfehlung
|
||||
|
||||
Der DSB empfiehlt die **Genehmigung** der Verarbeitungstätigkeit unter der Voraussetzung, dass:
|
||||
1. Die identifizierten Verbesserungsmaßnahmen innerhalb von 3 Monaten umgesetzt werden
|
||||
2. Eine jährliche Überprüfung der DSFA erfolgt
|
||||
3. Bei wesentlichen Änderungen eine Aktualisierung vorgenommen wird
|
||||
|
||||
---
|
||||
*Datum: 2026-01-28*
|
||||
*Unterschrift: [DSB]*`,
|
||||
status: 'COMPLETED',
|
||||
order: 5,
|
||||
},
|
||||
],
|
||||
approvals: [
|
||||
{
|
||||
id: 'dsfa-appr-1',
|
||||
approver: 'Dr. Thomas Schmidt',
|
||||
role: 'Datenschutzbeauftragter',
|
||||
status: 'APPROVED',
|
||||
comment: 'Unter den genannten Voraussetzungen genehmigt.',
|
||||
approvedAt: new Date('2026-01-28'),
|
||||
},
|
||||
{
|
||||
id: 'dsfa-appr-2',
|
||||
approver: 'Maria Weber',
|
||||
role: 'CISO',
|
||||
status: 'APPROVED',
|
||||
comment: 'Technische Maßnahmen sind angemessen.',
|
||||
approvedAt: new Date('2026-01-29'),
|
||||
},
|
||||
{
|
||||
id: 'dsfa-appr-3',
|
||||
approver: 'Michael Bauer',
|
||||
role: 'Geschäftsführung',
|
||||
status: 'PENDING',
|
||||
comment: null,
|
||||
approvedAt: null,
|
||||
},
|
||||
],
|
||||
createdAt: new Date('2026-01-15'),
|
||||
updatedAt: new Date('2026-02-01'),
|
||||
}
|
||||
|
||||
export function getDemoDSFA(): DSFA {
|
||||
return {
|
||||
...DEMO_DSFA,
|
||||
approvals: DEMO_DSFA.approvals.map(a => ({
|
||||
...a,
|
||||
approvedAt: a.approvedAt ? new Date(a.approvedAt) : null,
|
||||
})),
|
||||
createdAt: new Date(DEMO_DSFA.createdAt),
|
||||
updatedAt: new Date(DEMO_DSFA.updatedAt),
|
||||
}
|
||||
}
|
||||
556
admin-v2/lib/sdk/demo-data/index.ts
Normal file
556
admin-v2/lib/sdk/demo-data/index.ts
Normal file
@@ -0,0 +1,556 @@
|
||||
/**
|
||||
* Demo Data Seeding for AI Compliance SDK
|
||||
*
|
||||
* IMPORTANT: Demo data is NOT hardcoded in the frontend.
|
||||
* This module provides seed data that gets stored via the API,
|
||||
* exactly like real customer data would be stored.
|
||||
*
|
||||
* The seedDemoData() function writes data through the API,
|
||||
* and the data is then loaded from the database like any other data.
|
||||
*/
|
||||
|
||||
import { SDKState } from '../types'
|
||||
import { getSDKApiClient } from '../api-client'
|
||||
|
||||
// Seed data imports (these are templates, not runtime data)
|
||||
import { getDemoUseCases, DEMO_USE_CASES } from './use-cases'
|
||||
import { getDemoRisks, DEMO_RISKS } from './risks'
|
||||
import { getDemoControls, DEMO_CONTROLS } from './controls'
|
||||
import { getDemoDSFA, DEMO_DSFA } from './dsfa'
|
||||
import { getDemoTOMs, DEMO_TOMS } from './toms'
|
||||
import { getDemoProcessingActivities, getDemoRetentionPolicies, DEMO_PROCESSING_ACTIVITIES, DEMO_RETENTION_POLICIES } from './vvt'
|
||||
|
||||
// Re-export for direct access to seed templates (for testing/development)
|
||||
export {
|
||||
getDemoUseCases,
|
||||
getDemoRisks,
|
||||
getDemoControls,
|
||||
getDemoDSFA,
|
||||
getDemoTOMs,
|
||||
getDemoProcessingActivities,
|
||||
getDemoRetentionPolicies,
|
||||
// Raw data exports
|
||||
DEMO_USE_CASES,
|
||||
DEMO_RISKS,
|
||||
DEMO_CONTROLS,
|
||||
DEMO_DSFA,
|
||||
DEMO_TOMS,
|
||||
DEMO_PROCESSING_ACTIVITIES,
|
||||
DEMO_RETENTION_POLICIES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete demo state object
|
||||
* This is used as seed data for the API, not as runtime data
|
||||
*/
|
||||
export function generateDemoState(tenantId: string, userId: string): Partial<SDKState> {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
// Metadata
|
||||
version: '1.0.0',
|
||||
lastModified: now,
|
||||
|
||||
// Tenant & User
|
||||
tenantId,
|
||||
userId,
|
||||
subscription: 'PROFESSIONAL',
|
||||
|
||||
// Customer Type
|
||||
customerType: 'new',
|
||||
|
||||
// Company Profile (Demo: TechStart GmbH - SaaS-Startup aus Berlin)
|
||||
companyProfile: {
|
||||
companyName: 'TechStart GmbH',
|
||||
legalForm: 'gmbh',
|
||||
industry: 'Technologie / IT',
|
||||
foundedYear: 2022,
|
||||
businessModel: 'B2B_B2C',
|
||||
offerings: ['app_web', 'software_saas', 'services_consulting'],
|
||||
companySize: 'small',
|
||||
employeeCount: '10-49',
|
||||
annualRevenue: '2-10 Mio',
|
||||
headquartersCountry: 'DE',
|
||||
headquartersCity: 'Berlin',
|
||||
hasInternationalLocations: false,
|
||||
internationalCountries: [],
|
||||
targetMarkets: ['germany_only', 'dach'],
|
||||
primaryJurisdiction: 'DE',
|
||||
isDataController: true,
|
||||
isDataProcessor: true,
|
||||
usesAI: true,
|
||||
aiUseCases: ['KI-gestützte Kundenberatung', 'Automatisierte Dokumentenanalyse'],
|
||||
dpoName: 'Max Mustermann',
|
||||
dpoEmail: 'dsb@techstart.de',
|
||||
legalContactName: null,
|
||||
legalContactEmail: null,
|
||||
isComplete: true,
|
||||
completedAt: new Date('2026-01-14'),
|
||||
},
|
||||
|
||||
// Progress - showing a realistic partially completed workflow
|
||||
currentPhase: 2,
|
||||
currentStep: 'tom',
|
||||
completedSteps: [
|
||||
'company-profile',
|
||||
'use-case-assessment',
|
||||
'screening',
|
||||
'modules',
|
||||
'requirements',
|
||||
'controls',
|
||||
'evidence',
|
||||
'audit-checklist',
|
||||
'risks',
|
||||
'ai-act',
|
||||
'obligations',
|
||||
'dsfa',
|
||||
],
|
||||
checkpoints: {
|
||||
'CP-PROF': { checkpointId: 'CP-PROF', passed: true, validatedAt: new Date('2026-01-14'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-UC': { checkpointId: 'CP-UC', passed: true, validatedAt: new Date('2026-01-15'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-SCAN': { checkpointId: 'CP-SCAN', passed: true, validatedAt: new Date('2026-01-16'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-MOD': { checkpointId: 'CP-MOD', passed: true, validatedAt: new Date('2026-01-17'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-REQ': { checkpointId: 'CP-REQ', passed: true, validatedAt: new Date('2026-01-18'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-CTRL': { checkpointId: 'CP-CTRL', passed: true, validatedAt: new Date('2026-01-19'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-EVI': { checkpointId: 'CP-EVI', passed: true, validatedAt: new Date('2026-01-20'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-CHK': { checkpointId: 'CP-CHK', passed: true, validatedAt: new Date('2026-01-21'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-RISK': { checkpointId: 'CP-RISK', passed: true, validatedAt: new Date('2026-01-22'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-AI': { checkpointId: 'CP-AI', passed: true, validatedAt: new Date('2026-01-25'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-OBL': { checkpointId: 'CP-OBL', passed: true, validatedAt: new Date('2026-01-27'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
||||
'CP-DSFA': { checkpointId: 'CP-DSFA', passed: true, validatedAt: new Date('2026-01-30'), validatedBy: 'DSB', errors: [], warnings: [] },
|
||||
},
|
||||
|
||||
// Phase 1 Data
|
||||
useCases: getDemoUseCases(),
|
||||
activeUseCase: 'demo-uc-1',
|
||||
screening: {
|
||||
id: 'demo-scan-1',
|
||||
status: 'COMPLETED',
|
||||
startedAt: new Date('2026-01-16T09:00:00'),
|
||||
completedAt: new Date('2026-01-16T09:15:00'),
|
||||
sbom: {
|
||||
format: 'CycloneDX',
|
||||
version: '1.4',
|
||||
components: [
|
||||
{
|
||||
name: 'tensorflow',
|
||||
version: '2.15.0',
|
||||
type: 'library',
|
||||
purl: 'pkg:pypi/tensorflow@2.15.0',
|
||||
licenses: ['Apache-2.0'],
|
||||
vulnerabilities: [],
|
||||
},
|
||||
{
|
||||
name: 'scikit-learn',
|
||||
version: '1.4.0',
|
||||
type: 'library',
|
||||
purl: 'pkg:pypi/scikit-learn@1.4.0',
|
||||
licenses: ['BSD-3-Clause'],
|
||||
vulnerabilities: [],
|
||||
},
|
||||
{
|
||||
name: 'pandas',
|
||||
version: '2.2.0',
|
||||
type: 'library',
|
||||
purl: 'pkg:pypi/pandas@2.2.0',
|
||||
licenses: ['BSD-3-Clause'],
|
||||
vulnerabilities: [],
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
generatedAt: new Date('2026-01-16T09:10:00'),
|
||||
},
|
||||
securityScan: {
|
||||
totalIssues: 3,
|
||||
critical: 0,
|
||||
high: 1,
|
||||
medium: 1,
|
||||
low: 1,
|
||||
issues: [
|
||||
{
|
||||
id: 'sec-issue-1',
|
||||
severity: 'HIGH',
|
||||
title: 'Outdated cryptography library',
|
||||
description: 'The cryptography library version 41.0.0 has known vulnerabilities',
|
||||
cve: 'CVE-2024-1234',
|
||||
cvss: 7.5,
|
||||
affectedComponent: 'cryptography',
|
||||
remediation: 'Upgrade to cryptography >= 42.0.0',
|
||||
status: 'RESOLVED',
|
||||
},
|
||||
{
|
||||
id: 'sec-issue-2',
|
||||
severity: 'MEDIUM',
|
||||
title: 'Insecure default configuration',
|
||||
description: 'Debug mode enabled in production configuration',
|
||||
cve: null,
|
||||
cvss: 5.3,
|
||||
affectedComponent: 'app-config',
|
||||
remediation: 'Set DEBUG=false in production',
|
||||
status: 'RESOLVED',
|
||||
},
|
||||
{
|
||||
id: 'sec-issue-3',
|
||||
severity: 'LOW',
|
||||
title: 'Missing security headers',
|
||||
description: 'X-Content-Type-Options header not set',
|
||||
cve: null,
|
||||
cvss: 3.1,
|
||||
affectedComponent: 'web-server',
|
||||
remediation: 'Add security headers middleware',
|
||||
status: 'RESOLVED',
|
||||
},
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
modules: [
|
||||
{
|
||||
id: 'demo-mod-1',
|
||||
name: 'Kundendaten-Modul',
|
||||
description: 'Verarbeitung von Kundendaten für Marketing und Analyse',
|
||||
regulations: ['DSGVO', 'TTDSG'],
|
||||
criticality: 'HIGH',
|
||||
processesPersonalData: true,
|
||||
hasAIComponents: true,
|
||||
},
|
||||
{
|
||||
id: 'demo-mod-2',
|
||||
name: 'HR-Modul',
|
||||
description: 'Bewerbermanagement und Personalverwaltung',
|
||||
regulations: ['DSGVO', 'AGG', 'AI Act'],
|
||||
criticality: 'HIGH',
|
||||
processesPersonalData: true,
|
||||
hasAIComponents: true,
|
||||
},
|
||||
{
|
||||
id: 'demo-mod-3',
|
||||
name: 'Support-Modul',
|
||||
description: 'Kundenservice und Chatbot-System',
|
||||
regulations: ['DSGVO', 'AI Act'],
|
||||
criticality: 'MEDIUM',
|
||||
processesPersonalData: true,
|
||||
hasAIComponents: true,
|
||||
},
|
||||
],
|
||||
requirements: [
|
||||
{
|
||||
id: 'demo-req-1',
|
||||
regulation: 'DSGVO',
|
||||
article: 'Art. 5',
|
||||
title: 'Grundsätze der Verarbeitung',
|
||||
description: 'Einhaltung der Grundsätze für die Verarbeitung personenbezogener Daten',
|
||||
criticality: 'CRITICAL',
|
||||
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
|
||||
status: 'IMPLEMENTED',
|
||||
controls: ['demo-ctrl-1', 'demo-ctrl-2', 'demo-ctrl-9'],
|
||||
},
|
||||
{
|
||||
id: 'demo-req-2',
|
||||
regulation: 'DSGVO',
|
||||
article: 'Art. 32',
|
||||
title: 'Sicherheit der Verarbeitung',
|
||||
description: 'Geeignete technische und organisatorische Maßnahmen',
|
||||
criticality: 'CRITICAL',
|
||||
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
|
||||
status: 'IMPLEMENTED',
|
||||
controls: ['demo-ctrl-1', 'demo-ctrl-3', 'demo-ctrl-4'],
|
||||
},
|
||||
{
|
||||
id: 'demo-req-3',
|
||||
regulation: 'DSGVO',
|
||||
article: 'Art. 25',
|
||||
title: 'Datenschutz durch Technikgestaltung',
|
||||
description: 'Privacy by Design und Privacy by Default',
|
||||
criticality: 'HIGH',
|
||||
applicableModules: ['demo-mod-1', 'demo-mod-2'],
|
||||
status: 'IMPLEMENTED',
|
||||
controls: ['demo-ctrl-9', 'demo-ctrl-10'],
|
||||
},
|
||||
{
|
||||
id: 'demo-req-4',
|
||||
regulation: 'AI Act',
|
||||
article: 'Art. 13',
|
||||
title: 'Transparenz',
|
||||
description: 'Transparenzanforderungen für KI-Systeme',
|
||||
criticality: 'HIGH',
|
||||
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
|
||||
status: 'IMPLEMENTED',
|
||||
controls: ['demo-ctrl-7', 'demo-ctrl-8'],
|
||||
},
|
||||
{
|
||||
id: 'demo-req-5',
|
||||
regulation: 'AI Act',
|
||||
article: 'Art. 9',
|
||||
title: 'Risikomanagement',
|
||||
description: 'Risikomanagementsystem für Hochrisiko-KI',
|
||||
criticality: 'HIGH',
|
||||
applicableModules: ['demo-mod-2'],
|
||||
status: 'IMPLEMENTED',
|
||||
controls: ['demo-ctrl-5', 'demo-ctrl-6', 'demo-ctrl-11', 'demo-ctrl-12'],
|
||||
},
|
||||
],
|
||||
controls: getDemoControls(),
|
||||
evidence: [
|
||||
{
|
||||
id: 'demo-evi-1',
|
||||
controlId: 'demo-ctrl-1',
|
||||
type: 'SCREENSHOT',
|
||||
name: 'MFA-Konfiguration Azure AD',
|
||||
description: 'Screenshot der MFA-Einstellungen im Azure AD Admin Portal',
|
||||
fileUrl: null,
|
||||
validFrom: new Date('2026-01-01'),
|
||||
validUntil: new Date('2027-01-01'),
|
||||
uploadedBy: 'IT-Security',
|
||||
uploadedAt: new Date('2026-01-10'),
|
||||
},
|
||||
{
|
||||
id: 'demo-evi-2',
|
||||
controlId: 'demo-ctrl-2',
|
||||
type: 'DOCUMENT',
|
||||
name: 'Berechtigungskonzept v2.1',
|
||||
description: 'Dokumentiertes Berechtigungskonzept mit Rollenmatrix',
|
||||
fileUrl: null,
|
||||
validFrom: new Date('2026-01-01'),
|
||||
validUntil: null,
|
||||
uploadedBy: 'IT-Security',
|
||||
uploadedAt: new Date('2026-01-05'),
|
||||
},
|
||||
{
|
||||
id: 'demo-evi-5',
|
||||
controlId: 'demo-ctrl-5',
|
||||
type: 'AUDIT_REPORT',
|
||||
name: 'Bias-Audit Q1/2026',
|
||||
description: 'Externer Audit-Bericht zur Fairness des KI-Modells',
|
||||
fileUrl: null,
|
||||
validFrom: new Date('2026-01-15'),
|
||||
validUntil: new Date('2026-04-15'),
|
||||
uploadedBy: 'Data Science Lead',
|
||||
uploadedAt: new Date('2026-01-20'),
|
||||
},
|
||||
],
|
||||
checklist: [
|
||||
{
|
||||
id: 'demo-chk-1',
|
||||
requirementId: 'demo-req-1',
|
||||
title: 'Rechtmäßigkeit der Verarbeitung geprüft',
|
||||
description: 'Dokumentierte Prüfung der Rechtsgrundlagen',
|
||||
status: 'PASSED',
|
||||
notes: 'Geprüft durch DSB',
|
||||
verifiedBy: 'DSB',
|
||||
verifiedAt: new Date('2026-01-20'),
|
||||
},
|
||||
{
|
||||
id: 'demo-chk-2',
|
||||
requirementId: 'demo-req-2',
|
||||
title: 'TOMs dokumentiert und umgesetzt',
|
||||
description: 'Technische und organisatorische Maßnahmen',
|
||||
status: 'PASSED',
|
||||
notes: 'Alle TOMs implementiert',
|
||||
verifiedBy: 'CISO',
|
||||
verifiedAt: new Date('2026-01-21'),
|
||||
},
|
||||
],
|
||||
risks: getDemoRisks(),
|
||||
|
||||
// Phase 2 Data
|
||||
aiActClassification: {
|
||||
riskCategory: 'HIGH',
|
||||
systemType: 'Beschäftigungsbezogenes KI-System (Art. 6 Abs. 2 AI Act)',
|
||||
obligations: [
|
||||
{
|
||||
id: 'demo-ai-obl-1',
|
||||
article: 'Art. 9',
|
||||
title: 'Risikomanagementsystem',
|
||||
description: 'Einrichtung eines KI-Risikomanagementsystems',
|
||||
deadline: new Date('2026-08-01'),
|
||||
status: 'IN_PROGRESS',
|
||||
},
|
||||
{
|
||||
id: 'demo-ai-obl-2',
|
||||
article: 'Art. 10',
|
||||
title: 'Daten-Governance',
|
||||
description: 'Anforderungen an Trainingsdaten',
|
||||
deadline: new Date('2026-08-01'),
|
||||
status: 'COMPLETED',
|
||||
},
|
||||
{
|
||||
id: 'demo-ai-obl-3',
|
||||
article: 'Art. 13',
|
||||
title: 'Transparenz',
|
||||
description: 'Dokumentation für Nutzer',
|
||||
deadline: new Date('2026-08-01'),
|
||||
status: 'COMPLETED',
|
||||
},
|
||||
],
|
||||
assessmentDate: new Date('2026-01-25'),
|
||||
assessedBy: 'Compliance Team',
|
||||
justification: 'Das System fällt unter Art. 6 Abs. 2 lit. a AI Act (Einstellung und Auswahl von Personen).',
|
||||
},
|
||||
obligations: [
|
||||
{
|
||||
id: 'demo-obl-1',
|
||||
regulation: 'DSGVO',
|
||||
article: 'Art. 30',
|
||||
title: 'Verarbeitungsverzeichnis',
|
||||
description: 'Führung eines Verzeichnisses der Verarbeitungstätigkeiten',
|
||||
deadline: null,
|
||||
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
|
||||
status: 'COMPLETED',
|
||||
responsible: 'DSB',
|
||||
},
|
||||
{
|
||||
id: 'demo-obl-2',
|
||||
regulation: 'DSGVO',
|
||||
article: 'Art. 35',
|
||||
title: 'Datenschutz-Folgenabschätzung',
|
||||
description: 'Durchführung einer DSFA für Hochrisiko-Verarbeitungen',
|
||||
deadline: null,
|
||||
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
|
||||
status: 'COMPLETED',
|
||||
responsible: 'DSB',
|
||||
},
|
||||
{
|
||||
id: 'demo-obl-3',
|
||||
regulation: 'AI Act',
|
||||
article: 'Art. 49',
|
||||
title: 'CE-Kennzeichnung',
|
||||
description: 'CE-Kennzeichnung für Hochrisiko-KI-Systeme',
|
||||
deadline: new Date('2026-08-01'),
|
||||
penalty: 'Bis zu 35 Mio. EUR oder 7% des Jahresumsatzes',
|
||||
status: 'PENDING',
|
||||
responsible: 'Compliance',
|
||||
},
|
||||
],
|
||||
dsfa: getDemoDSFA(),
|
||||
toms: getDemoTOMs(),
|
||||
retentionPolicies: getDemoRetentionPolicies(),
|
||||
vvt: getDemoProcessingActivities(),
|
||||
|
||||
// Documents, Cookie Banner, etc. - partially filled
|
||||
documents: [],
|
||||
cookieBanner: null,
|
||||
consents: [],
|
||||
dsrConfig: null,
|
||||
escalationWorkflows: [],
|
||||
|
||||
// Security
|
||||
sbom: null,
|
||||
securityIssues: [],
|
||||
securityBacklog: [],
|
||||
|
||||
// UI State
|
||||
commandBarHistory: [],
|
||||
recentSearches: ['DSGVO Art. 5', 'Bias-Monitoring', 'TOM Verschlüsselung'],
|
||||
preferences: {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed demo data into the database via API
|
||||
* This ensures demo data is stored exactly like real customer data
|
||||
*/
|
||||
export async function seedDemoData(
|
||||
tenantId: string = 'demo-tenant',
|
||||
userId: string = 'demo-user',
|
||||
apiBaseUrl?: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const apiClient = getSDKApiClient(tenantId)
|
||||
|
||||
// Generate the demo state
|
||||
const demoState = generateDemoState(tenantId, userId) as SDKState
|
||||
|
||||
// Save via the same API that real data uses
|
||||
await apiClient.saveState(demoState)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Demo data successfully seeded for tenant ${tenantId}`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to seed demo data:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error during seeding',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if demo data exists for a tenant
|
||||
*/
|
||||
export async function hasDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
|
||||
try {
|
||||
const apiClient = getSDKApiClient(tenantId)
|
||||
const response = await apiClient.getState()
|
||||
|
||||
// Check if we have any use cases (indicating data exists)
|
||||
return response !== null && response.state && Array.isArray(response.state.useCases) && response.state.useCases.length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear demo data for a tenant
|
||||
*/
|
||||
export async function clearDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
|
||||
try {
|
||||
const apiClient = getSDKApiClient(tenantId)
|
||||
await apiClient.deleteState()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed demo data via direct API call (for use outside of React context)
|
||||
* This is useful for server-side seeding or CLI tools
|
||||
*/
|
||||
export async function seedDemoDataDirect(
|
||||
baseUrl: string,
|
||||
tenantId: string = 'demo-tenant',
|
||||
userId: string = 'demo-user'
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const demoState = generateDemoState(tenantId, userId)
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/sdk/v1/state`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId,
|
||||
userId,
|
||||
state: demoState,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
|
||||
throw new Error(error.message || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Demo data successfully seeded for tenant ${tenantId}`,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to seed demo data:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error during seeding',
|
||||
}
|
||||
}
|
||||
}
|
||||
268
admin-v2/lib/sdk/demo-data/risks.ts
Normal file
268
admin-v2/lib/sdk/demo-data/risks.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Demo Risks for AI Compliance SDK
|
||||
*/
|
||||
|
||||
import { Risk, RiskMitigation } from '../types'
|
||||
|
||||
export const DEMO_RISKS: Risk[] = [
|
||||
{
|
||||
id: 'demo-risk-1',
|
||||
title: 'Unbefugter Zugriff auf personenbezogene Daten',
|
||||
description: 'Risiko des unbefugten Zugriffs auf Kundendaten durch externe Angreifer oder interne Mitarbeiter ohne entsprechende Berechtigung.',
|
||||
category: 'Datensicherheit',
|
||||
likelihood: 3,
|
||||
impact: 5,
|
||||
severity: 'CRITICAL',
|
||||
inherentRiskScore: 15,
|
||||
residualRiskScore: 6,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-1a',
|
||||
description: 'Implementierung von Multi-Faktor-Authentifizierung für alle Systemzugriffe',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 40,
|
||||
controlId: 'demo-ctrl-1',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-1b',
|
||||
description: 'Rollenbasiertes Zugriffskonzept mit Least-Privilege-Prinzip',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 30,
|
||||
controlId: 'demo-ctrl-2',
|
||||
},
|
||||
],
|
||||
owner: 'CISO',
|
||||
relatedControls: ['demo-ctrl-1', 'demo-ctrl-2'],
|
||||
relatedRequirements: ['demo-req-1', 'demo-req-2'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-2',
|
||||
title: 'KI-Bias bei automatisierten Entscheidungen',
|
||||
description: 'Das KI-System könnte systematische Verzerrungen aufweisen, die zu diskriminierenden Entscheidungen führen, insbesondere bei der Bewerbungsvorauswahl.',
|
||||
category: 'KI-Ethik',
|
||||
likelihood: 4,
|
||||
impact: 4,
|
||||
severity: 'HIGH',
|
||||
inherentRiskScore: 16,
|
||||
residualRiskScore: 8,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-2a',
|
||||
description: 'Regelmäßiges Bias-Monitoring mit Fairness-Metriken',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 30,
|
||||
controlId: 'demo-ctrl-5',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-2b',
|
||||
description: 'Human-in-the-Loop bei kritischen Entscheidungen',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 25,
|
||||
controlId: 'demo-ctrl-6',
|
||||
},
|
||||
],
|
||||
owner: 'Data Science Lead',
|
||||
relatedControls: ['demo-ctrl-5', 'demo-ctrl-6'],
|
||||
relatedRequirements: ['demo-req-5', 'demo-req-6'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-3',
|
||||
title: 'Datenverlust durch Systemausfall',
|
||||
description: 'Verlust von Kundendaten und KI-Modellen durch Hardware-Defekte, Softwarefehler oder Naturkatastrophen.',
|
||||
category: 'Verfügbarkeit',
|
||||
likelihood: 2,
|
||||
impact: 5,
|
||||
severity: 'HIGH',
|
||||
inherentRiskScore: 10,
|
||||
residualRiskScore: 3,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-3a',
|
||||
description: 'Tägliche inkrementelle und wöchentliche Vollbackups',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 40,
|
||||
controlId: 'demo-ctrl-3',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-3b',
|
||||
description: 'Georedundante Datenspeicherung in zwei Rechenzentren',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 35,
|
||||
controlId: 'demo-ctrl-4',
|
||||
},
|
||||
],
|
||||
owner: 'IT-Leiter',
|
||||
relatedControls: ['demo-ctrl-3', 'demo-ctrl-4'],
|
||||
relatedRequirements: ['demo-req-3'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-4',
|
||||
title: 'Unzureichende Transparenz bei KI-Entscheidungen',
|
||||
description: 'Betroffene verstehen nicht, wie KI-Entscheidungen zustande kommen, was zu Beschwerden und regulatorischen Problemen führen kann.',
|
||||
category: 'Transparenz',
|
||||
likelihood: 4,
|
||||
impact: 3,
|
||||
severity: 'MEDIUM',
|
||||
inherentRiskScore: 12,
|
||||
residualRiskScore: 4,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-4a',
|
||||
description: 'Explainable AI Komponenten für nachvollziehbare Entscheidungen',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 40,
|
||||
controlId: 'demo-ctrl-7',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-4b',
|
||||
description: 'Verständliche Informationen für Betroffene gem. Art. 13-14 DSGVO',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 30,
|
||||
controlId: 'demo-ctrl-8',
|
||||
},
|
||||
],
|
||||
owner: 'DSB',
|
||||
relatedControls: ['demo-ctrl-7', 'demo-ctrl-8'],
|
||||
relatedRequirements: ['demo-req-4'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-5',
|
||||
title: 'Unerlaubte Profilbildung',
|
||||
description: 'Durch die Zusammenführung verschiedener Datenquellen könnte eine unzulässige umfassende Profilbildung von Personen entstehen.',
|
||||
category: 'Datenschutz',
|
||||
likelihood: 3,
|
||||
impact: 4,
|
||||
severity: 'HIGH',
|
||||
inherentRiskScore: 12,
|
||||
residualRiskScore: 6,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-5a',
|
||||
description: 'Strenge Zweckbindung der Datenverarbeitung',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 25,
|
||||
controlId: 'demo-ctrl-9',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-5b',
|
||||
description: 'Datensparsamkeit durch Aggregation und Anonymisierung',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 30,
|
||||
controlId: 'demo-ctrl-10',
|
||||
},
|
||||
],
|
||||
owner: 'DSB',
|
||||
relatedControls: ['demo-ctrl-9', 'demo-ctrl-10'],
|
||||
relatedRequirements: ['demo-req-7', 'demo-req-8'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-6',
|
||||
title: 'Mangelnde Modell-Robustheit',
|
||||
description: 'KI-Modelle könnten durch Adversarial Attacks oder veränderte Inputdaten manipuliert werden und falsche Ergebnisse liefern.',
|
||||
category: 'KI-Sicherheit',
|
||||
likelihood: 2,
|
||||
impact: 4,
|
||||
severity: 'MEDIUM',
|
||||
inherentRiskScore: 8,
|
||||
residualRiskScore: 4,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-6a',
|
||||
description: 'Input-Validierung und Anomalie-Erkennung',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 30,
|
||||
controlId: 'demo-ctrl-11',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-6b',
|
||||
description: 'Regelmäßige Modell-Retraining und Performance-Monitoring',
|
||||
type: 'MITIGATE',
|
||||
status: 'IN_PROGRESS',
|
||||
effectiveness: 20,
|
||||
controlId: 'demo-ctrl-12',
|
||||
},
|
||||
],
|
||||
owner: 'Data Science Lead',
|
||||
relatedControls: ['demo-ctrl-11', 'demo-ctrl-12'],
|
||||
relatedRequirements: ['demo-req-9'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-7',
|
||||
title: 'Verstoß gegen Aufbewahrungsfristen',
|
||||
description: 'Daten werden länger als zulässig gespeichert oder zu früh gelöscht, was zu Compliance-Verstößen führt.',
|
||||
category: 'Datenschutz',
|
||||
likelihood: 3,
|
||||
impact: 3,
|
||||
severity: 'MEDIUM',
|
||||
inherentRiskScore: 9,
|
||||
residualRiskScore: 3,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-7a',
|
||||
description: 'Automatisierte Löschroutinen mit Retention-Policy-Enforcement',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 40,
|
||||
controlId: 'demo-ctrl-13',
|
||||
},
|
||||
{
|
||||
id: 'demo-mit-7b',
|
||||
description: 'Quartalsmäßige Überprüfung der Löschprotokolle',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 25,
|
||||
controlId: 'demo-ctrl-14',
|
||||
},
|
||||
],
|
||||
owner: 'DSB',
|
||||
relatedControls: ['demo-ctrl-13', 'demo-ctrl-14'],
|
||||
relatedRequirements: ['demo-req-10'],
|
||||
},
|
||||
{
|
||||
id: 'demo-risk-8',
|
||||
title: 'Fehlende Nachvollziehbarkeit im Audit',
|
||||
description: 'Bei Prüfungen können Verarbeitungsvorgänge nicht lückenlos nachvollzogen werden.',
|
||||
category: 'Compliance',
|
||||
likelihood: 2,
|
||||
impact: 3,
|
||||
severity: 'MEDIUM',
|
||||
inherentRiskScore: 6,
|
||||
residualRiskScore: 2,
|
||||
status: 'MITIGATED',
|
||||
mitigation: [
|
||||
{
|
||||
id: 'demo-mit-8a',
|
||||
description: 'Umfassendes Audit-Logging aller Verarbeitungsvorgänge',
|
||||
type: 'MITIGATE',
|
||||
status: 'COMPLETED',
|
||||
effectiveness: 50,
|
||||
controlId: 'demo-ctrl-15',
|
||||
},
|
||||
],
|
||||
owner: 'IT-Leiter',
|
||||
relatedControls: ['demo-ctrl-15'],
|
||||
relatedRequirements: ['demo-req-11'],
|
||||
},
|
||||
]
|
||||
|
||||
export function getDemoRisks(): Risk[] {
|
||||
return DEMO_RISKS
|
||||
}
|
||||
296
admin-v2/lib/sdk/demo-data/toms.ts
Normal file
296
admin-v2/lib/sdk/demo-data/toms.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Demo TOMs (Technical & Organizational Measures) for AI Compliance SDK
|
||||
* These are seed data structures - actual data is stored in database
|
||||
*/
|
||||
|
||||
import { TOM } from '../types'
|
||||
|
||||
export const DEMO_TOMS: TOM[] = [
|
||||
// Zugangskontrolle
|
||||
{
|
||||
id: 'demo-tom-1',
|
||||
category: 'Zugangskontrolle',
|
||||
name: 'Physische Zutrittskontrolle',
|
||||
description: 'Elektronische Zugangskontrollsysteme mit personenbezogenen Zutrittskarten für alle Serverräume und Rechenzentren. Protokollierung aller Zutritte.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'Facility Management',
|
||||
implementationDate: new Date('2025-06-01'),
|
||||
reviewDate: new Date('2026-06-01'),
|
||||
evidence: ['demo-evi-tom-1'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-2',
|
||||
category: 'Zugangskontrolle',
|
||||
name: 'Besuchermanagement',
|
||||
description: 'Registrierung aller Besucher mit Identitätsprüfung, Ausgabe von Besucherausweisen und permanente Begleitung in sicherheitsrelevanten Bereichen.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'MEDIUM',
|
||||
responsiblePerson: 'Empfang/Security',
|
||||
implementationDate: new Date('2025-03-15'),
|
||||
reviewDate: new Date('2026-03-15'),
|
||||
evidence: ['demo-evi-tom-2'],
|
||||
},
|
||||
|
||||
// Zugriffskontrolle
|
||||
{
|
||||
id: 'demo-tom-3',
|
||||
category: 'Zugriffskontrolle',
|
||||
name: 'Identity & Access Management (IAM)',
|
||||
description: 'Zentrales IAM-System mit automatischer Provisionierung, Deprovisionierung und regelmäßiger Rezertifizierung aller Benutzerkonten.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Sicherheit',
|
||||
implementationDate: new Date('2025-01-01'),
|
||||
reviewDate: new Date('2026-01-01'),
|
||||
evidence: ['demo-evi-tom-3'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-4',
|
||||
category: 'Zugriffskontrolle',
|
||||
name: 'Privileged Access Management (PAM)',
|
||||
description: 'Spezielles Management für administrative Zugänge mit Session-Recording, automatischer Passwortrotation und Just-in-Time-Berechtigungen.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Sicherheit',
|
||||
implementationDate: new Date('2025-04-01'),
|
||||
reviewDate: new Date('2026-04-01'),
|
||||
evidence: ['demo-evi-tom-4'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-5',
|
||||
category: 'Zugriffskontrolle',
|
||||
name: 'Berechtigungskonzept-Review',
|
||||
description: 'Halbjährliche Überprüfung aller Berechtigungen durch die jeweiligen Fachbereichsleiter mit dokumentierter Rezertifizierung.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'Fachbereichsleiter',
|
||||
implementationDate: new Date('2025-02-01'),
|
||||
reviewDate: new Date('2026-02-01'),
|
||||
evidence: ['demo-evi-tom-5'],
|
||||
},
|
||||
|
||||
// Verschlüsselung
|
||||
{
|
||||
id: 'demo-tom-6',
|
||||
category: 'Verschlüsselung',
|
||||
name: 'Datenverschlüsselung at Rest',
|
||||
description: 'AES-256 Verschlüsselung aller personenbezogenen Daten in Datenbanken und Dateisystemen. Key Management über HSM.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Sicherheit',
|
||||
implementationDate: new Date('2025-01-15'),
|
||||
reviewDate: new Date('2026-01-15'),
|
||||
evidence: ['demo-evi-tom-6'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-7',
|
||||
category: 'Verschlüsselung',
|
||||
name: 'Transportverschlüsselung',
|
||||
description: 'TLS 1.3 für alle externen Verbindungen, mTLS für interne Service-Kommunikation. Regelmäßige Überprüfung der Cipher Suites.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Sicherheit',
|
||||
implementationDate: new Date('2025-01-01'),
|
||||
reviewDate: new Date('2026-01-01'),
|
||||
evidence: ['demo-evi-tom-7'],
|
||||
},
|
||||
|
||||
// Pseudonymisierung
|
||||
{
|
||||
id: 'demo-tom-8',
|
||||
category: 'Pseudonymisierung',
|
||||
name: 'Pseudonymisierungs-Pipeline',
|
||||
description: 'Automatisierte Pseudonymisierung von Daten vor der Verarbeitung in Analytics-Systemen. Reversible Zuordnung nur durch autorisierten Prozess.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'Data Engineering',
|
||||
implementationDate: new Date('2025-05-01'),
|
||||
reviewDate: new Date('2026-05-01'),
|
||||
evidence: ['demo-evi-tom-8'],
|
||||
},
|
||||
|
||||
// Integrität
|
||||
{
|
||||
id: 'demo-tom-9',
|
||||
category: 'Integrität',
|
||||
name: 'Datenintegritätsprüfung',
|
||||
description: 'Checksummen-Validierung bei allen Datentransfers, Hash-Verifikation gespeicherter Daten, automatische Alerts bei Abweichungen.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'IT-Betrieb',
|
||||
implementationDate: new Date('2025-03-01'),
|
||||
reviewDate: new Date('2026-03-01'),
|
||||
evidence: ['demo-evi-tom-9'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-10',
|
||||
category: 'Integrität',
|
||||
name: 'Change Management',
|
||||
description: 'Dokumentierter Change-Prozess mit Vier-Augen-Prinzip für alle Änderungen an produktiven Systemen. CAB-Freigabe für kritische Changes.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'IT-Leitung',
|
||||
implementationDate: new Date('2025-01-01'),
|
||||
reviewDate: new Date('2026-01-01'),
|
||||
evidence: ['demo-evi-tom-10'],
|
||||
},
|
||||
|
||||
// Verfügbarkeit
|
||||
{
|
||||
id: 'demo-tom-11',
|
||||
category: 'Verfügbarkeit',
|
||||
name: 'Disaster Recovery Plan',
|
||||
description: 'Dokumentierter und getesteter DR-Plan mit RTO <4h und RPO <1h. Jährliche DR-Tests mit Dokumentation.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Leitung',
|
||||
implementationDate: new Date('2025-02-01'),
|
||||
reviewDate: new Date('2026-02-01'),
|
||||
evidence: ['demo-evi-tom-11'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-12',
|
||||
category: 'Verfügbarkeit',
|
||||
name: 'High Availability Cluster',
|
||||
description: 'Aktiv-Aktiv-Cluster für alle kritischen Systeme mit automatischem Failover. 99,9% Verfügbarkeits-SLA.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Betrieb',
|
||||
implementationDate: new Date('2025-01-01'),
|
||||
reviewDate: new Date('2026-01-01'),
|
||||
evidence: ['demo-evi-tom-12'],
|
||||
},
|
||||
|
||||
// Belastbarkeit
|
||||
{
|
||||
id: 'demo-tom-13',
|
||||
category: 'Belastbarkeit',
|
||||
name: 'Load Balancing & Auto-Scaling',
|
||||
description: 'Dynamische Skalierung basierend auf Last-Metriken. Load Balancer mit Health Checks und automatischer Traffic-Umleitung.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'IT-Betrieb',
|
||||
implementationDate: new Date('2025-04-01'),
|
||||
reviewDate: new Date('2026-04-01'),
|
||||
evidence: ['demo-evi-tom-13'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-14',
|
||||
category: 'Belastbarkeit',
|
||||
name: 'DDoS-Schutz',
|
||||
description: 'Cloudbasierter DDoS-Schutz mit automatischer Traffic-Filterung. Kapazität für 10x Normal-Traffic.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'IT-Sicherheit',
|
||||
implementationDate: new Date('2025-01-01'),
|
||||
reviewDate: new Date('2026-01-01'),
|
||||
evidence: ['demo-evi-tom-14'],
|
||||
},
|
||||
|
||||
// Wiederherstellbarkeit
|
||||
{
|
||||
id: 'demo-tom-15',
|
||||
category: 'Wiederherstellbarkeit',
|
||||
name: 'Backup-Strategie',
|
||||
description: '3-2-1 Backup-Strategie: 3 Kopien, 2 verschiedene Medien, 1 Offsite. Tägliche inkrementelle, wöchentliche Vollbackups.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'CRITICAL',
|
||||
responsiblePerson: 'IT-Betrieb',
|
||||
implementationDate: new Date('2025-01-01'),
|
||||
reviewDate: new Date('2026-01-01'),
|
||||
evidence: ['demo-evi-tom-15'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-16',
|
||||
category: 'Wiederherstellbarkeit',
|
||||
name: 'Restore-Tests',
|
||||
description: 'Monatliche Restore-Tests mit zufällig ausgewählten Daten. Dokumentation der Recovery-Zeit und Vollständigkeit.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'IT-Betrieb',
|
||||
implementationDate: new Date('2025-02-01'),
|
||||
reviewDate: new Date('2026-02-01'),
|
||||
evidence: ['demo-evi-tom-16'],
|
||||
},
|
||||
|
||||
// Überprüfung & Bewertung
|
||||
{
|
||||
id: 'demo-tom-17',
|
||||
category: 'Überprüfung & Bewertung',
|
||||
name: 'Penetration Tests',
|
||||
description: 'Jährliche externe Penetration Tests durch zertifizierte Dienstleister. Zusätzliche Tests nach größeren Änderungen.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'IT-Sicherheit',
|
||||
implementationDate: new Date('2025-03-01'),
|
||||
reviewDate: new Date('2026-03-01'),
|
||||
evidence: ['demo-evi-tom-17'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-18',
|
||||
category: 'Überprüfung & Bewertung',
|
||||
name: 'Security Awareness Training',
|
||||
description: 'Verpflichtendes Security-Training für alle Mitarbeiter bei Einstellung und jährlich. Phishing-Simulationen quartalsweise.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'MEDIUM',
|
||||
responsiblePerson: 'HR / IT-Sicherheit',
|
||||
implementationDate: new Date('2025-01-15'),
|
||||
reviewDate: new Date('2026-01-15'),
|
||||
evidence: ['demo-evi-tom-18'],
|
||||
},
|
||||
|
||||
// KI-spezifische TOMs
|
||||
{
|
||||
id: 'demo-tom-19',
|
||||
category: 'KI-Governance',
|
||||
name: 'Model Governance Framework',
|
||||
description: 'Dokumentierter Prozess für Entwicklung, Test, Deployment und Monitoring von KI-Modellen. Model Cards für alle produktiven Modelle.',
|
||||
type: 'ORGANIZATIONAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'Data Science Lead',
|
||||
implementationDate: new Date('2025-06-01'),
|
||||
reviewDate: new Date('2026-06-01'),
|
||||
evidence: ['demo-evi-tom-19'],
|
||||
},
|
||||
{
|
||||
id: 'demo-tom-20',
|
||||
category: 'KI-Governance',
|
||||
name: 'Bias Detection & Monitoring',
|
||||
description: 'Automatisiertes Monitoring der Modell-Outputs auf Bias. Alerting bei signifikanten Abweichungen von Fairness-Metriken.',
|
||||
type: 'TECHNICAL',
|
||||
implementationStatus: 'IMPLEMENTED',
|
||||
priority: 'HIGH',
|
||||
responsiblePerson: 'Data Science Lead',
|
||||
implementationDate: new Date('2025-07-01'),
|
||||
reviewDate: new Date('2026-07-01'),
|
||||
evidence: ['demo-evi-tom-20'],
|
||||
},
|
||||
]
|
||||
|
||||
export function getDemoTOMs(): TOM[] {
|
||||
return DEMO_TOMS.map(tom => ({
|
||||
...tom,
|
||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
||||
}))
|
||||
}
|
||||
85
admin-v2/lib/sdk/demo-data/use-cases.ts
Normal file
85
admin-v2/lib/sdk/demo-data/use-cases.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Demo Use Cases for AI Compliance SDK
|
||||
*/
|
||||
|
||||
import { UseCaseAssessment, AssessmentResult } from '../types'
|
||||
|
||||
export const DEMO_USE_CASES: UseCaseAssessment[] = [
|
||||
{
|
||||
id: 'demo-uc-1',
|
||||
name: 'KI-gestützte Kundenanalyse',
|
||||
description: 'Analyse von Kundenverhalten und Präferenzen mittels Machine Learning zur Personalisierung von Angeboten und Verbesserung des Customer Lifetime Value. Das System verarbeitet Transaktionsdaten, Clickstreams und demographische Informationen.',
|
||||
category: 'Marketing',
|
||||
stepsCompleted: 5,
|
||||
steps: [
|
||||
{ id: 'uc1-step-1', name: 'Grunddaten', completed: true, data: { type: 'customer-analytics', department: 'Marketing' } },
|
||||
{ id: 'uc1-step-2', name: 'Datenquellen', completed: true, data: { sources: ['CRM', 'Webshop', 'Newsletter'] } },
|
||||
{ id: 'uc1-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['Clustering', 'Recommender', 'Churn-Prediction'] } },
|
||||
{ id: 'uc1-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
|
||||
{ id: 'uc1-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
|
||||
],
|
||||
assessmentResult: {
|
||||
riskLevel: 'HIGH',
|
||||
applicableRegulations: ['DSGVO', 'AI Act', 'TTDSG'],
|
||||
recommendedControls: ['Einwilligungsmanagement', 'Profilbildungstransparenz', 'Opt-out-Mechanismus'],
|
||||
dsfaRequired: true,
|
||||
aiActClassification: 'LIMITED',
|
||||
},
|
||||
createdAt: new Date('2026-01-15'),
|
||||
updatedAt: new Date('2026-02-01'),
|
||||
},
|
||||
{
|
||||
id: 'demo-uc-2',
|
||||
name: 'Automatisierte Bewerbungsvorauswahl',
|
||||
description: 'KI-System zur Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse, Qualifikationsabgleich und Erfahrungsbewertung. Ziel ist die Effizienzsteigerung im Recruiting-Prozess bei gleichzeitiger Gewährleistung von Fairness.',
|
||||
category: 'HR',
|
||||
stepsCompleted: 5,
|
||||
steps: [
|
||||
{ id: 'uc2-step-1', name: 'Grunddaten', completed: true, data: { type: 'hr-screening', department: 'Personal' } },
|
||||
{ id: 'uc2-step-2', name: 'Datenquellen', completed: true, data: { sources: ['Bewerbungsportal', 'LinkedIn', 'XING'] } },
|
||||
{ id: 'uc2-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['NLP', 'Matching', 'Scoring'] } },
|
||||
{ id: 'uc2-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Bewerber'] } },
|
||||
{ id: 'uc2-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
|
||||
],
|
||||
assessmentResult: {
|
||||
riskLevel: 'HIGH',
|
||||
applicableRegulations: ['DSGVO', 'AI Act', 'AGG'],
|
||||
recommendedControls: ['Bias-Monitoring', 'Human-in-the-Loop', 'Transparenzpflichten'],
|
||||
dsfaRequired: true,
|
||||
aiActClassification: 'HIGH',
|
||||
},
|
||||
createdAt: new Date('2026-01-20'),
|
||||
updatedAt: new Date('2026-02-02'),
|
||||
},
|
||||
{
|
||||
id: 'demo-uc-3',
|
||||
name: 'Chatbot für Kundenservice',
|
||||
description: 'Konversationeller KI-Assistent für die automatisierte Beantwortung von Kundenanfragen im First-Level-Support. Basiert auf Large Language Models mit firmeneigenem Wissen.',
|
||||
category: 'Kundenservice',
|
||||
stepsCompleted: 5,
|
||||
steps: [
|
||||
{ id: 'uc3-step-1', name: 'Grunddaten', completed: true, data: { type: 'chatbot', department: 'Support' } },
|
||||
{ id: 'uc3-step-2', name: 'Datenquellen', completed: true, data: { sources: ['FAQ', 'Wissensdatenbank', 'Ticketsystem'] } },
|
||||
{ id: 'uc3-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['LLM', 'RAG', 'Intent-Classification'] } },
|
||||
{ id: 'uc3-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
|
||||
{ id: 'uc3-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'MEDIUM' } },
|
||||
],
|
||||
assessmentResult: {
|
||||
riskLevel: 'MEDIUM',
|
||||
applicableRegulations: ['DSGVO', 'AI Act'],
|
||||
recommendedControls: ['KI-Kennzeichnung', 'Übergabe an Menschen', 'Datensparsamkeit'],
|
||||
dsfaRequired: false,
|
||||
aiActClassification: 'LIMITED',
|
||||
},
|
||||
createdAt: new Date('2026-01-25'),
|
||||
updatedAt: new Date('2026-02-03'),
|
||||
},
|
||||
]
|
||||
|
||||
export function getDemoUseCases(): UseCaseAssessment[] {
|
||||
return DEMO_USE_CASES.map(uc => ({
|
||||
...uc,
|
||||
createdAt: new Date(uc.createdAt),
|
||||
updatedAt: new Date(uc.updatedAt),
|
||||
}))
|
||||
}
|
||||
316
admin-v2/lib/sdk/demo-data/vvt.ts
Normal file
316
admin-v2/lib/sdk/demo-data/vvt.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Demo VVT (Verarbeitungsverzeichnis / Processing Activities Register) for AI Compliance SDK
|
||||
* Art. 30 DSGVO - These are seed data structures - actual data is stored in database
|
||||
*/
|
||||
|
||||
import { ProcessingActivity, RetentionPolicy } from '../types'
|
||||
|
||||
export const DEMO_PROCESSING_ACTIVITIES: ProcessingActivity[] = [
|
||||
{
|
||||
id: 'demo-pa-1',
|
||||
name: 'KI-gestützte Kundenanalyse',
|
||||
purpose: 'Analyse von Kundenverhalten und Präferenzen zur Personalisierung von Angeboten, Churn-Prediction und Marketing-Optimierung',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse) / Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweitertes Profiling)',
|
||||
dataCategories: [
|
||||
'Stammdaten (Name, Adresse, E-Mail, Telefon)',
|
||||
'Transaktionsdaten (Käufe, Bestellungen, Retouren)',
|
||||
'Nutzungsdaten (Clickstreams, Seitenaufrufe, Verweildauer)',
|
||||
'Demographische Daten (Alter, Geschlecht, PLZ-Region)',
|
||||
],
|
||||
dataSubjects: [
|
||||
'Bestandskunden (ca. 250.000 aktive)',
|
||||
'Registrierte Interessenten (ca. 100.000)',
|
||||
'Newsletter-Abonnenten (ca. 180.000)',
|
||||
],
|
||||
recipients: [
|
||||
'Interne Fachabteilungen (Marketing, Vertrieb)',
|
||||
'E-Mail-Marketing-Dienstleister (AV-Vertrag vorhanden)',
|
||||
'Cloud-Infrastruktur-Anbieter (AV-Vertrag vorhanden)',
|
||||
],
|
||||
thirdCountryTransfers: false,
|
||||
retentionPeriod: '3 Jahre nach letzter Aktivität, danach Anonymisierung',
|
||||
technicalMeasures: [
|
||||
'AES-256 Verschlüsselung',
|
||||
'Pseudonymisierung',
|
||||
'Zugriffskontrolle mit MFA',
|
||||
'Audit-Logging',
|
||||
],
|
||||
organizationalMeasures: [
|
||||
'Rollenbasiertes Berechtigungskonzept',
|
||||
'Verpflichtung auf Datengeheimnis',
|
||||
'Regelmäßige Datenschutzschulungen',
|
||||
'Dokumentierte Prozesse',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-pa-2',
|
||||
name: 'Automatisierte Bewerbungsvorauswahl',
|
||||
purpose: 'KI-gestützte Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse und Qualifikationsabgleich zur Effizienzsteigerung im Recruiting',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (vorvertragliche Maßnahmen) / § 26 BDSG (Beschäftigungsverhältnis)',
|
||||
dataCategories: [
|
||||
'Bewerberdaten (Name, Kontakt, Geburtsdatum)',
|
||||
'Qualifikationen (Ausbildung, Berufserfahrung, Zertifikate)',
|
||||
'Lebenslaufdaten (Werdegang, Fähigkeiten)',
|
||||
'Bewerbungsschreiben',
|
||||
],
|
||||
dataSubjects: [
|
||||
'Bewerber auf offene Stellen',
|
||||
'Initiativbewerber',
|
||||
],
|
||||
recipients: [
|
||||
'HR-Abteilung',
|
||||
'Fachabteilungsleiter (nur finale Kandidaten)',
|
||||
'Betriebsrat (Einsichtnahme möglich)',
|
||||
],
|
||||
thirdCountryTransfers: false,
|
||||
retentionPeriod: '6 Monate nach Abschluss des Bewerbungsverfahrens (bei Ablehnung), länger nur mit Einwilligung für Talentpool',
|
||||
technicalMeasures: [
|
||||
'Verschlüsselte Speicherung',
|
||||
'Zugangsbeschränkung auf HR',
|
||||
'Automatische Löschroutinen',
|
||||
'Bias-Monitoring',
|
||||
],
|
||||
organizationalMeasures: [
|
||||
'Human-in-the-Loop für finale Entscheidungen',
|
||||
'Dokumentierte KI-Entscheidungskriterien',
|
||||
'Transparente Information an Bewerber',
|
||||
'Regelmäßige Fairness-Audits',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-pa-3',
|
||||
name: 'Kundenservice-Chatbot',
|
||||
purpose: 'Automatisierte Beantwortung von Kundenanfragen im First-Level-Support mittels KI-gestütztem Dialogsystem',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) / Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)',
|
||||
dataCategories: [
|
||||
'Kundenstammdaten (zur Identifikation)',
|
||||
'Kommunikationsinhalte (Chat-Verläufe)',
|
||||
'Technische Daten (Session-ID, Zeitstempel)',
|
||||
'Serviceanfragen und deren Lösungen',
|
||||
],
|
||||
dataSubjects: [
|
||||
'Kunden mit aktiven Verträgen',
|
||||
'Interessenten mit Anfragen',
|
||||
],
|
||||
recipients: [
|
||||
'Kundenservice-Team (bei Eskalation)',
|
||||
'Cloud-Anbieter (Hosting, AV-Vertrag)',
|
||||
],
|
||||
thirdCountryTransfers: false,
|
||||
retentionPeriod: '2 Jahre für Chat-Verläufe, danach Anonymisierung für Training',
|
||||
technicalMeasures: [
|
||||
'TLS-Verschlüsselung',
|
||||
'Keine Speicherung sensitiver Daten im Chat',
|
||||
'Automatische PII-Erkennung und Maskierung',
|
||||
],
|
||||
organizationalMeasures: [
|
||||
'Klare KI-Kennzeichnung gegenüber Kunden',
|
||||
'Jederzeit Übergabe an Menschen möglich',
|
||||
'Schulung des Eskalations-Teams',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-pa-4',
|
||||
name: 'Mitarbeiterverwaltung',
|
||||
purpose: 'Verwaltung von Personalstammdaten, Gehaltsabrechnung, Zeiterfassung und Personalentwicklung',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Arbeitsvertrag) / § 26 BDSG (Beschäftigungsverhältnis) / gesetzliche Pflichten (Steuer, SV)',
|
||||
dataCategories: [
|
||||
'Personalstammdaten (Name, Adresse, Geburtsdatum, SV-Nr.)',
|
||||
'Vertragsdaten (Arbeitsvertrag, Gehalt, Arbeitszeit)',
|
||||
'Zeiterfassungsdaten',
|
||||
'Leistungsbeurteilungen',
|
||||
'Bankverbindung',
|
||||
],
|
||||
dataSubjects: [
|
||||
'Aktive Mitarbeiter',
|
||||
'Ehemalige Mitarbeiter (Archiv)',
|
||||
],
|
||||
recipients: [
|
||||
'HR-Abteilung',
|
||||
'Lohnbuchhaltung / Steuerberater',
|
||||
'Sozialversicherungsträger',
|
||||
'Finanzamt',
|
||||
],
|
||||
thirdCountryTransfers: false,
|
||||
retentionPeriod: '10 Jahre nach Ausscheiden (steuerliche Aufbewahrungspflichten)',
|
||||
technicalMeasures: [
|
||||
'Verschlüsselte Speicherung',
|
||||
'Strenge Zugriffskontrolle',
|
||||
'Getrennte Systeme für verschiedene Datenkategorien',
|
||||
],
|
||||
organizationalMeasures: [
|
||||
'Need-to-know-Prinzip',
|
||||
'Dokumentierte Prozesse',
|
||||
'Betriebsvereinbarung zur Datenverarbeitung',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-pa-5',
|
||||
name: 'Website-Analyse und Marketing',
|
||||
purpose: 'Analyse des Nutzerverhaltens auf der Website zur Optimierung der User Experience und für personalisierte Marketing-Maßnahmen',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung via Cookie-Banner)',
|
||||
dataCategories: [
|
||||
'Pseudonymisierte Nutzungsdaten',
|
||||
'Cookie-IDs und Tracking-Identifier',
|
||||
'Geräteinformationen',
|
||||
'Interaktionsdaten (Klicks, Scrollverhalten)',
|
||||
],
|
||||
dataSubjects: [
|
||||
'Website-Besucher (nur mit Einwilligung)',
|
||||
],
|
||||
recipients: [
|
||||
'Marketing-Team',
|
||||
'Analytics-Anbieter (AV-Vertrag)',
|
||||
'Advertising-Partner (nur mit erweiterter Einwilligung)',
|
||||
],
|
||||
thirdCountryTransfers: true,
|
||||
retentionPeriod: '13 Monate für Analytics-Daten, Cookie-Laufzeit max. 12 Monate',
|
||||
technicalMeasures: [
|
||||
'IP-Anonymisierung',
|
||||
'Secure Cookies',
|
||||
'Consent-Management-System',
|
||||
],
|
||||
organizationalMeasures: [
|
||||
'Transparente Cookie-Richtlinie',
|
||||
'Einfacher Widerruf möglich',
|
||||
'Regelmäßige Cookie-Audits',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-pa-6',
|
||||
name: 'Videoüberwachung',
|
||||
purpose: 'Schutz von Eigentum und Personen, Prävention und Aufklärung von Straftaten in Geschäftsräumen',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an Sicherheit)',
|
||||
dataCategories: [
|
||||
'Videoaufnahmen',
|
||||
'Zeitstempel',
|
||||
'Aufnahmeort',
|
||||
],
|
||||
dataSubjects: [
|
||||
'Mitarbeiter in überwachten Bereichen',
|
||||
'Besucher und Kunden',
|
||||
'Lieferanten',
|
||||
],
|
||||
recipients: [
|
||||
'Sicherheitspersonal',
|
||||
'Geschäftsleitung (bei Vorfällen)',
|
||||
'Strafverfolgungsbehörden (auf Anforderung)',
|
||||
],
|
||||
thirdCountryTransfers: false,
|
||||
retentionPeriod: '72 Stunden, bei Vorfällen bis zur Abschluss der Untersuchung',
|
||||
technicalMeasures: [
|
||||
'Verschlüsselte Speicherung',
|
||||
'Automatische Löschung nach Fristablauf',
|
||||
'Eingeschränkter Zugriff',
|
||||
],
|
||||
organizationalMeasures: [
|
||||
'Beschilderung der überwachten Bereiche',
|
||||
'Betriebsvereinbarung mit Betriebsrat',
|
||||
'Dokumentiertes Einsichtsprotokoll',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const DEMO_RETENTION_POLICIES: RetentionPolicy[] = [
|
||||
{
|
||||
id: 'demo-ret-1',
|
||||
dataCategory: 'Kundenstammdaten',
|
||||
description: 'Grundlegende Daten zur Kundenidentifikation (Name, Adresse, Kontaktdaten)',
|
||||
legalBasis: 'Handels- und steuerrechtliche Aufbewahrungspflichten (§ 257 HGB, § 147 AO)',
|
||||
retentionPeriod: '10 Jahre nach Vertragsende',
|
||||
deletionMethod: 'Sichere Löschung mit Protokollierung, bei Papier: Aktenvernichtung DIN 66399',
|
||||
exceptions: [
|
||||
'Laufende Rechtsstreitigkeiten',
|
||||
'Offene Forderungen',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-2',
|
||||
dataCategory: 'Transaktionsdaten',
|
||||
description: 'Bestellungen, Rechnungen, Zahlungen, Lieferungen',
|
||||
legalBasis: '§ 257 HGB, § 147 AO (handels- und steuerrechtliche Aufbewahrung)',
|
||||
retentionPeriod: '10 Jahre ab Ende des Geschäftsjahres',
|
||||
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
|
||||
exceptions: [
|
||||
'Garantiefälle (bis Ende der Garantiezeit)',
|
||||
'Prüfungen durch Finanzbehörden',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-3',
|
||||
dataCategory: 'Bewerberdaten',
|
||||
description: 'Lebenslauf, Anschreiben, Zeugnisse, Korrespondenz',
|
||||
legalBasis: 'AGG (Diskriminierungsschutz) / § 26 BDSG',
|
||||
retentionPeriod: '6 Monate nach Abschluss des Verfahrens',
|
||||
deletionMethod: 'Sichere Löschung, bei Papier: Aktenvernichtung',
|
||||
exceptions: [
|
||||
'Aufnahme in Talentpool (mit Einwilligung): 2 Jahre',
|
||||
'Diskriminierungsklagen: bis Abschluss',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-4',
|
||||
dataCategory: 'Personalakten',
|
||||
description: 'Arbeitsverträge, Gehaltsabrechnungen, Beurteilungen, Abmahnungen',
|
||||
legalBasis: '§ 257 HGB, § 147 AO, Sozialversicherungsrecht',
|
||||
retentionPeriod: '10 Jahre nach Ausscheiden (teilweise 30 Jahre für Rentenansprüche)',
|
||||
deletionMethod: 'Sichere Löschung mit Dokumentation',
|
||||
exceptions: [
|
||||
'Arbeitsrechtliche Streitigkeiten',
|
||||
'Rentenversicherungsnachweise (lebenslang empfohlen)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-5',
|
||||
dataCategory: 'Marketing-Profile',
|
||||
description: 'Analysedaten, Segmentierungen, Präferenzen, Kaufhistorie',
|
||||
legalBasis: 'Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)',
|
||||
retentionPeriod: '3 Jahre nach letzter Aktivität, dann Anonymisierung',
|
||||
deletionMethod: 'Pseudonymisierung → Anonymisierung → Löschung',
|
||||
exceptions: [
|
||||
'Widerruf der Einwilligung (sofortige Löschung)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-6',
|
||||
dataCategory: 'Videoaufnahmen',
|
||||
description: 'Aufnahmen der Sicherheitskameras',
|
||||
legalBasis: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO)',
|
||||
retentionPeriod: '72 Stunden',
|
||||
deletionMethod: 'Automatisches Überschreiben',
|
||||
exceptions: [
|
||||
'Sicherheitsvorfälle (bis Abschluss der Untersuchung)',
|
||||
'Anforderung durch Strafverfolgungsbehörden',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-7',
|
||||
dataCategory: 'KI-Trainingsdaten',
|
||||
description: 'Anonymisierte Datensätze für Modell-Training',
|
||||
legalBasis: 'Berechtigtes Interesse / ursprüngliche Zweckbindung (bei Kompatibilität)',
|
||||
retentionPeriod: 'Solange Modell aktiv, danach Löschung mit Modell-Archivierung',
|
||||
deletionMethod: 'Sichere Löschung bei Modell-Retirement',
|
||||
exceptions: [
|
||||
'Audit-Trail für Modell-Herkunft (anonymisierte Metadaten)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'demo-ret-8',
|
||||
dataCategory: 'Audit-Logs',
|
||||
description: 'Protokolle von Datenzugriffen und Systemereignissen',
|
||||
legalBasis: 'Nachweispflichten DSGVO, Compliance-Anforderungen',
|
||||
retentionPeriod: '10 Jahre',
|
||||
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
|
||||
exceptions: [
|
||||
'Laufende Untersuchungen oder Audits',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function getDemoProcessingActivities(): ProcessingActivity[] {
|
||||
return DEMO_PROCESSING_ACTIVITIES
|
||||
}
|
||||
|
||||
export function getDemoRetentionPolicies(): RetentionPolicy[] {
|
||||
return DEMO_RETENTION_POLICIES
|
||||
}
|
||||
548
admin-v2/lib/sdk/document-generator/datapoint-helpers.ts
Normal file
548
admin-v2/lib/sdk/document-generator/datapoint-helpers.ts
Normal file
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
|
||||
* in den Dokumentengenerator.
|
||||
*
|
||||
* Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf
|
||||
* den vom Benutzer ausgewählten Datenpunkten.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
RetentionPeriod,
|
||||
RiskLevel,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
RISK_LEVEL_STYLING,
|
||||
LocalizedText,
|
||||
SupportedLanguage
|
||||
} from '@/lib/sdk/einwilligungen/types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Sprach-Option für alle Helper-Funktionen
|
||||
*/
|
||||
export type Language = SupportedLanguage
|
||||
|
||||
/**
|
||||
* Generierte Platzhalter-Map für den Dokumentengenerator
|
||||
*/
|
||||
export interface DataPointPlaceholders {
|
||||
'[DATENPUNKTE_COUNT]': string
|
||||
'[DATENPUNKTE_LIST]': string
|
||||
'[DATENPUNKTE_TABLE]': string
|
||||
'[VERARBEITUNGSZWECKE]': string
|
||||
'[RECHTSGRUNDLAGEN]': string
|
||||
'[SPEICHERFRISTEN]': string
|
||||
'[EMPFAENGER]': string
|
||||
'[BESONDERE_KATEGORIEN]': string
|
||||
'[DRITTLAND_TRANSFERS]': string
|
||||
'[RISIKO_ZUSAMMENFASSUNG]': string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Extrahiert Text aus LocalizedText basierend auf Sprache
|
||||
*/
|
||||
function getText(text: LocalizedText, lang: Language): string {
|
||||
return text[lang] || text.de
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine Markdown-Tabelle der Datenpunkte
|
||||
*
|
||||
* @param dataPoints - Liste der ausgewählten Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Markdown-Tabelle als String
|
||||
*/
|
||||
export function generateDataPointsTable(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
if (dataPoints.length === 0) {
|
||||
return lang === 'de'
|
||||
? '*Keine Datenpunkte ausgewählt.*'
|
||||
: '*No data points selected.*'
|
||||
}
|
||||
|
||||
const header = lang === 'de'
|
||||
? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
|
||||
: '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
|
||||
const separator = '|------------|-----------|-------|-----------------|---------------|'
|
||||
|
||||
const rows = dataPoints.map(dp => {
|
||||
const category = CATEGORY_METADATA[dp.category]
|
||||
const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
|
||||
const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
|
||||
|
||||
const name = getText(dp.name, lang)
|
||||
const categoryName = getText(category.name, lang)
|
||||
const purpose = getText(dp.purpose, lang)
|
||||
const legalBasisName = getText(legalBasis.name, lang)
|
||||
const retentionLabel = getText(retention.label, lang)
|
||||
|
||||
// Truncate long texts for table readability
|
||||
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
|
||||
|
||||
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
|
||||
}).join('\n')
|
||||
|
||||
return `${header}\n${separator}\n${rows}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Speicherfrist
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @returns Record mit Speicherfrist als Key und Datenpunkten als Value
|
||||
*/
|
||||
export function groupByRetention(
|
||||
dataPoints: DataPoint[]
|
||||
): Record<RetentionPeriod, DataPoint[]> {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
const key = dp.retentionPeriod
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(dp)
|
||||
return acc
|
||||
}, {} as Record<RetentionPeriod, DataPoint[]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Kategorie
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @returns Record mit Kategorie als Key und Datenpunkten als Value
|
||||
*/
|
||||
export function groupByCategory(
|
||||
dataPoints: DataPoint[]
|
||||
): Record<DataPointCategory, DataPoint[]> {
|
||||
return dataPoints.reduce((acc, dp) => {
|
||||
const key = dp.category
|
||||
if (!acc[key]) {
|
||||
acc[key] = []
|
||||
}
|
||||
acc[key].push(dp)
|
||||
return acc
|
||||
}, {} as Record<DataPointCategory, DataPoint[]>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO)
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten)
|
||||
*/
|
||||
export function generateSpecialCategorySection(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const special = dataPoints.filter(dp => dp.isSpecialCategory)
|
||||
|
||||
if (special.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (lang === 'de') {
|
||||
const items = special.map(dp =>
|
||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
||||
).join('\n')
|
||||
|
||||
return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
|
||||
|
||||
Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
|
||||
|
||||
${items}
|
||||
|
||||
Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.`
|
||||
} else {
|
||||
const items = special.map(dp =>
|
||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
||||
).join('\n')
|
||||
|
||||
return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
|
||||
|
||||
We process the following special categories of personal data:
|
||||
|
||||
${items}
|
||||
|
||||
Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller eindeutigen Verarbeitungszwecke
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Kommaseparierte Liste der Zwecke
|
||||
*/
|
||||
export function generatePurposesList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const purposes = new Set<string>()
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
purposes.add(getText(dp.purpose, lang))
|
||||
})
|
||||
|
||||
return [...purposes].join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller verwendeten Rechtsgrundlagen
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Formatierte Liste der Rechtsgrundlagen
|
||||
*/
|
||||
export function generateLegalBasisList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const bases = new Set<LegalBasis>()
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
bases.add(dp.legalBasis)
|
||||
})
|
||||
|
||||
return [...bases].map(basis => {
|
||||
const info = LEGAL_BASIS_INFO[basis]
|
||||
return `${info.article} (${getText(info.name, lang)})`
|
||||
}).join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller Speicherfristen gruppiert
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien
|
||||
*/
|
||||
export function generateRetentionList(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const grouped = groupByRetention(dataPoints)
|
||||
const entries: string[] = []
|
||||
|
||||
for (const [period, points] of Object.entries(grouped)) {
|
||||
const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
|
||||
const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
|
||||
|
||||
entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
|
||||
}
|
||||
|
||||
return entries.join('; ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Liste aller Empfänger/Drittparteien
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @returns Kommaseparierte Liste der Empfänger
|
||||
*/
|
||||
export function generateRecipientsList(dataPoints: DataPoint[]): string {
|
||||
const recipients = new Set<string>()
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
|
||||
})
|
||||
|
||||
if (recipients.size === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return [...recipients].join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Abschnitt für Drittland-Übermittlungen
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Markdown-Abschnitt als String
|
||||
*/
|
||||
export function generateThirdCountrySection(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
// Note: We assume dataPoints have been filtered for thirdCountryTransfer
|
||||
// The actual flag would need to be added to the DataPoint interface
|
||||
// For now, we check if any thirdPartyRecipients suggest third country
|
||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
|
||||
|
||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
||||
dp.thirdPartyRecipients?.some(r =>
|
||||
thirdCountryIndicators.some(indicator =>
|
||||
r.toLowerCase().includes(indicator.toLowerCase())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (thirdCountryPoints.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const recipients = new Set<string>()
|
||||
thirdCountryPoints.forEach(dp => {
|
||||
dp.thirdPartyRecipients?.forEach(r => {
|
||||
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
|
||||
recipients.add(r)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (lang === 'de') {
|
||||
return `## Übermittlung in Drittländer
|
||||
|
||||
Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR):
|
||||
|
||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
||||
|
||||
Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
|
||||
} else {
|
||||
return `## Transfers to Third Countries
|
||||
|
||||
We transfer personal data to the following recipients in third countries (outside the EU/EEA):
|
||||
|
||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
||||
|
||||
The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Risiko-Zusammenfassung
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Formatierte Risiko-Zusammenfassung
|
||||
*/
|
||||
export function generateRiskSummary(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): string {
|
||||
const riskCounts: Record<RiskLevel, number> = {
|
||||
LOW: 0,
|
||||
MEDIUM: 0,
|
||||
HIGH: 0
|
||||
}
|
||||
|
||||
dataPoints.forEach(dp => {
|
||||
riskCounts[dp.riskLevel]++
|
||||
})
|
||||
|
||||
const parts = Object.entries(riskCounts)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([level, count]) => {
|
||||
const styling = RISK_LEVEL_STYLING[level as RiskLevel]
|
||||
return `${count} ${getText(styling.label, lang).toLowerCase()}`
|
||||
})
|
||||
|
||||
return parts.join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert alle Platzhalter für den Dokumentengenerator
|
||||
*
|
||||
* @param dataPoints - Liste der ausgewählten Datenpunkte
|
||||
* @param lang - Sprache für die Ausgabe
|
||||
* @returns Objekt mit allen Platzhaltern
|
||||
*/
|
||||
export function generateAllPlaceholders(
|
||||
dataPoints: DataPoint[],
|
||||
lang: Language = 'de'
|
||||
): DataPointPlaceholders {
|
||||
return {
|
||||
'[DATENPUNKTE_COUNT]': String(dataPoints.length),
|
||||
'[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
|
||||
'[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
|
||||
'[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
|
||||
'[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
|
||||
'[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
|
||||
'[EMPFAENGER]': generateRecipientsList(dataPoints),
|
||||
'[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
|
||||
'[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
|
||||
'[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VALIDATION HELPERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Validierungswarnung für den Dokumentengenerator
|
||||
*/
|
||||
export interface ValidationWarning {
|
||||
type: 'error' | 'warning' | 'info'
|
||||
code: string
|
||||
message: string
|
||||
suggestion: string
|
||||
affectedDataPoints?: DataPoint[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns ValidationWarning oder null
|
||||
*/
|
||||
export function checkSpecialCategoriesWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
|
||||
|
||||
if (specialCategories.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSection = lang === 'de'
|
||||
? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
|
||||
: documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
|
||||
|
||||
if (!hasSection) {
|
||||
return {
|
||||
type: 'error',
|
||||
code: 'MISSING_ART9_SECTION',
|
||||
message: lang === 'de'
|
||||
? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.`
|
||||
: `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
|
||||
: 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
|
||||
affectedDataPoints: specialCategories
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns ValidationWarning oder null
|
||||
*/
|
||||
export function checkThirdCountryWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
|
||||
|
||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
||||
dp.thirdPartyRecipients?.some(r =>
|
||||
thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
|
||||
)
|
||||
)
|
||||
|
||||
if (thirdCountryPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasSCCMention = lang === 'de'
|
||||
? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
||||
: documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
||||
|
||||
if (!hasSCCMention) {
|
||||
return {
|
||||
type: 'warning',
|
||||
code: 'MISSING_SCC_SECTION',
|
||||
message: lang === 'de'
|
||||
? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.`
|
||||
: `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
|
||||
: 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
|
||||
affectedDataPoints: thirdCountryPoints
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns ValidationWarning oder null
|
||||
*/
|
||||
export function checkExplicitConsentWarning(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning | null {
|
||||
const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
|
||||
|
||||
if (explicitConsentPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasConsentSection = lang === 'de'
|
||||
? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
|
||||
: documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
|
||||
|
||||
if (!hasConsentSection) {
|
||||
return {
|
||||
type: 'warning',
|
||||
code: 'MISSING_CONSENT_SECTION',
|
||||
message: lang === 'de'
|
||||
? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
|
||||
: `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
|
||||
suggestion: lang === 'de'
|
||||
? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
|
||||
: 'Add a section about the right to withdraw consent.',
|
||||
affectedDataPoints: explicitConsentPoints
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt alle Validierungsprüfungen durch
|
||||
*
|
||||
* @param dataPoints - Liste der Datenpunkte
|
||||
* @param documentContent - Der generierte Dokumentinhalt
|
||||
* @param lang - Sprache
|
||||
* @returns Array aller Warnungen
|
||||
*/
|
||||
export function validateDocument(
|
||||
dataPoints: DataPoint[],
|
||||
documentContent: string,
|
||||
lang: Language = 'de'
|
||||
): ValidationWarning[] {
|
||||
const warnings: ValidationWarning[] = []
|
||||
|
||||
const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
|
||||
if (specialCatWarning) warnings.push(specialCatWarning)
|
||||
|
||||
const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
|
||||
if (thirdCountryWarning) warnings.push(thirdCountryWarning)
|
||||
|
||||
const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
|
||||
if (consentWarning) warnings.push(consentWarning)
|
||||
|
||||
return warnings
|
||||
}
|
||||
8
admin-v2/lib/sdk/document-generator/index.ts
Normal file
8
admin-v2/lib/sdk/document-generator/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Document Generator Library
|
||||
*
|
||||
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
|
||||
* in den Dokumentengenerator.
|
||||
*/
|
||||
|
||||
export * from './datapoint-helpers'
|
||||
@@ -1,355 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn()
|
||||
global.fetch = mockFetch
|
||||
|
||||
describe('DSFA API Client', () => {
|
||||
beforeEach(() => {
|
||||
mockFetch.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('listDSFAs', () => {
|
||||
it('should fetch DSFAs without status filter', async () => {
|
||||
const mockDSFAs = [
|
||||
{ id: 'dsfa-1', name: 'Test DSFA 1', status: 'draft' },
|
||||
{ id: 'dsfa-2', name: 'Test DSFA 2', status: 'approved' },
|
||||
]
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ dsfas: mockDSFAs }),
|
||||
})
|
||||
|
||||
const { listDSFAs } = await import('../api')
|
||||
const result = await listDSFAs()
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].name).toBe('Test DSFA 1')
|
||||
})
|
||||
|
||||
it('should fetch DSFAs with status filter', async () => {
|
||||
const mockDSFAs = [{ id: 'dsfa-1', name: 'Draft DSFA', status: 'draft' }]
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ dsfas: mockDSFAs }),
|
||||
})
|
||||
|
||||
const { listDSFAs } = await import('../api')
|
||||
const result = await listDSFAs('draft')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
const calledUrl = mockFetch.mock.calls[0][0]
|
||||
expect(calledUrl).toContain('status=draft')
|
||||
})
|
||||
|
||||
it('should return empty array when no DSFAs', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ dsfas: null }),
|
||||
})
|
||||
|
||||
const { listDSFAs } = await import('../api')
|
||||
const result = await listDSFAs()
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDSFA', () => {
|
||||
it('should fetch a single DSFA by ID', async () => {
|
||||
const mockDSFA = {
|
||||
id: 'dsfa-123',
|
||||
name: 'Test DSFA',
|
||||
status: 'draft',
|
||||
risks: [],
|
||||
mitigations: [],
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockDSFA),
|
||||
})
|
||||
|
||||
const { getDSFA } = await import('../api')
|
||||
const result = await getDSFA('dsfa-123')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.id).toBe('dsfa-123')
|
||||
expect(result.name).toBe('Test DSFA')
|
||||
})
|
||||
|
||||
it('should throw error for non-existent DSFA', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: () => Promise.resolve('{"error": "DSFA not found"}'),
|
||||
})
|
||||
|
||||
const { getDSFA } = await import('../api')
|
||||
|
||||
await expect(getDSFA('non-existent')).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createDSFA', () => {
|
||||
it('should create a new DSFA', async () => {
|
||||
const newDSFA = {
|
||||
name: 'New DSFA',
|
||||
description: 'Test description',
|
||||
processing_purpose: 'Testing',
|
||||
}
|
||||
|
||||
const createdDSFA = {
|
||||
id: 'dsfa-new',
|
||||
...newDSFA,
|
||||
status: 'draft',
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createdDSFA),
|
||||
})
|
||||
|
||||
const { createDSFA } = await import('../api')
|
||||
const result = await createDSFA(newDSFA)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.id).toBe('dsfa-new')
|
||||
expect(result.name).toBe('New DSFA')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateDSFA', () => {
|
||||
it('should update an existing DSFA', async () => {
|
||||
const updates = {
|
||||
name: 'Updated DSFA Name',
|
||||
processing_purpose: 'Updated purpose',
|
||||
}
|
||||
|
||||
const updatedDSFA = {
|
||||
id: 'dsfa-123',
|
||||
...updates,
|
||||
status: 'draft',
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(updatedDSFA),
|
||||
})
|
||||
|
||||
const { updateDSFA } = await import('../api')
|
||||
const result = await updateDSFA('dsfa-123', updates)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.name).toBe('Updated DSFA Name')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteDSFA', () => {
|
||||
it('should delete a DSFA', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
})
|
||||
|
||||
const { deleteDSFA } = await import('../api')
|
||||
await deleteDSFA('dsfa-123')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
const calledConfig = mockFetch.mock.calls[0][1]
|
||||
expect(calledConfig.method).toBe('DELETE')
|
||||
})
|
||||
|
||||
it('should throw error when deletion fails', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
statusText: 'Not Found',
|
||||
})
|
||||
|
||||
const { deleteDSFA } = await import('../api')
|
||||
|
||||
await expect(deleteDSFA('non-existent')).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateDSFASection', () => {
|
||||
it('should update a specific section', async () => {
|
||||
const sectionData = {
|
||||
processing_purpose: 'Updated purpose',
|
||||
data_categories: ['personal_data', 'contact_data'],
|
||||
}
|
||||
|
||||
const updatedDSFA = {
|
||||
id: 'dsfa-123',
|
||||
section_progress: {
|
||||
section_1_complete: true,
|
||||
},
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(updatedDSFA),
|
||||
})
|
||||
|
||||
const { updateDSFASection } = await import('../api')
|
||||
const result = await updateDSFASection('dsfa-123', 1, sectionData)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
const calledUrl = mockFetch.mock.calls[0][0]
|
||||
expect(calledUrl).toContain('/sections/1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('submitDSFAForReview', () => {
|
||||
it('should submit DSFA for review', async () => {
|
||||
const response = {
|
||||
message: 'DSFA submitted for review',
|
||||
dsfa: {
|
||||
id: 'dsfa-123',
|
||||
status: 'in_review',
|
||||
},
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(response),
|
||||
})
|
||||
|
||||
const { submitDSFAForReview } = await import('../api')
|
||||
const result = await submitDSFAForReview('dsfa-123')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.dsfa.status).toBe('in_review')
|
||||
})
|
||||
})
|
||||
|
||||
describe('approveDSFA', () => {
|
||||
it('should approve a DSFA', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ message: 'DSFA approved' }),
|
||||
})
|
||||
|
||||
const { approveDSFA } = await import('../api')
|
||||
const result = await approveDSFA('dsfa-123', {
|
||||
dpo_opinion: 'Approved after review',
|
||||
approved: true,
|
||||
})
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.message).toBe('DSFA approved')
|
||||
})
|
||||
|
||||
it('should reject a DSFA', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ message: 'DSFA rejected' }),
|
||||
})
|
||||
|
||||
const { approveDSFA } = await import('../api')
|
||||
const result = await approveDSFA('dsfa-123', {
|
||||
dpo_opinion: 'Needs more details',
|
||||
approved: false,
|
||||
})
|
||||
|
||||
expect(result.message).toBe('DSFA rejected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDSFAStats', () => {
|
||||
it('should fetch DSFA statistics', async () => {
|
||||
const stats = {
|
||||
total: 10,
|
||||
status_stats: {
|
||||
draft: 4,
|
||||
in_review: 2,
|
||||
approved: 3,
|
||||
rejected: 1,
|
||||
},
|
||||
risk_stats: {
|
||||
low: 3,
|
||||
medium: 4,
|
||||
high: 2,
|
||||
very_high: 1,
|
||||
},
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(stats),
|
||||
})
|
||||
|
||||
const { getDSFAStats } = await import('../api')
|
||||
const result = await getDSFAStats()
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.total).toBe(10)
|
||||
expect(result.status_stats.approved).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createDSFAFromAssessment', () => {
|
||||
it('should create DSFA from UCCA assessment', async () => {
|
||||
const response = {
|
||||
dsfa: {
|
||||
id: 'dsfa-new',
|
||||
name: 'AI Chatbot DSFA',
|
||||
status: 'draft',
|
||||
},
|
||||
prefilled: true,
|
||||
message: 'DSFA created from assessment',
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(response),
|
||||
})
|
||||
|
||||
const { createDSFAFromAssessment } = await import('../api')
|
||||
const result = await createDSFAFromAssessment('assessment-123')
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1)
|
||||
expect(result.prefilled).toBe(true)
|
||||
expect(result.dsfa.id).toBe('dsfa-new')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getDSFAByAssessment', () => {
|
||||
it('should return DSFA linked to assessment', async () => {
|
||||
const dsfa = {
|
||||
id: 'dsfa-123',
|
||||
assessment_id: 'assessment-123',
|
||||
name: 'Linked DSFA',
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(dsfa),
|
||||
})
|
||||
|
||||
const { getDSFAByAssessment } = await import('../api')
|
||||
const result = await getDSFAByAssessment('assessment-123')
|
||||
|
||||
expect(result?.id).toBe('dsfa-123')
|
||||
})
|
||||
|
||||
it('should return null when no DSFA exists for assessment', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: () => Promise.resolve('Not found'),
|
||||
})
|
||||
|
||||
const { getDSFAByAssessment } = await import('../api')
|
||||
const result = await getDSFAByAssessment('no-dsfa-assessment')
|
||||
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,255 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
calculateRiskLevel,
|
||||
DSFA_SECTIONS,
|
||||
DSFA_STATUS_LABELS,
|
||||
DSFA_RISK_LEVEL_LABELS,
|
||||
DSFA_LEGAL_BASES,
|
||||
DSFA_AFFECTED_RIGHTS,
|
||||
RISK_MATRIX,
|
||||
type DSFARisk,
|
||||
type DSFAMitigation,
|
||||
type DSFASectionProgress,
|
||||
type DSFA,
|
||||
} from '../types'
|
||||
|
||||
describe('DSFA_SECTIONS', () => {
|
||||
it('should have 5 sections defined', () => {
|
||||
expect(DSFA_SECTIONS.length).toBe(5)
|
||||
})
|
||||
|
||||
it('should have sections numbered 1-5', () => {
|
||||
const numbers = DSFA_SECTIONS.map(s => s.number)
|
||||
expect(numbers).toEqual([1, 2, 3, 4, 5])
|
||||
})
|
||||
|
||||
it('should have GDPR references for all sections', () => {
|
||||
DSFA_SECTIONS.forEach(section => {
|
||||
expect(section.gdprRef).toBeDefined()
|
||||
expect(section.gdprRef).toContain('Art. 35')
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark first 4 sections as required', () => {
|
||||
const requiredSections = DSFA_SECTIONS.filter(s => s.required)
|
||||
expect(requiredSections.length).toBe(4)
|
||||
expect(requiredSections.map(s => s.number)).toEqual([1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('should mark section 5 as optional', () => {
|
||||
const section5 = DSFA_SECTIONS.find(s => s.number === 5)
|
||||
expect(section5?.required).toBe(false)
|
||||
})
|
||||
|
||||
it('should have German titles for all sections', () => {
|
||||
DSFA_SECTIONS.forEach(section => {
|
||||
expect(section.titleDE).toBeDefined()
|
||||
expect(section.titleDE.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFA_STATUS_LABELS', () => {
|
||||
it('should have all status labels defined', () => {
|
||||
expect(DSFA_STATUS_LABELS.draft).toBe('Entwurf')
|
||||
expect(DSFA_STATUS_LABELS.in_review).toBe('In Prüfung')
|
||||
expect(DSFA_STATUS_LABELS.approved).toBe('Genehmigt')
|
||||
expect(DSFA_STATUS_LABELS.rejected).toBe('Abgelehnt')
|
||||
expect(DSFA_STATUS_LABELS.needs_update).toBe('Überarbeitung erforderlich')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFA_RISK_LEVEL_LABELS', () => {
|
||||
it('should have all risk level labels defined', () => {
|
||||
expect(DSFA_RISK_LEVEL_LABELS.low).toBe('Niedrig')
|
||||
expect(DSFA_RISK_LEVEL_LABELS.medium).toBe('Mittel')
|
||||
expect(DSFA_RISK_LEVEL_LABELS.high).toBe('Hoch')
|
||||
expect(DSFA_RISK_LEVEL_LABELS.very_high).toBe('Sehr Hoch')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFA_LEGAL_BASES', () => {
|
||||
it('should have 6 legal bases defined', () => {
|
||||
expect(Object.keys(DSFA_LEGAL_BASES).length).toBe(6)
|
||||
})
|
||||
|
||||
it('should reference GDPR Article 6', () => {
|
||||
Object.values(DSFA_LEGAL_BASES).forEach(label => {
|
||||
expect(label).toContain('Art. 6')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFA_AFFECTED_RIGHTS', () => {
|
||||
it('should have multiple affected rights defined', () => {
|
||||
expect(DSFA_AFFECTED_RIGHTS.length).toBeGreaterThan(5)
|
||||
})
|
||||
|
||||
it('should have id and label for each right', () => {
|
||||
DSFA_AFFECTED_RIGHTS.forEach(right => {
|
||||
expect(right.id).toBeDefined()
|
||||
expect(right.label).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should include GDPR data subject rights', () => {
|
||||
const ids = DSFA_AFFECTED_RIGHTS.map(r => r.id)
|
||||
expect(ids).toContain('right_of_access')
|
||||
expect(ids).toContain('right_to_erasure')
|
||||
expect(ids).toContain('right_to_data_portability')
|
||||
})
|
||||
})
|
||||
|
||||
describe('RISK_MATRIX', () => {
|
||||
it('should have 9 cells defined (3x3 matrix)', () => {
|
||||
expect(RISK_MATRIX.length).toBe(9)
|
||||
})
|
||||
|
||||
it('should cover all combinations of likelihood and impact', () => {
|
||||
const likelihoodValues = ['low', 'medium', 'high']
|
||||
const impactValues = ['low', 'medium', 'high']
|
||||
|
||||
likelihoodValues.forEach(likelihood => {
|
||||
impactValues.forEach(impact => {
|
||||
const cell = RISK_MATRIX.find(
|
||||
c => c.likelihood === likelihood && c.impact === impact
|
||||
)
|
||||
expect(cell).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should have increasing scores for higher risks', () => {
|
||||
const lowLow = RISK_MATRIX.find(c => c.likelihood === 'low' && c.impact === 'low')
|
||||
const highHigh = RISK_MATRIX.find(c => c.likelihood === 'high' && c.impact === 'high')
|
||||
|
||||
expect(lowLow?.score).toBeLessThan(highHigh?.score || 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('calculateRiskLevel', () => {
|
||||
it('should return low for low likelihood and low impact', () => {
|
||||
const result = calculateRiskLevel('low', 'low')
|
||||
expect(result.level).toBe('low')
|
||||
expect(result.score).toBe(10)
|
||||
})
|
||||
|
||||
it('should return very_high for high likelihood and high impact', () => {
|
||||
const result = calculateRiskLevel('high', 'high')
|
||||
expect(result.level).toBe('very_high')
|
||||
expect(result.score).toBe(90)
|
||||
})
|
||||
|
||||
it('should return medium for medium likelihood and medium impact', () => {
|
||||
const result = calculateRiskLevel('medium', 'medium')
|
||||
expect(result.level).toBe('medium')
|
||||
expect(result.score).toBe(50)
|
||||
})
|
||||
|
||||
it('should return high for high likelihood and medium impact', () => {
|
||||
const result = calculateRiskLevel('high', 'medium')
|
||||
expect(result.level).toBe('high')
|
||||
expect(result.score).toBe(70)
|
||||
})
|
||||
|
||||
it('should return medium for low likelihood and high impact', () => {
|
||||
const result = calculateRiskLevel('low', 'high')
|
||||
expect(result.level).toBe('medium')
|
||||
expect(result.score).toBe(40)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFARisk type', () => {
|
||||
it('should accept valid risk data', () => {
|
||||
const risk: DSFARisk = {
|
||||
id: 'risk-001',
|
||||
category: 'confidentiality',
|
||||
description: 'Unauthorized access to personal data',
|
||||
likelihood: 'medium',
|
||||
impact: 'high',
|
||||
risk_level: 'high',
|
||||
affected_data: ['customer_data', 'financial_data'],
|
||||
}
|
||||
|
||||
expect(risk.id).toBe('risk-001')
|
||||
expect(risk.category).toBe('confidentiality')
|
||||
expect(risk.likelihood).toBe('medium')
|
||||
expect(risk.impact).toBe('high')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFAMitigation type', () => {
|
||||
it('should accept valid mitigation data', () => {
|
||||
const mitigation: DSFAMitigation = {
|
||||
id: 'mit-001',
|
||||
risk_id: 'risk-001',
|
||||
description: 'Implement encryption at rest',
|
||||
type: 'technical',
|
||||
status: 'implemented',
|
||||
residual_risk: 'low',
|
||||
responsible_party: 'IT Security Team',
|
||||
}
|
||||
|
||||
expect(mitigation.id).toBe('mit-001')
|
||||
expect(mitigation.type).toBe('technical')
|
||||
expect(mitigation.status).toBe('implemented')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFASectionProgress type', () => {
|
||||
it('should track completion for all 5 sections', () => {
|
||||
const progress: DSFASectionProgress = {
|
||||
section_1_complete: true,
|
||||
section_2_complete: true,
|
||||
section_3_complete: false,
|
||||
section_4_complete: false,
|
||||
section_5_complete: false,
|
||||
}
|
||||
|
||||
expect(progress.section_1_complete).toBe(true)
|
||||
expect(progress.section_2_complete).toBe(true)
|
||||
expect(progress.section_3_complete).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DSFA type', () => {
|
||||
it('should accept a complete DSFA object', () => {
|
||||
const dsfa: DSFA = {
|
||||
id: 'dsfa-001',
|
||||
tenant_id: 'tenant-001',
|
||||
name: 'AI Chatbot DSFA',
|
||||
description: 'Data Protection Impact Assessment for AI Chatbot',
|
||||
processing_description: 'Automated customer service using AI',
|
||||
processing_purpose: 'Customer support automation',
|
||||
data_categories: ['contact_data', 'inquiry_content'],
|
||||
data_subjects: ['customers'],
|
||||
recipients: ['internal_staff'],
|
||||
legal_basis: 'legitimate_interest',
|
||||
necessity_assessment: 'Required for efficient customer service',
|
||||
proportionality_assessment: 'Minimal data processing for the purpose',
|
||||
risks: [],
|
||||
overall_risk_level: 'medium',
|
||||
risk_score: 50,
|
||||
mitigations: [],
|
||||
dpo_consulted: false,
|
||||
authority_consulted: false,
|
||||
status: 'draft',
|
||||
section_progress: {
|
||||
section_1_complete: true,
|
||||
section_2_complete: true,
|
||||
section_3_complete: false,
|
||||
section_4_complete: false,
|
||||
section_5_complete: false,
|
||||
},
|
||||
conclusion: '',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_by: 'user-001',
|
||||
}
|
||||
|
||||
expect(dsfa.id).toBe('dsfa-001')
|
||||
expect(dsfa.name).toBe('AI Chatbot DSFA')
|
||||
expect(dsfa.status).toBe('draft')
|
||||
expect(dsfa.data_categories).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -1,399 +0,0 @@
|
||||
/**
|
||||
* DSFA API Client
|
||||
*
|
||||
* API client functions for DSFA (Data Protection Impact Assessment) endpoints.
|
||||
*/
|
||||
|
||||
import type {
|
||||
DSFA,
|
||||
DSFAListResponse,
|
||||
DSFAStatsResponse,
|
||||
CreateDSFARequest,
|
||||
CreateDSFAFromAssessmentRequest,
|
||||
CreateDSFAFromAssessmentResponse,
|
||||
UpdateDSFASectionRequest,
|
||||
SubmitForReviewResponse,
|
||||
ApproveDSFARequest,
|
||||
DSFATriggerInfo,
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Browser environment
|
||||
return process.env.NEXT_PUBLIC_SDK_API_URL || '/api/sdk/v1'
|
||||
}
|
||||
// Server environment
|
||||
return process.env.SDK_API_URL || 'http://localhost:8080/api/sdk/v1'
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep HTTP status message
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
function getHeaders(): HeadersInit {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSFA CRUD OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* List all DSFAs for the current tenant
|
||||
*/
|
||||
export async function listDSFAs(status?: string): Promise<DSFA[]> {
|
||||
const url = new URL(`${getBaseUrl()}/dsgvo/dsfas`, window.location.origin)
|
||||
if (status) {
|
||||
url.searchParams.set('status', status)
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const data = await handleResponse<DSFAListResponse>(response)
|
||||
return data.dsfas || []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single DSFA by ID
|
||||
*/
|
||||
export async function getDSFA(id: string): Promise<DSFA> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
return handleResponse<DSFA>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DSFA
|
||||
*/
|
||||
export async function createDSFA(data: CreateDSFARequest): Promise<DSFA> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
return handleResponse<DSFA>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing DSFA
|
||||
*/
|
||||
export async function updateDSFA(id: string, data: Partial<DSFA>): Promise<DSFA> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
return handleResponse<DSFA>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a DSFA
|
||||
*/
|
||||
export async function deleteDSFA(id: string): Promise<void> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete DSFA: ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSFA SECTION OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Update a specific section of a DSFA
|
||||
*/
|
||||
export async function updateDSFASection(
|
||||
id: string,
|
||||
sectionNumber: number,
|
||||
data: UpdateDSFASectionRequest
|
||||
): Promise<DSFA> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/sections/${sectionNumber}`, {
|
||||
method: 'PUT',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
return handleResponse<DSFA>(response)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSFA WORKFLOW OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Submit a DSFA for DPO review
|
||||
*/
|
||||
export async function submitDSFAForReview(id: string): Promise<SubmitForReviewResponse> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/submit-for-review`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
return handleResponse<SubmitForReviewResponse>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve or reject a DSFA (DPO/CISO/GF action)
|
||||
*/
|
||||
export async function approveDSFA(id: string, data: ApproveDSFARequest): Promise<{ message: string }> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/approve`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
return handleResponse<{ message: string }>(response)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSFA STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get DSFA statistics for the dashboard
|
||||
*/
|
||||
export async function getDSFAStats(): Promise<DSFAStatsResponse> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/stats`, {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
return handleResponse<DSFAStatsResponse>(response)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UCCA INTEGRATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Create a DSFA from a UCCA assessment (pre-filled)
|
||||
*/
|
||||
export async function createDSFAFromAssessment(
|
||||
assessmentId: string,
|
||||
data?: CreateDSFAFromAssessmentRequest
|
||||
): Promise<CreateDSFAFromAssessmentResponse> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/from-assessment/${assessmentId}`, {
|
||||
method: 'POST',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data || {}),
|
||||
})
|
||||
|
||||
return handleResponse<CreateDSFAFromAssessmentResponse>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a DSFA by its linked UCCA assessment ID
|
||||
*/
|
||||
export async function getDSFAByAssessment(assessmentId: string): Promise<DSFA | null> {
|
||||
try {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/by-assessment/${assessmentId}`, {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (response.status === 404) {
|
||||
return null
|
||||
}
|
||||
|
||||
return handleResponse<DSFA>(response)
|
||||
} catch (error) {
|
||||
// Return null if DSFA not found
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a DSFA is required for a UCCA assessment
|
||||
*/
|
||||
export async function checkDSFARequired(assessmentId: string): Promise<DSFATriggerInfo> {
|
||||
const response = await fetch(`${getBaseUrl()}/ucca/assessments/${assessmentId}/dsfa-required`, {
|
||||
method: 'GET',
|
||||
headers: getHeaders(),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
return handleResponse<DSFATriggerInfo>(response)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Export a DSFA as JSON
|
||||
*/
|
||||
export async function exportDSFAAsJSON(id: string): Promise<Blob> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/export?format=json`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Export failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a DSFA as PDF
|
||||
*/
|
||||
export async function exportDSFAAsPDF(id: string): Promise<Blob> {
|
||||
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/export/pdf`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/pdf',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`PDF export failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RISK & MITIGATION OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Add a risk to a DSFA
|
||||
*/
|
||||
export async function addDSFARisk(dsfaId: string, risk: {
|
||||
category: string
|
||||
description: string
|
||||
likelihood: 'low' | 'medium' | 'high'
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
affected_data?: string[]
|
||||
}): Promise<DSFA> {
|
||||
const dsfa = await getDSFA(dsfaId)
|
||||
const newRisk = {
|
||||
id: crypto.randomUUID(),
|
||||
...risk,
|
||||
risk_level: calculateRiskLevelString(risk.likelihood, risk.impact),
|
||||
affected_data: risk.affected_data || [],
|
||||
}
|
||||
|
||||
const updatedRisks = [...(dsfa.risks || []), newRisk]
|
||||
return updateDSFA(dsfaId, { risks: updatedRisks } as Partial<DSFA>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a risk from a DSFA
|
||||
*/
|
||||
export async function removeDSFARisk(dsfaId: string, riskId: string): Promise<DSFA> {
|
||||
const dsfa = await getDSFA(dsfaId)
|
||||
const updatedRisks = (dsfa.risks || []).filter(r => r.id !== riskId)
|
||||
return updateDSFA(dsfaId, { risks: updatedRisks } as Partial<DSFA>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mitigation to a DSFA
|
||||
*/
|
||||
export async function addDSFAMitigation(dsfaId: string, mitigation: {
|
||||
risk_id: string
|
||||
description: string
|
||||
type: 'technical' | 'organizational' | 'legal'
|
||||
responsible_party: string
|
||||
}): Promise<DSFA> {
|
||||
const dsfa = await getDSFA(dsfaId)
|
||||
const newMitigation = {
|
||||
id: crypto.randomUUID(),
|
||||
...mitigation,
|
||||
status: 'planned' as const,
|
||||
residual_risk: 'medium' as const,
|
||||
}
|
||||
|
||||
const updatedMitigations = [...(dsfa.mitigations || []), newMitigation]
|
||||
return updateDSFA(dsfaId, { mitigations: updatedMitigations } as Partial<DSFA>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mitigation status
|
||||
*/
|
||||
export async function updateDSFAMitigationStatus(
|
||||
dsfaId: string,
|
||||
mitigationId: string,
|
||||
status: 'planned' | 'in_progress' | 'implemented' | 'verified'
|
||||
): Promise<DSFA> {
|
||||
const dsfa = await getDSFA(dsfaId)
|
||||
const updatedMitigations = (dsfa.mitigations || []).map(m => {
|
||||
if (m.id === mitigationId) {
|
||||
return {
|
||||
...m,
|
||||
status,
|
||||
...(status === 'implemented' && { implemented_at: new Date().toISOString() }),
|
||||
...(status === 'verified' && { verified_at: new Date().toISOString() }),
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
return updateDSFA(dsfaId, { mitigations: updatedMitigations } as Partial<DSFA>)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function calculateRiskLevelString(
|
||||
likelihood: 'low' | 'medium' | 'high',
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
): string {
|
||||
const matrix: Record<string, Record<string, string>> = {
|
||||
low: { low: 'low', medium: 'low', high: 'medium' },
|
||||
medium: { low: 'low', medium: 'medium', high: 'high' },
|
||||
high: { low: 'medium', medium: 'high', high: 'very_high' },
|
||||
}
|
||||
return matrix[likelihood]?.[impact] || 'medium'
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* DSFA Module
|
||||
*
|
||||
* Exports for DSFA (Data Protection Impact Assessment) functionality.
|
||||
*/
|
||||
|
||||
export * from './types'
|
||||
export * from './api'
|
||||
@@ -1,365 +0,0 @@
|
||||
/**
|
||||
* DSFA Types - Datenschutz-Folgenabschätzung (Art. 35 DSGVO)
|
||||
*
|
||||
* TypeScript type definitions for DSFA (Data Protection Impact Assessment)
|
||||
* aligned with the backend Go models.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type DSFAStatus = 'draft' | 'in_review' | 'approved' | 'rejected' | 'needs_update'
|
||||
|
||||
export type DSFARiskLevel = 'low' | 'medium' | 'high' | 'very_high'
|
||||
|
||||
export type DSFARiskCategory = 'confidentiality' | 'integrity' | 'availability' | 'rights_freedoms'
|
||||
|
||||
export type DSFAMitigationType = 'technical' | 'organizational' | 'legal'
|
||||
|
||||
export type DSFAMitigationStatus = 'planned' | 'in_progress' | 'implemented' | 'verified'
|
||||
|
||||
export const DSFA_STATUS_LABELS: Record<DSFAStatus, string> = {
|
||||
draft: 'Entwurf',
|
||||
in_review: 'In Prüfung',
|
||||
approved: 'Genehmigt',
|
||||
rejected: 'Abgelehnt',
|
||||
needs_update: 'Überarbeitung erforderlich',
|
||||
}
|
||||
|
||||
export const DSFA_RISK_LEVEL_LABELS: Record<DSFARiskLevel, string> = {
|
||||
low: 'Niedrig',
|
||||
medium: 'Mittel',
|
||||
high: 'Hoch',
|
||||
very_high: 'Sehr Hoch',
|
||||
}
|
||||
|
||||
export const DSFA_LEGAL_BASES = {
|
||||
consent: 'Art. 6 Abs. 1 lit. a DSGVO - Einwilligung',
|
||||
contract: 'Art. 6 Abs. 1 lit. b DSGVO - Vertrag',
|
||||
legal_obligation: 'Art. 6 Abs. 1 lit. c DSGVO - Rechtliche Verpflichtung',
|
||||
vital_interests: 'Art. 6 Abs. 1 lit. d DSGVO - Lebenswichtige Interessen',
|
||||
public_interest: 'Art. 6 Abs. 1 lit. e DSGVO - Öffentliches Interesse',
|
||||
legitimate_interest: 'Art. 6 Abs. 1 lit. f DSGVO - Berechtigtes Interesse',
|
||||
}
|
||||
|
||||
export const DSFA_AFFECTED_RIGHTS = [
|
||||
{ id: 'right_to_information', label: 'Recht auf Information (Art. 13/14)' },
|
||||
{ id: 'right_of_access', label: 'Auskunftsrecht (Art. 15)' },
|
||||
{ id: 'right_to_rectification', label: 'Recht auf Berichtigung (Art. 16)' },
|
||||
{ id: 'right_to_erasure', label: 'Recht auf Löschung (Art. 17)' },
|
||||
{ id: 'right_to_restriction', label: 'Recht auf Einschränkung (Art. 18)' },
|
||||
{ id: 'right_to_data_portability', label: 'Recht auf Datenübertragbarkeit (Art. 20)' },
|
||||
{ id: 'right_to_object', label: 'Widerspruchsrecht (Art. 21)' },
|
||||
{ id: 'right_not_to_be_profiled', label: 'Recht bzgl. Profiling (Art. 22)' },
|
||||
{ id: 'freedom_of_expression', label: 'Meinungsfreiheit' },
|
||||
{ id: 'freedom_of_association', label: 'Versammlungsfreiheit' },
|
||||
{ id: 'non_discrimination', label: 'Nichtdiskriminierung' },
|
||||
{ id: 'data_security', label: 'Datensicherheit' },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// SUB-TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFARisk {
|
||||
id: string
|
||||
category: DSFARiskCategory
|
||||
description: string
|
||||
likelihood: 'low' | 'medium' | 'high'
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
risk_level: string
|
||||
affected_data: string[]
|
||||
}
|
||||
|
||||
export interface DSFAMitigation {
|
||||
id: string
|
||||
risk_id: string
|
||||
description: string
|
||||
type: DSFAMitigationType
|
||||
status: DSFAMitigationStatus
|
||||
implemented_at?: string
|
||||
verified_at?: string
|
||||
residual_risk: 'low' | 'medium' | 'high'
|
||||
tom_reference?: string
|
||||
responsible_party: string
|
||||
}
|
||||
|
||||
export interface DSFAReviewComment {
|
||||
id: string
|
||||
section: number
|
||||
comment: string
|
||||
created_by: string
|
||||
created_at: string
|
||||
resolved: boolean
|
||||
}
|
||||
|
||||
export interface DSFASectionProgress {
|
||||
section_1_complete: boolean
|
||||
section_2_complete: boolean
|
||||
section_3_complete: boolean
|
||||
section_4_complete: boolean
|
||||
section_5_complete: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN DSFA TYPE
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFA {
|
||||
id: string
|
||||
tenant_id: string
|
||||
namespace_id?: string
|
||||
processing_activity_id?: string
|
||||
assessment_id?: string
|
||||
name: string
|
||||
description: string
|
||||
|
||||
// Section 1: Systematische Beschreibung (Art. 35 Abs. 7 lit. a)
|
||||
processing_description: string
|
||||
processing_purpose: string
|
||||
data_categories: string[]
|
||||
data_subjects: string[]
|
||||
recipients: string[]
|
||||
legal_basis: string
|
||||
legal_basis_details?: string
|
||||
|
||||
// Section 2: Notwendigkeit & Verhältnismäßigkeit (Art. 35 Abs. 7 lit. b)
|
||||
necessity_assessment: string
|
||||
proportionality_assessment: string
|
||||
data_minimization?: string
|
||||
alternatives_considered?: string
|
||||
retention_justification?: string
|
||||
|
||||
// Section 3: Risikobewertung (Art. 35 Abs. 7 lit. c)
|
||||
risks: DSFARisk[]
|
||||
overall_risk_level: DSFARiskLevel
|
||||
risk_score: number
|
||||
affected_rights?: string[]
|
||||
triggered_rule_codes?: string[]
|
||||
|
||||
// Section 4: Abhilfemaßnahmen (Art. 35 Abs. 7 lit. d)
|
||||
mitigations: DSFAMitigation[]
|
||||
tom_references?: string[]
|
||||
|
||||
// Section 5: Stellungnahme DSB (Art. 35 Abs. 2 + Art. 36)
|
||||
dpo_consulted: boolean
|
||||
dpo_consulted_at?: string
|
||||
dpo_name?: string
|
||||
dpo_opinion?: string
|
||||
dpo_approved?: boolean
|
||||
authority_consulted: boolean
|
||||
authority_consulted_at?: string
|
||||
authority_reference?: string
|
||||
authority_decision?: string
|
||||
|
||||
// Workflow & Approval
|
||||
status: DSFAStatus
|
||||
submitted_for_review_at?: string
|
||||
submitted_by?: string
|
||||
conclusion: string
|
||||
review_comments?: DSFAReviewComment[]
|
||||
|
||||
// Section Progress Tracking
|
||||
section_progress: DSFASectionProgress
|
||||
|
||||
// Metadata & Audit
|
||||
metadata?: Record<string, unknown>
|
||||
created_at: string
|
||||
updated_at: string
|
||||
created_by: string
|
||||
approved_by?: string
|
||||
approved_at?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API REQUEST/RESPONSE TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFAListResponse {
|
||||
dsfas: DSFA[]
|
||||
}
|
||||
|
||||
export interface DSFAStatsResponse {
|
||||
status_stats: Record<DSFAStatus | 'total', number>
|
||||
risk_stats: Record<DSFARiskLevel, number>
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface CreateDSFARequest {
|
||||
name: string
|
||||
description?: string
|
||||
processing_description?: string
|
||||
processing_purpose?: string
|
||||
data_categories?: string[]
|
||||
legal_basis?: string
|
||||
}
|
||||
|
||||
export interface CreateDSFAFromAssessmentRequest {
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface CreateDSFAFromAssessmentResponse {
|
||||
dsfa: DSFA
|
||||
prefilled: boolean
|
||||
assessment: unknown // UCCA Assessment
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface UpdateDSFASectionRequest {
|
||||
// Section 1
|
||||
processing_description?: string
|
||||
processing_purpose?: string
|
||||
data_categories?: string[]
|
||||
data_subjects?: string[]
|
||||
recipients?: string[]
|
||||
legal_basis?: string
|
||||
legal_basis_details?: string
|
||||
|
||||
// Section 2
|
||||
necessity_assessment?: string
|
||||
proportionality_assessment?: string
|
||||
data_minimization?: string
|
||||
alternatives_considered?: string
|
||||
retention_justification?: string
|
||||
|
||||
// Section 3
|
||||
overall_risk_level?: DSFARiskLevel
|
||||
risk_score?: number
|
||||
affected_rights?: string[]
|
||||
|
||||
// Section 5
|
||||
dpo_consulted?: boolean
|
||||
dpo_name?: string
|
||||
dpo_opinion?: string
|
||||
authority_consulted?: boolean
|
||||
authority_reference?: string
|
||||
authority_decision?: string
|
||||
}
|
||||
|
||||
export interface SubmitForReviewResponse {
|
||||
message: string
|
||||
dsfa: DSFA
|
||||
}
|
||||
|
||||
export interface ApproveDSFARequest {
|
||||
dpo_opinion: string
|
||||
approved: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UCCA INTEGRATION TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFATriggerInfo {
|
||||
required: boolean
|
||||
reason: string
|
||||
triggered_rules: string[]
|
||||
assessment_id?: string
|
||||
existing_dsfa_id?: string
|
||||
}
|
||||
|
||||
export interface UCCATriggeredRule {
|
||||
code: string
|
||||
title: string
|
||||
description: string
|
||||
severity: 'INFO' | 'WARN' | 'BLOCK'
|
||||
gdpr_ref?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER TYPES FOR UI
|
||||
// =============================================================================
|
||||
|
||||
export interface DSFASectionConfig {
|
||||
number: number
|
||||
title: string
|
||||
titleDE: string
|
||||
description: string
|
||||
gdprRef: string
|
||||
fields: string[]
|
||||
required: boolean
|
||||
}
|
||||
|
||||
export const DSFA_SECTIONS: DSFASectionConfig[] = [
|
||||
{
|
||||
number: 1,
|
||||
title: 'Processing Description',
|
||||
titleDE: 'Systematische Beschreibung',
|
||||
description: 'Beschreiben Sie die geplante Verarbeitung, ihren Zweck, die Datenkategorien und Rechtsgrundlage.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. a DSGVO',
|
||||
fields: ['processing_description', 'processing_purpose', 'data_categories', 'data_subjects', 'recipients', 'legal_basis'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: 'Necessity & Proportionality',
|
||||
titleDE: 'Notwendigkeit & Verhältnismäßigkeit',
|
||||
description: 'Begründen Sie, warum die Verarbeitung notwendig ist und welche Alternativen geprüft wurden.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. b DSGVO',
|
||||
fields: ['necessity_assessment', 'proportionality_assessment', 'data_minimization', 'alternatives_considered'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: 'Risk Assessment',
|
||||
titleDE: 'Risikobewertung',
|
||||
description: 'Identifizieren und bewerten Sie die Risiken für die Rechte und Freiheiten der Betroffenen.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. c DSGVO',
|
||||
fields: ['risks', 'overall_risk_level', 'risk_score', 'affected_rights'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: 'Mitigation Measures',
|
||||
titleDE: 'Abhilfemaßnahmen',
|
||||
description: 'Definieren Sie technische und organisatorische Maßnahmen zur Risikominimierung.',
|
||||
gdprRef: 'Art. 35 Abs. 7 lit. d DSGVO',
|
||||
fields: ['mitigations', 'tom_references'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
title: 'DPO Opinion',
|
||||
titleDE: 'Stellungnahme DSB',
|
||||
description: 'Dokumentieren Sie die Konsultation des Datenschutzbeauftragten und ggf. der Aufsichtsbehörde.',
|
||||
gdprRef: 'Art. 35 Abs. 2 + Art. 36 DSGVO',
|
||||
fields: ['dpo_consulted', 'dpo_opinion', 'authority_consulted', 'authority_reference'],
|
||||
required: false,
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// RISK MATRIX HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export interface RiskMatrixCell {
|
||||
likelihood: 'low' | 'medium' | 'high'
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
level: DSFARiskLevel
|
||||
score: number
|
||||
}
|
||||
|
||||
export const RISK_MATRIX: RiskMatrixCell[] = [
|
||||
// Low likelihood
|
||||
{ likelihood: 'low', impact: 'low', level: 'low', score: 10 },
|
||||
{ likelihood: 'low', impact: 'medium', level: 'low', score: 20 },
|
||||
{ likelihood: 'low', impact: 'high', level: 'medium', score: 40 },
|
||||
// Medium likelihood
|
||||
{ likelihood: 'medium', impact: 'low', level: 'low', score: 20 },
|
||||
{ likelihood: 'medium', impact: 'medium', level: 'medium', score: 50 },
|
||||
{ likelihood: 'medium', impact: 'high', level: 'high', score: 70 },
|
||||
// High likelihood
|
||||
{ likelihood: 'high', impact: 'low', level: 'medium', score: 40 },
|
||||
{ likelihood: 'high', impact: 'medium', level: 'high', score: 70 },
|
||||
{ likelihood: 'high', impact: 'high', level: 'very_high', score: 90 },
|
||||
]
|
||||
|
||||
export function calculateRiskLevel(
|
||||
likelihood: 'low' | 'medium' | 'high',
|
||||
impact: 'low' | 'medium' | 'high'
|
||||
): { level: DSFARiskLevel; score: number } {
|
||||
const cell = RISK_MATRIX.find(c => c.likelihood === likelihood && c.impact === impact)
|
||||
return cell ? { level: cell.level, score: cell.score } : { level: 'medium', score: 50 }
|
||||
}
|
||||
664
admin-v2/lib/sdk/dsr/api.ts
Normal file
664
admin-v2/lib/sdk/dsr/api.ts
Normal file
@@ -0,0 +1,664 @@
|
||||
/**
|
||||
* DSR API Client
|
||||
*
|
||||
* API client for Data Subject Request management
|
||||
* Connects to the Go Consent Service backend
|
||||
*/
|
||||
|
||||
import {
|
||||
DSRRequest,
|
||||
DSRListResponse,
|
||||
DSRFilters,
|
||||
DSRCreateRequest,
|
||||
DSRUpdateRequest,
|
||||
DSRVerifyIdentityRequest,
|
||||
DSRCompleteRequest,
|
||||
DSRRejectRequest,
|
||||
DSRExtendDeadlineRequest,
|
||||
DSRSendCommunicationRequest,
|
||||
DSRCommunication,
|
||||
DSRAuditEntry,
|
||||
DSRStatistics,
|
||||
DSRDataExport,
|
||||
DSRErasureChecklist
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const DSR_API_BASE = process.env.NEXT_PUBLIC_CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
const API_TIMEOUT = 30000 // 30 seconds
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getTenantId(): string {
|
||||
// In a real app, this would come from auth context or localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('tenantId') || 'default-tenant'
|
||||
}
|
||||
return 'default-tenant'
|
||||
}
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': getTenantId()
|
||||
}
|
||||
|
||||
// Add auth token if available
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async function fetchWithTimeout<T>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
timeout: number = API_TIMEOUT
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep the HTTP status message
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {} as T
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSR LIST & CRUD
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch all DSR requests with optional filters
|
||||
*/
|
||||
export async function fetchDSRList(filters?: DSRFilters): Promise<DSRListResponse> {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (filters) {
|
||||
if (filters.status) {
|
||||
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
|
||||
statuses.forEach(s => params.append('status', s))
|
||||
}
|
||||
if (filters.type) {
|
||||
const types = Array.isArray(filters.type) ? filters.type : [filters.type]
|
||||
types.forEach(t => params.append('type', t))
|
||||
}
|
||||
if (filters.priority) params.set('priority', filters.priority)
|
||||
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
|
||||
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
|
||||
if (filters.search) params.set('search', filters.search)
|
||||
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
|
||||
if (filters.dateTo) params.set('dateTo', filters.dateTo)
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${DSR_API_BASE}/api/v1/admin/dsr${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
return fetchWithTimeout<DSRListResponse>(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single DSR request by ID
|
||||
*/
|
||||
export async function fetchDSR(id: string): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DSR request
|
||||
*/
|
||||
export async function createDSR(request: DSRCreateRequest): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a DSR request
|
||||
*/
|
||||
export async function updateDSR(id: string, update: DSRUpdateRequest): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a DSR request (soft delete - marks as cancelled)
|
||||
*/
|
||||
export async function deleteDSR(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSR WORKFLOW ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Verify the identity of the requester
|
||||
*/
|
||||
export async function verifyIdentity(
|
||||
dsrId: string,
|
||||
verification: DSRVerifyIdentityRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/verify-identity`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(verification)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a DSR request
|
||||
*/
|
||||
export async function completeDSR(
|
||||
dsrId: string,
|
||||
completion?: DSRCompleteRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/complete`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(completion || {})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a DSR request
|
||||
*/
|
||||
export async function rejectDSR(
|
||||
dsrId: string,
|
||||
rejection: DSRRejectRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/reject`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(rejection)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the deadline for a DSR request
|
||||
*/
|
||||
export async function extendDeadline(
|
||||
dsrId: string,
|
||||
extension: DSRExtendDeadlineRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/extend`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(extension)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a DSR request to a user
|
||||
*/
|
||||
export async function assignDSR(
|
||||
dsrId: string,
|
||||
assignedTo: string
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/assign`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ assignedTo })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get all communications for a DSR request
|
||||
*/
|
||||
export async function getCommunications(dsrId: string): Promise<DSRCommunication[]> {
|
||||
return fetchWithTimeout<DSRCommunication[]>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/communications`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a communication (email, letter, internal note)
|
||||
*/
|
||||
export async function sendCommunication(
|
||||
dsrId: string,
|
||||
communication: DSRSendCommunicationRequest
|
||||
): Promise<DSRCommunication> {
|
||||
return fetchWithTimeout<DSRCommunication>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/send-communication`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(communication)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDIT LOG
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get audit log entries for a DSR request
|
||||
*/
|
||||
export async function getAuditLog(dsrId: string): Promise<DSRAuditEntry[]> {
|
||||
return fetchWithTimeout<DSRAuditEntry[]>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/audit`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get DSR statistics
|
||||
*/
|
||||
export async function getDSRStatistics(): Promise<DSRStatistics> {
|
||||
return fetchWithTimeout<DSRStatistics>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/statistics`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATA EXPORT (Art. 15, 20)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate data export for Art. 15 (access) or Art. 20 (portability)
|
||||
*/
|
||||
export async function generateDataExport(
|
||||
dsrId: string,
|
||||
format: 'json' | 'csv' | 'xml' | 'pdf' = 'json'
|
||||
): Promise<DSRDataExport> {
|
||||
return fetchWithTimeout<DSRDataExport>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ format })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download generated data export
|
||||
*/
|
||||
export async function downloadDataExport(dsrId: string): Promise<Blob> {
|
||||
const response = await fetch(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export/download`,
|
||||
{
|
||||
headers: getAuthHeaders()
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ERASURE CHECKLIST (Art. 17)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the erasure checklist for an Art. 17 request
|
||||
*/
|
||||
export async function getErasureChecklist(dsrId: string): Promise<DSRErasureChecklist> {
|
||||
return fetchWithTimeout<DSRErasureChecklist>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the erasure checklist
|
||||
*/
|
||||
export async function updateErasureChecklist(
|
||||
dsrId: string,
|
||||
checklist: DSRErasureChecklist
|
||||
): Promise<DSRErasureChecklist> {
|
||||
return fetchWithTimeout<DSRErasureChecklist>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(checklist)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get available email templates
|
||||
*/
|
||||
export async function getEmailTemplates(): Promise<{ id: string; name: string; stage: string }[]> {
|
||||
return fetchWithTimeout<{ id: string; name: string; stage: string }[]>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview an email with variables filled in
|
||||
*/
|
||||
export async function previewEmail(
|
||||
templateId: string,
|
||||
dsrId: string
|
||||
): Promise<{ subject: string; body: string }> {
|
||||
return fetchWithTimeout<{ subject: string; body: string }>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates/${templateId}/preview`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ dsrId })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA FUNCTIONS (for development without backend)
|
||||
// =============================================================================
|
||||
|
||||
export function createMockDSRList(): DSRRequest[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'dsr-001',
|
||||
referenceNumber: 'DSR-2025-000001',
|
||||
type: 'access',
|
||||
status: 'intake',
|
||||
priority: 'high',
|
||||
requester: {
|
||||
name: 'Max Mustermann',
|
||||
email: 'max.mustermann@example.de'
|
||||
},
|
||||
source: 'web_form',
|
||||
sourceDetails: 'Kontaktformular auf breakpilot.de',
|
||||
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: false
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: null
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-002',
|
||||
referenceNumber: 'DSR-2025-000002',
|
||||
type: 'erasure',
|
||||
status: 'identity_verification',
|
||||
priority: 'high',
|
||||
requester: {
|
||||
name: 'Anna Schmidt',
|
||||
email: 'anna.schmidt@example.de',
|
||||
phone: '+49 170 1234567'
|
||||
},
|
||||
source: 'email',
|
||||
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
|
||||
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: false
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-003',
|
||||
referenceNumber: 'DSR-2025-000003',
|
||||
type: 'rectification',
|
||||
status: 'processing',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Peter Meier',
|
||||
email: 'peter.meier@example.de'
|
||||
},
|
||||
source: 'email',
|
||||
requestText: 'Meine Adresse ist falsch gespeichert.',
|
||||
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'existing_account',
|
||||
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
rectificationDetails: {
|
||||
fieldsToCorrect: [
|
||||
{
|
||||
field: 'Adresse',
|
||||
currentValue: 'Musterstr. 1, 12345 Berlin',
|
||||
requestedValue: 'Musterstr. 10, 12345 Berlin',
|
||||
corrected: false
|
||||
}
|
||||
]
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-004',
|
||||
referenceNumber: 'DSR-2025-000004',
|
||||
type: 'portability',
|
||||
status: 'processing',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Lisa Weber',
|
||||
email: 'lisa.weber@example.de'
|
||||
},
|
||||
source: 'web_form',
|
||||
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'id_document',
|
||||
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'IT Team',
|
||||
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
notes: 'JSON-Export wird vorbereitet',
|
||||
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-005',
|
||||
referenceNumber: 'DSR-2025-000005',
|
||||
type: 'objection',
|
||||
status: 'rejected',
|
||||
priority: 'low',
|
||||
requester: {
|
||||
name: 'Thomas Klein',
|
||||
email: 'thomas.klein@example.de'
|
||||
},
|
||||
source: 'letter',
|
||||
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
|
||||
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'postal',
|
||||
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'Rechtsabteilung',
|
||||
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
objectionDetails: {
|
||||
processingPurpose: 'Marketing',
|
||||
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
|
||||
objectionGrounds: 'Keine konkreten Gruende genannt',
|
||||
decision: 'rejected',
|
||||
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
|
||||
decisionBy: 'Rechtsabteilung',
|
||||
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
|
||||
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
},
|
||||
{
|
||||
id: 'dsr-006',
|
||||
referenceNumber: 'DSR-2025-000006',
|
||||
type: 'access',
|
||||
status: 'completed',
|
||||
priority: 'normal',
|
||||
requester: {
|
||||
name: 'Sarah Braun',
|
||||
email: 'sarah.braun@example.de'
|
||||
},
|
||||
source: 'email',
|
||||
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: {
|
||||
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
extended: false
|
||||
},
|
||||
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: 'id_document',
|
||||
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
verifiedBy: 'DSB Mueller'
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: 'DSB Mueller',
|
||||
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
dataExport: {
|
||||
format: 'pdf',
|
||||
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
generatedBy: 'DSB Mueller',
|
||||
fileName: 'datenauskunft_sarah_braun.pdf',
|
||||
fileSize: 245000,
|
||||
includesThirdPartyData: false
|
||||
},
|
||||
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
createdBy: 'system',
|
||||
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
tenantId: 'default-tenant'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function createMockStatistics(): DSRStatistics {
|
||||
return {
|
||||
total: 6,
|
||||
byStatus: {
|
||||
intake: 1,
|
||||
identity_verification: 1,
|
||||
processing: 2,
|
||||
completed: 1,
|
||||
rejected: 1,
|
||||
cancelled: 0
|
||||
},
|
||||
byType: {
|
||||
access: 2,
|
||||
rectification: 1,
|
||||
erasure: 1,
|
||||
restriction: 0,
|
||||
portability: 1,
|
||||
objection: 1
|
||||
},
|
||||
overdue: 0,
|
||||
dueThisWeek: 2,
|
||||
averageProcessingDays: 18,
|
||||
completedThisMonth: 1
|
||||
}
|
||||
}
|
||||
6
admin-v2/lib/sdk/dsr/index.ts
Normal file
6
admin-v2/lib/sdk/dsr/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* DSR Module Exports
|
||||
*/
|
||||
|
||||
export * from './types'
|
||||
export * from './api'
|
||||
581
admin-v2/lib/sdk/dsr/types.ts
Normal file
581
admin-v2/lib/sdk/dsr/types.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* DSR (Data Subject Request) Types
|
||||
*
|
||||
* TypeScript definitions for GDPR Art. 15-21 Data Subject Requests
|
||||
* Based on the Go Consent Service backend API structure
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type DSRType =
|
||||
| 'access' // Art. 15 - Auskunftsrecht
|
||||
| 'rectification' // Art. 16 - Berichtigungsrecht
|
||||
| 'erasure' // Art. 17 - Loeschungsrecht
|
||||
| 'restriction' // Art. 18 - Einschraenkungsrecht
|
||||
| 'portability' // Art. 20 - Datenuebertragbarkeit
|
||||
| 'objection' // Art. 21 - Widerspruchsrecht
|
||||
|
||||
export type DSRStatus =
|
||||
| 'intake' // Eingang - Anfrage dokumentiert
|
||||
| 'identity_verification' // Identitaetspruefung
|
||||
| 'processing' // In Bearbeitung
|
||||
| 'completed' // Abgeschlossen
|
||||
| 'rejected' // Abgelehnt
|
||||
| 'cancelled' // Storniert
|
||||
|
||||
export type DSRPriority = 'low' | 'normal' | 'high' | 'critical'
|
||||
|
||||
export type DSRSource =
|
||||
| 'web_form' // Kontaktformular/Portal
|
||||
| 'email' // E-Mail
|
||||
| 'letter' // Brief
|
||||
| 'phone' // Telefon
|
||||
| 'in_person' // Persoenlich
|
||||
| 'other' // Sonstiges
|
||||
|
||||
export type IdentityVerificationMethod =
|
||||
| 'id_document' // Ausweiskopie
|
||||
| 'email' // E-Mail-Bestaetigung
|
||||
| 'phone' // Telefonische Bestaetigung
|
||||
| 'postal' // Postalische Bestaetigung
|
||||
| 'existing_account' // Bestehendes Kundenkonto
|
||||
| 'other' // Sonstiges
|
||||
|
||||
export type CommunicationType =
|
||||
| 'incoming' // Eingehend (vom Betroffenen)
|
||||
| 'outgoing' // Ausgehend (an Betroffenen)
|
||||
| 'internal' // Intern (Notizen)
|
||||
|
||||
export type CommunicationChannel =
|
||||
| 'email'
|
||||
| 'letter'
|
||||
| 'phone'
|
||||
| 'portal'
|
||||
| 'internal_note'
|
||||
|
||||
// =============================================================================
|
||||
// DSR TYPE METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRTypeInfo {
|
||||
type: DSRType
|
||||
article: string
|
||||
label: string
|
||||
labelShort: string
|
||||
description: string
|
||||
defaultDeadlineDays: number
|
||||
maxExtensionMonths: number
|
||||
color: string
|
||||
bgColor: string
|
||||
processDocument?: string // Reference to process document
|
||||
}
|
||||
|
||||
export const DSR_TYPE_INFO: Record<DSRType, DSRTypeInfo> = {
|
||||
access: {
|
||||
type: 'access',
|
||||
article: 'Art. 15',
|
||||
label: 'Auskunftsrecht',
|
||||
labelShort: 'Auskunft',
|
||||
description: 'Recht auf Auskunft ueber gespeicherte personenbezogene Daten',
|
||||
defaultDeadlineDays: 30,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 15 DSGVO_v02.pdf'
|
||||
},
|
||||
rectification: {
|
||||
type: 'rectification',
|
||||
article: 'Art. 16',
|
||||
label: 'Berichtigungsrecht',
|
||||
labelShort: 'Berichtigung',
|
||||
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
|
||||
defaultDeadlineDays: 14,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100',
|
||||
processDocument: 'Prozessbeschriebung Art. 16 DSGVO_v02.pdf'
|
||||
},
|
||||
erasure: {
|
||||
type: 'erasure',
|
||||
article: 'Art. 17',
|
||||
label: 'Loeschungsrecht',
|
||||
labelShort: 'Loeschung',
|
||||
description: 'Recht auf Loeschung personenbezogener Daten ("Recht auf Vergessenwerden")',
|
||||
defaultDeadlineDays: 14,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 17 DSGVO_v03.pdf'
|
||||
},
|
||||
restriction: {
|
||||
type: 'restriction',
|
||||
article: 'Art. 18',
|
||||
label: 'Einschraenkungsrecht',
|
||||
labelShort: 'Einschraenkung',
|
||||
description: 'Recht auf Einschraenkung der Verarbeitung',
|
||||
defaultDeadlineDays: 14,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 18 DSGVO_v01.pdf'
|
||||
},
|
||||
portability: {
|
||||
type: 'portability',
|
||||
article: 'Art. 20',
|
||||
label: 'Datenuebertragbarkeit',
|
||||
labelShort: 'Uebertragung',
|
||||
description: 'Recht auf Datenuebertragbarkeit in maschinenlesbarem Format',
|
||||
defaultDeadlineDays: 30,
|
||||
maxExtensionMonths: 2,
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100',
|
||||
processDocument: 'Prozessbeschreibung Art. 20 DSGVO_v02.pdf'
|
||||
},
|
||||
objection: {
|
||||
type: 'objection',
|
||||
article: 'Art. 21',
|
||||
label: 'Widerspruchsrecht',
|
||||
labelShort: 'Widerspruch',
|
||||
description: 'Recht auf Widerspruch gegen die Verarbeitung',
|
||||
defaultDeadlineDays: 30,
|
||||
maxExtensionMonths: 0, // No extension allowed for objections
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
export const DSR_STATUS_INFO: Record<DSRStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
|
||||
intake: {
|
||||
label: 'Eingang',
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100',
|
||||
borderColor: 'border-blue-200'
|
||||
},
|
||||
identity_verification: {
|
||||
label: 'ID-Pruefung',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100',
|
||||
borderColor: 'border-yellow-200'
|
||||
},
|
||||
processing: {
|
||||
label: 'In Bearbeitung',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100',
|
||||
borderColor: 'border-purple-200'
|
||||
},
|
||||
completed: {
|
||||
label: 'Abgeschlossen',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100',
|
||||
borderColor: 'border-green-200'
|
||||
},
|
||||
rejected: {
|
||||
label: 'Abgelehnt',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100',
|
||||
borderColor: 'border-red-200'
|
||||
},
|
||||
cancelled: {
|
||||
label: 'Storniert',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100',
|
||||
borderColor: 'border-gray-200'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRRequester {
|
||||
name: string
|
||||
email: string
|
||||
phone?: string
|
||||
address?: string
|
||||
customerId?: string // If existing customer
|
||||
}
|
||||
|
||||
export interface DSRIdentityVerification {
|
||||
verified: boolean
|
||||
method?: IdentityVerificationMethod
|
||||
verifiedAt?: string
|
||||
verifiedBy?: string
|
||||
notes?: string
|
||||
documentRef?: string // Reference to uploaded ID document
|
||||
}
|
||||
|
||||
export interface DSRAssignment {
|
||||
assignedTo: string | null
|
||||
assignedAt?: string
|
||||
assignedBy?: string
|
||||
}
|
||||
|
||||
export interface DSRDeadline {
|
||||
originalDeadline: string
|
||||
currentDeadline: string
|
||||
extended: boolean
|
||||
extensionReason?: string
|
||||
extensionApprovedBy?: string
|
||||
extensionApprovedAt?: string
|
||||
}
|
||||
|
||||
export interface DSRRequest {
|
||||
id: string
|
||||
referenceNumber: string // e.g., "DSR-2025-000042"
|
||||
type: DSRType
|
||||
status: DSRStatus
|
||||
priority: DSRPriority
|
||||
|
||||
// Requester info
|
||||
requester: DSRRequester
|
||||
|
||||
// Request details
|
||||
source: DSRSource
|
||||
sourceDetails?: string // e.g., "Kontaktformular auf website.de"
|
||||
requestText?: string // Original request text
|
||||
|
||||
// Dates
|
||||
receivedAt: string
|
||||
deadline: DSRDeadline
|
||||
completedAt?: string
|
||||
|
||||
// Verification
|
||||
identityVerification: DSRIdentityVerification
|
||||
|
||||
// Assignment
|
||||
assignment: DSRAssignment
|
||||
|
||||
// Processing
|
||||
notes?: string
|
||||
internalNotes?: string
|
||||
|
||||
// Type-specific data
|
||||
erasureChecklist?: DSRErasureChecklist // For Art. 17
|
||||
dataExport?: DSRDataExport // For Art. 15, 20
|
||||
rectificationDetails?: DSRRectificationDetails // For Art. 16
|
||||
objectionDetails?: DSRObjectionDetails // For Art. 21
|
||||
|
||||
// Audit
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
updatedAt: string
|
||||
updatedBy?: string
|
||||
|
||||
// Metadata
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TYPE-SPECIFIC INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
// Art. 17(3) Erasure Exceptions Checklist
|
||||
export interface DSRErasureChecklistItem {
|
||||
id: string
|
||||
article: string // e.g., "17(3)(a)"
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
applies: boolean
|
||||
notes?: string
|
||||
}
|
||||
|
||||
export interface DSRErasureChecklist {
|
||||
items: DSRErasureChecklistItem[]
|
||||
canProceedWithErasure: boolean
|
||||
reviewedBy?: string
|
||||
reviewedAt?: string
|
||||
}
|
||||
|
||||
export const ERASURE_EXCEPTIONS: Omit<DSRErasureChecklistItem, 'checked' | 'applies' | 'notes'>[] = [
|
||||
{
|
||||
id: 'art17_3_a',
|
||||
article: '17(3)(a)',
|
||||
label: 'Meinungs- und Informationsfreiheit',
|
||||
description: 'Ausuebung des Rechts auf freie Meinungsaeusserung und Information'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_b',
|
||||
article: '17(3)(b)',
|
||||
label: 'Rechtliche Verpflichtung',
|
||||
description: 'Erfuellung einer rechtlichen Verpflichtung (z.B. Aufbewahrungspflichten)'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_c',
|
||||
article: '17(3)(c)',
|
||||
label: 'Oeffentliches Interesse',
|
||||
description: 'Gruende des oeffentlichen Interesses im Bereich Gesundheit'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_d',
|
||||
article: '17(3)(d)',
|
||||
label: 'Archivzwecke',
|
||||
description: 'Archivzwecke, wissenschaftliche/historische Forschung, Statistik'
|
||||
},
|
||||
{
|
||||
id: 'art17_3_e',
|
||||
article: '17(3)(e)',
|
||||
label: 'Rechtsansprueche',
|
||||
description: 'Geltendmachung, Ausuebung oder Verteidigung von Rechtsanspruechen'
|
||||
}
|
||||
]
|
||||
|
||||
// Data Export for Art. 15, 20
|
||||
export interface DSRDataExport {
|
||||
format: 'json' | 'csv' | 'xml' | 'pdf'
|
||||
generatedAt?: string
|
||||
generatedBy?: string
|
||||
fileUrl?: string
|
||||
fileName?: string
|
||||
fileSize?: number
|
||||
includesThirdPartyData: boolean
|
||||
anonymizedFields?: string[]
|
||||
transferMethod?: 'download' | 'email' | 'third_party' // For Art. 20 transfer
|
||||
transferRecipient?: string // For Art. 20 transfer to another controller
|
||||
}
|
||||
|
||||
// Rectification Details for Art. 16
|
||||
export interface DSRRectificationDetails {
|
||||
fieldsToCorrect: {
|
||||
field: string
|
||||
currentValue: string
|
||||
requestedValue: string
|
||||
corrected: boolean
|
||||
correctedAt?: string
|
||||
correctedBy?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
// Objection Details for Art. 21
|
||||
export interface DSRObjectionDetails {
|
||||
processingPurpose: string
|
||||
legalBasis: string
|
||||
objectionGrounds: string
|
||||
decision: 'accepted' | 'rejected' | 'pending'
|
||||
decisionReason?: string
|
||||
decisionBy?: string
|
||||
decisionAt?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATION
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRCommunication {
|
||||
id: string
|
||||
dsrId: string
|
||||
type: CommunicationType
|
||||
channel: CommunicationChannel
|
||||
subject?: string
|
||||
content: string
|
||||
templateUsed?: string // Reference to email template
|
||||
attachments?: {
|
||||
name: string
|
||||
url: string
|
||||
size: number
|
||||
type: string
|
||||
}[]
|
||||
sentAt?: string
|
||||
sentBy?: string
|
||||
receivedAt?: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDIT LOG
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRAuditEntry {
|
||||
id: string
|
||||
dsrId: string
|
||||
action: string // e.g., "status_changed", "identity_verified", "assigned"
|
||||
previousValue?: string
|
||||
newValue?: string
|
||||
performedBy: string
|
||||
performedAt: string
|
||||
notes?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSREmailTemplate {
|
||||
id: string
|
||||
name: string
|
||||
subject: string
|
||||
body: string
|
||||
type: DSRType | 'general'
|
||||
stage: DSRStatus | 'identity_request' | 'deadline_extension' | 'completion'
|
||||
language: 'de' | 'en'
|
||||
variables: string[] // e.g., ["requesterName", "referenceNumber", "deadline"]
|
||||
}
|
||||
|
||||
export const DSR_EMAIL_TEMPLATES: DSREmailTemplate[] = [
|
||||
{
|
||||
id: 'intake_confirmation',
|
||||
name: 'Eingangsbestaetigung',
|
||||
subject: 'Bestaetigung Ihrer Anfrage - {{referenceNumber}}',
|
||||
body: `Sehr geehrte(r) {{requesterName}},
|
||||
|
||||
wir bestaetigen den Eingang Ihrer Anfrage vom {{receivedDate}}.
|
||||
|
||||
Referenznummer: {{referenceNumber}}
|
||||
Art der Anfrage: {{requestType}}
|
||||
|
||||
Wir werden Ihre Anfrage innerhalb der gesetzlichen Frist von {{deadline}} bearbeiten.
|
||||
|
||||
Mit freundlichen Gruessen
|
||||
{{senderName}}
|
||||
Datenschutzbeauftragter`,
|
||||
type: 'general',
|
||||
stage: 'intake',
|
||||
language: 'de',
|
||||
variables: ['requesterName', 'receivedDate', 'referenceNumber', 'requestType', 'deadline', 'senderName']
|
||||
},
|
||||
{
|
||||
id: 'identity_request',
|
||||
name: 'Identitaetsanfrage',
|
||||
subject: 'Identitaetspruefung erforderlich - {{referenceNumber}}',
|
||||
body: `Sehr geehrte(r) {{requesterName}},
|
||||
|
||||
um Ihre Anfrage bearbeiten zu koennen, benoetigen wir einen Nachweis Ihrer Identitaet.
|
||||
|
||||
Bitte senden Sie uns eines der folgenden Dokumente:
|
||||
- Kopie Ihres Personalausweises (Vorder- und Rueckseite)
|
||||
- Kopie Ihres Reisepasses
|
||||
|
||||
Ihre Daten werden ausschliesslich zur Identitaetspruefung verwendet und anschliessend geloescht.
|
||||
|
||||
Mit freundlichen Gruessen
|
||||
{{senderName}}
|
||||
Datenschutzbeauftragter`,
|
||||
type: 'general',
|
||||
stage: 'identity_request',
|
||||
language: 'de',
|
||||
variables: ['requesterName', 'referenceNumber', 'senderName']
|
||||
}
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRFilters {
|
||||
status?: DSRStatus | DSRStatus[]
|
||||
type?: DSRType | DSRType[]
|
||||
priority?: DSRPriority
|
||||
assignedTo?: string
|
||||
overdue?: boolean
|
||||
search?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
export interface DSRListResponse {
|
||||
requests: DSRRequest[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface DSRCreateRequest {
|
||||
type: DSRType
|
||||
requester: DSRRequester
|
||||
source: DSRSource
|
||||
sourceDetails?: string
|
||||
requestText?: string
|
||||
priority?: DSRPriority
|
||||
}
|
||||
|
||||
export interface DSRUpdateRequest {
|
||||
status?: DSRStatus
|
||||
priority?: DSRPriority
|
||||
notes?: string
|
||||
internalNotes?: string
|
||||
assignment?: DSRAssignment
|
||||
}
|
||||
|
||||
export interface DSRVerifyIdentityRequest {
|
||||
method: IdentityVerificationMethod
|
||||
notes?: string
|
||||
documentRef?: string
|
||||
}
|
||||
|
||||
export interface DSRCompleteRequest {
|
||||
completionNotes?: string
|
||||
dataExport?: DSRDataExport
|
||||
}
|
||||
|
||||
export interface DSRRejectRequest {
|
||||
reason: string
|
||||
legalBasis?: string // e.g., Art. 17(3) exception
|
||||
}
|
||||
|
||||
export interface DSRExtendDeadlineRequest {
|
||||
extensionMonths: 1 | 2
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface DSRSendCommunicationRequest {
|
||||
type: CommunicationType
|
||||
channel: CommunicationChannel
|
||||
subject?: string
|
||||
content: string
|
||||
templateId?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export interface DSRStatistics {
|
||||
total: number
|
||||
byStatus: Record<DSRStatus, number>
|
||||
byType: Record<DSRType, number>
|
||||
overdue: number
|
||||
dueThisWeek: number
|
||||
averageProcessingDays: number
|
||||
completedThisMonth: number
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function getDaysRemaining(deadline: string): number {
|
||||
const deadlineDate = new Date(deadline)
|
||||
const now = new Date()
|
||||
const diff = deadlineDate.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
export function isOverdue(request: DSRRequest): boolean {
|
||||
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
|
||||
return false
|
||||
}
|
||||
return getDaysRemaining(request.deadline.currentDeadline) < 0
|
||||
}
|
||||
|
||||
export function isUrgent(request: DSRRequest, thresholdDays: number = 7): boolean {
|
||||
if (request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled') {
|
||||
return false
|
||||
}
|
||||
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
|
||||
return daysRemaining >= 0 && daysRemaining <= thresholdDays
|
||||
}
|
||||
|
||||
export function generateReferenceNumber(year: number, sequence: number): string {
|
||||
return `DSR-${year}-${String(sequence).padStart(6, '0')}`
|
||||
}
|
||||
|
||||
export function getTypeInfo(type: DSRType): DSRTypeInfo {
|
||||
return DSR_TYPE_INFO[type]
|
||||
}
|
||||
|
||||
export function getStatusInfo(status: DSRStatus) {
|
||||
return DSR_STATUS_INFO[status]
|
||||
}
|
||||
1084
admin-v2/lib/sdk/einwilligungen/catalog/data-points.yml
Normal file
1084
admin-v2/lib/sdk/einwilligungen/catalog/data-points.yml
Normal file
File diff suppressed because it is too large
Load Diff
308
admin-v2/lib/sdk/einwilligungen/catalog/loader.ts
Normal file
308
admin-v2/lib/sdk/einwilligungen/catalog/loader.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* YAML Catalog Loader - Erweiterte Version mit 128 Datenpunkten
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
RetentionMatrixEntry,
|
||||
CookieBannerCategory,
|
||||
LocalizedText,
|
||||
DataPointCatalog,
|
||||
} from '../types'
|
||||
|
||||
function l(de: string, en: string): LocalizedText {
|
||||
return { de, en }
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VORDEFINIERTE DATENPUNKTE (128 Stueck in 18 Kategorien)
|
||||
// =============================================================================
|
||||
|
||||
export const PREDEFINED_DATA_POINTS: DataPoint[] = [
|
||||
// KATEGORIE A: STAMMDATEN (8)
|
||||
{ id: 'dp-a1-firstname', code: 'A1', category: 'MASTER_DATA', name: l('Vorname', 'First Name'), description: l('Vorname der betroffenen Person', 'First name of the data subject'), purpose: l('Identifikation, Vertragserfuellung', 'Identification, contract fulfillment'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Erforderlich zur Vertragserfuellung', 'Required for contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['identity', 'master-data'] },
|
||||
{ id: 'dp-a2-lastname', code: 'A2', category: 'MASTER_DATA', name: l('Nachname', 'Last Name'), description: l('Nachname der betroffenen Person', 'Last name of the data subject'), purpose: l('Identifikation, Vertragserfuellung', 'Identification, contract fulfillment'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Erforderlich zur Vertragserfuellung', 'Required for contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['identity', 'master-data'] },
|
||||
{ id: 'dp-a3-birthdate', code: 'A3', category: 'MASTER_DATA', name: l('Geburtsdatum', 'Date of Birth'), description: l('Geburtsdatum zur Altersverifikation', 'Date of birth for age verification'), purpose: l('Altersverifikation, Identitaetspruefung', 'Age verification, identity check'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Erforderlich zur Altersverifikation', 'Required for age verification'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Fuer Identifikationszwecke', 'For identification purposes'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['identity', 'master-data'] },
|
||||
{ id: 'dp-a4-gender', code: 'A4', category: 'MASTER_DATA', name: l('Geschlecht', 'Gender'), description: l('Geschlechtsangabe (optional)', 'Gender (optional)'), purpose: l('Personalisierte Ansprache', 'Personalized communication'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['personal', 'master-data'] },
|
||||
{ id: 'dp-a5-title', code: 'A5', category: 'MASTER_DATA', name: l('Anrede/Titel', 'Salutation/Title'), description: l('Akademischer Titel oder Anrede', 'Academic title or salutation'), purpose: l('Korrekte Ansprache', 'Correct salutation'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Bestandteil der Kommunikation', 'Part of communication'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['personal', 'master-data'] },
|
||||
{ id: 'dp-a6-profile-picture', code: 'A6', category: 'MASTER_DATA', name: l('Profilbild', 'Profile Picture'), description: l('Vom Nutzer hochgeladenes Profilbild', 'User-uploaded profile picture'), purpose: l('Visuelle Identifikation', 'Visual identification'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Upload', 'Voluntary upload'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Loeschung bei Kontoschliessung', 'Deletion on account closure'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['CDN'], technicalMeasures: ['Verschluesselung'], tags: ['image', 'master-data'] },
|
||||
{ id: 'dp-a7-nationality', code: 'A7', category: 'MASTER_DATA', name: l('Staatsangehoerigkeit', 'Nationality'), description: l('Staatsangehoerigkeit der Person', 'Nationality of the person'), purpose: l('Compliance-Pruefung', 'Compliance check'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Compliance (Sanktionslisten)', 'Compliance (sanction lists)'), retentionPeriod: '10_YEARS', retentionJustification: l('Aufbewahrungspflichten', 'Retention obligations'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['compliance', 'master-data'] },
|
||||
{ id: 'dp-a8-username', code: 'A8', category: 'MASTER_DATA', name: l('Benutzername', 'Username'), description: l('Selbst gewaehlter Benutzername', 'Self-chosen username'), purpose: l('Identifikation, Login', 'Identification, login'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Kontoverwaltung', 'For account management'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['account', 'master-data'] },
|
||||
|
||||
// KATEGORIE B: KONTAKTDATEN (10)
|
||||
{ id: 'dp-b1-email', code: 'B1', category: 'CONTACT_DATA', name: l('E-Mail-Adresse', 'Email Address'), description: l('Primaere E-Mail-Adresse', 'Primary email address'), purpose: l('Kommunikation, Benachrichtigungen', 'Communication, notifications'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['E-Mail-Dienstleister'], technicalMeasures: ['TLS-Verschluesselung'], tags: ['contact', 'essential'] },
|
||||
{ id: 'dp-b2-phone', code: 'B2', category: 'CONTACT_DATA', name: l('Telefonnummer', 'Phone Number'), description: l('Festnetz-Telefonnummer', 'Landline phone number'), purpose: l('Telefonische Kontaktaufnahme', 'Phone contact'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Kundensupport', 'For customer support'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'phone'] },
|
||||
{ id: 'dp-b3-mobile', code: 'B3', category: 'CONTACT_DATA', name: l('Mobilnummer', 'Mobile Number'), description: l('Mobiltelefonnummer', 'Mobile phone number'), purpose: l('SMS-Benachrichtigungen, 2FA', 'SMS notifications, 2FA'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Sicherheit und Kommunikation', 'For security and communication'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SMS-Provider'], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'mobile', '2fa'] },
|
||||
{ id: 'dp-b4-address-street', code: 'B4', category: 'CONTACT_DATA', name: l('Strasse/Hausnummer', 'Street/House Number'), description: l('Strassenadresse', 'Street address'), purpose: l('Lieferung, Rechnungsstellung', 'Delivery, billing'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Versanddienstleister'], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'address'] },
|
||||
{ id: 'dp-b5-address-city', code: 'B5', category: 'CONTACT_DATA', name: l('PLZ/Ort', 'Postal Code/City'), description: l('Postleitzahl und Stadt', 'Postal code and city'), purpose: l('Lieferung, Rechnungsstellung', 'Delivery, billing'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Versanddienstleister'], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'address'] },
|
||||
{ id: 'dp-b6-address-country', code: 'B6', category: 'CONTACT_DATA', name: l('Land', 'Country'), description: l('Wohnsitzland', 'Country of residence'), purpose: l('Lieferung, Steuer', 'Delivery, tax'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'address'] },
|
||||
{ id: 'dp-b7-secondary-email', code: 'B7', category: 'CONTACT_DATA', name: l('Sekundaere E-Mail', 'Secondary Email'), description: l('Alternative E-Mail-Adresse', 'Alternative email address'), purpose: l('Backup-Kontakt, Wiederherstellung', 'Backup contact, recovery'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['TLS'], tags: ['contact', 'backup'] },
|
||||
{ id: 'dp-b8-fax', code: 'B8', category: 'CONTACT_DATA', name: l('Faxnummer', 'Fax Number'), description: l('Faxnummer (geschaeftlich)', 'Fax number (business)'), purpose: l('Geschaeftliche Kommunikation', 'Business communication'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer geschaeftliche Kommunikation', 'For business communication'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'business'] },
|
||||
{ id: 'dp-b9-emergency-contact', code: 'B9', category: 'CONTACT_DATA', name: l('Notfallkontakt', 'Emergency Contact'), description: l('Kontaktdaten fuer Notfaelle', 'Emergency contact details'), purpose: l('Notfallbenachrichtigung', 'Emergency notification'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['contact', 'emergency'] },
|
||||
{ id: 'dp-b10-social-profiles', code: 'B10', category: 'CONTACT_DATA', name: l('Social-Media-Profile', 'Social Media Profiles'), description: l('Links zu sozialen Netzwerken', 'Links to social networks'), purpose: l('Vernetzung, Kommunikation', 'Networking, communication'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Angabe', 'Voluntary'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contact', 'social'] },
|
||||
|
||||
// KATEGORIE C: AUTHENTIFIZIERUNGSDATEN (8)
|
||||
{ id: 'dp-c1-password-hash', code: 'C1', category: 'AUTHENTICATION', name: l('Passwort-Hash', 'Password Hash'), description: l('Kryptografisch gehashtes Passwort', 'Cryptographically hashed password'), purpose: l('Sichere Authentifizierung', 'Secure authentication'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer sichere Kontoverwaltung', 'For secure account management'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['bcrypt/Argon2', 'Salting'], tags: ['auth', 'security'] },
|
||||
{ id: 'dp-c2-session-token', code: 'C2', category: 'AUTHENTICATION', name: l('Session-Token', 'Session Token'), description: l('JWT oder Session-ID', 'JWT or Session ID'), purpose: l('Aufrechterhaltung der Sitzung', 'Maintaining session'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch erforderlich', 'Technically required'), retentionPeriod: '24_HOURS', retentionJustification: l('Kurze Lebensdauer', 'Short lifespan'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['JWT-Signatur', 'HttpOnly'], tags: ['auth', 'session'] },
|
||||
{ id: 'dp-c3-refresh-token', code: 'C3', category: 'AUTHENTICATION', name: l('Refresh-Token', 'Refresh Token'), description: l('Token zur Session-Erneuerung', 'Token for session renewal'), purpose: l('Session-Erneuerung', 'Session renewal'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Benutzerfreundlichkeit', 'For user experience'), retentionPeriod: '30_DAYS', retentionJustification: l('Balance Sicherheit/UX', 'Balance security/UX'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Token-Rotation'], tags: ['auth', 'token'] },
|
||||
{ id: 'dp-c4-2fa-secret', code: 'C4', category: 'AUTHENTICATION', name: l('2FA-Secret', '2FA Secret'), description: l('TOTP-Geheimnis fuer Zwei-Faktor-Auth', 'TOTP secret for two-factor auth'), purpose: l('Erhoehte Kontosicherheit', 'Enhanced account security'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Sicherheit', 'For security'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange 2FA aktiv', 'While 2FA active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'HSM'], tags: ['auth', '2fa', 'security'] },
|
||||
{ id: 'dp-c5-passkey', code: 'C5', category: 'AUTHENTICATION', name: l('Passkey/WebAuthn', 'Passkey/WebAuthn'), description: l('FIDO2/WebAuthn Credential', 'FIDO2/WebAuthn Credential'), purpose: l('Passwortlose Authentifizierung', 'Passwordless authentication'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer sichere Anmeldung', 'For secure login'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Passkey aktiv', 'While passkey active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Asymmetrische Kryptografie'], tags: ['auth', 'passkey'] },
|
||||
{ id: 'dp-c6-api-keys', code: 'C6', category: 'AUTHENTICATION', name: l('API-Keys', 'API Keys'), description: l('API-Schluessel fuer Integrationen', 'API keys for integrations'), purpose: l('API-Zugriff', 'API access'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer API-Nutzung', 'For API usage'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Hashing', 'Rate-Limiting'], tags: ['auth', 'api'] },
|
||||
{ id: 'dp-c7-oauth-provider', code: 'C7', category: 'AUTHENTICATION', name: l('OAuth-Provider-ID', 'OAuth Provider ID'), description: l('ID vom externen Auth-Provider', 'ID from external auth provider'), purpose: l('Social Login', 'Social Login'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Social Login', 'For social login'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange verknuepft', 'While linked'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['OAuth-Provider'], technicalMeasures: ['Minimale Daten'], tags: ['auth', 'oauth'] },
|
||||
{ id: 'dp-c8-recovery-codes', code: 'C8', category: 'AUTHENTICATION', name: l('Wiederherstellungscodes', 'Recovery Codes'), description: l('Backup-Codes fuer 2FA', 'Backup codes for 2FA'), purpose: l('Kontowiederherstellung', 'Account recovery'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Notfallzugriff', 'For emergency access'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange 2FA aktiv', 'While 2FA active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Hashing', 'Einmalnutzung'], tags: ['auth', 'recovery'] },
|
||||
|
||||
// KATEGORIE D: EINWILLIGUNGSDATEN (6)
|
||||
{ id: 'dp-d1-consent-records', code: 'D1', category: 'CONSENT', name: l('Consent-Protokolle', 'Consent Records'), description: l('Protokollierte Einwilligungen', 'Recorded consents'), purpose: l('Nachweis gegenueber Behoerden', 'Proof to authorities'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Nachweispflicht Art. 7 DSGVO', 'Accountability Art. 7 GDPR'), retentionPeriod: '6_YEARS', retentionJustification: l('Audit-Zwecke', 'Audit purposes'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Unveraenderbare Logs'], tags: ['consent', 'compliance'] },
|
||||
{ id: 'dp-d2-cookie-preferences', code: 'D2', category: 'CONSENT', name: l('Cookie-Praeferenzen', 'Cookie Preferences'), description: l('Cookie-Einstellungen des Nutzers', 'User cookie settings'), purpose: l('Speicherung der Consent-Entscheidung', 'Storage of consent decision'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Speicherung der Entscheidung', 'Storage of decision'), retentionPeriod: '12_MONTHS', retentionJustification: l('Branchenueblich', 'Industry standard'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['First-Party Cookie'], tags: ['consent', 'cookie'] },
|
||||
{ id: 'dp-d3-marketing-consent', code: 'D3', category: 'CONSENT', name: l('Marketing-Einwilligung', 'Marketing Consent'), description: l('Einwilligung fuer Werbung', 'Consent for advertising'), purpose: l('Marketing-Kommunikation', 'Marketing communication'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Einwilligung', 'Voluntary consent'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Double-Opt-In'], tags: ['consent', 'marketing'] },
|
||||
{ id: 'dp-d4-data-sharing', code: 'D4', category: 'CONSENT', name: l('Datenweitergabe-Einwilligung', 'Data Sharing Consent'), description: l('Einwilligung zur Datenweitergabe', 'Consent to data sharing'), purpose: l('Weitergabe an Partner', 'Sharing with partners'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Einwilligung', 'Voluntary consent'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Dokumentation'], tags: ['consent', 'sharing'] },
|
||||
{ id: 'dp-d5-locale-preferences', code: 'D5', category: 'CONSENT', name: l('Sprach-/Regionspraeferenz', 'Language/Region Preference'), description: l('Bevorzugte Sprache und Region', 'Preferred language and region'), purpose: l('Lokalisierung', 'Localization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nutzereinstellungen', 'User settings'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['First-Party Cookie'], tags: ['preferences', 'locale'] },
|
||||
{ id: 'dp-d6-newsletter-consent', code: 'D6', category: 'CONSENT', name: l('Newsletter-Einwilligung', 'Newsletter Consent'), description: l('Einwilligung fuer Newsletter', 'Newsletter subscription consent'), purpose: l('E-Mail-Marketing', 'Email marketing'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Double-Opt-In erforderlich', 'Double opt-in required'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['Double-Opt-In', 'Abmelde-Link'], tags: ['consent', 'newsletter'] },
|
||||
|
||||
// KATEGORIE E: KOMMUNIKATIONSDATEN (7)
|
||||
{ id: 'dp-e1-support-tickets', code: 'E1', category: 'COMMUNICATION', name: l('Support-Tickets', 'Support Tickets'), description: l('Inhalt von Kundenanfragen', 'Content of customer inquiries'), purpose: l('Kundenservice', 'Customer service'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Supportleistungen', 'For support services'), retentionPeriod: '24_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Helpdesk-Software'], technicalMeasures: ['Verschluesselung'], tags: ['support', 'communication'] },
|
||||
{ id: 'dp-e2-chat-history', code: 'E2', category: 'COMMUNICATION', name: l('Chat-Verlaeufe', 'Chat Histories'), description: l('Live-Chat und Chatbot-Verlaeufe', 'Live chat and chatbot histories'), purpose: l('Kundenservice, QA', 'Customer service, QA'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicequalitaet', 'Service quality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Qualitaetssicherung', 'Quality assurance'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Chat-Software'], technicalMeasures: ['Pseudonymisierung'], tags: ['support', 'chat'] },
|
||||
{ id: 'dp-e3-call-recordings', code: 'E3', category: 'COMMUNICATION', name: l('Anrufaufzeichnungen', 'Call Recordings'), description: l('Aufzeichnungen von Telefonaten', 'Recordings of phone calls'), purpose: l('Qualitaetssicherung, Schulung', 'Quality assurance, training'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung vor Aufzeichnung', 'Explicit consent before recording'), retentionPeriod: '90_DAYS', retentionJustification: l('Begrenzte Qualitaetspruefung', 'Limited quality review'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Telefonie-Anbieter'], technicalMeasures: ['Verschluesselung', 'Auto-Loeschung'], tags: ['support', 'recording'] },
|
||||
{ id: 'dp-e4-email-content', code: 'E4', category: 'COMMUNICATION', name: l('E-Mail-Inhalte', 'Email Content'), description: l('Inhalt von E-Mail-Korrespondenz', 'Content of email correspondence'), purpose: l('Kommunikation, Dokumentation', 'Communication, documentation'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Kommunikation', 'For communication'), retentionPeriod: '24_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['TLS', 'Archivierung'], tags: ['communication', 'email'] },
|
||||
{ id: 'dp-e5-feedback', code: 'E5', category: 'COMMUNICATION', name: l('Feedback/Bewertungen', 'Feedback/Reviews'), description: l('Nutzerbewertungen und Feedback', 'User ratings and feedback'), purpose: l('Qualitaetsmessung', 'Quality measurement'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktqualitaet', 'Product quality'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['feedback', 'quality'] },
|
||||
{ id: 'dp-e6-notifications', code: 'E6', category: 'COMMUNICATION', name: l('Benachrichtigungsverlauf', 'Notification History'), description: l('Historie gesendeter Benachrichtigungen', 'History of sent notifications'), purpose: l('Nachvollziehbarkeit', 'Traceability'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support-Zwecke', 'Support purposes'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Logging'], tags: ['communication', 'notifications'] },
|
||||
{ id: 'dp-e7-forum-posts', code: 'E7', category: 'COMMUNICATION', name: l('Forum-/Community-Beitraege', 'Forum/Community Posts'), description: l('Beitraege in Foren oder Communities', 'Posts in forums or communities'), purpose: l('Community-Interaktion', 'Community interaction'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Community-Nutzung', 'Community usage'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Moderation'], tags: ['community', 'content'] },
|
||||
|
||||
// KATEGORIE F: ZAHLUNGSDATEN (8)
|
||||
{ id: 'dp-f1-billing-address', code: 'F1', category: 'PAYMENT', name: l('Rechnungsadresse', 'Billing Address'), description: l('Vollstaendige Rechnungsanschrift', 'Complete billing address'), purpose: l('Rechnungsstellung, Steuer', 'Invoicing, tax'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO, §257 HGB', '§147 AO, §257 HGB'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Verschluesselung', 'Archivierung'], tags: ['payment', 'billing'] },
|
||||
{ id: 'dp-f2-payment-token', code: 'F2', category: 'PAYMENT', name: l('Zahlungs-Token', 'Payment Token'), description: l('Tokenisierte Zahlungsinformationen', 'Tokenized payment information'), purpose: l('Wiederkehrende Zahlungen', 'Recurring payments'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Zahlungsabwicklung', 'For payment processing'), retentionPeriod: '36_MONTHS', retentionJustification: l('Kundenbeziehung plus Rueckbuchungsfrist', 'Customer relationship plus chargeback period'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Payment Provider'], technicalMeasures: ['PCI-DSS', 'Tokenisierung'], tags: ['payment', 'token'] },
|
||||
{ id: 'dp-f3-transactions', code: 'F3', category: 'PAYMENT', name: l('Transaktionshistorie', 'Transaction History'), description: l('Historie aller Transaktionen', 'History of all transactions'), purpose: l('Buchfuehrung, Nachweis', 'Accounting, proof'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO', '§147 AO'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater', 'Wirtschaftspruefer'], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['payment', 'transactions'] },
|
||||
{ id: 'dp-f4-iban', code: 'F4', category: 'PAYMENT', name: l('IBAN/Bankverbindung', 'IBAN/Bank Details'), description: l('Bankverbindung fuer Lastschrift', 'Bank details for direct debit'), purpose: l('Lastschrifteinzug', 'Direct debit'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer SEPA-Lastschrift', 'For SEPA direct debit'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Bank'], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['payment', 'bank'] },
|
||||
{ id: 'dp-f5-invoices', code: 'F5', category: 'PAYMENT', name: l('Rechnungen', 'Invoices'), description: l('Ausgestellte Rechnungen', 'Issued invoices'), purpose: l('Buchfuehrung, Steuer', 'Accounting, tax'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO, §257 HGB', '§147 AO, §257 HGB'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['payment', 'invoices'] },
|
||||
{ id: 'dp-f6-tax-id', code: 'F6', category: 'PAYMENT', name: l('USt-IdNr./Steuernummer', 'VAT ID/Tax Number'), description: l('Umsatzsteuer-ID oder Steuernummer', 'VAT ID or tax number'), purpose: l('Steuerliche Dokumentation', 'Tax documentation'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Steuerrecht', 'Tax law'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Verschluesselung'], tags: ['payment', 'tax'] },
|
||||
{ id: 'dp-f7-subscription', code: 'F7', category: 'PAYMENT', name: l('Abonnement-Daten', 'Subscription Data'), description: l('Abonnement-Details und Status', 'Subscription details and status'), purpose: l('Abonnementverwaltung', 'Subscription management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Vertragserfuellung', 'For contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['payment', 'subscription'] },
|
||||
{ id: 'dp-f8-refunds', code: 'F8', category: 'PAYMENT', name: l('Erstattungen', 'Refunds'), description: l('Erstattungshistorie', 'Refund history'), purpose: l('Buchfuehrung', 'Accounting'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('§147 AO', '§147 AO'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Steuerberater'], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['payment', 'refunds'] },
|
||||
|
||||
// KATEGORIE G: NUTZUNGSDATEN (8)
|
||||
{ id: 'dp-g1-session-duration', code: 'G1', category: 'USAGE_DATA', name: l('Sitzungsdauer', 'Session Duration'), description: l('Dauer einzelner Sitzungen', 'Duration of individual sessions'), purpose: l('Nutzungsanalyse', 'Usage analysis'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['usage', 'analytics'] },
|
||||
{ id: 'dp-g2-page-views', code: 'G2', category: 'USAGE_DATA', name: l('Seitenaufrufe', 'Page Views'), description: l('Aufgerufene Seiten', 'Visited pages'), purpose: l('Nutzungsanalyse', 'Usage analysis'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['usage', 'analytics'] },
|
||||
{ id: 'dp-g3-click-paths', code: 'G3', category: 'USAGE_DATA', name: l('Klickpfade', 'Click Paths'), description: l('Navigationsverhalten', 'Navigation behavior'), purpose: l('UX-Optimierung', 'UX optimization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '12_MONTHS', retentionJustification: l('UX-Analyse', 'UX analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Pseudonymisierung'], tags: ['usage', 'ux'] },
|
||||
{ id: 'dp-g4-search-queries', code: 'G4', category: 'USAGE_DATA', name: l('Suchanfragen', 'Search Queries'), description: l('Interne Suchanfragen', 'Internal search queries'), purpose: l('Suchoptimierung', 'Search optimization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '90_DAYS', retentionJustification: l('Kurzfristige Analyse', 'Short-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Anonymisierung'], tags: ['usage', 'search'] },
|
||||
{ id: 'dp-g5-feature-usage', code: 'G5', category: 'USAGE_DATA', name: l('Feature-Nutzung', 'Feature Usage'), description: l('Nutzung einzelner Features', 'Usage of individual features'), purpose: l('Produktentwicklung', 'Product development'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['usage', 'features'] },
|
||||
{ id: 'dp-g6-error-logs', code: 'G6', category: 'USAGE_DATA', name: l('Fehlerprotokolle', 'Error Logs'), description: l('Client-seitige Fehler', 'Client-side errors'), purpose: l('Fehlerbehebung', 'Bug fixing'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Qualitaetssicherung', 'Quality assurance'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Error-Tracking-Dienst'], technicalMeasures: ['Pseudonymisierung'], tags: ['usage', 'errors'] },
|
||||
{ id: 'dp-g7-preferences', code: 'G7', category: 'USAGE_DATA', name: l('Nutzereinstellungen', 'User Preferences'), description: l('Individuelle Einstellungen', 'Individual settings'), purpose: l('Personalisierung', 'Personalization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange Konto aktiv', 'While account active'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['usage', 'preferences'] },
|
||||
{ id: 'dp-g8-ab-tests', code: 'G8', category: 'USAGE_DATA', name: l('A/B-Test-Zuordnung', 'A/B Test Assignment'), description: l('Zuordnung zu Testvarianten', 'Assignment to test variants'), purpose: l('Produktoptimierung', 'Product optimization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktverbesserung', 'Product improvement'), retentionPeriod: '90_DAYS', retentionJustification: l('Testdauer', 'Test duration'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Pseudonymisierung'], tags: ['usage', 'testing'] },
|
||||
|
||||
// KATEGORIE H: STANDORTDATEN (7)
|
||||
{ id: 'dp-h1-gps', code: 'H1', category: 'LOCATION', name: l('GPS-Standort', 'GPS Location'), description: l('Praeziser GPS-Standort', 'Precise GPS location'), purpose: l('Standortbasierte Dienste', 'Location-based services'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung', 'Explicit consent'), retentionPeriod: '30_DAYS', retentionJustification: l('Datensparsamkeit', 'Data minimization'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'On-Device'], tags: ['location', 'gps'] },
|
||||
{ id: 'dp-h2-ip-geo', code: 'H2', category: 'LOCATION', name: l('IP-Geolokation', 'IP Geolocation'), description: l('Ungefaehrer Standort aus IP', 'Approximate location from IP'), purpose: l('Regionalisierung', 'Regionalization'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '90_DAYS', retentionJustification: l('Sicherheitsanalyse', 'Security analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Nur Landesebene'], tags: ['location', 'ip'] },
|
||||
{ id: 'dp-h3-timezone', code: 'H3', category: 'LOCATION', name: l('Zeitzone', 'Timezone'), description: l('Zeitzone des Nutzers', 'User timezone'), purpose: l('Lokalisierung', 'Localization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Nutzereinstellung', 'User setting'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['location', 'timezone'] },
|
||||
{ id: 'dp-h4-location-history', code: 'H4', category: 'LOCATION', name: l('Standortverlauf', 'Location History'), description: l('Historie von Standorten', 'History of locations'), purpose: l('Personalisierung', 'Personalization'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Funktion', 'Optional feature'), retentionPeriod: '30_DAYS', retentionJustification: l('Datensparsamkeit', 'Data minimization'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['location', 'history'] },
|
||||
{ id: 'dp-h5-country', code: 'H5', category: 'LOCATION', name: l('Herkunftsland', 'Country of Origin'), description: l('Land basierend auf IP', 'Country based on IP'), purpose: l('Compliance, Geo-Blocking', 'Compliance, geo-blocking'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Compliance', 'Compliance'), retentionPeriod: '12_MONTHS', retentionJustification: l('Sicherheit', 'Security'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['location', 'country'] },
|
||||
{ id: 'dp-h6-wifi-networks', code: 'H6', category: 'LOCATION', name: l('WLAN-Netzwerke', 'WiFi Networks'), description: l('Erkannte WLAN-Netzwerke', 'Detected WiFi networks'), purpose: l('Standortbestimmung', 'Location detection'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Nur mit Einwilligung', 'Only with consent'), retentionPeriod: '24_HOURS', retentionJustification: l('Kurzlebig', 'Short-lived'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['On-Device'], tags: ['location', 'wifi'] },
|
||||
{ id: 'dp-h7-travel-info', code: 'H7', category: 'LOCATION', name: l('Reiseinformationen', 'Travel Information'), description: l('Reiseziele und Plaene', 'Travel destinations and plans'), purpose: l('Reiseservices', 'Travel services'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer Reisedienste', 'For travel services'), retentionPeriod: '12_MONTHS', retentionJustification: l('Serviceerbringung', 'Service delivery'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Reiseanbieter'], technicalMeasures: ['Verschluesselung'], tags: ['location', 'travel'] },
|
||||
|
||||
// KATEGORIE I: GERAETEDATEN (10)
|
||||
{ id: 'dp-i1-ip-address', code: 'I1', category: 'DEVICE_DATA', name: l('IP-Adresse', 'IP Address'), description: l('IP-Adresse des Nutzers', 'User IP address'), purpose: l('Sicherheit, Routing', 'Security, routing'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '90_DAYS', retentionJustification: l('Sicherheitsanalyse', 'Security analysis'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Security-Monitoring'], technicalMeasures: ['IP-Anonymisierung'], tags: ['device', 'network'] },
|
||||
{ id: 'dp-i2-fingerprint', code: 'I2', category: 'DEVICE_DATA', name: l('Device Fingerprint', 'Device Fingerprint'), description: l('Hash aus Geraetemerkmalen', 'Hash of device characteristics'), purpose: l('Betrugspraevention', 'Fraud prevention'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Betrugspraevention', 'Fraud prevention'), retentionPeriod: '30_DAYS', retentionJustification: l('Kurze Speicherung', 'Short storage'), cookieCategory: 'ESSENTIAL', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Einweg-Hashing'], tags: ['device', 'fingerprint'] },
|
||||
{ id: 'dp-i3-browser', code: 'I3', category: 'DEVICE_DATA', name: l('Browser/User-Agent', 'Browser/User Agent'), description: l('Browser und Version', 'Browser and version'), purpose: l('Kompatibilitaet', 'Compatibility'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch notwendig', 'Technically necessary'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support', 'Support'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'browser'] },
|
||||
{ id: 'dp-i4-os', code: 'I4', category: 'DEVICE_DATA', name: l('Betriebssystem', 'Operating System'), description: l('OS und Version', 'OS and version'), purpose: l('Kompatibilitaet', 'Compatibility'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch notwendig', 'Technically necessary'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support', 'Support'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'os'] },
|
||||
{ id: 'dp-i5-screen', code: 'I5', category: 'DEVICE_DATA', name: l('Bildschirmaufloesung', 'Screen Resolution'), description: l('Bildschirmgroesse', 'Screen size'), purpose: l('Responsive Design', 'Responsive design'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('UX-Optimierung', 'UX optimization'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analytics', 'Analytics'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'screen'] },
|
||||
{ id: 'dp-i6-language', code: 'I6', category: 'DEVICE_DATA', name: l('Browser-Sprache', 'Browser Language'), description: l('Spracheinstellung des Browsers', 'Browser language setting'), purpose: l('Lokalisierung', 'Localization'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Servicefunktionalitaet', 'Service functionality'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nutzereinstellung', 'User setting'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'language'] },
|
||||
{ id: 'dp-i7-push-token', code: 'I7', category: 'DEVICE_DATA', name: l('Push-Token', 'Push Token'), description: l('Token fuer Push-Nachrichten', 'Token for push notifications'), purpose: l('Push-Benachrichtigungen', 'Push notifications'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Opt-In fuer Push', 'Opt-in for push'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Deaktivierung', 'Until deactivation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Push-Dienst'], technicalMeasures: ['Verschluesselung'], tags: ['device', 'push'] },
|
||||
{ id: 'dp-i8-device-id', code: 'I8', category: 'DEVICE_DATA', name: l('Geraete-ID', 'Device ID'), description: l('Eindeutige Geraetekennung', 'Unique device identifier'), purpose: l('Geraeteverwaltung', 'Device management'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Multi-Device-Support', 'Multi-device support'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange verknuepft', 'While linked'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Hashing'], tags: ['device', 'id'] },
|
||||
{ id: 'dp-i9-app-version', code: 'I9', category: 'DEVICE_DATA', name: l('App-Version', 'App Version'), description: l('Installierte App-Version', 'Installed app version'), purpose: l('Support, Updates', 'Support, updates'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Technisch notwendig', 'Technically necessary'), retentionPeriod: '12_MONTHS', retentionJustification: l('Support', 'Support'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'app'] },
|
||||
{ id: 'dp-i10-hardware', code: 'I10', category: 'DEVICE_DATA', name: l('Hardware-Info', 'Hardware Info'), description: l('Geraetetyp, Hersteller', 'Device type, manufacturer'), purpose: l('Kompatibilitaet', 'Compatibility'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Produktentwicklung', 'Product development'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analytics', 'Analytics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['device', 'hardware'] },
|
||||
|
||||
// KATEGORIE J: MARKETINGDATEN (8)
|
||||
{ id: 'dp-j1-tracking-pixel', code: 'J1', category: 'MARKETING', name: l('Tracking-Pixel', 'Tracking Pixel'), description: l('Conversion-Tracking-Pixel', 'Conversion tracking pixel'), purpose: l('Werbemessung', 'Ad measurement'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent erforderlich', 'Cookie consent required'), retentionPeriod: '90_DAYS', retentionJustification: l('Conversion-Fenster', 'Conversion window'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Google Ads', 'Meta'], technicalMeasures: ['Nur bei Consent'], tags: ['marketing', 'tracking'] },
|
||||
{ id: 'dp-j2-advertising-id', code: 'J2', category: 'MARKETING', name: l('Werbe-ID', 'Advertising ID'), description: l('Geraetuebergreifende Werbe-ID', 'Cross-device advertising ID'), purpose: l('Personalisierte Werbung', 'Personalized advertising'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Wegen Profilbildung', 'Due to profiling'), retentionPeriod: '90_DAYS', retentionJustification: l('Kampagnenzeitraum', 'Campaign period'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Werbenetzwerke'], technicalMeasures: ['Opt-out'], tags: ['marketing', 'advertising'] },
|
||||
{ id: 'dp-j3-utm', code: 'J3', category: 'MARKETING', name: l('UTM-Parameter', 'UTM Parameters'), description: l('Kampagnen-Tracking-Parameter', 'Campaign tracking parameters'), purpose: l('Kampagnen-Attribution', 'Campaign attribution'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Kampagnenmessung', 'Campaign measurement'), retentionPeriod: '30_DAYS', retentionJustification: l('Session-Attribution', 'Session attribution'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Analytics'], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'utm'] },
|
||||
{ id: 'dp-j4-newsletter', code: 'J4', category: 'MARKETING', name: l('Newsletter-Daten', 'Newsletter Data'), description: l('E-Mail und Praeferenzen', 'Email and preferences'), purpose: l('Newsletter-Versand', 'Newsletter delivery'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Double-Opt-In', 'Double opt-in'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Abmeldung', 'Until unsubscribe'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['Double-Opt-In'], tags: ['marketing', 'newsletter'] },
|
||||
{ id: 'dp-j5-remarketing', code: 'J5', category: 'MARKETING', name: l('Remarketing-Listen', 'Remarketing Lists'), description: l('Zielgruppen fuer Remarketing', 'Audiences for remarketing'), purpose: l('Remarketing', 'Remarketing'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Profilbildung', 'Profiling'), retentionPeriod: '90_DAYS', retentionJustification: l('Kampagnenzeitraum', 'Campaign period'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Werbenetzwerke'], technicalMeasures: ['Hashing'], tags: ['marketing', 'remarketing'] },
|
||||
{ id: 'dp-j6-email-opens', code: 'J6', category: 'MARKETING', name: l('E-Mail-Oeffnungen', 'Email Opens'), description: l('Oeffnungsraten von E-Mails', 'Email open rates'), purpose: l('E-Mail-Optimierung', 'Email optimization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Teil der Newsletter-Einwilligung', 'Part of newsletter consent'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analyse', 'Analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['E-Mail-Provider'], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'email'] },
|
||||
{ id: 'dp-j7-ad-clicks', code: 'J7', category: 'MARKETING', name: l('Anzeigen-Klicks', 'Ad Clicks'), description: l('Klicks auf Werbeanzeigen', 'Clicks on advertisements'), purpose: l('Werbemessung', 'Ad measurement'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Teil der Werbe-Einwilligung', 'Part of advertising consent'), retentionPeriod: '90_DAYS', retentionJustification: l('Conversion-Fenster', 'Conversion window'), cookieCategory: 'PERSONALIZATION', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Werbenetzwerke'], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'advertising'] },
|
||||
{ id: 'dp-j8-referrer', code: 'J8', category: 'MARKETING', name: l('Referrer-URL', 'Referrer URL'), description: l('Herkunftsseite', 'Source page'), purpose: l('Traffic-Analyse', 'Traffic analysis'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Marketing-Attribution', 'Marketing attribution'), retentionPeriod: '30_DAYS', retentionJustification: l('Kurzfristige Analyse', 'Short-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['marketing', 'referrer'] },
|
||||
|
||||
// KATEGORIE K: ANALYSEDATEN (7)
|
||||
{ id: 'dp-k1-google-analytics', code: 'K1', category: 'ANALYTICS', name: l('Google Analytics', 'Google Analytics'), description: l('GA4-Analysedaten', 'GA4 analytics data'), purpose: l('Web-Analyse', 'Web analytics'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent erforderlich', 'Cookie consent required'), retentionPeriod: '26_MONTHS', retentionJustification: l('GA4-Standard', 'GA4 standard'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Google'], technicalMeasures: ['IP-Anonymisierung', 'Consent-Mode'], tags: ['analytics', 'ga4'] },
|
||||
{ id: 'dp-k2-heatmaps', code: 'K2', category: 'ANALYTICS', name: l('Heatmaps', 'Heatmaps'), description: l('Klick- und Scroll-Heatmaps', 'Click and scroll heatmaps'), purpose: l('UX-Analyse', 'UX analysis'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent erforderlich', 'Cookie consent required'), retentionPeriod: '12_MONTHS', retentionJustification: l('UX-Optimierung', 'UX optimization'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Hotjar/Clarity'], technicalMeasures: ['Anonymisierung'], tags: ['analytics', 'heatmaps'] },
|
||||
{ id: 'dp-k3-session-recording', code: 'K3', category: 'ANALYTICS', name: l('Session-Recordings', 'Session Recordings'), description: l('Aufzeichnung von Sitzungen', 'Recording of sessions'), purpose: l('UX-Analyse, Debugging', 'UX analysis, debugging'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung', 'Explicit consent'), retentionPeriod: '90_DAYS', retentionJustification: l('Begrenzte Aufbewahrung', 'Limited retention'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Recording-Dienst'], technicalMeasures: ['Passwort-Maskierung', 'PII-Filterung'], tags: ['analytics', 'recording'] },
|
||||
{ id: 'dp-k4-events', code: 'K4', category: 'ANALYTICS', name: l('Event-Tracking', 'Event Tracking'), description: l('Benutzerdefinierte Events', 'Custom events'), purpose: l('Produktanalyse', 'Product analysis'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'events'] },
|
||||
{ id: 'dp-k5-conversion', code: 'K5', category: 'ANALYTICS', name: l('Conversion-Daten', 'Conversion Data'), description: l('Konversions-Events', 'Conversion events'), purpose: l('Conversion-Optimierung', 'Conversion optimization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Business-Analyse', 'Business analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'conversion'] },
|
||||
{ id: 'dp-k6-funnel', code: 'K6', category: 'ANALYTICS', name: l('Funnel-Analyse', 'Funnel Analysis'), description: l('Trichterdaten', 'Funnel data'), purpose: l('Conversion-Optimierung', 'Conversion optimization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Business-Analyse', 'Business analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'funnel'] },
|
||||
{ id: 'dp-k7-cohort', code: 'K7', category: 'ANALYTICS', name: l('Kohorten-Analyse', 'Cohort Analysis'), description: l('Kohortenbasierte Daten', 'Cohort-based data'), purpose: l('Nutzeranalyse', 'User analysis'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Cookie-Consent', 'Cookie consent'), retentionPeriod: '26_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: 'PERFORMANCE', isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Analytics-Dienst'], technicalMeasures: ['Aggregierung'], tags: ['analytics', 'cohort'] },
|
||||
|
||||
// KATEGORIE L: SOCIAL-MEDIA-DATEN (6)
|
||||
{ id: 'dp-l1-profile-id', code: 'L1', category: 'SOCIAL_MEDIA', name: l('Social-Profil-ID', 'Social Profile ID'), description: l('ID aus Social Login', 'ID from social login'), purpose: l('Social Login', 'Social login'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Social Login', 'Voluntary social login'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange verknuepft', 'While linked'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Social-Network'], technicalMeasures: ['Minimale Daten'], tags: ['social', 'login'] },
|
||||
{ id: 'dp-l2-avatar', code: 'L2', category: 'SOCIAL_MEDIA', name: l('Social-Avatar', 'Social Avatar'), description: l('Profilbild aus Social Network', 'Profile picture from social network'), purpose: l('Personalisierung', 'Personalization'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Import', 'Voluntary import'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange gewuenscht', 'While desired'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Lokale Kopie'], tags: ['social', 'avatar'] },
|
||||
{ id: 'dp-l3-connections', code: 'L3', category: 'SOCIAL_MEDIA', name: l('Social-Verbindungen', 'Social Connections'), description: l('Freunde/Follower', 'Friends/followers'), purpose: l('Social Features', 'Social features'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliger Import', 'Voluntary import'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: ['Social-Network'], technicalMeasures: ['Minimale Daten'], tags: ['social', 'connections'] },
|
||||
{ id: 'dp-l4-shares', code: 'L4', category: 'SOCIAL_MEDIA', name: l('Geteilte Inhalte', 'Shared Content'), description: l('Auf Social Media geteilte Inhalte', 'Content shared on social media'), purpose: l('Social Sharing', 'Social sharing'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliges Teilen', 'Voluntary sharing'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Social-Networks'], technicalMeasures: ['Logging'], tags: ['social', 'sharing'] },
|
||||
{ id: 'dp-l5-likes', code: 'L5', category: 'SOCIAL_MEDIA', name: l('Likes/Reaktionen', 'Likes/Reactions'), description: l('Social-Media-Interaktionen', 'Social media interactions'), purpose: l('Social Features', 'Social features'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Teil des Services', 'Part of service'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Nutzerfunktion', 'User feature'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['social', 'interactions'] },
|
||||
{ id: 'dp-l6-oauth-tokens', code: 'L6', category: 'SOCIAL_MEDIA', name: l('OAuth-Tokens', 'OAuth Tokens'), description: l('Zugangs-Token fuer Social APIs', 'Access tokens for social APIs'), purpose: l('API-Zugriff', 'API access'), riskLevel: 'MEDIUM', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwillige Verknuepfung', 'Voluntary linking'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Social-Network'], technicalMeasures: ['Verschluesselung', 'Token-Rotation'], tags: ['social', 'oauth'] },
|
||||
|
||||
// KATEGORIE M: GESUNDHEITSDATEN (7) - ART. 9 DSGVO!
|
||||
{ id: 'dp-m1-health-status', code: 'M1', category: 'HEALTH_DATA', name: l('Gesundheitszustand', 'Health Status'), description: l('Allgemeiner Gesundheitszustand', 'General health status'), purpose: l('Gesundheitsdienste', 'Health services'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO - Ausdrueckliche Einwilligung', 'Art. 9(2)(a) GDPR - Explicit consent'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Aufbewahrung', 'Medical retention'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung', 'Zugriffskontrolle', 'Audit-Logging'], tags: ['health', 'article9', 'sensitive'] },
|
||||
{ id: 'dp-m2-fitness-data', code: 'M2', category: 'HEALTH_DATA', name: l('Fitnessdaten', 'Fitness Data'), description: l('Schritte, Kalorien, Aktivitaet', 'Steps, calories, activity'), purpose: l('Fitness-Tracking', 'Fitness tracking'), riskLevel: 'MEDIUM', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeit-Tracking', 'Long-term tracking'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: ['Fitness-App'], technicalMeasures: ['Verschluesselung', 'Pseudonymisierung'], tags: ['health', 'fitness', 'article9'] },
|
||||
{ id: 'dp-m3-medication', code: 'M3', category: 'HEALTH_DATA', name: l('Medikation', 'Medication'), description: l('Aktuelle Medikamente', 'Current medications'), purpose: l('Gesundheitsmanagement', 'Health management'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Dokumentation', 'Medical documentation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung', 'Strenge Zugriffskontrolle'], tags: ['health', 'medication', 'article9'] },
|
||||
{ id: 'dp-m4-biometric', code: 'M4', category: 'HEALTH_DATA', name: l('Biometrische Daten', 'Biometric Data'), description: l('Fingerabdruck, Face-ID (zur Identifikation)', 'Fingerprint, Face ID (for identification)'), purpose: l('Biometrische Authentifizierung', 'Biometric authentication'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: 'UNTIL_ACCOUNT_DELETION', retentionJustification: l('Solange gewuenscht', 'While desired'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['On-Device-Speicherung', 'Keine Cloud-Uebertragung'], tags: ['health', 'biometric', 'article9'] },
|
||||
{ id: 'dp-m5-allergies', code: 'M5', category: 'HEALTH_DATA', name: l('Allergien', 'Allergies'), description: l('Bekannte Allergien', 'Known allergies'), purpose: l('Gesundheitsschutz', 'Health protection'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Dokumentation', 'Medical documentation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung'], tags: ['health', 'allergies', 'article9'] },
|
||||
{ id: 'dp-m6-vital-signs', code: 'M6', category: 'HEALTH_DATA', name: l('Vitalzeichen', 'Vital Signs'), description: l('Blutdruck, Puls, etc.', 'Blood pressure, pulse, etc.'), purpose: l('Gesundheitsmonitoring', 'Health monitoring'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: '10_YEARS', retentionJustification: l('Medizinische Dokumentation', 'Medical documentation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Ende-zu-Ende-Verschluesselung', 'Audit-Logging'], tags: ['health', 'vitals', 'article9'] },
|
||||
{ id: 'dp-m7-disability', code: 'M7', category: 'HEALTH_DATA', name: l('Behinderung/Einschraenkung', 'Disability/Impairment'), description: l('Informationen zu Behinderungen', 'Information about disabilities'), purpose: l('Barrierefreiheit', 'Accessibility'), riskLevel: 'HIGH', legalBasis: 'EXPLICIT_CONSENT', legalBasisJustification: l('Art. 9 Abs. 2 lit. a DSGVO', 'Art. 9(2)(a) GDPR'), retentionPeriod: 'UNTIL_REVOCATION', retentionJustification: l('Bis Widerruf', 'Until revocation'), cookieCategory: null, isSpecialCategory: true, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Strenge Zugriffskontrolle'], tags: ['health', 'disability', 'article9'] },
|
||||
|
||||
// KATEGORIE N: BESCHAEFTIGTENDATEN (10) - BDSG § 26
|
||||
{ id: 'dp-n1-employee-id', code: 'N1', category: 'EMPLOYEE_DATA', name: l('Personalnummer', 'Employee ID'), description: l('Eindeutige Mitarbeiter-ID', 'Unique employee ID'), purpose: l('Personalverwaltung', 'HR management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26 - Beschaeftigungsverhaeltnis', 'BDSG § 26 - Employment relationship'), retentionPeriod: '10_YEARS', retentionJustification: l('Aufbewahrungspflichten', 'Retention obligations'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'hr'] },
|
||||
{ id: 'dp-n2-salary', code: 'N2', category: 'EMPLOYEE_DATA', name: l('Gehalt/Verguetung', 'Salary/Compensation'), description: l('Gehaltsinformationen', 'Salary information'), purpose: l('Lohnabrechnung', 'Payroll'), riskLevel: 'HIGH', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Lohnbuero', 'Finanzamt'], technicalMeasures: ['Verschluesselung', 'Strenge Zugriffskontrolle'], tags: ['employee', 'payroll'] },
|
||||
{ id: 'dp-n3-tax-id', code: 'N3', category: 'EMPLOYEE_DATA', name: l('Steuer-ID', 'Tax ID'), description: l('Steueridentifikationsnummer', 'Tax identification number'), purpose: l('Lohnsteuer', 'Payroll tax'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Steuerrecht', 'Tax law'), retentionPeriod: '10_YEARS', retentionJustification: l('Steuerliche Aufbewahrung', 'Tax retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Finanzamt'], technicalMeasures: ['Verschluesselung'], tags: ['employee', 'tax'] },
|
||||
{ id: 'dp-n4-social-security', code: 'N4', category: 'EMPLOYEE_DATA', name: l('Sozialversicherungsnummer', 'Social Security Number'), description: l('SV-Nummer', 'Social security number'), purpose: l('Sozialversicherung', 'Social security'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Sozialversicherungsrecht', 'Social security law'), retentionPeriod: '10_YEARS', retentionJustification: l('Gesetzliche Pflicht', 'Legal obligation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Krankenkasse', 'Rentenversicherung'], technicalMeasures: ['Verschluesselung'], tags: ['employee', 'social-security'] },
|
||||
{ id: 'dp-n5-working-hours', code: 'N5', category: 'EMPLOYEE_DATA', name: l('Arbeitszeiten', 'Working Hours'), description: l('Erfasste Arbeitszeiten', 'Recorded working hours'), purpose: l('Arbeitszeiterfassung', 'Time tracking'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('ArbZG', 'Working Time Act'), retentionPeriod: '6_YEARS', retentionJustification: l('Gesetzliche Aufbewahrung', 'Legal retention'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'time-tracking'] },
|
||||
{ id: 'dp-n6-vacation', code: 'N6', category: 'EMPLOYEE_DATA', name: l('Urlaubsdaten', 'Vacation Data'), description: l('Urlaubsanspruch und -nutzung', 'Vacation entitlement and usage'), purpose: l('Urlaubsverwaltung', 'Vacation management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'vacation'] },
|
||||
{ id: 'dp-n7-sick-leave', code: 'N7', category: 'EMPLOYEE_DATA', name: l('Krankheitstage', 'Sick Leave'), description: l('Krankheitstage (ohne Diagnose)', 'Sick days (without diagnosis)'), purpose: l('Personalplanung', 'HR planning'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Lohnfortzahlung', 'Sick pay'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Krankenkasse'], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'sick-leave'] },
|
||||
{ id: 'dp-n8-performance', code: 'N8', category: 'EMPLOYEE_DATA', name: l('Leistungsbeurteilung', 'Performance Review'), description: l('Mitarbeiterbeurteilungen', 'Employee evaluations'), purpose: l('Personalentwicklung', 'HR development'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Personalakte', 'Personnel file'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'performance'] },
|
||||
{ id: 'dp-n9-training', code: 'N9', category: 'EMPLOYEE_DATA', name: l('Schulungen/Weiterbildung', 'Training/Development'), description: l('Absolvierte Schulungen', 'Completed training'), purpose: l('Personalentwicklung', 'HR development'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '6_YEARS', retentionJustification: l('Personalakte', 'Personnel file'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['employee', 'training'] },
|
||||
{ id: 'dp-n10-contract', code: 'N10', category: 'EMPLOYEE_DATA', name: l('Arbeitsvertrag', 'Employment Contract'), description: l('Arbeitsvertragsdaten', 'Employment contract data'), purpose: l('Vertragsverwaltung', 'Contract management'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('BDSG § 26', 'BDSG § 26'), retentionPeriod: '10_YEARS', retentionJustification: l('Verjaehrungsfristen', 'Limitation periods'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Archivierung'], tags: ['employee', 'contract'] },
|
||||
|
||||
// KATEGORIE O: VERTRAGSDATEN (7)
|
||||
{ id: 'dp-o1-contract-number', code: 'O1', category: 'CONTRACT_DATA', name: l('Vertragsnummer', 'Contract Number'), description: l('Eindeutige Vertragsnummer', 'Unique contract number'), purpose: l('Vertragsverwaltung', 'Contract management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contract', 'id'] },
|
||||
{ id: 'dp-o2-contract-duration', code: 'O2', category: 'CONTRACT_DATA', name: l('Vertragslaufzeit', 'Contract Duration'), description: l('Start- und Enddatum', 'Start and end date'), purpose: l('Vertragsverwaltung', 'Contract management'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: [], tags: ['contract', 'duration'] },
|
||||
{ id: 'dp-o3-signature', code: 'O3', category: 'CONTRACT_DATA', name: l('Unterschrift', 'Signature'), description: l('Digitale oder gescannte Unterschrift', 'Digital or scanned signature'), purpose: l('Vertragsschluss', 'Contract conclusion'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('Beweissicherung', 'Evidence preservation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung', 'Integritaetsschutz'], tags: ['contract', 'signature'] },
|
||||
{ id: 'dp-o4-contract-documents', code: 'O4', category: 'CONTRACT_DATA', name: l('Vertragsdokumente', 'Contract Documents'), description: l('PDFs und Anlagen', 'PDFs and attachments'), purpose: l('Dokumentation', 'Documentation'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Revisionssichere Archivierung'], tags: ['contract', 'documents'] },
|
||||
{ id: 'dp-o5-contract-terms', code: 'O5', category: 'CONTRACT_DATA', name: l('Vertragskonditionen', 'Contract Terms'), description: l('Preise, Rabatte, Bedingungen', 'Prices, discounts, conditions'), purpose: l('Vertragsabwicklung', 'Contract processing'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Zugriffskontrolle'], tags: ['contract', 'terms'] },
|
||||
{ id: 'dp-o6-contract-history', code: 'O6', category: 'CONTRACT_DATA', name: l('Vertragshistorie', 'Contract History'), description: l('Aenderungen und Versionen', 'Changes and versions'), purpose: l('Nachvollziehbarkeit', 'Traceability'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Dokumentationspflicht', 'Documentation obligation'), retentionPeriod: '10_YEARS', retentionJustification: l('§ 147 AO', '§ 147 AO'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Versionierung'], tags: ['contract', 'history'] },
|
||||
{ id: 'dp-o7-termination', code: 'O7', category: 'CONTRACT_DATA', name: l('Kuendigungsdaten', 'Termination Data'), description: l('Kuendigungen und Gruende', 'Terminations and reasons'), purpose: l('Vertragsbeendigung', 'Contract termination'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Vertragserfuellung', 'Contract performance'), retentionPeriod: '10_YEARS', retentionJustification: l('Beweissicherung', 'Evidence preservation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Archivierung'], tags: ['contract', 'termination'] },
|
||||
|
||||
// KATEGORIE P: PROTOKOLLDATEN (7)
|
||||
{ id: 'dp-p1-login-logs', code: 'P1', category: 'LOG_DATA', name: l('Login-Protokolle', 'Login Logs'), description: l('Erfolgreiche und fehlgeschlagene Logins', 'Successful and failed logins'), purpose: l('Sicherheitsaudit', 'Security audit'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Sicherheitsforensik', 'Security forensics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SIEM'], technicalMeasures: ['Unveraenderbare Logs'], tags: ['logs', 'security'] },
|
||||
{ id: 'dp-p2-access-logs', code: 'P2', category: 'LOG_DATA', name: l('Zugriffsprotokolle', 'Access Logs'), description: l('HTTP-Zugriffe', 'HTTP accesses'), purpose: l('Sicherheit, Debugging', 'Security, debugging'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['IP-Anonymisierung'], tags: ['logs', 'access'] },
|
||||
{ id: 'dp-p3-api-logs', code: 'P3', category: 'LOG_DATA', name: l('API-Protokolle', 'API Logs'), description: l('API-Aufrufe', 'API calls'), purpose: l('Debugging, Monitoring', 'Debugging, monitoring'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicequalitaet', 'Service quality'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Pseudonymisierung'], tags: ['logs', 'api'] },
|
||||
{ id: 'dp-p4-admin-logs', code: 'P4', category: 'LOG_DATA', name: l('Admin-Aktionen', 'Admin Actions'), description: l('Protokoll von Admin-Aktivitaeten', 'Log of admin activities'), purpose: l('Revisionssicherheit', 'Audit compliance'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Dokumentationspflicht', 'Documentation obligation'), retentionPeriod: '6_YEARS', retentionJustification: l('Revisionssicherheit', 'Audit compliance'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Unveraenderbare Logs', 'Signatur'], tags: ['logs', 'admin'] },
|
||||
{ id: 'dp-p5-change-logs', code: 'P5', category: 'LOG_DATA', name: l('Aenderungshistorie', 'Change History'), description: l('Audit-Trail von Aenderungen', 'Audit trail of changes'), purpose: l('Nachvollziehbarkeit', 'Traceability'), riskLevel: 'LOW', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Dokumentationspflicht', 'Documentation obligation'), retentionPeriod: '6_YEARS', retentionJustification: l('Revisionssicherheit', 'Audit compliance'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Unveraenderbare Logs'], tags: ['logs', 'audit'] },
|
||||
{ id: 'dp-p6-error-logs', code: 'P6', category: 'LOG_DATA', name: l('Fehlerprotokolle', 'Error Logs'), description: l('System- und Anwendungsfehler', 'System and application errors'), purpose: l('Fehlerbehebung', 'Bug fixing'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Servicequalitaet', 'Service quality'), retentionPeriod: '90_DAYS', retentionJustification: l('Fehleranalyse', 'Error analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Error-Tracking'], technicalMeasures: ['PII-Filterung'], tags: ['logs', 'errors'] },
|
||||
{ id: 'dp-p7-security-logs', code: 'P7', category: 'LOG_DATA', name: l('Sicherheitsprotokolle', 'Security Logs'), description: l('Security Events', 'Security events'), purpose: l('Sicherheitsmonitoring', 'Security monitoring'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Forensik', 'Forensics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SIEM'], technicalMeasures: ['Unveraenderbare Logs'], tags: ['logs', 'security'] },
|
||||
|
||||
// KATEGORIE Q: KI-DATEN (7) - AI ACT
|
||||
{ id: 'dp-q1-ai-prompts', code: 'Q1', category: 'AI_DATA', name: l('KI-Prompts', 'AI Prompts'), description: l('Nutzereingaben an KI', 'User inputs to AI'), purpose: l('KI-Funktionalitaet', 'AI functionality'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer KI-Service', 'For AI service'), retentionPeriod: '90_DAYS', retentionJustification: l('Kontexterhaltung', 'Context preservation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['KI-Provider'], technicalMeasures: ['Keine Verwendung fuer Training', 'Verschluesselung'], tags: ['ai', 'prompts'] },
|
||||
{ id: 'dp-q2-ai-responses', code: 'Q2', category: 'AI_DATA', name: l('KI-Antworten', 'AI Responses'), description: l('Generierte KI-Antworten', 'Generated AI responses'), purpose: l('Qualitaetssicherung', 'Quality assurance'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer KI-Service', 'For AI service'), retentionPeriod: '90_DAYS', retentionJustification: l('QA', 'QA'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Logging'], tags: ['ai', 'responses'] },
|
||||
{ id: 'dp-q3-rag-context', code: 'Q3', category: 'AI_DATA', name: l('RAG-Kontext', 'RAG Context'), description: l('Retrieval-Kontext', 'Retrieval context'), purpose: l('Kontextuelle KI-Antworten', 'Contextual AI responses'), riskLevel: 'MEDIUM', legalBasis: 'CONTRACT', legalBasisJustification: l('Fuer RAG-Funktionalitaet', 'For RAG functionality'), retentionPeriod: '24_HOURS', retentionJustification: l('Session-Kontext', 'Session context'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['In-Memory', 'Auto-Loeschung'], tags: ['ai', 'rag'] },
|
||||
{ id: 'dp-q4-ai-feedback', code: 'Q4', category: 'AI_DATA', name: l('KI-Feedback', 'AI Feedback'), description: l('Nutzerfeedback zu KI-Antworten', 'User feedback on AI responses'), purpose: l('KI-Verbesserung', 'AI improvement'), riskLevel: 'LOW', legalBasis: 'CONSENT', legalBasisJustification: l('Freiwilliges Feedback', 'Voluntary feedback'), retentionPeriod: '24_MONTHS', retentionJustification: l('Qualitaetsanalyse', 'Quality analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Anonymisierung'], tags: ['ai', 'feedback'] },
|
||||
{ id: 'dp-q5-training-data', code: 'Q5', category: 'AI_DATA', name: l('Trainingsdaten (mit Einwilligung)', 'Training Data (with consent)'), description: l('Fuer KI-Training freigegebene Daten', 'Data released for AI training'), purpose: l('Modellverbesserung', 'Model improvement'), riskLevel: 'HIGH', legalBasis: 'CONSENT', legalBasisJustification: l('Ausdrueckliche Einwilligung', 'Explicit consent'), retentionPeriod: '36_MONTHS', retentionJustification: l('Modellentwicklung', 'Model development'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: true, thirdPartyRecipients: [], technicalMeasures: ['Anonymisierung', 'Zugriffskontrolle'], tags: ['ai', 'training'] },
|
||||
{ id: 'dp-q6-model-outputs', code: 'Q6', category: 'AI_DATA', name: l('Modell-Outputs', 'Model Outputs'), description: l('KI-generierte Inhalte', 'AI-generated content'), purpose: l('Dokumentation', 'Documentation'), riskLevel: 'LOW', legalBasis: 'CONTRACT', legalBasisJustification: l('Teil des Services', 'Part of service'), retentionPeriod: '12_MONTHS', retentionJustification: l('Nachvollziehbarkeit', 'Traceability'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Kennzeichnung als KI-generiert'], tags: ['ai', 'outputs'] },
|
||||
{ id: 'dp-q7-ai-usage', code: 'Q7', category: 'AI_DATA', name: l('KI-Nutzungsstatistik', 'AI Usage Statistics'), description: l('Aggregierte KI-Nutzungsdaten', 'Aggregated AI usage data'), purpose: l('Kapazitaetsplanung', 'Capacity planning'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Serviceoptimierung', 'Service optimization'), retentionPeriod: '24_MONTHS', retentionJustification: l('Langzeitanalyse', 'Long-term analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['ai', 'usage'] },
|
||||
|
||||
// KATEGORIE R: SICHERHEITSDATEN (7)
|
||||
{ id: 'dp-r1-failed-logins', code: 'R1', category: 'SECURITY', name: l('Fehlgeschlagene Logins', 'Failed Logins'), description: l('Fehlgeschlagene Anmeldeversuche', 'Failed login attempts'), purpose: l('Angriffserkennung', 'Attack detection'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Forensik', 'Forensics'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['SIEM'], technicalMeasures: ['Alerting', 'Rate-Limiting'], tags: ['security', 'auth'] },
|
||||
{ id: 'dp-r2-fraud-score', code: 'R2', category: 'SECURITY', name: l('Betrugsrisiko-Score', 'Fraud Risk Score'), description: l('Berechnetes Betrugsrisiko', 'Calculated fraud risk'), purpose: l('Betrugspraevention', 'Fraud prevention'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('Betrugspraevention', 'Fraud prevention'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analyse', 'Analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Maschinelles Lernen'], tags: ['security', 'fraud'] },
|
||||
{ id: 'dp-r3-incident-reports', code: 'R3', category: 'SECURITY', name: l('Vorfallberichte', 'Incident Reports'), description: l('Sicherheitsvorfaelle', 'Security incidents'), purpose: l('Vorfallmanagement', 'Incident management'), riskLevel: 'MEDIUM', legalBasis: 'LEGAL_OBLIGATION', legalBasisJustification: l('Art. 33 DSGVO Meldepflicht', 'Art. 33 GDPR notification obligation'), retentionPeriod: '6_YEARS', retentionJustification: l('Dokumentationspflicht', 'Documentation obligation'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: ['Aufsichtsbehoerde'], technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'], tags: ['security', 'incidents'] },
|
||||
{ id: 'dp-r4-threat-intel', code: 'R4', category: 'SECURITY', name: l('Bedrohungsinformationen', 'Threat Intelligence'), description: l('Erkannte Bedrohungen', 'Detected threats'), purpose: l('Sicherheitsmonitoring', 'Security monitoring'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Analyse', 'Analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Aggregierung'], tags: ['security', 'threats'] },
|
||||
{ id: 'dp-r5-blocked-ips', code: 'R5', category: 'SECURITY', name: l('Gesperrte IPs', 'Blocked IPs'), description: l('Blacklist von IP-Adressen', 'IP address blacklist'), purpose: l('Angriffspraevention', 'Attack prevention'), riskLevel: 'LOW', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Sicherheit', 'Security'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Automatische Bereinigung'], tags: ['security', 'blocking'] },
|
||||
{ id: 'dp-r6-vulnerability-scans', code: 'R6', category: 'SECURITY', name: l('Schwachstellen-Scans', 'Vulnerability Scans'), description: l('Ergebnisse von Security-Scans', 'Results of security scans'), purpose: l('Schwachstellenmanagement', 'Vulnerability management'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '12_MONTHS', retentionJustification: l('Trend-Analyse', 'Trend analysis'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Verschluesselung'], tags: ['security', 'vulnerabilities'] },
|
||||
{ id: 'dp-r7-penetration-tests', code: 'R7', category: 'SECURITY', name: l('Penetrationstests', 'Penetration Tests'), description: l('Ergebnisse von Pentests', 'Results of pentests'), purpose: l('Sicherheitspruefung', 'Security testing'), riskLevel: 'MEDIUM', legalBasis: 'LEGITIMATE_INTEREST', legalBasisJustification: l('IT-Sicherheit', 'IT security'), retentionPeriod: '6_YEARS', retentionJustification: l('Compliance-Nachweis', 'Compliance evidence'), cookieCategory: null, isSpecialCategory: false, requiresExplicitConsent: false, thirdPartyRecipients: [], technicalMeasures: ['Strenge Zugriffskontrolle'], tags: ['security', 'pentests'] },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// RETENTION MATRIX (18 Kategorien)
|
||||
// =============================================================================
|
||||
|
||||
export const RETENTION_MATRIX: RetentionMatrixEntry[] = [
|
||||
{ category: 'MASTER_DATA', categoryName: l('Stammdaten', 'Master Data'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO', exceptions: [] },
|
||||
{ category: 'CONTACT_DATA', categoryName: l('Kontaktdaten', 'Contact Data'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO', exceptions: [] },
|
||||
{ category: 'AUTHENTICATION', categoryName: l('Authentifizierung', 'Authentication'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO', exceptions: [{ condition: l('Session-Token', 'Session token'), period: '24_HOURS', reason: l('Sicherheit', 'Security') }] },
|
||||
{ category: 'CONSENT', categoryName: l('Einwilligungsdaten', 'Consent Data'), standardPeriod: '6_YEARS', legalBasis: 'Art. 7 DSGVO, § 147 AO', exceptions: [] },
|
||||
{ category: 'COMMUNICATION', categoryName: l('Kommunikation', 'Communication'), standardPeriod: '24_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. b/f DSGVO', exceptions: [] },
|
||||
{ category: 'PAYMENT', categoryName: l('Zahlungsdaten', 'Payment Data'), standardPeriod: '10_YEARS', legalBasis: '§ 147 AO, § 257 HGB', exceptions: [] },
|
||||
{ category: 'USAGE_DATA', categoryName: l('Nutzungsdaten', 'Usage Data'), standardPeriod: '24_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
|
||||
{ category: 'LOCATION', categoryName: l('Standortdaten', 'Location Data'), standardPeriod: '90_DAYS', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
|
||||
{ category: 'DEVICE_DATA', categoryName: l('Geraetedaten', 'Device Data'), standardPeriod: '12_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
|
||||
{ category: 'MARKETING', categoryName: l('Marketingdaten', 'Marketing Data'), standardPeriod: '90_DAYS', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
|
||||
{ category: 'ANALYTICS', categoryName: l('Analysedaten', 'Analytics Data'), standardPeriod: '26_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
|
||||
{ category: 'SOCIAL_MEDIA', categoryName: l('Social-Media-Daten', 'Social Media Data'), standardPeriod: 'UNTIL_ACCOUNT_DELETION', legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO', exceptions: [] },
|
||||
{ category: 'HEALTH_DATA', categoryName: l('Gesundheitsdaten', 'Health Data'), standardPeriod: '10_YEARS', legalBasis: 'Art. 9 Abs. 2 lit. a DSGVO', exceptions: [] },
|
||||
{ category: 'EMPLOYEE_DATA', categoryName: l('Beschaeftigtendaten', 'Employee Data'), standardPeriod: '10_YEARS', legalBasis: 'BDSG § 26', exceptions: [] },
|
||||
{ category: 'CONTRACT_DATA', categoryName: l('Vertragsdaten', 'Contract Data'), standardPeriod: '10_YEARS', legalBasis: '§ 147 AO, § 257 HGB', exceptions: [] },
|
||||
{ category: 'LOG_DATA', categoryName: l('Protokolldaten', 'Log Data'), standardPeriod: '12_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
|
||||
{ category: 'AI_DATA', categoryName: l('KI-Daten', 'AI Data'), standardPeriod: '90_DAYS', legalBasis: 'Art. 6 Abs. 1 lit. a/b DSGVO', exceptions: [] },
|
||||
{ category: 'SECURITY', categoryName: l('Sicherheitsdaten', 'Security Data'), standardPeriod: '12_MONTHS', legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO', exceptions: [] },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// COOKIE CATEGORIES
|
||||
// =============================================================================
|
||||
|
||||
export const DEFAULT_COOKIE_CATEGORIES: CookieBannerCategory[] = [
|
||||
{ id: 'ESSENTIAL', name: l('Technisch notwendig', 'Essential'), description: l('Diese Cookies sind fuer den Betrieb erforderlich', 'These cookies are required for operation'), isRequired: true, defaultEnabled: true, dataPointIds: ['dp-c2-session-token', 'dp-c3-refresh-token', 'dp-d1-consent-records', 'dp-d2-cookie-preferences'], cookies: [] },
|
||||
{ id: 'PERFORMANCE', name: l('Analyse & Performance', 'Analytics & Performance'), description: l('Helfen uns die Nutzung zu verstehen', 'Help us understand usage'), isRequired: false, defaultEnabled: false, dataPointIds: ['dp-g1-session-duration', 'dp-g2-page-views'], cookies: [] },
|
||||
{ id: 'PERSONALIZATION', name: l('Personalisierung', 'Personalization'), description: l('Ermoeglichen personalisierte Werbung', 'Enable personalized advertising'), isRequired: false, defaultEnabled: false, dataPointIds: [], cookies: [] },
|
||||
{ id: 'EXTERNAL_MEDIA', name: l('Externe Medien', 'External Media'), description: l('Erlauben Einbindung externer Medien', 'Allow embedding external media'), isRequired: false, defaultEnabled: false, dataPointIds: [], cookies: [] },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function getDataPointById(id: string): DataPoint | undefined {
|
||||
return PREDEFINED_DATA_POINTS.find((dp) => dp.id === id)
|
||||
}
|
||||
|
||||
export function getDataPointByCode(code: string): DataPoint | undefined {
|
||||
return PREDEFINED_DATA_POINTS.find((dp) => dp.code === code)
|
||||
}
|
||||
|
||||
export function getDataPointsByCategory(category: DataPointCategory): DataPoint[] {
|
||||
return PREDEFINED_DATA_POINTS.filter((dp) => dp.category === category)
|
||||
}
|
||||
|
||||
export function getDataPointsByLegalBasis(legalBasis: string): DataPoint[] {
|
||||
return PREDEFINED_DATA_POINTS.filter((dp) => dp.legalBasis === legalBasis)
|
||||
}
|
||||
|
||||
export function getDataPointsByCookieCategory(cookieCategory: string): DataPoint[] {
|
||||
return PREDEFINED_DATA_POINTS.filter((dp) => dp.cookieCategory === cookieCategory)
|
||||
}
|
||||
|
||||
export function getDataPointsRequiringConsent(): DataPoint[] {
|
||||
return PREDEFINED_DATA_POINTS.filter((dp) => dp.requiresExplicitConsent)
|
||||
}
|
||||
|
||||
export function getHighRiskDataPoints(): DataPoint[] {
|
||||
return PREDEFINED_DATA_POINTS.filter((dp) => dp.riskLevel === 'HIGH')
|
||||
}
|
||||
|
||||
export function getSpecialCategoryDataPoints(): DataPoint[] {
|
||||
return PREDEFINED_DATA_POINTS.filter((dp) => dp.isSpecialCategory)
|
||||
}
|
||||
|
||||
export function countDataPointsByCategory(): Record<DataPointCategory, number> {
|
||||
const counts = {} as Record<DataPointCategory, number>
|
||||
for (const dp of PREDEFINED_DATA_POINTS) {
|
||||
counts[dp.category] = (counts[dp.category] || 0) + 1
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
export function countDataPointsByRiskLevel(): Record<'LOW' | 'MEDIUM' | 'HIGH', number> {
|
||||
const counts = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||
for (const dp of PREDEFINED_DATA_POINTS) {
|
||||
counts[dp.riskLevel]++
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
export function createDefaultCatalog(tenantId: string): DataPointCatalog {
|
||||
return {
|
||||
id: `catalog-${tenantId}`,
|
||||
tenantId,
|
||||
version: '2.0.0',
|
||||
dataPoints: PREDEFINED_DATA_POINTS.map((dp) => ({ ...dp, isActive: true })),
|
||||
customDataPoints: [],
|
||||
retentionMatrix: RETENTION_MATRIX,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
export function searchDataPoints(dataPoints: DataPoint[], query: string, language: 'de' | 'en' = 'de'): DataPoint[] {
|
||||
const lowerQuery = query.toLowerCase()
|
||||
return dataPoints.filter(
|
||||
(dp) =>
|
||||
dp.code.toLowerCase().includes(lowerQuery) ||
|
||||
dp.name[language].toLowerCase().includes(lowerQuery) ||
|
||||
dp.description[language].toLowerCase().includes(lowerQuery) ||
|
||||
dp.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
|
||||
)
|
||||
}
|
||||
669
admin-v2/lib/sdk/einwilligungen/context.tsx
Normal file
669
admin-v2/lib/sdk/einwilligungen/context.tsx
Normal file
@@ -0,0 +1,669 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Einwilligungen Context & Reducer
|
||||
*
|
||||
* Zentrale State-Verwaltung fuer das Datenpunktkatalog & DSI-Generator Modul.
|
||||
* Verwendet React Context + useReducer fuer vorhersehbare State-Updates.
|
||||
*/
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useCallback,
|
||||
useMemo,
|
||||
ReactNode,
|
||||
Dispatch,
|
||||
} from 'react'
|
||||
import {
|
||||
EinwilligungenState,
|
||||
EinwilligungenAction,
|
||||
EinwilligungenTab,
|
||||
DataPoint,
|
||||
DataPointCatalog,
|
||||
GeneratedPrivacyPolicy,
|
||||
CookieBannerConfig,
|
||||
CompanyInfo,
|
||||
ConsentStatistics,
|
||||
PrivacyPolicySection,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
DataPointCategory,
|
||||
LegalBasis,
|
||||
RiskLevel,
|
||||
} from './types'
|
||||
import {
|
||||
PREDEFINED_DATA_POINTS,
|
||||
RETENTION_MATRIX,
|
||||
DEFAULT_COOKIE_CATEGORIES,
|
||||
createDefaultCatalog,
|
||||
getDataPointById,
|
||||
getDataPointsByCategory,
|
||||
countDataPointsByCategory,
|
||||
countDataPointsByRiskLevel,
|
||||
} from './catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE
|
||||
// =============================================================================
|
||||
|
||||
const initialState: EinwilligungenState = {
|
||||
// Data
|
||||
catalog: null,
|
||||
selectedDataPoints: [],
|
||||
privacyPolicy: null,
|
||||
cookieBannerConfig: null,
|
||||
companyInfo: null,
|
||||
consentStatistics: null,
|
||||
|
||||
// UI State
|
||||
activeTab: 'catalog',
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
error: null,
|
||||
|
||||
// Editor State
|
||||
editingDataPoint: null,
|
||||
editingSection: null,
|
||||
|
||||
// Preview
|
||||
previewLanguage: 'de',
|
||||
previewFormat: 'HTML',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REDUCER
|
||||
// =============================================================================
|
||||
|
||||
function einwilligungenReducer(
|
||||
state: EinwilligungenState,
|
||||
action: EinwilligungenAction
|
||||
): EinwilligungenState {
|
||||
switch (action.type) {
|
||||
case 'SET_CATALOG':
|
||||
return {
|
||||
...state,
|
||||
catalog: action.payload,
|
||||
// Automatisch alle aktiven Datenpunkte auswaehlen
|
||||
selectedDataPoints: [
|
||||
...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||
...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||
],
|
||||
}
|
||||
|
||||
case 'SET_SELECTED_DATA_POINTS':
|
||||
return {
|
||||
...state,
|
||||
selectedDataPoints: action.payload,
|
||||
}
|
||||
|
||||
case 'TOGGLE_DATA_POINT': {
|
||||
const id = action.payload
|
||||
const isSelected = state.selectedDataPoints.includes(id)
|
||||
return {
|
||||
...state,
|
||||
selectedDataPoints: isSelected
|
||||
? state.selectedDataPoints.filter((dpId) => dpId !== id)
|
||||
: [...state.selectedDataPoints, id],
|
||||
}
|
||||
}
|
||||
|
||||
case 'ADD_CUSTOM_DATA_POINT':
|
||||
if (!state.catalog) return state
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: [...state.catalog.customDataPoints, action.payload],
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
selectedDataPoints: [...state.selectedDataPoints, action.payload.id],
|
||||
}
|
||||
|
||||
case 'UPDATE_DATA_POINT': {
|
||||
if (!state.catalog) return state
|
||||
const { id, data } = action.payload
|
||||
|
||||
// Pruefe ob es ein vordefinierter oder kundenspezifischer Datenpunkt ist
|
||||
const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id)
|
||||
|
||||
if (isCustom) {
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: state.catalog.customDataPoints.map((dp) =>
|
||||
dp.id === id ? { ...dp, ...data } : dp
|
||||
),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Vordefinierte Datenpunkte: nur isActive aendern
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
dataPoints: state.catalog.dataPoints.map((dp) =>
|
||||
dp.id === id ? { ...dp, ...data } : dp
|
||||
),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case 'DELETE_CUSTOM_DATA_POINT':
|
||||
if (!state.catalog) return state
|
||||
return {
|
||||
...state,
|
||||
catalog: {
|
||||
...state.catalog,
|
||||
customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload),
|
||||
}
|
||||
|
||||
case 'SET_PRIVACY_POLICY':
|
||||
return {
|
||||
...state,
|
||||
privacyPolicy: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_COOKIE_BANNER_CONFIG':
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: action.payload,
|
||||
}
|
||||
|
||||
case 'UPDATE_COOKIE_BANNER_STYLING':
|
||||
if (!state.cookieBannerConfig) return state
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: {
|
||||
...state.cookieBannerConfig,
|
||||
styling: {
|
||||
...state.cookieBannerConfig.styling,
|
||||
...action.payload,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
case 'UPDATE_COOKIE_BANNER_TEXTS':
|
||||
if (!state.cookieBannerConfig) return state
|
||||
return {
|
||||
...state,
|
||||
cookieBannerConfig: {
|
||||
...state.cookieBannerConfig,
|
||||
texts: {
|
||||
...state.cookieBannerConfig.texts,
|
||||
...action.payload,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
case 'SET_COMPANY_INFO':
|
||||
return {
|
||||
...state,
|
||||
companyInfo: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_CONSENT_STATISTICS':
|
||||
return {
|
||||
...state,
|
||||
consentStatistics: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_ACTIVE_TAB':
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_LOADING':
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_SAVING':
|
||||
return {
|
||||
...state,
|
||||
isSaving: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_ERROR':
|
||||
return {
|
||||
...state,
|
||||
error: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_EDITING_DATA_POINT':
|
||||
return {
|
||||
...state,
|
||||
editingDataPoint: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_EDITING_SECTION':
|
||||
return {
|
||||
...state,
|
||||
editingSection: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_PREVIEW_LANGUAGE':
|
||||
return {
|
||||
...state,
|
||||
previewLanguage: action.payload,
|
||||
}
|
||||
|
||||
case 'SET_PREVIEW_FORMAT':
|
||||
return {
|
||||
...state,
|
||||
previewFormat: action.payload,
|
||||
}
|
||||
|
||||
case 'RESET_STATE':
|
||||
return initialState
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
interface EinwilligungenContextValue {
|
||||
state: EinwilligungenState
|
||||
dispatch: Dispatch<EinwilligungenAction>
|
||||
|
||||
// Computed Values
|
||||
allDataPoints: DataPoint[]
|
||||
selectedDataPointsData: DataPoint[]
|
||||
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
|
||||
categoryStats: Record<DataPointCategory, number>
|
||||
riskStats: Record<RiskLevel, number>
|
||||
legalBasisStats: Record<LegalBasis, number>
|
||||
|
||||
// Actions
|
||||
initializeCatalog: (tenantId: string) => void
|
||||
loadCatalog: (tenantId: string) => Promise<void>
|
||||
saveCatalog: () => Promise<void>
|
||||
toggleDataPoint: (id: string) => void
|
||||
addCustomDataPoint: (dataPoint: DataPoint) => void
|
||||
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
|
||||
deleteCustomDataPoint: (id: string) => void
|
||||
setActiveTab: (tab: EinwilligungenTab) => void
|
||||
setPreviewLanguage: (language: SupportedLanguage) => void
|
||||
setPreviewFormat: (format: ExportFormat) => void
|
||||
setCompanyInfo: (info: CompanyInfo) => void
|
||||
generatePrivacyPolicy: () => Promise<void>
|
||||
generateCookieBannerConfig: () => void
|
||||
}
|
||||
|
||||
const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
|
||||
|
||||
// =============================================================================
|
||||
// PROVIDER
|
||||
// =============================================================================
|
||||
|
||||
interface EinwilligungenProviderProps {
|
||||
children: ReactNode
|
||||
tenantId?: string
|
||||
}
|
||||
|
||||
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
|
||||
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// COMPUTED VALUES
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const allDataPoints = useMemo(() => {
|
||||
if (!state.catalog) return PREDEFINED_DATA_POINTS
|
||||
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
|
||||
}, [state.catalog])
|
||||
|
||||
const selectedDataPointsData = useMemo(() => {
|
||||
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
|
||||
}, [allDataPoints, state.selectedDataPoints])
|
||||
|
||||
const dataPointsByCategory = useMemo(() => {
|
||||
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
|
||||
// 18 Kategorien (A-R)
|
||||
const categories: DataPointCategory[] = [
|
||||
'MASTER_DATA', // A
|
||||
'CONTACT_DATA', // B
|
||||
'AUTHENTICATION', // C
|
||||
'CONSENT', // D
|
||||
'COMMUNICATION', // E
|
||||
'PAYMENT', // F
|
||||
'USAGE_DATA', // G
|
||||
'LOCATION', // H
|
||||
'DEVICE_DATA', // I
|
||||
'MARKETING', // J
|
||||
'ANALYTICS', // K
|
||||
'SOCIAL_MEDIA', // L
|
||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
||||
'CONTRACT_DATA', // O
|
||||
'LOG_DATA', // P
|
||||
'AI_DATA', // Q - AI Act
|
||||
'SECURITY', // R
|
||||
]
|
||||
for (const cat of categories) {
|
||||
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
|
||||
}
|
||||
return result as Record<DataPointCategory, DataPoint[]>
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const categoryStats = useMemo(() => {
|
||||
const counts: Partial<Record<DataPointCategory, number>> = {}
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.category] = (counts[dp.category] || 0) + 1
|
||||
}
|
||||
return counts as Record<DataPointCategory, number>
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const riskStats = useMemo(() => {
|
||||
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.riskLevel]++
|
||||
}
|
||||
return counts
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
const legalBasisStats = useMemo(() => {
|
||||
// Alle 7 Rechtsgrundlagen
|
||||
const counts: Record<LegalBasis, number> = {
|
||||
CONTRACT: 0,
|
||||
CONSENT: 0,
|
||||
EXPLICIT_CONSENT: 0,
|
||||
LEGITIMATE_INTEREST: 0,
|
||||
LEGAL_OBLIGATION: 0,
|
||||
VITAL_INTERESTS: 0,
|
||||
PUBLIC_INTEREST: 0,
|
||||
}
|
||||
for (const dp of selectedDataPointsData) {
|
||||
counts[dp.legalBasis]++
|
||||
}
|
||||
return counts
|
||||
}, [selectedDataPointsData])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ACTIONS
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const initializeCatalog = useCallback(
|
||||
(tid: string) => {
|
||||
const catalog = createDefaultCatalog(tid)
|
||||
dispatch({ type: 'SET_CATALOG', payload: catalog })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const loadCatalog = useCallback(
|
||||
async (tid: string) => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||
headers: {
|
||||
'X-Tenant-ID': tid,
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
|
||||
if (data.companyInfo) {
|
||||
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
|
||||
}
|
||||
if (data.cookieBannerConfig) {
|
||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
// Katalog existiert noch nicht - erstelle Default
|
||||
initializeCatalog(tid)
|
||||
} else {
|
||||
throw new Error('Failed to load catalog')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading catalog:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
|
||||
// Fallback zu Default
|
||||
initializeCatalog(tid)
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
},
|
||||
[dispatch, initializeCatalog]
|
||||
)
|
||||
|
||||
const saveCatalog = useCallback(async () => {
|
||||
if (!state.catalog) return
|
||||
|
||||
dispatch({ type: 'SET_SAVING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': state.catalog.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
catalog: state.catalog,
|
||||
companyInfo: state.companyInfo,
|
||||
cookieBannerConfig: state.cookieBannerConfig,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save catalog')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving catalog:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
|
||||
} finally {
|
||||
dispatch({ type: 'SET_SAVING', payload: false })
|
||||
}
|
||||
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
|
||||
|
||||
const toggleDataPoint = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const addCustomDataPoint = useCallback(
|
||||
(dataPoint: DataPoint) => {
|
||||
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateDataPoint = useCallback(
|
||||
(id: string, data: Partial<DataPoint>) => {
|
||||
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteCustomDataPoint = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(tab: EinwilligungenTab) => {
|
||||
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setPreviewLanguage = useCallback(
|
||||
(language: SupportedLanguage) => {
|
||||
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setPreviewFormat = useCallback(
|
||||
(format: ExportFormat) => {
|
||||
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const setCompanyInfo = useCallback(
|
||||
(info: CompanyInfo) => {
|
||||
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const generatePrivacyPolicy = useCallback(async () => {
|
||||
if (!state.catalog || !state.companyInfo) {
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_LOADING', payload: true })
|
||||
dispatch({ type: 'SET_ERROR', payload: null })
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': state.catalog.tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
dataPointIds: state.selectedDataPoints,
|
||||
companyInfo: state.companyInfo,
|
||||
language: state.previewLanguage,
|
||||
format: state.previewFormat,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const policy = await response.json()
|
||||
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
|
||||
} else {
|
||||
throw new Error('Failed to generate privacy policy')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating privacy policy:', error)
|
||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false })
|
||||
}
|
||||
}, [
|
||||
state.catalog,
|
||||
state.companyInfo,
|
||||
state.selectedDataPoints,
|
||||
state.previewLanguage,
|
||||
state.previewFormat,
|
||||
dispatch,
|
||||
])
|
||||
|
||||
const generateCookieBannerConfig = useCallback(() => {
|
||||
if (!state.catalog) return
|
||||
|
||||
const config: CookieBannerConfig = {
|
||||
id: `cookie-banner-${state.catalog.tenantId}`,
|
||||
tenantId: state.catalog.tenantId,
|
||||
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
|
||||
...cat,
|
||||
// Filtere nur die ausgewaehlten Datenpunkte
|
||||
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
|
||||
})),
|
||||
styling: {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1',
|
||||
borderRadius: 12,
|
||||
},
|
||||
texts: {
|
||||
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website.',
|
||||
},
|
||||
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
||||
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
|
||||
customize: { de: 'Anpassen', en: 'Customize' },
|
||||
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
||||
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
|
||||
}, [state.catalog, state.selectedDataPoints, dispatch])
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CONTEXT VALUE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const value: EinwilligungenContextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
|
||||
// Computed Values
|
||||
allDataPoints,
|
||||
selectedDataPointsData,
|
||||
dataPointsByCategory,
|
||||
categoryStats,
|
||||
riskStats,
|
||||
legalBasisStats,
|
||||
|
||||
// Actions
|
||||
initializeCatalog,
|
||||
loadCatalog,
|
||||
saveCatalog,
|
||||
toggleDataPoint,
|
||||
addCustomDataPoint,
|
||||
updateDataPoint,
|
||||
deleteCustomDataPoint,
|
||||
setActiveTab,
|
||||
setPreviewLanguage,
|
||||
setPreviewFormat,
|
||||
setCompanyInfo,
|
||||
generatePrivacyPolicy,
|
||||
generateCookieBannerConfig,
|
||||
}
|
||||
|
||||
return (
|
||||
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useEinwilligungen(): EinwilligungenContextValue {
|
||||
const context = useContext(EinwilligungenContext)
|
||||
if (!context) {
|
||||
throw new Error('useEinwilligungen must be used within EinwilligungenProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export { initialState, einwilligungenReducer }
|
||||
493
admin-v2/lib/sdk/einwilligungen/export/docx.ts
Normal file
493
admin-v2/lib/sdk/einwilligungen/export/docx.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
// =============================================================================
|
||||
// Privacy Policy DOCX Export
|
||||
// Export Datenschutzerklaerung to Microsoft Word format
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
GeneratedPrivacyPolicy,
|
||||
PrivacyPolicySection,
|
||||
CompanyInfo,
|
||||
SupportedLanguage,
|
||||
DataPoint,
|
||||
CATEGORY_METADATA,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DOCXExportOptions {
|
||||
language: SupportedLanguage
|
||||
includeTableOfContents: boolean
|
||||
includeDataPointList: boolean
|
||||
companyLogo?: string
|
||||
primaryColor?: string
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: DOCXExportOptions = {
|
||||
language: 'de',
|
||||
includeTableOfContents: true,
|
||||
includeDataPointList: true,
|
||||
primaryColor: '#6366f1',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX CONTENT STRUCTURE
|
||||
// =============================================================================
|
||||
|
||||
export interface DocxParagraph {
|
||||
type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet' | 'title'
|
||||
content: string
|
||||
style?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface DocxTableRow {
|
||||
cells: string[]
|
||||
isHeader?: boolean
|
||||
}
|
||||
|
||||
export interface DocxTable {
|
||||
type: 'table'
|
||||
headers: string[]
|
||||
rows: DocxTableRow[]
|
||||
}
|
||||
|
||||
export type DocxElement = DocxParagraph | DocxTable
|
||||
|
||||
// =============================================================================
|
||||
// DOCX CONTENT GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate DOCX content structure for Privacy Policy
|
||||
*/
|
||||
export function generateDOCXContent(
|
||||
policy: GeneratedPrivacyPolicy,
|
||||
companyInfo: CompanyInfo,
|
||||
dataPoints: DataPoint[],
|
||||
options: Partial<DOCXExportOptions> = {}
|
||||
): DocxElement[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const elements: DocxElement[] = []
|
||||
const lang = opts.language
|
||||
|
||||
// Title
|
||||
elements.push({
|
||||
type: 'title',
|
||||
content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? 'gemaess Art. 13, 14 DSGVO'
|
||||
: 'according to Art. 13, 14 GDPR',
|
||||
style: { fontStyle: 'italic', textAlign: 'center' },
|
||||
})
|
||||
|
||||
// Company Info
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: lang === 'de' ? 'Verantwortlicher' : 'Controller',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.name,
|
||||
style: { fontWeight: 'bold' },
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${companyInfo.address}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${companyInfo.postalCode} ${companyInfo.city}`,
|
||||
})
|
||||
|
||||
if (companyInfo.country) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.country,
|
||||
})
|
||||
}
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'E-Mail' : 'Email'}: ${companyInfo.email}`,
|
||||
})
|
||||
|
||||
if (companyInfo.phone) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'Telefon' : 'Phone'}: ${companyInfo.phone}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (companyInfo.website) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `Website: ${companyInfo.website}`,
|
||||
})
|
||||
}
|
||||
|
||||
// DPO Info
|
||||
if (companyInfo.dpoName || companyInfo.dpoEmail) {
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: lang === 'de' ? 'Datenschutzbeauftragter' : 'Data Protection Officer',
|
||||
})
|
||||
|
||||
if (companyInfo.dpoName) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.dpoName,
|
||||
})
|
||||
}
|
||||
|
||||
if (companyInfo.dpoEmail) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'E-Mail' : 'Email'}: ${companyInfo.dpoEmail}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (companyInfo.dpoPhone) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'Telefon' : 'Phone'}: ${companyInfo.dpoPhone}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Document metadata
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? `Stand: ${new Date(policy.generatedAt).toLocaleDateString('de-DE')}`
|
||||
: `Date: ${new Date(policy.generatedAt).toLocaleDateString('en-US')}`,
|
||||
style: { marginTop: '20px' },
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `Version: ${policy.version}`,
|
||||
})
|
||||
|
||||
// Table of Contents
|
||||
if (opts.includeTableOfContents) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
|
||||
})
|
||||
|
||||
policy.sections.forEach((section, idx) => {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: `${idx + 1}. ${section.title[lang]}`,
|
||||
})
|
||||
})
|
||||
|
||||
if (opts.includeDataPointList) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Privacy Policy Sections
|
||||
policy.sections.forEach((section, idx) => {
|
||||
elements.push({
|
||||
type: 'heading1',
|
||||
content: `${idx + 1}. ${section.title[lang]}`,
|
||||
})
|
||||
|
||||
// Parse content
|
||||
const content = section.content[lang]
|
||||
const paragraphs = content.split('\n\n')
|
||||
|
||||
for (const para of paragraphs) {
|
||||
if (para.startsWith('- ')) {
|
||||
// List items
|
||||
const items = para.split('\n').filter(l => l.startsWith('- '))
|
||||
for (const item of items) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: item.substring(2),
|
||||
})
|
||||
}
|
||||
} else if (para.startsWith('### ')) {
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: para.substring(4),
|
||||
})
|
||||
} else if (para.startsWith('## ')) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: para.substring(3),
|
||||
})
|
||||
} else if (para.trim()) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: para.replace(/\*\*(.*?)\*\*/g, '$1'),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Data Point Catalog Appendix
|
||||
if (opts.includeDataPointList && dataPoints.length > 0) {
|
||||
elements.push({
|
||||
type: 'heading1',
|
||||
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? 'Die folgende Tabelle zeigt alle verarbeiteten personenbezogenen Daten:'
|
||||
: 'The following table shows all processed personal data:',
|
||||
})
|
||||
|
||||
// Group by category
|
||||
const categories = [...new Set(dataPoints.map(dp => dp.category))]
|
||||
|
||||
for (const category of categories) {
|
||||
const categoryDPs = dataPoints.filter(dp => dp.category === category)
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'table',
|
||||
headers: lang === 'de'
|
||||
? ['Code', 'Datenpunkt', 'Rechtsgrundlage', 'Loeschfrist']
|
||||
: ['Code', 'Data Point', 'Legal Basis', 'Retention'],
|
||||
rows: categoryDPs.map(dp => ({
|
||||
cells: [
|
||||
dp.code,
|
||||
dp.name[lang],
|
||||
formatLegalBasis(dp.legalBasis, lang),
|
||||
RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod,
|
||||
],
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? `Dieses Dokument wurde automatisch generiert mit dem Datenschutzerklaerung-Generator am ${new Date().toLocaleDateString('de-DE')}.`
|
||||
: `This document was automatically generated with the Privacy Policy Generator on ${new Date().toLocaleDateString('en-US')}.`,
|
||||
style: { fontStyle: 'italic', fontSize: '9pt' },
|
||||
})
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function formatLegalBasis(basis: string, language: SupportedLanguage): string {
|
||||
const bases: Record<string, Record<SupportedLanguage, string>> = {
|
||||
CONTRACT: { de: 'Vertrag (Art. 6 Abs. 1 lit. b)', en: 'Contract (Art. 6(1)(b))' },
|
||||
CONSENT: { de: 'Einwilligung (Art. 6 Abs. 1 lit. a)', en: 'Consent (Art. 6(1)(a))' },
|
||||
LEGITIMATE_INTEREST: { de: 'Ber. Interesse (Art. 6 Abs. 1 lit. f)', en: 'Legitimate Interest (Art. 6(1)(f))' },
|
||||
LEGAL_OBLIGATION: { de: 'Rechtspflicht (Art. 6 Abs. 1 lit. c)', en: 'Legal Obligation (Art. 6(1)(c))' },
|
||||
}
|
||||
return bases[basis]?.[language] || basis
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX BLOB GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a DOCX file as a Blob
|
||||
* This generates HTML that Word can open
|
||||
*/
|
||||
export async function generateDOCXBlob(
|
||||
policy: GeneratedPrivacyPolicy,
|
||||
companyInfo: CompanyInfo,
|
||||
dataPoints: DataPoint[],
|
||||
options: Partial<DOCXExportOptions> = {}
|
||||
): Promise<Blob> {
|
||||
const content = generateDOCXContent(policy, companyInfo, dataPoints, options)
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
const html = generateHTMLFromContent(content, opts)
|
||||
|
||||
return new Blob([html], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
})
|
||||
}
|
||||
|
||||
function generateHTMLFromContent(
|
||||
content: DocxElement[],
|
||||
options: DOCXExportOptions
|
||||
): string {
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${options.language}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Calibri, Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18pt;
|
||||
color: ${options.primaryColor};
|
||||
border-bottom: 1px solid ${options.primaryColor};
|
||||
padding-bottom: 8px;
|
||||
margin-top: 24pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 14pt;
|
||||
color: ${options.primaryColor};
|
||||
margin-top: 18pt;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 12pt;
|
||||
color: #334155;
|
||||
margin-top: 14pt;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24pt;
|
||||
color: ${options.primaryColor};
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12pt;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8pt 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12pt 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #cbd5e1;
|
||||
padding: 6pt 10pt;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: ${options.primaryColor};
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 8pt 0;
|
||||
padding-left: 20pt;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4pt 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const element of content) {
|
||||
if (element.type === 'table') {
|
||||
html += '<table>\n<thead><tr>\n'
|
||||
for (const header of element.headers) {
|
||||
html += ` <th>${escapeHtml(header)}</th>\n`
|
||||
}
|
||||
html += '</tr></thead>\n<tbody>\n'
|
||||
for (const row of element.rows) {
|
||||
html += '<tr>\n'
|
||||
for (const cell of row.cells) {
|
||||
html += ` <td>${escapeHtml(cell)}</td>\n`
|
||||
}
|
||||
html += '</tr>\n'
|
||||
}
|
||||
html += '</tbody></table>\n'
|
||||
} else {
|
||||
const tag = getHtmlTag(element.type)
|
||||
const className = element.type === 'title' ? ' class="title"' : ''
|
||||
const processedContent = escapeHtml(element.content)
|
||||
html += `<${tag}${className}>${processedContent}</${tag}>\n`
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
function getHtmlTag(type: string): string {
|
||||
switch (type) {
|
||||
case 'title':
|
||||
return 'div'
|
||||
case 'heading1':
|
||||
return 'h1'
|
||||
case 'heading2':
|
||||
return 'h2'
|
||||
case 'heading3':
|
||||
return 'h3'
|
||||
case 'bullet':
|
||||
return 'li'
|
||||
default:
|
||||
return 'p'
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a filename for the DOCX export
|
||||
*/
|
||||
export function generateDOCXFilename(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage = 'de'
|
||||
): string {
|
||||
const companyName = companyInfo.name.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const prefix = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy-Policy'
|
||||
return `${prefix}-${companyName}-${date}.doc`
|
||||
}
|
||||
8
admin-v2/lib/sdk/einwilligungen/export/index.ts
Normal file
8
admin-v2/lib/sdk/einwilligungen/export/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Einwilligungen Export Module
|
||||
*
|
||||
* PDF and DOCX export functionality for Privacy Policy documents.
|
||||
*/
|
||||
|
||||
export * from './pdf'
|
||||
export * from './docx'
|
||||
505
admin-v2/lib/sdk/einwilligungen/export/pdf.ts
Normal file
505
admin-v2/lib/sdk/einwilligungen/export/pdf.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
// =============================================================================
|
||||
// Privacy Policy PDF Export
|
||||
// Export Datenschutzerklaerung to PDF format
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
GeneratedPrivacyPolicy,
|
||||
PrivacyPolicySection,
|
||||
CompanyInfo,
|
||||
SupportedLanguage,
|
||||
DataPoint,
|
||||
CATEGORY_METADATA,
|
||||
RETENTION_PERIOD_INFO,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface PDFExportOptions {
|
||||
language: SupportedLanguage
|
||||
includeTableOfContents: boolean
|
||||
includeDataPointList: boolean
|
||||
companyLogo?: string
|
||||
primaryColor?: string
|
||||
pageSize?: 'A4' | 'LETTER'
|
||||
orientation?: 'portrait' | 'landscape'
|
||||
fontSize?: number
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: PDFExportOptions = {
|
||||
language: 'de',
|
||||
includeTableOfContents: true,
|
||||
includeDataPointList: true,
|
||||
primaryColor: '#6366f1',
|
||||
pageSize: 'A4',
|
||||
orientation: 'portrait',
|
||||
fontSize: 11,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF CONTENT STRUCTURE
|
||||
// =============================================================================
|
||||
|
||||
export interface PDFSection {
|
||||
type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak'
|
||||
content?: string
|
||||
items?: string[]
|
||||
table?: {
|
||||
headers: string[]
|
||||
rows: string[][]
|
||||
}
|
||||
style?: {
|
||||
color?: string
|
||||
fontSize?: number
|
||||
bold?: boolean
|
||||
italic?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF CONTENT GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate PDF content structure for Privacy Policy
|
||||
*/
|
||||
export function generatePDFContent(
|
||||
policy: GeneratedPrivacyPolicy,
|
||||
companyInfo: CompanyInfo,
|
||||
dataPoints: DataPoint[],
|
||||
options: Partial<PDFExportOptions> = {}
|
||||
): PDFSection[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const sections: PDFSection[] = []
|
||||
const lang = opts.language
|
||||
|
||||
// Title page
|
||||
sections.push({
|
||||
type: 'title',
|
||||
content: lang === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy',
|
||||
style: { color: opts.primaryColor, fontSize: 28, bold: true, align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? 'gemaess Art. 13, 14 DSGVO'
|
||||
: 'according to Art. 13, 14 GDPR',
|
||||
style: { fontSize: 14, align: 'center', italic: true },
|
||||
})
|
||||
|
||||
// Company information
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: companyInfo.name,
|
||||
style: { fontSize: 16, bold: true, align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: `${companyInfo.address}, ${companyInfo.postalCode} ${companyInfo.city}`,
|
||||
style: { align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: `${lang === 'de' ? 'Stand' : 'Date'}: ${new Date(policy.generatedAt).toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')}`,
|
||||
style: { align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: `Version: ${policy.version}`,
|
||||
style: { align: 'center', fontSize: 10 },
|
||||
})
|
||||
|
||||
sections.push({ type: 'pagebreak' })
|
||||
|
||||
// Table of Contents
|
||||
if (opts.includeTableOfContents) {
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: lang === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
const tocItems = policy.sections.map((section, idx) =>
|
||||
`${idx + 1}. ${section.title[lang]}`
|
||||
)
|
||||
|
||||
if (opts.includeDataPointList) {
|
||||
tocItems.push(lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog')
|
||||
}
|
||||
|
||||
sections.push({
|
||||
type: 'list',
|
||||
items: tocItems,
|
||||
})
|
||||
|
||||
sections.push({ type: 'pagebreak' })
|
||||
}
|
||||
|
||||
// Privacy Policy Sections
|
||||
policy.sections.forEach((section, idx) => {
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: `${idx + 1}. ${section.title[lang]}`,
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
// Convert markdown-like content to paragraphs
|
||||
const content = section.content[lang]
|
||||
const paragraphs = content.split('\n\n')
|
||||
|
||||
for (const para of paragraphs) {
|
||||
if (para.startsWith('- ')) {
|
||||
// List items
|
||||
const items = para.split('\n').filter(l => l.startsWith('- ')).map(l => l.substring(2))
|
||||
sections.push({
|
||||
type: 'list',
|
||||
items,
|
||||
})
|
||||
} else if (para.startsWith('### ')) {
|
||||
sections.push({
|
||||
type: 'subheading',
|
||||
content: para.substring(4),
|
||||
})
|
||||
} else if (para.startsWith('## ')) {
|
||||
sections.push({
|
||||
type: 'subheading',
|
||||
content: para.substring(3),
|
||||
style: { bold: true },
|
||||
})
|
||||
} else if (para.trim()) {
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: para.replace(/\*\*(.*?)\*\*/g, '$1'), // Remove markdown bold for plain text
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add related data points if this section has them
|
||||
if (section.dataPointIds.length > 0 && opts.includeDataPointList) {
|
||||
const relatedDPs = dataPoints.filter(dp => section.dataPointIds.includes(dp.id))
|
||||
if (relatedDPs.length > 0) {
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? `Betroffene Datenkategorien: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`
|
||||
: `Affected data categories: ${relatedDPs.map(dp => dp.name[lang]).join(', ')}`,
|
||||
style: { italic: true, fontSize: 10 },
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Data Point Catalog Appendix
|
||||
if (opts.includeDataPointList && dataPoints.length > 0) {
|
||||
sections.push({ type: 'pagebreak' })
|
||||
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: lang === 'de' ? 'Anhang: Datenpunktkatalog' : 'Appendix: Data Point Catalog',
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? 'Die folgende Tabelle zeigt alle verarbeiteten personenbezogenen Daten:'
|
||||
: 'The following table shows all processed personal data:',
|
||||
})
|
||||
|
||||
// Group by category
|
||||
const categories = [...new Set(dataPoints.map(dp => dp.category))]
|
||||
|
||||
for (const category of categories) {
|
||||
const categoryDPs = dataPoints.filter(dp => dp.category === category)
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
|
||||
sections.push({
|
||||
type: 'subheading',
|
||||
content: `${categoryMeta.code}. ${categoryMeta.name[lang]}`,
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'table',
|
||||
table: {
|
||||
headers: lang === 'de'
|
||||
? ['Code', 'Datenpunkt', 'Zweck', 'Loeschfrist']
|
||||
: ['Code', 'Data Point', 'Purpose', 'Retention'],
|
||||
rows: categoryDPs.map(dp => [
|
||||
dp.code,
|
||||
dp.name[lang],
|
||||
dp.purpose[lang].substring(0, 50) + (dp.purpose[lang].length > 50 ? '...' : ''),
|
||||
RETENTION_PERIOD_INFO[dp.retentionPeriod]?.label[lang] || dp.retentionPeriod,
|
||||
]),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: lang === 'de'
|
||||
? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem Datenschutzerklaerung-Generator`
|
||||
: `Generated on ${new Date().toLocaleDateString('en-US')} with the Privacy Policy Generator`,
|
||||
style: { italic: true, align: 'center', fontSize: 9 },
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF BLOB GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a PDF file as a Blob
|
||||
* This generates HTML that can be printed to PDF or used with a PDF library
|
||||
*/
|
||||
export async function generatePDFBlob(
|
||||
policy: GeneratedPrivacyPolicy,
|
||||
companyInfo: CompanyInfo,
|
||||
dataPoints: DataPoint[],
|
||||
options: Partial<PDFExportOptions> = {}
|
||||
): Promise<Blob> {
|
||||
const content = generatePDFContent(policy, companyInfo, dataPoints, options)
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
// Generate HTML for PDF conversion
|
||||
const html = generateHTMLFromContent(content, opts)
|
||||
|
||||
return new Blob([html], { type: 'text/html' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate printable HTML from PDF content
|
||||
*/
|
||||
function generateHTMLFromContent(
|
||||
content: PDFSection[],
|
||||
options: PDFExportOptions
|
||||
): string {
|
||||
const pageWidth = options.pageSize === 'A4' ? '210mm' : '8.5in'
|
||||
const pageHeight = options.pageSize === 'A4' ? '297mm' : '11in'
|
||||
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="${options.language}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>${options.language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'}</title>
|
||||
<style>
|
||||
@page {
|
||||
size: ${pageWidth} ${pageHeight};
|
||||
margin: 20mm;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Calibri, Arial, sans-serif;
|
||||
font-size: ${options.fontSize}pt;
|
||||
line-height: 1.6;
|
||||
color: #1e293b;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24pt;
|
||||
color: ${options.primaryColor};
|
||||
border-bottom: 2px solid ${options.primaryColor};
|
||||
padding-bottom: 10px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16pt;
|
||||
color: ${options.primaryColor};
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 13pt;
|
||||
color: #334155;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 28pt;
|
||||
text-align: center;
|
||||
color: ${options.primaryColor};
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: ${options.primaryColor};
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.pagebreak {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
font-size: 9pt;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
.pagebreak {
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const section of content) {
|
||||
switch (section.type) {
|
||||
case 'title':
|
||||
html += `<div class="title" style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</div>\n`
|
||||
break
|
||||
|
||||
case 'heading':
|
||||
html += `<h1 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h1>\n`
|
||||
break
|
||||
|
||||
case 'subheading':
|
||||
html += `<h3 style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</h3>\n`
|
||||
break
|
||||
|
||||
case 'paragraph':
|
||||
const alignClass = section.style?.align === 'center' ? ' class="center"' : ''
|
||||
html += `<p${alignClass} style="${getStyleString(section.style)}">${escapeHtml(section.content || '')}</p>\n`
|
||||
break
|
||||
|
||||
case 'list':
|
||||
html += '<ul>\n'
|
||||
for (const item of section.items || []) {
|
||||
html += ` <li>${escapeHtml(item)}</li>\n`
|
||||
}
|
||||
html += '</ul>\n'
|
||||
break
|
||||
|
||||
case 'table':
|
||||
if (section.table) {
|
||||
html += '<table>\n<thead><tr>\n'
|
||||
for (const header of section.table.headers) {
|
||||
html += ` <th>${escapeHtml(header)}</th>\n`
|
||||
}
|
||||
html += '</tr></thead>\n<tbody>\n'
|
||||
for (const row of section.table.rows) {
|
||||
html += '<tr>\n'
|
||||
for (const cell of row) {
|
||||
html += ` <td>${escapeHtml(cell)}</td>\n`
|
||||
}
|
||||
html += '</tr>\n'
|
||||
}
|
||||
html += '</tbody></table>\n'
|
||||
}
|
||||
break
|
||||
|
||||
case 'pagebreak':
|
||||
html += '<div class="pagebreak"></div>\n'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
function getStyleString(style?: PDFSection['style']): string {
|
||||
if (!style) return ''
|
||||
|
||||
const parts: string[] = []
|
||||
if (style.color) parts.push(`color: ${style.color}`)
|
||||
if (style.fontSize) parts.push(`font-size: ${style.fontSize}pt`)
|
||||
if (style.bold) parts.push('font-weight: bold')
|
||||
if (style.italic) parts.push('font-style: italic')
|
||||
if (style.align) parts.push(`text-align: ${style.align}`)
|
||||
|
||||
return parts.join('; ')
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a filename for the PDF export
|
||||
*/
|
||||
export function generatePDFFilename(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage = 'de'
|
||||
): string {
|
||||
const companyName = companyInfo.name.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const prefix = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy-Policy'
|
||||
return `${prefix}-${companyName}-${date}.html`
|
||||
}
|
||||
595
admin-v2/lib/sdk/einwilligungen/generator/cookie-banner.ts
Normal file
595
admin-v2/lib/sdk/einwilligungen/generator/cookie-banner.ts
Normal file
@@ -0,0 +1,595 @@
|
||||
/**
|
||||
* Cookie Banner Generator
|
||||
*
|
||||
* Generiert Cookie-Banner Konfigurationen und Embed-Code aus dem Datenpunktkatalog.
|
||||
* Die Cookie-Kategorien werden automatisch aus den Datenpunkten abgeleitet.
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
CookieCategory,
|
||||
CookieBannerCategory,
|
||||
CookieBannerConfig,
|
||||
CookieBannerStyling,
|
||||
CookieBannerTexts,
|
||||
CookieBannerEmbedCode,
|
||||
CookieInfo,
|
||||
LocalizedText,
|
||||
SupportedLanguage,
|
||||
} from '../types'
|
||||
import { DEFAULT_COOKIE_CATEGORIES } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Holt den lokalisierten Text
|
||||
*/
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COOKIE BANNER CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Standard Cookie Banner Texte
|
||||
*/
|
||||
export const DEFAULT_COOKIE_BANNER_TEXTS: CookieBannerTexts = {
|
||||
title: {
|
||||
de: 'Cookie-Einstellungen',
|
||||
en: 'Cookie Settings',
|
||||
},
|
||||
description: {
|
||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Einige Cookies sind technisch notwendig, waehrend andere uns helfen, Ihre Nutzererfahrung zu verbessern.',
|
||||
en: 'We use cookies to provide you with the best possible experience on our website. Some cookies are technically necessary, while others help us improve your user experience.',
|
||||
},
|
||||
acceptAll: {
|
||||
de: 'Alle akzeptieren',
|
||||
en: 'Accept All',
|
||||
},
|
||||
rejectAll: {
|
||||
de: 'Nur notwendige',
|
||||
en: 'Essential Only',
|
||||
},
|
||||
customize: {
|
||||
de: 'Einstellungen',
|
||||
en: 'Customize',
|
||||
},
|
||||
save: {
|
||||
de: 'Auswahl speichern',
|
||||
en: 'Save Selection',
|
||||
},
|
||||
privacyPolicyLink: {
|
||||
de: 'Mehr in unserer Datenschutzerklaerung',
|
||||
en: 'More in our Privacy Policy',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard Styling fuer Cookie Banner
|
||||
*/
|
||||
export const DEFAULT_COOKIE_BANNER_STYLING: CookieBannerStyling = {
|
||||
position: 'BOTTOM',
|
||||
theme: 'LIGHT',
|
||||
primaryColor: '#6366f1', // Indigo
|
||||
secondaryColor: '#f1f5f9', // Slate-100
|
||||
textColor: '#1e293b', // Slate-800
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: 12,
|
||||
maxWidth: 480,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert Cookie-Banner Kategorien aus Datenpunkten
|
||||
*/
|
||||
export function generateCookieCategories(
|
||||
dataPoints: DataPoint[]
|
||||
): CookieBannerCategory[] {
|
||||
// Filtere nur Datenpunkte mit Cookie-Kategorie
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
// Erstelle die Kategorien basierend auf den Defaults
|
||||
return DEFAULT_COOKIE_CATEGORIES.map((defaultCat) => {
|
||||
// Filtere die Datenpunkte fuer diese Kategorie
|
||||
const categoryDataPoints = cookieDataPoints.filter(
|
||||
(dp) => dp.cookieCategory === defaultCat.id
|
||||
)
|
||||
|
||||
// Erstelle Cookie-Infos aus den Datenpunkten
|
||||
const cookies: CookieInfo[] = categoryDataPoints.map((dp) => ({
|
||||
name: dp.code,
|
||||
provider: 'First Party',
|
||||
purpose: dp.purpose,
|
||||
expiry: getExpiryFromRetention(dp.retentionPeriod),
|
||||
type: 'FIRST_PARTY',
|
||||
}))
|
||||
|
||||
return {
|
||||
...defaultCat,
|
||||
dataPointIds: categoryDataPoints.map((dp) => dp.id),
|
||||
cookies,
|
||||
}
|
||||
}).filter((cat) => cat.dataPointIds.length > 0 || cat.isRequired)
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert Retention Period zu Cookie-Expiry String
|
||||
*/
|
||||
function getExpiryFromRetention(retention: string): string {
|
||||
const mapping: Record<string, string> = {
|
||||
'24_HOURS': '24 Stunden / 24 hours',
|
||||
'30_DAYS': '30 Tage / 30 days',
|
||||
'90_DAYS': '90 Tage / 90 days',
|
||||
'12_MONTHS': '1 Jahr / 1 year',
|
||||
'24_MONTHS': '2 Jahre / 2 years',
|
||||
'36_MONTHS': '3 Jahre / 3 years',
|
||||
'UNTIL_REVOCATION': 'Bis Widerruf / Until revocation',
|
||||
'UNTIL_PURPOSE_FULFILLED': 'Session',
|
||||
'UNTIL_ACCOUNT_DELETION': 'Bis Kontoschliessung / Until account deletion',
|
||||
}
|
||||
return mapping[retention] || 'Session'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die vollstaendige Cookie Banner Konfiguration
|
||||
*/
|
||||
export function generateCookieBannerConfig(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
customTexts?: Partial<CookieBannerTexts>,
|
||||
customStyling?: Partial<CookieBannerStyling>
|
||||
): CookieBannerConfig {
|
||||
const categories = generateCookieCategories(dataPoints)
|
||||
|
||||
return {
|
||||
id: `cookie-banner-${tenantId}`,
|
||||
tenantId,
|
||||
categories,
|
||||
styling: {
|
||||
...DEFAULT_COOKIE_BANNER_STYLING,
|
||||
...customStyling,
|
||||
},
|
||||
texts: {
|
||||
...DEFAULT_COOKIE_BANNER_TEXTS,
|
||||
...customTexts,
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMBED CODE GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert den Embed-Code fuer den Cookie Banner
|
||||
*/
|
||||
export function generateEmbedCode(
|
||||
config: CookieBannerConfig,
|
||||
privacyPolicyUrl: string = '/datenschutz'
|
||||
): CookieBannerEmbedCode {
|
||||
const css = generateCSS(config.styling)
|
||||
const html = generateHTML(config, privacyPolicyUrl)
|
||||
const js = generateJS(config)
|
||||
|
||||
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
|
||||
|
||||
return {
|
||||
html,
|
||||
css,
|
||||
js,
|
||||
scriptTag,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das CSS fuer den Cookie Banner
|
||||
*/
|
||||
function generateCSS(styling: CookieBannerStyling): string {
|
||||
const positionStyles: Record<string, string> = {
|
||||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||||
TOP: 'top: 0; left: 0; right: 0;',
|
||||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
||||
}
|
||||
|
||||
const isDark = styling.theme === 'DARK'
|
||||
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
|
||||
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
|
||||
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||||
|
||||
return `
|
||||
/* Cookie Banner Styles */
|
||||
.cookie-banner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 9998;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-overlay.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.cookie-banner {
|
||||
position: fixed;
|
||||
${positionStyles[styling.position]}
|
||||
z-index: 9999;
|
||||
background: ${bgColor};
|
||||
color: ${textColor};
|
||||
border-radius: ${styling.borderRadius || 12}px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
max-width: ${styling.maxWidth}px;
|
||||
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cookie-banner.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cookie-banner-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 12px 20px;
|
||||
border-radius: ${(styling.borderRadius || 12) / 2}px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary {
|
||||
background: ${styling.primaryColor};
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cookie-banner-btn-primary:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary {
|
||||
background: ${styling.secondaryColor || borderColor};
|
||||
color: ${textColor};
|
||||
}
|
||||
|
||||
.cookie-banner-btn-secondary:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.cookie-banner-link {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: ${styling.primaryColor};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cookie-banner-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Category Details */
|
||||
.cookie-banner-details {
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid ${borderColor};
|
||||
padding-top: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cookie-banner-details.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cookie-banner-category {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
.cookie-banner-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cookie-banner-category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cookie-banner-category-name {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cookie-banner-category-desc {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
background: ${borderColor};
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active {
|
||||
background: ${styling.primaryColor};
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cookie-banner-toggle.active::after {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cookie-banner {
|
||||
margin: 0;
|
||||
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cookie-banner-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cookie-banner-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das HTML fuer den Cookie Banner
|
||||
*/
|
||||
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
|
||||
const categoriesHTML = config.categories
|
||||
.map((cat) => {
|
||||
const isRequired = cat.isRequired
|
||||
return `
|
||||
<div class="cookie-banner-category" data-category="${cat.id}">
|
||||
<div class="cookie-banner-category-info">
|
||||
<div class="cookie-banner-category-name">${cat.name.de}</div>
|
||||
<div class="cookie-banner-category-desc">${cat.description.de}</div>
|
||||
</div>
|
||||
<div class="cookie-banner-toggle ${cat.defaultEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
|
||||
data-category="${cat.id}"
|
||||
data-required="${isRequired}"></div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `
|
||||
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
|
||||
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
|
||||
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
|
||||
<div class="cookie-banner-description">${config.texts.description.de}</div>
|
||||
|
||||
<div class="cookie-banner-buttons">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
|
||||
${config.texts.rejectAll.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
|
||||
${config.texts.customize.de}
|
||||
</button>
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
|
||||
${config.texts.acceptAll.de}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cookie-banner-details" id="cookieBannerDetails">
|
||||
${categoriesHTML}
|
||||
<div class="cookie-banner-buttons" style="margin-top: 16px;">
|
||||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
|
||||
${config.texts.save.de}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
|
||||
${config.texts.privacyPolicyLink.de}
|
||||
</a>
|
||||
</div>
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert das JavaScript fuer den Cookie Banner
|
||||
*/
|
||||
function generateJS(config: CookieBannerConfig): string {
|
||||
const categoryIds = config.categories.map((c) => c.id)
|
||||
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
|
||||
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const COOKIE_NAME = 'cookie_consent';
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
const CATEGORIES = ${JSON.stringify(categoryIds)};
|
||||
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
|
||||
|
||||
// Get consent from cookie
|
||||
function getConsent() {
|
||||
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||||
if (!cookie) return null;
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save consent to cookie
|
||||
function saveConsent(consent) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
|
||||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
|
||||
';expires=' + date.toUTCString() +
|
||||
';path=/;SameSite=Lax';
|
||||
|
||||
// Dispatch event
|
||||
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
|
||||
}
|
||||
|
||||
// Check if category is consented
|
||||
function hasConsent(category) {
|
||||
const consent = getConsent();
|
||||
if (!consent) return REQUIRED_CATEGORIES.includes(category);
|
||||
return consent[category] === true;
|
||||
}
|
||||
|
||||
// Initialize banner
|
||||
function initBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
const details = document.getElementById('cookieBannerDetails');
|
||||
|
||||
if (!banner) return;
|
||||
|
||||
const consent = getConsent();
|
||||
if (consent) {
|
||||
// User has already consented
|
||||
return;
|
||||
}
|
||||
|
||||
// Show banner
|
||||
setTimeout(() => {
|
||||
banner.classList.add('active');
|
||||
overlay.classList.add('active');
|
||||
}, 500);
|
||||
|
||||
// Accept all
|
||||
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = true);
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Reject all (only essential)
|
||||
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Customize
|
||||
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
|
||||
details.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Save selection
|
||||
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
|
||||
const consent = {};
|
||||
CATEGORIES.forEach(cat => {
|
||||
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
|
||||
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
|
||||
});
|
||||
saveConsent(consent);
|
||||
closeBanner();
|
||||
});
|
||||
|
||||
// Toggle handlers
|
||||
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
|
||||
if (toggle.dataset.required === 'true') return;
|
||||
toggle.addEventListener('click', () => {
|
||||
toggle.classList.toggle('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Close on overlay click
|
||||
overlay?.addEventListener('click', () => {
|
||||
// Don't close - user must make a choice
|
||||
});
|
||||
}
|
||||
|
||||
function closeBanner() {
|
||||
const banner = document.getElementById('cookieBanner');
|
||||
const overlay = document.getElementById('cookieBannerOverlay');
|
||||
banner?.classList.remove('active');
|
||||
overlay?.classList.remove('active');
|
||||
}
|
||||
|
||||
// Expose API
|
||||
window.CookieConsent = {
|
||||
getConsent,
|
||||
saveConsent,
|
||||
hasConsent,
|
||||
show: () => {
|
||||
document.getElementById('cookieBanner')?.classList.add('active');
|
||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||||
},
|
||||
hide: closeBanner
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initBanner);
|
||||
} else {
|
||||
initBanner();
|
||||
}
|
||||
})();
|
||||
`.trim()
|
||||
}
|
||||
|
||||
// Note: All exports are defined inline with 'export const' and 'export function'
|
||||
965
admin-v2/lib/sdk/einwilligungen/generator/privacy-policy.ts
Normal file
965
admin-v2/lib/sdk/einwilligungen/generator/privacy-policy.ts
Normal file
@@ -0,0 +1,965 @@
|
||||
/**
|
||||
* Privacy Policy Generator
|
||||
*
|
||||
* Generiert Datenschutzerklaerungen (DSI) aus dem Datenpunktkatalog.
|
||||
* Die DSI wird aus 9 Abschnitten generiert:
|
||||
*
|
||||
* 1. Verantwortlicher (companyInfo)
|
||||
* 2. Erhobene Daten (dataPoints nach Kategorie)
|
||||
* 3. Verarbeitungszwecke (dataPoints.purpose)
|
||||
* 4. Rechtsgrundlagen (dataPoints.legalBasis)
|
||||
* 5. Empfaenger/Dritte (dataPoints.thirdPartyRecipients)
|
||||
* 6. Speicherdauer (retentionMatrix)
|
||||
* 7. Betroffenenrechte (statischer Text + Links)
|
||||
* 8. Cookies (cookieCategory-basiert)
|
||||
* 9. Aenderungen (statischer Text + Versionierung)
|
||||
*/
|
||||
|
||||
import {
|
||||
DataPoint,
|
||||
DataPointCategory,
|
||||
CompanyInfo,
|
||||
PrivacyPolicySection,
|
||||
GeneratedPrivacyPolicy,
|
||||
SupportedLanguage,
|
||||
ExportFormat,
|
||||
LocalizedText,
|
||||
RetentionMatrixEntry,
|
||||
LegalBasis,
|
||||
CATEGORY_METADATA,
|
||||
LEGAL_BASIS_INFO,
|
||||
RETENTION_PERIOD_INFO,
|
||||
ARTICLE_9_WARNING,
|
||||
} from '../types'
|
||||
import { RETENTION_MATRIX } from '../catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// KONSTANTEN - 18 Kategorien in der richtigen Reihenfolge
|
||||
// =============================================================================
|
||||
|
||||
const ALL_CATEGORIES: DataPointCategory[] = [
|
||||
'MASTER_DATA', // A
|
||||
'CONTACT_DATA', // B
|
||||
'AUTHENTICATION', // C
|
||||
'CONSENT', // D
|
||||
'COMMUNICATION', // E
|
||||
'PAYMENT', // F
|
||||
'USAGE_DATA', // G
|
||||
'LOCATION', // H
|
||||
'DEVICE_DATA', // I
|
||||
'MARKETING', // J
|
||||
'ANALYTICS', // K
|
||||
'SOCIAL_MEDIA', // L
|
||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
||||
'CONTRACT_DATA', // O
|
||||
'LOG_DATA', // P
|
||||
'AI_DATA', // Q - AI Act
|
||||
'SECURITY', // R
|
||||
]
|
||||
|
||||
// Alle Rechtsgrundlagen in der richtigen Reihenfolge
|
||||
const ALL_LEGAL_BASES: LegalBasis[] = [
|
||||
'CONTRACT',
|
||||
'CONSENT',
|
||||
'EXPLICIT_CONSENT',
|
||||
'LEGITIMATE_INTEREST',
|
||||
'LEGAL_OBLIGATION',
|
||||
'VITAL_INTERESTS',
|
||||
'PUBLIC_INTEREST',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Holt den lokalisierten Text
|
||||
*/
|
||||
function t(text: LocalizedText, language: SupportedLanguage): string {
|
||||
return text[language]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Kategorie
|
||||
*/
|
||||
function groupByCategory(dataPoints: DataPoint[]): Map<DataPointCategory, DataPoint[]> {
|
||||
const grouped = new Map<DataPointCategory, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.category) || []
|
||||
grouped.set(dp.category, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Datenpunkte nach Rechtsgrundlage
|
||||
*/
|
||||
function groupByLegalBasis(dataPoints: DataPoint[]): Map<LegalBasis, DataPoint[]> {
|
||||
const grouped = new Map<LegalBasis, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const existing = grouped.get(dp.legalBasis) || []
|
||||
grouped.set(dp.legalBasis, [...existing, dp])
|
||||
}
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert alle einzigartigen Drittanbieter
|
||||
*/
|
||||
function extractThirdParties(dataPoints: DataPoint[]): string[] {
|
||||
const thirdParties = new Set<string>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
thirdParties.add(recipient)
|
||||
}
|
||||
}
|
||||
return Array.from(thirdParties).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert ein Datum fuer die Anzeige
|
||||
*/
|
||||
function formatDate(date: Date, language: SupportedLanguage): string {
|
||||
return date.toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECTION GENERATORS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Abschnitt 1: Verantwortlicher
|
||||
*/
|
||||
function generateControllerSection(
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '1. Verantwortlicher',
|
||||
en: '1. Data Controller',
|
||||
}
|
||||
|
||||
const dpoSection = companyInfo.dpoName
|
||||
? language === 'de'
|
||||
? `\n\n**Datenschutzbeauftragter:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nE-Mail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nTelefon: ${companyInfo.dpoPhone}` : ''}`
|
||||
: `\n\n**Data Protection Officer:**\n${companyInfo.dpoName}${companyInfo.dpoEmail ? `\nEmail: ${companyInfo.dpoEmail}` : ''}${companyInfo.dpoPhone ? `\nPhone: ${companyInfo.dpoPhone}` : ''}`
|
||||
: ''
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Verantwortlich fuer die Datenverarbeitung auf dieser Website ist:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
E-Mail: ${companyInfo.email}${companyInfo.phone ? `\nTelefon: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nHandelsregister: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nUSt-IdNr.: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
en: `The controller responsible for data processing on this website is:
|
||||
|
||||
**${companyInfo.name}**
|
||||
${companyInfo.address}
|
||||
${companyInfo.postalCode} ${companyInfo.city}
|
||||
${companyInfo.country}
|
||||
|
||||
Email: ${companyInfo.email}${companyInfo.phone ? `\nPhone: ${companyInfo.phone}` : ''}${companyInfo.website ? `\nWebsite: ${companyInfo.website}` : ''}${companyInfo.registrationNumber ? `\n\nCommercial Register: ${companyInfo.registrationNumber}` : ''}${companyInfo.vatId ? `\nVAT ID: ${companyInfo.vatId}` : ''}${dpoSection}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'controller',
|
||||
order: 1,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 2: Erhobene Daten (18 Kategorien)
|
||||
*/
|
||||
function generateDataCollectionSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '2. Erhobene personenbezogene Daten',
|
||||
en: '2. Personal Data We Collect',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
// Prüfe ob Art. 9 Daten enthalten sind
|
||||
const hasSpecialCategoryData = dataPoints.some(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
for (const category of ALL_CATEGORIES) {
|
||||
const categoryData = grouped.get(category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryMeta = CATEGORY_METADATA[category]
|
||||
if (!categoryMeta) continue
|
||||
|
||||
const categoryTitle = t(categoryMeta.name, language)
|
||||
|
||||
// Spezielle Warnung für Art. 9 DSGVO Daten (Gesundheitsdaten)
|
||||
let categoryNote = ''
|
||||
if (category === 'HEALTH_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Diese Daten gehoeren zu den besonderen Kategorien personenbezogener Daten gemaess Art. 9 DSGVO und erfordern eine ausdrueckliche Einwilligung.`
|
||||
: `\n\n> **Note:** This data belongs to special categories of personal data under Art. 9 GDPR and requires explicit consent.`
|
||||
} else if (category === 'EMPLOYEE_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von Beschaeftigtendaten erfolgt gemaess § 26 BDSG.`
|
||||
: `\n\n> **Note:** Processing of employee data is carried out in accordance with § 26 BDSG (German Federal Data Protection Act).`
|
||||
} else if (category === 'AI_DATA') {
|
||||
categoryNote = language === 'de'
|
||||
? `\n\n> **Hinweis:** Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.`
|
||||
: `\n\n> **Note:** Processing of AI-related data is subject to AI Act transparency requirements.`
|
||||
}
|
||||
|
||||
const dataList = categoryData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- **${t(dp.name, language)}**${specialTag}: ${t(dp.description, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryMeta.code}. ${categoryTitle}\n\n${dataList}${categoryNote}`)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: 'Wir erheben und verarbeiten die folgenden personenbezogenen Daten:',
|
||||
en: 'We collect and process the following personal data:',
|
||||
}
|
||||
|
||||
// Zusätzlicher Hinweis für Art. 9 Daten
|
||||
const specialCategoryNote: LocalizedText = hasSpecialCategoryData
|
||||
? {
|
||||
de: '\n\n**Wichtig:** Einige der unten aufgefuehrten Daten gehoeren zu den besonderen Kategorien personenbezogener Daten nach Art. 9 DSGVO und werden nur mit Ihrer ausdruecklichen Einwilligung verarbeitet.',
|
||||
en: '\n\n**Important:** Some of the data listed below belongs to special categories of personal data under Art. 9 GDPR and is only processed with your explicit consent.',
|
||||
}
|
||||
: { de: '', en: '' }
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}${specialCategoryNote.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}${specialCategoryNote.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'data-collection',
|
||||
order: 2,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 3: Verarbeitungszwecke
|
||||
*/
|
||||
function generatePurposesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '3. Zwecke der Datenverarbeitung',
|
||||
en: '3. Purposes of Data Processing',
|
||||
}
|
||||
|
||||
// Gruppiere nach Zweck (unique purposes)
|
||||
const purposes = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
const purpose = t(dp.purpose, language)
|
||||
const existing = purposes.get(purpose) || []
|
||||
purposes.set(purpose, [...existing, dp])
|
||||
}
|
||||
|
||||
const purposeList = Array.from(purposes.entries())
|
||||
.map(([purpose, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${purpose}**\n Betroffene Daten: ${dataNames}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir verarbeiten Ihre personenbezogenen Daten fuer folgende Zwecke:\n\n${purposeList}`,
|
||||
en: `We process your personal data for the following purposes:\n\n${purposeList}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'purposes',
|
||||
order: 3,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 4: Rechtsgrundlagen (alle 7 Rechtsgrundlagen)
|
||||
*/
|
||||
function generateLegalBasisSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '4. Rechtsgrundlagen der Verarbeitung',
|
||||
en: '4. Legal Basis for Processing',
|
||||
}
|
||||
|
||||
const grouped = groupByLegalBasis(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
// Alle 7 Rechtsgrundlagen in der richtigen Reihenfolge
|
||||
for (const basis of ALL_LEGAL_BASES) {
|
||||
const basisData = grouped.get(basis)
|
||||
if (!basisData || basisData.length === 0) continue
|
||||
|
||||
const basisInfo = LEGAL_BASIS_INFO[basis]
|
||||
if (!basisInfo) continue
|
||||
|
||||
const basisTitle = `${t(basisInfo.name, language)} (${basisInfo.article})`
|
||||
const basisDesc = t(basisInfo.description, language)
|
||||
|
||||
// Für Art. 9 Daten (EXPLICIT_CONSENT) zusätzliche Warnung hinzufügen
|
||||
let additionalWarning = ''
|
||||
if (basis === 'EXPLICIT_CONSENT') {
|
||||
additionalWarning = language === 'de'
|
||||
? `\n\n> **Wichtig:** Fuer die Verarbeitung dieser besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO) ist eine separate, ausdrueckliche Einwilligung erforderlich, die Sie jederzeit widerrufen koennen.`
|
||||
: `\n\n> **Important:** Processing of these special categories of personal data (Art. 9 GDPR) requires a separate, explicit consent that you can withdraw at any time.`
|
||||
}
|
||||
|
||||
const dataLabel = language === 'de' ? 'Betroffene Daten' : 'Affected Data'
|
||||
const dataList = basisData
|
||||
.map((dp) => {
|
||||
const specialTag = dp.isSpecialCategory
|
||||
? (language === 'de' ? ' *(Art. 9 DSGVO)*' : ' *(Art. 9 GDPR)*')
|
||||
: ''
|
||||
return `- ${t(dp.name, language)}${specialTag}: ${t(dp.legalBasisJustification, language)}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${basisTitle}\n\n${basisDesc}${additionalWarning}\n\n**${dataLabel}:**\n${dataList}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf Grundlage folgender Rechtsgrundlagen:\n\n${sections.join('\n\n')}`,
|
||||
en: `The processing of your personal data is based on the following legal grounds:\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'legal-basis',
|
||||
order: 4,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 5: Empfaenger / Dritte
|
||||
*/
|
||||
function generateRecipientsSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '5. Empfaenger und Datenweitergabe',
|
||||
en: '5. Recipients and Data Sharing',
|
||||
}
|
||||
|
||||
const thirdParties = extractThirdParties(dataPoints)
|
||||
|
||||
if (thirdParties.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir geben Ihre personenbezogenen Daten grundsaetzlich nicht an Dritte weiter, es sei denn, dies ist zur Vertragserfuellung erforderlich oder Sie haben ausdruecklich eingewilligt.',
|
||||
en: 'We generally do not share your personal data with third parties unless this is necessary for contract performance or you have expressly consented.',
|
||||
}
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Drittanbieter
|
||||
const recipientDetails = new Map<string, DataPoint[]>()
|
||||
for (const dp of dataPoints) {
|
||||
for (const recipient of dp.thirdPartyRecipients) {
|
||||
const existing = recipientDetails.get(recipient) || []
|
||||
recipientDetails.set(recipient, [...existing, dp])
|
||||
}
|
||||
}
|
||||
|
||||
const recipientList = Array.from(recipientDetails.entries())
|
||||
.map(([recipient, dps]) => {
|
||||
const dataNames = dps.map((dp) => t(dp.name, language)).join(', ')
|
||||
return `- **${recipient}**: ${dataNames}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir uebermitteln Ihre personenbezogenen Daten an folgende Empfaenger bzw. Kategorien von Empfaengern:\n\n${recipientList}\n\nMit allen Auftragsverarbeitern haben wir Auftragsverarbeitungsvertraege nach Art. 28 DSGVO abgeschlossen.`,
|
||||
en: `We share your personal data with the following recipients or categories of recipients:\n\n${recipientList}\n\nWe have concluded data processing agreements pursuant to Art. 28 GDPR with all processors.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'recipients',
|
||||
order: 5,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.filter((dp) => dp.thirdPartyRecipients.length > 0).map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 6: Speicherdauer
|
||||
*/
|
||||
function generateRetentionSection(
|
||||
dataPoints: DataPoint[],
|
||||
retentionMatrix: RetentionMatrixEntry[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '6. Speicherdauer',
|
||||
en: '6. Data Retention',
|
||||
}
|
||||
|
||||
const grouped = groupByCategory(dataPoints)
|
||||
const sections: string[] = []
|
||||
|
||||
for (const entry of retentionMatrix) {
|
||||
const categoryData = grouped.get(entry.category)
|
||||
if (!categoryData || categoryData.length === 0) continue
|
||||
|
||||
const categoryName = t(entry.categoryName, language)
|
||||
const standardPeriod = t(RETENTION_PERIOD_INFO[entry.standardPeriod].label, language)
|
||||
|
||||
const dataRetention = categoryData
|
||||
.map((dp) => {
|
||||
const period = t(RETENTION_PERIOD_INFO[dp.retentionPeriod].label, language)
|
||||
return `- ${t(dp.name, language)}: ${period}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
sections.push(`### ${categoryName}\n\n**Standardfrist:** ${standardPeriod}\n\n${dataRetention}`)
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Wir speichern Ihre personenbezogenen Daten nur so lange, wie dies fuer die jeweiligen Zwecke erforderlich ist oder gesetzliche Aufbewahrungsfristen bestehen.\n\n${sections.join('\n\n')}`,
|
||||
en: `We store your personal data only for as long as is necessary for the respective purposes or as required by statutory retention periods.\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'retention',
|
||||
order: 6,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: dataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 6a: Besondere Kategorien (Art. 9 DSGVO)
|
||||
* Wird nur generiert, wenn Art. 9 Daten vorhanden sind
|
||||
*/
|
||||
function generateSpecialCategoriesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection | null {
|
||||
// Filtere Art. 9 Datenpunkte
|
||||
const specialCategoryDataPoints = dataPoints.filter(dp => dp.isSpecialCategory || dp.category === 'HEALTH_DATA')
|
||||
|
||||
if (specialCategoryDataPoints.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const title: LocalizedText = {
|
||||
de: '6a. Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: '6a. Special Categories of Personal Data (Art. 9 GDPR)',
|
||||
}
|
||||
|
||||
const dataList = specialCategoryDataPoints
|
||||
.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.description, language)}`)
|
||||
.join('\n')
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Gemaess Art. 9 DSGVO verarbeiten wir auch besondere Kategorien personenbezogener Daten, die einen erhoehten Schutz geniessen. Diese Daten umfassen:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Ihre ausdrueckliche Einwilligung
|
||||
|
||||
Die Verarbeitung dieser besonderen Kategorien personenbezogener Daten erfolgt nur auf Grundlage Ihrer **ausdruecklichen Einwilligung** gemaess Art. 9 Abs. 2 lit. a DSGVO.
|
||||
|
||||
### Ihre Rechte bei Art. 9 Daten
|
||||
|
||||
- Sie koennen Ihre Einwilligung **jederzeit widerrufen**
|
||||
- Der Widerruf beruehrt nicht die Rechtmaessigkeit der bisherigen Verarbeitung
|
||||
- Bei Widerruf werden Ihre Daten unverzueglich geloescht
|
||||
- Sie haben das Recht auf **Auskunft, Berichtigung und Loeschung**
|
||||
|
||||
### Besondere Schutzmassnahmen
|
||||
|
||||
Fuer diese sensiblen Daten haben wir besondere technische und organisatorische Massnahmen implementiert:
|
||||
- Ende-zu-Ende-Verschluesselung
|
||||
- Strenge Zugriffskontrolle (Need-to-Know-Prinzip)
|
||||
- Audit-Logging aller Zugriffe
|
||||
- Regelmaessige Datenschutz-Folgenabschaetzungen`,
|
||||
en: `In accordance with Art. 9 GDPR, we also process special categories of personal data that enjoy enhanced protection. This data includes:
|
||||
|
||||
${dataList}
|
||||
|
||||
### Your Explicit Consent
|
||||
|
||||
Processing of these special categories of personal data only takes place on the basis of your **explicit consent** pursuant to Art. 9(2)(a) GDPR.
|
||||
|
||||
### Your Rights Regarding Art. 9 Data
|
||||
|
||||
- You can **withdraw your consent at any time**
|
||||
- Withdrawal does not affect the lawfulness of previous processing
|
||||
- Upon withdrawal, your data will be deleted immediately
|
||||
- You have the right to **access, rectification, and erasure**
|
||||
|
||||
### Special Protection Measures
|
||||
|
||||
For this sensitive data, we have implemented special technical and organizational measures:
|
||||
- End-to-end encryption
|
||||
- Strict access control (need-to-know principle)
|
||||
- Audit logging of all access
|
||||
- Regular data protection impact assessments`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'special-categories',
|
||||
order: 6.5, // Zwischen Speicherdauer (6) und Rechte (7)
|
||||
title,
|
||||
content,
|
||||
dataPointIds: specialCategoryDataPoints.map((dp) => dp.id),
|
||||
isRequired: false,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 7: Betroffenenrechte
|
||||
*/
|
||||
function generateRightsSection(language: SupportedLanguage): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '7. Ihre Rechte als betroffene Person',
|
||||
en: '7. Your Rights as a Data Subject',
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Sie haben gegenueber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:
|
||||
|
||||
### Auskunftsrecht (Art. 15 DSGVO)
|
||||
Sie haben das Recht, Auskunft ueber die von uns verarbeiteten personenbezogenen Daten zu verlangen.
|
||||
|
||||
### Recht auf Berichtigung (Art. 16 DSGVO)
|
||||
Sie haben das Recht, die Berichtigung unrichtiger oder die Vervollstaendigung unvollstaendiger Daten zu verlangen.
|
||||
|
||||
### Recht auf Loeschung (Art. 17 DSGVO)
|
||||
Sie haben das Recht, die Loeschung Ihrer personenbezogenen Daten zu verlangen, sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
|
||||
|
||||
### Recht auf Einschraenkung der Verarbeitung (Art. 18 DSGVO)
|
||||
Sie haben das Recht, die Einschraenkung der Verarbeitung Ihrer Daten zu verlangen.
|
||||
|
||||
### Recht auf Datenuebertragbarkeit (Art. 20 DSGVO)
|
||||
Sie haben das Recht, Ihre Daten in einem strukturierten, gaengigen und maschinenlesbaren Format zu erhalten.
|
||||
|
||||
### Widerspruchsrecht (Art. 21 DSGVO)
|
||||
Sie haben das Recht, der Verarbeitung Ihrer Daten jederzeit zu widersprechen, soweit die Verarbeitung auf berechtigtem Interesse beruht.
|
||||
|
||||
### Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
|
||||
Sie haben das Recht, Ihre erteilte Einwilligung jederzeit zu widerrufen. Die Rechtmaessigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung wird dadurch nicht beruehrt.
|
||||
|
||||
### Beschwerderecht bei der Aufsichtsbehoerde (Art. 77 DSGVO)
|
||||
Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehoerde ueber die Verarbeitung Ihrer personenbezogenen Daten zu beschweren.
|
||||
|
||||
**Zur Ausuebung Ihrer Rechte wenden Sie sich bitte an die oben angegebenen Kontaktdaten.**`,
|
||||
en: `You have the following rights regarding your personal data:
|
||||
|
||||
### Right of Access (Art. 15 GDPR)
|
||||
You have the right to request information about the personal data we process about you.
|
||||
|
||||
### Right to Rectification (Art. 16 GDPR)
|
||||
You have the right to request the correction of inaccurate data or the completion of incomplete data.
|
||||
|
||||
### Right to Erasure (Art. 17 GDPR)
|
||||
You have the right to request the deletion of your personal data, unless statutory retention obligations apply.
|
||||
|
||||
### Right to Restriction of Processing (Art. 18 GDPR)
|
||||
You have the right to request the restriction of processing of your data.
|
||||
|
||||
### Right to Data Portability (Art. 20 GDPR)
|
||||
You have the right to receive your data in a structured, commonly used, and machine-readable format.
|
||||
|
||||
### Right to Object (Art. 21 GDPR)
|
||||
You have the right to object to the processing of your data at any time, insofar as the processing is based on legitimate interest.
|
||||
|
||||
### Right to Withdraw Consent (Art. 7(3) GDPR)
|
||||
You have the right to withdraw your consent at any time. The lawfulness of processing based on consent before its withdrawal is not affected.
|
||||
|
||||
### Right to Lodge a Complaint with a Supervisory Authority (Art. 77 GDPR)
|
||||
You have the right to lodge a complaint with a data protection supervisory authority about the processing of your personal data.
|
||||
|
||||
**To exercise your rights, please contact us using the contact details provided above.**`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'rights',
|
||||
order: 7,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 8: Cookies
|
||||
*/
|
||||
function generateCookiesSection(
|
||||
dataPoints: DataPoint[],
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '8. Cookies und aehnliche Technologien',
|
||||
en: '8. Cookies and Similar Technologies',
|
||||
}
|
||||
|
||||
// Filtere Datenpunkte mit Cookie-Kategorie
|
||||
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
||||
|
||||
if (cookieDataPoints.length === 0) {
|
||||
const content: LocalizedText = {
|
||||
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
||||
en: 'We do not use cookies on this website.',
|
||||
}
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: false,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Gruppiere nach Cookie-Kategorie
|
||||
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
||||
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
||||
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
||||
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
||||
|
||||
const sections: string[] = []
|
||||
|
||||
if (essential.length > 0) {
|
||||
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
||||
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (performance.length > 0) {
|
||||
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
||||
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (personalization.length > 0) {
|
||||
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
||||
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
if (externalMedia.length > 0) {
|
||||
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
||||
sections.push(
|
||||
language === 'de'
|
||||
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
||||
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
||||
)
|
||||
}
|
||||
|
||||
const intro: LocalizedText = {
|
||||
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
||||
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
||||
}
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
||||
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'cookies',
|
||||
order: 8,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
||||
isRequired: true,
|
||||
isGenerated: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abschnitt 9: Aenderungen
|
||||
*/
|
||||
function generateChangesSection(
|
||||
version: string,
|
||||
date: Date,
|
||||
language: SupportedLanguage
|
||||
): PrivacyPolicySection {
|
||||
const title: LocalizedText = {
|
||||
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
||||
en: '9. Changes to this Privacy Policy',
|
||||
}
|
||||
|
||||
const formattedDate = formatDate(date, language)
|
||||
|
||||
const content: LocalizedText = {
|
||||
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
||||
|
||||
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
||||
|
||||
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
||||
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
||||
|
||||
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
||||
|
||||
The new privacy policy will then apply for your next visit.`,
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'changes',
|
||||
order: 9,
|
||||
title,
|
||||
content,
|
||||
dataPointIds: [],
|
||||
isRequired: true,
|
||||
isGenerated: false,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN GENERATOR FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generiert alle Abschnitte der Privacy Policy (18 Kategorien + Art. 9)
|
||||
*/
|
||||
export function generatePrivacyPolicySections(
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
version: string = '1.0.0'
|
||||
): PrivacyPolicySection[] {
|
||||
const now = new Date()
|
||||
|
||||
const sections: PrivacyPolicySection[] = [
|
||||
generateControllerSection(companyInfo, language),
|
||||
generateDataCollectionSection(dataPoints, language),
|
||||
generatePurposesSection(dataPoints, language),
|
||||
generateLegalBasisSection(dataPoints, language),
|
||||
generateRecipientsSection(dataPoints, language),
|
||||
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
||||
]
|
||||
|
||||
// Art. 9 DSGVO Abschnitt nur einfügen, wenn besondere Kategorien vorhanden
|
||||
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
||||
if (specialCategoriesSection) {
|
||||
sections.push(specialCategoriesSection)
|
||||
}
|
||||
|
||||
sections.push(
|
||||
generateRightsSection(language),
|
||||
generateCookiesSection(dataPoints, language),
|
||||
generateChangesSection(version, now, language)
|
||||
)
|
||||
|
||||
// Abschnittsnummern neu vergeben
|
||||
sections.forEach((section, index) => {
|
||||
section.order = index + 1
|
||||
// Titel-Nummer aktualisieren
|
||||
const titleDe = section.title.de
|
||||
const titleEn = section.title.en
|
||||
if (titleDe.match(/^\d+[a-z]?\./)) {
|
||||
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
if (titleEn.match(/^\d+[a-z]?\./)) {
|
||||
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
||||
}
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert die vollstaendige Privacy Policy
|
||||
*/
|
||||
export function generatePrivacyPolicy(
|
||||
tenantId: string,
|
||||
dataPoints: DataPoint[],
|
||||
companyInfo: CompanyInfo,
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat = 'HTML'
|
||||
): GeneratedPrivacyPolicy {
|
||||
const version = '1.0.0'
|
||||
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
||||
|
||||
// Generiere den Inhalt
|
||||
const content = renderPrivacyPolicy(sections, language, format)
|
||||
|
||||
return {
|
||||
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
||||
tenantId,
|
||||
language,
|
||||
sections,
|
||||
companyInfo,
|
||||
generatedAt: new Date(),
|
||||
version,
|
||||
format,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert die Privacy Policy im gewuenschten Format
|
||||
*/
|
||||
function renderPrivacyPolicy(
|
||||
sections: PrivacyPolicySection[],
|
||||
language: SupportedLanguage,
|
||||
format: ExportFormat
|
||||
): string {
|
||||
switch (format) {
|
||||
case 'HTML':
|
||||
return renderAsHTML(sections, language)
|
||||
case 'MARKDOWN':
|
||||
return renderAsMarkdown(sections, language)
|
||||
default:
|
||||
return renderAsMarkdown(sections, language)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert als HTML
|
||||
*/
|
||||
function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsHTML = sections
|
||||
.map((section) => {
|
||||
const content = t(section.content, language)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/### (.+)/g, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
||||
|
||||
return `
|
||||
<section id="${section.id}">
|
||||
<h2>${t(section.title, language)}</h2>
|
||||
<p>${content}</p>
|
||||
</section>
|
||||
`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="${language}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
||||
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
||||
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
||||
p { margin: 1rem 0; }
|
||||
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
||||
li { margin: 0.5rem 0; }
|
||||
strong { font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${title}</h1>
|
||||
${sectionsHTML}
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert als Markdown
|
||||
*/
|
||||
function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
||||
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
||||
|
||||
const sectionsMarkdown = sections
|
||||
.map((section) => {
|
||||
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
||||
})
|
||||
.join('\n\n---\n\n')
|
||||
|
||||
return `# ${title}\n\n${sectionsMarkdown}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
generateControllerSection,
|
||||
generateDataCollectionSection,
|
||||
generatePurposesSection,
|
||||
generateLegalBasisSection,
|
||||
generateRecipientsSection,
|
||||
generateRetentionSection,
|
||||
generateSpecialCategoriesSection,
|
||||
generateRightsSection,
|
||||
generateCookiesSection,
|
||||
generateChangesSection,
|
||||
renderAsHTML,
|
||||
renderAsMarkdown,
|
||||
}
|
||||
79
admin-v2/lib/sdk/einwilligungen/index.ts
Normal file
79
admin-v2/lib/sdk/einwilligungen/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Datenpunktkatalog & Datenschutzinformationen-Generator
|
||||
*
|
||||
* Dieses Modul erweitert das SDK Einwilligungen-Modul um:
|
||||
* - Datenpunktkatalog mit 28 vordefinierten + kundenspezifischen Datenpunkten
|
||||
* - Automatische Privacy Policy Generierung
|
||||
* - Cookie Banner Konfiguration
|
||||
* - Retention Matrix Visualisierung
|
||||
*
|
||||
* @module lib/sdk/einwilligungen
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export * from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CATALOG
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
PREDEFINED_DATA_POINTS,
|
||||
RETENTION_MATRIX,
|
||||
DEFAULT_COOKIE_CATEGORIES,
|
||||
getDataPointById,
|
||||
getDataPointByCode,
|
||||
getDataPointsByCategory,
|
||||
getDataPointsByLegalBasis,
|
||||
getDataPointsByCookieCategory,
|
||||
getDataPointsRequiringConsent,
|
||||
getHighRiskDataPoints,
|
||||
countDataPointsByCategory,
|
||||
countDataPointsByRiskLevel,
|
||||
createDefaultCatalog,
|
||||
searchDataPoints,
|
||||
} from './catalog/loader'
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
EinwilligungenProvider,
|
||||
useEinwilligungen,
|
||||
initialState as einwilligungenInitialState,
|
||||
einwilligungenReducer,
|
||||
} from './context'
|
||||
|
||||
// =============================================================================
|
||||
// GENERATORS (to be implemented)
|
||||
// =============================================================================
|
||||
|
||||
// Privacy Policy Generator
|
||||
export { generatePrivacyPolicy, generatePrivacyPolicySections } from './generator/privacy-policy'
|
||||
|
||||
// Cookie Banner Generator
|
||||
export { generateCookieBannerConfig, generateEmbedCode } from './generator/cookie-banner'
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT
|
||||
// =============================================================================
|
||||
|
||||
// PDF Export
|
||||
export {
|
||||
generatePDFContent as generatePrivacyPolicyPDFContent,
|
||||
generatePDFBlob as generatePrivacyPolicyPDFBlob,
|
||||
generatePDFFilename as generatePrivacyPolicyPDFFilename,
|
||||
} from './export/pdf'
|
||||
export type { PDFExportOptions as PrivacyPolicyPDFExportOptions } from './export/pdf'
|
||||
|
||||
// DOCX Export
|
||||
export {
|
||||
generateDOCXContent as generatePrivacyPolicyDOCXContent,
|
||||
generateDOCXBlob as generatePrivacyPolicyDOCXBlob,
|
||||
generateDOCXFilename as generatePrivacyPolicyDOCXFilename,
|
||||
} from './export/docx'
|
||||
export type { DOCXExportOptions as PrivacyPolicyDOCXExportOptions } from './export/docx'
|
||||
838
admin-v2/lib/sdk/einwilligungen/types.ts
Normal file
838
admin-v2/lib/sdk/einwilligungen/types.ts
Normal file
@@ -0,0 +1,838 @@
|
||||
/**
|
||||
* Datenpunktkatalog & Datenschutzinformationen-Generator
|
||||
* TypeScript Interfaces
|
||||
*
|
||||
* Dieses Modul definiert alle Typen für:
|
||||
* - Datenpunktkatalog (32 vordefinierte + kundenspezifische)
|
||||
* - Privacy Policy Generator
|
||||
* - Cookie Banner Configuration
|
||||
* - Retention Matrix
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kategorien für Datenpunkte (18 Kategorien: A-R)
|
||||
*/
|
||||
export type DataPointCategory =
|
||||
| 'MASTER_DATA' // A: Stammdaten
|
||||
| 'CONTACT_DATA' // B: Kontaktdaten
|
||||
| 'AUTHENTICATION' // C: Authentifizierungsdaten
|
||||
| 'CONSENT' // D: Einwilligungsdaten
|
||||
| 'COMMUNICATION' // E: Kommunikationsdaten
|
||||
| 'PAYMENT' // F: Zahlungsdaten
|
||||
| 'USAGE_DATA' // G: Nutzungsdaten
|
||||
| 'LOCATION' // H: Standortdaten
|
||||
| 'DEVICE_DATA' // I: Gerätedaten
|
||||
| 'MARKETING' // J: Marketingdaten
|
||||
| 'ANALYTICS' // K: Analysedaten
|
||||
| 'SOCIAL_MEDIA' // L: Social-Media-Daten
|
||||
| 'HEALTH_DATA' // M: Gesundheitsdaten (Art. 9 DSGVO)
|
||||
| 'EMPLOYEE_DATA' // N: Beschäftigtendaten
|
||||
| 'CONTRACT_DATA' // O: Vertragsdaten
|
||||
| 'LOG_DATA' // P: Protokolldaten
|
||||
| 'AI_DATA' // Q: KI-Daten
|
||||
| 'SECURITY' // R: Sicherheitsdaten
|
||||
|
||||
/**
|
||||
* Risikoniveau für Datenpunkte
|
||||
*/
|
||||
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
|
||||
/**
|
||||
* Rechtsgrundlagen nach DSGVO Art. 6 und Art. 9
|
||||
*/
|
||||
export type LegalBasis =
|
||||
| 'CONTRACT' // Art. 6 Abs. 1 lit. b DSGVO
|
||||
| 'CONSENT' // Art. 6 Abs. 1 lit. a DSGVO
|
||||
| 'EXPLICIT_CONSENT' // Art. 9 Abs. 2 lit. a DSGVO (für Art. 9 Daten)
|
||||
| 'LEGITIMATE_INTEREST' // Art. 6 Abs. 1 lit. f DSGVO
|
||||
| 'LEGAL_OBLIGATION' // Art. 6 Abs. 1 lit. c DSGVO
|
||||
| 'VITAL_INTERESTS' // Art. 6 Abs. 1 lit. d DSGVO
|
||||
| 'PUBLIC_INTEREST' // Art. 6 Abs. 1 lit. e DSGVO
|
||||
|
||||
/**
|
||||
* Aufbewahrungsfristen
|
||||
*/
|
||||
export type RetentionPeriod =
|
||||
| '24_HOURS'
|
||||
| '30_DAYS'
|
||||
| '90_DAYS'
|
||||
| '12_MONTHS'
|
||||
| '24_MONTHS'
|
||||
| '26_MONTHS' // Google Analytics Standard
|
||||
| '36_MONTHS'
|
||||
| '48_MONTHS'
|
||||
| '6_YEARS'
|
||||
| '10_YEARS'
|
||||
| 'UNTIL_REVOCATION'
|
||||
| 'UNTIL_PURPOSE_FULFILLED'
|
||||
| 'UNTIL_ACCOUNT_DELETION'
|
||||
|
||||
/**
|
||||
* Cookie-Kategorien für Cookie-Banner
|
||||
*/
|
||||
export type CookieCategory =
|
||||
| 'ESSENTIAL' // Technisch notwendig
|
||||
| 'PERFORMANCE' // Analyse & Performance
|
||||
| 'PERSONALIZATION' // Personalisierung
|
||||
| 'EXTERNAL_MEDIA' // Externe Medien
|
||||
|
||||
/**
|
||||
* Export-Formate für Privacy Policy
|
||||
*/
|
||||
export type ExportFormat = 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX'
|
||||
|
||||
/**
|
||||
* Sprachen
|
||||
*/
|
||||
export type SupportedLanguage = 'de' | 'en'
|
||||
|
||||
// =============================================================================
|
||||
// DATA POINT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Lokalisierter Text (DE/EN)
|
||||
*/
|
||||
export interface LocalizedText {
|
||||
de: string
|
||||
en: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelner Datenpunkt im Katalog
|
||||
*/
|
||||
export interface DataPoint {
|
||||
id: string
|
||||
code: string // z.B. "A1", "B2", "C3"
|
||||
category: DataPointCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
purpose: LocalizedText
|
||||
riskLevel: RiskLevel
|
||||
legalBasis: LegalBasis
|
||||
legalBasisJustification: LocalizedText
|
||||
retentionPeriod: RetentionPeriod
|
||||
retentionJustification: LocalizedText
|
||||
cookieCategory: CookieCategory | null // null = kein Cookie
|
||||
isSpecialCategory: boolean // Art. 9 DSGVO (sensible Daten)
|
||||
requiresExplicitConsent: boolean
|
||||
thirdPartyRecipients: string[]
|
||||
technicalMeasures: string[]
|
||||
tags: string[]
|
||||
isCustom?: boolean // Kundenspezifischer Datenpunkt
|
||||
isActive?: boolean // Aktiviert fuer diesen Tenant
|
||||
}
|
||||
|
||||
/**
|
||||
* YAML-Struktur fuer Datenpunkte (fuer Loader)
|
||||
*/
|
||||
export interface DataPointYAML {
|
||||
id: string
|
||||
code: string
|
||||
category: string
|
||||
name_de: string
|
||||
name_en: string
|
||||
description_de: string
|
||||
description_en: string
|
||||
purpose_de: string
|
||||
purpose_en: string
|
||||
risk_level: string
|
||||
legal_basis: string
|
||||
legal_basis_justification_de: string
|
||||
legal_basis_justification_en: string
|
||||
retention_period: string
|
||||
retention_justification_de: string
|
||||
retention_justification_en: string
|
||||
cookie_category: string | null
|
||||
is_special_category: boolean
|
||||
requires_explicit_consent: boolean
|
||||
third_party_recipients: string[]
|
||||
technical_measures: string[]
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CATALOG & RETENTION MATRIX
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Gesamter Datenpunktkatalog eines Tenants
|
||||
*/
|
||||
export interface DataPointCatalog {
|
||||
id: string
|
||||
tenantId: string
|
||||
version: string
|
||||
dataPoints: DataPoint[] // Vordefinierte (32)
|
||||
customDataPoints: DataPoint[] // Kundenspezifische
|
||||
retentionMatrix: RetentionMatrixEntry[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Eintrag in der Retention Matrix
|
||||
*/
|
||||
export interface RetentionMatrixEntry {
|
||||
category: DataPointCategory
|
||||
categoryName: LocalizedText
|
||||
standardPeriod: RetentionPeriod
|
||||
legalBasis: string
|
||||
exceptions: RetentionException[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausnahme von der Standard-Loeschfrist
|
||||
*/
|
||||
export interface RetentionException {
|
||||
condition: LocalizedText
|
||||
period: RetentionPeriod
|
||||
reason: LocalizedText
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PRIVACY POLICY GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Abschnitt in der Privacy Policy
|
||||
*/
|
||||
export interface PrivacyPolicySection {
|
||||
id: string
|
||||
order: number
|
||||
title: LocalizedText
|
||||
content: LocalizedText
|
||||
dataPointIds: string[]
|
||||
isRequired: boolean
|
||||
isGenerated: boolean // true = aus Datenpunkten generiert
|
||||
}
|
||||
|
||||
/**
|
||||
* Unternehmensinfo fuer Privacy Policy
|
||||
*/
|
||||
export interface CompanyInfo {
|
||||
name: string
|
||||
address: string
|
||||
city: string
|
||||
postalCode: string
|
||||
country: string
|
||||
email: string
|
||||
phone?: string
|
||||
website?: string
|
||||
dpoName?: string // Datenschutzbeauftragter
|
||||
dpoEmail?: string
|
||||
dpoPhone?: string
|
||||
registrationNumber?: string // Handelsregister
|
||||
vatId?: string // USt-IdNr
|
||||
}
|
||||
|
||||
/**
|
||||
* Generierte Privacy Policy
|
||||
*/
|
||||
export interface GeneratedPrivacyPolicy {
|
||||
id: string
|
||||
tenantId: string
|
||||
language: SupportedLanguage
|
||||
sections: PrivacyPolicySection[]
|
||||
companyInfo: CompanyInfo
|
||||
generatedAt: Date
|
||||
version: string
|
||||
format: ExportFormat
|
||||
content?: string // Rendered content (HTML/MD)
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionen fuer Privacy Policy Generierung
|
||||
*/
|
||||
export interface PrivacyPolicyGenerationOptions {
|
||||
language: SupportedLanguage
|
||||
format: ExportFormat
|
||||
includeDataPoints: string[] // Welche Datenpunkte einschliessen
|
||||
customSections?: PrivacyPolicySection[] // Zusaetzliche Abschnitte
|
||||
styling?: PrivacyPolicyStyling
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling-Optionen fuer PDF/HTML Export
|
||||
*/
|
||||
export interface PrivacyPolicyStyling {
|
||||
primaryColor?: string
|
||||
fontFamily?: string
|
||||
fontSize?: number
|
||||
headerFontSize?: number
|
||||
includeTableOfContents?: boolean
|
||||
includeDateFooter?: boolean
|
||||
logoUrl?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COOKIE BANNER CONFIG
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Einzelner Cookie in einer Kategorie
|
||||
*/
|
||||
export interface CookieInfo {
|
||||
name: string
|
||||
provider: string
|
||||
purpose: LocalizedText
|
||||
expiry: string
|
||||
type: 'FIRST_PARTY' | 'THIRD_PARTY'
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookie-Banner Kategorie
|
||||
*/
|
||||
export interface CookieBannerCategory {
|
||||
id: CookieCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
isRequired: boolean // Essentiell = required
|
||||
defaultEnabled: boolean
|
||||
dataPointIds: string[] // Verknuepfte Datenpunkte
|
||||
cookies: CookieInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerStyling {
|
||||
position: 'BOTTOM' | 'TOP' | 'CENTER'
|
||||
theme: 'LIGHT' | 'DARK' | 'CUSTOM'
|
||||
primaryColor?: string
|
||||
secondaryColor?: string
|
||||
textColor?: string
|
||||
backgroundColor?: string
|
||||
borderRadius?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Texte fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerTexts {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
acceptAll: LocalizedText
|
||||
rejectAll: LocalizedText
|
||||
customize: LocalizedText
|
||||
save: LocalizedText
|
||||
privacyPolicyLink: LocalizedText
|
||||
}
|
||||
|
||||
/**
|
||||
* Generierter Code fuer Cookie Banner
|
||||
*/
|
||||
export interface CookieBannerEmbedCode {
|
||||
html: string
|
||||
css: string
|
||||
js: string
|
||||
scriptTag: string // Fertiger Script-Tag zum Einbinden
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollstaendige Cookie Banner Konfiguration
|
||||
*/
|
||||
export interface CookieBannerConfig {
|
||||
id: string
|
||||
tenantId: string
|
||||
categories: CookieBannerCategory[]
|
||||
styling: CookieBannerStyling
|
||||
texts: CookieBannerTexts
|
||||
embedCode?: CookieBannerEmbedCode
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONSENT MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Einzelne Einwilligung eines Nutzers
|
||||
*/
|
||||
export interface ConsentEntry {
|
||||
id: string
|
||||
userId: string
|
||||
dataPointId: string
|
||||
granted: boolean
|
||||
grantedAt: Date
|
||||
revokedAt?: Date
|
||||
ipAddress?: string
|
||||
userAgent?: string
|
||||
consentVersion: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregierte Consent-Statistiken
|
||||
*/
|
||||
export interface ConsentStatistics {
|
||||
totalConsents: number
|
||||
activeConsents: number
|
||||
revokedConsents: number
|
||||
byCategory: Record<DataPointCategory, {
|
||||
total: number
|
||||
active: number
|
||||
revoked: number
|
||||
}>
|
||||
byLegalBasis: Record<LegalBasis, {
|
||||
total: number
|
||||
active: number
|
||||
}>
|
||||
conversionRate: number // Prozent der Nutzer mit Consent
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EINWILLIGUNGEN STATE & ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Aktiver Tab in der Einwilligungen-Ansicht
|
||||
*/
|
||||
export type EinwilligungenTab =
|
||||
| 'catalog'
|
||||
| 'privacy-policy'
|
||||
| 'cookie-banner'
|
||||
| 'retention'
|
||||
| 'consents'
|
||||
|
||||
/**
|
||||
* State fuer Einwilligungen-Modul
|
||||
*/
|
||||
export interface EinwilligungenState {
|
||||
// Data
|
||||
catalog: DataPointCatalog | null
|
||||
selectedDataPoints: string[]
|
||||
privacyPolicy: GeneratedPrivacyPolicy | null
|
||||
cookieBannerConfig: CookieBannerConfig | null
|
||||
companyInfo: CompanyInfo | null
|
||||
consentStatistics: ConsentStatistics | null
|
||||
|
||||
// UI State
|
||||
activeTab: EinwilligungenTab
|
||||
isLoading: boolean
|
||||
isSaving: boolean
|
||||
error: string | null
|
||||
|
||||
// Editor State
|
||||
editingDataPoint: DataPoint | null
|
||||
editingSection: PrivacyPolicySection | null
|
||||
|
||||
// Preview
|
||||
previewLanguage: SupportedLanguage
|
||||
previewFormat: ExportFormat
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions fuer Einwilligungen-Reducer
|
||||
*/
|
||||
export type EinwilligungenAction =
|
||||
| { type: 'SET_CATALOG'; payload: DataPointCatalog }
|
||||
| { type: 'SET_SELECTED_DATA_POINTS'; payload: string[] }
|
||||
| { type: 'TOGGLE_DATA_POINT'; payload: string }
|
||||
| { type: 'ADD_CUSTOM_DATA_POINT'; payload: DataPoint }
|
||||
| { type: 'UPDATE_DATA_POINT'; payload: { id: string; data: Partial<DataPoint> } }
|
||||
| { type: 'DELETE_CUSTOM_DATA_POINT'; payload: string }
|
||||
| { type: 'SET_PRIVACY_POLICY'; payload: GeneratedPrivacyPolicy }
|
||||
| { type: 'SET_COOKIE_BANNER_CONFIG'; payload: CookieBannerConfig }
|
||||
| { type: 'UPDATE_COOKIE_BANNER_STYLING'; payload: Partial<CookieBannerStyling> }
|
||||
| { type: 'UPDATE_COOKIE_BANNER_TEXTS'; payload: Partial<CookieBannerTexts> }
|
||||
| { type: 'SET_COMPANY_INFO'; payload: CompanyInfo }
|
||||
| { type: 'SET_CONSENT_STATISTICS'; payload: ConsentStatistics }
|
||||
| { type: 'SET_ACTIVE_TAB'; payload: EinwilligungenTab }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_SAVING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_EDITING_DATA_POINT'; payload: DataPoint | null }
|
||||
| { type: 'SET_EDITING_SECTION'; payload: PrivacyPolicySection | null }
|
||||
| { type: 'SET_PREVIEW_LANGUAGE'; payload: SupportedLanguage }
|
||||
| { type: 'SET_PREVIEW_FORMAT'; payload: ExportFormat }
|
||||
| { type: 'RESET_STATE' }
|
||||
|
||||
// =============================================================================
|
||||
// HELPER TYPES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kategorie-Metadaten
|
||||
*/
|
||||
export interface CategoryMetadata {
|
||||
id: DataPointCategory
|
||||
code: string // A, B, C, etc.
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
icon: string // Icon name
|
||||
color: string // Tailwind color class
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Kategorie zu Metadaten (18 Kategorien)
|
||||
*/
|
||||
export const CATEGORY_METADATA: Record<DataPointCategory, CategoryMetadata> = {
|
||||
MASTER_DATA: {
|
||||
id: 'MASTER_DATA',
|
||||
code: 'A',
|
||||
name: { de: 'Stammdaten', en: 'Master Data' },
|
||||
description: { de: 'Grundlegende personenbezogene Daten', en: 'Basic personal data' },
|
||||
icon: 'User',
|
||||
color: 'blue'
|
||||
},
|
||||
CONTACT_DATA: {
|
||||
id: 'CONTACT_DATA',
|
||||
code: 'B',
|
||||
name: { de: 'Kontaktdaten', en: 'Contact Data' },
|
||||
description: { de: 'Kontaktinformationen und Erreichbarkeit', en: 'Contact information and availability' },
|
||||
icon: 'Mail',
|
||||
color: 'sky'
|
||||
},
|
||||
AUTHENTICATION: {
|
||||
id: 'AUTHENTICATION',
|
||||
code: 'C',
|
||||
name: { de: 'Authentifizierungsdaten', en: 'Authentication Data' },
|
||||
description: { de: 'Daten zur Benutzeranmeldung und Session-Verwaltung', en: 'Data for user login and session management' },
|
||||
icon: 'Key',
|
||||
color: 'slate'
|
||||
},
|
||||
CONSENT: {
|
||||
id: 'CONSENT',
|
||||
code: 'D',
|
||||
name: { de: 'Einwilligungsdaten', en: 'Consent Data' },
|
||||
description: { de: 'Einwilligungen und Datenschutzpraeferenzen', en: 'Consents and privacy preferences' },
|
||||
icon: 'CheckCircle',
|
||||
color: 'green'
|
||||
},
|
||||
COMMUNICATION: {
|
||||
id: 'COMMUNICATION',
|
||||
code: 'E',
|
||||
name: { de: 'Kommunikationsdaten', en: 'Communication Data' },
|
||||
description: { de: 'Kundenservice und Kommunikationsdaten', en: 'Customer service and communication data' },
|
||||
icon: 'MessageSquare',
|
||||
color: 'cyan'
|
||||
},
|
||||
PAYMENT: {
|
||||
id: 'PAYMENT',
|
||||
code: 'F',
|
||||
name: { de: 'Zahlungsdaten', en: 'Payment Data' },
|
||||
description: { de: 'Rechnungs- und Zahlungsinformationen', en: 'Billing and payment information' },
|
||||
icon: 'CreditCard',
|
||||
color: 'amber'
|
||||
},
|
||||
USAGE_DATA: {
|
||||
id: 'USAGE_DATA',
|
||||
code: 'G',
|
||||
name: { de: 'Nutzungsdaten', en: 'Usage Data' },
|
||||
description: { de: 'Daten zur Nutzung des Dienstes', en: 'Data about service usage' },
|
||||
icon: 'Activity',
|
||||
color: 'violet'
|
||||
},
|
||||
LOCATION: {
|
||||
id: 'LOCATION',
|
||||
code: 'H',
|
||||
name: { de: 'Standortdaten', en: 'Location Data' },
|
||||
description: { de: 'Geografische Standortinformationen', en: 'Geographic location information' },
|
||||
icon: 'MapPin',
|
||||
color: 'emerald'
|
||||
},
|
||||
DEVICE_DATA: {
|
||||
id: 'DEVICE_DATA',
|
||||
code: 'I',
|
||||
name: { de: 'Geraetedaten', en: 'Device Data' },
|
||||
description: { de: 'Technische Geraete- und Browserinformationen', en: 'Technical device and browser information' },
|
||||
icon: 'Smartphone',
|
||||
color: 'zinc'
|
||||
},
|
||||
MARKETING: {
|
||||
id: 'MARKETING',
|
||||
code: 'J',
|
||||
name: { de: 'Marketingdaten', en: 'Marketing Data' },
|
||||
description: { de: 'Marketing- und Werbedaten', en: 'Marketing and advertising data' },
|
||||
icon: 'Megaphone',
|
||||
color: 'purple'
|
||||
},
|
||||
ANALYTICS: {
|
||||
id: 'ANALYTICS',
|
||||
code: 'K',
|
||||
name: { de: 'Analysedaten', en: 'Analytics Data' },
|
||||
description: { de: 'Web-Analyse und Nutzungsstatistiken', en: 'Web analytics and usage statistics' },
|
||||
icon: 'BarChart3',
|
||||
color: 'indigo'
|
||||
},
|
||||
SOCIAL_MEDIA: {
|
||||
id: 'SOCIAL_MEDIA',
|
||||
code: 'L',
|
||||
name: { de: 'Social-Media-Daten', en: 'Social Media Data' },
|
||||
description: { de: 'Daten aus sozialen Netzwerken', en: 'Data from social networks' },
|
||||
icon: 'Share2',
|
||||
color: 'pink'
|
||||
},
|
||||
HEALTH_DATA: {
|
||||
id: 'HEALTH_DATA',
|
||||
code: 'M',
|
||||
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
|
||||
description: { de: 'Besondere Kategorie nach Art. 9 DSGVO - Gesundheitsbezogene Daten', en: 'Special category under Art. 9 GDPR - Health-related data' },
|
||||
icon: 'Heart',
|
||||
color: 'rose'
|
||||
},
|
||||
EMPLOYEE_DATA: {
|
||||
id: 'EMPLOYEE_DATA',
|
||||
code: 'N',
|
||||
name: { de: 'Beschaeftigtendaten', en: 'Employee Data' },
|
||||
description: { de: 'Personalverwaltung und Arbeitnehmerinformationen (BDSG § 26)', en: 'HR management and employee information' },
|
||||
icon: 'Briefcase',
|
||||
color: 'orange'
|
||||
},
|
||||
CONTRACT_DATA: {
|
||||
id: 'CONTRACT_DATA',
|
||||
code: 'O',
|
||||
name: { de: 'Vertragsdaten', en: 'Contract Data' },
|
||||
description: { de: 'Vertragsinformationen und -dokumente', en: 'Contract information and documents' },
|
||||
icon: 'FileText',
|
||||
color: 'teal'
|
||||
},
|
||||
LOG_DATA: {
|
||||
id: 'LOG_DATA',
|
||||
code: 'P',
|
||||
name: { de: 'Protokolldaten', en: 'Log Data' },
|
||||
description: { de: 'System- und Zugriffsprotokolle', en: 'System and access logs' },
|
||||
icon: 'FileCode',
|
||||
color: 'gray'
|
||||
},
|
||||
AI_DATA: {
|
||||
id: 'AI_DATA',
|
||||
code: 'Q',
|
||||
name: { de: 'KI-Daten', en: 'AI Data' },
|
||||
description: { de: 'KI-Interaktionen, Prompts und generierte Inhalte (AI Act)', en: 'AI interactions, prompts and generated content (AI Act)' },
|
||||
icon: 'Bot',
|
||||
color: 'fuchsia'
|
||||
},
|
||||
SECURITY: {
|
||||
id: 'SECURITY',
|
||||
code: 'R',
|
||||
name: { de: 'Sicherheitsdaten', en: 'Security Data' },
|
||||
description: { de: 'Sicherheitsrelevante Daten und Vorfallberichte', en: 'Security-relevant data and incident reports' },
|
||||
icon: 'Shield',
|
||||
color: 'red'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Rechtsgrundlage zu Beschreibung
|
||||
*/
|
||||
export const LEGAL_BASIS_INFO: Record<LegalBasis, { article: string; name: LocalizedText; description: LocalizedText }> = {
|
||||
CONTRACT: {
|
||||
article: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||
name: { de: 'Vertragserfuellung', en: 'Contract Performance' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich fuer die Erfuellung eines Vertrags oder zur Durchfuehrung vorvertraglicher Massnahmen.',
|
||||
en: 'Processing is necessary for the performance of a contract or pre-contractual measures.'
|
||||
}
|
||||
},
|
||||
CONSENT: {
|
||||
article: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
name: { de: 'Einwilligung', en: 'Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ihre Einwilligung zu der Verarbeitung gegeben.',
|
||||
en: 'The data subject has given consent to the processing.'
|
||||
}
|
||||
},
|
||||
EXPLICIT_CONSENT: {
|
||||
article: 'Art. 9 Abs. 2 lit. a DSGVO',
|
||||
name: { de: 'Ausdrueckliche Einwilligung', en: 'Explicit Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ausdruecklich in die Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) eingewilligt. Dies betrifft Gesundheitsdaten, biometrische Daten, Daten zur ethnischen Herkunft, politische Meinungen, religiöse Überzeugungen etc.',
|
||||
en: 'The data subject has given explicit consent to the processing of special categories of personal data (Art. 9 GDPR). This includes health data, biometric data, racial or ethnic origin, political opinions, religious beliefs, etc.'
|
||||
}
|
||||
},
|
||||
LEGITIMATE_INTEREST: {
|
||||
article: 'Art. 6 Abs. 1 lit. f DSGVO',
|
||||
name: { de: 'Berechtigtes Interesse', en: 'Legitimate Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Wahrung berechtigter Interessen des Verantwortlichen erforderlich.',
|
||||
en: 'Processing is necessary for legitimate interests pursued by the controller.'
|
||||
}
|
||||
},
|
||||
LEGAL_OBLIGATION: {
|
||||
article: 'Art. 6 Abs. 1 lit. c DSGVO',
|
||||
name: { de: 'Rechtliche Verpflichtung', en: 'Legal Obligation' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Erfuellung einer rechtlichen Verpflichtung erforderlich.',
|
||||
en: 'Processing is necessary for compliance with a legal obligation.'
|
||||
}
|
||||
},
|
||||
VITAL_INTERESTS: {
|
||||
article: 'Art. 6 Abs. 1 lit. d DSGVO',
|
||||
name: { de: 'Lebenswichtige Interessen', en: 'Vital Interests' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich, um lebenswichtige Interessen der betroffenen Person oder einer anderen natuerlichen Person zu schuetzen.',
|
||||
en: 'Processing is necessary to protect the vital interests of the data subject or another natural person.'
|
||||
}
|
||||
},
|
||||
PUBLIC_INTEREST: {
|
||||
article: 'Art. 6 Abs. 1 lit. e DSGVO',
|
||||
name: { de: 'Oeffentliches Interesse', en: 'Public Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist fuer die Wahrnehmung einer Aufgabe erforderlich, die im oeffentlichen Interesse liegt oder in Ausuebung oeffentlicher Gewalt erfolgt.',
|
||||
en: 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping von Aufbewahrungsfrist zu Beschreibung
|
||||
*/
|
||||
export const RETENTION_PERIOD_INFO: Record<RetentionPeriod, { label: LocalizedText; days: number | null }> = {
|
||||
'24_HOURS': { label: { de: '24 Stunden', en: '24 Hours' }, days: 1 },
|
||||
'30_DAYS': { label: { de: '30 Tage', en: '30 Days' }, days: 30 },
|
||||
'90_DAYS': { label: { de: '90 Tage', en: '90 Days' }, days: 90 },
|
||||
'12_MONTHS': { label: { de: '12 Monate', en: '12 Months' }, days: 365 },
|
||||
'24_MONTHS': { label: { de: '24 Monate', en: '24 Months' }, days: 730 },
|
||||
'26_MONTHS': { label: { de: '26 Monate (Google Analytics)', en: '26 Months (Google Analytics)' }, days: 790 },
|
||||
'36_MONTHS': { label: { de: '36 Monate', en: '36 Months' }, days: 1095 },
|
||||
'48_MONTHS': { label: { de: '48 Monate', en: '48 Months' }, days: 1460 },
|
||||
'6_YEARS': { label: { de: '6 Jahre', en: '6 Years' }, days: 2190 },
|
||||
'10_YEARS': { label: { de: '10 Jahre', en: '10 Years' }, days: 3650 },
|
||||
'UNTIL_REVOCATION': { label: { de: 'Bis Widerruf', en: 'Until Revocation' }, days: null },
|
||||
'UNTIL_PURPOSE_FULFILLED': { label: { de: 'Bis Zweckerfuellung', en: 'Until Purpose Fulfilled' }, days: null },
|
||||
'UNTIL_ACCOUNT_DELETION': { label: { de: 'Bis Kontoschliessung', en: 'Until Account Deletion' }, days: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise für Art. 9 DSGVO Kategorien
|
||||
*/
|
||||
export interface Article9Warning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const ARTICLE_9_WARNING: Article9Warning = {
|
||||
title: {
|
||||
de: 'Besondere Kategorie personenbezogener Daten (Art. 9 DSGVO)',
|
||||
en: 'Special Category of Personal Data (Art. 9 GDPR)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung dieser Daten unterliegt besonderen Anforderungen nach Art. 9 DSGVO. Diese Daten sind besonders schuetzenswert.',
|
||||
en: 'Processing of this data is subject to special requirements under Art. 9 GDPR. This data requires special protection.'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Ausdrueckliche Einwilligung erforderlich (Art. 9 Abs. 2 lit. a DSGVO)',
|
||||
en: 'Explicit consent required (Art. 9(2)(a) GDPR)'
|
||||
},
|
||||
{
|
||||
de: 'Separate Einwilligungserklaerung im UI notwendig',
|
||||
en: 'Separate consent declaration required in UI'
|
||||
},
|
||||
{
|
||||
de: 'Hoehere Dokumentationspflichten',
|
||||
en: 'Higher documentation requirements'
|
||||
},
|
||||
{
|
||||
de: 'Spezielle Loeschverfahren erforderlich',
|
||||
en: 'Special deletion procedures required'
|
||||
},
|
||||
{
|
||||
de: 'Datenschutz-Folgenabschaetzung (DSFA) empfohlen',
|
||||
en: 'Data Protection Impact Assessment (DPIA) recommended'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise für Beschäftigtendaten (BDSG § 26)
|
||||
*/
|
||||
export interface EmployeeDataWarning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const EMPLOYEE_DATA_WARNING: EmployeeDataWarning = {
|
||||
title: {
|
||||
de: 'Beschaeftigtendaten (BDSG § 26)',
|
||||
en: 'Employee Data (BDSG § 26)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung von Beschaeftigtendaten unterliegt besonderen Anforderungen nach § 26 BDSG.',
|
||||
en: 'Processing of employee data is subject to special requirements under § 26 BDSG (German Federal Data Protection Act).'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Aufbewahrungspflichten fuer Lohnunterlagen (6-10 Jahre)',
|
||||
en: 'Retention obligations for payroll records (6-10 years)'
|
||||
},
|
||||
{
|
||||
de: 'Betriebsrat-Beteiligung ggf. erforderlich',
|
||||
en: 'Works council involvement may be required'
|
||||
},
|
||||
{
|
||||
de: 'Verarbeitung nur fuer Zwecke des Beschaeftigungsverhaeltnisses',
|
||||
en: 'Processing only for employment purposes'
|
||||
},
|
||||
{
|
||||
de: 'Besondere Vertraulichkeit bei Gesundheitsdaten',
|
||||
en: 'Special confidentiality for health data'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezielle Hinweise für KI-Daten (AI Act)
|
||||
*/
|
||||
export interface AIDataWarning {
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
}
|
||||
|
||||
export const AI_DATA_WARNING: AIDataWarning = {
|
||||
title: {
|
||||
de: 'KI-Daten (AI Act)',
|
||||
en: 'AI Data (AI Act)'
|
||||
},
|
||||
description: {
|
||||
de: 'Die Verarbeitung von KI-bezogenen Daten unterliegt den Transparenzpflichten des AI Acts.',
|
||||
en: 'Processing of AI-related data is subject to AI Act transparency requirements.'
|
||||
},
|
||||
requirements: [
|
||||
{
|
||||
de: 'Transparenzpflichten bei KI-Verarbeitung',
|
||||
en: 'Transparency obligations for AI processing'
|
||||
},
|
||||
{
|
||||
de: 'Kennzeichnung von KI-generierten Inhalten',
|
||||
en: 'Labeling of AI-generated content'
|
||||
},
|
||||
{
|
||||
de: 'Dokumentation der KI-Modell-Nutzung',
|
||||
en: 'Documentation of AI model usage'
|
||||
},
|
||||
{
|
||||
de: 'Keine Verwendung fuer unerlaubtes Training ohne Einwilligung',
|
||||
en: 'No use for unauthorized training without consent'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk Level Styling
|
||||
*/
|
||||
export const RISK_LEVEL_STYLING: Record<RiskLevel, { label: LocalizedText; color: string; bgColor: string }> = {
|
||||
LOW: {
|
||||
label: { de: 'Niedrig', en: 'Low' },
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
MEDIUM: {
|
||||
label: { de: 'Mittel', en: 'Medium' },
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
HIGH: {
|
||||
label: { de: 'Hoch', en: 'High' },
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
}
|
||||
}
|
||||
753
admin-v2/lib/sdk/export.ts
Normal file
753
admin-v2/lib/sdk/export.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
/**
|
||||
* SDK Export Utilities
|
||||
* Handles PDF and ZIP export of SDK state and documents
|
||||
*/
|
||||
|
||||
import jsPDF from 'jspdf'
|
||||
import JSZip from 'jszip'
|
||||
import { SDKState, SDK_STEPS, getStepById } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ExportOptions {
|
||||
includeEvidence?: boolean
|
||||
includeDocuments?: boolean
|
||||
includeRawData?: boolean
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ExportOptions = {
|
||||
includeEvidence: true,
|
||||
includeDocuments: true,
|
||||
includeRawData: true,
|
||||
language: 'de',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LABELS (German)
|
||||
// =============================================================================
|
||||
|
||||
const LABELS_DE = {
|
||||
title: 'AI Compliance SDK - Export',
|
||||
subtitle: 'Compliance-Dokumentation',
|
||||
generatedAt: 'Generiert am',
|
||||
page: 'Seite',
|
||||
summary: 'Zusammenfassung',
|
||||
progress: 'Fortschritt',
|
||||
phase1: 'Phase 1: Automatisches Compliance Assessment',
|
||||
phase2: 'Phase 2: Dokumentengenerierung',
|
||||
useCases: 'Use Cases',
|
||||
risks: 'Risiken',
|
||||
controls: 'Controls',
|
||||
requirements: 'Anforderungen',
|
||||
modules: 'Compliance-Module',
|
||||
evidence: 'Nachweise',
|
||||
checkpoints: 'Checkpoints',
|
||||
noData: 'Keine Daten vorhanden',
|
||||
status: 'Status',
|
||||
completed: 'Abgeschlossen',
|
||||
pending: 'Ausstehend',
|
||||
inProgress: 'In Bearbeitung',
|
||||
severity: 'Schweregrad',
|
||||
mitigation: 'Mitigation',
|
||||
description: 'Beschreibung',
|
||||
category: 'Kategorie',
|
||||
implementation: 'Implementierung',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF EXPORT
|
||||
// =============================================================================
|
||||
|
||||
function formatDate(date: Date | string | undefined): string {
|
||||
if (!date) return '-'
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
|
||||
// Header line
|
||||
doc.setDrawColor(147, 51, 234) // Purple
|
||||
doc.setLineWidth(0.5)
|
||||
doc.line(20, 15, pageWidth - 20, 15)
|
||||
|
||||
// Title
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(100)
|
||||
doc.text(title, 20, 12)
|
||||
|
||||
// Page number
|
||||
doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12)
|
||||
}
|
||||
|
||||
function addFooter(doc: jsPDF, state: SDKState): void {
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
const pageHeight = doc.internal.pageSize.getHeight()
|
||||
|
||||
// Footer line
|
||||
doc.setDrawColor(200)
|
||||
doc.setLineWidth(0.3)
|
||||
doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15)
|
||||
|
||||
// Footer text
|
||||
doc.setFontSize(8)
|
||||
doc.setTextColor(150)
|
||||
doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10)
|
||||
}
|
||||
|
||||
function addSectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||
doc.setFontSize(14)
|
||||
doc.setTextColor(147, 51, 234) // Purple
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(title, 20, y)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
return y + 10
|
||||
}
|
||||
|
||||
function addSubsectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(60)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(title, 25, y)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
return y + 7
|
||||
}
|
||||
|
||||
function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number {
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
const lines = doc.splitTextToSize(text, maxWidth)
|
||||
doc.text(lines, x, y)
|
||||
return y + lines.length * 5
|
||||
}
|
||||
|
||||
function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number {
|
||||
const pageHeight = doc.internal.pageSize.getHeight()
|
||||
if (y + requiredSpace > pageHeight - 25) {
|
||||
doc.addPage()
|
||||
return 30
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const doc = new jsPDF()
|
||||
|
||||
let y = 30
|
||||
const pageWidth = doc.internal.pageSize.getWidth()
|
||||
|
||||
// ==========================================================================
|
||||
// Title Page
|
||||
// ==========================================================================
|
||||
|
||||
// Logo/Title area
|
||||
doc.setFillColor(147, 51, 234)
|
||||
doc.rect(0, 0, pageWidth, 60, 'F')
|
||||
|
||||
doc.setFontSize(24)
|
||||
doc.setTextColor(255)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(LABELS_DE.title, 20, 35)
|
||||
|
||||
doc.setFontSize(14)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
doc.text(LABELS_DE.subtitle, 20, 48)
|
||||
|
||||
// Reset for content
|
||||
y = 80
|
||||
|
||||
// Summary box
|
||||
doc.setDrawColor(200)
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
|
||||
|
||||
y += 15
|
||||
doc.setFontSize(12)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
|
||||
|
||||
y += 10
|
||||
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
|
||||
|
||||
y += 10
|
||||
doc.text(`Version: ${state.version}`, 30, y)
|
||||
|
||||
y += 10
|
||||
const completedSteps = state.completedSteps.length
|
||||
const totalSteps = SDK_STEPS.length
|
||||
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
|
||||
|
||||
y += 30
|
||||
|
||||
// Table of Contents
|
||||
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
|
||||
|
||||
const tocItems = [
|
||||
{ title: 'Zusammenfassung', page: 2 },
|
||||
{ title: 'Phase 1: Compliance Assessment', page: 3 },
|
||||
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
|
||||
{ title: 'Risiken & Controls', page: 5 },
|
||||
{ title: 'Checkpoints', page: 6 },
|
||||
]
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(80)
|
||||
tocItems.forEach((item, idx) => {
|
||||
doc.text(`${idx + 1}. ${item.title}`, 25, y)
|
||||
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
|
||||
y += 7
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Summary Page
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
|
||||
y = addSectionTitle(doc, LABELS_DE.summary, y)
|
||||
|
||||
// Progress overview
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
|
||||
|
||||
y += 15
|
||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
||||
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(60)
|
||||
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
y += 8
|
||||
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||
|
||||
y += 25
|
||||
|
||||
// Key metrics
|
||||
y = addSubsectionTitle(doc, 'Kennzahlen', y)
|
||||
|
||||
const metrics = [
|
||||
{ label: 'Use Cases', value: state.useCases.length },
|
||||
{ label: 'Risiken identifiziert', value: state.risks.length },
|
||||
{ label: 'Controls definiert', value: state.controls.length },
|
||||
{ label: 'Anforderungen', value: state.requirements.length },
|
||||
{ label: 'Nachweise', value: state.evidence.length },
|
||||
]
|
||||
|
||||
metrics.forEach(metric => {
|
||||
doc.text(`${metric.label}: ${metric.value}`, 30, y)
|
||||
y += 7
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// Use Cases
|
||||
// ==========================================================================
|
||||
|
||||
y += 10
|
||||
y = checkPageBreak(doc, y)
|
||||
y = addSectionTitle(doc, LABELS_DE.useCases, y)
|
||||
|
||||
if (state.useCases.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.useCases.forEach((uc, idx) => {
|
||||
y = checkPageBreak(doc, y, 50)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
|
||||
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
|
||||
|
||||
if (uc.description) {
|
||||
y = addText(doc, uc.description, 25, y + 21, 160)
|
||||
}
|
||||
|
||||
y += 40
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Risks
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.risks, y)
|
||||
|
||||
if (state.risks.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
// Sort by severity
|
||||
const sortedRisks = [...state.risks].sort((a, b) => {
|
||||
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
return (order[a.severity] || 4) - (order[b.severity] || 4)
|
||||
})
|
||||
|
||||
sortedRisks.forEach((risk, idx) => {
|
||||
y = checkPageBreak(doc, y, 45)
|
||||
|
||||
// Severity color
|
||||
const severityColors: Record<string, [number, number, number]> = {
|
||||
CRITICAL: [220, 38, 38],
|
||||
HIGH: [234, 88, 12],
|
||||
MEDIUM: [234, 179, 8],
|
||||
LOW: [34, 197, 94],
|
||||
}
|
||||
const color = severityColors[risk.severity] || [100, 100, 100]
|
||||
|
||||
doc.setFillColor(color[0], color[1], color[2])
|
||||
doc.rect(20, y - 3, 3, 30, 'F')
|
||||
|
||||
doc.setFontSize(11)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
|
||||
|
||||
if (risk.description) {
|
||||
y = addText(doc, risk.description, 28, y + 21, 155)
|
||||
}
|
||||
|
||||
if (risk.mitigation && risk.mitigation.length > 0) {
|
||||
y += 5
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(34, 197, 94)
|
||||
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
|
||||
}
|
||||
|
||||
y += 15
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Controls
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.controls, y)
|
||||
|
||||
if (state.controls.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
state.controls.forEach((ctrl, idx) => {
|
||||
y = checkPageBreak(doc, y, 35)
|
||||
|
||||
doc.setFillColor(249, 250, 251)
|
||||
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.setFont('helvetica', 'bold')
|
||||
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
|
||||
doc.setFont('helvetica', 'normal')
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
|
||||
|
||||
if (ctrl.description) {
|
||||
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
|
||||
}
|
||||
|
||||
y += 35
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Checkpoints
|
||||
// ==========================================================================
|
||||
|
||||
doc.addPage()
|
||||
y = 30
|
||||
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
|
||||
|
||||
const checkpointIds = Object.keys(state.checkpoints)
|
||||
|
||||
if (checkpointIds.length === 0) {
|
||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||
} else {
|
||||
checkpointIds.forEach((cpId) => {
|
||||
const cp = state.checkpoints[cpId]
|
||||
y = checkPageBreak(doc, y, 25)
|
||||
|
||||
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
|
||||
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
|
||||
doc.circle(25, y + 2, 3, 'F')
|
||||
|
||||
doc.setFontSize(10)
|
||||
doc.setTextColor(40)
|
||||
doc.text(cpId, 35, y + 5)
|
||||
|
||||
doc.setFontSize(9)
|
||||
doc.setTextColor(100)
|
||||
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
|
||||
|
||||
if (cp.errors && cp.errors.length > 0) {
|
||||
doc.setTextColor(220, 38, 38)
|
||||
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
|
||||
y += 7
|
||||
}
|
||||
|
||||
y += 20
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Add page numbers
|
||||
// ==========================================================================
|
||||
|
||||
const pageCount = doc.getNumberOfPages()
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i)
|
||||
if (i > 1) {
|
||||
addHeader(doc, LABELS_DE.title, i, pageCount)
|
||||
}
|
||||
addFooter(doc, state)
|
||||
}
|
||||
|
||||
return doc.output('blob')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ZIP EXPORT
|
||||
// =============================================================================
|
||||
|
||||
export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const zip = new JSZip()
|
||||
|
||||
// Create folder structure
|
||||
const rootFolder = zip.folder('ai-compliance-sdk-export')
|
||||
if (!rootFolder) throw new Error('Failed to create ZIP folder')
|
||||
|
||||
const phase1Folder = rootFolder.folder('phase1-assessment')
|
||||
const phase2Folder = rootFolder.folder('phase2-documents')
|
||||
const dataFolder = rootFolder.folder('data')
|
||||
|
||||
// ==========================================================================
|
||||
// Main State JSON
|
||||
// ==========================================================================
|
||||
|
||||
if (opts.includeRawData && dataFolder) {
|
||||
dataFolder.file('state.json', JSON.stringify(state, null, 2))
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// README
|
||||
// ==========================================================================
|
||||
|
||||
const readmeContent = `# AI Compliance SDK Export
|
||||
|
||||
Generated: ${formatDate(new Date())}
|
||||
Tenant: ${state.tenantId}
|
||||
Version: ${state.version}
|
||||
|
||||
## Folder Structure
|
||||
|
||||
- **phase1-assessment/**: Compliance Assessment Ergebnisse
|
||||
- use-cases.json: Alle Use Cases
|
||||
- risks.json: Identifizierte Risiken
|
||||
- controls.json: Definierte Controls
|
||||
- requirements.json: Compliance-Anforderungen
|
||||
|
||||
- **phase2-documents/**: Generierte Dokumente
|
||||
- dsfa.json: Datenschutz-Folgenabschaetzung
|
||||
- toms.json: Technische und organisatorische Massnahmen
|
||||
- vvt.json: Verarbeitungsverzeichnis
|
||||
- documents.json: Rechtliche Dokumente
|
||||
|
||||
- **data/**: Rohdaten
|
||||
- state.json: Kompletter SDK State
|
||||
|
||||
## Progress
|
||||
|
||||
Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed
|
||||
Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed
|
||||
|
||||
## Key Metrics
|
||||
|
||||
- Use Cases: ${state.useCases.length}
|
||||
- Risks: ${state.risks.length}
|
||||
- Controls: ${state.controls.length}
|
||||
- Requirements: ${state.requirements.length}
|
||||
- Evidence: ${state.evidence.length}
|
||||
`
|
||||
|
||||
rootFolder.file('README.md', readmeContent)
|
||||
|
||||
// ==========================================================================
|
||||
// Phase 1 Files
|
||||
// ==========================================================================
|
||||
|
||||
if (phase1Folder) {
|
||||
// Use Cases
|
||||
phase1Folder.file('use-cases.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.useCases.length,
|
||||
useCases: state.useCases,
|
||||
}, null, 2))
|
||||
|
||||
// Risks
|
||||
phase1Folder.file('risks.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.risks.length,
|
||||
risks: state.risks,
|
||||
summary: {
|
||||
critical: state.risks.filter(r => r.severity === 'CRITICAL').length,
|
||||
high: state.risks.filter(r => r.severity === 'HIGH').length,
|
||||
medium: state.risks.filter(r => r.severity === 'MEDIUM').length,
|
||||
low: state.risks.filter(r => r.severity === 'LOW').length,
|
||||
},
|
||||
}, null, 2))
|
||||
|
||||
// Controls
|
||||
phase1Folder.file('controls.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.controls.length,
|
||||
controls: state.controls,
|
||||
}, null, 2))
|
||||
|
||||
// Requirements
|
||||
phase1Folder.file('requirements.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.requirements.length,
|
||||
requirements: state.requirements,
|
||||
}, null, 2))
|
||||
|
||||
// Modules
|
||||
phase1Folder.file('modules.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.modules.length,
|
||||
modules: state.modules,
|
||||
}, null, 2))
|
||||
|
||||
// Evidence
|
||||
if (opts.includeEvidence) {
|
||||
phase1Folder.file('evidence.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.evidence.length,
|
||||
evidence: state.evidence,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Checkpoints
|
||||
phase1Folder.file('checkpoints.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
checkpoints: state.checkpoints,
|
||||
}, null, 2))
|
||||
|
||||
// Screening
|
||||
if (state.screening) {
|
||||
phase1Folder.file('screening.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
screening: state.screening,
|
||||
}, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Phase 2 Files
|
||||
// ==========================================================================
|
||||
|
||||
if (phase2Folder) {
|
||||
// DSFA
|
||||
if (state.dsfa) {
|
||||
phase2Folder.file('dsfa.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
dsfa: state.dsfa,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// TOMs
|
||||
phase2Folder.file('toms.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.toms.length,
|
||||
toms: state.toms,
|
||||
}, null, 2))
|
||||
|
||||
// VVT (Processing Activities)
|
||||
phase2Folder.file('vvt.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.vvt.length,
|
||||
processingActivities: state.vvt,
|
||||
}, null, 2))
|
||||
|
||||
// Legal Documents
|
||||
if (opts.includeDocuments) {
|
||||
phase2Folder.file('documents.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.documents.length,
|
||||
documents: state.documents,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Cookie Banner Config
|
||||
if (state.cookieBanner) {
|
||||
phase2Folder.file('cookie-banner.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
config: state.cookieBanner,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Retention Policies
|
||||
phase2Folder.file('retention-policies.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.retentionPolicies.length,
|
||||
policies: state.retentionPolicies,
|
||||
}, null, 2))
|
||||
|
||||
// AI Act Classification
|
||||
if (state.aiActClassification) {
|
||||
phase2Folder.file('ai-act-classification.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
classification: state.aiActClassification,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Obligations
|
||||
phase2Folder.file('obligations.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.obligations.length,
|
||||
obligations: state.obligations,
|
||||
}, null, 2))
|
||||
|
||||
// Consent Records
|
||||
phase2Folder.file('consents.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.consents.length,
|
||||
consents: state.consents,
|
||||
}, null, 2))
|
||||
|
||||
// DSR Config
|
||||
if (state.dsrConfig) {
|
||||
phase2Folder.file('dsr-config.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
config: state.dsrConfig,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// Escalation Workflows
|
||||
phase2Folder.file('escalation-workflows.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.escalationWorkflows.length,
|
||||
workflows: state.escalationWorkflows,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Security Data
|
||||
// ==========================================================================
|
||||
|
||||
if (dataFolder) {
|
||||
if (state.sbom) {
|
||||
dataFolder.file('sbom.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
sbom: state.sbom,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
if (state.securityIssues.length > 0) {
|
||||
dataFolder.file('security-issues.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.securityIssues.length,
|
||||
issues: state.securityIssues,
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
if (state.securityBacklog.length > 0) {
|
||||
dataFolder.file('security-backlog.json', JSON.stringify({
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: state.securityBacklog.length,
|
||||
backlog: state.securityBacklog,
|
||||
}, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Generate PDF and include in ZIP
|
||||
// ==========================================================================
|
||||
|
||||
try {
|
||||
const pdfBlob = await exportToPDF(state, options)
|
||||
const pdfArrayBuffer = await pdfBlob.arrayBuffer()
|
||||
rootFolder.file('compliance-report.pdf', pdfArrayBuffer)
|
||||
} catch (error) {
|
||||
console.error('Failed to generate PDF for ZIP:', error)
|
||||
// Continue without PDF
|
||||
}
|
||||
|
||||
// Generate ZIP
|
||||
return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT HELPER
|
||||
// =============================================================================
|
||||
|
||||
export async function downloadExport(
|
||||
state: SDKState,
|
||||
format: 'json' | 'pdf' | 'zip',
|
||||
options: ExportOptions = {}
|
||||
): Promise<void> {
|
||||
let blob: Blob
|
||||
let filename: string
|
||||
|
||||
const timestamp = new Date().toISOString().slice(0, 10)
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
filename = `ai-compliance-sdk-${timestamp}.json`
|
||||
break
|
||||
|
||||
case 'pdf':
|
||||
blob = await exportToPDF(state, options)
|
||||
filename = `ai-compliance-sdk-${timestamp}.pdf`
|
||||
break
|
||||
|
||||
case 'zip':
|
||||
blob = await exportToZIP(state, options)
|
||||
filename = `ai-compliance-sdk-${timestamp}.zip`
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown export format: ${format}`)
|
||||
}
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
73
admin-v2/lib/sdk/index.ts
Normal file
73
admin-v2/lib/sdk/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* AI Compliance SDK - Main Export
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types'
|
||||
|
||||
// Context & Provider
|
||||
export { SDKProvider, useSDK, SDKContext, initialState } from './context'
|
||||
|
||||
// Export utilities
|
||||
export { exportToPDF, exportToZIP, downloadExport } from './export'
|
||||
export type { ExportOptions } from './export'
|
||||
|
||||
// API Client
|
||||
export {
|
||||
SDKApiClient,
|
||||
getSDKApiClient,
|
||||
resetSDKApiClient,
|
||||
} from './api-client'
|
||||
export type {
|
||||
APIResponse,
|
||||
StateResponse,
|
||||
SaveStateRequest,
|
||||
CheckpointValidationResult,
|
||||
APIError,
|
||||
} from './api-client'
|
||||
|
||||
// Sync Manager
|
||||
export {
|
||||
StateSyncManager,
|
||||
createStateSyncManager,
|
||||
} from './sync'
|
||||
export type {
|
||||
SyncStatus,
|
||||
SyncState,
|
||||
ConflictResolution,
|
||||
SyncOptions,
|
||||
SyncCallbacks,
|
||||
} from './sync'
|
||||
|
||||
// SDK Backend Client (RAG + LLM)
|
||||
export {
|
||||
SDKBackendClient,
|
||||
getSDKBackendClient,
|
||||
resetSDKBackendClient,
|
||||
isLegalQuery,
|
||||
extractRegulationReferences,
|
||||
} from './sdk-client'
|
||||
export type {
|
||||
SearchResult,
|
||||
SearchResponse,
|
||||
CorpusStatus,
|
||||
GenerateRequest,
|
||||
GenerateResponse,
|
||||
} from './sdk-client'
|
||||
|
||||
// Demo Data Seeding (stored via API like real customer data)
|
||||
export {
|
||||
generateDemoState,
|
||||
seedDemoData,
|
||||
seedDemoDataDirect,
|
||||
hasDemoData,
|
||||
clearDemoData,
|
||||
// Seed data templates (for testing/reference only)
|
||||
getDemoUseCases,
|
||||
getDemoRisks,
|
||||
getDemoControls,
|
||||
getDemoDSFA,
|
||||
getDemoTOMs,
|
||||
getDemoProcessingActivities,
|
||||
getDemoRetentionPolicies,
|
||||
} from './demo-data'
|
||||
327
admin-v2/lib/sdk/sdk-client.ts
Normal file
327
admin-v2/lib/sdk/sdk-client.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* SDK Backend Client
|
||||
*
|
||||
* Client for communicating with the SDK Backend (Go service)
|
||||
* for RAG search and document generation.
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface SearchResult {
|
||||
id: string
|
||||
content: string
|
||||
source: string
|
||||
score: number
|
||||
metadata?: Record<string, string>
|
||||
highlights?: string[]
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
query: string
|
||||
topK: number
|
||||
results: SearchResult[]
|
||||
source: 'qdrant' | 'mock'
|
||||
}
|
||||
|
||||
export interface CorpusStatus {
|
||||
status: 'ready' | 'unavailable' | 'indexing'
|
||||
collections: string[]
|
||||
documents: number
|
||||
lastUpdated?: string
|
||||
}
|
||||
|
||||
export interface GenerateRequest {
|
||||
tenantId: string
|
||||
context: Record<string, unknown>
|
||||
template?: string
|
||||
language?: 'de' | 'en'
|
||||
useRag?: boolean
|
||||
ragQuery?: string
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
}
|
||||
|
||||
export interface GenerateResponse {
|
||||
content: string
|
||||
generatedAt: string
|
||||
model: string
|
||||
tokensUsed: number
|
||||
ragSources?: SearchResult[]
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
export interface SDKBackendResponse<T> {
|
||||
success: boolean
|
||||
data?: T
|
||||
error?: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const SDK_BACKEND_URL = process.env.NEXT_PUBLIC_SDK_BACKEND_URL || 'http://localhost:8085'
|
||||
const DEFAULT_TIMEOUT = 60000 // 60 seconds for generation
|
||||
|
||||
// =============================================================================
|
||||
// SDK BACKEND CLIENT
|
||||
// =============================================================================
|
||||
|
||||
export class SDKBackendClient {
|
||||
private baseUrl: string
|
||||
private timeout: number
|
||||
|
||||
constructor(options: {
|
||||
baseUrl?: string
|
||||
timeout?: number
|
||||
} = {}) {
|
||||
this.baseUrl = options.baseUrl || SDK_BACKEND_URL
|
||||
this.timeout = options.timeout || DEFAULT_TIMEOUT
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async fetch<T>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
})
|
||||
|
||||
const data = await response.json() as SDKBackendResponse<T>
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return data.data as T
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RAG Search
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Search the legal corpus using semantic search
|
||||
*/
|
||||
async search(
|
||||
query: string,
|
||||
options: {
|
||||
topK?: number
|
||||
collection?: string
|
||||
filter?: string
|
||||
} = {}
|
||||
): Promise<SearchResponse> {
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
top_k: String(options.topK || 5),
|
||||
})
|
||||
|
||||
if (options.collection) {
|
||||
params.set('collection', options.collection)
|
||||
}
|
||||
if (options.filter) {
|
||||
params.set('filter', options.filter)
|
||||
}
|
||||
|
||||
return this.fetch<SearchResponse>(`/sdk/v1/rag/search?${params}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of the legal corpus
|
||||
*/
|
||||
async getCorpusStatus(): Promise<CorpusStatus> {
|
||||
return this.fetch<CorpusStatus>('/sdk/v1/rag/status')
|
||||
}
|
||||
|
||||
/**
|
||||
* Index a new document into the corpus
|
||||
*/
|
||||
async indexDocument(
|
||||
collection: string,
|
||||
id: string,
|
||||
content: string,
|
||||
metadata?: Record<string, string>
|
||||
): Promise<{ indexed: boolean; id: string }> {
|
||||
return this.fetch('/sdk/v1/rag/index', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
collection,
|
||||
id,
|
||||
content,
|
||||
metadata,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Document Generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate a Data Protection Impact Assessment (DSFA)
|
||||
*/
|
||||
async generateDSFA(request: GenerateRequest): Promise<GenerateResponse> {
|
||||
return this.fetch<GenerateResponse>('/sdk/v1/generate/dsfa', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Technical and Organizational Measures (TOM)
|
||||
*/
|
||||
async generateTOM(request: GenerateRequest): Promise<GenerateResponse> {
|
||||
return this.fetch<GenerateResponse>('/sdk/v1/generate/tom', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Processing Activity Register (VVT)
|
||||
*/
|
||||
async generateVVT(request: GenerateRequest): Promise<GenerateResponse> {
|
||||
return this.fetch<GenerateResponse>('/sdk/v1/generate/vvt', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an expert opinion/assessment (Gutachten)
|
||||
*/
|
||||
async generateGutachten(request: GenerateRequest): Promise<GenerateResponse> {
|
||||
return this.fetch<GenerateResponse>('/sdk/v1/generate/gutachten', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health Check
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check if the SDK backend is healthy
|
||||
*/
|
||||
async healthCheck(): Promise<{
|
||||
status: string
|
||||
timestamp: string
|
||||
services: {
|
||||
database: boolean
|
||||
rag: boolean
|
||||
llm: boolean
|
||||
}
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/health`, {
|
||||
method: 'GET',
|
||||
})
|
||||
return response.json()
|
||||
} catch {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
services: {
|
||||
database: false,
|
||||
rag: false,
|
||||
llm: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SINGLETON INSTANCE
|
||||
// =============================================================================
|
||||
|
||||
let clientInstance: SDKBackendClient | null = null
|
||||
|
||||
export function getSDKBackendClient(): SDKBackendClient {
|
||||
if (!clientInstance) {
|
||||
clientInstance = new SDKBackendClient()
|
||||
}
|
||||
return clientInstance
|
||||
}
|
||||
|
||||
export function resetSDKBackendClient(): void {
|
||||
clientInstance = null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if a query is likely a legal/compliance search
|
||||
*/
|
||||
export function isLegalQuery(query: string): boolean {
|
||||
const legalKeywords = [
|
||||
'dsgvo', 'gdpr', 'datenschutz', 'privacy',
|
||||
'ai act', 'ki-gesetz', 'ai-verordnung',
|
||||
'nis2', 'cybersicherheit',
|
||||
'artikel', 'article', 'art.',
|
||||
'verordnung', 'regulation', 'richtlinie', 'directive',
|
||||
'gesetz', 'law', 'compliance',
|
||||
'dsfa', 'dpia', 'folgenabschätzung',
|
||||
'tom', 'maßnahmen', 'measures',
|
||||
'vvt', 'verarbeitungsverzeichnis',
|
||||
]
|
||||
|
||||
const normalizedQuery = query.toLowerCase()
|
||||
return legalKeywords.some(keyword => normalizedQuery.includes(keyword))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract regulation references from text
|
||||
*/
|
||||
export function extractRegulationReferences(text: string): Array<{
|
||||
regulation: string
|
||||
article: string
|
||||
fullReference: string
|
||||
}> {
|
||||
const references: Array<{
|
||||
regulation: string
|
||||
article: string
|
||||
fullReference: string
|
||||
}> = []
|
||||
|
||||
// Match patterns like "Art. 5 DSGVO", "Article 6 GDPR", "Art. 35 Abs. 1 DSGVO"
|
||||
const patterns = [
|
||||
/Art(?:ikel|\.|\s)*(\d+)(?:\s*Abs\.\s*(\d+))?\s*(DSGVO|GDPR|AI\s*Act|NIS2)/gi,
|
||||
/Article\s*(\d+)(?:\s*para(?:graph)?\s*(\d+))?\s*(DSGVO|GDPR|AI\s*Act|NIS2)/gi,
|
||||
]
|
||||
|
||||
for (const pattern of patterns) {
|
||||
let match
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
references.push({
|
||||
regulation: match[3].toUpperCase().replace(/\s+/g, ' '),
|
||||
article: match[2] ? `${match[1]} Abs. ${match[2]}` : match[1],
|
||||
fullReference: match[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return references
|
||||
}
|
||||
482
admin-v2/lib/sdk/sync.ts
Normal file
482
admin-v2/lib/sdk/sync.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* SDK State Synchronization
|
||||
*
|
||||
* Handles offline/online sync, multi-tab coordination,
|
||||
* and conflict resolution for SDK state.
|
||||
*/
|
||||
|
||||
import { SDKState } from './types'
|
||||
import { SDKApiClient, StateResponse } from './api-client'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export type SyncStatus = 'idle' | 'syncing' | 'error' | 'offline' | 'conflict'
|
||||
|
||||
export interface SyncState {
|
||||
status: SyncStatus
|
||||
lastSyncedAt: Date | null
|
||||
localVersion: number
|
||||
serverVersion: number
|
||||
pendingChanges: number
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface ConflictResolution {
|
||||
strategy: 'local' | 'server' | 'merge'
|
||||
mergedState?: SDKState
|
||||
}
|
||||
|
||||
export interface SyncOptions {
|
||||
debounceMs?: number
|
||||
maxRetries?: number
|
||||
conflictHandler?: (local: SDKState, server: SDKState) => Promise<ConflictResolution>
|
||||
}
|
||||
|
||||
export interface SyncCallbacks {
|
||||
onSyncStart?: () => void
|
||||
onSyncComplete?: (state: SDKState) => void
|
||||
onSyncError?: (error: Error) => void
|
||||
onConflict?: (local: SDKState, server: SDKState) => void
|
||||
onOffline?: () => void
|
||||
onOnline?: () => void
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'ai-compliance-sdk-state'
|
||||
const SYNC_CHANNEL = 'sdk-state-sync'
|
||||
const DEFAULT_DEBOUNCE_MS = 2000
|
||||
const DEFAULT_MAX_RETRIES = 3
|
||||
|
||||
// =============================================================================
|
||||
// STATE SYNC MANAGER
|
||||
// =============================================================================
|
||||
|
||||
export class StateSyncManager {
|
||||
private apiClient: SDKApiClient
|
||||
private tenantId: string
|
||||
private options: Required<SyncOptions>
|
||||
private callbacks: SyncCallbacks
|
||||
private syncState: SyncState
|
||||
private broadcastChannel: BroadcastChannel | null = null
|
||||
private debounceTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
private pendingState: SDKState | null = null
|
||||
private isOnline: boolean = true
|
||||
|
||||
constructor(
|
||||
apiClient: SDKApiClient,
|
||||
tenantId: string,
|
||||
options: SyncOptions = {},
|
||||
callbacks: SyncCallbacks = {}
|
||||
) {
|
||||
this.apiClient = apiClient
|
||||
this.tenantId = tenantId
|
||||
this.callbacks = callbacks
|
||||
this.options = {
|
||||
debounceMs: options.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
||||
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
||||
conflictHandler: options.conflictHandler ?? this.defaultConflictHandler,
|
||||
}
|
||||
|
||||
this.syncState = {
|
||||
status: 'idle',
|
||||
lastSyncedAt: null,
|
||||
localVersion: 0,
|
||||
serverVersion: 0,
|
||||
pendingChanges: 0,
|
||||
error: null,
|
||||
}
|
||||
|
||||
this.setupBroadcastChannel()
|
||||
this.setupOnlineListener()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private setupBroadcastChannel(): void {
|
||||
if (typeof window === 'undefined' || !('BroadcastChannel' in window)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.broadcastChannel = new BroadcastChannel(`${SYNC_CHANNEL}-${this.tenantId}`)
|
||||
this.broadcastChannel.onmessage = this.handleBroadcastMessage.bind(this)
|
||||
} catch (error) {
|
||||
console.warn('BroadcastChannel not available:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private setupOnlineListener(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
this.isOnline = true
|
||||
this.syncState.status = 'idle'
|
||||
this.callbacks.onOnline?.()
|
||||
// Attempt to sync any pending changes
|
||||
if (this.pendingState) {
|
||||
this.syncToServer(this.pendingState)
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOnline = false
|
||||
this.syncState.status = 'offline'
|
||||
this.callbacks.onOffline?.()
|
||||
})
|
||||
|
||||
// Check initial online status
|
||||
this.isOnline = navigator.onLine
|
||||
if (!this.isOnline) {
|
||||
this.syncState.status = 'offline'
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Broadcast Channel Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private handleBroadcastMessage(event: MessageEvent): void {
|
||||
const { type, state, version, tabId } = event.data
|
||||
|
||||
switch (type) {
|
||||
case 'STATE_UPDATED':
|
||||
// Another tab updated the state
|
||||
if (version > this.syncState.localVersion) {
|
||||
this.syncState.localVersion = version
|
||||
this.saveToLocalStorage(state)
|
||||
this.callbacks.onSyncComplete?.(state)
|
||||
}
|
||||
break
|
||||
|
||||
case 'SYNC_COMPLETE':
|
||||
// Another tab completed a sync
|
||||
this.syncState.serverVersion = version
|
||||
break
|
||||
|
||||
case 'REQUEST_STATE':
|
||||
// Another tab is requesting the current state
|
||||
this.broadcastState()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private broadcastState(): void {
|
||||
if (!this.broadcastChannel) return
|
||||
|
||||
const state = this.loadFromLocalStorage()
|
||||
if (state) {
|
||||
this.broadcastChannel.postMessage({
|
||||
type: 'STATE_UPDATED',
|
||||
state,
|
||||
version: this.syncState.localVersion,
|
||||
tabId: this.getTabId(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private broadcastSyncComplete(version: number): void {
|
||||
if (!this.broadcastChannel) return
|
||||
|
||||
this.broadcastChannel.postMessage({
|
||||
type: 'SYNC_COMPLETE',
|
||||
version,
|
||||
tabId: this.getTabId(),
|
||||
})
|
||||
}
|
||||
|
||||
private getTabId(): string {
|
||||
if (typeof window === 'undefined') return 'server'
|
||||
|
||||
let tabId = sessionStorage.getItem('sdk-tab-id')
|
||||
if (!tabId) {
|
||||
tabId = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
sessionStorage.setItem('sdk-tab-id', tabId)
|
||||
}
|
||||
return tabId
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local Storage Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private getStorageKey(): string {
|
||||
return `${STORAGE_KEY_PREFIX}-${this.tenantId}`
|
||||
}
|
||||
|
||||
saveToLocalStorage(state: SDKState): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
const data = {
|
||||
state,
|
||||
version: this.syncState.localVersion,
|
||||
savedAt: new Date().toISOString(),
|
||||
}
|
||||
localStorage.setItem(this.getStorageKey(), JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('Failed to save to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
loadFromLocalStorage(): SDKState | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(this.getStorageKey())
|
||||
if (stored) {
|
||||
const data = JSON.parse(stored)
|
||||
this.syncState.localVersion = data.version || 0
|
||||
return data.state
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load from localStorage:', error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
clearLocalStorage(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
localStorage.removeItem(this.getStorageKey())
|
||||
} catch (error) {
|
||||
console.error('Failed to clear localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sync Methods
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Queue a state change for syncing (debounced)
|
||||
*/
|
||||
queueSync(state: SDKState): void {
|
||||
this.pendingState = state
|
||||
this.syncState.pendingChanges++
|
||||
|
||||
// Save to localStorage immediately
|
||||
this.syncState.localVersion++
|
||||
this.saveToLocalStorage(state)
|
||||
|
||||
// Broadcast to other tabs
|
||||
this.broadcastState()
|
||||
|
||||
// Debounce server sync
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout)
|
||||
}
|
||||
|
||||
this.debounceTimeout = setTimeout(() => {
|
||||
this.syncToServer(state)
|
||||
}, this.options.debounceMs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Force immediate sync to server
|
||||
*/
|
||||
async forcSync(state: SDKState): Promise<void> {
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout)
|
||||
this.debounceTimeout = null
|
||||
}
|
||||
|
||||
await this.syncToServer(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync state to server
|
||||
*/
|
||||
private async syncToServer(state: SDKState): Promise<void> {
|
||||
if (!this.isOnline) {
|
||||
this.syncState.status = 'offline'
|
||||
return
|
||||
}
|
||||
|
||||
this.syncState.status = 'syncing'
|
||||
this.callbacks.onSyncStart?.()
|
||||
|
||||
try {
|
||||
const response = await this.apiClient.saveState(state, this.syncState.serverVersion)
|
||||
|
||||
this.syncState = {
|
||||
...this.syncState,
|
||||
status: 'idle',
|
||||
lastSyncedAt: new Date(),
|
||||
serverVersion: response.version,
|
||||
pendingChanges: 0,
|
||||
error: null,
|
||||
}
|
||||
|
||||
this.pendingState = null
|
||||
this.broadcastSyncComplete(response.version)
|
||||
this.callbacks.onSyncComplete?.(state)
|
||||
} catch (error) {
|
||||
// Handle version conflict (409)
|
||||
if ((error as { status?: number }).status === 409) {
|
||||
await this.handleConflict(state)
|
||||
} else {
|
||||
this.syncState.status = 'error'
|
||||
this.syncState.error = (error as Error).message
|
||||
this.callbacks.onSyncError?.(error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load state from server
|
||||
*/
|
||||
async loadFromServer(): Promise<SDKState | null> {
|
||||
if (!this.isOnline) {
|
||||
return this.loadFromLocalStorage()
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.apiClient.getState()
|
||||
|
||||
if (response) {
|
||||
this.syncState.serverVersion = response.version
|
||||
this.syncState.localVersion = response.version
|
||||
this.saveToLocalStorage(response.state)
|
||||
return response.state
|
||||
}
|
||||
|
||||
// No server state, return local if available
|
||||
return this.loadFromLocalStorage()
|
||||
} catch (error) {
|
||||
console.error('Failed to load from server:', error)
|
||||
// Fallback to local storage
|
||||
return this.loadFromLocalStorage()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conflict Resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private async handleConflict(localState: SDKState): Promise<void> {
|
||||
this.syncState.status = 'conflict'
|
||||
|
||||
try {
|
||||
// Fetch server state
|
||||
const serverResponse = await this.apiClient.getState()
|
||||
|
||||
if (!serverResponse) {
|
||||
// Server has no state, use local
|
||||
await this.apiClient.saveState(localState)
|
||||
return
|
||||
}
|
||||
|
||||
const serverState = serverResponse.state
|
||||
this.callbacks.onConflict?.(localState, serverState)
|
||||
|
||||
// Resolve conflict
|
||||
const resolution = await this.options.conflictHandler(localState, serverState)
|
||||
|
||||
let resolvedState: SDKState
|
||||
switch (resolution.strategy) {
|
||||
case 'local':
|
||||
resolvedState = localState
|
||||
break
|
||||
case 'server':
|
||||
resolvedState = serverState
|
||||
break
|
||||
case 'merge':
|
||||
resolvedState = resolution.mergedState || localState
|
||||
break
|
||||
}
|
||||
|
||||
// Save resolved state
|
||||
const response = await this.apiClient.saveState(resolvedState)
|
||||
this.syncState.serverVersion = response.version
|
||||
this.syncState.localVersion = response.version
|
||||
this.saveToLocalStorage(resolvedState)
|
||||
this.syncState.status = 'idle'
|
||||
this.callbacks.onSyncComplete?.(resolvedState)
|
||||
} catch (error) {
|
||||
this.syncState.status = 'error'
|
||||
this.syncState.error = (error as Error).message
|
||||
this.callbacks.onSyncError?.(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
private async defaultConflictHandler(
|
||||
local: SDKState,
|
||||
server: SDKState
|
||||
): Promise<ConflictResolution> {
|
||||
// Default: Server wins, but we preserve certain local-only data
|
||||
const localTime = new Date(local.lastModified).getTime()
|
||||
const serverTime = new Date(server.lastModified).getTime()
|
||||
|
||||
if (localTime > serverTime) {
|
||||
// Local is newer, use local
|
||||
return { strategy: 'local' }
|
||||
}
|
||||
|
||||
// Merge: Use server state but preserve local UI preferences
|
||||
const mergedState: SDKState = {
|
||||
...server,
|
||||
preferences: local.preferences,
|
||||
commandBarHistory: [
|
||||
...local.commandBarHistory,
|
||||
...server.commandBarHistory.filter(
|
||||
h => !local.commandBarHistory.some(lh => lh.id === h.id)
|
||||
),
|
||||
].slice(0, 50),
|
||||
recentSearches: [...new Set([...local.recentSearches, ...server.recentSearches])].slice(0, 20),
|
||||
}
|
||||
|
||||
return { strategy: 'merge', mergedState }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Getters & Cleanup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getSyncState(): SyncState {
|
||||
return { ...this.syncState }
|
||||
}
|
||||
|
||||
isOnlineStatus(): boolean {
|
||||
return this.isOnline
|
||||
}
|
||||
|
||||
hasPendingChanges(): boolean {
|
||||
return this.syncState.pendingChanges > 0 || this.pendingState !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.debounceTimeout) {
|
||||
clearTimeout(this.debounceTimeout)
|
||||
}
|
||||
|
||||
if (this.broadcastChannel) {
|
||||
this.broadcastChannel.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FACTORY FUNCTION
|
||||
// =============================================================================
|
||||
|
||||
export function createStateSyncManager(
|
||||
apiClient: SDKApiClient,
|
||||
tenantId: string,
|
||||
options?: SyncOptions,
|
||||
callbacks?: SyncCallbacks
|
||||
): StateSyncManager {
|
||||
return new StateSyncManager(apiClient, tenantId, options, callbacks)
|
||||
}
|
||||
414
admin-v2/lib/sdk/tom-generator/ai/document-analyzer.ts
Normal file
414
admin-v2/lib/sdk/tom-generator/ai/document-analyzer.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Document Analyzer
|
||||
// AI-powered analysis of uploaded evidence documents
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
EvidenceDocument,
|
||||
AIDocumentAnalysis,
|
||||
ExtractedClause,
|
||||
DocumentType,
|
||||
} from '../types'
|
||||
import {
|
||||
getDocumentAnalysisPrompt,
|
||||
getDocumentTypeDetectionPrompt,
|
||||
DocumentAnalysisPromptContext,
|
||||
} from './prompts'
|
||||
import { getAllControls } from '../controls/loader'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface AnalysisResult {
|
||||
success: boolean
|
||||
analysis: AIDocumentAnalysis | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface DocumentTypeDetectionResult {
|
||||
documentType: DocumentType
|
||||
confidence: number
|
||||
reasoning: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCUMENT ANALYZER CLASS
|
||||
// =============================================================================
|
||||
|
||||
export class TOMDocumentAnalyzer {
|
||||
private apiEndpoint: string
|
||||
private apiKey: string | null
|
||||
|
||||
constructor(options?: { apiEndpoint?: string; apiKey?: string }) {
|
||||
this.apiEndpoint = options?.apiEndpoint || '/api/sdk/v1/tom-generator/evidence/analyze'
|
||||
this.apiKey = options?.apiKey || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze a document and extract relevant TOM information
|
||||
*/
|
||||
async analyzeDocument(
|
||||
document: EvidenceDocument,
|
||||
documentText: string,
|
||||
language: 'de' | 'en' = 'de'
|
||||
): Promise<AnalysisResult> {
|
||||
try {
|
||||
// Get all control IDs for context
|
||||
const allControls = getAllControls()
|
||||
const controlIds = allControls.map((c) => c.id)
|
||||
|
||||
// Build the prompt context
|
||||
const promptContext: DocumentAnalysisPromptContext = {
|
||||
documentType: document.documentType,
|
||||
documentText,
|
||||
controlIds,
|
||||
language,
|
||||
}
|
||||
|
||||
const prompt = getDocumentAnalysisPrompt(promptContext)
|
||||
|
||||
// Call the AI API
|
||||
const response = await this.callAI(prompt)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return {
|
||||
success: false,
|
||||
analysis: null,
|
||||
error: response.error || 'Failed to analyze document',
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the AI response
|
||||
const parsedResponse = this.parseAnalysisResponse(response.data)
|
||||
|
||||
const analysis: AIDocumentAnalysis = {
|
||||
summary: parsedResponse.summary,
|
||||
extractedClauses: parsedResponse.extractedClauses,
|
||||
applicableControls: parsedResponse.applicableControls,
|
||||
gaps: parsedResponse.gaps,
|
||||
confidence: parsedResponse.confidence,
|
||||
analyzedAt: new Date(),
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
analysis,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
analysis: null,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the document type from content
|
||||
*/
|
||||
async detectDocumentType(
|
||||
documentText: string,
|
||||
filename: string
|
||||
): Promise<DocumentTypeDetectionResult> {
|
||||
try {
|
||||
const prompt = getDocumentTypeDetectionPrompt(documentText, filename)
|
||||
const response = await this.callAI(prompt)
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return {
|
||||
documentType: 'OTHER',
|
||||
confidence: 0,
|
||||
reasoning: 'Could not detect document type',
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = this.parseJSONResponse(response.data)
|
||||
|
||||
return {
|
||||
documentType: this.mapDocumentType(String(parsed.documentType || 'OTHER')),
|
||||
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0,
|
||||
reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : '',
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
documentType: 'OTHER',
|
||||
confidence: 0,
|
||||
reasoning: error instanceof Error ? error.message : 'Detection failed',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link document to applicable controls based on analysis
|
||||
*/
|
||||
async suggestControlLinks(
|
||||
analysis: AIDocumentAnalysis
|
||||
): Promise<string[]> {
|
||||
// Use the applicable controls from the analysis
|
||||
const suggestedControls = [...analysis.applicableControls]
|
||||
|
||||
// Also check extracted clauses for related controls
|
||||
for (const clause of analysis.extractedClauses) {
|
||||
if (clause.relatedControlId && !suggestedControls.includes(clause.relatedControlId)) {
|
||||
suggestedControls.push(clause.relatedControlId)
|
||||
}
|
||||
}
|
||||
|
||||
return suggestedControls
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate evidence coverage for a control
|
||||
*/
|
||||
calculateEvidenceCoverage(
|
||||
controlId: string,
|
||||
documents: EvidenceDocument[]
|
||||
): {
|
||||
coverage: number
|
||||
linkedDocuments: string[]
|
||||
missingEvidence: string[]
|
||||
} {
|
||||
const control = getAllControls().find((c) => c.id === controlId)
|
||||
if (!control) {
|
||||
return { coverage: 0, linkedDocuments: [], missingEvidence: [] }
|
||||
}
|
||||
|
||||
const linkedDocuments: string[] = []
|
||||
const coveredRequirements = new Set<string>()
|
||||
|
||||
for (const doc of documents) {
|
||||
// Check if document is explicitly linked
|
||||
if (doc.linkedControlIds.includes(controlId)) {
|
||||
linkedDocuments.push(doc.id)
|
||||
}
|
||||
|
||||
// Check if AI analysis suggests this document covers the control
|
||||
if (doc.aiAnalysis?.applicableControls.includes(controlId)) {
|
||||
if (!linkedDocuments.includes(doc.id)) {
|
||||
linkedDocuments.push(doc.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Check which evidence requirements are covered
|
||||
if (doc.aiAnalysis) {
|
||||
for (const requirement of control.evidenceRequirements) {
|
||||
const reqLower = requirement.toLowerCase()
|
||||
if (
|
||||
doc.aiAnalysis.summary.toLowerCase().includes(reqLower) ||
|
||||
doc.aiAnalysis.extractedClauses.some((c) =>
|
||||
c.text.toLowerCase().includes(reqLower)
|
||||
)
|
||||
) {
|
||||
coveredRequirements.add(requirement)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missingEvidence = control.evidenceRequirements.filter(
|
||||
(req) => !coveredRequirements.has(req)
|
||||
)
|
||||
|
||||
const coverage =
|
||||
control.evidenceRequirements.length > 0
|
||||
? Math.round(
|
||||
(coveredRequirements.size / control.evidenceRequirements.length) * 100
|
||||
)
|
||||
: 100
|
||||
|
||||
return {
|
||||
coverage,
|
||||
linkedDocuments,
|
||||
missingEvidence,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the AI API
|
||||
*/
|
||||
private async callAI(
|
||||
prompt: string
|
||||
): Promise<{ success: boolean; data?: string; error?: string }> {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['Authorization'] = `Bearer ${this.apiKey}`
|
||||
}
|
||||
|
||||
const response = await fetch(this.apiEndpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ prompt }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: `API error: ${response.status} ${response.statusText}`,
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: data.response || data.content || JSON.stringify(data),
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'API call failed',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the AI analysis response
|
||||
*/
|
||||
private parseAnalysisResponse(response: string): {
|
||||
summary: string
|
||||
extractedClauses: ExtractedClause[]
|
||||
applicableControls: string[]
|
||||
gaps: string[]
|
||||
confidence: number
|
||||
} {
|
||||
const parsed = this.parseJSONResponse(response)
|
||||
|
||||
return {
|
||||
summary: typeof parsed.summary === 'string' ? parsed.summary : '',
|
||||
extractedClauses: (Array.isArray(parsed.extractedClauses) ? parsed.extractedClauses : []).map(
|
||||
(clause: Record<string, unknown>) => ({
|
||||
id: String(clause.id || ''),
|
||||
text: String(clause.text || ''),
|
||||
type: String(clause.type || ''),
|
||||
relatedControlId: clause.relatedControlId
|
||||
? String(clause.relatedControlId)
|
||||
: null,
|
||||
})
|
||||
),
|
||||
applicableControls: Array.isArray(parsed.applicableControls)
|
||||
? parsed.applicableControls.map(String)
|
||||
: [],
|
||||
gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [],
|
||||
confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON from AI response (handles markdown code blocks)
|
||||
*/
|
||||
private parseJSONResponse(response: string): Record<string, unknown> {
|
||||
let jsonStr = response.trim()
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
if (jsonStr.startsWith('```json')) {
|
||||
jsonStr = jsonStr.slice(7)
|
||||
} else if (jsonStr.startsWith('```')) {
|
||||
jsonStr = jsonStr.slice(3)
|
||||
}
|
||||
|
||||
if (jsonStr.endsWith('```')) {
|
||||
jsonStr = jsonStr.slice(0, -3)
|
||||
}
|
||||
|
||||
jsonStr = jsonStr.trim()
|
||||
|
||||
try {
|
||||
return JSON.parse(jsonStr)
|
||||
} catch {
|
||||
// Try to extract JSON from the response
|
||||
const jsonMatch = jsonStr.match(/\{[\s\S]*\}/)
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
return JSON.parse(jsonMatch[0])
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map string to DocumentType
|
||||
*/
|
||||
private mapDocumentType(type: string): DocumentType {
|
||||
const typeMap: Record<string, DocumentType> = {
|
||||
AVV: 'AVV',
|
||||
DPA: 'DPA',
|
||||
SLA: 'SLA',
|
||||
NDA: 'NDA',
|
||||
POLICY: 'POLICY',
|
||||
CERTIFICATE: 'CERTIFICATE',
|
||||
AUDIT_REPORT: 'AUDIT_REPORT',
|
||||
OTHER: 'OTHER',
|
||||
}
|
||||
|
||||
return typeMap[type.toUpperCase()] || 'OTHER'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SINGLETON INSTANCE
|
||||
// =============================================================================
|
||||
|
||||
let analyzerInstance: TOMDocumentAnalyzer | null = null
|
||||
|
||||
export function getDocumentAnalyzer(
|
||||
options?: { apiEndpoint?: string; apiKey?: string }
|
||||
): TOMDocumentAnalyzer {
|
||||
if (!analyzerInstance) {
|
||||
analyzerInstance = new TOMDocumentAnalyzer(options)
|
||||
}
|
||||
return analyzerInstance
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Quick document analysis
|
||||
*/
|
||||
export async function analyzeEvidenceDocument(
|
||||
document: EvidenceDocument,
|
||||
documentText: string,
|
||||
language: 'de' | 'en' = 'de'
|
||||
): Promise<AnalysisResult> {
|
||||
return getDocumentAnalyzer().analyzeDocument(document, documentText, language)
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick document type detection
|
||||
*/
|
||||
export async function detectEvidenceDocumentType(
|
||||
documentText: string,
|
||||
filename: string
|
||||
): Promise<DocumentTypeDetectionResult> {
|
||||
return getDocumentAnalyzer().detectDocumentType(documentText, filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get evidence gaps for all controls
|
||||
*/
|
||||
export function getEvidenceGapsForAllControls(
|
||||
documents: EvidenceDocument[]
|
||||
): Map<string, { coverage: number; missing: string[] }> {
|
||||
const analyzer = getDocumentAnalyzer()
|
||||
const allControls = getAllControls()
|
||||
const gaps = new Map<string, { coverage: number; missing: string[] }>()
|
||||
|
||||
for (const control of allControls) {
|
||||
const result = analyzer.calculateEvidenceCoverage(control.id, documents)
|
||||
gaps.set(control.id, {
|
||||
coverage: result.coverage,
|
||||
missing: result.missingEvidence,
|
||||
})
|
||||
}
|
||||
|
||||
return gaps
|
||||
}
|
||||
427
admin-v2/lib/sdk/tom-generator/ai/prompts.ts
Normal file
427
admin-v2/lib/sdk/tom-generator/ai/prompts.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
// =============================================================================
|
||||
// TOM Generator AI Prompts
|
||||
// Prompts for document analysis and TOM description generation
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
CompanyProfile,
|
||||
DataProfile,
|
||||
ArchitectureProfile,
|
||||
RiskProfile,
|
||||
DocumentType,
|
||||
ControlLibraryEntry,
|
||||
} from '../types'
|
||||
|
||||
// =============================================================================
|
||||
// DOCUMENT ANALYSIS PROMPT
|
||||
// =============================================================================
|
||||
|
||||
export interface DocumentAnalysisPromptContext {
|
||||
documentType: DocumentType
|
||||
documentText: string
|
||||
controlIds?: string[]
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
export function getDocumentAnalysisPrompt(
|
||||
context: DocumentAnalysisPromptContext
|
||||
): string {
|
||||
const { documentType, documentText, controlIds, language = 'de' } = context
|
||||
|
||||
const controlContext = controlIds?.length
|
||||
? `\nRELEVANT CONTROL IDS: ${controlIds.join(', ')}`
|
||||
: ''
|
||||
|
||||
if (language === 'de') {
|
||||
return `Du bist ein Experte für Datenschutz-Compliance und analysierst ein Dokument für die TOM-Dokumentation nach DSGVO Art. 32.
|
||||
|
||||
DOKUMENTTYP: ${documentType}
|
||||
${controlContext}
|
||||
|
||||
DOKUMENTTEXT:
|
||||
${documentText}
|
||||
|
||||
AUFGABE: Analysiere das Dokument und extrahiere die folgenden Informationen:
|
||||
|
||||
1. SUMMARY: Eine Zusammenfassung in 2-3 Sätzen, die die Relevanz für den Datenschutz beschreibt.
|
||||
|
||||
2. EXTRACTED_CLAUSES: Alle Klauseln, die sich auf technische und organisatorische Sicherheitsmaßnahmen beziehen. Für jede Klausel:
|
||||
- id: Eindeutige ID (z.B. "clause-1")
|
||||
- text: Der extrahierte Text
|
||||
- type: Art der Maßnahme (z.B. "encryption", "access-control", "backup", "training")
|
||||
- relatedControlId: Falls zutreffend, die TOM-Control-ID (z.B. "TOM-ENC-01")
|
||||
|
||||
3. APPLICABLE_CONTROLS: Liste der TOM-Control-IDs, die durch dieses Dokument belegt werden könnten.
|
||||
|
||||
4. GAPS: Identifizierte Lücken oder fehlende Maßnahmen, die im Dokument nicht adressiert werden.
|
||||
|
||||
5. CONFIDENCE: Dein Vertrauenswert für die Analyse (0.0 bis 1.0).
|
||||
|
||||
Antworte im JSON-Format:
|
||||
{
|
||||
"summary": "...",
|
||||
"extractedClauses": [
|
||||
{ "id": "...", "text": "...", "type": "...", "relatedControlId": "..." }
|
||||
],
|
||||
"applicableControls": ["TOM-..."],
|
||||
"gaps": ["..."],
|
||||
"confidence": 0.85
|
||||
}`
|
||||
}
|
||||
|
||||
return `You are a data protection compliance expert analyzing a document for TOM documentation according to GDPR Art. 32.
|
||||
|
||||
DOCUMENT TYPE: ${documentType}
|
||||
${controlContext}
|
||||
|
||||
DOCUMENT TEXT:
|
||||
${documentText}
|
||||
|
||||
TASK: Analyze the document and extract the following information:
|
||||
|
||||
1. SUMMARY: A 2-3 sentence summary describing the relevance for data protection.
|
||||
|
||||
2. EXTRACTED_CLAUSES: All clauses related to technical and organizational security measures. For each clause:
|
||||
- id: Unique ID (e.g., "clause-1")
|
||||
- text: The extracted text
|
||||
- type: Type of measure (e.g., "encryption", "access-control", "backup", "training")
|
||||
- relatedControlId: If applicable, the TOM control ID (e.g., "TOM-ENC-01")
|
||||
|
||||
3. APPLICABLE_CONTROLS: List of TOM control IDs that could be evidenced by this document.
|
||||
|
||||
4. GAPS: Identified gaps or missing measures not addressed in the document.
|
||||
|
||||
5. CONFIDENCE: Your confidence score for the analysis (0.0 to 1.0).
|
||||
|
||||
Respond in JSON format:
|
||||
{
|
||||
"summary": "...",
|
||||
"extractedClauses": [
|
||||
{ "id": "...", "text": "...", "type": "...", "relatedControlId": "..." }
|
||||
],
|
||||
"applicableControls": ["TOM-..."],
|
||||
"gaps": ["..."],
|
||||
"confidence": 0.85
|
||||
}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOM DESCRIPTION GENERATION PROMPT
|
||||
// =============================================================================
|
||||
|
||||
export interface TOMDescriptionPromptContext {
|
||||
control: ControlLibraryEntry
|
||||
companyProfile: CompanyProfile
|
||||
dataProfile: DataProfile
|
||||
architectureProfile: ArchitectureProfile
|
||||
riskProfile: RiskProfile
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
export function getTOMDescriptionPrompt(
|
||||
context: TOMDescriptionPromptContext
|
||||
): string {
|
||||
const {
|
||||
control,
|
||||
companyProfile,
|
||||
dataProfile,
|
||||
architectureProfile,
|
||||
riskProfile,
|
||||
language = 'de',
|
||||
} = context
|
||||
|
||||
if (language === 'de') {
|
||||
return `Du bist ein Experte für Datenschutz-Compliance und erstellst eine unternehmensspezifische TOM-Beschreibung.
|
||||
|
||||
KONTROLLE:
|
||||
- Name: ${control.name.de}
|
||||
- Beschreibung: ${control.description.de}
|
||||
- Kategorie: ${control.category}
|
||||
- Typ: ${control.type}
|
||||
|
||||
UNTERNEHMENSPROFIL:
|
||||
- Branche: ${companyProfile.industry}
|
||||
- Größe: ${companyProfile.size}
|
||||
- Rolle: ${companyProfile.role}
|
||||
- Produkte/Services: ${companyProfile.products.join(', ')}
|
||||
|
||||
DATENPROFIL:
|
||||
- Datenkategorien: ${dataProfile.categories.join(', ')}
|
||||
- Besondere Kategorien: ${dataProfile.hasSpecialCategories ? 'Ja' : 'Nein'}
|
||||
- Betroffene: ${dataProfile.subjects.join(', ')}
|
||||
- Datenvolumen: ${dataProfile.dataVolume}
|
||||
|
||||
ARCHITEKTUR:
|
||||
- Hosting-Modell: ${architectureProfile.hostingModel}
|
||||
- Standort: ${architectureProfile.hostingLocation}
|
||||
- Mandantentrennung: ${architectureProfile.multiTenancy}
|
||||
|
||||
SCHUTZBEDARF: ${riskProfile.protectionLevel}
|
||||
|
||||
AUFGABE: Erstelle eine unternehmensspezifische Beschreibung dieser TOM in 3-5 Sätzen.
|
||||
Die Beschreibung soll:
|
||||
- Auf das spezifische Unternehmensprofil zugeschnitten sein
|
||||
- Konkrete Maßnahmen beschreiben, die für dieses Unternehmen relevant sind
|
||||
- In formeller Geschäftssprache verfasst sein
|
||||
- Keine Platzhalter oder generischen Formulierungen enthalten
|
||||
|
||||
Antworte nur mit der Beschreibung, ohne zusätzliche Erklärungen.`
|
||||
}
|
||||
|
||||
return `You are a data protection compliance expert creating a company-specific TOM description.
|
||||
|
||||
CONTROL:
|
||||
- Name: ${control.name.en}
|
||||
- Description: ${control.description.en}
|
||||
- Category: ${control.category}
|
||||
- Type: ${control.type}
|
||||
|
||||
COMPANY PROFILE:
|
||||
- Industry: ${companyProfile.industry}
|
||||
- Size: ${companyProfile.size}
|
||||
- Role: ${companyProfile.role}
|
||||
- Products/Services: ${companyProfile.products.join(', ')}
|
||||
|
||||
DATA PROFILE:
|
||||
- Data Categories: ${dataProfile.categories.join(', ')}
|
||||
- Special Categories: ${dataProfile.hasSpecialCategories ? 'Yes' : 'No'}
|
||||
- Data Subjects: ${dataProfile.subjects.join(', ')}
|
||||
- Data Volume: ${dataProfile.dataVolume}
|
||||
|
||||
ARCHITECTURE:
|
||||
- Hosting Model: ${architectureProfile.hostingModel}
|
||||
- Location: ${architectureProfile.hostingLocation}
|
||||
- Multi-tenancy: ${architectureProfile.multiTenancy}
|
||||
|
||||
PROTECTION LEVEL: ${riskProfile.protectionLevel}
|
||||
|
||||
TASK: Create a company-specific description of this TOM in 3-5 sentences.
|
||||
The description should:
|
||||
- Be tailored to the specific company profile
|
||||
- Describe concrete measures relevant to this company
|
||||
- Be written in formal business language
|
||||
- Contain no placeholders or generic formulations
|
||||
|
||||
Respond only with the description, without additional explanations.`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GAP RECOMMENDATIONS PROMPT
|
||||
// =============================================================================
|
||||
|
||||
export interface GapRecommendationsPromptContext {
|
||||
missingControls: Array<{ controlId: string; name: string; priority: string }>
|
||||
partialControls: Array<{ controlId: string; name: string; missingAspects: string[] }>
|
||||
companyProfile: CompanyProfile
|
||||
riskProfile: RiskProfile
|
||||
language?: 'de' | 'en'
|
||||
}
|
||||
|
||||
export function getGapRecommendationsPrompt(
|
||||
context: GapRecommendationsPromptContext
|
||||
): string {
|
||||
const {
|
||||
missingControls,
|
||||
partialControls,
|
||||
companyProfile,
|
||||
riskProfile,
|
||||
language = 'de',
|
||||
} = context
|
||||
|
||||
const missingList = missingControls
|
||||
.map((c) => `- ${c.name} (${c.controlId}, Priorität: ${c.priority})`)
|
||||
.join('\n')
|
||||
|
||||
const partialList = partialControls
|
||||
.map((c) => `- ${c.name} (${c.controlId}): Fehlend: ${c.missingAspects.join(', ')}`)
|
||||
.join('\n')
|
||||
|
||||
if (language === 'de') {
|
||||
return `Du bist ein Experte für Datenschutz-Compliance und erstellst Handlungsempfehlungen für TOM-Lücken.
|
||||
|
||||
UNTERNEHMEN:
|
||||
- Branche: ${companyProfile.industry}
|
||||
- Größe: ${companyProfile.size}
|
||||
- Rolle: ${companyProfile.role}
|
||||
|
||||
SCHUTZBEDARF: ${riskProfile.protectionLevel}
|
||||
|
||||
FEHLENDE KONTROLLEN:
|
||||
${missingList || 'Keine'}
|
||||
|
||||
TEILWEISE IMPLEMENTIERTE KONTROLLEN:
|
||||
${partialList || 'Keine'}
|
||||
|
||||
AUFGABE: Erstelle konkrete Handlungsempfehlungen, um die Lücken zu schließen.
|
||||
|
||||
Für jede Empfehlung:
|
||||
1. Priorisiere nach Schutzbedarf und DSGVO-Relevanz
|
||||
2. Berücksichtige die Unternehmensgröße und Branche
|
||||
3. Gib konkrete, umsetzbare Schritte an
|
||||
4. Schätze den Aufwand ein (niedrig/mittel/hoch)
|
||||
|
||||
Antworte im JSON-Format:
|
||||
{
|
||||
"recommendations": [
|
||||
{
|
||||
"priority": "HIGH",
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"steps": ["..."],
|
||||
"effort": "MEDIUM",
|
||||
"relatedControls": ["TOM-..."]
|
||||
}
|
||||
],
|
||||
"summary": "Kurze Zusammenfassung der wichtigsten Maßnahmen"
|
||||
}`
|
||||
}
|
||||
|
||||
return `You are a data protection compliance expert creating recommendations for TOM gaps.
|
||||
|
||||
COMPANY:
|
||||
- Industry: ${companyProfile.industry}
|
||||
- Size: ${companyProfile.size}
|
||||
- Role: ${companyProfile.role}
|
||||
|
||||
PROTECTION LEVEL: ${riskProfile.protectionLevel}
|
||||
|
||||
MISSING CONTROLS:
|
||||
${missingList || 'None'}
|
||||
|
||||
PARTIALLY IMPLEMENTED CONTROLS:
|
||||
${partialList || 'None'}
|
||||
|
||||
TASK: Create concrete recommendations to close the gaps.
|
||||
|
||||
For each recommendation:
|
||||
1. Prioritize by protection level and GDPR relevance
|
||||
2. Consider company size and industry
|
||||
3. Provide concrete, actionable steps
|
||||
4. Estimate effort (low/medium/high)
|
||||
|
||||
Respond in JSON format:
|
||||
{
|
||||
"recommendations": [
|
||||
{
|
||||
"priority": "HIGH",
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"steps": ["..."],
|
||||
"effort": "MEDIUM",
|
||||
"relatedControls": ["TOM-..."]
|
||||
}
|
||||
],
|
||||
"summary": "Brief summary of the most important measures"
|
||||
}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCUMENT TYPE DETECTION PROMPT
|
||||
// =============================================================================
|
||||
|
||||
export function getDocumentTypeDetectionPrompt(
|
||||
documentText: string,
|
||||
filename: string
|
||||
): string {
|
||||
return `Du bist ein Experte für Datenschutz-Dokumente und sollst den Dokumenttyp erkennen.
|
||||
|
||||
DATEINAME: ${filename}
|
||||
|
||||
DOKUMENTTEXT (Auszug):
|
||||
${documentText.substring(0, 2000)}
|
||||
|
||||
MÖGLICHE DOKUMENTTYPEN:
|
||||
- AVV: Auftragsverarbeitungsvertrag
|
||||
- DPA: Data Processing Agreement (englisch)
|
||||
- SLA: Service Level Agreement
|
||||
- NDA: Geheimhaltungsvereinbarung
|
||||
- POLICY: Interne Richtlinie (z.B. Passwortrichtlinie, IT-Sicherheitsrichtlinie)
|
||||
- CERTIFICATE: Zertifikat (z.B. ISO 27001, SOC 2)
|
||||
- AUDIT_REPORT: Audit-Bericht oder Prüfbericht
|
||||
- OTHER: Sonstiges Dokument
|
||||
|
||||
Antworte im JSON-Format:
|
||||
{
|
||||
"documentType": "...",
|
||||
"confidence": 0.85,
|
||||
"reasoning": "Kurze Begründung"
|
||||
}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CLAUSE EXTRACTION PROMPT
|
||||
// =============================================================================
|
||||
|
||||
export function getClauseExtractionPrompt(
|
||||
documentText: string,
|
||||
controlCategory: string
|
||||
): string {
|
||||
return `Du bist ein Experte für Datenschutz-Compliance und extrahierst Klauseln aus einem Dokument.
|
||||
|
||||
GESUCHTE KATEGORIE: ${controlCategory}
|
||||
|
||||
DOKUMENTTEXT:
|
||||
${documentText}
|
||||
|
||||
AUFGABE: Extrahiere alle Klauseln, die sich auf die Kategorie "${controlCategory}" beziehen.
|
||||
|
||||
Antworte im JSON-Format:
|
||||
{
|
||||
"clauses": [
|
||||
{
|
||||
"id": "clause-1",
|
||||
"text": "Der extrahierte Text der Klausel",
|
||||
"section": "Abschnittsnummer oder -name falls vorhanden",
|
||||
"relevance": "Kurze Erklärung der Relevanz",
|
||||
"matchScore": 0.9
|
||||
}
|
||||
],
|
||||
"totalFound": 3
|
||||
}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPLIANCE ASSESSMENT PROMPT
|
||||
// =============================================================================
|
||||
|
||||
export function getComplianceAssessmentPrompt(
|
||||
tomDescription: string,
|
||||
evidenceDescriptions: string[],
|
||||
controlRequirements: string[]
|
||||
): string {
|
||||
return `Du bist ein Experte für Datenschutz-Compliance und bewertest die Umsetzung einer TOM.
|
||||
|
||||
TOM-BESCHREIBUNG:
|
||||
${tomDescription}
|
||||
|
||||
ANFORDERUNGEN AN NACHWEISE:
|
||||
${controlRequirements.map((r, i) => `${i + 1}. ${r}`).join('\n')}
|
||||
|
||||
VORHANDENE NACHWEISE:
|
||||
${evidenceDescriptions.map((e, i) => `${i + 1}. ${e}`).join('\n') || 'Keine Nachweise vorhanden'}
|
||||
|
||||
AUFGABE: Bewerte den Umsetzungsgrad dieser TOM.
|
||||
|
||||
Antworte im JSON-Format:
|
||||
{
|
||||
"implementationStatus": "NOT_IMPLEMENTED" | "PARTIAL" | "IMPLEMENTED",
|
||||
"score": 0-100,
|
||||
"coveredRequirements": ["..."],
|
||||
"missingRequirements": ["..."],
|
||||
"recommendations": ["..."],
|
||||
"reasoning": "Begründung der Bewertung"
|
||||
}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export const AI_PROMPTS = {
|
||||
documentAnalysis: getDocumentAnalysisPrompt,
|
||||
tomDescription: getTOMDescriptionPrompt,
|
||||
gapRecommendations: getGapRecommendationsPrompt,
|
||||
documentTypeDetection: getDocumentTypeDetectionPrompt,
|
||||
clauseExtraction: getClauseExtractionPrompt,
|
||||
complianceAssessment: getComplianceAssessmentPrompt,
|
||||
}
|
||||
698
admin-v2/lib/sdk/tom-generator/context.tsx
Normal file
698
admin-v2/lib/sdk/tom-generator/context.tsx
Normal file
@@ -0,0 +1,698 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// TOM Generator Context
|
||||
// State management for the TOM Generator Wizard
|
||||
// =============================================================================
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
TOMGeneratorState,
|
||||
TOMGeneratorStepId,
|
||||
CompanyProfile,
|
||||
DataProfile,
|
||||
ArchitectureProfile,
|
||||
SecurityProfile,
|
||||
RiskProfile,
|
||||
EvidenceDocument,
|
||||
DerivedTOM,
|
||||
GapAnalysisResult,
|
||||
ExportRecord,
|
||||
WizardStep,
|
||||
createInitialTOMGeneratorState,
|
||||
TOM_GENERATOR_STEPS,
|
||||
getStepIndex,
|
||||
calculateProtectionLevel,
|
||||
isDSFARequired,
|
||||
hasSpecialCategories,
|
||||
} from './types'
|
||||
import { TOMRulesEngine } from './rules-engine'
|
||||
|
||||
// =============================================================================
|
||||
// ACTION TYPES
|
||||
// =============================================================================
|
||||
|
||||
type TOMGeneratorAction =
|
||||
| { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } }
|
||||
| { type: 'RESET'; payload: { tenantId: string } }
|
||||
| { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId }
|
||||
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
|
||||
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial<CompanyProfile> }
|
||||
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
|
||||
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
|
||||
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
|
||||
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
|
||||
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
|
||||
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
|
||||
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
|
||||
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
|
||||
| { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } }
|
||||
| { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId }
|
||||
| { type: 'ADD_EVIDENCE'; payload: EvidenceDocument }
|
||||
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<EvidenceDocument> } }
|
||||
| { type: 'DELETE_EVIDENCE'; payload: string }
|
||||
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
|
||||
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
|
||||
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
|
||||
| { type: 'ADD_EXPORT'; payload: ExportRecord }
|
||||
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
|
||||
|
||||
// =============================================================================
|
||||
// REDUCER
|
||||
// =============================================================================
|
||||
|
||||
function tomGeneratorReducer(
|
||||
state: TOMGeneratorState,
|
||||
action: TOMGeneratorAction
|
||||
): TOMGeneratorState {
|
||||
const updateState = (updates: Partial<TOMGeneratorState>): TOMGeneratorState => ({
|
||||
...state,
|
||||
...updates,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
switch (action.type) {
|
||||
case 'INITIALIZE': {
|
||||
if (action.payload.state) {
|
||||
return action.payload.state
|
||||
}
|
||||
return createInitialTOMGeneratorState(action.payload.tenantId)
|
||||
}
|
||||
|
||||
case 'RESET': {
|
||||
return createInitialTOMGeneratorState(action.payload.tenantId)
|
||||
}
|
||||
|
||||
case 'SET_CURRENT_STEP': {
|
||||
return updateState({ currentStep: action.payload })
|
||||
}
|
||||
|
||||
case 'SET_COMPANY_PROFILE': {
|
||||
return updateState({ companyProfile: action.payload })
|
||||
}
|
||||
|
||||
case 'UPDATE_COMPANY_PROFILE': {
|
||||
if (!state.companyProfile) return state
|
||||
return updateState({
|
||||
companyProfile: { ...state.companyProfile, ...action.payload },
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_DATA_PROFILE': {
|
||||
// Automatically set hasSpecialCategories based on categories
|
||||
const profile: DataProfile = {
|
||||
...action.payload,
|
||||
hasSpecialCategories: hasSpecialCategories(action.payload.categories),
|
||||
}
|
||||
return updateState({ dataProfile: profile })
|
||||
}
|
||||
|
||||
case 'UPDATE_DATA_PROFILE': {
|
||||
if (!state.dataProfile) return state
|
||||
const updatedProfile = { ...state.dataProfile, ...action.payload }
|
||||
// Recalculate hasSpecialCategories if categories changed
|
||||
if (action.payload.categories) {
|
||||
updatedProfile.hasSpecialCategories = hasSpecialCategories(
|
||||
action.payload.categories
|
||||
)
|
||||
}
|
||||
return updateState({ dataProfile: updatedProfile })
|
||||
}
|
||||
|
||||
case 'SET_ARCHITECTURE_PROFILE': {
|
||||
return updateState({ architectureProfile: action.payload })
|
||||
}
|
||||
|
||||
case 'UPDATE_ARCHITECTURE_PROFILE': {
|
||||
if (!state.architectureProfile) return state
|
||||
return updateState({
|
||||
architectureProfile: { ...state.architectureProfile, ...action.payload },
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_SECURITY_PROFILE': {
|
||||
return updateState({ securityProfile: action.payload })
|
||||
}
|
||||
|
||||
case 'UPDATE_SECURITY_PROFILE': {
|
||||
if (!state.securityProfile) return state
|
||||
return updateState({
|
||||
securityProfile: { ...state.securityProfile, ...action.payload },
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_RISK_PROFILE': {
|
||||
// Automatically calculate protection level and DSFA requirement
|
||||
const profile: RiskProfile = {
|
||||
...action.payload,
|
||||
protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment),
|
||||
dsfaRequired: isDSFARequired(state.dataProfile, action.payload),
|
||||
}
|
||||
return updateState({ riskProfile: profile })
|
||||
}
|
||||
|
||||
case 'UPDATE_RISK_PROFILE': {
|
||||
if (!state.riskProfile) return state
|
||||
const updatedProfile = { ...state.riskProfile, ...action.payload }
|
||||
// Recalculate protection level if CIA assessment changed
|
||||
if (action.payload.ciaAssessment) {
|
||||
updatedProfile.protectionLevel = calculateProtectionLevel(
|
||||
action.payload.ciaAssessment
|
||||
)
|
||||
}
|
||||
// Recalculate DSFA requirement
|
||||
updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile)
|
||||
return updateState({ riskProfile: updatedProfile })
|
||||
}
|
||||
|
||||
case 'COMPLETE_STEP': {
|
||||
const updatedSteps = state.steps.map((step) =>
|
||||
step.id === action.payload.stepId
|
||||
? {
|
||||
...step,
|
||||
completed: true,
|
||||
data: action.payload.data,
|
||||
validatedAt: new Date(),
|
||||
}
|
||||
: step
|
||||
)
|
||||
return updateState({ steps: updatedSteps })
|
||||
}
|
||||
|
||||
case 'UNCOMPLETE_STEP': {
|
||||
const updatedSteps = state.steps.map((step) =>
|
||||
step.id === action.payload
|
||||
? { ...step, completed: false, validatedAt: null }
|
||||
: step
|
||||
)
|
||||
return updateState({ steps: updatedSteps })
|
||||
}
|
||||
|
||||
case 'ADD_EVIDENCE': {
|
||||
return updateState({
|
||||
documents: [...state.documents, action.payload],
|
||||
})
|
||||
}
|
||||
|
||||
case 'UPDATE_EVIDENCE': {
|
||||
const updatedDocuments = state.documents.map((doc) =>
|
||||
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
|
||||
)
|
||||
return updateState({ documents: updatedDocuments })
|
||||
}
|
||||
|
||||
case 'DELETE_EVIDENCE': {
|
||||
return updateState({
|
||||
documents: state.documents.filter((doc) => doc.id !== action.payload),
|
||||
})
|
||||
}
|
||||
|
||||
case 'SET_DERIVED_TOMS': {
|
||||
return updateState({ derivedTOMs: action.payload })
|
||||
}
|
||||
|
||||
case 'UPDATE_DERIVED_TOM': {
|
||||
const updatedTOMs = state.derivedTOMs.map((tom) =>
|
||||
tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom
|
||||
)
|
||||
return updateState({ derivedTOMs: updatedTOMs })
|
||||
}
|
||||
|
||||
case 'SET_GAP_ANALYSIS': {
|
||||
return updateState({ gapAnalysis: action.payload })
|
||||
}
|
||||
|
||||
case 'ADD_EXPORT': {
|
||||
return updateState({
|
||||
exports: [...state.exports, action.payload],
|
||||
})
|
||||
}
|
||||
|
||||
case 'LOAD_STATE': {
|
||||
return action.payload
|
||||
}
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT VALUE INTERFACE
|
||||
// =============================================================================
|
||||
|
||||
interface TOMGeneratorContextValue {
|
||||
state: TOMGeneratorState
|
||||
dispatch: React.Dispatch<TOMGeneratorAction>
|
||||
|
||||
// Navigation
|
||||
currentStepIndex: number
|
||||
totalSteps: number
|
||||
canGoNext: boolean
|
||||
canGoPrevious: boolean
|
||||
goToStep: (stepId: TOMGeneratorStepId) => void
|
||||
goToNextStep: () => void
|
||||
goToPreviousStep: () => void
|
||||
completeCurrentStep: (data: unknown) => void
|
||||
|
||||
// Profile setters
|
||||
setCompanyProfile: (profile: CompanyProfile) => void
|
||||
updateCompanyProfile: (data: Partial<CompanyProfile>) => void
|
||||
setDataProfile: (profile: DataProfile) => void
|
||||
updateDataProfile: (data: Partial<DataProfile>) => void
|
||||
setArchitectureProfile: (profile: ArchitectureProfile) => void
|
||||
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
|
||||
setSecurityProfile: (profile: SecurityProfile) => void
|
||||
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
|
||||
setRiskProfile: (profile: RiskProfile) => void
|
||||
updateRiskProfile: (data: Partial<RiskProfile>) => void
|
||||
|
||||
// Evidence management
|
||||
addEvidence: (document: EvidenceDocument) => void
|
||||
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
|
||||
deleteEvidence: (id: string) => void
|
||||
|
||||
// TOM derivation
|
||||
deriveTOMs: () => void
|
||||
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
|
||||
|
||||
// Gap analysis
|
||||
runGapAnalysis: () => void
|
||||
|
||||
// Export
|
||||
addExport: (record: ExportRecord) => void
|
||||
|
||||
// Persistence
|
||||
saveState: () => Promise<void>
|
||||
loadState: () => Promise<void>
|
||||
resetState: () => void
|
||||
|
||||
// Status
|
||||
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
|
||||
getCompletionPercentage: () => number
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT
|
||||
// =============================================================================
|
||||
|
||||
const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(null)
|
||||
|
||||
// =============================================================================
|
||||
// STORAGE KEYS
|
||||
// =============================================================================
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'tom-generator-state-'
|
||||
|
||||
function getStorageKey(tenantId: string): string {
|
||||
return `${STORAGE_KEY_PREFIX}${tenantId}`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROVIDER COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
interface TOMGeneratorProviderProps {
|
||||
children: ReactNode
|
||||
tenantId: string
|
||||
initialState?: TOMGeneratorState
|
||||
enablePersistence?: boolean
|
||||
}
|
||||
|
||||
export function TOMGeneratorProvider({
|
||||
children,
|
||||
tenantId,
|
||||
initialState,
|
||||
enablePersistence = true,
|
||||
}: TOMGeneratorProviderProps) {
|
||||
const [state, dispatch] = useReducer(
|
||||
tomGeneratorReducer,
|
||||
initialState ?? createInitialTOMGeneratorState(tenantId)
|
||||
)
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [error, setError] = React.useState<string | null>(null)
|
||||
|
||||
const rulesEngineRef = useRef<TOMRulesEngine | null>(null)
|
||||
|
||||
// Initialize rules engine
|
||||
useEffect(() => {
|
||||
if (!rulesEngineRef.current) {
|
||||
rulesEngineRef.current = new TOMRulesEngine()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load state from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (enablePersistence && typeof window !== 'undefined') {
|
||||
try {
|
||||
const stored = localStorage.getItem(getStorageKey(tenantId))
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
// Convert date strings back to Date objects
|
||||
if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt)
|
||||
if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt)
|
||||
if (parsed.steps) {
|
||||
parsed.steps = parsed.steps.map((step: WizardStep) => ({
|
||||
...step,
|
||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
||||
}))
|
||||
}
|
||||
if (parsed.documents) {
|
||||
parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({
|
||||
...doc,
|
||||
uploadedAt: new Date(doc.uploadedAt),
|
||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
||||
aiAnalysis: doc.aiAnalysis
|
||||
? {
|
||||
...doc.aiAnalysis,
|
||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
||||
}
|
||||
: null,
|
||||
}))
|
||||
}
|
||||
if (parsed.derivedTOMs) {
|
||||
parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({
|
||||
...tom,
|
||||
implementationDate: tom.implementationDate
|
||||
? new Date(tom.implementationDate)
|
||||
: null,
|
||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
||||
}))
|
||||
}
|
||||
if (parsed.gapAnalysis?.generatedAt) {
|
||||
parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt)
|
||||
}
|
||||
if (parsed.exports) {
|
||||
parsed.exports = parsed.exports.map((exp: ExportRecord) => ({
|
||||
...exp,
|
||||
generatedAt: new Date(exp.generatedAt),
|
||||
}))
|
||||
}
|
||||
dispatch({ type: 'LOAD_STATE', payload: parsed })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load TOM Generator state from localStorage:', e)
|
||||
}
|
||||
}
|
||||
}, [tenantId, enablePersistence])
|
||||
|
||||
// Save state to localStorage on changes
|
||||
useEffect(() => {
|
||||
if (enablePersistence && typeof window !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state))
|
||||
} catch (e) {
|
||||
console.error('Failed to save TOM Generator state to localStorage:', e)
|
||||
}
|
||||
}
|
||||
}, [state, tenantId, enablePersistence])
|
||||
|
||||
// Navigation helpers
|
||||
const currentStepIndex = getStepIndex(state.currentStep)
|
||||
const totalSteps = TOM_GENERATOR_STEPS.length
|
||||
|
||||
const canGoNext = currentStepIndex < totalSteps - 1
|
||||
const canGoPrevious = currentStepIndex > 0
|
||||
|
||||
const goToStep = useCallback((stepId: TOMGeneratorStepId) => {
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
||||
}, [])
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
if (canGoNext) {
|
||||
const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1]
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id })
|
||||
}
|
||||
}, [canGoNext, currentStepIndex])
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
if (canGoPrevious) {
|
||||
const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1]
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id })
|
||||
}
|
||||
}, [canGoPrevious, currentStepIndex])
|
||||
|
||||
const completeCurrentStep = useCallback(
|
||||
(data: unknown) => {
|
||||
dispatch({
|
||||
type: 'COMPLETE_STEP',
|
||||
payload: { stepId: state.currentStep, data },
|
||||
})
|
||||
},
|
||||
[state.currentStep]
|
||||
)
|
||||
|
||||
// Profile setters
|
||||
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
|
||||
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
|
||||
}, [])
|
||||
|
||||
const updateCompanyProfile = useCallback((data: Partial<CompanyProfile>) => {
|
||||
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
|
||||
}, [])
|
||||
|
||||
const setDataProfile = useCallback((profile: DataProfile) => {
|
||||
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
|
||||
}, [])
|
||||
|
||||
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
|
||||
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
|
||||
}, [])
|
||||
|
||||
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
|
||||
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
|
||||
}, [])
|
||||
|
||||
const updateArchitectureProfile = useCallback(
|
||||
(data: Partial<ArchitectureProfile>) => {
|
||||
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
|
||||
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
|
||||
}, [])
|
||||
|
||||
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
|
||||
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
|
||||
}, [])
|
||||
|
||||
const setRiskProfile = useCallback((profile: RiskProfile) => {
|
||||
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
|
||||
}, [])
|
||||
|
||||
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
|
||||
dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data })
|
||||
}, [])
|
||||
|
||||
// Evidence management
|
||||
const addEvidence = useCallback((document: EvidenceDocument) => {
|
||||
dispatch({ type: 'ADD_EVIDENCE', payload: document })
|
||||
}, [])
|
||||
|
||||
const updateEvidence = useCallback(
|
||||
(id: string, data: Partial<EvidenceDocument>) => {
|
||||
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const deleteEvidence = useCallback((id: string) => {
|
||||
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
||||
}, [])
|
||||
|
||||
// TOM derivation
|
||||
const deriveTOMs = useCallback(() => {
|
||||
if (!rulesEngineRef.current) return
|
||||
|
||||
const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({
|
||||
companyProfile: state.companyProfile,
|
||||
dataProfile: state.dataProfile,
|
||||
architectureProfile: state.architectureProfile,
|
||||
securityProfile: state.securityProfile,
|
||||
riskProfile: state.riskProfile,
|
||||
})
|
||||
|
||||
dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs })
|
||||
}, [
|
||||
state.companyProfile,
|
||||
state.dataProfile,
|
||||
state.architectureProfile,
|
||||
state.securityProfile,
|
||||
state.riskProfile,
|
||||
])
|
||||
|
||||
const updateDerivedTOM = useCallback(
|
||||
(id: string, data: Partial<DerivedTOM>) => {
|
||||
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Gap analysis
|
||||
const runGapAnalysis = useCallback(() => {
|
||||
if (!rulesEngineRef.current) return
|
||||
|
||||
const result = rulesEngineRef.current.performGapAnalysis(
|
||||
state.derivedTOMs,
|
||||
state.documents
|
||||
)
|
||||
|
||||
dispatch({ type: 'SET_GAP_ANALYSIS', payload: result })
|
||||
}, [state.derivedTOMs, state.documents])
|
||||
|
||||
// Export
|
||||
const addExport = useCallback((record: ExportRecord) => {
|
||||
dispatch({ type: 'ADD_EXPORT', payload: record })
|
||||
}, [])
|
||||
|
||||
// Persistence
|
||||
const saveState = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
// API call to save state
|
||||
const response = await fetch('/api/sdk/v1/tom-generator/state', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tenantId, state }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save state')
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unknown error')
|
||||
throw e
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [tenantId, state])
|
||||
|
||||
const loadState = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/sdk/v1/tom-generator/state?tenantId=${tenantId}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load state')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
if (data.state) {
|
||||
dispatch({ type: 'LOAD_STATE', payload: data.state })
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unknown error')
|
||||
throw e
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [tenantId])
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
dispatch({ type: 'RESET', payload: { tenantId } })
|
||||
}, [tenantId])
|
||||
|
||||
// Status helpers
|
||||
const isStepCompleted = useCallback(
|
||||
(stepId: TOMGeneratorStepId) => {
|
||||
const step = state.steps.find((s) => s.id === stepId)
|
||||
return step?.completed ?? false
|
||||
},
|
||||
[state.steps]
|
||||
)
|
||||
|
||||
const getCompletionPercentage = useCallback(() => {
|
||||
const completedSteps = state.steps.filter((s) => s.completed).length
|
||||
return Math.round((completedSteps / totalSteps) * 100)
|
||||
}, [state.steps, totalSteps])
|
||||
|
||||
const contextValue: TOMGeneratorContextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
|
||||
currentStepIndex,
|
||||
totalSteps,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
completeCurrentStep,
|
||||
|
||||
setCompanyProfile,
|
||||
updateCompanyProfile,
|
||||
setDataProfile,
|
||||
updateDataProfile,
|
||||
setArchitectureProfile,
|
||||
updateArchitectureProfile,
|
||||
setSecurityProfile,
|
||||
updateSecurityProfile,
|
||||
setRiskProfile,
|
||||
updateRiskProfile,
|
||||
|
||||
addEvidence,
|
||||
updateEvidence,
|
||||
deleteEvidence,
|
||||
|
||||
deriveTOMs,
|
||||
updateDerivedTOM,
|
||||
|
||||
runGapAnalysis,
|
||||
|
||||
addExport,
|
||||
|
||||
saveState,
|
||||
loadState,
|
||||
resetState,
|
||||
|
||||
isStepCompleted,
|
||||
getCompletionPercentage,
|
||||
isLoading,
|
||||
error,
|
||||
}
|
||||
|
||||
return (
|
||||
<TOMGeneratorContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</TOMGeneratorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useTOMGenerator(): TOMGeneratorContextValue {
|
||||
const context = useContext(TOMGeneratorContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useTOMGenerator must be used within a TOMGeneratorProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export { TOMGeneratorContext }
|
||||
export type { TOMGeneratorAction, TOMGeneratorContextValue }
|
||||
1848
admin-v2/lib/sdk/tom-generator/controls/controls.yml
Normal file
1848
admin-v2/lib/sdk/tom-generator/controls/controls.yml
Normal file
File diff suppressed because it is too large
Load Diff
2240
admin-v2/lib/sdk/tom-generator/controls/loader.ts
Normal file
2240
admin-v2/lib/sdk/tom-generator/controls/loader.ts
Normal file
File diff suppressed because it is too large
Load Diff
518
admin-v2/lib/sdk/tom-generator/demo-data/index.ts
Normal file
518
admin-v2/lib/sdk/tom-generator/demo-data/index.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Demo Data
|
||||
// Sample data for demonstration and testing
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
TOMGeneratorState,
|
||||
CompanyProfile,
|
||||
DataProfile,
|
||||
ArchitectureProfile,
|
||||
SecurityProfile,
|
||||
RiskProfile,
|
||||
EvidenceDocument,
|
||||
DerivedTOM,
|
||||
GapAnalysisResult,
|
||||
TOM_GENERATOR_STEPS,
|
||||
} from '../types'
|
||||
import { getTOMRulesEngine } from '../rules-engine'
|
||||
|
||||
// =============================================================================
|
||||
// DEMO COMPANY PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_COMPANY_PROFILES: Record<string, CompanyProfile> = {
|
||||
saas: {
|
||||
id: 'demo-company-saas',
|
||||
name: 'CloudTech Solutions GmbH',
|
||||
industry: 'Software / SaaS',
|
||||
size: 'MEDIUM',
|
||||
role: 'PROCESSOR',
|
||||
products: ['Cloud CRM', 'Analytics Platform', 'API Services'],
|
||||
dpoPerson: 'Dr. Maria Schmidt',
|
||||
dpoEmail: 'dpo@cloudtech.de',
|
||||
itSecurityContact: 'Thomas Müller',
|
||||
},
|
||||
healthcare: {
|
||||
id: 'demo-company-health',
|
||||
name: 'MediCare Digital GmbH',
|
||||
industry: 'Gesundheitswesen / HealthTech',
|
||||
size: 'SMALL',
|
||||
role: 'CONTROLLER',
|
||||
products: ['Patientenportal', 'Telemedizin-App', 'Terminbuchung'],
|
||||
dpoPerson: 'Dr. Klaus Weber',
|
||||
dpoEmail: 'datenschutz@medicare.de',
|
||||
itSecurityContact: 'Anna Bauer',
|
||||
},
|
||||
enterprise: {
|
||||
id: 'demo-company-enterprise',
|
||||
name: 'GlobalCorp AG',
|
||||
industry: 'Finanzdienstleistungen',
|
||||
size: 'ENTERPRISE',
|
||||
role: 'CONTROLLER',
|
||||
products: ['Online Banking', 'Investment Platform', 'Payment Services'],
|
||||
dpoPerson: 'Prof. Dr. Hans Meyer',
|
||||
dpoEmail: 'privacy@globalcorp.de',
|
||||
itSecurityContact: 'Security Team',
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO DATA PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_DATA_PROFILES: Record<string, DataProfile> = {
|
||||
saas: {
|
||||
categories: ['IDENTIFICATION', 'CONTACT', 'PROFESSIONAL', 'BEHAVIORAL'],
|
||||
subjects: ['CUSTOMERS', 'EMPLOYEES'],
|
||||
hasSpecialCategories: false,
|
||||
processesMinors: false,
|
||||
dataVolume: 'HIGH',
|
||||
thirdCountryTransfers: true,
|
||||
thirdCountryList: ['USA'],
|
||||
},
|
||||
healthcare: {
|
||||
categories: ['IDENTIFICATION', 'CONTACT', 'HEALTH', 'BIOMETRIC'],
|
||||
subjects: ['PATIENTS', 'EMPLOYEES'],
|
||||
hasSpecialCategories: true,
|
||||
processesMinors: true,
|
||||
dataVolume: 'MEDIUM',
|
||||
thirdCountryTransfers: false,
|
||||
thirdCountryList: [],
|
||||
},
|
||||
enterprise: {
|
||||
categories: ['IDENTIFICATION', 'CONTACT', 'FINANCIAL', 'BEHAVIORAL'],
|
||||
subjects: ['CUSTOMERS', 'EMPLOYEES', 'PROSPECTS'],
|
||||
hasSpecialCategories: false,
|
||||
processesMinors: false,
|
||||
dataVolume: 'VERY_HIGH',
|
||||
thirdCountryTransfers: true,
|
||||
thirdCountryList: ['USA', 'UK', 'Schweiz'],
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO ARCHITECTURE PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_ARCHITECTURE_PROFILES: Record<string, ArchitectureProfile> = {
|
||||
saas: {
|
||||
hostingModel: 'PUBLIC_CLOUD',
|
||||
hostingLocation: 'EU',
|
||||
providers: [
|
||||
{ name: 'AWS', location: 'EU', certifications: ['ISO 27001', 'SOC 2', 'C5'] },
|
||||
{ name: 'Cloudflare', location: 'EU', certifications: ['ISO 27001'] },
|
||||
],
|
||||
multiTenancy: 'MULTI_TENANT',
|
||||
hasSubprocessors: true,
|
||||
subprocessorCount: 5,
|
||||
encryptionAtRest: true,
|
||||
encryptionInTransit: true,
|
||||
},
|
||||
healthcare: {
|
||||
hostingModel: 'PRIVATE_CLOUD',
|
||||
hostingLocation: 'DE',
|
||||
providers: [
|
||||
{ name: 'Telekom Cloud', location: 'DE', certifications: ['ISO 27001', 'C5', 'TISAX'] },
|
||||
],
|
||||
multiTenancy: 'SINGLE_TENANT',
|
||||
hasSubprocessors: true,
|
||||
subprocessorCount: 2,
|
||||
encryptionAtRest: true,
|
||||
encryptionInTransit: true,
|
||||
},
|
||||
enterprise: {
|
||||
hostingModel: 'HYBRID',
|
||||
hostingLocation: 'DE',
|
||||
providers: [
|
||||
{ name: 'Private Datacenter', location: 'DE', certifications: ['ISO 27001', 'SOC 2'] },
|
||||
{ name: 'Azure', location: 'EU', certifications: ['ISO 27001', 'C5', 'SOC 2'] },
|
||||
],
|
||||
multiTenancy: 'DEDICATED',
|
||||
hasSubprocessors: true,
|
||||
subprocessorCount: 10,
|
||||
encryptionAtRest: true,
|
||||
encryptionInTransit: true,
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO SECURITY PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_SECURITY_PROFILES: Record<string, SecurityProfile> = {
|
||||
saas: {
|
||||
authMethods: [
|
||||
{ type: 'PASSWORD', provider: null },
|
||||
{ type: 'MFA', provider: 'Auth0' },
|
||||
{ type: 'SSO', provider: 'Auth0' },
|
||||
],
|
||||
hasMFA: true,
|
||||
hasSSO: true,
|
||||
hasIAM: true,
|
||||
hasPAM: false,
|
||||
hasEncryptionAtRest: true,
|
||||
hasEncryptionInTransit: true,
|
||||
hasLogging: true,
|
||||
logRetentionDays: 90,
|
||||
hasBackup: true,
|
||||
backupFrequency: 'DAILY',
|
||||
backupRetentionDays: 30,
|
||||
hasDRPlan: true,
|
||||
rtoHours: 4,
|
||||
rpoHours: 1,
|
||||
hasVulnerabilityManagement: true,
|
||||
hasPenetrationTests: true,
|
||||
hasSecurityTraining: true,
|
||||
},
|
||||
healthcare: {
|
||||
authMethods: [
|
||||
{ type: 'PASSWORD', provider: null },
|
||||
{ type: 'MFA', provider: 'Microsoft Authenticator' },
|
||||
{ type: 'CERTIFICATE', provider: 'Internal PKI' },
|
||||
],
|
||||
hasMFA: true,
|
||||
hasSSO: false,
|
||||
hasIAM: true,
|
||||
hasPAM: true,
|
||||
hasEncryptionAtRest: true,
|
||||
hasEncryptionInTransit: true,
|
||||
hasLogging: true,
|
||||
logRetentionDays: 365,
|
||||
hasBackup: true,
|
||||
backupFrequency: 'HOURLY',
|
||||
backupRetentionDays: 90,
|
||||
hasDRPlan: true,
|
||||
rtoHours: 2,
|
||||
rpoHours: 0.5,
|
||||
hasVulnerabilityManagement: true,
|
||||
hasPenetrationTests: true,
|
||||
hasSecurityTraining: true,
|
||||
},
|
||||
enterprise: {
|
||||
authMethods: [
|
||||
{ type: 'PASSWORD', provider: null },
|
||||
{ type: 'MFA', provider: 'Okta' },
|
||||
{ type: 'SSO', provider: 'Okta' },
|
||||
{ type: 'BIOMETRIC', provider: 'Windows Hello' },
|
||||
],
|
||||
hasMFA: true,
|
||||
hasSSO: true,
|
||||
hasIAM: true,
|
||||
hasPAM: true,
|
||||
hasEncryptionAtRest: true,
|
||||
hasEncryptionInTransit: true,
|
||||
hasLogging: true,
|
||||
logRetentionDays: 730,
|
||||
hasBackup: true,
|
||||
backupFrequency: 'HOURLY',
|
||||
backupRetentionDays: 365,
|
||||
hasDRPlan: true,
|
||||
rtoHours: 1,
|
||||
rpoHours: 0.25,
|
||||
hasVulnerabilityManagement: true,
|
||||
hasPenetrationTests: true,
|
||||
hasSecurityTraining: true,
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO RISK PROFILES
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_RISK_PROFILES: Record<string, RiskProfile> = {
|
||||
saas: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 3,
|
||||
integrity: 3,
|
||||
availability: 4,
|
||||
justification: 'Als SaaS-Anbieter ist die Verfügbarkeit kritisch für unsere Kunden. Vertraulichkeit und Integrität sind wichtig aufgrund der verarbeiteten Geschäftsdaten.',
|
||||
},
|
||||
protectionLevel: 'HIGH',
|
||||
specialRisks: ['Cloud-Abhängigkeit', 'Multi-Mandanten-Umgebung'],
|
||||
regulatoryRequirements: ['DSGVO', 'Kundenvorgaben'],
|
||||
hasHighRiskProcessing: false,
|
||||
dsfaRequired: false,
|
||||
},
|
||||
healthcare: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 5,
|
||||
integrity: 5,
|
||||
availability: 4,
|
||||
justification: 'Gesundheitsdaten erfordern höchsten Schutz. Fehlerhafte Daten können Patientensicherheit gefährden.',
|
||||
},
|
||||
protectionLevel: 'VERY_HIGH',
|
||||
specialRisks: ['Gesundheitsdaten', 'Minderjährige', 'Telemedizin'],
|
||||
regulatoryRequirements: ['DSGVO', 'SGB', 'MDR'],
|
||||
hasHighRiskProcessing: true,
|
||||
dsfaRequired: true,
|
||||
},
|
||||
enterprise: {
|
||||
ciaAssessment: {
|
||||
confidentiality: 4,
|
||||
integrity: 5,
|
||||
availability: 5,
|
||||
justification: 'Finanzdienstleistungen erfordern höchste Integrität und Verfügbarkeit. Vertraulichkeit ist kritisch für Kundendaten und Transaktionen.',
|
||||
},
|
||||
protectionLevel: 'VERY_HIGH',
|
||||
specialRisks: ['Finanztransaktionen', 'Regulatorische Auflagen', 'Cyber-Risiken'],
|
||||
regulatoryRequirements: ['DSGVO', 'MaRisk', 'BAIT', 'PSD2'],
|
||||
hasHighRiskProcessing: true,
|
||||
dsfaRequired: true,
|
||||
},
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEMO EVIDENCE DOCUMENTS
|
||||
// =============================================================================
|
||||
|
||||
export const DEMO_EVIDENCE_DOCUMENTS: EvidenceDocument[] = [
|
||||
{
|
||||
id: 'demo-evidence-1',
|
||||
filename: 'iso27001-certificate.pdf',
|
||||
originalName: 'ISO 27001 Zertifikat.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 245678,
|
||||
uploadedAt: new Date('2025-01-15'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'CERTIFICATE',
|
||||
detectedType: 'CERTIFICATE',
|
||||
hash: 'sha256:abc123def456',
|
||||
validFrom: new Date('2024-06-01'),
|
||||
validUntil: new Date('2027-05-31'),
|
||||
linkedControlIds: ['TOM-RV-04', 'TOM-AZ-01'],
|
||||
aiAnalysis: {
|
||||
summary: 'ISO 27001:2022 Zertifikat bestätigt die Implementierung eines Informationssicherheits-Managementsystems.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'Zertifiziert nach ISO/IEC 27001:2022',
|
||||
type: 'certification',
|
||||
relatedControlId: 'TOM-RV-04',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-RV-04', 'TOM-AZ-01', 'TOM-RV-01'],
|
||||
gaps: [],
|
||||
confidence: 0.95,
|
||||
analyzedAt: new Date('2025-01-15'),
|
||||
},
|
||||
status: 'VERIFIED',
|
||||
},
|
||||
{
|
||||
id: 'demo-evidence-2',
|
||||
filename: 'passwort-richtlinie.pdf',
|
||||
originalName: 'Passwortrichtlinie v2.1.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 128456,
|
||||
uploadedAt: new Date('2025-01-10'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'POLICY',
|
||||
detectedType: 'POLICY',
|
||||
hash: 'sha256:xyz789abc012',
|
||||
validFrom: new Date('2024-09-01'),
|
||||
validUntil: null,
|
||||
linkedControlIds: ['TOM-ADM-02'],
|
||||
aiAnalysis: {
|
||||
summary: 'Interne Passwortrichtlinie definiert Anforderungen an Passwortlänge, Komplexität und Wechselintervalle.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'Mindestlänge 12 Zeichen, Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen erforderlich',
|
||||
type: 'password-policy',
|
||||
relatedControlId: 'TOM-ADM-02',
|
||||
},
|
||||
{
|
||||
id: 'clause-2',
|
||||
text: 'Passwörter müssen alle 90 Tage geändert werden',
|
||||
type: 'password-policy',
|
||||
relatedControlId: 'TOM-ADM-02',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-ADM-02'],
|
||||
gaps: ['Keine Regelung zur Passwort-Historie gefunden'],
|
||||
confidence: 0.85,
|
||||
analyzedAt: new Date('2025-01-10'),
|
||||
},
|
||||
status: 'ANALYZED',
|
||||
},
|
||||
{
|
||||
id: 'demo-evidence-3',
|
||||
filename: 'aws-avv.pdf',
|
||||
originalName: 'AWS Data Processing Addendum.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 456789,
|
||||
uploadedAt: new Date('2025-01-05'),
|
||||
uploadedBy: 'admin@company.de',
|
||||
documentType: 'AVV',
|
||||
detectedType: 'DPA',
|
||||
hash: 'sha256:qwe123rty456',
|
||||
validFrom: new Date('2024-01-01'),
|
||||
validUntil: null,
|
||||
linkedControlIds: ['TOM-OR-01', 'TOM-OR-02'],
|
||||
aiAnalysis: {
|
||||
summary: 'AWS Data Processing Addendum regelt die Auftragsverarbeitung durch AWS als Unterauftragsverarbeiter.',
|
||||
extractedClauses: [
|
||||
{
|
||||
id: 'clause-1',
|
||||
text: 'AWS verpflichtet sich zur Einhaltung der DSGVO-Anforderungen',
|
||||
type: 'data-processing',
|
||||
relatedControlId: 'TOM-OR-01',
|
||||
},
|
||||
{
|
||||
id: 'clause-2',
|
||||
text: 'Jährliche SOC 2 und ISO 27001 Audits werden durchgeführt',
|
||||
type: 'audit',
|
||||
relatedControlId: 'TOM-OR-02',
|
||||
},
|
||||
],
|
||||
applicableControls: ['TOM-OR-01', 'TOM-OR-02', 'TOM-OR-04'],
|
||||
gaps: [],
|
||||
confidence: 0.9,
|
||||
analyzedAt: new Date('2025-01-05'),
|
||||
},
|
||||
status: 'VERIFIED',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// DEMO STATE GENERATOR
|
||||
// =============================================================================
|
||||
|
||||
export type DemoScenario = 'saas' | 'healthcare' | 'enterprise'
|
||||
|
||||
/**
|
||||
* Generate a complete demo state for a given scenario
|
||||
*/
|
||||
export function generateDemoState(
|
||||
tenantId: string,
|
||||
scenario: DemoScenario = 'saas'
|
||||
): TOMGeneratorState {
|
||||
const companyProfile = DEMO_COMPANY_PROFILES[scenario]
|
||||
const dataProfile = DEMO_DATA_PROFILES[scenario]
|
||||
const architectureProfile = DEMO_ARCHITECTURE_PROFILES[scenario]
|
||||
const securityProfile = DEMO_SECURITY_PROFILES[scenario]
|
||||
const riskProfile = DEMO_RISK_PROFILES[scenario]
|
||||
|
||||
// Generate derived TOMs using the rules engine
|
||||
const rulesEngine = getTOMRulesEngine()
|
||||
const derivedTOMs = rulesEngine.deriveAllTOMs({
|
||||
companyProfile,
|
||||
dataProfile,
|
||||
architectureProfile,
|
||||
securityProfile,
|
||||
riskProfile,
|
||||
})
|
||||
|
||||
// Set some TOMs as implemented for demo
|
||||
const implementedTOMs = derivedTOMs.map((tom, index) => ({
|
||||
...tom,
|
||||
implementationStatus:
|
||||
index % 3 === 0
|
||||
? 'IMPLEMENTED' as const
|
||||
: index % 3 === 1
|
||||
? 'PARTIAL' as const
|
||||
: 'NOT_IMPLEMENTED' as const,
|
||||
responsiblePerson:
|
||||
index % 2 === 0 ? 'IT Security Team' : 'Datenschutzbeauftragter',
|
||||
implementationDate:
|
||||
index % 3 === 0 ? new Date('2024-06-15') : null,
|
||||
}))
|
||||
|
||||
// Generate gap analysis
|
||||
const gapAnalysis = rulesEngine.performGapAnalysis(
|
||||
implementedTOMs,
|
||||
DEMO_EVIDENCE_DOCUMENTS
|
||||
)
|
||||
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: `demo-state-${scenario}-${Date.now()}`,
|
||||
tenantId,
|
||||
companyProfile,
|
||||
dataProfile,
|
||||
architectureProfile,
|
||||
securityProfile,
|
||||
riskProfile,
|
||||
currentStep: 'review-export',
|
||||
steps: TOM_GENERATOR_STEPS.map((step) => ({
|
||||
id: step.id,
|
||||
completed: true,
|
||||
data: null,
|
||||
validatedAt: now,
|
||||
})),
|
||||
documents: DEMO_EVIDENCE_DOCUMENTS,
|
||||
derivedTOMs: implementedTOMs,
|
||||
gapAnalysis,
|
||||
exports: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an empty starter state
|
||||
*/
|
||||
export function generateEmptyState(tenantId: string): TOMGeneratorState {
|
||||
const now = new Date()
|
||||
|
||||
return {
|
||||
id: `new-state-${Date.now()}`,
|
||||
tenantId,
|
||||
companyProfile: null,
|
||||
dataProfile: null,
|
||||
architectureProfile: null,
|
||||
securityProfile: null,
|
||||
riskProfile: null,
|
||||
currentStep: 'scope-roles',
|
||||
steps: TOM_GENERATOR_STEPS.map((step) => ({
|
||||
id: step.id,
|
||||
completed: false,
|
||||
data: null,
|
||||
validatedAt: null,
|
||||
})),
|
||||
documents: [],
|
||||
derivedTOMs: [],
|
||||
gapAnalysis: null,
|
||||
exports: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate partial state (first 3 steps completed)
|
||||
*/
|
||||
export function generatePartialState(
|
||||
tenantId: string,
|
||||
scenario: DemoScenario = 'saas'
|
||||
): TOMGeneratorState {
|
||||
const state = generateEmptyState(tenantId)
|
||||
const now = new Date()
|
||||
|
||||
state.companyProfile = DEMO_COMPANY_PROFILES[scenario]
|
||||
state.dataProfile = DEMO_DATA_PROFILES[scenario]
|
||||
state.architectureProfile = DEMO_ARCHITECTURE_PROFILES[scenario]
|
||||
state.currentStep = 'security-profile'
|
||||
|
||||
state.steps = state.steps.map((step, index) => ({
|
||||
...step,
|
||||
completed: index < 3,
|
||||
validatedAt: index < 3 ? now : null,
|
||||
}))
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
DEMO_COMPANY_PROFILES as demoCompanyProfiles,
|
||||
DEMO_DATA_PROFILES as demoDataProfiles,
|
||||
DEMO_ARCHITECTURE_PROFILES as demoArchitectureProfiles,
|
||||
DEMO_SECURITY_PROFILES as demoSecurityProfiles,
|
||||
DEMO_RISK_PROFILES as demoRiskProfiles,
|
||||
DEMO_EVIDENCE_DOCUMENTS as demoEvidenceDocuments,
|
||||
}
|
||||
67
admin-v2/lib/sdk/tom-generator/evidence-store.ts
Normal file
67
admin-v2/lib/sdk/tom-generator/evidence-store.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Evidence Store
|
||||
// Shared in-memory storage for evidence documents
|
||||
// =============================================================================
|
||||
|
||||
import { EvidenceDocument, DocumentType } from './types'
|
||||
|
||||
interface StoredEvidence {
|
||||
tenantId: string
|
||||
documents: EvidenceDocument[]
|
||||
}
|
||||
|
||||
class InMemoryEvidenceStore {
|
||||
private store: Map<string, StoredEvidence> = new Map()
|
||||
|
||||
async getAll(tenantId: string): Promise<EvidenceDocument[]> {
|
||||
const stored = this.store.get(tenantId)
|
||||
return stored?.documents || []
|
||||
}
|
||||
|
||||
async getById(tenantId: string, documentId: string): Promise<EvidenceDocument | null> {
|
||||
const stored = this.store.get(tenantId)
|
||||
return stored?.documents.find((d) => d.id === documentId) || null
|
||||
}
|
||||
|
||||
async add(tenantId: string, document: EvidenceDocument): Promise<EvidenceDocument> {
|
||||
const stored = this.store.get(tenantId) || { tenantId, documents: [] }
|
||||
stored.documents.push(document)
|
||||
this.store.set(tenantId, stored)
|
||||
return document
|
||||
}
|
||||
|
||||
async update(tenantId: string, documentId: string, updates: Partial<EvidenceDocument>): Promise<EvidenceDocument | null> {
|
||||
const stored = this.store.get(tenantId)
|
||||
if (!stored) return null
|
||||
|
||||
const index = stored.documents.findIndex((d) => d.id === documentId)
|
||||
if (index === -1) return null
|
||||
|
||||
stored.documents[index] = { ...stored.documents[index], ...updates }
|
||||
this.store.set(tenantId, stored)
|
||||
return stored.documents[index]
|
||||
}
|
||||
|
||||
async delete(tenantId: string, documentId: string): Promise<boolean> {
|
||||
const stored = this.store.get(tenantId)
|
||||
if (!stored) return false
|
||||
|
||||
const initialLength = stored.documents.length
|
||||
stored.documents = stored.documents.filter((d) => d.id !== documentId)
|
||||
this.store.set(tenantId, stored)
|
||||
return stored.documents.length < initialLength
|
||||
}
|
||||
|
||||
async getByType(tenantId: string, type: DocumentType): Promise<EvidenceDocument[]> {
|
||||
const stored = this.store.get(tenantId)
|
||||
return stored?.documents.filter((d) => d.documentType === type) || []
|
||||
}
|
||||
|
||||
async getByStatus(tenantId: string, status: string): Promise<EvidenceDocument[]> {
|
||||
const stored = this.store.get(tenantId)
|
||||
return stored?.documents.filter((d) => d.status === status) || []
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance for the application
|
||||
export const evidenceStore = new InMemoryEvidenceStore()
|
||||
525
admin-v2/lib/sdk/tom-generator/export/docx.ts
Normal file
525
admin-v2/lib/sdk/tom-generator/export/docx.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
// =============================================================================
|
||||
// TOM Generator DOCX Export
|
||||
// Export TOMs to Microsoft Word format
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
TOMGeneratorState,
|
||||
DerivedTOM,
|
||||
ControlCategory,
|
||||
CONTROL_CATEGORIES,
|
||||
} from '../types'
|
||||
import { getControlById, getCategoryMetadata } from '../controls/loader'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface DOCXExportOptions {
|
||||
language: 'de' | 'en'
|
||||
includeNotApplicable: boolean
|
||||
includeEvidence: boolean
|
||||
includeGapAnalysis: boolean
|
||||
companyLogo?: string
|
||||
primaryColor?: string
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: DOCXExportOptions = {
|
||||
language: 'de',
|
||||
includeNotApplicable: false,
|
||||
includeEvidence: true,
|
||||
includeGapAnalysis: true,
|
||||
primaryColor: '#1a56db',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX CONTENT GENERATION
|
||||
// =============================================================================
|
||||
|
||||
export interface DocxParagraph {
|
||||
type: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'bullet'
|
||||
content: string
|
||||
style?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface DocxTableRow {
|
||||
cells: string[]
|
||||
isHeader?: boolean
|
||||
}
|
||||
|
||||
export interface DocxTable {
|
||||
type: 'table'
|
||||
headers: string[]
|
||||
rows: DocxTableRow[]
|
||||
}
|
||||
|
||||
export type DocxElement = DocxParagraph | DocxTable
|
||||
|
||||
/**
|
||||
* Generate DOCX content structure for TOMs
|
||||
*/
|
||||
export function generateDOCXContent(
|
||||
state: TOMGeneratorState,
|
||||
options: Partial<DOCXExportOptions> = {}
|
||||
): DocxElement[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const elements: DocxElement[] = []
|
||||
|
||||
// Title page
|
||||
elements.push({
|
||||
type: 'heading1',
|
||||
content: opts.language === 'de'
|
||||
? 'Technische und Organisatorische Maßnahmen (TOMs)'
|
||||
: 'Technical and Organizational Measures (TOMs)',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `gemäß Art. 32 DSGVO`
|
||||
: 'according to Art. 32 GDPR',
|
||||
})
|
||||
|
||||
// Company info
|
||||
if (state.companyProfile) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: opts.language === 'de' ? 'Unternehmen' : 'Company',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `${state.companyProfile.name}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Branche: ${state.companyProfile.industry}`
|
||||
: `Industry: ${state.companyProfile.industry}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Rolle: ${formatRole(state.companyProfile.role, opts.language)}`
|
||||
: `Role: ${formatRole(state.companyProfile.role, opts.language)}`,
|
||||
})
|
||||
|
||||
if (state.companyProfile.dpoPerson) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Datenschutzbeauftragter: ${state.companyProfile.dpoPerson}`
|
||||
: `Data Protection Officer: ${state.companyProfile.dpoPerson}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Document metadata
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Stand: ${new Date().toLocaleDateString('de-DE')}`
|
||||
: `Date: ${new Date().toLocaleDateString('en-US')}`,
|
||||
})
|
||||
|
||||
// Protection level summary
|
||||
if (state.riskProfile) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: opts.language === 'de' ? 'Schutzbedarf' : 'Protection Level',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Ermittelter Schutzbedarf: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`
|
||||
: `Determined Protection Level: ${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `CIA-Bewertung: Vertraulichkeit ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrität ${state.riskProfile.ciaAssessment.integrity}/5, Verfügbarkeit ${state.riskProfile.ciaAssessment.availability}/5`
|
||||
: `CIA Assessment: Confidentiality ${state.riskProfile.ciaAssessment.confidentiality}/5, Integrity ${state.riskProfile.ciaAssessment.integrity}/5, Availability ${state.riskProfile.ciaAssessment.availability}/5`,
|
||||
})
|
||||
|
||||
if (state.riskProfile.dsfaRequired) {
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? '⚠️ Eine Datenschutz-Folgenabschätzung (DSFA) ist erforderlich.'
|
||||
: '⚠️ A Data Protection Impact Assessment (DPIA) is required.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TOMs by category
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: opts.language === 'de'
|
||||
? 'Übersicht der Maßnahmen'
|
||||
: 'Measures Overview',
|
||||
})
|
||||
|
||||
// Group TOMs by category
|
||||
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs, opts.includeNotApplicable)
|
||||
|
||||
for (const category of CONTROL_CATEGORIES) {
|
||||
const categoryTOMs = tomsByCategory.get(category.id)
|
||||
if (!categoryTOMs || categoryTOMs.length === 0) continue
|
||||
|
||||
const categoryName = category.name[opts.language]
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: `${categoryName} (${category.gdprReference})`,
|
||||
})
|
||||
|
||||
// Create table for this category
|
||||
const tableHeaders = opts.language === 'de'
|
||||
? ['ID', 'Maßnahme', 'Typ', 'Status', 'Anwendbarkeit']
|
||||
: ['ID', 'Measure', 'Type', 'Status', 'Applicability']
|
||||
|
||||
const tableRows: DocxTableRow[] = categoryTOMs.map((tom) => ({
|
||||
cells: [
|
||||
tom.controlId,
|
||||
tom.name,
|
||||
formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language),
|
||||
formatImplementationStatus(tom.implementationStatus, opts.language),
|
||||
formatApplicability(tom.applicability, opts.language),
|
||||
],
|
||||
}))
|
||||
|
||||
elements.push({
|
||||
type: 'table',
|
||||
headers: tableHeaders,
|
||||
rows: tableRows,
|
||||
})
|
||||
|
||||
// Add detailed descriptions
|
||||
for (const tom of categoryTOMs) {
|
||||
if (tom.applicability === 'NOT_APPLICABLE' && !opts.includeNotApplicable) {
|
||||
continue
|
||||
}
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: `**${tom.controlId}: ${tom.name}**`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: tom.aiGeneratedDescription || tom.description,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: opts.language === 'de'
|
||||
? `Anwendbarkeit: ${formatApplicability(tom.applicability, opts.language)}`
|
||||
: `Applicability: ${formatApplicability(tom.applicability, opts.language)}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: opts.language === 'de'
|
||||
? `Begründung: ${tom.applicabilityReason}`
|
||||
: `Reason: ${tom.applicabilityReason}`,
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: opts.language === 'de'
|
||||
? `Umsetzungsstatus: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`
|
||||
: `Implementation Status: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`,
|
||||
})
|
||||
|
||||
if (tom.responsiblePerson) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: opts.language === 'de'
|
||||
? `Verantwortlich: ${tom.responsiblePerson}`
|
||||
: `Responsible: ${tom.responsiblePerson}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (opts.includeEvidence && tom.linkedEvidence.length > 0) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: opts.language === 'de'
|
||||
? `Nachweise: ${tom.linkedEvidence.length} Dokument(e) verknüpft`
|
||||
: `Evidence: ${tom.linkedEvidence.length} document(s) linked`,
|
||||
})
|
||||
}
|
||||
|
||||
if (tom.evidenceGaps.length > 0) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: opts.language === 'de'
|
||||
? `Fehlende Nachweise: ${tom.evidenceGaps.join(', ')}`
|
||||
: `Missing Evidence: ${tom.evidenceGaps.join(', ')}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gap Analysis
|
||||
if (opts.includeGapAnalysis && state.gapAnalysis) {
|
||||
elements.push({
|
||||
type: 'heading2',
|
||||
content: opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis',
|
||||
})
|
||||
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Gesamtscore: ${state.gapAnalysis.overallScore}%`
|
||||
: `Overall Score: ${state.gapAnalysis.overallScore}%`,
|
||||
})
|
||||
|
||||
if (state.gapAnalysis.missingControls.length > 0) {
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: opts.language === 'de'
|
||||
? 'Fehlende Maßnahmen'
|
||||
: 'Missing Measures',
|
||||
})
|
||||
|
||||
for (const missing of state.gapAnalysis.missingControls) {
|
||||
const control = getControlById(missing.controlId)
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: `${missing.controlId}: ${control?.name[opts.language] || 'Unknown'} (${missing.priority})`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (state.gapAnalysis.recommendations.length > 0) {
|
||||
elements.push({
|
||||
type: 'heading3',
|
||||
content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations',
|
||||
})
|
||||
|
||||
for (const rec of state.gapAnalysis.recommendations) {
|
||||
elements.push({
|
||||
type: 'bullet',
|
||||
content: rec,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
elements.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Dieses Dokument wurde automatisch generiert mit dem TOM Generator am ${new Date().toLocaleDateString('de-DE')}.`
|
||||
: `This document was automatically generated with the TOM Generator on ${new Date().toLocaleDateString('en-US')}.`,
|
||||
})
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function groupTOMsByCategory(
|
||||
toms: DerivedTOM[],
|
||||
includeNotApplicable: boolean
|
||||
): Map<ControlCategory, DerivedTOM[]> {
|
||||
const grouped = new Map<ControlCategory, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
if (!includeNotApplicable && tom.applicability === 'NOT_APPLICABLE') {
|
||||
continue
|
||||
}
|
||||
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
const existing = grouped.get(category) || []
|
||||
existing.push(tom)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
function formatRole(role: string, language: 'de' | 'en'): string {
|
||||
const roles: Record<string, Record<'de' | 'en', string>> = {
|
||||
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
|
||||
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
|
||||
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
|
||||
}
|
||||
return roles[role]?.[language] || role
|
||||
}
|
||||
|
||||
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
|
||||
const levels: Record<string, Record<'de' | 'en', string>> = {
|
||||
NORMAL: { de: 'Normal', en: 'Normal' },
|
||||
HIGH: { de: 'Hoch', en: 'High' },
|
||||
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return levels[level]?.[language] || level
|
||||
}
|
||||
|
||||
function formatType(type: string, language: 'de' | 'en'): string {
|
||||
const types: Record<string, Record<'de' | 'en', string>> = {
|
||||
TECHNICAL: { de: 'Technisch', en: 'Technical' },
|
||||
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
|
||||
}
|
||||
return types[type]?.[language] || type
|
||||
}
|
||||
|
||||
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise umgesetzt', en: 'Partially Implemented' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[language] || status
|
||||
}
|
||||
|
||||
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
|
||||
const apps: Record<string, Record<'de' | 'en', string>> = {
|
||||
REQUIRED: { de: 'Erforderlich', en: 'Required' },
|
||||
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
|
||||
OPTIONAL: { de: 'Optional', en: 'Optional' },
|
||||
NOT_APPLICABLE: { de: 'Nicht anwendbar', en: 'Not Applicable' },
|
||||
}
|
||||
return apps[applicability]?.[language] || applicability
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DOCX BLOB GENERATION
|
||||
// Uses simple XML structure compatible with docx libraries
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a DOCX file as a Blob
|
||||
* Note: For production, use docx library (npm install docx)
|
||||
* This is a simplified version that generates XML-based content
|
||||
*/
|
||||
export async function generateDOCXBlob(
|
||||
state: TOMGeneratorState,
|
||||
options: Partial<DOCXExportOptions> = {}
|
||||
): Promise<Blob> {
|
||||
const content = generateDOCXContent(state, options)
|
||||
|
||||
// Generate simple HTML that can be converted to DOCX
|
||||
// In production, use the docx library for proper DOCX generation
|
||||
const html = generateHTMLFromContent(content, options)
|
||||
|
||||
// Return as a Word-compatible HTML blob
|
||||
// The proper way would be to use the docx library
|
||||
const blob = new Blob([html], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
})
|
||||
|
||||
return blob
|
||||
}
|
||||
|
||||
function generateHTMLFromContent(
|
||||
content: DocxElement[],
|
||||
options: Partial<DOCXExportOptions>
|
||||
): string {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
|
||||
let html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Calibri, Arial, sans-serif; font-size: 11pt; line-height: 1.5; }
|
||||
h1 { font-size: 24pt; color: ${opts.primaryColor}; border-bottom: 2px solid ${opts.primaryColor}; }
|
||||
h2 { font-size: 18pt; color: ${opts.primaryColor}; margin-top: 24pt; }
|
||||
h3 { font-size: 14pt; color: #333; margin-top: 18pt; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12pt 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8pt; text-align: left; }
|
||||
th { background-color: ${opts.primaryColor}; color: white; }
|
||||
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||
ul { margin: 6pt 0; }
|
||||
li { margin: 3pt 0; }
|
||||
.warning { color: #dc2626; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
`
|
||||
|
||||
for (const element of content) {
|
||||
if (element.type === 'table') {
|
||||
html += '<table>'
|
||||
html += '<tr>'
|
||||
for (const header of element.headers) {
|
||||
html += `<th>${escapeHtml(header)}</th>`
|
||||
}
|
||||
html += '</tr>'
|
||||
for (const row of element.rows) {
|
||||
html += '<tr>'
|
||||
for (const cell of row.cells) {
|
||||
html += `<td>${escapeHtml(cell)}</td>`
|
||||
}
|
||||
html += '</tr>'
|
||||
}
|
||||
html += '</table>'
|
||||
} else {
|
||||
const tag = getHtmlTag(element.type)
|
||||
const processedContent = processContent(element.content)
|
||||
html += `<${tag}>${processedContent}</${tag}>\n`
|
||||
}
|
||||
}
|
||||
|
||||
html += '</body></html>'
|
||||
return html
|
||||
}
|
||||
|
||||
function getHtmlTag(type: string): string {
|
||||
switch (type) {
|
||||
case 'heading1':
|
||||
return 'h1'
|
||||
case 'heading2':
|
||||
return 'h2'
|
||||
case 'heading3':
|
||||
return 'h3'
|
||||
case 'bullet':
|
||||
return 'li'
|
||||
default:
|
||||
return 'p'
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function processContent(content: string): string {
|
||||
// Convert markdown-style bold to HTML
|
||||
return escapeHtml(content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a filename for the DOCX export
|
||||
*/
|
||||
export function generateDOCXFilename(
|
||||
state: TOMGeneratorState,
|
||||
language: 'de' | 'en' = 'de'
|
||||
): string {
|
||||
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const prefix = language === 'de' ? 'TOMs' : 'TOMs'
|
||||
return `${prefix}-${companyName}-${date}.docx`
|
||||
}
|
||||
|
||||
// Types are exported at their definition site above
|
||||
517
admin-v2/lib/sdk/tom-generator/export/pdf.ts
Normal file
517
admin-v2/lib/sdk/tom-generator/export/pdf.ts
Normal file
@@ -0,0 +1,517 @@
|
||||
// =============================================================================
|
||||
// TOM Generator PDF Export
|
||||
// Export TOMs to PDF format
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
TOMGeneratorState,
|
||||
DerivedTOM,
|
||||
CONTROL_CATEGORIES,
|
||||
} from '../types'
|
||||
import { getControlById } from '../controls/loader'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface PDFExportOptions {
|
||||
language: 'de' | 'en'
|
||||
includeNotApplicable: boolean
|
||||
includeEvidence: boolean
|
||||
includeGapAnalysis: boolean
|
||||
companyLogo?: string
|
||||
primaryColor?: string
|
||||
pageSize?: 'A4' | 'LETTER'
|
||||
orientation?: 'portrait' | 'landscape'
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: PDFExportOptions = {
|
||||
language: 'de',
|
||||
includeNotApplicable: false,
|
||||
includeEvidence: true,
|
||||
includeGapAnalysis: true,
|
||||
primaryColor: '#1a56db',
|
||||
pageSize: 'A4',
|
||||
orientation: 'portrait',
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF CONTENT STRUCTURE
|
||||
// =============================================================================
|
||||
|
||||
export interface PDFSection {
|
||||
type: 'title' | 'heading' | 'subheading' | 'paragraph' | 'table' | 'list' | 'pagebreak'
|
||||
content?: string
|
||||
items?: string[]
|
||||
table?: {
|
||||
headers: string[]
|
||||
rows: string[][]
|
||||
}
|
||||
style?: {
|
||||
color?: string
|
||||
fontSize?: number
|
||||
bold?: boolean
|
||||
italic?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PDF content structure for TOMs
|
||||
*/
|
||||
export function generatePDFContent(
|
||||
state: TOMGeneratorState,
|
||||
options: Partial<PDFExportOptions> = {}
|
||||
): PDFSection[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const sections: PDFSection[] = []
|
||||
|
||||
// Title page
|
||||
sections.push({
|
||||
type: 'title',
|
||||
content: opts.language === 'de'
|
||||
? 'Technische und Organisatorische Maßnahmen (TOMs)'
|
||||
: 'Technical and Organizational Measures (TOMs)',
|
||||
style: { color: opts.primaryColor, fontSize: 24, bold: true, align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? 'gemäß Art. 32 DSGVO'
|
||||
: 'according to Art. 32 GDPR',
|
||||
style: { fontSize: 14, align: 'center' },
|
||||
})
|
||||
|
||||
// Company information
|
||||
if (state.companyProfile) {
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: state.companyProfile.name,
|
||||
style: { fontSize: 16, bold: true, align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: `${opts.language === 'de' ? 'Branche' : 'Industry'}: ${state.companyProfile.industry}`,
|
||||
style: { align: 'center' },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: `${opts.language === 'de' ? 'Stand' : 'Date'}: ${new Date().toLocaleDateString(opts.language === 'de' ? 'de-DE' : 'en-US')}`,
|
||||
style: { align: 'center' },
|
||||
})
|
||||
}
|
||||
|
||||
sections.push({ type: 'pagebreak' })
|
||||
|
||||
// Table of Contents
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: opts.language === 'de' ? 'Inhaltsverzeichnis' : 'Table of Contents',
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
const tocItems = [
|
||||
opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary',
|
||||
opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level',
|
||||
opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview',
|
||||
]
|
||||
|
||||
let sectionNum = 4
|
||||
for (const category of CONTROL_CATEGORIES) {
|
||||
const categoryTOMs = state.derivedTOMs.filter((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === category.id &&
|
||||
(opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
|
||||
})
|
||||
if (categoryTOMs.length > 0) {
|
||||
tocItems.push(`${sectionNum}. ${category.name[opts.language]}`)
|
||||
sectionNum++
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.includeGapAnalysis && state.gapAnalysis) {
|
||||
tocItems.push(`${sectionNum}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`)
|
||||
}
|
||||
|
||||
sections.push({
|
||||
type: 'list',
|
||||
items: tocItems,
|
||||
})
|
||||
|
||||
sections.push({ type: 'pagebreak' })
|
||||
|
||||
// Executive Summary
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: opts.language === 'de' ? '1. Zusammenfassung' : '1. Summary',
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
const totalTOMs = state.derivedTOMs.length
|
||||
const requiredTOMs = state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length
|
||||
const implementedTOMs = state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Dieses Dokument beschreibt die technischen und organisatorischen Maßnahmen (TOMs) gemäß Art. 32 DSGVO. Insgesamt wurden ${totalTOMs} Kontrollen bewertet, davon ${requiredTOMs} als erforderlich eingestuft. Aktuell sind ${implementedTOMs} Maßnahmen vollständig umgesetzt.`
|
||||
: `This document describes the technical and organizational measures (TOMs) according to Art. 32 GDPR. A total of ${totalTOMs} controls were evaluated, of which ${requiredTOMs} are classified as required. Currently, ${implementedTOMs} measures are fully implemented.`,
|
||||
})
|
||||
|
||||
// Summary statistics table
|
||||
sections.push({
|
||||
type: 'table',
|
||||
table: {
|
||||
headers: opts.language === 'de'
|
||||
? ['Kategorie', 'Anzahl', 'Erforderlich', 'Umgesetzt']
|
||||
: ['Category', 'Count', 'Required', 'Implemented'],
|
||||
rows: generateCategorySummary(state.derivedTOMs, opts),
|
||||
},
|
||||
})
|
||||
|
||||
// Protection Level
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: opts.language === 'de' ? '2. Schutzbedarf' : '2. Protection Level',
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
if (state.riskProfile) {
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Der ermittelte Schutzbedarf beträgt: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`
|
||||
: `The determined protection level is: **${formatProtectionLevel(state.riskProfile.protectionLevel, opts.language)}**`,
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'table',
|
||||
table: {
|
||||
headers: opts.language === 'de'
|
||||
? ['Schutzziel', 'Bewertung (1-5)', 'Bedeutung']
|
||||
: ['Protection Goal', 'Rating (1-5)', 'Meaning'],
|
||||
rows: [
|
||||
[
|
||||
opts.language === 'de' ? 'Vertraulichkeit' : 'Confidentiality',
|
||||
String(state.riskProfile.ciaAssessment.confidentiality),
|
||||
getCIAMeaning(state.riskProfile.ciaAssessment.confidentiality, opts.language),
|
||||
],
|
||||
[
|
||||
opts.language === 'de' ? 'Integrität' : 'Integrity',
|
||||
String(state.riskProfile.ciaAssessment.integrity),
|
||||
getCIAMeaning(state.riskProfile.ciaAssessment.integrity, opts.language),
|
||||
],
|
||||
[
|
||||
opts.language === 'de' ? 'Verfügbarkeit' : 'Availability',
|
||||
String(state.riskProfile.ciaAssessment.availability),
|
||||
getCIAMeaning(state.riskProfile.ciaAssessment.availability, opts.language),
|
||||
],
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
if (state.riskProfile.dsfaRequired) {
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? '⚠️ HINWEIS: Aufgrund der Verarbeitung ist eine Datenschutz-Folgenabschätzung (DSFA) nach Art. 35 DSGVO erforderlich.'
|
||||
: '⚠️ NOTE: Due to the processing, a Data Protection Impact Assessment (DPIA) according to Art. 35 GDPR is required.',
|
||||
style: { bold: true, color: '#dc2626' },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Measures Overview
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: opts.language === 'de' ? '3. Maßnahmenübersicht' : '3. Measures Overview',
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'table',
|
||||
table: {
|
||||
headers: opts.language === 'de'
|
||||
? ['ID', 'Maßnahme', 'Anwendbarkeit', 'Status']
|
||||
: ['ID', 'Measure', 'Applicability', 'Status'],
|
||||
rows: state.derivedTOMs
|
||||
.filter((tom) => opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
|
||||
.map((tom) => [
|
||||
tom.controlId,
|
||||
tom.name,
|
||||
formatApplicability(tom.applicability, opts.language),
|
||||
formatImplementationStatus(tom.implementationStatus, opts.language),
|
||||
]),
|
||||
},
|
||||
})
|
||||
|
||||
// Detailed sections by category
|
||||
let currentSection = 4
|
||||
for (const category of CONTROL_CATEGORIES) {
|
||||
const categoryTOMs = state.derivedTOMs.filter((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === category.id &&
|
||||
(opts.includeNotApplicable || tom.applicability !== 'NOT_APPLICABLE')
|
||||
})
|
||||
|
||||
if (categoryTOMs.length === 0) continue
|
||||
|
||||
sections.push({ type: 'pagebreak' })
|
||||
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: `${currentSection}. ${category.name[opts.language]}`,
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: `${opts.language === 'de' ? 'Rechtsgrundlage' : 'Legal Basis'}: ${category.gdprReference}`,
|
||||
style: { italic: true },
|
||||
})
|
||||
|
||||
for (const tom of categoryTOMs) {
|
||||
sections.push({
|
||||
type: 'subheading',
|
||||
content: `${tom.controlId}: ${tom.name}`,
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: tom.aiGeneratedDescription || tom.description,
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'list',
|
||||
items: [
|
||||
`${opts.language === 'de' ? 'Typ' : 'Type'}: ${formatType(getControlById(tom.controlId)?.type || 'TECHNICAL', opts.language)}`,
|
||||
`${opts.language === 'de' ? 'Anwendbarkeit' : 'Applicability'}: ${formatApplicability(tom.applicability, opts.language)}`,
|
||||
`${opts.language === 'de' ? 'Begründung' : 'Reason'}: ${tom.applicabilityReason}`,
|
||||
`${opts.language === 'de' ? 'Umsetzungsstatus' : 'Implementation Status'}: ${formatImplementationStatus(tom.implementationStatus, opts.language)}`,
|
||||
...(tom.responsiblePerson ? [`${opts.language === 'de' ? 'Verantwortlich' : 'Responsible'}: ${tom.responsiblePerson}`] : []),
|
||||
...(opts.includeEvidence && tom.linkedEvidence.length > 0
|
||||
? [`${opts.language === 'de' ? 'Verknüpfte Nachweise' : 'Linked Evidence'}: ${tom.linkedEvidence.length}`]
|
||||
: []),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
currentSection++
|
||||
}
|
||||
|
||||
// Gap Analysis
|
||||
if (opts.includeGapAnalysis && state.gapAnalysis) {
|
||||
sections.push({ type: 'pagebreak' })
|
||||
|
||||
sections.push({
|
||||
type: 'heading',
|
||||
content: `${currentSection}. ${opts.language === 'de' ? 'Lückenanalyse' : 'Gap Analysis'}`,
|
||||
style: { color: opts.primaryColor },
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Gesamtscore: ${state.gapAnalysis.overallScore}%`
|
||||
: `Overall Score: ${state.gapAnalysis.overallScore}%`,
|
||||
style: { fontSize: 16, bold: true },
|
||||
})
|
||||
|
||||
if (state.gapAnalysis.missingControls.length > 0) {
|
||||
sections.push({
|
||||
type: 'subheading',
|
||||
content: opts.language === 'de' ? 'Fehlende Maßnahmen' : 'Missing Measures',
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'table',
|
||||
table: {
|
||||
headers: opts.language === 'de'
|
||||
? ['ID', 'Maßnahme', 'Priorität']
|
||||
: ['ID', 'Measure', 'Priority'],
|
||||
rows: state.gapAnalysis.missingControls.map((mc) => {
|
||||
const control = getControlById(mc.controlId)
|
||||
return [
|
||||
mc.controlId,
|
||||
control?.name[opts.language] || 'Unknown',
|
||||
mc.priority,
|
||||
]
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (state.gapAnalysis.recommendations.length > 0) {
|
||||
sections.push({
|
||||
type: 'subheading',
|
||||
content: opts.language === 'de' ? 'Empfehlungen' : 'Recommendations',
|
||||
})
|
||||
|
||||
sections.push({
|
||||
type: 'list',
|
||||
items: state.gapAnalysis.recommendations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
sections.push({
|
||||
type: 'paragraph',
|
||||
content: opts.language === 'de'
|
||||
? `Generiert am ${new Date().toLocaleDateString('de-DE')} mit dem TOM Generator`
|
||||
: `Generated on ${new Date().toLocaleDateString('en-US')} with the TOM Generator`,
|
||||
style: { italic: true, align: 'center', fontSize: 10 },
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function generateCategorySummary(
|
||||
toms: DerivedTOM[],
|
||||
opts: PDFExportOptions
|
||||
): string[][] {
|
||||
const summary: string[][] = []
|
||||
|
||||
for (const category of CONTROL_CATEGORIES) {
|
||||
const categoryTOMs = toms.filter((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
return control?.category === category.id
|
||||
})
|
||||
|
||||
if (categoryTOMs.length === 0) continue
|
||||
|
||||
const required = categoryTOMs.filter((t) => t.applicability === 'REQUIRED').length
|
||||
const implemented = categoryTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
|
||||
|
||||
summary.push([
|
||||
category.name[opts.language],
|
||||
String(categoryTOMs.length),
|
||||
String(required),
|
||||
String(implemented),
|
||||
])
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
function formatProtectionLevel(level: string, language: 'de' | 'en'): string {
|
||||
const levels: Record<string, Record<'de' | 'en', string>> = {
|
||||
NORMAL: { de: 'Normal', en: 'Normal' },
|
||||
HIGH: { de: 'Hoch', en: 'High' },
|
||||
VERY_HIGH: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return levels[level]?.[language] || level
|
||||
}
|
||||
|
||||
function formatType(type: string, language: 'de' | 'en'): string {
|
||||
const types: Record<string, Record<'de' | 'en', string>> = {
|
||||
TECHNICAL: { de: 'Technisch', en: 'Technical' },
|
||||
ORGANIZATIONAL: { de: 'Organisatorisch', en: 'Organizational' },
|
||||
}
|
||||
return types[type]?.[language] || type
|
||||
}
|
||||
|
||||
function formatImplementationStatus(status: string, language: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise', en: 'Partial' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[language] || status
|
||||
}
|
||||
|
||||
function formatApplicability(applicability: string, language: 'de' | 'en'): string {
|
||||
const apps: Record<string, Record<'de' | 'en', string>> = {
|
||||
REQUIRED: { de: 'Erforderlich', en: 'Required' },
|
||||
RECOMMENDED: { de: 'Empfohlen', en: 'Recommended' },
|
||||
OPTIONAL: { de: 'Optional', en: 'Optional' },
|
||||
NOT_APPLICABLE: { de: 'N/A', en: 'N/A' },
|
||||
}
|
||||
return apps[applicability]?.[language] || applicability
|
||||
}
|
||||
|
||||
function getCIAMeaning(rating: number, language: 'de' | 'en'): string {
|
||||
const meanings: Record<number, Record<'de' | 'en', string>> = {
|
||||
1: { de: 'Sehr gering', en: 'Very Low' },
|
||||
2: { de: 'Gering', en: 'Low' },
|
||||
3: { de: 'Mittel', en: 'Medium' },
|
||||
4: { de: 'Hoch', en: 'High' },
|
||||
5: { de: 'Sehr hoch', en: 'Very High' },
|
||||
}
|
||||
return meanings[rating]?.[language] || String(rating)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PDF BLOB GENERATION
|
||||
// Note: For production, use jspdf or pdfmake library
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a PDF file as a Blob
|
||||
* This is a placeholder - in production, use jspdf or similar library
|
||||
*/
|
||||
export async function generatePDFBlob(
|
||||
state: TOMGeneratorState,
|
||||
options: Partial<PDFExportOptions> = {}
|
||||
): Promise<Blob> {
|
||||
const content = generatePDFContent(state, options)
|
||||
|
||||
// Convert to simple text-based content for now
|
||||
// In production, use jspdf library
|
||||
const textContent = content
|
||||
.map((section) => {
|
||||
switch (section.type) {
|
||||
case 'title':
|
||||
return `\n\n${'='.repeat(60)}\n${section.content}\n${'='.repeat(60)}\n`
|
||||
case 'heading':
|
||||
return `\n\n${section.content}\n${'-'.repeat(40)}\n`
|
||||
case 'subheading':
|
||||
return `\n${section.content}\n`
|
||||
case 'paragraph':
|
||||
return `${section.content}\n`
|
||||
case 'list':
|
||||
return section.items?.map((item) => ` • ${item}`).join('\n') + '\n'
|
||||
case 'table':
|
||||
if (section.table) {
|
||||
const headerLine = section.table.headers.join(' | ')
|
||||
const separator = '-'.repeat(headerLine.length)
|
||||
const rows = section.table.rows.map((row) => row.join(' | ')).join('\n')
|
||||
return `\n${headerLine}\n${separator}\n${rows}\n`
|
||||
}
|
||||
return ''
|
||||
case 'pagebreak':
|
||||
return '\n\n' + '='.repeat(60) + '\n\n'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
|
||||
return new Blob([textContent], { type: 'application/pdf' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a filename for the PDF export
|
||||
*/
|
||||
export function generatePDFFilename(
|
||||
state: TOMGeneratorState,
|
||||
language: 'de' | 'en' = 'de'
|
||||
): string {
|
||||
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const prefix = language === 'de' ? 'TOMs' : 'TOMs'
|
||||
return `${prefix}-${companyName}-${date}.pdf`
|
||||
}
|
||||
|
||||
// Types are exported at their definition site above
|
||||
544
admin-v2/lib/sdk/tom-generator/export/zip.ts
Normal file
544
admin-v2/lib/sdk/tom-generator/export/zip.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
// =============================================================================
|
||||
// TOM Generator ZIP Export
|
||||
// Export complete TOM package as ZIP archive
|
||||
// =============================================================================
|
||||
|
||||
import { TOMGeneratorState, DerivedTOM, EvidenceDocument } from '../types'
|
||||
import { generateDOCXContent, DOCXExportOptions } from './docx'
|
||||
import { generatePDFContent, PDFExportOptions } from './pdf'
|
||||
import { getControlById, getAllControls, getLibraryMetadata } from '../controls/loader'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ZIPExportOptions {
|
||||
language: 'de' | 'en'
|
||||
includeNotApplicable: boolean
|
||||
includeEvidence: boolean
|
||||
includeGapAnalysis: boolean
|
||||
includeControlLibrary: boolean
|
||||
includeRawData: boolean
|
||||
formats: Array<'json' | 'docx' | 'pdf'>
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: ZIPExportOptions = {
|
||||
language: 'de',
|
||||
includeNotApplicable: false,
|
||||
includeEvidence: true,
|
||||
includeGapAnalysis: true,
|
||||
includeControlLibrary: true,
|
||||
includeRawData: true,
|
||||
formats: ['json', 'docx'],
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ZIP CONTENT STRUCTURE
|
||||
// =============================================================================
|
||||
|
||||
export interface ZIPFileEntry {
|
||||
path: string
|
||||
content: string | Blob
|
||||
mimeType: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all files for the ZIP archive
|
||||
*/
|
||||
export function generateZIPFiles(
|
||||
state: TOMGeneratorState,
|
||||
options: Partial<ZIPExportOptions> = {}
|
||||
): ZIPFileEntry[] {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||
const files: ZIPFileEntry[] = []
|
||||
|
||||
// README
|
||||
files.push({
|
||||
path: 'README.md',
|
||||
content: generateReadme(state, opts),
|
||||
mimeType: 'text/markdown',
|
||||
})
|
||||
|
||||
// State JSON
|
||||
if (opts.includeRawData) {
|
||||
files.push({
|
||||
path: 'data/state.json',
|
||||
content: JSON.stringify(state, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// Profile data
|
||||
files.push({
|
||||
path: 'data/profiles/company-profile.json',
|
||||
content: JSON.stringify(state.companyProfile, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
files.push({
|
||||
path: 'data/profiles/data-profile.json',
|
||||
content: JSON.stringify(state.dataProfile, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
files.push({
|
||||
path: 'data/profiles/architecture-profile.json',
|
||||
content: JSON.stringify(state.architectureProfile, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
files.push({
|
||||
path: 'data/profiles/security-profile.json',
|
||||
content: JSON.stringify(state.securityProfile, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
files.push({
|
||||
path: 'data/profiles/risk-profile.json',
|
||||
content: JSON.stringify(state.riskProfile, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// Derived TOMs
|
||||
files.push({
|
||||
path: 'data/toms/derived-toms.json',
|
||||
content: JSON.stringify(state.derivedTOMs, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// TOMs by category
|
||||
const tomsByCategory = groupTOMsByCategory(state.derivedTOMs)
|
||||
for (const [category, toms] of tomsByCategory.entries()) {
|
||||
files.push({
|
||||
path: `data/toms/by-category/${category.toLowerCase()}.json`,
|
||||
content: JSON.stringify(toms, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// Required TOMs summary
|
||||
const requiredTOMs = state.derivedTOMs.filter(
|
||||
(tom) => tom.applicability === 'REQUIRED'
|
||||
)
|
||||
files.push({
|
||||
path: 'data/toms/required-toms.json',
|
||||
content: JSON.stringify(requiredTOMs, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// Implementation status summary
|
||||
const implementationSummary = generateImplementationSummary(state.derivedTOMs)
|
||||
files.push({
|
||||
path: 'data/toms/implementation-summary.json',
|
||||
content: JSON.stringify(implementationSummary, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// Evidence documents
|
||||
if (opts.includeEvidence && state.documents.length > 0) {
|
||||
files.push({
|
||||
path: 'data/evidence/documents.json',
|
||||
content: JSON.stringify(state.documents, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// Evidence by control
|
||||
const evidenceByControl = groupEvidenceByControl(state.documents)
|
||||
files.push({
|
||||
path: 'data/evidence/by-control.json',
|
||||
content: JSON.stringify(Object.fromEntries(evidenceByControl), null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// Gap Analysis
|
||||
if (opts.includeGapAnalysis && state.gapAnalysis) {
|
||||
files.push({
|
||||
path: 'data/gap-analysis/analysis.json',
|
||||
content: JSON.stringify(state.gapAnalysis, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// Missing controls details
|
||||
if (state.gapAnalysis.missingControls.length > 0) {
|
||||
files.push({
|
||||
path: 'data/gap-analysis/missing-controls.json',
|
||||
content: JSON.stringify(state.gapAnalysis.missingControls, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
if (state.gapAnalysis.recommendations.length > 0) {
|
||||
files.push({
|
||||
path: 'data/gap-analysis/recommendations.md',
|
||||
content: generateRecommendationsMarkdown(
|
||||
state.gapAnalysis.recommendations,
|
||||
opts.language
|
||||
),
|
||||
mimeType: 'text/markdown',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Control Library
|
||||
if (opts.includeControlLibrary) {
|
||||
const controls = getAllControls()
|
||||
const metadata = getLibraryMetadata()
|
||||
|
||||
files.push({
|
||||
path: 'reference/control-library/metadata.json',
|
||||
content: JSON.stringify(metadata, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
files.push({
|
||||
path: 'reference/control-library/all-controls.json',
|
||||
content: JSON.stringify(controls, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
|
||||
// Controls by category
|
||||
for (const category of new Set(controls.map((c) => c.category))) {
|
||||
const categoryControls = controls.filter((c) => c.category === category)
|
||||
files.push({
|
||||
path: `reference/control-library/by-category/${category.toLowerCase()}.json`,
|
||||
content: JSON.stringify(categoryControls, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Export history
|
||||
if (state.exports.length > 0) {
|
||||
files.push({
|
||||
path: 'data/exports/history.json',
|
||||
content: JSON.stringify(state.exports, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// DOCX content structure (if requested)
|
||||
if (opts.formats.includes('docx')) {
|
||||
const docxOptions: Partial<DOCXExportOptions> = {
|
||||
language: opts.language,
|
||||
includeNotApplicable: opts.includeNotApplicable,
|
||||
includeEvidence: opts.includeEvidence,
|
||||
includeGapAnalysis: opts.includeGapAnalysis,
|
||||
}
|
||||
const docxContent = generateDOCXContent(state, docxOptions)
|
||||
files.push({
|
||||
path: 'documents/tom-document-structure.json',
|
||||
content: JSON.stringify(docxContent, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// PDF content structure (if requested)
|
||||
if (opts.formats.includes('pdf')) {
|
||||
const pdfOptions: Partial<PDFExportOptions> = {
|
||||
language: opts.language,
|
||||
includeNotApplicable: opts.includeNotApplicable,
|
||||
includeEvidence: opts.includeEvidence,
|
||||
includeGapAnalysis: opts.includeGapAnalysis,
|
||||
}
|
||||
const pdfContent = generatePDFContent(state, pdfOptions)
|
||||
files.push({
|
||||
path: 'documents/tom-document-structure-pdf.json',
|
||||
content: JSON.stringify(pdfContent, null, 2),
|
||||
mimeType: 'application/json',
|
||||
})
|
||||
}
|
||||
|
||||
// Markdown summary
|
||||
files.push({
|
||||
path: 'documents/tom-summary.md',
|
||||
content: generateMarkdownSummary(state, opts),
|
||||
mimeType: 'text/markdown',
|
||||
})
|
||||
|
||||
// CSV export for spreadsheet import
|
||||
files.push({
|
||||
path: 'documents/toms.csv',
|
||||
content: generateCSV(state.derivedTOMs, opts),
|
||||
mimeType: 'text/csv',
|
||||
})
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function generateReadme(
|
||||
state: TOMGeneratorState,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const lang = opts.language
|
||||
|
||||
return `# TOM Export Package
|
||||
|
||||
${lang === 'de' ? 'Exportiert am' : 'Exported on'}: ${date}
|
||||
${lang === 'de' ? 'Unternehmen' : 'Company'}: ${state.companyProfile?.name || 'N/A'}
|
||||
|
||||
## ${lang === 'de' ? 'Inhalt' : 'Contents'}
|
||||
|
||||
### /data
|
||||
- **profiles/** - ${lang === 'de' ? 'Profilinformationen (Unternehmen, Daten, Architektur, Sicherheit, Risiko)' : 'Profile information (company, data, architecture, security, risk)'}
|
||||
- **toms/** - ${lang === 'de' ? 'Abgeleitete TOMs und Zusammenfassungen' : 'Derived TOMs and summaries'}
|
||||
- **evidence/** - ${lang === 'de' ? 'Nachweisdokumente und Zuordnungen' : 'Evidence documents and mappings'}
|
||||
- **gap-analysis/** - ${lang === 'de' ? 'Lückenanalyse und Empfehlungen' : 'Gap analysis and recommendations'}
|
||||
|
||||
### /reference
|
||||
- **control-library/** - ${lang === 'de' ? 'Kontrollbibliothek mit allen 60+ Kontrollen' : 'Control library with all 60+ controls'}
|
||||
|
||||
### /documents
|
||||
- **tom-summary.md** - ${lang === 'de' ? 'Zusammenfassung als Markdown' : 'Summary as Markdown'}
|
||||
- **toms.csv** - ${lang === 'de' ? 'CSV für Tabellenimport' : 'CSV for spreadsheet import'}
|
||||
|
||||
## ${lang === 'de' ? 'Statistiken' : 'Statistics'}
|
||||
|
||||
- ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'}: ${state.derivedTOMs.length}
|
||||
- ${lang === 'de' ? 'Erforderlich' : 'Required'}: ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length}
|
||||
- ${lang === 'de' ? 'Umgesetzt' : 'Implemented'}: ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length}
|
||||
- ${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}: ${state.riskProfile?.protectionLevel || 'N/A'}
|
||||
${state.gapAnalysis ? `- ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}: ${state.gapAnalysis.overallScore}%` : ''}
|
||||
|
||||
---
|
||||
|
||||
${lang === 'de' ? 'Generiert mit dem TOM Generator' : 'Generated with TOM Generator'}
|
||||
`
|
||||
}
|
||||
|
||||
function groupTOMsByCategory(
|
||||
toms: DerivedTOM[]
|
||||
): Map<string, DerivedTOM[]> {
|
||||
const grouped = new Map<string, DerivedTOM[]>()
|
||||
|
||||
for (const tom of toms) {
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const category = control.category
|
||||
const existing: DerivedTOM[] = grouped.get(category) || []
|
||||
existing.push(tom)
|
||||
grouped.set(category, existing)
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
function generateImplementationSummary(
|
||||
toms: Array<{ implementationStatus: string; applicability: string }>
|
||||
): Record<string, number> {
|
||||
return {
|
||||
total: toms.length,
|
||||
required: toms.filter((t) => t.applicability === 'REQUIRED').length,
|
||||
recommended: toms.filter((t) => t.applicability === 'RECOMMENDED').length,
|
||||
optional: toms.filter((t) => t.applicability === 'OPTIONAL').length,
|
||||
notApplicable: toms.filter((t) => t.applicability === 'NOT_APPLICABLE').length,
|
||||
implemented: toms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
|
||||
partial: toms.filter((t) => t.implementationStatus === 'PARTIAL').length,
|
||||
notImplemented: toms.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length,
|
||||
}
|
||||
}
|
||||
|
||||
function groupEvidenceByControl(
|
||||
documents: Array<{ id: string; linkedControlIds: string[] }>
|
||||
): Map<string, string[]> {
|
||||
const grouped = new Map<string, string[]>()
|
||||
|
||||
for (const doc of documents) {
|
||||
for (const controlId of doc.linkedControlIds) {
|
||||
const existing = grouped.get(controlId) || []
|
||||
existing.push(doc.id)
|
||||
grouped.set(controlId, existing)
|
||||
}
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
function generateRecommendationsMarkdown(
|
||||
recommendations: string[],
|
||||
language: 'de' | 'en'
|
||||
): string {
|
||||
const title = language === 'de' ? 'Empfehlungen' : 'Recommendations'
|
||||
|
||||
return `# ${title}
|
||||
|
||||
${recommendations.map((rec, i) => `${i + 1}. ${rec}`).join('\n\n')}
|
||||
|
||||
---
|
||||
|
||||
${language === 'de' ? 'Generiert am' : 'Generated on'} ${new Date().toISOString().split('T')[0]}
|
||||
`
|
||||
}
|
||||
|
||||
function generateMarkdownSummary(
|
||||
state: TOMGeneratorState,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const lang = opts.language
|
||||
const date = new Date().toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US')
|
||||
|
||||
let md = `# ${lang === 'de' ? 'Technische und Organisatorische Maßnahmen' : 'Technical and Organizational Measures'}
|
||||
|
||||
**${lang === 'de' ? 'Unternehmen' : 'Company'}:** ${state.companyProfile?.name || 'N/A'}
|
||||
**${lang === 'de' ? 'Stand' : 'Date'}:** ${date}
|
||||
**${lang === 'de' ? 'Schutzbedarf' : 'Protection Level'}:** ${state.riskProfile?.protectionLevel || 'N/A'}
|
||||
|
||||
## ${lang === 'de' ? 'Zusammenfassung' : 'Summary'}
|
||||
|
||||
| ${lang === 'de' ? 'Metrik' : 'Metric'} | ${lang === 'de' ? 'Wert' : 'Value'} |
|
||||
|--------|-------|
|
||||
| ${lang === 'de' ? 'Gesamtzahl TOMs' : 'Total TOMs'} | ${state.derivedTOMs.length} |
|
||||
| ${lang === 'de' ? 'Erforderlich' : 'Required'} | ${state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length} |
|
||||
| ${lang === 'de' ? 'Umgesetzt' : 'Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length} |
|
||||
| ${lang === 'de' ? 'Teilweise umgesetzt' : 'Partially Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length} |
|
||||
| ${lang === 'de' ? 'Nicht umgesetzt' : 'Not Implemented'} | ${state.derivedTOMs.filter((t) => t.implementationStatus === 'NOT_IMPLEMENTED').length} |
|
||||
|
||||
`
|
||||
|
||||
if (state.gapAnalysis) {
|
||||
md += `
|
||||
## ${lang === 'de' ? 'Compliance Score' : 'Compliance Score'}
|
||||
|
||||
**${state.gapAnalysis.overallScore}%**
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
// Add required TOMs table
|
||||
const requiredTOMs = state.derivedTOMs.filter(
|
||||
(t) => t.applicability === 'REQUIRED'
|
||||
)
|
||||
|
||||
if (requiredTOMs.length > 0) {
|
||||
md += `
|
||||
## ${lang === 'de' ? 'Erforderliche Maßnahmen' : 'Required Measures'}
|
||||
|
||||
| ID | ${lang === 'de' ? 'Maßnahme' : 'Measure'} | Status |
|
||||
|----|----------|--------|
|
||||
${requiredTOMs.map((tom) => `| ${tom.controlId} | ${tom.name} | ${formatStatus(tom.implementationStatus, lang)} |`).join('\n')}
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
function generateCSV(
|
||||
toms: Array<{
|
||||
controlId: string
|
||||
name: string
|
||||
description: string
|
||||
applicability: string
|
||||
implementationStatus: string
|
||||
responsiblePerson: string | null
|
||||
}>,
|
||||
opts: ZIPExportOptions
|
||||
): string {
|
||||
const lang = opts.language
|
||||
|
||||
const headers = lang === 'de'
|
||||
? ['ID', 'Name', 'Beschreibung', 'Anwendbarkeit', 'Status', 'Verantwortlich']
|
||||
: ['ID', 'Name', 'Description', 'Applicability', 'Status', 'Responsible']
|
||||
|
||||
const rows = toms.map((tom) => [
|
||||
tom.controlId,
|
||||
escapeCSV(tom.name),
|
||||
escapeCSV(tom.description),
|
||||
tom.applicability,
|
||||
tom.implementationStatus,
|
||||
tom.responsiblePerson || '',
|
||||
])
|
||||
|
||||
return [
|
||||
headers.join(','),
|
||||
...rows.map((row) => row.join(',')),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function escapeCSV(value: string): string {
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function formatStatus(status: string, lang: 'de' | 'en'): string {
|
||||
const statuses: Record<string, Record<'de' | 'en', string>> = {
|
||||
NOT_IMPLEMENTED: { de: 'Nicht umgesetzt', en: 'Not Implemented' },
|
||||
PARTIAL: { de: 'Teilweise', en: 'Partial' },
|
||||
IMPLEMENTED: { de: 'Umgesetzt', en: 'Implemented' },
|
||||
}
|
||||
return statuses[status]?.[lang] || status
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ZIP BLOB GENERATION
|
||||
// Note: For production, use jszip library
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a ZIP file as a Blob
|
||||
* This is a placeholder - in production, use jszip library
|
||||
*/
|
||||
export async function generateZIPBlob(
|
||||
state: TOMGeneratorState,
|
||||
options: Partial<ZIPExportOptions> = {}
|
||||
): Promise<Blob> {
|
||||
const files = generateZIPFiles(state, options)
|
||||
|
||||
// Create a simple JSON representation for now
|
||||
// In production, use JSZip library
|
||||
const manifest = {
|
||||
generated: new Date().toISOString(),
|
||||
files: files.map((f) => ({
|
||||
path: f.path,
|
||||
mimeType: f.mimeType,
|
||||
size: typeof f.content === 'string' ? f.content.length : 0,
|
||||
})),
|
||||
}
|
||||
|
||||
const allContent = files
|
||||
.filter((f) => typeof f.content === 'string')
|
||||
.map((f) => `\n\n=== ${f.path} ===\n\n${f.content}`)
|
||||
.join('\n')
|
||||
|
||||
const output = `TOM Export Package
|
||||
Generated: ${manifest.generated}
|
||||
|
||||
Files:
|
||||
${manifest.files.map((f) => ` - ${f.path} (${f.mimeType})`).join('\n')}
|
||||
|
||||
${allContent}`
|
||||
|
||||
return new Blob([output], { type: 'application/zip' })
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FILENAME GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate a filename for ZIP export
|
||||
*/
|
||||
export function generateZIPFilename(
|
||||
state: TOMGeneratorState,
|
||||
language: 'de' | 'en' = 'de'
|
||||
): string {
|
||||
const companyName = state.companyProfile?.name?.replace(/[^a-zA-Z0-9]/g, '-') || 'unknown'
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
const prefix = language === 'de' ? 'TOMs-Export' : 'TOMs-Export'
|
||||
return `${prefix}-${companyName}-${date}.zip`
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT
|
||||
// =============================================================================
|
||||
|
||||
// Types are exported at their definition site above
|
||||
206
admin-v2/lib/sdk/tom-generator/index.ts
Normal file
206
admin-v2/lib/sdk/tom-generator/index.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Module - Public API
|
||||
// =============================================================================
|
||||
|
||||
// Types
|
||||
export * from './types'
|
||||
|
||||
// Context and Hooks
|
||||
export {
|
||||
TOMGeneratorProvider,
|
||||
useTOMGenerator,
|
||||
TOMGeneratorContext,
|
||||
} from './context'
|
||||
export type {
|
||||
TOMGeneratorAction,
|
||||
TOMGeneratorContextValue,
|
||||
} from './context'
|
||||
|
||||
// Rules Engine
|
||||
export {
|
||||
TOMRulesEngine,
|
||||
getTOMRulesEngine,
|
||||
evaluateControlsForContext,
|
||||
deriveTOMsForContext,
|
||||
performQuickGapAnalysis,
|
||||
} from './rules-engine'
|
||||
|
||||
// Control Library
|
||||
export {
|
||||
getControlLibrary,
|
||||
getAllControls,
|
||||
getControlById,
|
||||
getControlsByCategory,
|
||||
getControlsByType,
|
||||
getControlsByPriority,
|
||||
getControlsByTag,
|
||||
getAllTags,
|
||||
getCategoryMetadata,
|
||||
getAllCategories,
|
||||
getLibraryMetadata,
|
||||
searchControls,
|
||||
getControlsByFramework,
|
||||
getControlsCountByCategory,
|
||||
} from './controls/loader'
|
||||
export type { ControlLibrary } from './controls/loader'
|
||||
|
||||
// AI Integration
|
||||
export {
|
||||
AI_PROMPTS,
|
||||
getDocumentAnalysisPrompt,
|
||||
getTOMDescriptionPrompt,
|
||||
getGapRecommendationsPrompt,
|
||||
getDocumentTypeDetectionPrompt,
|
||||
getClauseExtractionPrompt,
|
||||
getComplianceAssessmentPrompt,
|
||||
} from './ai/prompts'
|
||||
export type {
|
||||
DocumentAnalysisPromptContext,
|
||||
TOMDescriptionPromptContext,
|
||||
GapRecommendationsPromptContext,
|
||||
} from './ai/prompts'
|
||||
|
||||
export {
|
||||
TOMDocumentAnalyzer,
|
||||
getDocumentAnalyzer,
|
||||
analyzeEvidenceDocument,
|
||||
detectEvidenceDocumentType,
|
||||
getEvidenceGapsForAllControls,
|
||||
} from './ai/document-analyzer'
|
||||
export type {
|
||||
AnalysisResult,
|
||||
DocumentTypeDetectionResult,
|
||||
} from './ai/document-analyzer'
|
||||
|
||||
// Export Functions
|
||||
export {
|
||||
generateDOCXContent,
|
||||
generateDOCXBlob,
|
||||
} from './export/docx'
|
||||
export type {
|
||||
DOCXExportOptions,
|
||||
DocxElement,
|
||||
DocxParagraph,
|
||||
DocxTable,
|
||||
DocxTableRow,
|
||||
} from './export/docx'
|
||||
|
||||
export {
|
||||
generatePDFContent,
|
||||
generatePDFBlob,
|
||||
} from './export/pdf'
|
||||
export type {
|
||||
PDFExportOptions,
|
||||
PDFSection,
|
||||
} from './export/pdf'
|
||||
|
||||
export {
|
||||
generateZIPFiles,
|
||||
generateZIPBlob,
|
||||
} from './export/zip'
|
||||
export type {
|
||||
ZIPExportOptions,
|
||||
ZIPFileEntry,
|
||||
} from './export/zip'
|
||||
|
||||
// Demo Data
|
||||
export {
|
||||
generateDemoState,
|
||||
generateEmptyState,
|
||||
generatePartialState,
|
||||
DEMO_COMPANY_PROFILES,
|
||||
DEMO_DATA_PROFILES,
|
||||
DEMO_ARCHITECTURE_PROFILES,
|
||||
DEMO_SECURITY_PROFILES,
|
||||
DEMO_RISK_PROFILES,
|
||||
DEMO_EVIDENCE_DOCUMENTS,
|
||||
} from './demo-data'
|
||||
export type { DemoScenario } from './demo-data'
|
||||
|
||||
// =============================================================================
|
||||
// CONVENIENCE EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
import { TOMRulesEngine } from './rules-engine'
|
||||
import {
|
||||
TOMGeneratorState,
|
||||
RulesEngineEvaluationContext,
|
||||
DerivedTOM,
|
||||
GapAnalysisResult,
|
||||
EvidenceDocument,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* Create a new TOM Rules Engine instance
|
||||
*/
|
||||
export function createRulesEngine(): TOMRulesEngine {
|
||||
return new TOMRulesEngine()
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive TOMs for a given state
|
||||
*/
|
||||
export function deriveTOMsFromState(state: TOMGeneratorState): DerivedTOM[] {
|
||||
const engine = new TOMRulesEngine()
|
||||
return engine.deriveAllTOMs({
|
||||
companyProfile: state.companyProfile,
|
||||
dataProfile: state.dataProfile,
|
||||
architectureProfile: state.architectureProfile,
|
||||
securityProfile: state.securityProfile,
|
||||
riskProfile: state.riskProfile,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform gap analysis on a state
|
||||
*/
|
||||
export function analyzeGapsFromState(
|
||||
state: TOMGeneratorState
|
||||
): GapAnalysisResult {
|
||||
const engine = new TOMRulesEngine()
|
||||
return engine.performGapAnalysis(state.derivedTOMs, state.documents)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completion statistics for a state
|
||||
*/
|
||||
export function getStateStatistics(state: TOMGeneratorState): {
|
||||
totalControls: number
|
||||
requiredControls: number
|
||||
implementedControls: number
|
||||
partialControls: number
|
||||
notImplementedControls: number
|
||||
complianceScore: number
|
||||
stepsCompleted: number
|
||||
totalSteps: number
|
||||
documentsUploaded: number
|
||||
} {
|
||||
const totalControls = state.derivedTOMs.length
|
||||
const requiredControls = state.derivedTOMs.filter(
|
||||
(t) => t.applicability === 'REQUIRED'
|
||||
).length
|
||||
const implementedControls = state.derivedTOMs.filter(
|
||||
(t) => t.implementationStatus === 'IMPLEMENTED'
|
||||
).length
|
||||
const partialControls = state.derivedTOMs.filter(
|
||||
(t) => t.implementationStatus === 'PARTIAL'
|
||||
).length
|
||||
const notImplementedControls = state.derivedTOMs.filter(
|
||||
(t) => t.implementationStatus === 'NOT_IMPLEMENTED'
|
||||
).length
|
||||
|
||||
const stepsCompleted = state.steps.filter((s) => s.completed).length
|
||||
const totalSteps = state.steps.length
|
||||
|
||||
return {
|
||||
totalControls,
|
||||
requiredControls,
|
||||
implementedControls,
|
||||
partialControls,
|
||||
notImplementedControls,
|
||||
complianceScore: state.gapAnalysis?.overallScore ?? 0,
|
||||
stepsCompleted,
|
||||
totalSteps,
|
||||
documentsUploaded: state.documents.length,
|
||||
}
|
||||
}
|
||||
560
admin-v2/lib/sdk/tom-generator/rules-engine.ts
Normal file
560
admin-v2/lib/sdk/tom-generator/rules-engine.ts
Normal file
@@ -0,0 +1,560 @@
|
||||
// =============================================================================
|
||||
// TOM Rules Engine
|
||||
// Evaluates control applicability based on company context
|
||||
// =============================================================================
|
||||
|
||||
import {
|
||||
ControlLibraryEntry,
|
||||
ApplicabilityCondition,
|
||||
ControlApplicability,
|
||||
RulesEngineResult,
|
||||
RulesEngineEvaluationContext,
|
||||
DerivedTOM,
|
||||
EvidenceDocument,
|
||||
GapAnalysisResult,
|
||||
MissingControl,
|
||||
PartialControl,
|
||||
MissingEvidence,
|
||||
ConditionOperator,
|
||||
} from './types'
|
||||
import { getAllControls, getControlById } from './controls/loader'
|
||||
|
||||
// =============================================================================
|
||||
// RULES ENGINE CLASS
|
||||
// =============================================================================
|
||||
|
||||
export class TOMRulesEngine {
|
||||
private controls: ControlLibraryEntry[]
|
||||
|
||||
constructor() {
|
||||
this.controls = getAllControls()
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate all controls against the current context
|
||||
*/
|
||||
evaluateControls(context: RulesEngineEvaluationContext): RulesEngineResult[] {
|
||||
return this.controls.map((control) => this.evaluateControl(control, context))
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single control against the context
|
||||
*/
|
||||
evaluateControl(
|
||||
control: ControlLibraryEntry,
|
||||
context: RulesEngineEvaluationContext
|
||||
): RulesEngineResult {
|
||||
// Sort conditions by priority (highest first)
|
||||
const sortedConditions = [...control.applicabilityConditions].sort(
|
||||
(a, b) => b.priority - a.priority
|
||||
)
|
||||
|
||||
// Evaluate conditions in priority order
|
||||
for (const condition of sortedConditions) {
|
||||
const matches = this.evaluateCondition(condition, context)
|
||||
if (matches) {
|
||||
return {
|
||||
controlId: control.id,
|
||||
applicability: condition.result,
|
||||
reason: this.formatConditionReason(condition, context),
|
||||
matchedCondition: condition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No condition matched, use default applicability
|
||||
return {
|
||||
controlId: control.id,
|
||||
applicability: control.defaultApplicability,
|
||||
reason: 'Standard-Anwendbarkeit (keine spezifische Bedingung erfüllt)',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single condition
|
||||
*/
|
||||
private evaluateCondition(
|
||||
condition: ApplicabilityCondition,
|
||||
context: RulesEngineEvaluationContext
|
||||
): boolean {
|
||||
const value = this.getFieldValue(condition.field, context)
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.evaluateOperator(condition.operator, value, condition.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a nested field value from the context
|
||||
*/
|
||||
private getFieldValue(
|
||||
fieldPath: string,
|
||||
context: RulesEngineEvaluationContext
|
||||
): unknown {
|
||||
const parts = fieldPath.split('.')
|
||||
let current: unknown = context
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (typeof current === 'object') {
|
||||
current = (current as Record<string, unknown>)[part]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate an operator with given values
|
||||
*/
|
||||
private evaluateOperator(
|
||||
operator: ConditionOperator,
|
||||
actualValue: unknown,
|
||||
expectedValue: unknown
|
||||
): boolean {
|
||||
switch (operator) {
|
||||
case 'EQUALS':
|
||||
return actualValue === expectedValue
|
||||
|
||||
case 'NOT_EQUALS':
|
||||
return actualValue !== expectedValue
|
||||
|
||||
case 'CONTAINS':
|
||||
if (Array.isArray(actualValue)) {
|
||||
return actualValue.includes(expectedValue)
|
||||
}
|
||||
if (typeof actualValue === 'string' && typeof expectedValue === 'string') {
|
||||
return actualValue.includes(expectedValue)
|
||||
}
|
||||
return false
|
||||
|
||||
case 'GREATER_THAN':
|
||||
if (typeof actualValue === 'number' && typeof expectedValue === 'number') {
|
||||
return actualValue > expectedValue
|
||||
}
|
||||
return false
|
||||
|
||||
case 'IN':
|
||||
if (Array.isArray(expectedValue)) {
|
||||
return expectedValue.includes(actualValue)
|
||||
}
|
||||
return false
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a human-readable reason for the condition match
|
||||
*/
|
||||
private formatConditionReason(
|
||||
condition: ApplicabilityCondition,
|
||||
context: RulesEngineEvaluationContext
|
||||
): string {
|
||||
const fieldValue = this.getFieldValue(condition.field, context)
|
||||
const fieldLabel = this.getFieldLabel(condition.field)
|
||||
|
||||
switch (condition.operator) {
|
||||
case 'EQUALS':
|
||||
return `${fieldLabel} ist "${this.formatValue(fieldValue)}"`
|
||||
|
||||
case 'NOT_EQUALS':
|
||||
return `${fieldLabel} ist nicht "${this.formatValue(condition.value)}"`
|
||||
|
||||
case 'CONTAINS':
|
||||
return `${fieldLabel} enthält "${this.formatValue(condition.value)}"`
|
||||
|
||||
case 'GREATER_THAN':
|
||||
return `${fieldLabel} ist größer als ${this.formatValue(condition.value)}`
|
||||
|
||||
case 'IN':
|
||||
return `${fieldLabel} ("${this.formatValue(fieldValue)}") ist in [${Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}]`
|
||||
|
||||
default:
|
||||
return `Bedingung erfüllt: ${condition.field} ${condition.operator} ${this.formatValue(condition.value)}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label for a field path
|
||||
*/
|
||||
private getFieldLabel(fieldPath: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'companyProfile.role': 'Unternehmensrolle',
|
||||
'companyProfile.size': 'Unternehmensgröße',
|
||||
'dataProfile.hasSpecialCategories': 'Besondere Datenkategorien',
|
||||
'dataProfile.processesMinors': 'Verarbeitung von Minderjährigen-Daten',
|
||||
'dataProfile.dataVolume': 'Datenvolumen',
|
||||
'dataProfile.thirdCountryTransfers': 'Drittlandübermittlungen',
|
||||
'architectureProfile.hostingModel': 'Hosting-Modell',
|
||||
'architectureProfile.hostingLocation': 'Hosting-Standort',
|
||||
'architectureProfile.multiTenancy': 'Mandantentrennung',
|
||||
'architectureProfile.hasSubprocessors': 'Unterauftragsverarbeiter',
|
||||
'architectureProfile.encryptionAtRest': 'Verschlüsselung ruhender Daten',
|
||||
'securityProfile.hasMFA': 'Multi-Faktor-Authentifizierung',
|
||||
'securityProfile.hasSSO': 'Single Sign-On',
|
||||
'securityProfile.hasPAM': 'Privileged Access Management',
|
||||
'riskProfile.protectionLevel': 'Schutzbedarf',
|
||||
'riskProfile.dsfaRequired': 'DSFA erforderlich',
|
||||
'riskProfile.ciaAssessment.confidentiality': 'Vertraulichkeit',
|
||||
'riskProfile.ciaAssessment.integrity': 'Integrität',
|
||||
'riskProfile.ciaAssessment.availability': 'Verfügbarkeit',
|
||||
}
|
||||
|
||||
return labels[fieldPath] || fieldPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a value for display
|
||||
*/
|
||||
private formatValue(value: unknown): string {
|
||||
if (value === true) return 'Ja'
|
||||
if (value === false) return 'Nein'
|
||||
if (value === null || value === undefined) return 'nicht gesetzt'
|
||||
if (Array.isArray(value)) return value.join(', ')
|
||||
return String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive all TOMs based on the current context
|
||||
*/
|
||||
deriveAllTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
|
||||
const results = this.evaluateControls(context)
|
||||
|
||||
return results.map((result) => {
|
||||
const control = getControlById(result.controlId)
|
||||
if (!control) {
|
||||
throw new Error(`Control not found: ${result.controlId}`)
|
||||
}
|
||||
|
||||
return {
|
||||
id: `derived-${result.controlId}`,
|
||||
controlId: result.controlId,
|
||||
name: control.name.de,
|
||||
description: control.description.de,
|
||||
applicability: result.applicability,
|
||||
applicabilityReason: result.reason,
|
||||
implementationStatus: 'NOT_IMPLEMENTED',
|
||||
responsiblePerson: null,
|
||||
responsibleDepartment: null,
|
||||
implementationDate: null,
|
||||
reviewDate: null,
|
||||
linkedEvidence: [],
|
||||
evidenceGaps: [...control.evidenceRequirements],
|
||||
aiGeneratedDescription: null,
|
||||
aiRecommendations: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only required and recommended TOMs
|
||||
*/
|
||||
getApplicableTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
|
||||
const allTOMs = this.deriveAllTOMs(context)
|
||||
return allTOMs.filter(
|
||||
(tom) =>
|
||||
tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only required TOMs
|
||||
*/
|
||||
getRequiredTOMs(context: RulesEngineEvaluationContext): DerivedTOM[] {
|
||||
const allTOMs = this.deriveAllTOMs(context)
|
||||
return allTOMs.filter((tom) => tom.applicability === 'REQUIRED')
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform gap analysis on derived TOMs and evidence
|
||||
*/
|
||||
performGapAnalysis(
|
||||
derivedTOMs: DerivedTOM[],
|
||||
documents: EvidenceDocument[]
|
||||
): GapAnalysisResult {
|
||||
const missingControls: MissingControl[] = []
|
||||
const partialControls: PartialControl[] = []
|
||||
const missingEvidence: MissingEvidence[] = []
|
||||
const recommendations: string[] = []
|
||||
|
||||
let totalScore = 0
|
||||
let totalWeight = 0
|
||||
|
||||
// Analyze each required/recommended TOM
|
||||
const applicableTOMs = derivedTOMs.filter(
|
||||
(tom) =>
|
||||
tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
|
||||
)
|
||||
|
||||
for (const tom of applicableTOMs) {
|
||||
const control = getControlById(tom.controlId)
|
||||
if (!control) continue
|
||||
|
||||
const weight = tom.applicability === 'REQUIRED' ? 3 : 1
|
||||
totalWeight += weight
|
||||
|
||||
// Check implementation status
|
||||
if (tom.implementationStatus === 'NOT_IMPLEMENTED') {
|
||||
missingControls.push({
|
||||
controlId: tom.controlId,
|
||||
reason: `${control.name.de} ist nicht implementiert`,
|
||||
priority: control.priority,
|
||||
})
|
||||
// Score: 0 for not implemented
|
||||
} else if (tom.implementationStatus === 'PARTIAL') {
|
||||
partialControls.push({
|
||||
controlId: tom.controlId,
|
||||
missingAspects: tom.evidenceGaps,
|
||||
})
|
||||
// Score: 50% for partial
|
||||
totalScore += weight * 0.5
|
||||
} else {
|
||||
// Fully implemented
|
||||
totalScore += weight
|
||||
}
|
||||
|
||||
// Check evidence
|
||||
const linkedEvidenceIds = tom.linkedEvidence
|
||||
const requiredEvidence = control.evidenceRequirements
|
||||
const providedEvidence = documents.filter((doc) =>
|
||||
linkedEvidenceIds.includes(doc.id)
|
||||
)
|
||||
|
||||
if (providedEvidence.length < requiredEvidence.length) {
|
||||
const missing = requiredEvidence.filter(
|
||||
(req) =>
|
||||
!providedEvidence.some(
|
||||
(doc) =>
|
||||
doc.documentType === 'POLICY' ||
|
||||
doc.documentType === 'CERTIFICATE' ||
|
||||
doc.originalName.toLowerCase().includes(req.toLowerCase())
|
||||
)
|
||||
)
|
||||
|
||||
if (missing.length > 0) {
|
||||
missingEvidence.push({
|
||||
controlId: tom.controlId,
|
||||
requiredEvidence: missing,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall score as percentage
|
||||
const overallScore =
|
||||
totalWeight > 0 ? Math.round((totalScore / totalWeight) * 100) : 0
|
||||
|
||||
// Generate recommendations
|
||||
if (missingControls.length > 0) {
|
||||
const criticalMissing = missingControls.filter(
|
||||
(mc) => mc.priority === 'CRITICAL'
|
||||
)
|
||||
if (criticalMissing.length > 0) {
|
||||
recommendations.push(
|
||||
`${criticalMissing.length} kritische Kontrollen sind nicht implementiert. Diese sollten priorisiert werden.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (partialControls.length > 0) {
|
||||
recommendations.push(
|
||||
`${partialControls.length} Kontrollen sind nur teilweise implementiert. Vervollständigen Sie die Implementierung.`
|
||||
)
|
||||
}
|
||||
|
||||
if (missingEvidence.length > 0) {
|
||||
recommendations.push(
|
||||
`Für ${missingEvidence.length} Kontrollen fehlen Nachweisdokumente. Laden Sie die entsprechenden Dokumente hoch.`
|
||||
)
|
||||
}
|
||||
|
||||
if (overallScore >= 80) {
|
||||
recommendations.push(
|
||||
'Ihr TOM-Compliance-Score ist gut. Führen Sie regelmäßige Überprüfungen durch.'
|
||||
)
|
||||
} else if (overallScore >= 50) {
|
||||
recommendations.push(
|
||||
'Ihr TOM-Compliance-Score erfordert Verbesserungen. Fokussieren Sie sich auf die kritischen Lücken.'
|
||||
)
|
||||
} else {
|
||||
recommendations.push(
|
||||
'Ihr TOM-Compliance-Score ist niedrig. Eine systematische Überarbeitung der Maßnahmen wird empfohlen.'
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
overallScore,
|
||||
missingControls,
|
||||
partialControls,
|
||||
missingEvidence,
|
||||
recommendations,
|
||||
generatedAt: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controls by applicability level
|
||||
*/
|
||||
getControlsByApplicability(
|
||||
context: RulesEngineEvaluationContext,
|
||||
applicability: ControlApplicability
|
||||
): ControlLibraryEntry[] {
|
||||
const results = this.evaluateControls(context)
|
||||
return results
|
||||
.filter((r) => r.applicability === applicability)
|
||||
.map((r) => getControlById(r.controlId))
|
||||
.filter((c): c is ControlLibraryEntry => c !== undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get summary statistics for the evaluation
|
||||
*/
|
||||
getSummaryStatistics(context: RulesEngineEvaluationContext): {
|
||||
total: number
|
||||
required: number
|
||||
recommended: number
|
||||
optional: number
|
||||
notApplicable: number
|
||||
byCategory: Map<string, { required: number; recommended: number }>
|
||||
} {
|
||||
const results = this.evaluateControls(context)
|
||||
|
||||
const stats = {
|
||||
total: results.length,
|
||||
required: 0,
|
||||
recommended: 0,
|
||||
optional: 0,
|
||||
notApplicable: 0,
|
||||
byCategory: new Map<string, { required: number; recommended: number }>(),
|
||||
}
|
||||
|
||||
for (const result of results) {
|
||||
switch (result.applicability) {
|
||||
case 'REQUIRED':
|
||||
stats.required++
|
||||
break
|
||||
case 'RECOMMENDED':
|
||||
stats.recommended++
|
||||
break
|
||||
case 'OPTIONAL':
|
||||
stats.optional++
|
||||
break
|
||||
case 'NOT_APPLICABLE':
|
||||
stats.notApplicable++
|
||||
break
|
||||
}
|
||||
|
||||
// Count by category
|
||||
const control = getControlById(result.controlId)
|
||||
if (control) {
|
||||
const category = control.category
|
||||
const existing = stats.byCategory.get(category) || {
|
||||
required: 0,
|
||||
recommended: 0,
|
||||
}
|
||||
|
||||
if (result.applicability === 'REQUIRED') {
|
||||
existing.required++
|
||||
} else if (result.applicability === 'RECOMMENDED') {
|
||||
existing.recommended++
|
||||
}
|
||||
|
||||
stats.byCategory.set(category, existing)
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific control is applicable
|
||||
*/
|
||||
isControlApplicable(
|
||||
controlId: string,
|
||||
context: RulesEngineEvaluationContext
|
||||
): boolean {
|
||||
const control = getControlById(controlId)
|
||||
if (!control) return false
|
||||
|
||||
const result = this.evaluateControl(control, context)
|
||||
return (
|
||||
result.applicability === 'REQUIRED' ||
|
||||
result.applicability === 'RECOMMENDED'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all controls that match a specific tag
|
||||
*/
|
||||
getControlsByTagWithApplicability(
|
||||
tag: string,
|
||||
context: RulesEngineEvaluationContext
|
||||
): Array<{ control: ControlLibraryEntry; result: RulesEngineResult }> {
|
||||
return this.controls
|
||||
.filter((control) => control.tags.includes(tag))
|
||||
.map((control) => ({
|
||||
control,
|
||||
result: this.evaluateControl(control, context),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload controls (useful if the control library is updated)
|
||||
*/
|
||||
reloadControls(): void {
|
||||
this.controls = getAllControls()
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SINGLETON INSTANCE
|
||||
// =============================================================================
|
||||
|
||||
let rulesEngineInstance: TOMRulesEngine | null = null
|
||||
|
||||
export function getTOMRulesEngine(): TOMRulesEngine {
|
||||
if (!rulesEngineInstance) {
|
||||
rulesEngineInstance = new TOMRulesEngine()
|
||||
}
|
||||
return rulesEngineInstance
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Quick evaluation of controls for a context
|
||||
*/
|
||||
export function evaluateControlsForContext(
|
||||
context: RulesEngineEvaluationContext
|
||||
): RulesEngineResult[] {
|
||||
return getTOMRulesEngine().evaluateControls(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick derivation of TOMs for a context
|
||||
*/
|
||||
export function deriveTOMsForContext(
|
||||
context: RulesEngineEvaluationContext
|
||||
): DerivedTOM[] {
|
||||
return getTOMRulesEngine().deriveAllTOMs(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick gap analysis
|
||||
*/
|
||||
export function performQuickGapAnalysis(
|
||||
derivedTOMs: DerivedTOM[],
|
||||
documents: EvidenceDocument[]
|
||||
): GapAnalysisResult {
|
||||
return getTOMRulesEngine().performGapAnalysis(derivedTOMs, documents)
|
||||
}
|
||||
901
admin-v2/lib/sdk/tom-generator/types.ts
Normal file
901
admin-v2/lib/sdk/tom-generator/types.ts
Normal file
@@ -0,0 +1,901 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Module - TypeScript Types
|
||||
// DSGVO Art. 32 Technical and Organizational Measures
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & LITERAL TYPES
|
||||
// =============================================================================
|
||||
|
||||
export type TOMGeneratorStepId =
|
||||
| 'scope-roles'
|
||||
| 'data-categories'
|
||||
| 'architecture-hosting'
|
||||
| 'security-profile'
|
||||
| 'risk-protection'
|
||||
| 'review-export'
|
||||
|
||||
export type CompanyRole = 'CONTROLLER' | 'PROCESSOR' | 'JOINT_CONTROLLER'
|
||||
|
||||
export type DataCategory =
|
||||
| 'IDENTIFICATION'
|
||||
| 'CONTACT'
|
||||
| 'FINANCIAL'
|
||||
| 'PROFESSIONAL'
|
||||
| 'LOCATION'
|
||||
| 'BEHAVIORAL'
|
||||
| 'BIOMETRIC'
|
||||
| 'HEALTH'
|
||||
| 'GENETIC'
|
||||
| 'POLITICAL'
|
||||
| 'RELIGIOUS'
|
||||
| 'SEXUAL_ORIENTATION'
|
||||
| 'CRIMINAL'
|
||||
|
||||
export type DataSubject =
|
||||
| 'EMPLOYEES'
|
||||
| 'CUSTOMERS'
|
||||
| 'PROSPECTS'
|
||||
| 'SUPPLIERS'
|
||||
| 'MINORS'
|
||||
| 'PATIENTS'
|
||||
| 'STUDENTS'
|
||||
| 'GENERAL_PUBLIC'
|
||||
|
||||
export type HostingLocation =
|
||||
| 'DE'
|
||||
| 'EU'
|
||||
| 'EEA'
|
||||
| 'THIRD_COUNTRY_ADEQUATE'
|
||||
| 'THIRD_COUNTRY'
|
||||
|
||||
export type HostingModel = 'ON_PREMISE' | 'PRIVATE_CLOUD' | 'PUBLIC_CLOUD' | 'HYBRID'
|
||||
|
||||
export type MultiTenancy = 'SINGLE_TENANT' | 'MULTI_TENANT' | 'DEDICATED'
|
||||
|
||||
export type ControlApplicability =
|
||||
| 'REQUIRED'
|
||||
| 'RECOMMENDED'
|
||||
| 'OPTIONAL'
|
||||
| 'NOT_APPLICABLE'
|
||||
|
||||
export type DocumentType =
|
||||
| 'AVV'
|
||||
| 'DPA'
|
||||
| 'SLA'
|
||||
| 'NDA'
|
||||
| 'POLICY'
|
||||
| 'CERTIFICATE'
|
||||
| 'AUDIT_REPORT'
|
||||
| 'OTHER'
|
||||
|
||||
export type ProtectionLevel = 'NORMAL' | 'HIGH' | 'VERY_HIGH'
|
||||
|
||||
export type CIARating = 1 | 2 | 3 | 4 | 5
|
||||
|
||||
export type ControlCategory =
|
||||
| 'ACCESS_CONTROL'
|
||||
| 'ADMISSION_CONTROL'
|
||||
| 'ACCESS_AUTHORIZATION'
|
||||
| 'TRANSFER_CONTROL'
|
||||
| 'INPUT_CONTROL'
|
||||
| 'ORDER_CONTROL'
|
||||
| 'AVAILABILITY'
|
||||
| 'SEPARATION'
|
||||
| 'ENCRYPTION'
|
||||
| 'PSEUDONYMIZATION'
|
||||
| 'RESILIENCE'
|
||||
| 'RECOVERY'
|
||||
| 'REVIEW'
|
||||
|
||||
export type CompanySize = 'MICRO' | 'SMALL' | 'MEDIUM' | 'LARGE' | 'ENTERPRISE'
|
||||
|
||||
export type DataVolume = 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
|
||||
|
||||
export type AuthMethodType =
|
||||
| 'PASSWORD'
|
||||
| 'MFA'
|
||||
| 'SSO'
|
||||
| 'CERTIFICATE'
|
||||
| 'BIOMETRIC'
|
||||
|
||||
export type BackupFrequency = 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY'
|
||||
|
||||
export type ReviewFrequency = 'MONTHLY' | 'QUARTERLY' | 'SEMI_ANNUAL' | 'ANNUAL'
|
||||
|
||||
export type ControlPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
||||
|
||||
export type ControlComplexity = 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
|
||||
export type ImplementationStatus = 'NOT_IMPLEMENTED' | 'PARTIAL' | 'IMPLEMENTED'
|
||||
|
||||
export type EvidenceStatus = 'PENDING' | 'ANALYZED' | 'VERIFIED' | 'REJECTED'
|
||||
|
||||
export type ConditionOperator =
|
||||
| 'EQUALS'
|
||||
| 'NOT_EQUALS'
|
||||
| 'CONTAINS'
|
||||
| 'GREATER_THAN'
|
||||
| 'IN'
|
||||
|
||||
// =============================================================================
|
||||
// PROFILE INTERFACES (Wizard Steps 1-5)
|
||||
// =============================================================================
|
||||
|
||||
export interface CompanyProfile {
|
||||
id: string
|
||||
name: string
|
||||
industry: string
|
||||
size: CompanySize
|
||||
role: CompanyRole
|
||||
products: string[]
|
||||
dpoPerson: string | null
|
||||
dpoEmail: string | null
|
||||
itSecurityContact: string | null
|
||||
}
|
||||
|
||||
export interface DataProfile {
|
||||
categories: DataCategory[]
|
||||
subjects: DataSubject[]
|
||||
hasSpecialCategories: boolean
|
||||
processesMinors: boolean
|
||||
dataVolume: DataVolume
|
||||
thirdCountryTransfers: boolean
|
||||
thirdCountryList: string[]
|
||||
}
|
||||
|
||||
export interface CloudProvider {
|
||||
name: string
|
||||
location: HostingLocation
|
||||
certifications: string[]
|
||||
}
|
||||
|
||||
export interface ArchitectureProfile {
|
||||
hostingModel: HostingModel
|
||||
hostingLocation: HostingLocation
|
||||
providers: CloudProvider[]
|
||||
multiTenancy: MultiTenancy
|
||||
hasSubprocessors: boolean
|
||||
subprocessorCount: number
|
||||
encryptionAtRest: boolean
|
||||
encryptionInTransit: boolean
|
||||
}
|
||||
|
||||
export interface AuthMethod {
|
||||
type: AuthMethodType
|
||||
provider: string | null
|
||||
}
|
||||
|
||||
export interface SecurityProfile {
|
||||
authMethods: AuthMethod[]
|
||||
hasMFA: boolean
|
||||
hasSSO: boolean
|
||||
hasIAM: boolean
|
||||
hasPAM: boolean
|
||||
hasEncryptionAtRest: boolean
|
||||
hasEncryptionInTransit: boolean
|
||||
hasLogging: boolean
|
||||
logRetentionDays: number
|
||||
hasBackup: boolean
|
||||
backupFrequency: BackupFrequency
|
||||
backupRetentionDays: number
|
||||
hasDRPlan: boolean
|
||||
rtoHours: number | null
|
||||
rpoHours: number | null
|
||||
hasVulnerabilityManagement: boolean
|
||||
hasPenetrationTests: boolean
|
||||
hasSecurityTraining: boolean
|
||||
}
|
||||
|
||||
export interface CIAAssessment {
|
||||
confidentiality: CIARating
|
||||
integrity: CIARating
|
||||
availability: CIARating
|
||||
justification: string
|
||||
}
|
||||
|
||||
export interface RiskProfile {
|
||||
ciaAssessment: CIAAssessment
|
||||
protectionLevel: ProtectionLevel
|
||||
specialRisks: string[]
|
||||
regulatoryRequirements: string[]
|
||||
hasHighRiskProcessing: boolean
|
||||
dsfaRequired: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EVIDENCE DOCUMENT
|
||||
// =============================================================================
|
||||
|
||||
export interface ExtractedClause {
|
||||
id: string
|
||||
text: string
|
||||
type: string
|
||||
relatedControlId: string | null
|
||||
}
|
||||
|
||||
export interface AIDocumentAnalysis {
|
||||
summary: string
|
||||
extractedClauses: ExtractedClause[]
|
||||
applicableControls: string[]
|
||||
gaps: string[]
|
||||
confidence: number
|
||||
analyzedAt: Date
|
||||
}
|
||||
|
||||
export interface EvidenceDocument {
|
||||
id: string
|
||||
filename: string
|
||||
originalName: string
|
||||
mimeType: string
|
||||
size: number
|
||||
uploadedAt: Date
|
||||
uploadedBy: string
|
||||
documentType: DocumentType
|
||||
detectedType: DocumentType | null
|
||||
hash: string
|
||||
validFrom: Date | null
|
||||
validUntil: Date | null
|
||||
linkedControlIds: string[]
|
||||
aiAnalysis: AIDocumentAnalysis | null
|
||||
status: EvidenceStatus
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CONTROL LIBRARY
|
||||
// =============================================================================
|
||||
|
||||
export interface LocalizedString {
|
||||
de: string
|
||||
en: string
|
||||
}
|
||||
|
||||
export interface FrameworkMapping {
|
||||
framework: string
|
||||
reference: string
|
||||
}
|
||||
|
||||
export interface ApplicabilityCondition {
|
||||
field: string
|
||||
operator: ConditionOperator
|
||||
value: unknown
|
||||
result: ControlApplicability
|
||||
priority: number
|
||||
}
|
||||
|
||||
export interface ControlLibraryEntry {
|
||||
id: string
|
||||
code: string
|
||||
category: ControlCategory
|
||||
type: 'TECHNICAL' | 'ORGANIZATIONAL'
|
||||
name: LocalizedString
|
||||
description: LocalizedString
|
||||
mappings: FrameworkMapping[]
|
||||
applicabilityConditions: ApplicabilityCondition[]
|
||||
defaultApplicability: ControlApplicability
|
||||
evidenceRequirements: string[]
|
||||
reviewFrequency: ReviewFrequency
|
||||
priority: ControlPriority
|
||||
complexity: ControlComplexity
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DERIVED TOM
|
||||
// =============================================================================
|
||||
|
||||
export interface DerivedTOM {
|
||||
id: string
|
||||
controlId: string
|
||||
name: string
|
||||
description: string
|
||||
applicability: ControlApplicability
|
||||
applicabilityReason: string
|
||||
implementationStatus: ImplementationStatus
|
||||
responsiblePerson: string | null
|
||||
responsibleDepartment: string | null
|
||||
implementationDate: Date | null
|
||||
reviewDate: Date | null
|
||||
linkedEvidence: string[]
|
||||
evidenceGaps: string[]
|
||||
aiGeneratedDescription: string | null
|
||||
aiRecommendations: string[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GAP ANALYSIS
|
||||
// =============================================================================
|
||||
|
||||
export interface MissingControl {
|
||||
controlId: string
|
||||
reason: string
|
||||
priority: string
|
||||
}
|
||||
|
||||
export interface PartialControl {
|
||||
controlId: string
|
||||
missingAspects: string[]
|
||||
}
|
||||
|
||||
export interface MissingEvidence {
|
||||
controlId: string
|
||||
requiredEvidence: string[]
|
||||
}
|
||||
|
||||
export interface GapAnalysisResult {
|
||||
overallScore: number
|
||||
missingControls: MissingControl[]
|
||||
partialControls: PartialControl[]
|
||||
missingEvidence: MissingEvidence[]
|
||||
recommendations: string[]
|
||||
generatedAt: Date
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WIZARD STEP
|
||||
// =============================================================================
|
||||
|
||||
export interface WizardStep {
|
||||
id: TOMGeneratorStepId
|
||||
completed: boolean
|
||||
data: unknown
|
||||
validatedAt: Date | null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT RECORD
|
||||
// =============================================================================
|
||||
|
||||
export interface ExportRecord {
|
||||
id: string
|
||||
format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
|
||||
generatedAt: Date
|
||||
filename: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOM GENERATOR STATE
|
||||
// =============================================================================
|
||||
|
||||
export interface TOMGeneratorState {
|
||||
id: string
|
||||
tenantId: string
|
||||
companyProfile: CompanyProfile | null
|
||||
dataProfile: DataProfile | null
|
||||
architectureProfile: ArchitectureProfile | null
|
||||
securityProfile: SecurityProfile | null
|
||||
riskProfile: RiskProfile | null
|
||||
currentStep: TOMGeneratorStepId
|
||||
steps: WizardStep[]
|
||||
documents: EvidenceDocument[]
|
||||
derivedTOMs: DerivedTOM[]
|
||||
gapAnalysis: GapAnalysisResult | null
|
||||
exports: ExportRecord[]
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RULES ENGINE TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface RulesEngineResult {
|
||||
controlId: string
|
||||
applicability: ControlApplicability
|
||||
reason: string
|
||||
matchedCondition?: ApplicabilityCondition
|
||||
}
|
||||
|
||||
export interface RulesEngineEvaluationContext {
|
||||
companyProfile: CompanyProfile | null
|
||||
dataProfile: DataProfile | null
|
||||
architectureProfile: ArchitectureProfile | null
|
||||
securityProfile: SecurityProfile | null
|
||||
riskProfile: RiskProfile | null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface TOMGeneratorStateRequest {
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
export interface TOMGeneratorStateResponse {
|
||||
success: boolean
|
||||
state: TOMGeneratorState | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ControlsEvaluationRequest {
|
||||
tenantId: string
|
||||
context: RulesEngineEvaluationContext
|
||||
}
|
||||
|
||||
export interface ControlsEvaluationResponse {
|
||||
success: boolean
|
||||
results: RulesEngineResult[]
|
||||
evaluatedAt: string
|
||||
}
|
||||
|
||||
export interface EvidenceUploadRequest {
|
||||
tenantId: string
|
||||
documentType: DocumentType
|
||||
validFrom?: string
|
||||
validUntil?: string
|
||||
}
|
||||
|
||||
export interface EvidenceUploadResponse {
|
||||
success: boolean
|
||||
document: EvidenceDocument | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface EvidenceAnalyzeRequest {
|
||||
documentId: string
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
export interface EvidenceAnalyzeResponse {
|
||||
success: boolean
|
||||
analysis: AIDocumentAnalysis | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ExportRequest {
|
||||
tenantId: string
|
||||
format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
|
||||
language: 'de' | 'en'
|
||||
}
|
||||
|
||||
export interface ExportResponse {
|
||||
success: boolean
|
||||
exportId: string
|
||||
filename: string
|
||||
downloadUrl?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface GapAnalysisRequest {
|
||||
tenantId: string
|
||||
}
|
||||
|
||||
export interface GapAnalysisResponse {
|
||||
success: boolean
|
||||
result: GapAnalysisResult | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STEP CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export interface StepConfig {
|
||||
id: TOMGeneratorStepId
|
||||
title: LocalizedString
|
||||
description: LocalizedString
|
||||
checkpointId: string
|
||||
path: string
|
||||
/** Alias for path (for convenience) */
|
||||
url: string
|
||||
/** German title for display (for convenience) */
|
||||
name: string
|
||||
}
|
||||
|
||||
export const TOM_GENERATOR_STEPS: StepConfig[] = [
|
||||
{
|
||||
id: 'scope-roles',
|
||||
title: { de: 'Scope & Rollen', en: 'Scope & Roles' },
|
||||
description: {
|
||||
de: 'Unternehmensname, Branche, Größe und Rolle definieren',
|
||||
en: 'Define company name, industry, size and role',
|
||||
},
|
||||
checkpointId: 'CP-TOM-SCOPE',
|
||||
path: '/sdk/tom-generator/scope',
|
||||
url: '/sdk/tom-generator/scope',
|
||||
name: 'Scope & Rollen',
|
||||
},
|
||||
{
|
||||
id: 'data-categories',
|
||||
title: { de: 'Datenkategorien', en: 'Data Categories' },
|
||||
description: {
|
||||
de: 'Datenkategorien und betroffene Personen erfassen',
|
||||
en: 'Capture data categories and data subjects',
|
||||
},
|
||||
checkpointId: 'CP-TOM-DATA',
|
||||
path: '/sdk/tom-generator/data',
|
||||
url: '/sdk/tom-generator/data',
|
||||
name: 'Datenkategorien',
|
||||
},
|
||||
{
|
||||
id: 'architecture-hosting',
|
||||
title: { de: 'Architektur & Hosting', en: 'Architecture & Hosting' },
|
||||
description: {
|
||||
de: 'Hosting-Modell, Standort und Provider definieren',
|
||||
en: 'Define hosting model, location and providers',
|
||||
},
|
||||
checkpointId: 'CP-TOM-ARCH',
|
||||
path: '/sdk/tom-generator/architecture',
|
||||
url: '/sdk/tom-generator/architecture',
|
||||
name: 'Architektur & Hosting',
|
||||
},
|
||||
{
|
||||
id: 'security-profile',
|
||||
title: { de: 'Security-Profil', en: 'Security Profile' },
|
||||
description: {
|
||||
de: 'Authentifizierung, Verschlüsselung und Backup konfigurieren',
|
||||
en: 'Configure authentication, encryption and backup',
|
||||
},
|
||||
checkpointId: 'CP-TOM-SEC',
|
||||
path: '/sdk/tom-generator/security',
|
||||
url: '/sdk/tom-generator/security',
|
||||
name: 'Security-Profil',
|
||||
},
|
||||
{
|
||||
id: 'risk-protection',
|
||||
title: { de: 'Risiko & Schutzbedarf', en: 'Risk & Protection Level' },
|
||||
description: {
|
||||
de: 'CIA-Bewertung und Schutzbedarf ermitteln',
|
||||
en: 'Determine CIA assessment and protection level',
|
||||
},
|
||||
checkpointId: 'CP-TOM-RISK',
|
||||
path: '/sdk/tom-generator/risk',
|
||||
url: '/sdk/tom-generator/risk',
|
||||
name: 'Risiko & Schutzbedarf',
|
||||
},
|
||||
{
|
||||
id: 'review-export',
|
||||
title: { de: 'Review & Export', en: 'Review & Export' },
|
||||
description: {
|
||||
de: 'Zusammenfassung prüfen und TOMs exportieren',
|
||||
en: 'Review summary and export TOMs',
|
||||
},
|
||||
checkpointId: 'CP-TOM-REVIEW',
|
||||
path: '/sdk/tom-generator/review',
|
||||
url: '/sdk/tom-generator/review',
|
||||
name: 'Review & Export',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// CATEGORY METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface CategoryMetadata {
|
||||
id: ControlCategory
|
||||
name: LocalizedString
|
||||
gdprReference: string
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export const CONTROL_CATEGORIES: CategoryMetadata[] = [
|
||||
{
|
||||
id: 'ACCESS_CONTROL',
|
||||
name: { de: 'Zutrittskontrolle', en: 'Physical Access Control' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'ADMISSION_CONTROL',
|
||||
name: { de: 'Zugangskontrolle', en: 'System Access Control' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'ACCESS_AUTHORIZATION',
|
||||
name: { de: 'Zugriffskontrolle', en: 'Access Authorization' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'TRANSFER_CONTROL',
|
||||
name: { de: 'Weitergabekontrolle', en: 'Transfer Control' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'INPUT_CONTROL',
|
||||
name: { de: 'Eingabekontrolle', en: 'Input Control' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'ORDER_CONTROL',
|
||||
name: { de: 'Auftragskontrolle', en: 'Order Control' },
|
||||
gdprReference: 'Art. 28',
|
||||
},
|
||||
{
|
||||
id: 'AVAILABILITY',
|
||||
name: { de: 'Verfügbarkeit', en: 'Availability' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b, c',
|
||||
},
|
||||
{
|
||||
id: 'SEPARATION',
|
||||
name: { de: 'Trennbarkeit', en: 'Separation' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'ENCRYPTION',
|
||||
name: { de: 'Verschlüsselung', en: 'Encryption' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. a',
|
||||
},
|
||||
{
|
||||
id: 'PSEUDONYMIZATION',
|
||||
name: { de: 'Pseudonymisierung', en: 'Pseudonymization' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. a',
|
||||
},
|
||||
{
|
||||
id: 'RESILIENCE',
|
||||
name: { de: 'Belastbarkeit', en: 'Resilience' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. b',
|
||||
},
|
||||
{
|
||||
id: 'RECOVERY',
|
||||
name: { de: 'Wiederherstellbarkeit', en: 'Recovery' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. c',
|
||||
},
|
||||
{
|
||||
id: 'REVIEW',
|
||||
name: { de: 'Überprüfung & Bewertung', en: 'Review & Assessment' },
|
||||
gdprReference: 'Art. 32 Abs. 1 lit. d',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// DATA CATEGORY METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface DataCategoryMetadata {
|
||||
id: DataCategory
|
||||
name: LocalizedString
|
||||
isSpecialCategory: boolean
|
||||
gdprReference?: string
|
||||
}
|
||||
|
||||
export const DATA_CATEGORIES_METADATA: DataCategoryMetadata[] = [
|
||||
{
|
||||
id: 'IDENTIFICATION',
|
||||
name: { de: 'Identifikationsdaten', en: 'Identification Data' },
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
id: 'CONTACT',
|
||||
name: { de: 'Kontaktdaten', en: 'Contact Data' },
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
id: 'FINANCIAL',
|
||||
name: { de: 'Finanzdaten', en: 'Financial Data' },
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
id: 'PROFESSIONAL',
|
||||
name: { de: 'Berufliche Daten', en: 'Professional Data' },
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
id: 'LOCATION',
|
||||
name: { de: 'Standortdaten', en: 'Location Data' },
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
id: 'BEHAVIORAL',
|
||||
name: { de: 'Verhaltensdaten', en: 'Behavioral Data' },
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
id: 'BIOMETRIC',
|
||||
name: { de: 'Biometrische Daten', en: 'Biometric Data' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 9 Abs. 1',
|
||||
},
|
||||
{
|
||||
id: 'HEALTH',
|
||||
name: { de: 'Gesundheitsdaten', en: 'Health Data' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 9 Abs. 1',
|
||||
},
|
||||
{
|
||||
id: 'GENETIC',
|
||||
name: { de: 'Genetische Daten', en: 'Genetic Data' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 9 Abs. 1',
|
||||
},
|
||||
{
|
||||
id: 'POLITICAL',
|
||||
name: { de: 'Politische Meinungen', en: 'Political Opinions' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 9 Abs. 1',
|
||||
},
|
||||
{
|
||||
id: 'RELIGIOUS',
|
||||
name: { de: 'Religiöse Überzeugungen', en: 'Religious Beliefs' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 9 Abs. 1',
|
||||
},
|
||||
{
|
||||
id: 'SEXUAL_ORIENTATION',
|
||||
name: { de: 'Sexuelle Orientierung', en: 'Sexual Orientation' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 9 Abs. 1',
|
||||
},
|
||||
{
|
||||
id: 'CRIMINAL',
|
||||
name: { de: 'Strafrechtliche Daten', en: 'Criminal Data' },
|
||||
isSpecialCategory: true,
|
||||
gdprReference: 'Art. 10',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// DATA SUBJECT METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface DataSubjectMetadata {
|
||||
id: DataSubject
|
||||
name: LocalizedString
|
||||
isVulnerable: boolean
|
||||
}
|
||||
|
||||
export const DATA_SUBJECTS_METADATA: DataSubjectMetadata[] = [
|
||||
{
|
||||
id: 'EMPLOYEES',
|
||||
name: { de: 'Mitarbeiter', en: 'Employees' },
|
||||
isVulnerable: false,
|
||||
},
|
||||
{
|
||||
id: 'CUSTOMERS',
|
||||
name: { de: 'Kunden', en: 'Customers' },
|
||||
isVulnerable: false,
|
||||
},
|
||||
{
|
||||
id: 'PROSPECTS',
|
||||
name: { de: 'Interessenten', en: 'Prospects' },
|
||||
isVulnerable: false,
|
||||
},
|
||||
{
|
||||
id: 'SUPPLIERS',
|
||||
name: { de: 'Lieferanten', en: 'Suppliers' },
|
||||
isVulnerable: false,
|
||||
},
|
||||
{
|
||||
id: 'MINORS',
|
||||
name: { de: 'Minderjährige', en: 'Minors' },
|
||||
isVulnerable: true,
|
||||
},
|
||||
{
|
||||
id: 'PATIENTS',
|
||||
name: { de: 'Patienten', en: 'Patients' },
|
||||
isVulnerable: true,
|
||||
},
|
||||
{
|
||||
id: 'STUDENTS',
|
||||
name: { de: 'Schüler/Studenten', en: 'Students' },
|
||||
isVulnerable: false,
|
||||
},
|
||||
{
|
||||
id: 'GENERAL_PUBLIC',
|
||||
name: { de: 'Allgemeine Öffentlichkeit', en: 'General Public' },
|
||||
isVulnerable: false,
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function getStepByIndex(index: number): StepConfig | undefined {
|
||||
return TOM_GENERATOR_STEPS[index]
|
||||
}
|
||||
|
||||
export function getStepById(id: TOMGeneratorStepId): StepConfig | undefined {
|
||||
return TOM_GENERATOR_STEPS.find((step) => step.id === id)
|
||||
}
|
||||
|
||||
export function getStepIndex(id: TOMGeneratorStepId): number {
|
||||
return TOM_GENERATOR_STEPS.findIndex((step) => step.id === id)
|
||||
}
|
||||
|
||||
export function getNextStep(
|
||||
currentId: TOMGeneratorStepId
|
||||
): StepConfig | undefined {
|
||||
const currentIndex = getStepIndex(currentId)
|
||||
return TOM_GENERATOR_STEPS[currentIndex + 1]
|
||||
}
|
||||
|
||||
export function getPreviousStep(
|
||||
currentId: TOMGeneratorStepId
|
||||
): StepConfig | undefined {
|
||||
const currentIndex = getStepIndex(currentId)
|
||||
return currentIndex > 0 ? TOM_GENERATOR_STEPS[currentIndex - 1] : undefined
|
||||
}
|
||||
|
||||
export function isSpecialCategory(category: DataCategory): boolean {
|
||||
const meta = DATA_CATEGORIES_METADATA.find((c) => c.id === category)
|
||||
return meta?.isSpecialCategory ?? false
|
||||
}
|
||||
|
||||
export function hasSpecialCategories(categories: DataCategory[]): boolean {
|
||||
return categories.some(isSpecialCategory)
|
||||
}
|
||||
|
||||
export function isVulnerableSubject(subject: DataSubject): boolean {
|
||||
const meta = DATA_SUBJECTS_METADATA.find((s) => s.id === subject)
|
||||
return meta?.isVulnerable ?? false
|
||||
}
|
||||
|
||||
export function hasVulnerableSubjects(subjects: DataSubject[]): boolean {
|
||||
return subjects.some(isVulnerableSubject)
|
||||
}
|
||||
|
||||
export function calculateProtectionLevel(
|
||||
ciaAssessment: CIAAssessment
|
||||
): ProtectionLevel {
|
||||
const maxRating = Math.max(
|
||||
ciaAssessment.confidentiality,
|
||||
ciaAssessment.integrity,
|
||||
ciaAssessment.availability
|
||||
)
|
||||
|
||||
if (maxRating >= 4) return 'VERY_HIGH'
|
||||
if (maxRating >= 3) return 'HIGH'
|
||||
return 'NORMAL'
|
||||
}
|
||||
|
||||
export function isDSFARequired(
|
||||
dataProfile: DataProfile | null,
|
||||
riskProfile: RiskProfile | null
|
||||
): boolean {
|
||||
if (!dataProfile) return false
|
||||
|
||||
// DSFA required if:
|
||||
// 1. Special categories are processed
|
||||
if (dataProfile.hasSpecialCategories) return true
|
||||
|
||||
// 2. Minors data is processed
|
||||
if (dataProfile.processesMinors) return true
|
||||
|
||||
// 3. Large scale processing
|
||||
if (dataProfile.dataVolume === 'VERY_HIGH') return true
|
||||
|
||||
// 4. High risk processing indicated
|
||||
if (riskProfile?.hasHighRiskProcessing) return true
|
||||
|
||||
// 5. Very high protection level
|
||||
if (riskProfile?.protectionLevel === 'VERY_HIGH') return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE FACTORY
|
||||
// =============================================================================
|
||||
|
||||
export function createInitialTOMGeneratorState(
|
||||
tenantId: string
|
||||
): TOMGeneratorState {
|
||||
const now = new Date()
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
tenantId,
|
||||
companyProfile: null,
|
||||
dataProfile: null,
|
||||
architectureProfile: null,
|
||||
securityProfile: null,
|
||||
riskProfile: null,
|
||||
currentStep: 'scope-roles',
|
||||
steps: TOM_GENERATOR_STEPS.map((step) => ({
|
||||
id: step.id,
|
||||
completed: false,
|
||||
data: null,
|
||||
validatedAt: null,
|
||||
})),
|
||||
documents: [],
|
||||
derivedTOMs: [],
|
||||
gapAnalysis: null,
|
||||
exports: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for createInitialTOMGeneratorState (for API compatibility)
|
||||
*/
|
||||
export const createEmptyTOMGeneratorState = createInitialTOMGeneratorState
|
||||
1785
admin-v2/lib/sdk/types.ts
Normal file
1785
admin-v2/lib/sdk/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
9
admin-v2/lib/sdk/vendor-compliance/catalog/index.ts
Normal file
9
admin-v2/lib/sdk/vendor-compliance/catalog/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Catalog exports
|
||||
*
|
||||
* Pre-defined templates, categories, and reference data
|
||||
*/
|
||||
|
||||
export * from './processing-activities'
|
||||
export * from './vendor-templates'
|
||||
export * from './legal-basis'
|
||||
562
admin-v2/lib/sdk/vendor-compliance/catalog/legal-basis.ts
Normal file
562
admin-v2/lib/sdk/vendor-compliance/catalog/legal-basis.ts
Normal file
@@ -0,0 +1,562 @@
|
||||
/**
|
||||
* Legal Basis Catalog
|
||||
*
|
||||
* Comprehensive information about GDPR legal bases (Art. 6, 9, 10)
|
||||
*/
|
||||
|
||||
import { LegalBasisType, LocalizedText, PersonalDataCategory } from '../types'
|
||||
|
||||
export interface LegalBasisInfo {
|
||||
type: LegalBasisType
|
||||
article: string
|
||||
name: LocalizedText
|
||||
shortName: LocalizedText
|
||||
description: LocalizedText
|
||||
requirements: LocalizedText[]
|
||||
suitableFor: string[]
|
||||
notSuitableFor: string[]
|
||||
documentationNeeded: LocalizedText[]
|
||||
isSpecialCategory: boolean
|
||||
notes?: LocalizedText
|
||||
}
|
||||
|
||||
export interface RetentionPeriodInfo {
|
||||
id: string
|
||||
name: LocalizedText
|
||||
legalBasis: string
|
||||
duration: {
|
||||
value: number
|
||||
unit: 'DAYS' | 'MONTHS' | 'YEARS'
|
||||
}
|
||||
description: LocalizedText
|
||||
applicableTo: string[]
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// LEGAL BASIS INFORMATION (Art. 6 DSGVO)
|
||||
// ==========================================
|
||||
|
||||
export const LEGAL_BASIS_INFO: LegalBasisInfo[] = [
|
||||
// Art. 6 Abs. 1 DSGVO - Standard legal bases
|
||||
{
|
||||
type: 'CONSENT',
|
||||
article: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
name: { de: 'Einwilligung', en: 'Consent' },
|
||||
shortName: { de: 'Einwilligung', en: 'Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat ihre Einwilligung zu der Verarbeitung der sie betreffenden personenbezogenen Daten für einen oder mehrere bestimmte Zwecke gegeben.',
|
||||
en: 'The data subject has given consent to the processing of his or her personal data for one or more specific purposes.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Freiwillig erteilt', en: 'Freely given' },
|
||||
{ de: 'Für bestimmten Zweck', en: 'For a specific purpose' },
|
||||
{ de: 'Informiert', en: 'Informed' },
|
||||
{ de: 'Unmissverständlich', en: 'Unambiguous' },
|
||||
{ de: 'Jederzeit widerrufbar', en: 'Revocable at any time' },
|
||||
{ de: 'Nachweis muss möglich sein', en: 'Must be demonstrable' },
|
||||
],
|
||||
suitableFor: ['Newsletter', 'Marketing', 'Cookies', 'Tracking', 'Fotos/Videos'],
|
||||
notSuitableFor: ['Vertragsdurchführung', 'Gesetzliche Pflichten', 'Arbeitsverhältnis'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Einwilligungstext', en: 'Consent text' },
|
||||
{ de: 'Zeitpunkt der Einwilligung', en: 'Time of consent' },
|
||||
{ de: 'Art der Erteilung (Opt-in)', en: 'Method of consent (opt-in)' },
|
||||
{ de: 'Widerrufsbelehrung', en: 'Information about withdrawal' },
|
||||
],
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
type: 'CONTRACT',
|
||||
article: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||
name: { de: 'Vertragserfüllung', en: 'Contract Performance' },
|
||||
shortName: { de: 'Vertrag', en: 'Contract' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist für die Erfüllung eines Vertrags, dessen Vertragspartei die betroffene Person ist, oder zur Durchführung vorvertraglicher Maßnahmen erforderlich.',
|
||||
en: 'Processing is necessary for the performance of a contract to which the data subject is party or for pre-contractual measures.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Vertrag besteht oder wird angebahnt', en: 'Contract exists or is being initiated' },
|
||||
{ de: 'Verarbeitung ist für Erfüllung erforderlich', en: 'Processing is necessary for performance' },
|
||||
{ de: 'Betroffene Person ist Vertragspartei', en: 'Data subject is a party to the contract' },
|
||||
],
|
||||
suitableFor: ['Kundendaten', 'Bestellabwicklung', 'Lieferung', 'Rechnungsstellung', 'Kundenservice'],
|
||||
notSuitableFor: ['Marketing', 'Profiling', 'Weitergabe an Dritte ohne Vertragsbezug'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Vertrag oder AGB', en: 'Contract or T&C' },
|
||||
{ de: 'Zusammenhang zur Verarbeitung', en: 'Connection to processing' },
|
||||
],
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
type: 'LEGAL_OBLIGATION',
|
||||
article: 'Art. 6 Abs. 1 lit. c DSGVO',
|
||||
name: { de: 'Rechtliche Verpflichtung', en: 'Legal Obligation' },
|
||||
shortName: { de: 'Gesetz', en: 'Legal' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Erfüllung einer rechtlichen Verpflichtung erforderlich, der der Verantwortliche unterliegt.',
|
||||
en: 'Processing is necessary for compliance with a legal obligation to which the controller is subject.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Rechtliche Verpflichtung im EU/nationalen Recht', en: 'Legal obligation in EU/national law' },
|
||||
{ de: 'Verarbeitung ist zur Erfüllung erforderlich', en: 'Processing is necessary for compliance' },
|
||||
{ de: 'Konkrete Rechtsgrundlage benennen', en: 'Cite specific legal basis' },
|
||||
],
|
||||
suitableFor: ['Steuerliche Aufbewahrung', 'Sozialversicherung', 'AML/KYC', 'Meldepflichten'],
|
||||
notSuitableFor: ['Freiwillige Maßnahmen', 'Marketing'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Konkrete Rechtsvorschrift', en: 'Specific legal provision' },
|
||||
{ de: 'HGB, AO, SGB, etc.', en: 'Commercial code, tax code, etc.' },
|
||||
],
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
type: 'VITAL_INTEREST',
|
||||
article: 'Art. 6 Abs. 1 lit. d DSGVO',
|
||||
name: { de: 'Lebenswichtige Interessen', en: 'Vital Interests' },
|
||||
shortName: { de: 'Vital', en: 'Vital' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich, um lebenswichtige Interessen der betroffenen Person oder einer anderen natürlichen Person zu schützen.',
|
||||
en: 'Processing is necessary to protect the vital interests of the data subject or of another natural person.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Gefahr für Leben oder Gesundheit', en: 'Danger to life or health' },
|
||||
{ de: 'Keine andere Rechtsgrundlage möglich', en: 'No other legal basis possible' },
|
||||
{ de: 'Subsidiär zu anderen Rechtsgrundlagen', en: 'Subsidiary to other legal bases' },
|
||||
],
|
||||
suitableFor: ['Notfall', 'Medizinische Notversorgung', 'Katastrophenschutz'],
|
||||
notSuitableFor: ['Regelmäßige Verarbeitung', 'Vorsorgemaßnahmen'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Dokumentation des Notfalls', en: 'Documentation of emergency' },
|
||||
{ de: 'Begründung der Erforderlichkeit', en: 'Justification of necessity' },
|
||||
],
|
||||
isSpecialCategory: false,
|
||||
notes: {
|
||||
de: 'Sollte nur in Ausnahmefällen verwendet werden, wenn keine andere Rechtsgrundlage greift.',
|
||||
en: 'Should only be used in exceptional cases when no other legal basis applies.',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'PUBLIC_TASK',
|
||||
article: 'Art. 6 Abs. 1 lit. e DSGVO',
|
||||
name: { de: 'Öffentliche Aufgabe', en: 'Public Task' },
|
||||
shortName: { de: 'Öffentlich', en: 'Public' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist für die Wahrnehmung einer Aufgabe erforderlich, die im öffentlichen Interesse liegt oder in Ausübung öffentlicher Gewalt erfolgt.',
|
||||
en: 'Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Öffentliches Interesse oder hoheitliche Aufgabe', en: 'Public interest or official authority' },
|
||||
{ de: 'Rechtsgrundlage im EU/nationalen Recht', en: 'Legal basis in EU/national law' },
|
||||
],
|
||||
suitableFor: ['Behörden', 'Öffentlich-rechtliche Einrichtungen', 'Bildungseinrichtungen'],
|
||||
notSuitableFor: ['Private Unternehmen (in der Regel)'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Rechtsgrundlage für öffentliche Aufgabe', en: 'Legal basis for public task' },
|
||||
{ de: 'Zusammenhang zur Aufgabe', en: 'Connection to task' },
|
||||
],
|
||||
isSpecialCategory: false,
|
||||
},
|
||||
{
|
||||
type: 'LEGITIMATE_INTEREST',
|
||||
article: 'Art. 6 Abs. 1 lit. f DSGVO',
|
||||
name: { de: 'Berechtigtes Interesse', en: 'Legitimate Interest' },
|
||||
shortName: { de: 'Ber. Interesse', en: 'Leg. Interest' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Wahrung der berechtigten Interessen des Verantwortlichen oder eines Dritten erforderlich, sofern nicht die Interessen oder Grundrechte der betroffenen Person überwiegen.',
|
||||
en: 'Processing is necessary for the legitimate interests pursued by the controller or a third party, except where such interests are overridden by the interests or rights of the data subject.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Berechtigtes Interesse identifizieren', en: 'Identify legitimate interest' },
|
||||
{ de: 'Erforderlichkeit prüfen', en: 'Check necessity' },
|
||||
{ de: 'Interessenabwägung durchführen', en: 'Conduct balancing test' },
|
||||
{ de: 'Dokumentieren', en: 'Document' },
|
||||
],
|
||||
suitableFor: ['B2B-Marketing', 'IT-Sicherheit', 'Betrugsprävention', 'Konzerninterner Datenaustausch'],
|
||||
notSuitableFor: ['Behörden', 'Verarbeitung sensibler Daten', 'Wenn Einwilligung verweigert wurde'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Interessenabwägung (LIA)', en: 'Legitimate Interest Assessment (LIA)' },
|
||||
{ de: 'Konkrete Interessen', en: 'Specific interests' },
|
||||
{ de: 'Abwägung der Betroffenenrechte', en: 'Balancing of data subject rights' },
|
||||
],
|
||||
isSpecialCategory: false,
|
||||
notes: {
|
||||
de: 'Nicht anwendbar für Behörden bei Aufgabenerfüllung. Interessenabwägung (LIA) erforderlich.',
|
||||
en: 'Not applicable for public authorities performing their tasks. Legitimate Interest Assessment (LIA) required.',
|
||||
},
|
||||
},
|
||||
|
||||
// Art. 9 Abs. 2 DSGVO - Special categories
|
||||
{
|
||||
type: 'ART9_CONSENT',
|
||||
article: 'Art. 9 Abs. 2 lit. a DSGVO',
|
||||
name: { de: 'Ausdrückliche Einwilligung', en: 'Explicit Consent' },
|
||||
shortName: { de: 'Ausd. Einwilligung', en: 'Explicit Consent' },
|
||||
description: {
|
||||
de: 'Die betroffene Person hat in die Verarbeitung der besonderen Kategorien personenbezogener Daten ausdrücklich eingewilligt.',
|
||||
en: 'The data subject has given explicit consent to the processing of special categories of personal data.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Alle Anforderungen der normalen Einwilligung', en: 'All requirements of normal consent' },
|
||||
{ de: 'Zusätzlich: Ausdrücklich', en: 'Additionally: Explicit' },
|
||||
{ de: 'Besonderer Hinweis auf sensible Daten', en: 'Special notice about sensitive data' },
|
||||
],
|
||||
suitableFor: ['Gesundheitsdaten mit Einwilligung', 'Religiöse Daten mit Einwilligung'],
|
||||
notSuitableFor: ['Arbeitsverhältnis (in der Regel)', 'Wenn nationales Recht es verbietet'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Einwilligungstext mit Hinweis auf sensible Daten', en: 'Consent text with reference to sensitive data' },
|
||||
{ de: 'Nachweis der ausdrücklichen Erteilung', en: 'Proof of explicit consent' },
|
||||
],
|
||||
isSpecialCategory: true,
|
||||
},
|
||||
{
|
||||
type: 'ART9_EMPLOYMENT',
|
||||
article: 'Art. 9 Abs. 2 lit. b DSGVO',
|
||||
name: { de: 'Arbeitsrecht', en: 'Employment Law' },
|
||||
shortName: { de: 'Arbeitsrecht', en: 'Employment' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist erforderlich für arbeitsrechtliche Zwecke auf Grundlage von nationalen Rechtsvorschriften.',
|
||||
en: 'Processing is necessary for employment law purposes based on national law provisions.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Arbeitsrechtliche Grundlage (z.B. § 26 BDSG)', en: 'Employment law basis (e.g., § 26 BDSG)' },
|
||||
{ de: 'Erforderlichkeit für Beschäftigung', en: 'Necessity for employment' },
|
||||
{ de: 'Angemessene Garantien', en: 'Appropriate safeguards' },
|
||||
],
|
||||
suitableFor: ['Gesundheitsdaten im Arbeitsverhältnis', 'Schwerbehinderung', 'Gewerkschaftszugehörigkeit'],
|
||||
notSuitableFor: ['Verarbeitung über das Erforderliche hinaus'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Rechtsgrundlage (§ 26 BDSG)', en: 'Legal basis (§ 26 BDSG)' },
|
||||
{ de: 'Erforderlichkeit dokumentieren', en: 'Document necessity' },
|
||||
],
|
||||
isSpecialCategory: true,
|
||||
},
|
||||
{
|
||||
type: 'ART9_VITAL_INTEREST',
|
||||
article: 'Art. 9 Abs. 2 lit. c DSGVO',
|
||||
name: { de: 'Lebenswichtige Interessen (Art. 9)', en: 'Vital Interests (Art. 9)' },
|
||||
shortName: { de: 'Vital (Art. 9)', en: 'Vital (Art. 9)' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zum Schutz lebenswichtiger Interessen erforderlich und die betroffene Person ist nicht einwilligungsfähig.',
|
||||
en: 'Processing is necessary to protect vital interests and the data subject is incapable of giving consent.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Schutz lebenswichtiger Interessen', en: 'Protection of vital interests' },
|
||||
{ de: 'Betroffene Person nicht einwilligungsfähig', en: 'Data subject incapable of consent' },
|
||||
],
|
||||
suitableFor: ['Medizinische Notfälle', 'Bewusstlose Personen'],
|
||||
notSuitableFor: ['Regelmäßige Verarbeitung'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Dokumentation des Notfalls', en: 'Documentation of emergency' },
|
||||
{ de: 'Nachweis der fehlenden Einwilligungsfähigkeit', en: 'Proof of incapacity to consent' },
|
||||
],
|
||||
isSpecialCategory: true,
|
||||
},
|
||||
{
|
||||
type: 'ART9_HEALTH',
|
||||
article: 'Art. 9 Abs. 2 lit. h DSGVO',
|
||||
name: { de: 'Gesundheitsversorgung', en: 'Health Care' },
|
||||
shortName: { de: 'Gesundheit', en: 'Health' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist für Zwecke der Gesundheitsvorsorge oder Arbeitsmedizin erforderlich, auf Grundlage von EU- oder nationalem Recht.',
|
||||
en: 'Processing is necessary for health care purposes or occupational medicine based on EU or national law.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Gesundheitsvorsorge, Arbeitsmedizin', en: 'Health care, occupational medicine' },
|
||||
{ de: 'Rechtsgrundlage im EU/nationalen Recht', en: 'Legal basis in EU/national law' },
|
||||
{ de: 'Verarbeitung durch Fachpersonal', en: 'Processing by health professionals' },
|
||||
{ de: 'Berufsgeheimnis beachten', en: 'Professional secrecy' },
|
||||
],
|
||||
suitableFor: ['Medizinische Behandlung', 'Betriebsärztliche Untersuchungen', 'Gesundheitsmanagement'],
|
||||
notSuitableFor: ['Verarbeitung ohne medizinischen Kontext'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Rechtsgrundlage', en: 'Legal basis' },
|
||||
{ de: 'Fachliche Zuständigkeit', en: 'Professional competence' },
|
||||
],
|
||||
isSpecialCategory: true,
|
||||
},
|
||||
{
|
||||
type: 'ART9_PUBLIC_HEALTH',
|
||||
article: 'Art. 9 Abs. 2 lit. i DSGVO',
|
||||
name: { de: 'Öffentliche Gesundheit', en: 'Public Health' },
|
||||
shortName: { de: 'Öff. Gesundheit', en: 'Public Health' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist aus Gründen des öffentlichen Interesses im Bereich der öffentlichen Gesundheit erforderlich.',
|
||||
en: 'Processing is necessary for reasons of public interest in the area of public health.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Öffentliches Interesse an öffentlicher Gesundheit', en: 'Public interest in public health' },
|
||||
{ de: 'Rechtsgrundlage im EU/nationalen Recht', en: 'Legal basis in EU/national law' },
|
||||
{ de: 'Angemessene Garantien', en: 'Appropriate safeguards' },
|
||||
],
|
||||
suitableFor: ['Pandemiebekämpfung', 'Seuchenprävention', 'Qualitätssicherung im Gesundheitswesen'],
|
||||
notSuitableFor: ['Private Interessen'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Rechtsgrundlage', en: 'Legal basis' },
|
||||
{ de: 'Nachweis öffentliches Interesse', en: 'Proof of public interest' },
|
||||
],
|
||||
isSpecialCategory: true,
|
||||
},
|
||||
{
|
||||
type: 'ART9_LEGAL_CLAIMS',
|
||||
article: 'Art. 9 Abs. 2 lit. f DSGVO',
|
||||
name: { de: 'Rechtsansprüche', en: 'Legal Claims' },
|
||||
shortName: { de: 'Rechtsansprüche', en: 'Legal Claims' },
|
||||
description: {
|
||||
de: 'Die Verarbeitung ist zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen erforderlich.',
|
||||
en: 'Processing is necessary for the establishment, exercise or defence of legal claims.',
|
||||
},
|
||||
requirements: [
|
||||
{ de: 'Rechtsansprüche bestehen oder drohen', en: 'Legal claims exist or are anticipated' },
|
||||
{ de: 'Verarbeitung ist erforderlich', en: 'Processing is necessary' },
|
||||
],
|
||||
suitableFor: ['Rechtsstreitigkeiten', 'Compliance-Untersuchungen', 'Interne Ermittlungen'],
|
||||
notSuitableFor: ['Präventive Maßnahmen ohne konkreten Anlass'],
|
||||
documentationNeeded: [
|
||||
{ de: 'Dokumentation des Rechtsstreits/Anspruchs', en: 'Documentation of legal dispute/claim' },
|
||||
{ de: 'Erforderlichkeit der Verarbeitung', en: 'Necessity of processing' },
|
||||
],
|
||||
isSpecialCategory: true,
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// RETENTION PERIODS
|
||||
// ==========================================
|
||||
|
||||
export const STANDARD_RETENTION_PERIODS: RetentionPeriodInfo[] = [
|
||||
// Handelsrechtliche Aufbewahrung
|
||||
{
|
||||
id: 'hgb-257',
|
||||
name: { de: 'Handelsbücher und Buchungsbelege', en: 'Commercial Books and Vouchers' },
|
||||
legalBasis: '§ 257 HGB',
|
||||
duration: { value: 10, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Handelsbücher, Inventare, Eröffnungsbilanzen, Jahresabschlüsse, Lageberichte, Konzernabschlüsse, Buchungsbelege',
|
||||
en: 'Commercial books, inventories, opening balance sheets, annual financial statements, management reports, consolidated financial statements, accounting vouchers',
|
||||
},
|
||||
applicableTo: ['Buchhaltung', 'Jahresabschlüsse', 'Rechnungen', 'Verträge'],
|
||||
},
|
||||
{
|
||||
id: 'hgb-257-6',
|
||||
name: { de: 'Handels- und Geschäftsbriefe', en: 'Commercial and Business Correspondence' },
|
||||
legalBasis: '§ 257 Abs. 1 Nr. 2, 3 HGB',
|
||||
duration: { value: 6, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Empfangene Handels- und Geschäftsbriefe, Wiedergaben der abgesandten Handels- und Geschäftsbriefe',
|
||||
en: 'Received commercial and business correspondence, copies of sent correspondence',
|
||||
},
|
||||
applicableTo: ['Geschäftskorrespondenz', 'Angebote', 'Auftragsbestätigungen'],
|
||||
},
|
||||
// Steuerrechtliche Aufbewahrung
|
||||
{
|
||||
id: 'ao-147',
|
||||
name: { de: 'Steuerrechtliche Unterlagen', en: 'Tax Documents' },
|
||||
legalBasis: '§ 147 AO',
|
||||
duration: { value: 10, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Bücher und Aufzeichnungen, Inventare, Jahresabschlüsse, Buchungsbelege, steuerrelevante Unterlagen',
|
||||
en: 'Books and records, inventories, annual financial statements, accounting vouchers, tax-relevant documents',
|
||||
},
|
||||
applicableTo: ['Steuererklärungen', 'Buchhaltung', 'Belege'],
|
||||
},
|
||||
// Arbeitsrechtliche Aufbewahrung
|
||||
{
|
||||
id: 'arbeitsrecht-personal',
|
||||
name: { de: 'Personalunterlagen', en: 'Personnel Records' },
|
||||
legalBasis: 'Verschiedene (AGG, ArbZG, etc.)',
|
||||
duration: { value: 3, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Personalakte nach Beendigung des Arbeitsverhältnisses (Regelverjährung)',
|
||||
en: 'Personnel file after termination of employment (standard limitation period)',
|
||||
},
|
||||
applicableTo: ['Personalakten', 'Arbeitsverträge', 'Zeugnisse'],
|
||||
},
|
||||
{
|
||||
id: 'arbzg',
|
||||
name: { de: 'Arbeitszeitaufzeichnungen', en: 'Working Time Records' },
|
||||
legalBasis: '§ 16 Abs. 2 ArbZG',
|
||||
duration: { value: 2, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Aufzeichnungen über Arbeitszeiten, die über 8 Stunden hinausgehen',
|
||||
en: 'Records of working hours exceeding 8 hours',
|
||||
},
|
||||
applicableTo: ['Zeiterfassung', 'Überstunden'],
|
||||
},
|
||||
{
|
||||
id: 'lohnsteuer',
|
||||
name: { de: 'Lohnunterlagen', en: 'Payroll Documents' },
|
||||
legalBasis: '§ 41 EStG, § 28f SGB IV',
|
||||
duration: { value: 6, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Lohnkonten und Unterlagen für den Lohnsteuerabzug',
|
||||
en: 'Payroll accounts and documents for wage tax deduction',
|
||||
},
|
||||
applicableTo: ['Lohnabrechnungen', 'Lohnsteuerbescheinigungen'],
|
||||
},
|
||||
{
|
||||
id: 'sozialversicherung',
|
||||
name: { de: 'Sozialversicherungsunterlagen', en: 'Social Security Documents' },
|
||||
legalBasis: '§ 28f SGB IV',
|
||||
duration: { value: 5, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Unterlagen zum Gesamtsozialversicherungsbeitrag',
|
||||
en: 'Documents for total social security contributions',
|
||||
},
|
||||
applicableTo: ['Sozialversicherungsmeldungen', 'Beitragsnachweise'],
|
||||
},
|
||||
// Bewerberdaten
|
||||
{
|
||||
id: 'bewerbung',
|
||||
name: { de: 'Bewerbungsunterlagen', en: 'Application Documents' },
|
||||
legalBasis: '§ 15 Abs. 4 AGG',
|
||||
duration: { value: 6, unit: 'MONTHS' },
|
||||
description: {
|
||||
de: 'Bewerbungsunterlagen nach Absage (AGG-Frist)',
|
||||
en: 'Application documents after rejection (AGG deadline)',
|
||||
},
|
||||
applicableTo: ['Bewerbungen', 'Lebensläufe', 'Zeugnisse von Bewerbern'],
|
||||
},
|
||||
// Datenschutzrechtliche Fristen
|
||||
{
|
||||
id: 'einwilligung',
|
||||
name: { de: 'Einwilligungen', en: 'Consents' },
|
||||
legalBasis: 'Art. 7 Abs. 1 DSGVO',
|
||||
duration: { value: 3, unit: 'YEARS' },
|
||||
description: {
|
||||
de: 'Dokumentation der Einwilligung (Regelverjährung)',
|
||||
en: 'Documentation of consent (standard limitation period)',
|
||||
},
|
||||
applicableTo: ['Einwilligungsnachweise', 'Opt-in-Dokumentation'],
|
||||
},
|
||||
{
|
||||
id: 'videoüberwachung',
|
||||
name: { de: 'Videoüberwachung', en: 'Video Surveillance' },
|
||||
legalBasis: 'Verhältnismäßigkeit',
|
||||
duration: { value: 72, unit: 'DAYS' },
|
||||
description: {
|
||||
de: 'Videoaufnahmen (max. 72 Stunden, sofern kein Vorfall)',
|
||||
en: 'Video recordings (max. 72 hours, unless incident occurred)',
|
||||
},
|
||||
applicableTo: ['CCTV-Aufnahmen', 'Überwachungsvideos'],
|
||||
},
|
||||
// Löschung nach Vertrag
|
||||
{
|
||||
id: 'avv-loeschung',
|
||||
name: { de: 'AVV-Daten nach Vertragsende', en: 'DPA Data after Contract End' },
|
||||
legalBasis: 'Art. 28 Abs. 3 lit. g DSGVO',
|
||||
duration: { value: 30, unit: 'DAYS' },
|
||||
description: {
|
||||
de: 'Löschung oder Rückgabe aller personenbezogenen Daten nach Vertragsende',
|
||||
en: 'Deletion or return of all personal data after contract end',
|
||||
},
|
||||
applicableTo: ['Auftragsverarbeitung', 'Dienstleister-Daten'],
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get legal basis info by type
|
||||
*/
|
||||
export function getLegalBasisInfo(type: LegalBasisType): LegalBasisInfo | undefined {
|
||||
return LEGAL_BASIS_INFO.find((lb) => lb.type === type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legal bases for standard data (non-special categories)
|
||||
*/
|
||||
export function getStandardLegalBases(): LegalBasisInfo[] {
|
||||
return LEGAL_BASIS_INFO.filter((lb) => !lb.isSpecialCategory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legal bases for special category data (Art. 9)
|
||||
*/
|
||||
export function getSpecialCategoryLegalBases(): LegalBasisInfo[] {
|
||||
return LEGAL_BASIS_INFO.filter((lb) => lb.isSpecialCategory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate legal bases for data categories
|
||||
*/
|
||||
export function getAppropriateLegalBases(
|
||||
dataCategories: PersonalDataCategory[]
|
||||
): LegalBasisInfo[] {
|
||||
const hasSpecialCategory = dataCategories.some((cat) =>
|
||||
[
|
||||
'HEALTH_DATA', 'GENETIC_DATA', 'BIOMETRIC_DATA', 'RACIAL_ETHNIC',
|
||||
'POLITICAL_OPINIONS', 'RELIGIOUS_BELIEFS', 'TRADE_UNION', 'SEX_LIFE',
|
||||
].includes(cat)
|
||||
)
|
||||
|
||||
if (hasSpecialCategory) {
|
||||
// Return Art. 9 bases plus compatible Art. 6 bases
|
||||
return [
|
||||
...getSpecialCategoryLegalBases(),
|
||||
...getStandardLegalBases().filter((lb) =>
|
||||
['LEGAL_OBLIGATION', 'VITAL_INTEREST', 'PUBLIC_TASK'].includes(lb.type)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
return getStandardLegalBases()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get retention period by ID
|
||||
*/
|
||||
export function getRetentionPeriod(id: string): RetentionPeriodInfo | undefined {
|
||||
return STANDARD_RETENTION_PERIODS.find((rp) => rp.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get retention periods applicable to a category
|
||||
*/
|
||||
export function getRetentionPeriodsForCategory(category: string): RetentionPeriodInfo[] {
|
||||
return STANDARD_RETENTION_PERIODS.filter((rp) =>
|
||||
rp.applicableTo.some((a) => a.toLowerCase().includes(category.toLowerCase()))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get longest applicable retention period
|
||||
*/
|
||||
export function getLongestRetentionPeriod(categories: string[]): RetentionPeriodInfo | undefined {
|
||||
const applicable = categories.flatMap((cat) => getRetentionPeriodsForCategory(cat))
|
||||
|
||||
if (applicable.length === 0) return undefined
|
||||
|
||||
return applicable.reduce((longest, current) => {
|
||||
const longestMonths = toMonths(longest.duration)
|
||||
const currentMonths = toMonths(current.duration)
|
||||
return currentMonths > longestMonths ? current : longest
|
||||
})
|
||||
}
|
||||
|
||||
function toMonths(duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' }): number {
|
||||
switch (duration.unit) {
|
||||
case 'DAYS':
|
||||
return duration.value / 30
|
||||
case 'MONTHS':
|
||||
return duration.value
|
||||
case 'YEARS':
|
||||
return duration.value * 12
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format retention period for display
|
||||
*/
|
||||
export function formatRetentionPeriod(
|
||||
duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' },
|
||||
locale: 'de' | 'en' = 'de'
|
||||
): string {
|
||||
const units = {
|
||||
de: { DAYS: 'Tage', MONTHS: 'Monate', YEARS: 'Jahre' },
|
||||
en: { DAYS: 'days', MONTHS: 'months', YEARS: 'years' },
|
||||
}
|
||||
|
||||
return `${duration.value} ${units[locale][duration.unit]}`
|
||||
}
|
||||
@@ -0,0 +1,813 @@
|
||||
/**
|
||||
* Standard Processing Activities Catalog
|
||||
*
|
||||
* 28 predefined processing activities templates following Art. 30 DSGVO
|
||||
*/
|
||||
|
||||
import {
|
||||
ProcessingActivityFormData,
|
||||
DataSubjectCategory,
|
||||
PersonalDataCategory,
|
||||
LegalBasisType,
|
||||
ProtectionLevel,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
|
||||
export interface ProcessingActivityTemplate {
|
||||
id: string
|
||||
category: ProcessingActivityCategory
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
purposes: LocalizedText[]
|
||||
dataSubjectCategories: DataSubjectCategory[]
|
||||
personalDataCategories: PersonalDataCategory[]
|
||||
suggestedLegalBasis: LegalBasisType[]
|
||||
suggestedRetentionYears: number
|
||||
suggestedProtectionLevel: ProtectionLevel
|
||||
dpiaLikely: boolean
|
||||
commonSystems: string[]
|
||||
commonVendorCategories: string[]
|
||||
}
|
||||
|
||||
export type ProcessingActivityCategory =
|
||||
| 'HR' // Human Resources
|
||||
| 'SALES' // Vertrieb
|
||||
| 'MARKETING' // Marketing
|
||||
| 'FINANCE' // Finanzen
|
||||
| 'IT' // IT & Sicherheit
|
||||
| 'CUSTOMER_SERVICE' // Kundenservice
|
||||
| 'WEBSITE' // Website & Apps
|
||||
| 'GENERAL' // Allgemein
|
||||
|
||||
export const PROCESSING_ACTIVITY_CATEGORY_META: Record<ProcessingActivityCategory, LocalizedText> = {
|
||||
HR: { de: 'Personal', en: 'Human Resources' },
|
||||
SALES: { de: 'Vertrieb', en: 'Sales' },
|
||||
MARKETING: { de: 'Marketing', en: 'Marketing' },
|
||||
FINANCE: { de: 'Finanzen', en: 'Finance' },
|
||||
IT: { de: 'IT & Sicherheit', en: 'IT & Security' },
|
||||
CUSTOMER_SERVICE: { de: 'Kundenservice', en: 'Customer Service' },
|
||||
WEBSITE: { de: 'Website & Apps', en: 'Website & Apps' },
|
||||
GENERAL: { de: 'Allgemein', en: 'General' },
|
||||
}
|
||||
|
||||
export const PROCESSING_ACTIVITY_TEMPLATES: ProcessingActivityTemplate[] = [
|
||||
// ==========================================
|
||||
// HR - Human Resources
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-hr-recruitment',
|
||||
category: 'HR',
|
||||
name: {
|
||||
de: 'Bewerbermanagement',
|
||||
en: 'Recruitment Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Verarbeitung von Bewerberdaten im Rahmen des Recruiting-Prozesses',
|
||||
en: 'Processing of applicant data as part of the recruitment process',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Durchführung des Bewerbungsverfahrens', en: 'Conducting the application process' },
|
||||
{ de: 'Prüfung der Eignung', en: 'Assessing suitability' },
|
||||
{ de: 'Aufbau eines Talentpools (bei Einwilligung)', en: 'Building a talent pool (with consent)' },
|
||||
],
|
||||
dataSubjectCategories: ['APPLICANTS'],
|
||||
personalDataCategories: [
|
||||
'NAME', 'CONTACT', 'ADDRESS', 'DOB', 'EDUCATION_DATA',
|
||||
'EMPLOYMENT_DATA', 'PHOTO_VIDEO',
|
||||
],
|
||||
suggestedLegalBasis: ['CONTRACT', 'CONSENT'],
|
||||
suggestedRetentionYears: 0.5, // 6 Monate nach Absage
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['E-Recruiting', 'Personio', 'Workday'],
|
||||
commonVendorCategories: ['HR_SOFTWARE', 'CLOUD_INFRASTRUCTURE'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-hr-personnel',
|
||||
category: 'HR',
|
||||
name: {
|
||||
de: 'Personalverwaltung',
|
||||
en: 'Personnel Administration',
|
||||
},
|
||||
description: {
|
||||
de: 'Führung der Personalakte und Verwaltung des Beschäftigungsverhältnisses',
|
||||
en: 'Maintaining personnel files and managing employment relationships',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Führung der Personalakte', en: 'Maintaining personnel files' },
|
||||
{ de: 'Durchführung des Arbeitsverhältnisses', en: 'Executing the employment relationship' },
|
||||
{ de: 'Erfüllung gesetzlicher Pflichten', en: 'Fulfilling legal obligations' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES'],
|
||||
personalDataCategories: [
|
||||
'NAME', 'CONTACT', 'ADDRESS', 'DOB', 'ID_NUMBER',
|
||||
'SOCIAL_SECURITY', 'TAX_ID', 'BANK_ACCOUNT', 'EMPLOYMENT_DATA',
|
||||
'SALARY_DATA', 'EDUCATION_DATA', 'PHOTO_VIDEO',
|
||||
],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 10, // Nach Beendigung
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['SAP HCM', 'Personio', 'DATEV'],
|
||||
commonVendorCategories: ['HR_SOFTWARE', 'ERP'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-hr-payroll',
|
||||
category: 'HR',
|
||||
name: {
|
||||
de: 'Lohn- und Gehaltsabrechnung',
|
||||
en: 'Payroll Processing',
|
||||
},
|
||||
description: {
|
||||
de: 'Berechnung und Auszahlung von Gehältern, Abführung von Steuern und Sozialabgaben',
|
||||
en: 'Calculation and payment of salaries, tax and social security contributions',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Gehaltsberechnung und -auszahlung', en: 'Salary calculation and payment' },
|
||||
{ de: 'Abführung von Lohnsteuer und Sozialabgaben', en: 'Payment of payroll taxes and social contributions' },
|
||||
{ de: 'Erstellung von Lohnabrechnungen', en: 'Creating payslips' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES'],
|
||||
personalDataCategories: [
|
||||
'NAME', 'ADDRESS', 'DOB', 'SOCIAL_SECURITY', 'TAX_ID',
|
||||
'BANK_ACCOUNT', 'SALARY_DATA', 'EMPLOYMENT_DATA',
|
||||
],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 10, // Handels- und Steuerrecht
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['DATEV', 'SAP', 'Lexware'],
|
||||
commonVendorCategories: ['ACCOUNTING', 'HR_SOFTWARE'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-hr-time-tracking',
|
||||
category: 'HR',
|
||||
name: {
|
||||
de: 'Arbeitszeiterfassung',
|
||||
en: 'Time Tracking',
|
||||
},
|
||||
description: {
|
||||
de: 'Erfassung der Arbeitszeiten zur Einhaltung des Arbeitszeitgesetzes',
|
||||
en: 'Recording working hours for compliance with working time regulations',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Erfassung der Arbeitszeiten', en: 'Recording working hours' },
|
||||
{ de: 'Einhaltung des Arbeitszeitgesetzes', en: 'Compliance with working time regulations' },
|
||||
{ de: 'Grundlage für Gehaltsabrechnung', en: 'Basis for payroll' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES'],
|
||||
personalDataCategories: ['NAME', 'EMPLOYMENT_DATA', 'USAGE_DATA'],
|
||||
suggestedLegalBasis: ['LEGAL_OBLIGATION', 'CONTRACT'],
|
||||
suggestedRetentionYears: 2,
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['ATOSS', 'Clockodo', 'Toggl'],
|
||||
commonVendorCategories: ['HR_SOFTWARE'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-hr-health-management',
|
||||
category: 'HR',
|
||||
name: {
|
||||
de: 'Betriebliches Gesundheitsmanagement',
|
||||
en: 'Occupational Health Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Arbeitsunfähigkeitsbescheinigungen und betriebsärztlichen Untersuchungen',
|
||||
en: 'Management of sick notes and occupational health examinations',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Verwaltung von Krankmeldungen', en: 'Managing sick leave' },
|
||||
{ de: 'Organisation betriebsärztlicher Untersuchungen', en: 'Organizing occupational health examinations' },
|
||||
{ de: 'Betriebliches Eingliederungsmanagement', en: 'Occupational reintegration management' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES'],
|
||||
personalDataCategories: ['NAME', 'EMPLOYMENT_DATA', 'HEALTH_DATA'],
|
||||
suggestedLegalBasis: ['ART9_EMPLOYMENT', 'LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 3,
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: true,
|
||||
commonSystems: ['HR-Software', 'BEM-System'],
|
||||
commonVendorCategories: ['HR_SOFTWARE', 'CONSULTING'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// SALES - Vertrieb
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-sales-crm',
|
||||
category: 'SALES',
|
||||
name: {
|
||||
de: 'Kundenbeziehungsmanagement (CRM)',
|
||||
en: 'Customer Relationship Management (CRM)',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Kundenbeziehungen, Kontakthistorie und Verkaufschancen',
|
||||
en: 'Managing customer relationships, contact history, and sales opportunities',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Pflege von Kundenbeziehungen', en: 'Maintaining customer relationships' },
|
||||
{ de: 'Dokumentation von Kundenkontakten', en: 'Documenting customer contacts' },
|
||||
{ de: 'Vertriebssteuerung', en: 'Sales management' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS', 'PROSPECTIVE_CUSTOMERS', 'BUSINESS_PARTNERS'],
|
||||
personalDataCategories: [
|
||||
'NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'COMMUNICATION_DATA',
|
||||
],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 3, // Nach letztem Kontakt
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Salesforce', 'HubSpot', 'Pipedrive', 'Microsoft Dynamics'],
|
||||
commonVendorCategories: ['CRM'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-sales-contract-management',
|
||||
category: 'SALES',
|
||||
name: {
|
||||
de: 'Vertragsmanagement',
|
||||
en: 'Contract Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Kundenverträgen, Angeboten und Aufträgen',
|
||||
en: 'Managing customer contracts, quotes, and orders',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Erstellung und Verwaltung von Verträgen', en: 'Creating and managing contracts' },
|
||||
{ de: 'Angebotsverfolgung', en: 'Quote tracking' },
|
||||
{ de: 'Auftragsabwicklung', en: 'Order processing' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS', 'BUSINESS_PARTNERS'],
|
||||
personalDataCategories: [
|
||||
'NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'PAYMENT_DATA',
|
||||
],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 10, // Handelsrechtlich
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['ERP', 'CRM', 'Vertragsverwaltung'],
|
||||
commonVendorCategories: ['ERP', 'CRM'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// MARKETING
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-marketing-newsletter',
|
||||
category: 'MARKETING',
|
||||
name: {
|
||||
de: 'Newsletter-Versand',
|
||||
en: 'Newsletter Distribution',
|
||||
},
|
||||
description: {
|
||||
de: 'Versand von E-Mail-Newslettern und Marketing-Kommunikation',
|
||||
en: 'Sending email newsletters and marketing communications',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Versand von Newsletter und Marketing-E-Mails', en: 'Sending newsletters and marketing emails' },
|
||||
{ de: 'Messung von Öffnungs- und Klickraten', en: 'Measuring open and click rates' },
|
||||
],
|
||||
dataSubjectCategories: ['NEWSLETTER_SUBSCRIBERS', 'CUSTOMERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
|
||||
suggestedLegalBasis: ['CONSENT'],
|
||||
suggestedRetentionYears: 0, // Bis Widerruf
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Mailchimp', 'CleverReach', 'Sendinblue'],
|
||||
commonVendorCategories: ['EMAIL', 'MARKETING'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-marketing-advertising',
|
||||
category: 'MARKETING',
|
||||
name: {
|
||||
de: 'Online-Werbung',
|
||||
en: 'Online Advertising',
|
||||
},
|
||||
description: {
|
||||
de: 'Schaltung und Auswertung von Online-Werbeanzeigen',
|
||||
en: 'Running and analyzing online advertisements',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Schaltung von Online-Werbung', en: 'Running online advertisements' },
|
||||
{ de: 'Conversion-Tracking', en: 'Conversion tracking' },
|
||||
{ de: 'Retargeting', en: 'Retargeting' },
|
||||
],
|
||||
dataSubjectCategories: ['WEBSITE_USERS'],
|
||||
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA'],
|
||||
suggestedLegalBasis: ['CONSENT'],
|
||||
suggestedRetentionYears: 1,
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: true,
|
||||
commonSystems: ['Google Ads', 'Meta Ads', 'LinkedIn Ads'],
|
||||
commonVendorCategories: ['MARKETING', 'ANALYTICS'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-marketing-events',
|
||||
category: 'MARKETING',
|
||||
name: {
|
||||
de: 'Veranstaltungsmanagement',
|
||||
en: 'Event Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Organisation und Durchführung von Veranstaltungen, Messen und Webinaren',
|
||||
en: 'Organizing and conducting events, trade shows, and webinars',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Teilnehmerregistrierung', en: 'Participant registration' },
|
||||
{ de: 'Veranstaltungsdurchführung', en: 'Event execution' },
|
||||
{ de: 'Nachbereitung und Follow-up', en: 'Follow-up activities' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS', 'PROSPECTIVE_CUSTOMERS', 'BUSINESS_PARTNERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'PHOTO_VIDEO'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'CONSENT'],
|
||||
suggestedRetentionYears: 2,
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Eventbrite', 'GoToWebinar', 'Zoom'],
|
||||
commonVendorCategories: ['MARKETING', 'COMMUNICATION'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// FINANCE
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-finance-accounting',
|
||||
category: 'FINANCE',
|
||||
name: {
|
||||
de: 'Finanzbuchhaltung',
|
||||
en: 'Financial Accounting',
|
||||
},
|
||||
description: {
|
||||
de: 'Führung der Finanzbuchhaltung, Rechnungsstellung und Zahlungsabwicklung',
|
||||
en: 'Financial accounting, invoicing, and payment processing',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Buchführung und Rechnungswesen', en: 'Bookkeeping and accounting' },
|
||||
{ de: 'Rechnungsstellung', en: 'Invoicing' },
|
||||
{ de: 'Zahlungsabwicklung', en: 'Payment processing' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS', 'SUPPLIERS', 'BUSINESS_PARTNERS'],
|
||||
personalDataCategories: [
|
||||
'NAME', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA', 'CONTRACT_DATA', 'TAX_ID',
|
||||
],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 10, // HGB/AO
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['DATEV', 'SAP', 'Lexware', 'Xero'],
|
||||
commonVendorCategories: ['ACCOUNTING', 'ERP'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-finance-debt-collection',
|
||||
category: 'FINANCE',
|
||||
name: {
|
||||
de: 'Forderungsmanagement',
|
||||
en: 'Debt Collection',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung offener Forderungen und Mahnwesen',
|
||||
en: 'Managing outstanding receivables and dunning',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Überwachung offener Forderungen', en: 'Monitoring outstanding receivables' },
|
||||
{ de: 'Mahnwesen', en: 'Dunning process' },
|
||||
{ de: 'Inkasso bei Bedarf', en: 'Debt collection if necessary' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS'],
|
||||
personalDataCategories: ['NAME', 'ADDRESS', 'CONTACT', 'PAYMENT_DATA', 'CONTRACT_DATA'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 10,
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['ERP', 'Inkasso-Software'],
|
||||
commonVendorCategories: ['ACCOUNTING', 'LEGAL'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// IT & SICHERHEIT
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-it-user-management',
|
||||
category: 'IT',
|
||||
name: {
|
||||
de: 'IT-Benutzerverwaltung',
|
||||
en: 'IT User Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Benutzerkonten, Zugriffsrechten und Authentifizierung',
|
||||
en: 'Managing user accounts, access rights, and authentication',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Verwaltung von Benutzerkonten', en: 'Managing user accounts' },
|
||||
{ de: 'Zugriffssteuerung', en: 'Access control' },
|
||||
{ de: 'Single Sign-On', en: 'Single Sign-On' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'LOGIN_DATA', 'USAGE_DATA'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 1, // Nach Kontoschließung
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Active Directory', 'Okta', 'Azure AD'],
|
||||
commonVendorCategories: ['SECURITY', 'CLOUD_INFRASTRUCTURE'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-it-logging',
|
||||
category: 'IT',
|
||||
name: {
|
||||
de: 'IT-Protokollierung',
|
||||
en: 'IT Logging',
|
||||
},
|
||||
description: {
|
||||
de: 'Protokollierung von IT-Aktivitäten zur Sicherheit und Fehleranalyse',
|
||||
en: 'Logging IT activities for security and error analysis',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Sicherheitsüberwachung', en: 'Security monitoring' },
|
||||
{ de: 'Fehleranalyse', en: 'Error analysis' },
|
||||
{ de: 'Nachvollziehbarkeit', en: 'Traceability' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES', 'CUSTOMERS', 'WEBSITE_USERS'],
|
||||
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOGIN_DATA'],
|
||||
suggestedLegalBasis: ['LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 1,
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Splunk', 'ELK Stack', 'Datadog'],
|
||||
commonVendorCategories: ['SECURITY', 'ANALYTICS'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-it-video-surveillance',
|
||||
category: 'IT',
|
||||
name: {
|
||||
de: 'Videoüberwachung',
|
||||
en: 'Video Surveillance',
|
||||
},
|
||||
description: {
|
||||
de: 'Videoüberwachung von Geschäftsräumen zum Schutz vor Diebstahl und Vandalismus',
|
||||
en: 'Video surveillance of business premises for theft and vandalism prevention',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Schutz vor Diebstahl und Vandalismus', en: 'Protection against theft and vandalism' },
|
||||
{ de: 'Zugangskontrolle', en: 'Access control' },
|
||||
{ de: 'Beweissicherung', en: 'Evidence preservation' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES', 'VISITORS', 'CUSTOMERS'],
|
||||
personalDataCategories: ['PHOTO_VIDEO', 'BIOMETRIC_DATA'],
|
||||
suggestedLegalBasis: ['LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 0.1, // 72 Stunden
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: true,
|
||||
commonSystems: ['CCTV-System'],
|
||||
commonVendorCategories: ['SECURITY'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-it-backup',
|
||||
category: 'IT',
|
||||
name: {
|
||||
de: 'Datensicherung (Backup)',
|
||||
en: 'Data Backup',
|
||||
},
|
||||
description: {
|
||||
de: 'Regelmäßige Sicherung von Unternehmensdaten',
|
||||
en: 'Regular backup of company data',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Datensicherung', en: 'Data backup' },
|
||||
{ de: 'Disaster Recovery', en: 'Disaster Recovery' },
|
||||
{ de: 'Geschäftskontinuität', en: 'Business continuity' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES', 'CUSTOMERS', 'SUPPLIERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
|
||||
suggestedLegalBasis: ['LEGITIMATE_INTEREST', 'LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 1, // Je nach Backup-Konzept
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Veeam', 'AWS Backup', 'Azure Backup'],
|
||||
commonVendorCategories: ['BACKUP', 'CLOUD_INFRASTRUCTURE'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// CUSTOMER SERVICE
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-cs-support',
|
||||
category: 'CUSTOMER_SERVICE',
|
||||
name: {
|
||||
de: 'Kundenbetreuung und Support',
|
||||
en: 'Customer Support',
|
||||
},
|
||||
description: {
|
||||
de: 'Bearbeitung von Kundenanfragen, Beschwerden und Support-Tickets',
|
||||
en: 'Handling customer inquiries, complaints, and support tickets',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Bearbeitung von Kundenanfragen', en: 'Handling customer inquiries' },
|
||||
{ de: 'Beschwerdemanagement', en: 'Complaint management' },
|
||||
{ de: 'Technischer Support', en: 'Technical support' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA', 'COMMUNICATION_DATA'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 3,
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Zendesk', 'Freshdesk', 'Intercom'],
|
||||
commonVendorCategories: ['SUPPORT', 'CRM'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-cs-satisfaction',
|
||||
category: 'CUSTOMER_SERVICE',
|
||||
name: {
|
||||
de: 'Kundenzufriedenheitsbefragungen',
|
||||
en: 'Customer Satisfaction Surveys',
|
||||
},
|
||||
description: {
|
||||
de: 'Durchführung von Umfragen zur Messung der Kundenzufriedenheit',
|
||||
en: 'Conducting surveys to measure customer satisfaction',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Messung der Kundenzufriedenheit', en: 'Measuring customer satisfaction' },
|
||||
{ de: 'Qualitätsverbesserung', en: 'Quality improvement' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
|
||||
suggestedLegalBasis: ['CONSENT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 2,
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['SurveyMonkey', 'Typeform', 'NPS-Tools'],
|
||||
commonVendorCategories: ['ANALYTICS', 'MARKETING'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// WEBSITE & APPS
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-web-analytics',
|
||||
category: 'WEBSITE',
|
||||
name: {
|
||||
de: 'Web-Analyse',
|
||||
en: 'Web Analytics',
|
||||
},
|
||||
description: {
|
||||
de: 'Analyse des Nutzerverhaltens auf der Website zur Optimierung',
|
||||
en: 'Analyzing user behavior on the website for optimization',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Analyse des Nutzerverhaltens', en: 'Analyzing user behavior' },
|
||||
{ de: 'Website-Optimierung', en: 'Website optimization' },
|
||||
{ de: 'Conversion-Tracking', en: 'Conversion tracking' },
|
||||
],
|
||||
dataSubjectCategories: ['WEBSITE_USERS'],
|
||||
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOCATION_DATA'],
|
||||
suggestedLegalBasis: ['CONSENT'],
|
||||
suggestedRetentionYears: 2,
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Google Analytics', 'Matomo', 'Plausible'],
|
||||
commonVendorCategories: ['ANALYTICS'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-web-contact-form',
|
||||
category: 'WEBSITE',
|
||||
name: {
|
||||
de: 'Kontaktformular',
|
||||
en: 'Contact Form',
|
||||
},
|
||||
description: {
|
||||
de: 'Verarbeitung von Anfragen über das Website-Kontaktformular',
|
||||
en: 'Processing inquiries submitted via the website contact form',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Bearbeitung von Kontaktanfragen', en: 'Processing contact inquiries' },
|
||||
{ de: 'Kommunikation mit Interessenten', en: 'Communication with prospects' },
|
||||
],
|
||||
dataSubjectCategories: ['PROSPECTIVE_CUSTOMERS', 'WEBSITE_USERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 1,
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['CRM', 'E-Mail-System'],
|
||||
commonVendorCategories: ['CRM', 'EMAIL'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-web-user-accounts',
|
||||
category: 'WEBSITE',
|
||||
name: {
|
||||
de: 'Benutzerkonten / Kundenportal',
|
||||
en: 'User Accounts / Customer Portal',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Benutzerkonten im Kundenportal oder Online-Shop',
|
||||
en: 'Managing user accounts in customer portal or online shop',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Bereitstellung des Kundenportals', en: 'Providing customer portal' },
|
||||
{ de: 'Benutzerverwaltung', en: 'User management' },
|
||||
{ de: 'Personalisierung', en: 'Personalization' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS', 'APP_USERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'LOGIN_DATA', 'USAGE_DATA', 'CONTRACT_DATA'],
|
||||
suggestedLegalBasis: ['CONTRACT'],
|
||||
suggestedRetentionYears: 1, // Nach Kontoschließung
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['E-Commerce', 'CRM', 'Auth0'],
|
||||
commonVendorCategories: ['HOSTING', 'CRM', 'SECURITY'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-web-cookies',
|
||||
category: 'WEBSITE',
|
||||
name: {
|
||||
de: 'Cookie-Verwaltung',
|
||||
en: 'Cookie Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Cookies und Einholung von Cookie-Einwilligungen',
|
||||
en: 'Managing cookies and obtaining cookie consents',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Speicherung von Cookie-Präferenzen', en: 'Storing cookie preferences' },
|
||||
{ de: 'Einwilligungsmanagement', en: 'Consent management' },
|
||||
],
|
||||
dataSubjectCategories: ['WEBSITE_USERS'],
|
||||
personalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA'],
|
||||
suggestedLegalBasis: ['CONSENT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 1,
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Cookiebot', 'Usercentrics', 'OneTrust'],
|
||||
commonVendorCategories: ['ANALYTICS', 'SECURITY'],
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// GENERAL
|
||||
// ==========================================
|
||||
{
|
||||
id: 'tpl-gen-communication',
|
||||
category: 'GENERAL',
|
||||
name: {
|
||||
de: 'Geschäftliche Kommunikation',
|
||||
en: 'Business Communication',
|
||||
},
|
||||
description: {
|
||||
de: 'E-Mail-Kommunikation, Telefonie und Messaging im Geschäftsverkehr',
|
||||
en: 'Email communication, telephony, and messaging in business operations',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Geschäftliche Kommunikation', en: 'Business communication' },
|
||||
{ de: 'Dokumentation von Korrespondenz', en: 'Documentation of correspondence' },
|
||||
],
|
||||
dataSubjectCategories: ['CUSTOMERS', 'SUPPLIERS', 'BUSINESS_PARTNERS', 'EMPLOYEES'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 6, // Handelsrechtlich relevant
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Microsoft 365', 'Google Workspace', 'Slack'],
|
||||
commonVendorCategories: ['EMAIL', 'COMMUNICATION', 'CLOUD_INFRASTRUCTURE'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-gen-visitor',
|
||||
category: 'GENERAL',
|
||||
name: {
|
||||
de: 'Besucherverwaltung',
|
||||
en: 'Visitor Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Erfassung und Verwaltung von Besuchern in Geschäftsräumen',
|
||||
en: 'Recording and managing visitors in business premises',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Zutrittskontrolle', en: 'Access control' },
|
||||
{ de: 'Sicherheit', en: 'Security' },
|
||||
{ de: 'Nachvollziehbarkeit', en: 'Traceability' },
|
||||
],
|
||||
dataSubjectCategories: ['VISITORS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'PHOTO_VIDEO'],
|
||||
suggestedLegalBasis: ['LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 0.1, // 1 Monat
|
||||
suggestedProtectionLevel: 'LOW',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['Besuchermanagement-System'],
|
||||
commonVendorCategories: ['SECURITY'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-gen-supplier',
|
||||
category: 'GENERAL',
|
||||
name: {
|
||||
de: 'Lieferantenverwaltung',
|
||||
en: 'Supplier Management',
|
||||
},
|
||||
description: {
|
||||
de: 'Verwaltung von Lieferantenbeziehungen und Beschaffung',
|
||||
en: 'Managing supplier relationships and procurement',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Lieferantenverwaltung', en: 'Supplier management' },
|
||||
{ de: 'Beschaffung', en: 'Procurement' },
|
||||
{ de: 'Qualitätsmanagement', en: 'Quality management' },
|
||||
],
|
||||
dataSubjectCategories: ['SUPPLIERS', 'BUSINESS_PARTNERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'CONTRACT_DATA', 'BANK_ACCOUNT'],
|
||||
suggestedLegalBasis: ['CONTRACT', 'LEGITIMATE_INTEREST'],
|
||||
suggestedRetentionYears: 10,
|
||||
suggestedProtectionLevel: 'MEDIUM',
|
||||
dpiaLikely: false,
|
||||
commonSystems: ['ERP', 'Lieferantenportal'],
|
||||
commonVendorCategories: ['ERP'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-gen-whistleblower',
|
||||
category: 'GENERAL',
|
||||
name: {
|
||||
de: 'Hinweisgebersystem',
|
||||
en: 'Whistleblower System',
|
||||
},
|
||||
description: {
|
||||
de: 'Entgegennahme und Bearbeitung von Hinweisen gemäß Hinweisgeberschutzgesetz',
|
||||
en: 'Receiving and processing reports according to whistleblower protection law',
|
||||
},
|
||||
purposes: [
|
||||
{ de: 'Entgegennahme von Hinweisen', en: 'Receiving reports' },
|
||||
{ de: 'Untersuchung von Verstößen', en: 'Investigating violations' },
|
||||
{ de: 'Schutz von Hinweisgebern', en: 'Protecting whistleblowers' },
|
||||
],
|
||||
dataSubjectCategories: ['EMPLOYEES', 'BUSINESS_PARTNERS'],
|
||||
personalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
|
||||
suggestedLegalBasis: ['LEGAL_OBLIGATION'],
|
||||
suggestedRetentionYears: 3,
|
||||
suggestedProtectionLevel: 'HIGH',
|
||||
dpiaLikely: true,
|
||||
commonSystems: ['Hinweisgeberportal'],
|
||||
commonVendorCategories: ['SECURITY', 'LEGAL'],
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Get templates by category
|
||||
*/
|
||||
export function getTemplatesByCategory(
|
||||
category: ProcessingActivityCategory
|
||||
): ProcessingActivityTemplate[] {
|
||||
return PROCESSING_ACTIVITY_TEMPLATES.filter((t) => t.category === category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template by ID
|
||||
*/
|
||||
export function getTemplateById(id: string): ProcessingActivityTemplate | undefined {
|
||||
return PROCESSING_ACTIVITY_TEMPLATES.find((t) => t.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories with their templates
|
||||
*/
|
||||
export function getGroupedTemplates(): Map<ProcessingActivityCategory, ProcessingActivityTemplate[]> {
|
||||
const grouped = new Map<ProcessingActivityCategory, ProcessingActivityTemplate[]>()
|
||||
|
||||
for (const template of PROCESSING_ACTIVITY_TEMPLATES) {
|
||||
const existing = grouped.get(template.category) || []
|
||||
grouped.set(template.category, [...existing, template])
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Create form data from template
|
||||
*/
|
||||
export function createFormDataFromTemplate(
|
||||
template: ProcessingActivityTemplate,
|
||||
organizationDefaults?: {
|
||||
responsible?: ProcessingActivityFormData['responsible']
|
||||
dpoContact?: ProcessingActivityFormData['dpoContact']
|
||||
}
|
||||
): Partial<ProcessingActivityFormData> {
|
||||
return {
|
||||
vvtId: '', // Will be generated
|
||||
name: template.name,
|
||||
purposes: template.purposes,
|
||||
dataSubjectCategories: template.dataSubjectCategories,
|
||||
personalDataCategories: template.personalDataCategories,
|
||||
legalBasis: template.suggestedLegalBasis.map((type) => ({ type })),
|
||||
protectionLevel: template.suggestedProtectionLevel,
|
||||
dpiaRequired: template.dpiaLikely,
|
||||
retentionPeriod: {
|
||||
duration: template.suggestedRetentionYears,
|
||||
durationUnit: 'YEARS',
|
||||
description: { de: '', en: '' },
|
||||
},
|
||||
recipientCategories: [],
|
||||
thirdCountryTransfers: [],
|
||||
technicalMeasures: [],
|
||||
dataSources: [],
|
||||
systems: [],
|
||||
dataFlows: [],
|
||||
subProcessors: [],
|
||||
owner: '',
|
||||
responsible: organizationDefaults?.responsible,
|
||||
dpoContact: organizationDefaults?.dpoContact,
|
||||
}
|
||||
}
|
||||
564
admin-v2/lib/sdk/vendor-compliance/catalog/vendor-templates.ts
Normal file
564
admin-v2/lib/sdk/vendor-compliance/catalog/vendor-templates.ts
Normal file
@@ -0,0 +1,564 @@
|
||||
/**
|
||||
* Vendor Templates and Categories
|
||||
*
|
||||
* Pre-defined vendor templates and risk profiles
|
||||
*/
|
||||
|
||||
import {
|
||||
VendorFormData,
|
||||
VendorRole,
|
||||
ServiceCategory,
|
||||
DataAccessLevel,
|
||||
TransferMechanismType,
|
||||
DocumentType,
|
||||
ReviewFrequency,
|
||||
LocalizedText,
|
||||
PersonalDataCategory,
|
||||
} from '../types'
|
||||
|
||||
export interface VendorTemplate {
|
||||
id: string
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
serviceCategory: ServiceCategory
|
||||
suggestedRole: VendorRole
|
||||
suggestedDataAccess: DataAccessLevel
|
||||
suggestedTransferMechanisms: TransferMechanismType[]
|
||||
suggestedContractTypes: DocumentType[]
|
||||
typicalDataCategories: PersonalDataCategory[]
|
||||
typicalCertifications: string[]
|
||||
inherentRiskFactors: RiskFactorWeight[]
|
||||
commonProviders: string[]
|
||||
}
|
||||
|
||||
export interface RiskFactorWeight {
|
||||
factor: string
|
||||
weight: number // 0-1
|
||||
description: LocalizedText
|
||||
}
|
||||
|
||||
export interface CountryRiskProfile {
|
||||
code: string // ISO 3166-1 alpha-2
|
||||
name: LocalizedText
|
||||
isEU: boolean
|
||||
isEEA: boolean
|
||||
hasAdequacyDecision: boolean
|
||||
adequacyDecisionDate?: string
|
||||
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH'
|
||||
notes?: LocalizedText
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// VENDOR TEMPLATES
|
||||
// ==========================================
|
||||
|
||||
export const VENDOR_TEMPLATES: VendorTemplate[] = [
|
||||
// Cloud & Infrastructure
|
||||
{
|
||||
id: 'tpl-vendor-cloud-iaas',
|
||||
name: { de: 'Cloud IaaS-Anbieter', en: 'Cloud IaaS Provider' },
|
||||
description: {
|
||||
de: 'Infrastructure-as-a-Service Anbieter (AWS, Azure, GCP)',
|
||||
en: 'Infrastructure-as-a-Service provider (AWS, Azure, GCP)',
|
||||
},
|
||||
serviceCategory: 'CLOUD_INFRASTRUCTURE',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA', 'TOM_ANNEX'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA', 'IP_ADDRESS'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2', 'C5'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'data_volume', weight: 0.9, description: { de: 'Hohes Datenvolumen', en: 'High data volume' } },
|
||||
{ factor: 'criticality', weight: 0.9, description: { de: 'Geschäftskritisch', en: 'Business critical' } },
|
||||
{ factor: 'sub_processors', weight: 0.7, description: { de: 'Viele Unterauftragnehmer', en: 'Many sub-processors' } },
|
||||
],
|
||||
commonProviders: ['AWS', 'Microsoft Azure', 'Google Cloud Platform', 'Hetzner', 'OVH'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-hosting',
|
||||
name: { de: 'Webhosting-Anbieter', en: 'Web Hosting Provider' },
|
||||
description: {
|
||||
de: 'Hosting von Websites und Webanwendungen',
|
||||
en: 'Hosting of websites and web applications',
|
||||
},
|
||||
serviceCategory: 'HOSTING',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'ADMINISTRATIVE',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
|
||||
typicalDataCategories: ['IP_ADDRESS', 'USAGE_DATA', 'LOGIN_DATA'],
|
||||
typicalCertifications: ['ISO 27001'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'data_volume', weight: 0.6, description: { de: 'Mittleres Datenvolumen', en: 'Medium data volume' } },
|
||||
{ factor: 'criticality', weight: 0.7, description: { de: 'Wichtig für Betrieb', en: 'Important for operations' } },
|
||||
],
|
||||
commonProviders: ['Hetzner', 'All-Inkl', 'IONOS', 'Strato', 'DigitalOcean'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-cdn',
|
||||
name: { de: 'CDN-Anbieter', en: 'CDN Provider' },
|
||||
description: {
|
||||
de: 'Content Delivery Network für schnelle Inhaltsauslieferung',
|
||||
en: 'Content Delivery Network for fast content delivery',
|
||||
},
|
||||
serviceCategory: 'CDN',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'POTENTIAL',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA'],
|
||||
typicalDataCategories: ['IP_ADDRESS', 'USAGE_DATA'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'data_transit', weight: 0.5, description: { de: 'Daten im Transit', en: 'Data in transit' } },
|
||||
{ factor: 'global_presence', weight: 0.6, description: { de: 'Globale Präsenz', en: 'Global presence' } },
|
||||
],
|
||||
commonProviders: ['Cloudflare', 'Fastly', 'Akamai', 'AWS CloudFront'],
|
||||
},
|
||||
|
||||
// Business Software
|
||||
{
|
||||
id: 'tpl-vendor-crm',
|
||||
name: { de: 'CRM-System', en: 'CRM System' },
|
||||
description: {
|
||||
de: 'Customer Relationship Management System',
|
||||
en: 'Customer Relationship Management System',
|
||||
},
|
||||
serviceCategory: 'CRM',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA', 'TOM_ANNEX'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'ADDRESS', 'COMMUNICATION_DATA', 'CONTRACT_DATA'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'customer_data', weight: 0.8, description: { de: 'Kundendaten', en: 'Customer data' } },
|
||||
{ factor: 'data_volume', weight: 0.7, description: { de: 'Hohes Datenvolumen', en: 'High data volume' } },
|
||||
],
|
||||
commonProviders: ['Salesforce', 'HubSpot', 'Pipedrive', 'Microsoft Dynamics', 'Zoho CRM'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-erp',
|
||||
name: { de: 'ERP-System', en: 'ERP System' },
|
||||
description: {
|
||||
de: 'Enterprise Resource Planning System',
|
||||
en: 'Enterprise Resource Planning System',
|
||||
},
|
||||
serviceCategory: 'ERP',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA', 'TOM_ANNEX'],
|
||||
typicalDataCategories: [
|
||||
'NAME', 'CONTACT', 'ADDRESS', 'BANK_ACCOUNT', 'CONTRACT_DATA',
|
||||
'EMPLOYMENT_DATA', 'SALARY_DATA',
|
||||
],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'data_volume', weight: 0.9, description: { de: 'Sehr hohes Datenvolumen', en: 'Very high data volume' } },
|
||||
{ factor: 'criticality', weight: 0.95, description: { de: 'Geschäftskritisch', en: 'Business critical' } },
|
||||
{ factor: 'sensitive_data', weight: 0.8, description: { de: 'Sensible Daten', en: 'Sensitive data' } },
|
||||
],
|
||||
commonProviders: ['SAP', 'Oracle', 'Microsoft Dynamics', 'Sage', 'Odoo'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-hr',
|
||||
name: { de: 'HR-Software', en: 'HR Software' },
|
||||
description: {
|
||||
de: 'Personalverwaltung und HR-Management',
|
||||
en: 'Personnel administration and HR management',
|
||||
},
|
||||
serviceCategory: 'HR_SOFTWARE',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'TOM_ANNEX'],
|
||||
typicalDataCategories: [
|
||||
'NAME', 'CONTACT', 'ADDRESS', 'DOB', 'SOCIAL_SECURITY', 'TAX_ID',
|
||||
'BANK_ACCOUNT', 'EMPLOYMENT_DATA', 'SALARY_DATA', 'HEALTH_DATA',
|
||||
],
|
||||
typicalCertifications: ['ISO 27001'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'employee_data', weight: 0.9, description: { de: 'Mitarbeiterdaten', en: 'Employee data' } },
|
||||
{ factor: 'sensitive_data', weight: 0.85, description: { de: 'Sensible Daten', en: 'Sensitive data' } },
|
||||
{ factor: 'special_categories', weight: 0.7, description: { de: 'Besondere Kategorien möglich', en: 'Special categories possible' } },
|
||||
],
|
||||
commonProviders: ['Personio', 'Workday', 'SAP SuccessFactors', 'HRworks', 'Factorial'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-accounting',
|
||||
name: { de: 'Buchhaltungssoftware', en: 'Accounting Software' },
|
||||
description: {
|
||||
de: 'Finanzbuchhaltung und Rechnungswesen',
|
||||
en: 'Financial accounting and bookkeeping',
|
||||
},
|
||||
serviceCategory: 'ACCOUNTING',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION'],
|
||||
suggestedContractTypes: ['AVV', 'MSA'],
|
||||
typicalDataCategories: ['NAME', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA', 'TAX_ID'],
|
||||
typicalCertifications: ['ISO 27001', 'IDW PS 951'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'financial_data', weight: 0.85, description: { de: 'Finanzdaten', en: 'Financial data' } },
|
||||
{ factor: 'legal_retention', weight: 0.7, description: { de: 'Aufbewahrungspflichten', en: 'Retention requirements' } },
|
||||
],
|
||||
commonProviders: ['DATEV', 'Lexware', 'SevDesk', 'Xero', 'Sage'],
|
||||
},
|
||||
|
||||
// Communication & Collaboration
|
||||
{
|
||||
id: 'tpl-vendor-email',
|
||||
name: { de: 'E-Mail-Dienst', en: 'Email Service' },
|
||||
description: {
|
||||
de: 'E-Mail-Hosting und -Kommunikation',
|
||||
en: 'Email hosting and communication',
|
||||
},
|
||||
serviceCategory: 'EMAIL',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'communication_data', weight: 0.8, description: { de: 'Kommunikationsdaten', en: 'Communication data' } },
|
||||
{ factor: 'criticality', weight: 0.8, description: { de: 'Geschäftskritisch', en: 'Business critical' } },
|
||||
],
|
||||
commonProviders: ['Microsoft 365', 'Google Workspace', 'Zoho Mail', 'ProtonMail'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-communication',
|
||||
name: { de: 'Kollaborations-Tool', en: 'Collaboration Tool' },
|
||||
description: {
|
||||
de: 'Team-Kommunikation und Zusammenarbeit',
|
||||
en: 'Team communication and collaboration',
|
||||
},
|
||||
serviceCategory: 'COMMUNICATION',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA', 'PHOTO_VIDEO'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'communication_data', weight: 0.7, description: { de: 'Kommunikationsdaten', en: 'Communication data' } },
|
||||
{ factor: 'file_sharing', weight: 0.6, description: { de: 'Dateifreigabe', en: 'File sharing' } },
|
||||
],
|
||||
commonProviders: ['Slack', 'Microsoft Teams', 'Zoom', 'Google Meet', 'Webex'],
|
||||
},
|
||||
|
||||
// Marketing & Analytics
|
||||
{
|
||||
id: 'tpl-vendor-analytics',
|
||||
name: { de: 'Analytics-Tool', en: 'Analytics Tool' },
|
||||
description: {
|
||||
de: 'Web-Analyse und Nutzerverhalten',
|
||||
en: 'Web analytics and user behavior',
|
||||
},
|
||||
serviceCategory: 'ANALYTICS',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA'],
|
||||
typicalDataCategories: ['IP_ADDRESS', 'DEVICE_ID', 'USAGE_DATA', 'LOCATION_DATA'],
|
||||
typicalCertifications: ['ISO 27001'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'tracking', weight: 0.7, description: { de: 'Tracking', en: 'Tracking' } },
|
||||
{ factor: 'profiling', weight: 0.6, description: { de: 'Profiling möglich', en: 'Profiling possible' } },
|
||||
],
|
||||
commonProviders: ['Google Analytics', 'Matomo', 'Plausible', 'Mixpanel', 'Amplitude'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-vendor-marketing-automation',
|
||||
name: { de: 'Marketing-Automatisierung', en: 'Marketing Automation' },
|
||||
description: {
|
||||
de: 'E-Mail-Marketing und Automatisierung',
|
||||
en: 'Email marketing and automation',
|
||||
},
|
||||
serviceCategory: 'MARKETING',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'USAGE_DATA'],
|
||||
typicalCertifications: ['ISO 27001'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'marketing_data', weight: 0.6, description: { de: 'Marketing-Daten', en: 'Marketing data' } },
|
||||
{ factor: 'consent_management', weight: 0.7, description: { de: 'Einwilligungsmanagement', en: 'Consent management' } },
|
||||
],
|
||||
commonProviders: ['Mailchimp', 'HubSpot', 'Sendinblue', 'CleverReach', 'ActiveCampaign'],
|
||||
},
|
||||
|
||||
// Support & Service
|
||||
{
|
||||
id: 'tpl-vendor-support',
|
||||
name: { de: 'Support-/Ticketsystem', en: 'Support/Ticket System' },
|
||||
description: {
|
||||
de: 'Kundenservice und Ticket-Management',
|
||||
en: 'Customer service and ticket management',
|
||||
},
|
||||
serviceCategory: 'SUPPORT',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'COMMUNICATION_DATA', 'CONTRACT_DATA'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'customer_data', weight: 0.7, description: { de: 'Kundendaten', en: 'Customer data' } },
|
||||
{ factor: 'communication', weight: 0.6, description: { de: 'Kommunikationsinhalte', en: 'Communication content' } },
|
||||
],
|
||||
commonProviders: ['Zendesk', 'Freshdesk', 'Intercom', 'HelpScout', 'Jira Service Management'],
|
||||
},
|
||||
|
||||
// Payment & Finance
|
||||
{
|
||||
id: 'tpl-vendor-payment',
|
||||
name: { de: 'Zahlungsdienstleister', en: 'Payment Service Provider' },
|
||||
description: {
|
||||
de: 'Zahlungsabwicklung und Payment Gateway',
|
||||
en: 'Payment processing and payment gateway',
|
||||
},
|
||||
serviceCategory: 'PAYMENT',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['SCC_PROCESSOR', 'BCR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
|
||||
typicalDataCategories: ['NAME', 'ADDRESS', 'BANK_ACCOUNT', 'PAYMENT_DATA'],
|
||||
typicalCertifications: ['PCI DSS', 'ISO 27001'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'financial_data', weight: 0.9, description: { de: 'Finanzdaten', en: 'Financial data' } },
|
||||
{ factor: 'pci_scope', weight: 0.8, description: { de: 'PCI-Scope', en: 'PCI scope' } },
|
||||
],
|
||||
commonProviders: ['Stripe', 'PayPal', 'Adyen', 'Mollie', 'Klarna'],
|
||||
},
|
||||
|
||||
// Security
|
||||
{
|
||||
id: 'tpl-vendor-security',
|
||||
name: { de: 'Sicherheitsdienstleister', en: 'Security Service Provider' },
|
||||
description: {
|
||||
de: 'IT-Sicherheit, Penetrationstests, SIEM',
|
||||
en: 'IT security, penetration testing, SIEM',
|
||||
},
|
||||
serviceCategory: 'SECURITY',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'ADMINISTRATIVE',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'NDA'],
|
||||
typicalDataCategories: ['IP_ADDRESS', 'USAGE_DATA', 'LOGIN_DATA'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'system_access', weight: 0.8, description: { de: 'Systemzugriff', en: 'System access' } },
|
||||
{ factor: 'security_data', weight: 0.7, description: { de: 'Sicherheitsdaten', en: 'Security data' } },
|
||||
],
|
||||
commonProviders: ['CrowdStrike', 'Splunk', 'Palo Alto Networks', 'Tenable'],
|
||||
},
|
||||
|
||||
// Backup & Storage
|
||||
{
|
||||
id: 'tpl-vendor-backup',
|
||||
name: { de: 'Backup-Anbieter', en: 'Backup Provider' },
|
||||
description: {
|
||||
de: 'Datensicherung und Disaster Recovery',
|
||||
en: 'Data backup and disaster recovery',
|
||||
},
|
||||
serviceCategory: 'BACKUP',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'CONTENT',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION', 'SCC_PROCESSOR'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'SLA'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA'],
|
||||
typicalCertifications: ['ISO 27001', 'SOC 2'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'full_backup', weight: 0.9, description: { de: 'Vollständige Kopie', en: 'Full copy' } },
|
||||
{ factor: 'retention', weight: 0.7, description: { de: 'Lange Aufbewahrung', en: 'Long retention' } },
|
||||
],
|
||||
commonProviders: ['Veeam', 'Acronis', 'Commvault', 'AWS Backup'],
|
||||
},
|
||||
|
||||
// Consulting
|
||||
{
|
||||
id: 'tpl-vendor-consulting',
|
||||
name: { de: 'Beratungsunternehmen', en: 'Consulting Company' },
|
||||
description: {
|
||||
de: 'IT-Beratung, Projektunterstützung',
|
||||
en: 'IT consulting, project support',
|
||||
},
|
||||
serviceCategory: 'CONSULTING',
|
||||
suggestedRole: 'PROCESSOR',
|
||||
suggestedDataAccess: 'POTENTIAL',
|
||||
suggestedTransferMechanisms: ['ADEQUACY_DECISION'],
|
||||
suggestedContractTypes: ['AVV', 'MSA', 'NDA'],
|
||||
typicalDataCategories: ['NAME', 'CONTACT', 'CONTRACT_DATA'],
|
||||
typicalCertifications: ['ISO 27001'],
|
||||
inherentRiskFactors: [
|
||||
{ factor: 'project_access', weight: 0.5, description: { de: 'Projektzugriff', en: 'Project access' } },
|
||||
{ factor: 'temporary', weight: 0.4, description: { de: 'Temporär', en: 'Temporary' } },
|
||||
],
|
||||
commonProviders: ['Accenture', 'McKinsey', 'Deloitte', 'PwC', 'KPMG'],
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// COUNTRY RISK PROFILES
|
||||
// ==========================================
|
||||
|
||||
export const COUNTRY_RISK_PROFILES: CountryRiskProfile[] = [
|
||||
// EU Countries (Low Risk)
|
||||
{ code: 'DE', name: { de: 'Deutschland', en: 'Germany' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'AT', name: { de: 'Österreich', en: 'Austria' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'FR', name: { de: 'Frankreich', en: 'France' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'NL', name: { de: 'Niederlande', en: 'Netherlands' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'BE', name: { de: 'Belgien', en: 'Belgium' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'IT', name: { de: 'Italien', en: 'Italy' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'ES', name: { de: 'Spanien', en: 'Spain' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'PT', name: { de: 'Portugal', en: 'Portugal' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'PL', name: { de: 'Polen', en: 'Poland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'CZ', name: { de: 'Tschechien', en: 'Czech Republic' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'SE', name: { de: 'Schweden', en: 'Sweden' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'DK', name: { de: 'Dänemark', en: 'Denmark' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'FI', name: { de: 'Finnland', en: 'Finland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'IE', name: { de: 'Irland', en: 'Ireland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'LU', name: { de: 'Luxemburg', en: 'Luxembourg' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
|
||||
// EEA Countries
|
||||
{ code: 'NO', name: { de: 'Norwegen', en: 'Norway' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'IS', name: { de: 'Island', en: 'Iceland' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'LI', name: { de: 'Liechtenstein', en: 'Liechtenstein' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
|
||||
// Adequacy Decision Countries
|
||||
{ code: 'CH', name: { de: 'Schweiz', en: 'Switzerland' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'GB', name: { de: 'Vereinigtes Königreich', en: 'United Kingdom' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2021-06-28', riskLevel: 'LOW' },
|
||||
{ code: 'JP', name: { de: 'Japan', en: 'Japan' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2019-01-23', riskLevel: 'LOW' },
|
||||
{ code: 'KR', name: { de: 'Südkorea', en: 'South Korea' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2022-12-17', riskLevel: 'LOW' },
|
||||
{ code: 'IL', name: { de: 'Israel', en: 'Israel' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'NZ', name: { de: 'Neuseeland', en: 'New Zealand' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'CA', name: { de: 'Kanada', en: 'Canada' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW', notes: { de: 'Nur PIPEDA-Bereich', en: 'PIPEDA scope only' } },
|
||||
{ code: 'AR', name: { de: 'Argentinien', en: 'Argentina' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
{ code: 'UY', name: { de: 'Uruguay', en: 'Uruguay' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' },
|
||||
|
||||
// US (Special - DPF)
|
||||
{ code: 'US', name: { de: 'USA', en: 'United States' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2023-07-10', riskLevel: 'MEDIUM', notes: { de: 'EU-US Data Privacy Framework erforderlich', en: 'EU-US Data Privacy Framework required' } },
|
||||
|
||||
// Third Countries without Adequacy (High Risk)
|
||||
{ code: 'CN', name: { de: 'China', en: 'China' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Staatlicher Datenzugriff möglich', en: 'Government data access possible' } },
|
||||
{ code: 'RU', name: { de: 'Russland', en: 'Russia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Sanktionen beachten', en: 'Consider sanctions' } },
|
||||
{ code: 'IN', name: { de: 'Indien', en: 'India' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' },
|
||||
{ code: 'BR', name: { de: 'Brasilien', en: 'Brazil' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'LGPD vorhanden', en: 'LGPD in place' } },
|
||||
{ code: 'AU', name: { de: 'Australien', en: 'Australia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM' },
|
||||
{ code: 'SG', name: { de: 'Singapur', en: 'Singapore' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'PDPA vorhanden', en: 'PDPA in place' } },
|
||||
{ code: 'HK', name: { de: 'Hongkong', en: 'Hong Kong' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' },
|
||||
{ code: 'AE', name: { de: 'VAE', en: 'UAE' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' },
|
||||
{ code: 'ZA', name: { de: 'Südafrika', en: 'South Africa' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'POPIA vorhanden', en: 'POPIA in place' } },
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get vendor template by ID
|
||||
*/
|
||||
export function getVendorTemplateById(id: string): VendorTemplate | undefined {
|
||||
return VENDOR_TEMPLATES.find((t) => t.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vendor templates by category
|
||||
*/
|
||||
export function getVendorTemplatesByCategory(category: ServiceCategory): VendorTemplate[] {
|
||||
return VENDOR_TEMPLATES.filter((t) => t.serviceCategory === category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country risk profile
|
||||
*/
|
||||
export function getCountryRiskProfile(countryCode: string): CountryRiskProfile | undefined {
|
||||
return COUNTRY_RISK_PROFILES.find((c) => c.code === countryCode.toUpperCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if country requires transfer mechanism
|
||||
*/
|
||||
export function requiresTransferMechanism(countryCode: string): boolean {
|
||||
const profile = getCountryRiskProfile(countryCode)
|
||||
if (!profile) return true // Unknown country = requires mechanism
|
||||
return !profile.isEU && !profile.isEEA && !profile.hasAdequacyDecision
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested transfer mechanisms for country
|
||||
*/
|
||||
export function getSuggestedTransferMechanisms(countryCode: string): TransferMechanismType[] {
|
||||
const profile = getCountryRiskProfile(countryCode)
|
||||
|
||||
if (!profile) {
|
||||
return ['SCC_PROCESSOR']
|
||||
}
|
||||
|
||||
if (profile.isEU || profile.isEEA) {
|
||||
return [] // No mechanism needed
|
||||
}
|
||||
|
||||
if (profile.hasAdequacyDecision) {
|
||||
return ['ADEQUACY_DECISION']
|
||||
}
|
||||
|
||||
// Third country without adequacy
|
||||
return ['SCC_PROCESSOR', 'BCR']
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate inherent risk score for vendor template
|
||||
*/
|
||||
export function calculateTemplateRiskScore(template: VendorTemplate): number {
|
||||
const baseScore = template.inherentRiskFactors.reduce(
|
||||
(sum, factor) => sum + factor.weight * 100,
|
||||
0
|
||||
)
|
||||
return Math.min(100, baseScore / template.inherentRiskFactors.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create form data from vendor template
|
||||
*/
|
||||
export function createVendorFormDataFromTemplate(
|
||||
template: VendorTemplate
|
||||
): Partial<VendorFormData> {
|
||||
return {
|
||||
serviceCategory: template.serviceCategory,
|
||||
role: template.suggestedRole,
|
||||
dataAccessLevel: template.suggestedDataAccess,
|
||||
transferMechanisms: template.suggestedTransferMechanisms,
|
||||
contractTypes: template.suggestedContractTypes,
|
||||
certifications: template.typicalCertifications.map((type) => ({
|
||||
type,
|
||||
issuedDate: undefined,
|
||||
expirationDate: undefined,
|
||||
})),
|
||||
reviewFrequency: 'ANNUAL',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all EU/EEA countries
|
||||
*/
|
||||
export function getEUEEACountries(): CountryRiskProfile[] {
|
||||
return COUNTRY_RISK_PROFILES.filter((c) => c.isEU || c.isEEA)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all countries with adequacy decision
|
||||
*/
|
||||
export function getAdequateCountries(): CountryRiskProfile[] {
|
||||
return COUNTRY_RISK_PROFILES.filter((c) => c.hasAdequacyDecision)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all high-risk countries
|
||||
*/
|
||||
export function getHighRiskCountries(): CountryRiskProfile[] {
|
||||
return COUNTRY_RISK_PROFILES.filter((c) => c.riskLevel === 'HIGH' || c.riskLevel === 'VERY_HIGH')
|
||||
}
|
||||
1010
admin-v2/lib/sdk/vendor-compliance/context.tsx
Normal file
1010
admin-v2/lib/sdk/vendor-compliance/context.tsx
Normal file
File diff suppressed because it is too large
Load Diff
459
admin-v2/lib/sdk/vendor-compliance/contract-review/analyzer.ts
Normal file
459
admin-v2/lib/sdk/vendor-compliance/contract-review/analyzer.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* Contract Analyzer
|
||||
*
|
||||
* LLM-based contract review for GDPR compliance
|
||||
*/
|
||||
|
||||
import {
|
||||
Finding,
|
||||
Citation,
|
||||
FindingType,
|
||||
FindingCategory,
|
||||
FindingSeverity,
|
||||
DocumentType,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
import { AVV_CHECKLIST, INCIDENT_CHECKLIST, TRANSFER_CHECKLIST } from './checklists'
|
||||
|
||||
// ==========================================
|
||||
// TYPES
|
||||
// ==========================================
|
||||
|
||||
export interface ContractAnalysisRequest {
|
||||
contractId: string
|
||||
vendorId: string
|
||||
tenantId: string
|
||||
documentText: string
|
||||
documentType?: DocumentType
|
||||
language?: 'de' | 'en'
|
||||
analysisScope?: AnalysisScope[]
|
||||
}
|
||||
|
||||
export interface ContractAnalysisResponse {
|
||||
documentType: DocumentType
|
||||
language: 'de' | 'en'
|
||||
parties: ContractPartyInfo[]
|
||||
findings: Finding[]
|
||||
complianceScore: number
|
||||
topRisks: LocalizedText[]
|
||||
requiredActions: LocalizedText[]
|
||||
metadata: ExtractedMetadata
|
||||
}
|
||||
|
||||
export interface ContractPartyInfo {
|
||||
role: 'CONTROLLER' | 'PROCESSOR' | 'PARTY'
|
||||
name: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
export interface ExtractedMetadata {
|
||||
effectiveDate?: string
|
||||
expirationDate?: string
|
||||
autoRenewal?: boolean
|
||||
terminationNoticePeriod?: number
|
||||
governingLaw?: string
|
||||
jurisdiction?: string
|
||||
}
|
||||
|
||||
export type AnalysisScope =
|
||||
| 'AVV_COMPLIANCE'
|
||||
| 'SUBPROCESSOR'
|
||||
| 'INCIDENT_RESPONSE'
|
||||
| 'AUDIT_RIGHTS'
|
||||
| 'DELETION'
|
||||
| 'TOM'
|
||||
| 'TRANSFER'
|
||||
| 'LIABILITY'
|
||||
| 'SLA'
|
||||
|
||||
// ==========================================
|
||||
// SYSTEM PROMPTS
|
||||
// ==========================================
|
||||
|
||||
export const CONTRACT_REVIEW_SYSTEM_PROMPT = `Du bist ein Datenschutz-Rechtsexperte, der Verträge auf DSGVO-Konformität prüft.
|
||||
|
||||
WICHTIG:
|
||||
1. Jede Feststellung MUSS mit einer Textstelle belegt werden (Citation)
|
||||
2. Gib niemals Rechtsberatung - nur Compliance-Hinweise
|
||||
3. Markiere unklare Stellen als UNKNOWN, nicht als GAP
|
||||
4. Sei konservativ: im Zweifel RISK statt OK
|
||||
|
||||
PRÜFUNGSSCHEMA Art. 28 DSGVO AVV:
|
||||
${AVV_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')}
|
||||
|
||||
INCIDENT RESPONSE:
|
||||
${INCIDENT_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')}
|
||||
|
||||
DRITTLANDTRANSFER:
|
||||
${TRANSFER_CHECKLIST.map((item) => `- ${item.id}: ${item.requirement.de} (${item.article})`).join('\n')}
|
||||
|
||||
AUSGABEFORMAT (JSON):
|
||||
{
|
||||
"document_type": "AVV|MSA|SLA|SCC|NDA|TOM_ANNEX|OTHER|UNKNOWN",
|
||||
"language": "de|en",
|
||||
"parties": [
|
||||
{
|
||||
"role": "CONTROLLER|PROCESSOR|PARTY",
|
||||
"name": "...",
|
||||
"address": "..."
|
||||
}
|
||||
],
|
||||
"findings": [
|
||||
{
|
||||
"category": "AVV_CONTENT|SUBPROCESSOR|INCIDENT|AUDIT_RIGHTS|DELETION|TOM|TRANSFER|LIABILITY|SLA|DATA_SUBJECT_RIGHTS|CONFIDENTIALITY|INSTRUCTION|GENERAL",
|
||||
"type": "OK|GAP|RISK|UNKNOWN",
|
||||
"severity": "LOW|MEDIUM|HIGH|CRITICAL",
|
||||
"title_de": "...",
|
||||
"title_en": "...",
|
||||
"description_de": "...",
|
||||
"description_en": "...",
|
||||
"recommendation_de": "...",
|
||||
"recommendation_en": "...",
|
||||
"citations": [
|
||||
{
|
||||
"page": 3,
|
||||
"quoted_text": "Der Auftragnehmer...",
|
||||
"start_char": 1234,
|
||||
"end_char": 1456
|
||||
}
|
||||
],
|
||||
"affected_requirement": "Art. 28 Abs. 3 lit. a DSGVO"
|
||||
}
|
||||
],
|
||||
"compliance_score": 72,
|
||||
"top_risks": [
|
||||
{"de": "...", "en": "..."}
|
||||
],
|
||||
"required_actions": [
|
||||
{"de": "...", "en": "..."}
|
||||
],
|
||||
"metadata": {
|
||||
"effective_date": "2024-01-01",
|
||||
"expiration_date": "2025-12-31",
|
||||
"auto_renewal": true,
|
||||
"termination_notice_period": 90,
|
||||
"governing_law": "Germany",
|
||||
"jurisdiction": "Frankfurt am Main"
|
||||
}
|
||||
}`
|
||||
|
||||
export const CONTRACT_CLASSIFICATION_PROMPT = `Analysiere den folgenden Vertragstext und klassifiziere ihn:
|
||||
|
||||
1. Dokumenttyp (AVV, MSA, SLA, SCC, NDA, TOM_ANNEX, OTHER)
|
||||
2. Sprache (de, en)
|
||||
3. Vertragsparteien mit Rollen
|
||||
|
||||
Antworte im JSON-Format:
|
||||
{
|
||||
"document_type": "...",
|
||||
"language": "...",
|
||||
"parties": [...]
|
||||
}`
|
||||
|
||||
export const METADATA_EXTRACTION_PROMPT = `Extrahiere die folgenden Metadaten aus dem Vertrag:
|
||||
|
||||
1. Inkrafttreten / Effective Date
|
||||
2. Laufzeit / Ablaufdatum
|
||||
3. Automatische Verlängerung
|
||||
4. Kündigungsfrist
|
||||
5. Anwendbares Recht
|
||||
6. Gerichtsstand
|
||||
|
||||
Antworte im JSON-Format.`
|
||||
|
||||
// ==========================================
|
||||
// ANALYSIS FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Analyze a contract for GDPR compliance
|
||||
*/
|
||||
export async function analyzeContract(
|
||||
request: ContractAnalysisRequest
|
||||
): Promise<ContractAnalysisResponse> {
|
||||
// This function would typically call an LLM API
|
||||
// For now, we provide the structure that would be used
|
||||
|
||||
const apiEndpoint = '/api/sdk/v1/vendor-compliance/contracts/analyze'
|
||||
|
||||
const response = await fetch(apiEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
contract_id: request.contractId,
|
||||
vendor_id: request.vendorId,
|
||||
tenant_id: request.tenantId,
|
||||
document_text: request.documentText,
|
||||
document_type: request.documentType,
|
||||
language: request.language || 'de',
|
||||
analysis_scope: request.analysisScope || [
|
||||
'AVV_COMPLIANCE',
|
||||
'SUBPROCESSOR',
|
||||
'INCIDENT_RESPONSE',
|
||||
'AUDIT_RIGHTS',
|
||||
'DELETION',
|
||||
'TOM',
|
||||
'TRANSFER',
|
||||
],
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Contract analysis failed')
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return transformAnalysisResponse(result, request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform LLM response to typed response
|
||||
*/
|
||||
function transformAnalysisResponse(
|
||||
llmResponse: Record<string, unknown>,
|
||||
request: ContractAnalysisRequest
|
||||
): ContractAnalysisResponse {
|
||||
const findings: Finding[] = (llmResponse.findings as Array<Record<string, unknown>> || []).map((f, idx) => ({
|
||||
id: `finding-${request.contractId}-${idx}`,
|
||||
tenantId: request.tenantId,
|
||||
contractId: request.contractId,
|
||||
vendorId: request.vendorId,
|
||||
type: (f.type as FindingType) || 'UNKNOWN',
|
||||
category: (f.category as FindingCategory) || 'GENERAL',
|
||||
severity: (f.severity as FindingSeverity) || 'MEDIUM',
|
||||
title: {
|
||||
de: (f.title_de as string) || '',
|
||||
en: (f.title_en as string) || '',
|
||||
},
|
||||
description: {
|
||||
de: (f.description_de as string) || '',
|
||||
en: (f.description_en as string) || '',
|
||||
},
|
||||
recommendation: f.recommendation_de ? {
|
||||
de: f.recommendation_de as string,
|
||||
en: (f.recommendation_en as string) || '',
|
||||
} : undefined,
|
||||
citations: ((f.citations as Array<Record<string, unknown>>) || []).map((c) => ({
|
||||
documentId: request.contractId,
|
||||
page: (c.page as number) || 1,
|
||||
startChar: (c.start_char as number) || 0,
|
||||
endChar: (c.end_char as number) || 0,
|
||||
quotedText: (c.quoted_text as string) || '',
|
||||
quoteHash: generateQuoteHash((c.quoted_text as string) || ''),
|
||||
})),
|
||||
affectedRequirement: f.affected_requirement as string | undefined,
|
||||
triggeredControls: [],
|
||||
status: 'OPEN',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
|
||||
const metadata = llmResponse.metadata as Record<string, unknown> || {}
|
||||
|
||||
return {
|
||||
documentType: (llmResponse.document_type as DocumentType) || 'OTHER',
|
||||
language: (llmResponse.language as 'de' | 'en') || 'de',
|
||||
parties: ((llmResponse.parties as Array<Record<string, unknown>>) || []).map((p) => ({
|
||||
role: (p.role as 'CONTROLLER' | 'PROCESSOR' | 'PARTY') || 'PARTY',
|
||||
name: (p.name as string) || '',
|
||||
address: p.address as string | undefined,
|
||||
})),
|
||||
findings,
|
||||
complianceScore: (llmResponse.compliance_score as number) || 0,
|
||||
topRisks: ((llmResponse.top_risks as Array<Record<string, string>>) || []).map((r) => ({
|
||||
de: r.de || '',
|
||||
en: r.en || '',
|
||||
})),
|
||||
requiredActions: ((llmResponse.required_actions as Array<Record<string, string>>) || []).map((a) => ({
|
||||
de: a.de || '',
|
||||
en: a.en || '',
|
||||
})),
|
||||
metadata: {
|
||||
effectiveDate: metadata.effective_date as string | undefined,
|
||||
expirationDate: metadata.expiration_date as string | undefined,
|
||||
autoRenewal: metadata.auto_renewal as boolean | undefined,
|
||||
terminationNoticePeriod: metadata.termination_notice_period as number | undefined,
|
||||
governingLaw: metadata.governing_law as string | undefined,
|
||||
jurisdiction: metadata.jurisdiction as string | undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash for quote verification
|
||||
*/
|
||||
function generateQuoteHash(text: string): string {
|
||||
// Simple hash for demo - in production use crypto.subtle.digest
|
||||
let hash = 0
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash
|
||||
}
|
||||
return Math.abs(hash).toString(16).padStart(16, '0')
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CITATION UTILITIES
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Verify citation integrity
|
||||
*/
|
||||
export function verifyCitation(
|
||||
citation: Citation,
|
||||
documentText: string
|
||||
): boolean {
|
||||
const extractedText = documentText.substring(citation.startChar, citation.endChar)
|
||||
const expectedHash = generateQuoteHash(extractedText)
|
||||
return citation.quoteHash === expectedHash
|
||||
}
|
||||
|
||||
/**
|
||||
* Find citation context in document
|
||||
*/
|
||||
export function getCitationContext(
|
||||
citation: Citation,
|
||||
documentText: string,
|
||||
contextChars: number = 100
|
||||
): {
|
||||
before: string
|
||||
quoted: string
|
||||
after: string
|
||||
} {
|
||||
const start = Math.max(0, citation.startChar - contextChars)
|
||||
const end = Math.min(documentText.length, citation.endChar + contextChars)
|
||||
|
||||
return {
|
||||
before: documentText.substring(start, citation.startChar),
|
||||
quoted: documentText.substring(citation.startChar, citation.endChar),
|
||||
after: documentText.substring(citation.endChar, end),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight citations in text
|
||||
*/
|
||||
export function highlightCitations(
|
||||
documentText: string,
|
||||
citations: Citation[]
|
||||
): string {
|
||||
// Sort citations by start position (reverse to avoid offset issues)
|
||||
const sortedCitations = [...citations].sort((a, b) => b.startChar - a.startChar)
|
||||
|
||||
let result = documentText
|
||||
|
||||
for (const citation of sortedCitations) {
|
||||
const before = result.substring(0, citation.startChar)
|
||||
const quoted = result.substring(citation.startChar, citation.endChar)
|
||||
const after = result.substring(citation.endChar)
|
||||
|
||||
result = `${before}<mark data-citation-id="${citation.documentId}">${quoted}</mark>${after}`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// COMPLIANCE SCORE CALCULATION
|
||||
// ==========================================
|
||||
|
||||
export interface ComplianceScoreBreakdown {
|
||||
totalScore: number
|
||||
categoryScores: Record<FindingCategory, number>
|
||||
severityCounts: Record<FindingSeverity, number>
|
||||
findingCounts: {
|
||||
total: number
|
||||
gaps: number
|
||||
risks: number
|
||||
ok: number
|
||||
unknown: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate detailed compliance score
|
||||
*/
|
||||
export function calculateComplianceScore(findings: Finding[]): ComplianceScoreBreakdown {
|
||||
const severityWeights: Record<FindingSeverity, number> = {
|
||||
CRITICAL: 25,
|
||||
HIGH: 15,
|
||||
MEDIUM: 8,
|
||||
LOW: 3,
|
||||
}
|
||||
|
||||
const categoryWeights: Partial<Record<FindingCategory, number>> = {
|
||||
AVV_CONTENT: 1.5,
|
||||
SUBPROCESSOR: 1.3,
|
||||
INCIDENT: 1.3,
|
||||
DELETION: 1.2,
|
||||
AUDIT_RIGHTS: 1.1,
|
||||
TOM: 1.2,
|
||||
TRANSFER: 1.4,
|
||||
}
|
||||
|
||||
let totalDeductions = 0
|
||||
const maxPossibleDeductions = 100
|
||||
|
||||
const categoryScores: Partial<Record<FindingCategory, number>> = {}
|
||||
const severityCounts: Record<FindingSeverity, number> = {
|
||||
LOW: 0,
|
||||
MEDIUM: 0,
|
||||
HIGH: 0,
|
||||
CRITICAL: 0,
|
||||
}
|
||||
|
||||
let gaps = 0
|
||||
let risks = 0
|
||||
let ok = 0
|
||||
let unknown = 0
|
||||
|
||||
for (const finding of findings) {
|
||||
severityCounts[finding.severity]++
|
||||
|
||||
switch (finding.type) {
|
||||
case 'GAP':
|
||||
gaps++
|
||||
totalDeductions += severityWeights[finding.severity] * (categoryWeights[finding.category] || 1)
|
||||
break
|
||||
case 'RISK':
|
||||
risks++
|
||||
totalDeductions += severityWeights[finding.severity] * 0.7 * (categoryWeights[finding.category] || 1)
|
||||
break
|
||||
case 'OK':
|
||||
ok++
|
||||
break
|
||||
case 'UNKNOWN':
|
||||
unknown++
|
||||
totalDeductions += severityWeights[finding.severity] * 0.3 * (categoryWeights[finding.category] || 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate category scores
|
||||
const categories = new Set(findings.map((f) => f.category))
|
||||
for (const category of categories) {
|
||||
const categoryFindings = findings.filter((f) => f.category === category)
|
||||
const categoryOk = categoryFindings.filter((f) => f.type === 'OK').length
|
||||
const categoryTotal = categoryFindings.length
|
||||
categoryScores[category] = categoryTotal > 0 ? Math.round((categoryOk / categoryTotal) * 100) : 100
|
||||
}
|
||||
|
||||
const totalScore = Math.max(0, Math.round(100 - (totalDeductions / maxPossibleDeductions) * 100))
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
categoryScores: categoryScores as Record<FindingCategory, number>,
|
||||
severityCounts,
|
||||
findingCounts: {
|
||||
total: findings.length,
|
||||
gaps,
|
||||
risks,
|
||||
ok,
|
||||
unknown,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
508
admin-v2/lib/sdk/vendor-compliance/contract-review/checklists.ts
Normal file
508
admin-v2/lib/sdk/vendor-compliance/contract-review/checklists.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
/**
|
||||
* Contract Review Checklists
|
||||
*
|
||||
* DSGVO Art. 28 compliance checklists for contract reviews
|
||||
*/
|
||||
|
||||
import { LocalizedText, FindingCategory } from '../types'
|
||||
|
||||
export interface ChecklistItem {
|
||||
id: string
|
||||
category: FindingCategory
|
||||
requirement: LocalizedText
|
||||
article: string
|
||||
description: LocalizedText
|
||||
checkPoints: LocalizedText[]
|
||||
isRequired: boolean
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
||||
}
|
||||
|
||||
export interface ChecklistGroup {
|
||||
id: string
|
||||
name: LocalizedText
|
||||
description: LocalizedText
|
||||
items: ChecklistItem[]
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ART. 28 DSGVO CHECKLIST
|
||||
// ==========================================
|
||||
|
||||
export const AVV_CHECKLIST: ChecklistItem[] = [
|
||||
// Art. 28 Abs. 3 lit. a - Weisungsgebundenheit
|
||||
{
|
||||
id: 'art28_3_a',
|
||||
category: 'INSTRUCTION',
|
||||
requirement: {
|
||||
de: 'Weisungsgebundenheit',
|
||||
en: 'Instruction binding',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. a DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter verarbeitet personenbezogene Daten nur auf dokumentierte Weisung des Verantwortlichen.',
|
||||
en: 'The processor processes personal data only on documented instructions from the controller.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Weisungsgebundenheit explizit vereinbart', en: 'Instruction binding explicitly agreed' },
|
||||
{ de: 'Dokumentierte Weisungen vorgesehen', en: 'Documented instructions provided for' },
|
||||
{ de: 'Hinweispflicht bei rechtswidrigen Weisungen', en: 'Obligation to notify of unlawful instructions' },
|
||||
{ de: 'Keine eigenständige Verarbeitung erlaubt', en: 'No independent processing allowed' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'CRITICAL',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. b - Vertraulichkeit
|
||||
{
|
||||
id: 'art28_3_b',
|
||||
category: 'CONFIDENTIALITY',
|
||||
requirement: {
|
||||
de: 'Vertraulichkeitsverpflichtung',
|
||||
en: 'Confidentiality obligation',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. b DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter gewährleistet, dass sich die zur Verarbeitung befugten Personen zur Vertraulichkeit verpflichtet haben.',
|
||||
en: 'The processor ensures that persons authorised to process personal data have committed themselves to confidentiality.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Vertraulichkeitsverpflichtung der Mitarbeiter', en: 'Confidentiality obligation of employees' },
|
||||
{ de: 'Gesetzliche Verschwiegenheitspflicht oder', en: 'Statutory confidentiality obligation or' },
|
||||
{ de: 'Vertragliche Verpflichtung', en: 'Contractual obligation' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'HIGH',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. c - Technische und organisatorische Maßnahmen
|
||||
{
|
||||
id: 'art28_3_c',
|
||||
category: 'TOM',
|
||||
requirement: {
|
||||
de: 'Technische und organisatorische Maßnahmen',
|
||||
en: 'Technical and organisational measures',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. c DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter trifft alle gemäß Art. 32 erforderlichen Maßnahmen.',
|
||||
en: 'The processor takes all measures required pursuant to Art. 32.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'TOM-Anlage vorhanden', en: 'TOM annex present' },
|
||||
{ de: 'TOM konkret und aktuell', en: 'TOM specific and current' },
|
||||
{ de: 'Bezug zu Art. 32 DSGVO', en: 'Reference to Art. 32 GDPR' },
|
||||
{ de: 'Aktualisierungspflicht vereinbart', en: 'Update obligation agreed' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'CRITICAL',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. d - Unterauftragsverarbeitung
|
||||
{
|
||||
id: 'art28_3_d',
|
||||
category: 'SUBPROCESSOR',
|
||||
requirement: {
|
||||
de: 'Unterauftragsverarbeitung',
|
||||
en: 'Sub-processing',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. d DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter nimmt keinen weiteren Auftragsverarbeiter ohne vorherige Genehmigung in Anspruch.',
|
||||
en: 'The processor does not engage another processor without prior authorisation.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Genehmigungserfordernis (allgemein oder spezifisch)', en: 'Authorisation requirement (general or specific)' },
|
||||
{ de: 'Bei allgemeiner Genehmigung: Informationspflicht', en: 'With general authorisation: notification obligation' },
|
||||
{ de: 'Einspruchsrecht des Verantwortlichen', en: 'Right of objection for controller' },
|
||||
{ de: 'Liste aktueller Unterauftragnehmer', en: 'List of current sub-processors' },
|
||||
{ de: 'Weitergabe der Pflichten an Unterauftragnehmer', en: 'Transfer of obligations to sub-processors' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'CRITICAL',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. e - Unterstützung bei Betroffenenrechten
|
||||
{
|
||||
id: 'art28_3_e',
|
||||
category: 'DATA_SUBJECT_RIGHTS',
|
||||
requirement: {
|
||||
de: 'Unterstützung bei Betroffenenrechten',
|
||||
en: 'Assistance with data subject rights',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. e DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter unterstützt den Verantwortlichen bei der Erfüllung der Betroffenenrechte.',
|
||||
en: 'The processor assists the controller in fulfilling data subject rights obligations.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Unterstützungspflicht vereinbart', en: 'Assistance obligation agreed' },
|
||||
{ de: 'Verfahren zur Weiterleitung von Anfragen', en: 'Procedure for forwarding requests' },
|
||||
{ de: 'Fristen für Unterstützung', en: 'Deadlines for assistance' },
|
||||
{ de: 'Kostenregelung', en: 'Cost arrangement' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'HIGH',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. f - Unterstützung bei DSFA
|
||||
{
|
||||
id: 'art28_3_f',
|
||||
category: 'GENERAL',
|
||||
requirement: {
|
||||
de: 'Unterstützung bei DSFA und Konsultation',
|
||||
en: 'Assistance with DPIA and consultation',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. f DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter unterstützt den Verantwortlichen bei der Einhaltung der Pflichten gemäß Art. 32-36.',
|
||||
en: 'The processor assists the controller in ensuring compliance with obligations pursuant to Art. 32-36.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Unterstützung bei DSFA', en: 'Assistance with DPIA' },
|
||||
{ de: 'Unterstützung bei vorheriger Konsultation', en: 'Assistance with prior consultation' },
|
||||
{ de: 'Bereitstellung notwendiger Informationen', en: 'Provision of necessary information' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'MEDIUM',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. g - Löschung/Rückgabe
|
||||
{
|
||||
id: 'art28_3_g',
|
||||
category: 'DELETION',
|
||||
requirement: {
|
||||
de: 'Löschung/Rückgabe nach Vertragsende',
|
||||
en: 'Deletion/return after contract end',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. g DSGVO',
|
||||
description: {
|
||||
de: 'Nach Abschluss der Verarbeitung werden alle personenbezogenen Daten gelöscht oder zurückgegeben.',
|
||||
en: 'After the end of processing, all personal data is deleted or returned.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Löschung oder Rückgabe nach Wahl des Verantwortlichen', en: 'Deletion or return at controller choice' },
|
||||
{ de: 'Frist für Löschung/Rückgabe (max. 30 Tage empfohlen)', en: 'Deadline for deletion/return (max. 30 days recommended)' },
|
||||
{ de: 'Löschung auch bei Unterauftragnehmern', en: 'Deletion also at sub-processors' },
|
||||
{ de: 'Löschbestätigung/Nachweis', en: 'Deletion confirmation/proof' },
|
||||
{ de: 'Ausnahme nur bei gesetzlicher Aufbewahrungspflicht', en: 'Exception only for legal retention obligation' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'CRITICAL',
|
||||
},
|
||||
|
||||
// Art. 28 Abs. 3 lit. h - Audit/Inspektion
|
||||
{
|
||||
id: 'art28_3_h',
|
||||
category: 'AUDIT_RIGHTS',
|
||||
requirement: {
|
||||
de: 'Audit- und Inspektionsrechte',
|
||||
en: 'Audit and inspection rights',
|
||||
},
|
||||
article: 'Art. 28 Abs. 3 lit. h DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter ermöglicht und unterstützt Überprüfungen durch den Verantwortlichen.',
|
||||
en: 'The processor enables and contributes to audits and inspections by the controller.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Auditrecht ausdrücklich vereinbart', en: 'Audit right explicitly agreed' },
|
||||
{ de: 'Vor-Ort-Inspektionen möglich', en: 'On-site inspections possible' },
|
||||
{ de: 'Angemessene Vorlaufzeit (max. 30 Tage)', en: 'Reasonable notice period (max. 30 days)' },
|
||||
{ de: 'Keine unangemessenen Einschränkungen', en: 'No unreasonable restrictions' },
|
||||
{ de: 'Bereitstellung aller relevanten Informationen', en: 'Provision of all relevant information' },
|
||||
{ de: 'Akzeptanz unabhängiger Prüfer', en: 'Acceptance of independent auditors' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'HIGH',
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// INCIDENT RESPONSE CHECKLIST
|
||||
// ==========================================
|
||||
|
||||
export const INCIDENT_CHECKLIST: ChecklistItem[] = [
|
||||
{
|
||||
id: 'incident_notification',
|
||||
category: 'INCIDENT',
|
||||
requirement: {
|
||||
de: 'Meldung von Datenschutzverletzungen',
|
||||
en: 'Notification of data breaches',
|
||||
},
|
||||
article: 'Art. 33 Abs. 2 DSGVO',
|
||||
description: {
|
||||
de: 'Der Auftragsverarbeiter meldet dem Verantwortlichen unverzüglich jede Datenschutzverletzung.',
|
||||
en: 'The processor notifies the controller without undue delay of any personal data breach.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Meldepflicht vereinbart', en: 'Notification obligation agreed' },
|
||||
{ de: 'Frist: Unverzüglich (max. 24-48h empfohlen)', en: 'Deadline: Without undue delay (max. 24-48h recommended)' },
|
||||
{ de: 'Mindestinhalt der Meldung definiert', en: 'Minimum content of notification defined' },
|
||||
{ de: 'Kontaktstelle für Meldungen', en: 'Contact point for notifications' },
|
||||
{ de: 'Unterstützung bei Dokumentation', en: 'Assistance with documentation' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'CRITICAL',
|
||||
},
|
||||
{
|
||||
id: 'incident_content',
|
||||
category: 'INCIDENT',
|
||||
requirement: {
|
||||
de: 'Inhalt der Incident-Meldung',
|
||||
en: 'Content of incident notification',
|
||||
},
|
||||
article: 'Art. 33 Abs. 3 DSGVO',
|
||||
description: {
|
||||
de: 'Die Meldung muss bestimmte Mindestinformationen enthalten.',
|
||||
en: 'The notification must contain certain minimum information.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Art der Verletzung', en: 'Nature of the breach' },
|
||||
{ de: 'Betroffene Datenkategorien', en: 'Affected data categories' },
|
||||
{ de: 'Ungefähre Anzahl betroffener Personen', en: 'Approximate number of affected persons' },
|
||||
{ de: 'Wahrscheinliche Folgen', en: 'Likely consequences' },
|
||||
{ de: 'Ergriffene Maßnahmen', en: 'Measures taken' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'HIGH',
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// THIRD COUNTRY TRANSFER CHECKLIST
|
||||
// ==========================================
|
||||
|
||||
export const TRANSFER_CHECKLIST: ChecklistItem[] = [
|
||||
{
|
||||
id: 'transfer_basis',
|
||||
category: 'TRANSFER',
|
||||
requirement: {
|
||||
de: 'Rechtsgrundlage für Drittlandtransfer',
|
||||
en: 'Legal basis for third country transfer',
|
||||
},
|
||||
article: 'Art. 44-49 DSGVO',
|
||||
description: {
|
||||
de: 'Drittlandtransfers nur auf Basis geeigneter Garantien.',
|
||||
en: 'Third country transfers only on the basis of appropriate safeguards.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Angemessenheitsbeschluss oder', en: 'Adequacy decision or' },
|
||||
{ de: 'Standardvertragsklauseln (SCC) oder', en: 'Standard contractual clauses (SCC) or' },
|
||||
{ de: 'Binding Corporate Rules (BCR) oder', en: 'Binding Corporate Rules (BCR) or' },
|
||||
{ de: 'Sonstige Ausnahme Art. 49', en: 'Other derogation Art. 49' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'CRITICAL',
|
||||
},
|
||||
{
|
||||
id: 'transfer_scc',
|
||||
category: 'TRANSFER',
|
||||
requirement: {
|
||||
de: 'Standardvertragsklauseln',
|
||||
en: 'Standard Contractual Clauses',
|
||||
},
|
||||
article: 'Art. 46 Abs. 2 lit. c DSGVO',
|
||||
description: {
|
||||
de: 'Bei SCC: Verwendung der aktuellen EU-Kommission-Klauseln.',
|
||||
en: 'With SCC: Use of current EU Commission clauses.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'SCC 2021 verwendet', en: 'SCC 2021 used' },
|
||||
{ de: 'Korrektes Modul gewählt', en: 'Correct module selected' },
|
||||
{ de: 'Anhänge vollständig ausgefüllt', en: 'Annexes completely filled out' },
|
||||
{ de: 'TIA durchgeführt (bei Risiko)', en: 'TIA conducted (if risk)' },
|
||||
{ de: 'Zusätzliche Maßnahmen dokumentiert', en: 'Additional measures documented' },
|
||||
],
|
||||
isRequired: false,
|
||||
severity: 'HIGH',
|
||||
},
|
||||
{
|
||||
id: 'transfer_tia',
|
||||
category: 'TRANSFER',
|
||||
requirement: {
|
||||
de: 'Transfer Impact Assessment',
|
||||
en: 'Transfer Impact Assessment',
|
||||
},
|
||||
article: 'Schrems II, EDSA 01/2020',
|
||||
description: {
|
||||
de: 'Bewertung der Risiken im Drittland.',
|
||||
en: 'Assessment of risks in the third country.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Rechtslage im Drittland analysiert', en: 'Legal situation in third country analyzed' },
|
||||
{ de: 'Zugriff durch Behörden bewertet', en: 'Access by authorities assessed' },
|
||||
{ de: 'Zusätzliche technische Maßnahmen', en: 'Additional technical measures' },
|
||||
{ de: 'Dokumentation der Bewertung', en: 'Documentation of assessment' },
|
||||
],
|
||||
isRequired: true,
|
||||
severity: 'HIGH',
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// SLA & LIABILITY CHECKLIST
|
||||
// ==========================================
|
||||
|
||||
export const SLA_LIABILITY_CHECKLIST: ChecklistItem[] = [
|
||||
{
|
||||
id: 'sla_availability',
|
||||
category: 'SLA',
|
||||
requirement: {
|
||||
de: 'Verfügbarkeit',
|
||||
en: 'Availability',
|
||||
},
|
||||
article: 'Vertragliche Vereinbarung',
|
||||
description: {
|
||||
de: 'Service Level Agreement für Verfügbarkeit des Dienstes.',
|
||||
en: 'Service Level Agreement for service availability.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Verfügbarkeit definiert (z.B. 99,9%)', en: 'Availability defined (e.g., 99.9%)' },
|
||||
{ de: 'Messzeitraum festgelegt', en: 'Measurement period defined' },
|
||||
{ de: 'Ausnahmen klar definiert', en: 'Exceptions clearly defined' },
|
||||
{ de: 'Konsequenzen bei Nichteinhaltung', en: 'Consequences of non-compliance' },
|
||||
],
|
||||
isRequired: false,
|
||||
severity: 'MEDIUM',
|
||||
},
|
||||
{
|
||||
id: 'liability_cap',
|
||||
category: 'LIABILITY',
|
||||
requirement: {
|
||||
de: 'Haftungsbegrenzung',
|
||||
en: 'Liability cap',
|
||||
},
|
||||
article: 'Vertragliche Vereinbarung',
|
||||
description: {
|
||||
de: 'Prüfung von Haftungsbegrenzungen und deren Auswirkungen.',
|
||||
en: 'Review of liability caps and their implications.',
|
||||
},
|
||||
checkPoints: [
|
||||
{ de: 'Haftungshöchstgrenze prüfen', en: 'Check liability cap' },
|
||||
{ de: 'Ausschluss von Vorsatz/grober Fahrlässigkeit', en: 'Exclusion of intent/gross negligence' },
|
||||
{ de: 'Freistellungsklauseln (Indemnity)', en: 'Indemnification clauses' },
|
||||
{ de: 'Versicherungsnachweis', en: 'Insurance proof' },
|
||||
],
|
||||
isRequired: false,
|
||||
severity: 'MEDIUM',
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// GROUPED CHECKLISTS
|
||||
// ==========================================
|
||||
|
||||
export const CHECKLIST_GROUPS: ChecklistGroup[] = [
|
||||
{
|
||||
id: 'avv',
|
||||
name: { de: 'Art. 28 DSGVO - AVV Pflichtinhalte', en: 'Art. 28 GDPR - DPA Mandatory Content' },
|
||||
description: {
|
||||
de: 'Prüfung der Pflichtinhalte eines Auftragsverarbeitungsvertrags',
|
||||
en: 'Review of mandatory content of a Data Processing Agreement',
|
||||
},
|
||||
items: AVV_CHECKLIST,
|
||||
},
|
||||
{
|
||||
id: 'incident',
|
||||
name: { de: 'Incident Response', en: 'Incident Response' },
|
||||
description: {
|
||||
de: 'Prüfung der Regelungen zu Datenschutzverletzungen',
|
||||
en: 'Review of data breach provisions',
|
||||
},
|
||||
items: INCIDENT_CHECKLIST,
|
||||
},
|
||||
{
|
||||
id: 'transfer',
|
||||
name: { de: 'Drittlandtransfer', en: 'Third Country Transfer' },
|
||||
description: {
|
||||
de: 'Prüfung der Regelungen zu Drittlandtransfers',
|
||||
en: 'Review of third country transfer provisions',
|
||||
},
|
||||
items: TRANSFER_CHECKLIST,
|
||||
},
|
||||
{
|
||||
id: 'sla_liability',
|
||||
name: { de: 'SLA & Haftung', en: 'SLA & Liability' },
|
||||
description: {
|
||||
de: 'Prüfung von Service Levels und Haftungsregelungen',
|
||||
en: 'Review of service levels and liability provisions',
|
||||
},
|
||||
items: SLA_LIABILITY_CHECKLIST,
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get all required checklist items
|
||||
*/
|
||||
export function getRequiredChecklistItems(): ChecklistItem[] {
|
||||
return [
|
||||
...AVV_CHECKLIST,
|
||||
...INCIDENT_CHECKLIST,
|
||||
...TRANSFER_CHECKLIST,
|
||||
].filter((item) => item.isRequired)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checklist items by category
|
||||
*/
|
||||
export function getChecklistItemsByCategory(category: FindingCategory): ChecklistItem[] {
|
||||
return [
|
||||
...AVV_CHECKLIST,
|
||||
...INCIDENT_CHECKLIST,
|
||||
...TRANSFER_CHECKLIST,
|
||||
...SLA_LIABILITY_CHECKLIST,
|
||||
].filter((item) => item.category === category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get checklist item by ID
|
||||
*/
|
||||
export function getChecklistItemById(id: string): ChecklistItem | undefined {
|
||||
return [
|
||||
...AVV_CHECKLIST,
|
||||
...INCIDENT_CHECKLIST,
|
||||
...TRANSFER_CHECKLIST,
|
||||
...SLA_LIABILITY_CHECKLIST,
|
||||
].find((item) => item.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate compliance score based on checklist results
|
||||
*/
|
||||
export function calculateChecklistComplianceScore(
|
||||
results: Map<string, 'PASS' | 'PARTIAL' | 'FAIL' | 'NOT_CHECKED'>
|
||||
): number {
|
||||
const allItems = [
|
||||
...AVV_CHECKLIST,
|
||||
...INCIDENT_CHECKLIST,
|
||||
...TRANSFER_CHECKLIST,
|
||||
]
|
||||
|
||||
let totalWeight = 0
|
||||
let earnedScore = 0
|
||||
|
||||
for (const item of allItems) {
|
||||
const weight = item.severity === 'CRITICAL' ? 3 : item.severity === 'HIGH' ? 2 : 1
|
||||
const result = results.get(item.id) || 'NOT_CHECKED'
|
||||
|
||||
totalWeight += weight
|
||||
|
||||
switch (result) {
|
||||
case 'PASS':
|
||||
earnedScore += weight
|
||||
break
|
||||
case 'PARTIAL':
|
||||
earnedScore += weight * 0.5
|
||||
break
|
||||
case 'FAIL':
|
||||
case 'NOT_CHECKED':
|
||||
// No score
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return totalWeight > 0 ? Math.round((earnedScore / totalWeight) * 100) : 0
|
||||
}
|
||||
573
admin-v2/lib/sdk/vendor-compliance/contract-review/findings.ts
Normal file
573
admin-v2/lib/sdk/vendor-compliance/contract-review/findings.ts
Normal file
@@ -0,0 +1,573 @@
|
||||
/**
|
||||
* Finding Types and Templates
|
||||
*
|
||||
* Pre-defined finding templates for contract reviews
|
||||
*/
|
||||
|
||||
import {
|
||||
FindingType,
|
||||
FindingCategory,
|
||||
FindingSeverity,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
|
||||
export interface FindingTemplate {
|
||||
id: string
|
||||
type: FindingType
|
||||
category: FindingCategory
|
||||
severity: FindingSeverity
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
recommendation: LocalizedText
|
||||
affectedRequirement: string
|
||||
triggerControls: string[]
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// FINDING SEVERITY DEFINITIONS
|
||||
// ==========================================
|
||||
|
||||
export const SEVERITY_DEFINITIONS: Record<FindingSeverity, {
|
||||
label: LocalizedText
|
||||
description: LocalizedText
|
||||
responseTime: LocalizedText
|
||||
color: string
|
||||
}> = {
|
||||
LOW: {
|
||||
label: { de: 'Niedrig', en: 'Low' },
|
||||
description: {
|
||||
de: 'Geringfügige Abweichung ohne wesentliche Auswirkungen',
|
||||
en: 'Minor deviation without significant impact',
|
||||
},
|
||||
responseTime: {
|
||||
de: 'Bei nächster Vertragserneuerung',
|
||||
en: 'At next contract renewal',
|
||||
},
|
||||
color: 'blue',
|
||||
},
|
||||
MEDIUM: {
|
||||
label: { de: 'Mittel', en: 'Medium' },
|
||||
description: {
|
||||
de: 'Abweichung mit potenziellen Auswirkungen auf Compliance',
|
||||
en: 'Deviation with potential impact on compliance',
|
||||
},
|
||||
responseTime: {
|
||||
de: 'Innerhalb von 90 Tagen',
|
||||
en: 'Within 90 days',
|
||||
},
|
||||
color: 'yellow',
|
||||
},
|
||||
HIGH: {
|
||||
label: { de: 'Hoch', en: 'High' },
|
||||
description: {
|
||||
de: 'Erhebliche Abweichung mit Auswirkungen auf Datenschutz-Compliance',
|
||||
en: 'Significant deviation with impact on data protection compliance',
|
||||
},
|
||||
responseTime: {
|
||||
de: 'Innerhalb von 30 Tagen',
|
||||
en: 'Within 30 days',
|
||||
},
|
||||
color: 'orange',
|
||||
},
|
||||
CRITICAL: {
|
||||
label: { de: 'Kritisch', en: 'Critical' },
|
||||
description: {
|
||||
de: 'Schwerwiegende Abweichung - unmittelbarer Handlungsbedarf',
|
||||
en: 'Serious deviation - immediate action required',
|
||||
},
|
||||
responseTime: {
|
||||
de: 'Sofort / vor Vertragsabschluss',
|
||||
en: 'Immediately / before contract signing',
|
||||
},
|
||||
color: 'red',
|
||||
},
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// FINDING TYPE DEFINITIONS
|
||||
// ==========================================
|
||||
|
||||
export const FINDING_TYPE_DEFINITIONS: Record<FindingType, {
|
||||
label: LocalizedText
|
||||
description: LocalizedText
|
||||
icon: string
|
||||
}> = {
|
||||
OK: {
|
||||
label: { de: 'Erfüllt', en: 'Fulfilled' },
|
||||
description: {
|
||||
de: 'Anforderung ist vollständig erfüllt',
|
||||
en: 'Requirement is fully met',
|
||||
},
|
||||
icon: 'check-circle',
|
||||
},
|
||||
GAP: {
|
||||
label: { de: 'Lücke', en: 'Gap' },
|
||||
description: {
|
||||
de: 'Anforderung fehlt oder ist unvollständig',
|
||||
en: 'Requirement is missing or incomplete',
|
||||
},
|
||||
icon: 'alert-circle',
|
||||
},
|
||||
RISK: {
|
||||
label: { de: 'Risiko', en: 'Risk' },
|
||||
description: {
|
||||
de: 'Potenzielles Risiko identifiziert',
|
||||
en: 'Potential risk identified',
|
||||
},
|
||||
icon: 'alert-triangle',
|
||||
},
|
||||
UNKNOWN: {
|
||||
label: { de: 'Unklar', en: 'Unknown' },
|
||||
description: {
|
||||
de: 'Nicht eindeutig bestimmbar',
|
||||
en: 'Cannot be clearly determined',
|
||||
},
|
||||
icon: 'help-circle',
|
||||
},
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// FINDING TEMPLATES
|
||||
// ==========================================
|
||||
|
||||
export const FINDING_TEMPLATES: FindingTemplate[] = [
|
||||
// AVV_CONTENT - Weisungsgebundenheit
|
||||
{
|
||||
id: 'tpl-avv-instruction-missing',
|
||||
type: 'GAP',
|
||||
category: 'AVV_CONTENT',
|
||||
severity: 'CRITICAL',
|
||||
title: {
|
||||
de: 'Weisungsgebundenheit fehlt',
|
||||
en: 'Instruction binding missing',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält keine Regelung zur Weisungsgebundenheit des Auftragsverarbeiters.',
|
||||
en: 'The contract does not contain a provision on the processor\'s instruction binding.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Klausel, die den Auftragsverarbeiter verpflichtet, personenbezogene Daten nur auf dokumentierte Weisung des Verantwortlichen zu verarbeiten.',
|
||||
en: 'Add a clause obligating the processor to process personal data only on documented instructions from the controller.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. a DSGVO',
|
||||
triggerControls: ['VND-CON-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-avv-instruction-weak',
|
||||
type: 'RISK',
|
||||
category: 'AVV_CONTENT',
|
||||
severity: 'MEDIUM',
|
||||
title: {
|
||||
de: 'Weisungsgebundenheit unvollständig',
|
||||
en: 'Instruction binding incomplete',
|
||||
},
|
||||
description: {
|
||||
de: 'Die Regelung zur Weisungsgebundenheit ist vorhanden, aber es fehlt die Hinweispflicht bei rechtswidrigen Weisungen.',
|
||||
en: 'The instruction binding provision exists, but the obligation to notify of unlawful instructions is missing.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Pflicht des Auftragsverarbeiters, den Verantwortlichen unverzüglich zu informieren, wenn eine Weisung nach seiner Auffassung gegen Datenschutzrecht verstößt.',
|
||||
en: 'Add an obligation for the processor to immediately inform the controller if an instruction, in their opinion, violates data protection law.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. a DSGVO',
|
||||
triggerControls: ['VND-CON-01'],
|
||||
},
|
||||
|
||||
// AVV_CONTENT - TOM
|
||||
{
|
||||
id: 'tpl-avv-tom-missing',
|
||||
type: 'GAP',
|
||||
category: 'TOM',
|
||||
severity: 'CRITICAL',
|
||||
title: {
|
||||
de: 'TOM-Anlage fehlt',
|
||||
en: 'TOM annex missing',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält keine technischen und organisatorischen Maßnahmen (TOM) als Anlage.',
|
||||
en: 'The contract does not contain technical and organizational measures (TOM) as an annex.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Fordern Sie eine detaillierte TOM-Anlage an, die die Maßnahmen gemäß Art. 32 DSGVO beschreibt.',
|
||||
en: 'Request a detailed TOM annex describing the measures according to Art. 32 GDPR.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. c DSGVO',
|
||||
triggerControls: ['VND-TOM-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-avv-tom-generic',
|
||||
type: 'RISK',
|
||||
category: 'TOM',
|
||||
severity: 'MEDIUM',
|
||||
title: {
|
||||
de: 'TOM zu unspezifisch',
|
||||
en: 'TOM too generic',
|
||||
},
|
||||
description: {
|
||||
de: 'Die TOM-Anlage enthält nur allgemeine Aussagen ohne konkrete Maßnahmen.',
|
||||
en: 'The TOM annex contains only general statements without specific measures.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Fordern Sie eine konkretere Beschreibung der Maßnahmen mit Bezug zum spezifischen Verarbeitungskontext an.',
|
||||
en: 'Request a more specific description of measures with reference to the specific processing context.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. c DSGVO',
|
||||
triggerControls: ['VND-TOM-01'],
|
||||
},
|
||||
|
||||
// SUBPROCESSOR
|
||||
{
|
||||
id: 'tpl-subprocessor-no-approval',
|
||||
type: 'GAP',
|
||||
category: 'SUBPROCESSOR',
|
||||
severity: 'CRITICAL',
|
||||
title: {
|
||||
de: 'Keine Genehmigungspflicht für Unterauftragnehmer',
|
||||
en: 'No approval requirement for sub-processors',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag regelt nicht, ob und wie der Einsatz von Unterauftragnehmern zu genehmigen ist.',
|
||||
en: 'The contract does not regulate whether and how the use of sub-processors must be approved.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Klausel, die entweder eine spezifische oder allgemeine Genehmigung für Unterauftragnehmer vorsieht, einschließlich Informations- und Einspruchsrechten.',
|
||||
en: 'Add a clause providing either specific or general authorization for sub-processors, including information and objection rights.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. d DSGVO',
|
||||
triggerControls: ['VND-SUB-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-subprocessor-no-list',
|
||||
type: 'RISK',
|
||||
category: 'SUBPROCESSOR',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Keine Liste der Unterauftragnehmer',
|
||||
en: 'No list of sub-processors',
|
||||
},
|
||||
description: {
|
||||
de: 'Es liegt keine aktuelle Liste der eingesetzten Unterauftragnehmer vor.',
|
||||
en: 'There is no current list of sub-processors used.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Fordern Sie eine vollständige Liste aller Unterauftragnehmer mit Name, Sitz und Verarbeitungszweck an.',
|
||||
en: 'Request a complete list of all sub-processors with name, location, and processing purpose.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. d DSGVO',
|
||||
triggerControls: ['VND-SUB-02'],
|
||||
},
|
||||
|
||||
// INCIDENT
|
||||
{
|
||||
id: 'tpl-incident-no-notification',
|
||||
type: 'GAP',
|
||||
category: 'INCIDENT',
|
||||
severity: 'CRITICAL',
|
||||
title: {
|
||||
de: 'Keine Meldepflicht bei Datenpannen',
|
||||
en: 'No notification obligation for data breaches',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält keine Regelung zur Meldung von Datenschutzverletzungen.',
|
||||
en: 'The contract does not contain a provision for reporting data breaches.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Klausel, die den Auftragsverarbeiter verpflichtet, Datenschutzverletzungen unverzüglich (innerhalb von 24-48h) zu melden.',
|
||||
en: 'Add a clause obligating the processor to report data breaches without undue delay (within 24-48h).',
|
||||
},
|
||||
affectedRequirement: 'Art. 33 Abs. 2 DSGVO',
|
||||
triggerControls: ['VND-INC-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-incident-long-deadline',
|
||||
type: 'RISK',
|
||||
category: 'INCIDENT',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Zu lange Meldefrist',
|
||||
en: 'Notification deadline too long',
|
||||
},
|
||||
description: {
|
||||
de: 'Die vereinbarte Meldefrist für Datenschutzverletzungen ist zu lang (>72h), um die eigene Meldepflicht einhalten zu können.',
|
||||
en: 'The agreed notification deadline for data breaches is too long (>72h) to meet own notification obligations.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Verkürzen Sie die Meldefrist auf maximal 24-48 Stunden, um ausreichend Zeit für die eigene Meldung an die Aufsichtsbehörde zu haben.',
|
||||
en: 'Reduce the notification deadline to a maximum of 24-48 hours to have sufficient time for own notification to the supervisory authority.',
|
||||
},
|
||||
affectedRequirement: 'Art. 33 DSGVO',
|
||||
triggerControls: ['VND-INC-01'],
|
||||
},
|
||||
|
||||
// AUDIT_RIGHTS
|
||||
{
|
||||
id: 'tpl-audit-no-right',
|
||||
type: 'GAP',
|
||||
category: 'AUDIT_RIGHTS',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Kein Auditrecht vereinbart',
|
||||
en: 'No audit right agreed',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält kein Recht des Verantwortlichen auf Prüfungen und Inspektionen.',
|
||||
en: 'The contract does not contain a right of the controller to audits and inspections.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie ein ausdrückliches Recht auf Vor-Ort-Inspektionen und die Einsicht in relevante Unterlagen.',
|
||||
en: 'Add an explicit right to on-site inspections and access to relevant documents.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. h DSGVO',
|
||||
triggerControls: ['VND-AUD-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-audit-restricted',
|
||||
type: 'RISK',
|
||||
category: 'AUDIT_RIGHTS',
|
||||
severity: 'MEDIUM',
|
||||
title: {
|
||||
de: 'Auditrecht eingeschränkt',
|
||||
en: 'Audit right restricted',
|
||||
},
|
||||
description: {
|
||||
de: 'Das Auditrecht ist durch unangemessene Einschränkungen (z.B. sehr lange Vorlaufzeit, Ausschluss von Vor-Ort-Inspektionen) begrenzt.',
|
||||
en: 'The audit right is limited by unreasonable restrictions (e.g., very long notice period, exclusion of on-site inspections).',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Verhandeln Sie angemessene Bedingungen für Audits (max. 30 Tage Vorlaufzeit, Möglichkeit zur Vor-Ort-Inspektion).',
|
||||
en: 'Negotiate reasonable audit conditions (max. 30 days notice, possibility for on-site inspection).',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. h DSGVO',
|
||||
triggerControls: ['VND-AUD-01'],
|
||||
},
|
||||
|
||||
// DELETION
|
||||
{
|
||||
id: 'tpl-deletion-no-clause',
|
||||
type: 'GAP',
|
||||
category: 'DELETION',
|
||||
severity: 'CRITICAL',
|
||||
title: {
|
||||
de: 'Keine Lösch-/Rückgaberegelung',
|
||||
en: 'No deletion/return clause',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag regelt nicht, was mit den Daten nach Vertragsende geschieht.',
|
||||
en: 'The contract does not regulate what happens to the data after contract termination.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Klausel zur Löschung oder Rückgabe aller personenbezogenen Daten nach Vertragsende (max. 30 Tage).',
|
||||
en: 'Add a clause for deletion or return of all personal data after contract end (max. 30 days).',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. g DSGVO',
|
||||
triggerControls: ['VND-DEL-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-deletion-no-confirmation',
|
||||
type: 'RISK',
|
||||
category: 'DELETION',
|
||||
severity: 'MEDIUM',
|
||||
title: {
|
||||
de: 'Keine Löschbestätigung vorgesehen',
|
||||
en: 'No deletion confirmation provided',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag sieht keine Bestätigung der Löschung durch den Auftragsverarbeiter vor.',
|
||||
en: 'The contract does not provide for confirmation of deletion by the processor.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Pflicht zur schriftlichen Bestätigung der vollständigen Löschung.',
|
||||
en: 'Add an obligation for written confirmation of complete deletion.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. g DSGVO',
|
||||
triggerControls: ['VND-DEL-01'],
|
||||
},
|
||||
|
||||
// TRANSFER
|
||||
{
|
||||
id: 'tpl-transfer-no-basis',
|
||||
type: 'GAP',
|
||||
category: 'TRANSFER',
|
||||
severity: 'CRITICAL',
|
||||
title: {
|
||||
de: 'Drittlandtransfer ohne Rechtsgrundlage',
|
||||
en: 'Third country transfer without legal basis',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag erlaubt oder impliziert Transfers in Drittländer ohne geeignete Garantien.',
|
||||
en: 'The contract allows or implies transfers to third countries without appropriate safeguards.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Vereinbaren Sie geeignete Garantien (SCC, BCR) oder beschränken Sie die Verarbeitung auf EU/EWR.',
|
||||
en: 'Agree on appropriate safeguards (SCC, BCR) or restrict processing to EU/EEA.',
|
||||
},
|
||||
affectedRequirement: 'Art. 44-49 DSGVO',
|
||||
triggerControls: ['VND-TRF-01'],
|
||||
},
|
||||
{
|
||||
id: 'tpl-transfer-old-scc',
|
||||
type: 'RISK',
|
||||
category: 'TRANSFER',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Veraltete Standardvertragsklauseln',
|
||||
en: 'Outdated Standard Contractual Clauses',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag verwendet die alten SCC (vor 2021), die nicht mehr gültig sind.',
|
||||
en: 'The contract uses old SCC (pre-2021) that are no longer valid.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Aktualisieren Sie auf die SCC 2021 (Durchführungsbeschluss (EU) 2021/914).',
|
||||
en: 'Update to SCC 2021 (Implementing Decision (EU) 2021/914).',
|
||||
},
|
||||
affectedRequirement: 'Art. 46 Abs. 2 lit. c DSGVO',
|
||||
triggerControls: ['VND-TRF-02'],
|
||||
},
|
||||
|
||||
// LIABILITY
|
||||
{
|
||||
id: 'tpl-liability-excessive-cap',
|
||||
type: 'RISK',
|
||||
category: 'LIABILITY',
|
||||
severity: 'MEDIUM',
|
||||
title: {
|
||||
de: 'Unangemessene Haftungsbegrenzung',
|
||||
en: 'Inappropriate liability cap',
|
||||
},
|
||||
description: {
|
||||
de: 'Die Haftungsbegrenzung ist sehr niedrig und könnte bei Datenschutzverletzungen problematisch sein.',
|
||||
en: 'The liability cap is very low and could be problematic in case of data protection violations.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Prüfen Sie, ob die Haftungsbegrenzung angemessen ist. Erwägen Sie eine Ausnahme für Datenschutzverletzungen oder eine höhere Obergrenze.',
|
||||
en: 'Check if the liability cap is appropriate. Consider an exception for data protection violations or a higher limit.',
|
||||
},
|
||||
affectedRequirement: 'Vertragliche Vereinbarung',
|
||||
triggerControls: [],
|
||||
},
|
||||
|
||||
// DATA_SUBJECT_RIGHTS
|
||||
{
|
||||
id: 'tpl-dsr-no-support',
|
||||
type: 'GAP',
|
||||
category: 'DATA_SUBJECT_RIGHTS',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Keine Unterstützung bei Betroffenenrechten',
|
||||
en: 'No support for data subject rights',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält keine Regelung zur Unterstützung bei der Erfüllung von Betroffenenrechten.',
|
||||
en: 'The contract does not contain a provision for support in fulfilling data subject rights.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Klausel zur Unterstützung bei Auskunft, Berichtigung, Löschung und anderen Betroffenenrechten.',
|
||||
en: 'Add a clause for support with access, rectification, deletion, and other data subject rights.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. e DSGVO',
|
||||
triggerControls: ['VND-DSR-01'],
|
||||
},
|
||||
|
||||
// CONFIDENTIALITY
|
||||
{
|
||||
id: 'tpl-confidentiality-missing',
|
||||
type: 'GAP',
|
||||
category: 'CONFIDENTIALITY',
|
||||
severity: 'HIGH',
|
||||
title: {
|
||||
de: 'Keine Vertraulichkeitsverpflichtung',
|
||||
en: 'No confidentiality obligation',
|
||||
},
|
||||
description: {
|
||||
de: 'Der Vertrag enthält keine Verpflichtung zur Vertraulichkeit der Mitarbeiter.',
|
||||
en: 'The contract does not contain an obligation for employee confidentiality.',
|
||||
},
|
||||
recommendation: {
|
||||
de: 'Ergänzen Sie eine Klausel, die die Verpflichtung der Mitarbeiter zur Vertraulichkeit sicherstellt.',
|
||||
en: 'Add a clause ensuring the obligation of employees to confidentiality.',
|
||||
},
|
||||
affectedRequirement: 'Art. 28 Abs. 3 lit. b DSGVO',
|
||||
triggerControls: ['VND-CON-02'],
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get finding template by ID
|
||||
*/
|
||||
export function getFindingTemplateById(id: string): FindingTemplate | undefined {
|
||||
return FINDING_TEMPLATES.find((t) => t.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get finding templates by category
|
||||
*/
|
||||
export function getFindingTemplatesByCategory(category: FindingCategory): FindingTemplate[] {
|
||||
return FINDING_TEMPLATES.filter((t) => t.category === category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get finding templates by type
|
||||
*/
|
||||
export function getFindingTemplatesByType(type: FindingType): FindingTemplate[] {
|
||||
return FINDING_TEMPLATES.filter((t) => t.type === type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get finding templates by severity
|
||||
*/
|
||||
export function getFindingTemplatesBySeverity(severity: FindingSeverity): FindingTemplate[] {
|
||||
return FINDING_TEMPLATES.filter((t) => t.severity === severity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get severity color class
|
||||
*/
|
||||
export function getSeverityColorClass(severity: FindingSeverity): string {
|
||||
return SEVERITY_DEFINITIONS[severity].color
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort findings by severity (critical first)
|
||||
*/
|
||||
export function sortFindingsBySeverity<T extends { severity: FindingSeverity }>(
|
||||
findings: T[]
|
||||
): T[] {
|
||||
const order: Record<FindingSeverity, number> = {
|
||||
CRITICAL: 0,
|
||||
HIGH: 1,
|
||||
MEDIUM: 2,
|
||||
LOW: 3,
|
||||
}
|
||||
|
||||
return [...findings].sort((a, b) => order[a.severity] - order[b.severity])
|
||||
}
|
||||
|
||||
/**
|
||||
* Count findings by severity
|
||||
*/
|
||||
export function countFindingsBySeverity<T extends { severity: FindingSeverity }>(
|
||||
findings: T[]
|
||||
): Record<FindingSeverity, number> {
|
||||
return findings.reduce(
|
||||
(acc, f) => {
|
||||
acc[f.severity] = (acc[f.severity] || 0) + 1
|
||||
return acc
|
||||
},
|
||||
{ LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0 } as Record<FindingSeverity, number>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get overall severity from list of findings
|
||||
*/
|
||||
export function getOverallSeverity(findings: { severity: FindingSeverity }[]): FindingSeverity {
|
||||
if (findings.some((f) => f.severity === 'CRITICAL')) return 'CRITICAL'
|
||||
if (findings.some((f) => f.severity === 'HIGH')) return 'HIGH'
|
||||
if (findings.some((f) => f.severity === 'MEDIUM')) return 'MEDIUM'
|
||||
return 'LOW'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Contract Review exports
|
||||
*/
|
||||
|
||||
export * from './analyzer'
|
||||
export * from './checklists'
|
||||
export * from './findings'
|
||||
72
admin-v2/lib/sdk/vendor-compliance/export/index.ts
Normal file
72
admin-v2/lib/sdk/vendor-compliance/export/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Export Utilities
|
||||
*
|
||||
* Functions for generating compliance reports and exports:
|
||||
* - VVT Export (Art. 30 DSGVO)
|
||||
* - RoPA Export (Art. 30(2) DSGVO)
|
||||
* - Vendor Audit Pack
|
||||
* - Management Summary
|
||||
*/
|
||||
|
||||
// ==========================================
|
||||
// VVT EXPORT
|
||||
// ==========================================
|
||||
export {
|
||||
// Types
|
||||
type VVTExportOptions,
|
||||
type VVTExportResult,
|
||||
type VVTRow,
|
||||
// Functions
|
||||
transformToVVTRows,
|
||||
generateVVTJson,
|
||||
generateVVTCsv,
|
||||
getLocalizedText,
|
||||
formatDataSubjects,
|
||||
formatPersonalData,
|
||||
formatLegalBasis,
|
||||
formatRecipients,
|
||||
formatTransfers,
|
||||
formatRetention,
|
||||
hasSpecialCategoryData,
|
||||
hasThirdCountryTransfers,
|
||||
generateComplianceSummary,
|
||||
} from './vvt-export'
|
||||
|
||||
// ==========================================
|
||||
// VENDOR AUDIT PACK
|
||||
// ==========================================
|
||||
export {
|
||||
// Types
|
||||
type VendorAuditPackOptions,
|
||||
type VendorAuditSection,
|
||||
type VendorAuditPackResult,
|
||||
// Functions
|
||||
generateVendorOverview,
|
||||
generateContactsSection,
|
||||
generateLocationsSection,
|
||||
generateTransferSection,
|
||||
generateCertificationsSection,
|
||||
generateContractsSection,
|
||||
generateFindingsSection,
|
||||
generateControlStatusSection,
|
||||
generateRiskSection,
|
||||
generateReviewScheduleSection,
|
||||
generateVendorAuditPack,
|
||||
generateVendorAuditJson,
|
||||
} from './vendor-audit-pack'
|
||||
|
||||
// ==========================================
|
||||
// ROPA EXPORT
|
||||
// ==========================================
|
||||
export {
|
||||
// Types
|
||||
type RoPAExportOptions,
|
||||
type RoPARow,
|
||||
type RoPAExportResult,
|
||||
// Functions
|
||||
transformToRoPARows,
|
||||
generateRoPAJson,
|
||||
generateRoPACsv,
|
||||
generateProcessorSummary,
|
||||
validateRoPACompleteness,
|
||||
} from './ropa-export'
|
||||
356
admin-v2/lib/sdk/vendor-compliance/export/ropa-export.ts
Normal file
356
admin-v2/lib/sdk/vendor-compliance/export/ropa-export.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* RoPA (Records of Processing Activities) Export Utilities
|
||||
*
|
||||
* Functions for generating Art. 30(2) DSGVO compliant
|
||||
* processor-perspective records.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ProcessingActivity,
|
||||
Vendor,
|
||||
Organization,
|
||||
LocalizedText,
|
||||
ThirdCountryTransfer,
|
||||
} from '../types'
|
||||
|
||||
// ==========================================
|
||||
// TYPES
|
||||
// ==========================================
|
||||
|
||||
export interface RoPAExportOptions {
|
||||
activities: ProcessingActivity[]
|
||||
vendors: Vendor[]
|
||||
organization: Organization
|
||||
language: 'de' | 'en'
|
||||
format: 'PDF' | 'DOCX' | 'XLSX'
|
||||
includeSubProcessors?: boolean
|
||||
includeTransfers?: boolean
|
||||
}
|
||||
|
||||
export interface RoPARow {
|
||||
// Art. 30(2)(a) - Controller info
|
||||
controllerName: string
|
||||
controllerAddress: string
|
||||
controllerDPO: string
|
||||
|
||||
// Art. 30(2)(b) - Processing categories
|
||||
processingCategories: string[]
|
||||
|
||||
// Art. 30(2)(c) - Third country transfers
|
||||
thirdCountryTransfers: string[]
|
||||
transferMechanisms: string[]
|
||||
|
||||
// Art. 30(2)(d) - Technical measures (general description)
|
||||
technicalMeasures: string[]
|
||||
|
||||
// Additional info
|
||||
subProcessors: string[]
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface RoPAExportResult {
|
||||
success: boolean
|
||||
filename: string
|
||||
mimeType: string
|
||||
content: string
|
||||
metadata: {
|
||||
controllerCount: number
|
||||
processingCount: number
|
||||
generatedAt: Date
|
||||
language: string
|
||||
processorName: string
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
function getLocalizedText(text: LocalizedText | undefined, lang: 'de' | 'en'): string {
|
||||
if (!text) return ''
|
||||
return text[lang] || text.de || ''
|
||||
}
|
||||
|
||||
function formatTransfers(transfers: ThirdCountryTransfer[], lang: 'de' | 'en'): string[] {
|
||||
const mechanismLabels: Record<string, LocalizedText> = {
|
||||
ADEQUACY_DECISION: { de: 'Angemessenheitsbeschluss', en: 'Adequacy Decision' },
|
||||
SCC_CONTROLLER: { de: 'SCC (C2C)', en: 'SCC (C2C)' },
|
||||
SCC_PROCESSOR: { de: 'SCC (C2P)', en: 'SCC (C2P)' },
|
||||
BCR: { de: 'Binding Corporate Rules', en: 'Binding Corporate Rules' },
|
||||
DEROGATION_CONSENT: { de: 'Ausdrückliche Einwilligung', en: 'Explicit Consent' },
|
||||
DEROGATION_CONTRACT: { de: 'Vertragserfüllung', en: 'Contract Performance' },
|
||||
CERTIFICATION: { de: 'Zertifizierung', en: 'Certification' },
|
||||
}
|
||||
|
||||
return transfers.map((t) => {
|
||||
const mechanism = mechanismLabels[t.transferMechanism]?.[lang] || t.transferMechanism
|
||||
return `${t.country}: ${t.recipient} (${mechanism})`
|
||||
})
|
||||
}
|
||||
|
||||
function formatAddress(address: { street?: string; city?: string; postalCode?: string; country?: string }): string {
|
||||
if (!address) return '-'
|
||||
const parts = [address.street, address.postalCode, address.city, address.country].filter(Boolean)
|
||||
return parts.join(', ') || '-'
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// EXPORT FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Transform activities to RoPA rows from processor perspective
|
||||
* Groups by controller (responsible party)
|
||||
*/
|
||||
export function transformToRoPARows(
|
||||
activities: ProcessingActivity[],
|
||||
vendors: Vendor[],
|
||||
lang: 'de' | 'en'
|
||||
): RoPARow[] {
|
||||
// Group activities by controller
|
||||
const byController = new Map<string, ProcessingActivity[]>()
|
||||
|
||||
for (const activity of activities) {
|
||||
const controllerName = activity.responsible.organizationName
|
||||
if (!byController.has(controllerName)) {
|
||||
byController.set(controllerName, [])
|
||||
}
|
||||
byController.get(controllerName)!.push(activity)
|
||||
}
|
||||
|
||||
// Transform to RoPA rows
|
||||
const rows: RoPARow[] = []
|
||||
|
||||
for (const [controllerName, controllerActivities] of byController) {
|
||||
const firstActivity = controllerActivities[0]
|
||||
|
||||
// Collect all processing categories
|
||||
const processingCategories = controllerActivities.map((a) =>
|
||||
getLocalizedText(a.name, lang)
|
||||
)
|
||||
|
||||
// Collect all third country transfers
|
||||
const allTransfers = controllerActivities.flatMap((a) => a.thirdCountryTransfers)
|
||||
const uniqueTransfers = formatTransfers(
|
||||
allTransfers.filter(
|
||||
(t, i, arr) => arr.findIndex((x) => x.country === t.country && x.recipient === t.recipient) === i
|
||||
),
|
||||
lang
|
||||
)
|
||||
|
||||
// Collect unique transfer mechanisms
|
||||
const uniqueMechanisms = [...new Set(allTransfers.map((t) => t.transferMechanism))]
|
||||
|
||||
// Collect all TOM references
|
||||
const allTOM = [...new Set(controllerActivities.flatMap((a) => a.technicalMeasures))]
|
||||
|
||||
// Collect sub-processors from vendors
|
||||
const subProcessorIds = [...new Set(controllerActivities.flatMap((a) => a.subProcessors))]
|
||||
const subProcessorNames = subProcessorIds
|
||||
.map((id) => vendors.find((v) => v.id === id)?.name)
|
||||
.filter((name): name is string => !!name)
|
||||
|
||||
// DPO contact
|
||||
const dpoContact = firstActivity.dpoContact
|
||||
? `${firstActivity.dpoContact.name} (${firstActivity.dpoContact.email})`
|
||||
: firstActivity.responsible.contact
|
||||
? `${firstActivity.responsible.contact.name} (${firstActivity.responsible.contact.email})`
|
||||
: '-'
|
||||
|
||||
rows.push({
|
||||
controllerName,
|
||||
controllerAddress: formatAddress(firstActivity.responsible.address),
|
||||
controllerDPO: dpoContact,
|
||||
processingCategories,
|
||||
thirdCountryTransfers: uniqueTransfers,
|
||||
transferMechanisms: uniqueMechanisms,
|
||||
technicalMeasures: allTOM,
|
||||
subProcessors: subProcessorNames,
|
||||
status: controllerActivities.every((a) => a.status === 'APPROVED') ? 'APPROVED' : 'PENDING',
|
||||
})
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate RoPA as JSON
|
||||
*/
|
||||
export function generateRoPAJson(options: RoPAExportOptions): RoPAExportResult {
|
||||
const rows = transformToRoPARows(options.activities, options.vendors, options.language)
|
||||
|
||||
const exportData = {
|
||||
metadata: {
|
||||
processor: {
|
||||
name: options.organization.name,
|
||||
address: formatAddress(options.organization.address),
|
||||
dpo: options.organization.dpoContact
|
||||
? `${options.organization.dpoContact.name} (${options.organization.dpoContact.email})`
|
||||
: '-',
|
||||
},
|
||||
generatedAt: new Date().toISOString(),
|
||||
language: options.language,
|
||||
gdprArticle: 'Art. 30(2) DSGVO',
|
||||
version: '1.0',
|
||||
},
|
||||
records: rows.map((row, index) => ({
|
||||
recordNumber: index + 1,
|
||||
...row,
|
||||
})),
|
||||
summary: {
|
||||
controllerCount: rows.length,
|
||||
totalProcessingCategories: rows.reduce((sum, r) => sum + r.processingCategories.length, 0),
|
||||
withThirdCountryTransfers: rows.filter((r) => r.thirdCountryTransfers.length > 0).length,
|
||||
uniqueSubProcessors: [...new Set(rows.flatMap((r) => r.subProcessors))].length,
|
||||
},
|
||||
}
|
||||
|
||||
const content = JSON.stringify(exportData, null, 2)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filename: `RoPA_${options.organization.name.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.json`,
|
||||
mimeType: 'application/json',
|
||||
content,
|
||||
metadata: {
|
||||
controllerCount: rows.length,
|
||||
processingCount: options.activities.length,
|
||||
generatedAt: new Date(),
|
||||
language: options.language,
|
||||
processorName: options.organization.name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate RoPA as CSV
|
||||
*/
|
||||
export function generateRoPACsv(options: RoPAExportOptions): string {
|
||||
const rows = transformToRoPARows(options.activities, options.vendors, options.language)
|
||||
const lang = options.language
|
||||
|
||||
const headers =
|
||||
lang === 'de'
|
||||
? [
|
||||
'Nr.',
|
||||
'Verantwortlicher',
|
||||
'Anschrift',
|
||||
'DSB',
|
||||
'Verarbeitungskategorien',
|
||||
'Drittlandtransfers',
|
||||
'Transfermechanismen',
|
||||
'TOM',
|
||||
'Unterauftragnehmer',
|
||||
'Status',
|
||||
]
|
||||
: [
|
||||
'No.',
|
||||
'Controller',
|
||||
'Address',
|
||||
'DPO',
|
||||
'Processing Categories',
|
||||
'Third Country Transfers',
|
||||
'Transfer Mechanisms',
|
||||
'Technical Measures',
|
||||
'Sub-Processors',
|
||||
'Status',
|
||||
]
|
||||
|
||||
const csvRows = rows.map((row, index) => [
|
||||
(index + 1).toString(),
|
||||
row.controllerName,
|
||||
row.controllerAddress,
|
||||
row.controllerDPO,
|
||||
row.processingCategories.join('; '),
|
||||
row.thirdCountryTransfers.join('; '),
|
||||
row.transferMechanisms.join('; '),
|
||||
row.technicalMeasures.join('; '),
|
||||
row.subProcessors.join('; '),
|
||||
row.status,
|
||||
])
|
||||
|
||||
const escape = (val: string) => `"${val.replace(/"/g, '""')}"`
|
||||
|
||||
return [
|
||||
headers.map(escape).join(','),
|
||||
...csvRows.map((row) => row.map(escape).join(',')),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate processor summary for RoPA
|
||||
*/
|
||||
export function generateProcessorSummary(
|
||||
activities: ProcessingActivity[],
|
||||
vendors: Vendor[],
|
||||
lang: 'de' | 'en'
|
||||
): {
|
||||
totalControllers: number
|
||||
totalCategories: number
|
||||
withTransfers: number
|
||||
subProcessorCount: number
|
||||
pendingApproval: number
|
||||
} {
|
||||
const rows = transformToRoPARows(activities, vendors, lang)
|
||||
|
||||
return {
|
||||
totalControllers: rows.length,
|
||||
totalCategories: rows.reduce((sum, r) => sum + r.processingCategories.length, 0),
|
||||
withTransfers: rows.filter((r) => r.thirdCountryTransfers.length > 0).length,
|
||||
subProcessorCount: [...new Set(rows.flatMap((r) => r.subProcessors))].length,
|
||||
pendingApproval: rows.filter((r) => r.status !== 'APPROVED').length,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate RoPA completeness
|
||||
*/
|
||||
export function validateRoPACompleteness(
|
||||
rows: RoPARow[],
|
||||
lang: 'de' | 'en'
|
||||
): { isComplete: boolean; issues: string[] } {
|
||||
const issues: string[] = []
|
||||
|
||||
for (const row of rows) {
|
||||
// Art. 30(2)(a) - Controller info
|
||||
if (!row.controllerName) {
|
||||
issues.push(
|
||||
lang === 'de'
|
||||
? 'Name des Verantwortlichen fehlt'
|
||||
: 'Controller name missing'
|
||||
)
|
||||
}
|
||||
|
||||
// Art. 30(2)(b) - Processing categories
|
||||
if (row.processingCategories.length === 0) {
|
||||
issues.push(
|
||||
lang === 'de'
|
||||
? `${row.controllerName}: Keine Verarbeitungskategorien angegeben`
|
||||
: `${row.controllerName}: No processing categories specified`
|
||||
)
|
||||
}
|
||||
|
||||
// Art. 30(2)(c) - Transfers without mechanism
|
||||
if (row.thirdCountryTransfers.length > 0 && row.transferMechanisms.length === 0) {
|
||||
issues.push(
|
||||
lang === 'de'
|
||||
? `${row.controllerName}: Drittlandtransfer ohne Rechtsgrundlage`
|
||||
: `${row.controllerName}: Third country transfer without legal basis`
|
||||
)
|
||||
}
|
||||
|
||||
// Art. 30(2)(d) - TOM
|
||||
if (row.technicalMeasures.length === 0) {
|
||||
issues.push(
|
||||
lang === 'de'
|
||||
? `${row.controllerName}: Keine TOM angegeben`
|
||||
: `${row.controllerName}: No technical measures specified`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isComplete: issues.length === 0,
|
||||
issues,
|
||||
}
|
||||
}
|
||||
489
admin-v2/lib/sdk/vendor-compliance/export/vendor-audit-pack.ts
Normal file
489
admin-v2/lib/sdk/vendor-compliance/export/vendor-audit-pack.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
/**
|
||||
* Vendor Audit Pack Export Utilities
|
||||
*
|
||||
* Functions for generating comprehensive vendor audit documentation.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Vendor,
|
||||
ContractDocument,
|
||||
Finding,
|
||||
ControlInstance,
|
||||
RiskAssessment,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
|
||||
// ==========================================
|
||||
// TYPES
|
||||
// ==========================================
|
||||
|
||||
export interface VendorAuditPackOptions {
|
||||
vendor: Vendor
|
||||
contracts: ContractDocument[]
|
||||
findings: Finding[]
|
||||
controlInstances: ControlInstance[]
|
||||
riskAssessment?: RiskAssessment
|
||||
language: 'de' | 'en'
|
||||
format: 'PDF' | 'DOCX'
|
||||
includeContracts?: boolean
|
||||
includeFindings?: boolean
|
||||
includeControlStatus?: boolean
|
||||
includeRiskAssessment?: boolean
|
||||
}
|
||||
|
||||
export interface VendorAuditSection {
|
||||
title: string
|
||||
content: string | Record<string, unknown>
|
||||
level: 1 | 2 | 3
|
||||
}
|
||||
|
||||
export interface VendorAuditPackResult {
|
||||
success: boolean
|
||||
filename: string
|
||||
sections: VendorAuditSection[]
|
||||
metadata: {
|
||||
vendorName: string
|
||||
generatedAt: Date
|
||||
language: string
|
||||
contractCount: number
|
||||
findingCount: number
|
||||
openFindingCount: number
|
||||
riskLevel: string
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CONSTANTS
|
||||
// ==========================================
|
||||
|
||||
const VENDOR_ROLE_LABELS: Record<string, LocalizedText> = {
|
||||
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
|
||||
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
|
||||
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
|
||||
SUB_PROCESSOR: { de: 'Unterauftragnehmer', en: 'Sub-Processor' },
|
||||
THIRD_PARTY: { de: 'Dritter', en: 'Third Party' },
|
||||
}
|
||||
|
||||
const SERVICE_CATEGORY_LABELS: Record<string, LocalizedText> = {
|
||||
HOSTING: { de: 'Hosting', en: 'Hosting' },
|
||||
CLOUD_INFRASTRUCTURE: { de: 'Cloud-Infrastruktur', en: 'Cloud Infrastructure' },
|
||||
ANALYTICS: { de: 'Analytics', en: 'Analytics' },
|
||||
CRM: { de: 'CRM-System', en: 'CRM System' },
|
||||
ERP: { de: 'ERP-System', en: 'ERP System' },
|
||||
HR_SOFTWARE: { de: 'HR-Software', en: 'HR Software' },
|
||||
PAYMENT: { de: 'Zahlungsabwicklung', en: 'Payment Processing' },
|
||||
EMAIL: { de: 'E-Mail-Dienst', en: 'Email Service' },
|
||||
MARKETING: { de: 'Marketing', en: 'Marketing' },
|
||||
SUPPORT: { de: 'Support', en: 'Support' },
|
||||
SECURITY: { de: 'Sicherheit', en: 'Security' },
|
||||
INTEGRATION: { de: 'Integration', en: 'Integration' },
|
||||
CONSULTING: { de: 'Beratung', en: 'Consulting' },
|
||||
LEGAL: { de: 'Recht', en: 'Legal' },
|
||||
ACCOUNTING: { de: 'Buchhaltung', en: 'Accounting' },
|
||||
COMMUNICATION: { de: 'Kommunikation', en: 'Communication' },
|
||||
STORAGE: { de: 'Speicher', en: 'Storage' },
|
||||
OTHER: { de: 'Sonstiges', en: 'Other' },
|
||||
}
|
||||
|
||||
const DATA_ACCESS_LABELS: Record<string, LocalizedText> = {
|
||||
NONE: { de: 'Kein Zugriff', en: 'No Access' },
|
||||
POTENTIAL: { de: 'Potentieller Zugriff', en: 'Potential Access' },
|
||||
ADMINISTRATIVE: { de: 'Administrativer Zugriff', en: 'Administrative Access' },
|
||||
CONTENT: { de: 'Inhaltlicher Zugriff', en: 'Content Access' },
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
function getLabel(labels: Record<string, LocalizedText>, key: string, lang: 'de' | 'en'): string {
|
||||
return labels[key]?.[lang] || key
|
||||
}
|
||||
|
||||
function formatDate(date: Date | string | undefined, lang: 'de' | 'en'): string {
|
||||
if (!date) return lang === 'de' ? 'Nicht angegeben' : 'Not specified'
|
||||
const d = new Date(date)
|
||||
return d.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function getRiskLevelLabel(score: number, lang: 'de' | 'en'): string {
|
||||
if (score >= 70) return lang === 'de' ? 'KRITISCH' : 'CRITICAL'
|
||||
if (score >= 50) return lang === 'de' ? 'HOCH' : 'HIGH'
|
||||
if (score >= 30) return lang === 'de' ? 'MITTEL' : 'MEDIUM'
|
||||
return lang === 'de' ? 'NIEDRIG' : 'LOW'
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SECTION GENERATORS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Generate vendor overview section
|
||||
*/
|
||||
export function generateVendorOverview(
|
||||
vendor: Vendor,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Vendor-Übersicht' : 'Vendor Overview'
|
||||
|
||||
const content = {
|
||||
name: vendor.name,
|
||||
legalForm: vendor.legalForm || '-',
|
||||
country: vendor.country,
|
||||
address: vendor.address
|
||||
? `${vendor.address.street}, ${vendor.address.postalCode} ${vendor.address.city}`
|
||||
: '-',
|
||||
website: vendor.website || '-',
|
||||
role: getLabel(VENDOR_ROLE_LABELS, vendor.role, lang),
|
||||
serviceCategory: getLabel(SERVICE_CATEGORY_LABELS, vendor.serviceCategory, lang),
|
||||
serviceDescription: vendor.serviceDescription,
|
||||
dataAccessLevel: getLabel(DATA_ACCESS_LABELS, vendor.dataAccessLevel, lang),
|
||||
status: vendor.status,
|
||||
createdAt: formatDate(vendor.createdAt, lang),
|
||||
}
|
||||
|
||||
return { title, content, level: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate contacts section
|
||||
*/
|
||||
export function generateContactsSection(
|
||||
vendor: Vendor,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Kontaktdaten' : 'Contact Information'
|
||||
|
||||
const content = {
|
||||
primaryContact: {
|
||||
name: vendor.primaryContact.name,
|
||||
email: vendor.primaryContact.email,
|
||||
phone: vendor.primaryContact.phone || '-',
|
||||
department: vendor.primaryContact.department || '-',
|
||||
role: vendor.primaryContact.role || '-',
|
||||
},
|
||||
dpoContact: vendor.dpoContact
|
||||
? {
|
||||
name: vendor.dpoContact.name,
|
||||
email: vendor.dpoContact.email,
|
||||
phone: vendor.dpoContact.phone || '-',
|
||||
}
|
||||
: null,
|
||||
}
|
||||
|
||||
return { title, content, level: 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate processing locations section
|
||||
*/
|
||||
export function generateLocationsSection(
|
||||
vendor: Vendor,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Verarbeitungsstandorte' : 'Processing Locations'
|
||||
|
||||
const locations = vendor.processingLocations.map((loc) => ({
|
||||
country: loc.country,
|
||||
region: loc.region || '-',
|
||||
city: loc.city || '-',
|
||||
dataCenter: loc.dataCenter || '-',
|
||||
isEU: loc.isEU,
|
||||
isAdequate: loc.isAdequate,
|
||||
}))
|
||||
|
||||
return { title, content: { locations }, level: 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate transfer mechanisms section
|
||||
*/
|
||||
export function generateTransferSection(
|
||||
vendor: Vendor,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Drittlandtransfers' : 'Third Country Transfers'
|
||||
|
||||
const mechanismLabels: Record<string, LocalizedText> = {
|
||||
ADEQUACY_DECISION: { de: 'Angemessenheitsbeschluss', en: 'Adequacy Decision' },
|
||||
SCC_CONTROLLER: { de: 'SCC (C2C)', en: 'SCC (C2C)' },
|
||||
SCC_PROCESSOR: { de: 'SCC (C2P)', en: 'SCC (C2P)' },
|
||||
BCR: { de: 'Binding Corporate Rules', en: 'Binding Corporate Rules' },
|
||||
DEROGATION_CONSENT: { de: 'Ausdrückliche Einwilligung', en: 'Explicit Consent' },
|
||||
DEROGATION_CONTRACT: { de: 'Vertragserfüllung', en: 'Contract Performance' },
|
||||
CERTIFICATION: { de: 'Zertifizierung', en: 'Certification' },
|
||||
CODE_OF_CONDUCT: { de: 'Verhaltensregeln', en: 'Code of Conduct' },
|
||||
}
|
||||
|
||||
const mechanisms = vendor.transferMechanisms.map((tm) => ({
|
||||
type: tm,
|
||||
label: mechanismLabels[tm]?.[lang] || tm,
|
||||
}))
|
||||
|
||||
return { title, content: { mechanisms }, level: 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate certifications section
|
||||
*/
|
||||
export function generateCertificationsSection(
|
||||
vendor: Vendor,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Zertifizierungen' : 'Certifications'
|
||||
|
||||
const certifications = vendor.certifications.map((cert) => ({
|
||||
type: cert.type,
|
||||
issuer: cert.issuer || '-',
|
||||
issuedDate: cert.issuedDate ? formatDate(cert.issuedDate, lang) : '-',
|
||||
expirationDate: cert.expirationDate ? formatDate(cert.expirationDate, lang) : '-',
|
||||
scope: cert.scope || '-',
|
||||
certificateNumber: cert.certificateNumber || '-',
|
||||
}))
|
||||
|
||||
return { title, content: { certifications }, level: 2 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate contracts section
|
||||
*/
|
||||
export function generateContractsSection(
|
||||
contracts: ContractDocument[],
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Verträge' : 'Contracts'
|
||||
|
||||
const contractList = contracts.map((contract) => ({
|
||||
type: contract.documentType,
|
||||
fileName: contract.originalName,
|
||||
version: contract.version,
|
||||
effectiveDate: contract.effectiveDate ? formatDate(contract.effectiveDate, lang) : '-',
|
||||
expirationDate: contract.expirationDate ? formatDate(contract.expirationDate, lang) : '-',
|
||||
status: contract.status,
|
||||
reviewStatus: contract.reviewStatus,
|
||||
complianceScore: contract.complianceScore !== undefined ? `${contract.complianceScore}%` : '-',
|
||||
}))
|
||||
|
||||
return { title, content: { contracts: contractList }, level: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate findings section
|
||||
*/
|
||||
export function generateFindingsSection(
|
||||
findings: Finding[],
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Findings' : 'Findings'
|
||||
|
||||
const summary = {
|
||||
total: findings.length,
|
||||
open: findings.filter((f) => f.status === 'OPEN').length,
|
||||
inProgress: findings.filter((f) => f.status === 'IN_PROGRESS').length,
|
||||
resolved: findings.filter((f) => f.status === 'RESOLVED').length,
|
||||
critical: findings.filter((f) => f.severity === 'CRITICAL').length,
|
||||
high: findings.filter((f) => f.severity === 'HIGH').length,
|
||||
medium: findings.filter((f) => f.severity === 'MEDIUM').length,
|
||||
low: findings.filter((f) => f.severity === 'LOW').length,
|
||||
}
|
||||
|
||||
const findingList = findings.map((finding) => ({
|
||||
id: finding.id.slice(0, 8),
|
||||
type: finding.type,
|
||||
category: finding.category,
|
||||
severity: finding.severity,
|
||||
title: finding.title[lang] || finding.title.de,
|
||||
description: finding.description[lang] || finding.description.de,
|
||||
recommendation: finding.recommendation
|
||||
? finding.recommendation[lang] || finding.recommendation.de
|
||||
: '-',
|
||||
status: finding.status,
|
||||
affectedRequirement: finding.affectedRequirement || '-',
|
||||
createdAt: formatDate(finding.createdAt, lang),
|
||||
resolvedAt: finding.resolvedAt ? formatDate(finding.resolvedAt, lang) : '-',
|
||||
}))
|
||||
|
||||
return { title, content: { summary, findings: findingList }, level: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate control status section
|
||||
*/
|
||||
export function generateControlStatusSection(
|
||||
controlInstances: ControlInstance[],
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Control-Status' : 'Control Status'
|
||||
|
||||
const summary = {
|
||||
total: controlInstances.length,
|
||||
pass: controlInstances.filter((c) => c.status === 'PASS').length,
|
||||
partial: controlInstances.filter((c) => c.status === 'PARTIAL').length,
|
||||
fail: controlInstances.filter((c) => c.status === 'FAIL').length,
|
||||
notApplicable: controlInstances.filter((c) => c.status === 'NOT_APPLICABLE').length,
|
||||
planned: controlInstances.filter((c) => c.status === 'PLANNED').length,
|
||||
}
|
||||
|
||||
const passRate =
|
||||
summary.total > 0
|
||||
? Math.round((summary.pass / (summary.total - summary.notApplicable)) * 100)
|
||||
: 0
|
||||
|
||||
const controlList = controlInstances.map((ci) => ({
|
||||
controlId: ci.controlId,
|
||||
status: ci.status,
|
||||
lastAssessedAt: formatDate(ci.lastAssessedAt, lang),
|
||||
nextAssessmentDate: formatDate(ci.nextAssessmentDate, lang),
|
||||
notes: ci.notes || '-',
|
||||
}))
|
||||
|
||||
return {
|
||||
title,
|
||||
content: { summary, passRate: `${passRate}%`, controls: controlList },
|
||||
level: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate risk assessment section
|
||||
*/
|
||||
export function generateRiskSection(
|
||||
vendor: Vendor,
|
||||
riskAssessment: RiskAssessment | undefined,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Risikobewertung' : 'Risk Assessment'
|
||||
|
||||
const content = {
|
||||
inherentRiskScore: vendor.inherentRiskScore,
|
||||
inherentRiskLevel: getRiskLevelLabel(vendor.inherentRiskScore, lang),
|
||||
residualRiskScore: vendor.residualRiskScore,
|
||||
residualRiskLevel: getRiskLevelLabel(vendor.residualRiskScore, lang),
|
||||
manualAdjustment: vendor.manualRiskAdjustment || 0,
|
||||
justification: vendor.riskJustification || '-',
|
||||
assessment: riskAssessment
|
||||
? {
|
||||
assessedBy: riskAssessment.assessedBy,
|
||||
assessedAt: formatDate(riskAssessment.assessedAt, lang),
|
||||
approvedBy: riskAssessment.approvedBy || '-',
|
||||
approvedAt: riskAssessment.approvedAt
|
||||
? formatDate(riskAssessment.approvedAt, lang)
|
||||
: '-',
|
||||
nextAssessmentDate: formatDate(riskAssessment.nextAssessmentDate, lang),
|
||||
riskFactors: riskAssessment.riskFactors.map((rf) => ({
|
||||
name: rf.name[lang] || rf.name.de,
|
||||
category: rf.category,
|
||||
value: rf.value,
|
||||
weight: rf.weight,
|
||||
rationale: rf.rationale || '-',
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
|
||||
return { title, content, level: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate review schedule section
|
||||
*/
|
||||
export function generateReviewScheduleSection(
|
||||
vendor: Vendor,
|
||||
lang: 'de' | 'en'
|
||||
): VendorAuditSection {
|
||||
const title = lang === 'de' ? 'Review-Zeitplan' : 'Review Schedule'
|
||||
|
||||
const frequencyLabels: Record<string, LocalizedText> = {
|
||||
QUARTERLY: { de: 'Vierteljährlich', en: 'Quarterly' },
|
||||
SEMI_ANNUAL: { de: 'Halbjährlich', en: 'Semi-Annual' },
|
||||
ANNUAL: { de: 'Jährlich', en: 'Annual' },
|
||||
BIENNIAL: { de: 'Alle 2 Jahre', en: 'Biennial' },
|
||||
}
|
||||
|
||||
const content = {
|
||||
reviewFrequency: getLabel(frequencyLabels, vendor.reviewFrequency, lang),
|
||||
lastReviewDate: vendor.lastReviewDate ? formatDate(vendor.lastReviewDate, lang) : '-',
|
||||
nextReviewDate: vendor.nextReviewDate ? formatDate(vendor.nextReviewDate, lang) : '-',
|
||||
isOverdue:
|
||||
vendor.nextReviewDate && new Date(vendor.nextReviewDate) < new Date()
|
||||
? lang === 'de'
|
||||
? 'Ja'
|
||||
: 'Yes'
|
||||
: lang === 'de'
|
||||
? 'Nein'
|
||||
: 'No',
|
||||
}
|
||||
|
||||
return { title, content, level: 2 }
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MAIN EXPORT FUNCTION
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Generate complete vendor audit pack
|
||||
*/
|
||||
export function generateVendorAuditPack(options: VendorAuditPackOptions): VendorAuditPackResult {
|
||||
const { vendor, contracts, findings, controlInstances, riskAssessment, language } = options
|
||||
|
||||
const sections: VendorAuditSection[] = []
|
||||
|
||||
// Always include vendor overview
|
||||
sections.push(generateVendorOverview(vendor, language))
|
||||
sections.push(generateContactsSection(vendor, language))
|
||||
sections.push(generateLocationsSection(vendor, language))
|
||||
sections.push(generateTransferSection(vendor, language))
|
||||
sections.push(generateCertificationsSection(vendor, language))
|
||||
sections.push(generateReviewScheduleSection(vendor, language))
|
||||
|
||||
// Contracts (optional)
|
||||
if (options.includeContracts !== false && contracts.length > 0) {
|
||||
sections.push(generateContractsSection(contracts, language))
|
||||
}
|
||||
|
||||
// Findings (optional)
|
||||
if (options.includeFindings !== false && findings.length > 0) {
|
||||
sections.push(generateFindingsSection(findings, language))
|
||||
}
|
||||
|
||||
// Control status (optional)
|
||||
if (options.includeControlStatus !== false && controlInstances.length > 0) {
|
||||
sections.push(generateControlStatusSection(controlInstances, language))
|
||||
}
|
||||
|
||||
// Risk assessment (optional)
|
||||
if (options.includeRiskAssessment !== false) {
|
||||
sections.push(generateRiskSection(vendor, riskAssessment, language))
|
||||
}
|
||||
|
||||
// Calculate metadata
|
||||
const openFindings = findings.filter((f) => f.status === 'OPEN').length
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filename: `Vendor_Audit_${vendor.name.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.${options.format.toLowerCase()}`,
|
||||
sections,
|
||||
metadata: {
|
||||
vendorName: vendor.name,
|
||||
generatedAt: new Date(),
|
||||
language,
|
||||
contractCount: contracts.length,
|
||||
findingCount: findings.length,
|
||||
openFindingCount: openFindings,
|
||||
riskLevel: getRiskLevelLabel(vendor.inherentRiskScore, language),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate vendor audit pack as JSON
|
||||
*/
|
||||
export function generateVendorAuditJson(options: VendorAuditPackOptions): string {
|
||||
const result = generateVendorAuditPack(options)
|
||||
return JSON.stringify(result, null, 2)
|
||||
}
|
||||
444
admin-v2/lib/sdk/vendor-compliance/export/vvt-export.ts
Normal file
444
admin-v2/lib/sdk/vendor-compliance/export/vvt-export.ts
Normal file
@@ -0,0 +1,444 @@
|
||||
/**
|
||||
* VVT Export Utilities
|
||||
*
|
||||
* Functions for generating Art. 30 DSGVO compliant
|
||||
* Verarbeitungsverzeichnis (VVT) exports.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ProcessingActivity,
|
||||
Organization,
|
||||
LocalizedText,
|
||||
LegalBasis,
|
||||
DataSubjectCategory,
|
||||
PersonalDataCategory,
|
||||
RecipientCategory,
|
||||
ThirdCountryTransfer,
|
||||
RetentionPeriod,
|
||||
} from '../types'
|
||||
|
||||
// ==========================================
|
||||
// TYPES
|
||||
// ==========================================
|
||||
|
||||
export interface VVTExportOptions {
|
||||
activities: ProcessingActivity[]
|
||||
organization: Organization
|
||||
language: 'de' | 'en'
|
||||
format: 'PDF' | 'DOCX' | 'XLSX'
|
||||
includeAnnexes?: boolean
|
||||
includeRiskAssessment?: boolean
|
||||
watermark?: string
|
||||
}
|
||||
|
||||
export interface VVTExportResult {
|
||||
success: boolean
|
||||
filename: string
|
||||
mimeType: string
|
||||
content: Uint8Array | string
|
||||
metadata: {
|
||||
activityCount: number
|
||||
generatedAt: Date
|
||||
language: string
|
||||
organization: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface VVTRow {
|
||||
vvtId: string
|
||||
name: string
|
||||
responsible: string
|
||||
purposes: string[]
|
||||
legalBasis: string[]
|
||||
dataSubjects: string[]
|
||||
personalData: string[]
|
||||
recipients: string[]
|
||||
thirdCountryTransfers: string[]
|
||||
retentionPeriod: string
|
||||
technicalMeasures: string[]
|
||||
dpiaRequired: string
|
||||
status: string
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CONSTANTS
|
||||
// ==========================================
|
||||
|
||||
const DATA_SUBJECT_LABELS: Record<DataSubjectCategory, LocalizedText> = {
|
||||
EMPLOYEES: { de: 'Beschäftigte', en: 'Employees' },
|
||||
APPLICANTS: { de: 'Bewerber', en: 'Job Applicants' },
|
||||
CUSTOMERS: { de: 'Kunden', en: 'Customers' },
|
||||
PROSPECTIVE_CUSTOMERS: { de: 'Interessenten', en: 'Prospective Customers' },
|
||||
SUPPLIERS: { de: 'Lieferanten', en: 'Suppliers' },
|
||||
BUSINESS_PARTNERS: { de: 'Geschäftspartner', en: 'Business Partners' },
|
||||
VISITORS: { de: 'Besucher', en: 'Visitors' },
|
||||
WEBSITE_USERS: { de: 'Website-Nutzer', en: 'Website Users' },
|
||||
APP_USERS: { de: 'App-Nutzer', en: 'App Users' },
|
||||
NEWSLETTER_SUBSCRIBERS: { de: 'Newsletter-Abonnenten', en: 'Newsletter Subscribers' },
|
||||
MEMBERS: { de: 'Mitglieder', en: 'Members' },
|
||||
PATIENTS: { de: 'Patienten', en: 'Patients' },
|
||||
STUDENTS: { de: 'Schüler/Studenten', en: 'Students' },
|
||||
MINORS: { de: 'Minderjährige', en: 'Minors' },
|
||||
OTHER: { de: 'Sonstige', en: 'Other' },
|
||||
}
|
||||
|
||||
const PERSONAL_DATA_LABELS: Record<PersonalDataCategory, LocalizedText> = {
|
||||
NAME: { de: 'Name', en: 'Name' },
|
||||
CONTACT: { de: 'Kontaktdaten', en: 'Contact Data' },
|
||||
ADDRESS: { de: 'Adressdaten', en: 'Address' },
|
||||
DOB: { de: 'Geburtsdatum', en: 'Date of Birth' },
|
||||
ID_NUMBER: { de: 'Ausweisnummern', en: 'ID Numbers' },
|
||||
SOCIAL_SECURITY: { de: 'Sozialversicherungsnummer', en: 'Social Security Number' },
|
||||
TAX_ID: { de: 'Steuer-ID', en: 'Tax ID' },
|
||||
BANK_ACCOUNT: { de: 'Bankverbindung', en: 'Bank Account' },
|
||||
PAYMENT_DATA: { de: 'Zahlungsdaten', en: 'Payment Data' },
|
||||
EMPLOYMENT_DATA: { de: 'Beschäftigungsdaten', en: 'Employment Data' },
|
||||
SALARY_DATA: { de: 'Gehaltsdaten', en: 'Salary Data' },
|
||||
EDUCATION_DATA: { de: 'Bildungsdaten', en: 'Education Data' },
|
||||
PHOTO_VIDEO: { de: 'Fotos/Videos', en: 'Photos/Videos' },
|
||||
IP_ADDRESS: { de: 'IP-Adressen', en: 'IP Addresses' },
|
||||
DEVICE_ID: { de: 'Geräte-Kennungen', en: 'Device IDs' },
|
||||
LOCATION_DATA: { de: 'Standortdaten', en: 'Location Data' },
|
||||
USAGE_DATA: { de: 'Nutzungsdaten', en: 'Usage Data' },
|
||||
COMMUNICATION_DATA: { de: 'Kommunikationsdaten', en: 'Communication Data' },
|
||||
CONTRACT_DATA: { de: 'Vertragsdaten', en: 'Contract Data' },
|
||||
LOGIN_DATA: { de: 'Login-Daten', en: 'Login Data' },
|
||||
HEALTH_DATA: { de: 'Gesundheitsdaten (Art. 9)', en: 'Health Data (Art. 9)' },
|
||||
GENETIC_DATA: { de: 'Genetische Daten (Art. 9)', en: 'Genetic Data (Art. 9)' },
|
||||
BIOMETRIC_DATA: { de: 'Biometrische Daten (Art. 9)', en: 'Biometric Data (Art. 9)' },
|
||||
RACIAL_ETHNIC: { de: 'Rassische/Ethnische Herkunft (Art. 9)', en: 'Racial/Ethnic Origin (Art. 9)' },
|
||||
POLITICAL_OPINIONS: { de: 'Politische Meinungen (Art. 9)', en: 'Political Opinions (Art. 9)' },
|
||||
RELIGIOUS_BELIEFS: { de: 'Religiöse Überzeugungen (Art. 9)', en: 'Religious Beliefs (Art. 9)' },
|
||||
TRADE_UNION: { de: 'Gewerkschaftszugehörigkeit (Art. 9)', en: 'Trade Union Membership (Art. 9)' },
|
||||
SEX_LIFE: { de: 'Sexualleben/Orientierung (Art. 9)', en: 'Sex Life/Orientation (Art. 9)' },
|
||||
CRIMINAL_DATA: { de: 'Strafrechtliche Daten (Art. 10)', en: 'Criminal Data (Art. 10)' },
|
||||
OTHER: { de: 'Sonstige', en: 'Other' },
|
||||
}
|
||||
|
||||
const LEGAL_BASIS_LABELS: Record<string, LocalizedText> = {
|
||||
CONSENT: { de: 'Einwilligung (Art. 6 Abs. 1 lit. a)', en: 'Consent (Art. 6(1)(a))' },
|
||||
CONTRACT: { de: 'Vertragserfüllung (Art. 6 Abs. 1 lit. b)', en: 'Contract (Art. 6(1)(b))' },
|
||||
LEGAL_OBLIGATION: { de: 'Rechtliche Verpflichtung (Art. 6 Abs. 1 lit. c)', en: 'Legal Obligation (Art. 6(1)(c))' },
|
||||
VITAL_INTEREST: { de: 'Lebenswichtige Interessen (Art. 6 Abs. 1 lit. d)', en: 'Vital Interests (Art. 6(1)(d))' },
|
||||
PUBLIC_TASK: { de: 'Öffentliche Aufgabe (Art. 6 Abs. 1 lit. e)', en: 'Public Task (Art. 6(1)(e))' },
|
||||
LEGITIMATE_INTEREST: { de: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f)', en: 'Legitimate Interest (Art. 6(1)(f))' },
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
export function getLocalizedText(text: LocalizedText | undefined, lang: 'de' | 'en'): string {
|
||||
if (!text) return ''
|
||||
return text[lang] || text.de || ''
|
||||
}
|
||||
|
||||
export function formatDataSubjects(categories: DataSubjectCategory[], lang: 'de' | 'en'): string[] {
|
||||
return categories.map((cat) => DATA_SUBJECT_LABELS[cat]?.[lang] || cat)
|
||||
}
|
||||
|
||||
export function formatPersonalData(categories: PersonalDataCategory[], lang: 'de' | 'en'): string[] {
|
||||
return categories.map((cat) => PERSONAL_DATA_LABELS[cat]?.[lang] || cat)
|
||||
}
|
||||
|
||||
export function formatLegalBasis(bases: LegalBasis[], lang: 'de' | 'en'): string[] {
|
||||
return bases.map((basis) => {
|
||||
const label = LEGAL_BASIS_LABELS[basis.type]?.[lang] || basis.type
|
||||
return basis.description ? `${label}: ${basis.description}` : label
|
||||
})
|
||||
}
|
||||
|
||||
export function formatRecipients(recipients: RecipientCategory[], lang: 'de' | 'en'): string[] {
|
||||
return recipients.map((r) => {
|
||||
const suffix = r.isThirdCountry && r.country ? ` (${r.country})` : ''
|
||||
return r.name + suffix
|
||||
})
|
||||
}
|
||||
|
||||
export function formatTransfers(transfers: ThirdCountryTransfer[], lang: 'de' | 'en'): string[] {
|
||||
const labels: Record<string, LocalizedText> = {
|
||||
ADEQUACY_DECISION: { de: 'Angemessenheitsbeschluss', en: 'Adequacy Decision' },
|
||||
SCC_CONTROLLER: { de: 'SCC (C2C)', en: 'SCC (C2C)' },
|
||||
SCC_PROCESSOR: { de: 'SCC (C2P)', en: 'SCC (C2P)' },
|
||||
BCR: { de: 'BCR', en: 'BCR' },
|
||||
}
|
||||
|
||||
return transfers.map((t) => {
|
||||
const mechanism = labels[t.transferMechanism]?.[lang] || t.transferMechanism
|
||||
return `${t.country}: ${t.recipient} (${mechanism})`
|
||||
})
|
||||
}
|
||||
|
||||
export function formatRetention(retention: RetentionPeriod | undefined, lang: 'de' | 'en'): string {
|
||||
if (!retention) return lang === 'de' ? 'Nicht festgelegt' : 'Not specified'
|
||||
|
||||
// If description is available, use it
|
||||
if (retention.description) {
|
||||
const desc = retention.description[lang] || retention.description.de
|
||||
if (desc) return desc
|
||||
}
|
||||
|
||||
// Otherwise build from duration
|
||||
if (!retention.duration) {
|
||||
return lang === 'de' ? 'Nicht festgelegt' : 'Not specified'
|
||||
}
|
||||
|
||||
const periodLabels: Record<string, LocalizedText> = {
|
||||
DAYS: { de: 'Tage', en: 'days' },
|
||||
MONTHS: { de: 'Monate', en: 'months' },
|
||||
YEARS: { de: 'Jahre', en: 'years' },
|
||||
}
|
||||
|
||||
const unit = periodLabels[retention.durationUnit || 'YEARS'][lang]
|
||||
let result = `${retention.duration} ${unit}`
|
||||
|
||||
if (retention.legalBasis) {
|
||||
result += ` (${retention.legalBasis})`
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// EXPORT FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Transform processing activities to VVT rows for export
|
||||
*/
|
||||
export function transformToVVTRows(
|
||||
activities: ProcessingActivity[],
|
||||
lang: 'de' | 'en'
|
||||
): VVTRow[] {
|
||||
return activities.map((activity) => ({
|
||||
vvtId: activity.vvtId,
|
||||
name: getLocalizedText(activity.name, lang),
|
||||
responsible: activity.responsible.organizationName,
|
||||
purposes: activity.purposes.map((p) => getLocalizedText(p, lang)),
|
||||
legalBasis: formatLegalBasis(activity.legalBasis, lang),
|
||||
dataSubjects: formatDataSubjects(activity.dataSubjectCategories, lang),
|
||||
personalData: formatPersonalData(activity.personalDataCategories, lang),
|
||||
recipients: formatRecipients(activity.recipientCategories, lang),
|
||||
thirdCountryTransfers: formatTransfers(activity.thirdCountryTransfers, lang),
|
||||
retentionPeriod: formatRetention(activity.retentionPeriod, lang),
|
||||
technicalMeasures: activity.technicalMeasures,
|
||||
dpiaRequired: activity.dpiaRequired
|
||||
? lang === 'de'
|
||||
? 'Ja'
|
||||
: 'Yes'
|
||||
: lang === 'de'
|
||||
? 'Nein'
|
||||
: 'No',
|
||||
status: activity.status,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate VVT as JSON (for further processing)
|
||||
*/
|
||||
export function generateVVTJson(options: VVTExportOptions): VVTExportResult {
|
||||
const rows = transformToVVTRows(options.activities, options.language)
|
||||
|
||||
const exportData = {
|
||||
metadata: {
|
||||
organization: options.organization.name,
|
||||
generatedAt: new Date().toISOString(),
|
||||
language: options.language,
|
||||
activityCount: rows.length,
|
||||
version: '1.0',
|
||||
gdprArticle: 'Art. 30 DSGVO',
|
||||
},
|
||||
responsible: {
|
||||
name: options.organization.name,
|
||||
legalForm: options.organization.legalForm,
|
||||
address: options.organization.address,
|
||||
dpo: options.organization.dpoContact,
|
||||
},
|
||||
activities: rows,
|
||||
}
|
||||
|
||||
const content = JSON.stringify(exportData, null, 2)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filename: `VVT_${options.organization.name.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.json`,
|
||||
mimeType: 'application/json',
|
||||
content,
|
||||
metadata: {
|
||||
activityCount: rows.length,
|
||||
generatedAt: new Date(),
|
||||
language: options.language,
|
||||
organization: options.organization.name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate VVT CSV content (for Excel compatibility)
|
||||
*/
|
||||
export function generateVVTCsv(options: VVTExportOptions): string {
|
||||
const rows = transformToVVTRows(options.activities, options.language)
|
||||
const lang = options.language
|
||||
|
||||
const headers = lang === 'de'
|
||||
? [
|
||||
'VVT-Nr.',
|
||||
'Bezeichnung',
|
||||
'Verantwortlicher',
|
||||
'Zwecke',
|
||||
'Rechtsgrundlage',
|
||||
'Betroffene',
|
||||
'Datenkategorien',
|
||||
'Empfänger',
|
||||
'Drittlandtransfers',
|
||||
'Löschfristen',
|
||||
'TOM',
|
||||
'DSFA erforderlich',
|
||||
'Status',
|
||||
]
|
||||
: [
|
||||
'VVT ID',
|
||||
'Name',
|
||||
'Responsible',
|
||||
'Purposes',
|
||||
'Legal Basis',
|
||||
'Data Subjects',
|
||||
'Data Categories',
|
||||
'Recipients',
|
||||
'Third Country Transfers',
|
||||
'Retention Period',
|
||||
'Technical Measures',
|
||||
'DPIA Required',
|
||||
'Status',
|
||||
]
|
||||
|
||||
const csvRows = rows.map((row) => [
|
||||
row.vvtId,
|
||||
row.name,
|
||||
row.responsible,
|
||||
row.purposes.join('; '),
|
||||
row.legalBasis.join('; '),
|
||||
row.dataSubjects.join('; '),
|
||||
row.personalData.join('; '),
|
||||
row.recipients.join('; '),
|
||||
row.thirdCountryTransfers.join('; '),
|
||||
row.retentionPeriod,
|
||||
row.technicalMeasures.join('; '),
|
||||
row.dpiaRequired,
|
||||
row.status,
|
||||
])
|
||||
|
||||
const escape = (val: string) => `"${val.replace(/"/g, '""')}"`
|
||||
|
||||
return [
|
||||
headers.map(escape).join(','),
|
||||
...csvRows.map((row) => row.map(escape).join(',')),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if activities have special category data (Art. 9)
|
||||
*/
|
||||
export function hasSpecialCategoryData(activities: ProcessingActivity[]): boolean {
|
||||
const specialCategories: PersonalDataCategory[] = [
|
||||
'HEALTH_DATA',
|
||||
'GENETIC_DATA',
|
||||
'BIOMETRIC_DATA',
|
||||
'RACIAL_ETHNIC',
|
||||
'POLITICAL_OPINIONS',
|
||||
'RELIGIOUS_BELIEFS',
|
||||
'TRADE_UNION',
|
||||
'SEX_LIFE',
|
||||
]
|
||||
|
||||
return activities.some((activity) =>
|
||||
activity.personalDataCategories.some((cat) => specialCategories.includes(cat))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if activities have third country transfers
|
||||
*/
|
||||
export function hasThirdCountryTransfers(activities: ProcessingActivity[]): boolean {
|
||||
return activities.some((activity) => activity.thirdCountryTransfers.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate compliance summary for VVT
|
||||
*/
|
||||
export function generateComplianceSummary(
|
||||
activities: ProcessingActivity[],
|
||||
lang: 'de' | 'en'
|
||||
): {
|
||||
totalActivities: number
|
||||
byStatus: Record<string, number>
|
||||
withSpecialCategories: number
|
||||
withThirdCountryTransfers: number
|
||||
dpiaRequired: number
|
||||
issues: string[]
|
||||
} {
|
||||
const byStatus: Record<string, number> = {}
|
||||
let withSpecialCategories = 0
|
||||
let withThirdCountryTransfers = 0
|
||||
let dpiaRequired = 0
|
||||
const issues: string[] = []
|
||||
|
||||
for (const activity of activities) {
|
||||
// Count by status
|
||||
byStatus[activity.status] = (byStatus[activity.status] || 0) + 1
|
||||
|
||||
// Check for special categories
|
||||
if (
|
||||
activity.personalDataCategories.some((cat) =>
|
||||
[
|
||||
'HEALTH_DATA',
|
||||
'GENETIC_DATA',
|
||||
'BIOMETRIC_DATA',
|
||||
'RACIAL_ETHNIC',
|
||||
'POLITICAL_OPINIONS',
|
||||
'RELIGIOUS_BELIEFS',
|
||||
'TRADE_UNION',
|
||||
'SEX_LIFE',
|
||||
].includes(cat)
|
||||
)
|
||||
) {
|
||||
withSpecialCategories++
|
||||
}
|
||||
|
||||
// Check for third country transfers
|
||||
if (activity.thirdCountryTransfers.length > 0) {
|
||||
withThirdCountryTransfers++
|
||||
}
|
||||
|
||||
// Check DPIA
|
||||
if (activity.dpiaRequired) {
|
||||
dpiaRequired++
|
||||
}
|
||||
|
||||
// Check for issues
|
||||
if (activity.legalBasis.length === 0) {
|
||||
issues.push(
|
||||
lang === 'de'
|
||||
? `${activity.vvtId}: Keine Rechtsgrundlage angegeben`
|
||||
: `${activity.vvtId}: No legal basis specified`
|
||||
)
|
||||
}
|
||||
|
||||
if (!activity.retentionPeriod) {
|
||||
issues.push(
|
||||
lang === 'de'
|
||||
? `${activity.vvtId}: Keine Löschfrist angegeben`
|
||||
: `${activity.vvtId}: No retention period specified`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalActivities: activities.length,
|
||||
byStatus,
|
||||
withSpecialCategories,
|
||||
withThirdCountryTransfers,
|
||||
dpiaRequired,
|
||||
issues,
|
||||
}
|
||||
}
|
||||
196
admin-v2/lib/sdk/vendor-compliance/index.ts
Normal file
196
admin-v2/lib/sdk/vendor-compliance/index.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Vendor & Contract Compliance Module (VVT/RoPA)
|
||||
*
|
||||
* Public exports for:
|
||||
* - VVT (Verarbeitungsverzeichnis) - Art. 30 DSGVO Controller-Perspektive
|
||||
* - RoPA (Records of Processing Activities) - Processor-Perspektive
|
||||
* - Vendor Register - Lieferanten-/Auftragsverarbeiter-Verwaltung
|
||||
* - Contract Reviewer - LLM-gestuetzte Vertragspruefung mit Citations
|
||||
* - Risk & Controls - Risikobewertung und Massnahmenmanagement
|
||||
* - Audit Reports - Automatisierte Berichtsgenerierung
|
||||
*/
|
||||
|
||||
// ==========================================
|
||||
// TYPES
|
||||
// ==========================================
|
||||
export * from './types'
|
||||
|
||||
// ==========================================
|
||||
// CONTEXT & HOOKS
|
||||
// ==========================================
|
||||
export {
|
||||
VendorComplianceProvider,
|
||||
useVendorCompliance,
|
||||
useVendor,
|
||||
useProcessingActivity,
|
||||
useVendorContracts,
|
||||
useVendorFindings,
|
||||
useContractFindings,
|
||||
useControlInstancesForEntity,
|
||||
} from './context'
|
||||
|
||||
// ==========================================
|
||||
// CATALOGS
|
||||
// ==========================================
|
||||
export {
|
||||
// Processing Activity Templates
|
||||
PROCESSING_ACTIVITY_TEMPLATES,
|
||||
PROCESSING_ACTIVITY_CATEGORY_META,
|
||||
getTemplatesByCategory,
|
||||
getTemplateById,
|
||||
getGroupedTemplates,
|
||||
createFormDataFromTemplate,
|
||||
type ProcessingActivityTemplate,
|
||||
type ProcessingActivityCategory,
|
||||
} from './catalog/processing-activities'
|
||||
|
||||
export {
|
||||
// Vendor Templates
|
||||
VENDOR_TEMPLATES,
|
||||
COUNTRY_RISK_PROFILES,
|
||||
getVendorTemplateById,
|
||||
getVendorTemplatesByCategory,
|
||||
getCountryRiskProfile,
|
||||
requiresTransferMechanism,
|
||||
getSuggestedTransferMechanisms,
|
||||
calculateTemplateRiskScore,
|
||||
createVendorFormDataFromTemplate,
|
||||
getEUEEACountries,
|
||||
getAdequateCountries,
|
||||
getHighRiskCountries,
|
||||
type VendorTemplate,
|
||||
type CountryRiskProfile,
|
||||
type RiskFactorWeight,
|
||||
} from './catalog/vendor-templates'
|
||||
|
||||
export {
|
||||
// Legal Basis
|
||||
LEGAL_BASIS_INFO,
|
||||
STANDARD_RETENTION_PERIODS,
|
||||
getLegalBasisInfo,
|
||||
getStandardLegalBases,
|
||||
getSpecialCategoryLegalBases,
|
||||
getAppropriateLegalBases,
|
||||
getRetentionPeriod,
|
||||
getRetentionPeriodsForCategory,
|
||||
getLongestRetentionPeriod,
|
||||
formatRetentionPeriod,
|
||||
type LegalBasisInfo,
|
||||
type RetentionPeriodInfo,
|
||||
} from './catalog/legal-basis'
|
||||
|
||||
// ==========================================
|
||||
// CONTRACT REVIEW
|
||||
// ==========================================
|
||||
export {
|
||||
// Analyzer
|
||||
analyzeContract,
|
||||
verifyCitation,
|
||||
getCitationContext,
|
||||
highlightCitations,
|
||||
calculateComplianceScore as calculateContractComplianceScore,
|
||||
CONTRACT_REVIEW_SYSTEM_PROMPT,
|
||||
CONTRACT_CLASSIFICATION_PROMPT,
|
||||
METADATA_EXTRACTION_PROMPT,
|
||||
type ContractAnalysisRequest,
|
||||
type ContractAnalysisResponse,
|
||||
type ContractPartyInfo,
|
||||
type ExtractedMetadata,
|
||||
type AnalysisScope,
|
||||
type ComplianceScoreBreakdown,
|
||||
} from './contract-review/analyzer'
|
||||
|
||||
export {
|
||||
// Checklists
|
||||
AVV_CHECKLIST,
|
||||
INCIDENT_CHECKLIST,
|
||||
TRANSFER_CHECKLIST,
|
||||
SLA_LIABILITY_CHECKLIST,
|
||||
CHECKLIST_GROUPS,
|
||||
getRequiredChecklistItems,
|
||||
getChecklistItemsByCategory,
|
||||
getChecklistItemById,
|
||||
calculateChecklistComplianceScore as calculateChecklistScore,
|
||||
type ChecklistItem,
|
||||
type ChecklistGroup,
|
||||
} from './contract-review/checklists'
|
||||
|
||||
export {
|
||||
// Findings
|
||||
FINDING_TEMPLATES,
|
||||
SEVERITY_DEFINITIONS,
|
||||
FINDING_TYPE_DEFINITIONS,
|
||||
getFindingTemplateById,
|
||||
getFindingTemplatesByCategory,
|
||||
getFindingTemplatesByType,
|
||||
getFindingTemplatesBySeverity,
|
||||
getSeverityColorClass,
|
||||
sortFindingsBySeverity,
|
||||
countFindingsBySeverity,
|
||||
getOverallSeverity,
|
||||
type FindingTemplate,
|
||||
} from './contract-review/findings'
|
||||
|
||||
// ==========================================
|
||||
// RISK & CONTROLS
|
||||
// ==========================================
|
||||
export {
|
||||
// Risk Calculator
|
||||
RISK_FACTOR_DEFINITIONS,
|
||||
calculateVendorInherentRisk,
|
||||
calculateProcessingActivityInherentRisk,
|
||||
calculateResidualRisk,
|
||||
generateRiskMatrix,
|
||||
getRiskLevelColor,
|
||||
calculateRiskTrend,
|
||||
type RiskFactorDefinition,
|
||||
type RiskContext,
|
||||
type RiskMatrixCell,
|
||||
type RiskTrend,
|
||||
} from './risk/calculator'
|
||||
|
||||
export {
|
||||
// Controls Library
|
||||
CONTROLS_LIBRARY,
|
||||
getAllControls,
|
||||
getControlsByDomain,
|
||||
getControlById,
|
||||
getRequiredControls,
|
||||
getControlsByFrequency,
|
||||
getVendorControls,
|
||||
getProcessingActivityControls,
|
||||
getControlsGroupedByDomain,
|
||||
getControlDomainMeta,
|
||||
calculateControlCoverage,
|
||||
} from './risk/controls-library'
|
||||
|
||||
// ==========================================
|
||||
// EXPORT UTILITIES
|
||||
// ==========================================
|
||||
export {
|
||||
// VVT Export
|
||||
type VVTExportOptions,
|
||||
type VVTExportResult,
|
||||
type VVTRow,
|
||||
transformToVVTRows,
|
||||
generateVVTJson,
|
||||
generateVVTCsv,
|
||||
hasSpecialCategoryData,
|
||||
hasThirdCountryTransfers,
|
||||
generateComplianceSummary,
|
||||
// Vendor Audit Pack
|
||||
type VendorAuditPackOptions,
|
||||
type VendorAuditSection,
|
||||
type VendorAuditPackResult,
|
||||
generateVendorAuditPack,
|
||||
generateVendorAuditJson,
|
||||
// RoPA Export
|
||||
type RoPAExportOptions,
|
||||
type RoPARow,
|
||||
type RoPAExportResult,
|
||||
transformToRoPARows,
|
||||
generateRoPAJson,
|
||||
generateRoPACsv,
|
||||
generateProcessorSummary,
|
||||
validateRoPACompleteness,
|
||||
} from './export'
|
||||
488
admin-v2/lib/sdk/vendor-compliance/risk/calculator.ts
Normal file
488
admin-v2/lib/sdk/vendor-compliance/risk/calculator.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
/**
|
||||
* Risk Score Calculator
|
||||
*
|
||||
* Calculate inherent and residual risk scores for vendors and processing activities
|
||||
*/
|
||||
|
||||
import {
|
||||
Vendor,
|
||||
ProcessingActivity,
|
||||
RiskScore,
|
||||
RiskLevel,
|
||||
RiskFactor,
|
||||
ControlInstance,
|
||||
DataAccessLevel,
|
||||
VendorRole,
|
||||
ServiceCategory,
|
||||
PersonalDataCategory,
|
||||
getRiskLevelFromScore,
|
||||
isSpecialCategory,
|
||||
hasAdequacyDecision,
|
||||
LocalizedText,
|
||||
} from '../types'
|
||||
|
||||
// ==========================================
|
||||
// RISK FACTOR DEFINITIONS
|
||||
// ==========================================
|
||||
|
||||
export interface RiskFactorDefinition {
|
||||
id: string
|
||||
name: LocalizedText
|
||||
category: 'DATA' | 'ACCESS' | 'LOCATION' | 'VENDOR' | 'PROCESSING'
|
||||
description: LocalizedText
|
||||
weight: number // 0-1
|
||||
evaluator: (context: RiskContext) => number // Returns 1-5
|
||||
}
|
||||
|
||||
export interface RiskContext {
|
||||
vendor?: Vendor
|
||||
processingActivity?: ProcessingActivity
|
||||
controlInstances?: ControlInstance[]
|
||||
}
|
||||
|
||||
export const RISK_FACTOR_DEFINITIONS: RiskFactorDefinition[] = [
|
||||
// DATA FACTORS
|
||||
{
|
||||
id: 'data_volume',
|
||||
name: { de: 'Datenvolumen', en: 'Data Volume' },
|
||||
category: 'DATA',
|
||||
description: { de: 'Menge der verarbeiteten Daten', en: 'Volume of data processed' },
|
||||
weight: 0.15,
|
||||
evaluator: (ctx) => {
|
||||
// Based on vendor service category or processing activity scope
|
||||
if (ctx.vendor) {
|
||||
const highVolume: ServiceCategory[] = ['CLOUD_INFRASTRUCTURE', 'ERP', 'CRM', 'HR_SOFTWARE']
|
||||
if (highVolume.includes(ctx.vendor.serviceCategory)) return 5
|
||||
const medVolume: ServiceCategory[] = ['HOSTING', 'EMAIL', 'ANALYTICS', 'BACKUP']
|
||||
if (medVolume.includes(ctx.vendor.serviceCategory)) return 3
|
||||
return 2
|
||||
}
|
||||
return 3 // Default medium
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'data_sensitivity',
|
||||
name: { de: 'Datensensibilität', en: 'Data Sensitivity' },
|
||||
category: 'DATA',
|
||||
description: { de: 'Sensibilität der verarbeiteten Daten', en: 'Sensitivity of data processed' },
|
||||
weight: 0.2,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.processingActivity) {
|
||||
const hasSpecial = ctx.processingActivity.personalDataCategories.some(isSpecialCategory)
|
||||
if (hasSpecial) return 5
|
||||
const hasFinancial = ctx.processingActivity.personalDataCategories.some(
|
||||
(c) => ['BANK_ACCOUNT', 'PAYMENT_DATA', 'SALARY_DATA', 'TAX_ID'].includes(c)
|
||||
)
|
||||
if (hasFinancial) return 4
|
||||
const hasIdentifiers = ctx.processingActivity.personalDataCategories.some(
|
||||
(c) => ['ID_NUMBER', 'SOCIAL_SECURITY'].includes(c)
|
||||
)
|
||||
if (hasIdentifiers) return 4
|
||||
return 2
|
||||
}
|
||||
return 3
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'special_categories',
|
||||
name: { de: 'Besondere Kategorien', en: 'Special Categories' },
|
||||
category: 'DATA',
|
||||
description: { de: 'Verarbeitung besonderer Datenkategorien (Art. 9)', en: 'Processing of special data categories (Art. 9)' },
|
||||
weight: 0.15,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.processingActivity) {
|
||||
const specialCount = ctx.processingActivity.personalDataCategories.filter(isSpecialCategory).length
|
||||
if (specialCount >= 3) return 5
|
||||
if (specialCount >= 1) return 4
|
||||
return 1
|
||||
}
|
||||
return 1
|
||||
},
|
||||
},
|
||||
|
||||
// ACCESS FACTORS
|
||||
{
|
||||
id: 'data_access_level',
|
||||
name: { de: 'Datenzugriffsebene', en: 'Data Access Level' },
|
||||
category: 'ACCESS',
|
||||
description: { de: 'Art des Zugriffs auf personenbezogene Daten', en: 'Type of access to personal data' },
|
||||
weight: 0.15,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.vendor) {
|
||||
const accessLevelScores: Record<DataAccessLevel, number> = {
|
||||
NONE: 1,
|
||||
POTENTIAL: 2,
|
||||
ADMINISTRATIVE: 4,
|
||||
CONTENT: 5,
|
||||
}
|
||||
return accessLevelScores[ctx.vendor.dataAccessLevel]
|
||||
}
|
||||
return 3
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vendor_role',
|
||||
name: { de: 'Vendor-Rolle', en: 'Vendor Role' },
|
||||
category: 'ACCESS',
|
||||
description: { de: 'Rolle des Vendors im Datenschutzkontext', en: 'Role of vendor in data protection context' },
|
||||
weight: 0.1,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.vendor) {
|
||||
const roleScores: Record<VendorRole, number> = {
|
||||
THIRD_PARTY: 1,
|
||||
CONTROLLER: 3,
|
||||
JOINT_CONTROLLER: 4,
|
||||
PROCESSOR: 4,
|
||||
SUB_PROCESSOR: 5,
|
||||
}
|
||||
return roleScores[ctx.vendor.role]
|
||||
}
|
||||
return 3
|
||||
},
|
||||
},
|
||||
|
||||
// LOCATION FACTORS
|
||||
{
|
||||
id: 'third_country_transfer',
|
||||
name: { de: 'Drittlandtransfer', en: 'Third Country Transfer' },
|
||||
category: 'LOCATION',
|
||||
description: { de: 'Datenübermittlung in Drittländer', en: 'Data transfer to third countries' },
|
||||
weight: 0.15,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.vendor) {
|
||||
const locations = ctx.vendor.processingLocations
|
||||
const hasNonAdequate = locations.some((l) => !l.isEU && !l.isAdequate)
|
||||
if (hasNonAdequate) return 5
|
||||
const hasAdequate = locations.some((l) => !l.isEU && l.isAdequate)
|
||||
if (hasAdequate) return 3
|
||||
return 1 // EU only
|
||||
}
|
||||
if (ctx.processingActivity && ctx.processingActivity.thirdCountryTransfers.length > 0) {
|
||||
const hasNonAdequate = ctx.processingActivity.thirdCountryTransfers.some(
|
||||
(t) => !hasAdequacyDecision(t.country)
|
||||
)
|
||||
if (hasNonAdequate) return 5
|
||||
return 3
|
||||
}
|
||||
return 1
|
||||
},
|
||||
},
|
||||
|
||||
// VENDOR FACTORS
|
||||
{
|
||||
id: 'certification_status',
|
||||
name: { de: 'Zertifizierungsstatus', en: 'Certification Status' },
|
||||
category: 'VENDOR',
|
||||
description: { de: 'Vorhandensein relevanter Zertifizierungen', en: 'Presence of relevant certifications' },
|
||||
weight: 0.1,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.vendor) {
|
||||
const certs = ctx.vendor.certifications
|
||||
const relevantCerts = ['ISO 27001', 'SOC 2', 'SOC2', 'TISAX', 'C5', 'PCI DSS']
|
||||
const hasCert = certs.some((c) => relevantCerts.some((rc) => c.type.includes(rc)))
|
||||
const hasExpired = certs.some((c) => c.expirationDate && new Date(c.expirationDate) < new Date())
|
||||
if (hasCert && !hasExpired) return 1
|
||||
if (hasCert && hasExpired) return 3
|
||||
return 4
|
||||
}
|
||||
return 3
|
||||
},
|
||||
},
|
||||
|
||||
// PROCESSING FACTORS
|
||||
{
|
||||
id: 'processing_scope',
|
||||
name: { de: 'Verarbeitungsumfang', en: 'Processing Scope' },
|
||||
category: 'PROCESSING',
|
||||
description: { de: 'Umfang und Art der Verarbeitung', en: 'Scope and nature of processing' },
|
||||
weight: 0.1,
|
||||
evaluator: (ctx) => {
|
||||
if (ctx.processingActivity) {
|
||||
const subjectCount = ctx.processingActivity.dataSubjectCategories.length
|
||||
const categoryCount = ctx.processingActivity.personalDataCategories.length
|
||||
const totalScope = subjectCount + categoryCount
|
||||
if (totalScope > 15) return 5
|
||||
if (totalScope > 10) return 4
|
||||
if (totalScope > 5) return 3
|
||||
return 2
|
||||
}
|
||||
return 3
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// RISK CALCULATION FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Calculate inherent risk score for a vendor
|
||||
*/
|
||||
export function calculateVendorInherentRisk(vendor: Vendor): {
|
||||
score: RiskScore
|
||||
factors: RiskFactor[]
|
||||
} {
|
||||
const context: RiskContext = { vendor }
|
||||
return calculateInherentRisk(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate inherent risk score for a processing activity
|
||||
*/
|
||||
export function calculateProcessingActivityInherentRisk(activity: ProcessingActivity): {
|
||||
score: RiskScore
|
||||
factors: RiskFactor[]
|
||||
} {
|
||||
const context: RiskContext = { processingActivity: activity }
|
||||
return calculateInherentRisk(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic inherent risk calculation
|
||||
*/
|
||||
function calculateInherentRisk(context: RiskContext): {
|
||||
score: RiskScore
|
||||
factors: RiskFactor[]
|
||||
} {
|
||||
const factors: RiskFactor[] = []
|
||||
let totalWeight = 0
|
||||
let weightedSum = 0
|
||||
|
||||
for (const definition of RISK_FACTOR_DEFINITIONS) {
|
||||
const value = definition.evaluator(context)
|
||||
const factor: RiskFactor = {
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
category: definition.category,
|
||||
weight: definition.weight,
|
||||
value,
|
||||
}
|
||||
factors.push(factor)
|
||||
|
||||
totalWeight += definition.weight
|
||||
weightedSum += value * definition.weight
|
||||
}
|
||||
|
||||
// Calculate average (1-5 scale)
|
||||
const averageScore = weightedSum / totalWeight
|
||||
|
||||
// Map to likelihood and impact
|
||||
const likelihood = Math.round(averageScore) as 1 | 2 | 3 | 4 | 5
|
||||
const impact = calculateImpact(context)
|
||||
const score = likelihood * impact
|
||||
const level = getRiskLevelFromScore(score)
|
||||
|
||||
return {
|
||||
score: {
|
||||
likelihood,
|
||||
impact,
|
||||
score,
|
||||
level,
|
||||
rationale: generateRationale(factors, likelihood, impact),
|
||||
},
|
||||
factors,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate impact based on context
|
||||
*/
|
||||
function calculateImpact(context: RiskContext): 1 | 2 | 3 | 4 | 5 {
|
||||
let impactScore = 3 // Default medium
|
||||
|
||||
if (context.vendor) {
|
||||
// Higher impact for critical services
|
||||
const criticalServices: ServiceCategory[] = ['CLOUD_INFRASTRUCTURE', 'ERP', 'PAYMENT', 'SECURITY']
|
||||
if (criticalServices.includes(context.vendor.serviceCategory)) {
|
||||
impactScore += 1
|
||||
}
|
||||
|
||||
// Higher impact for content access
|
||||
if (context.vendor.dataAccessLevel === 'CONTENT') {
|
||||
impactScore += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (context.processingActivity) {
|
||||
// Higher impact for special categories
|
||||
if (context.processingActivity.personalDataCategories.some(isSpecialCategory)) {
|
||||
impactScore += 1
|
||||
}
|
||||
|
||||
// Higher impact for high protection level
|
||||
if (context.processingActivity.protectionLevel === 'HIGH') {
|
||||
impactScore += 1
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(5, Math.max(1, impactScore)) as 1 | 2 | 3 | 4 | 5
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate rationale text
|
||||
*/
|
||||
function generateRationale(
|
||||
factors: RiskFactor[],
|
||||
likelihood: number,
|
||||
impact: number
|
||||
): string {
|
||||
const highFactors = factors.filter((f) => f.value >= 4).map((f) => f.name.de)
|
||||
const lowFactors = factors.filter((f) => f.value <= 2).map((f) => f.name.de)
|
||||
|
||||
let rationale = `Bewertung: Eintrittswahrscheinlichkeit ${likelihood}/5, Auswirkung ${impact}/5.`
|
||||
|
||||
if (highFactors.length > 0) {
|
||||
rationale += ` Erhöhte Risikofaktoren: ${highFactors.join(', ')}.`
|
||||
}
|
||||
|
||||
if (lowFactors.length > 0) {
|
||||
rationale += ` Risikomindernde Faktoren: ${lowFactors.join(', ')}.`
|
||||
}
|
||||
|
||||
return rationale
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RESIDUAL RISK CALCULATION
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Calculate residual risk based on control effectiveness
|
||||
*/
|
||||
export function calculateResidualRisk(
|
||||
inherentRisk: RiskScore,
|
||||
controlInstances: ControlInstance[]
|
||||
): RiskScore {
|
||||
// Calculate control effectiveness (0-1)
|
||||
const effectiveness = calculateControlEffectiveness(controlInstances)
|
||||
|
||||
// Reduce likelihood based on control effectiveness
|
||||
const reducedLikelihood = Math.max(1, Math.round(inherentRisk.likelihood * (1 - effectiveness * 0.6)))
|
||||
|
||||
// Impact reduction is smaller (controls primarily reduce likelihood)
|
||||
const reducedImpact = Math.max(1, Math.round(inherentRisk.impact * (1 - effectiveness * 0.3)))
|
||||
|
||||
const residualScore = reducedLikelihood * reducedImpact
|
||||
const level = getRiskLevelFromScore(residualScore)
|
||||
|
||||
return {
|
||||
likelihood: reducedLikelihood as 1 | 2 | 3 | 4 | 5,
|
||||
impact: reducedImpact as 1 | 2 | 3 | 4 | 5,
|
||||
score: residualScore,
|
||||
level,
|
||||
rationale: `Restrisiko nach Berücksichtigung von ${controlInstances.length} Kontrollen (Effektivität: ${Math.round(effectiveness * 100)}%).`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall control effectiveness
|
||||
*/
|
||||
function calculateControlEffectiveness(controlInstances: ControlInstance[]): number {
|
||||
if (controlInstances.length === 0) return 0
|
||||
|
||||
const statusScores: Record<string, number> = {
|
||||
PASS: 1,
|
||||
PARTIAL: 0.5,
|
||||
FAIL: 0,
|
||||
NOT_APPLICABLE: 0,
|
||||
PLANNED: 0.2,
|
||||
}
|
||||
|
||||
const totalScore = controlInstances.reduce(
|
||||
(sum, ci) => sum + (statusScores[ci.status] || 0),
|
||||
0
|
||||
)
|
||||
|
||||
return totalScore / controlInstances.length
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RISK MATRIX
|
||||
// ==========================================
|
||||
|
||||
export interface RiskMatrixCell {
|
||||
likelihood: number
|
||||
impact: number
|
||||
level: RiskLevel
|
||||
score: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate full risk matrix
|
||||
*/
|
||||
export function generateRiskMatrix(): RiskMatrixCell[][] {
|
||||
const matrix: RiskMatrixCell[][] = []
|
||||
|
||||
for (let likelihood = 1; likelihood <= 5; likelihood++) {
|
||||
const row: RiskMatrixCell[] = []
|
||||
for (let impact = 1; impact <= 5; impact++) {
|
||||
const score = likelihood * impact
|
||||
row.push({
|
||||
likelihood,
|
||||
impact,
|
||||
score,
|
||||
level: getRiskLevelFromScore(score),
|
||||
})
|
||||
}
|
||||
matrix.push(row)
|
||||
}
|
||||
|
||||
return matrix
|
||||
}
|
||||
|
||||
/**
|
||||
* Get risk matrix colors
|
||||
*/
|
||||
export function getRiskLevelColor(level: RiskLevel): {
|
||||
bg: string
|
||||
text: string
|
||||
border: string
|
||||
} {
|
||||
switch (level) {
|
||||
case 'LOW':
|
||||
return { bg: 'bg-green-100', text: 'text-green-800', border: 'border-green-300' }
|
||||
case 'MEDIUM':
|
||||
return { bg: 'bg-yellow-100', text: 'text-yellow-800', border: 'border-yellow-300' }
|
||||
case 'HIGH':
|
||||
return { bg: 'bg-orange-100', text: 'text-orange-800', border: 'border-orange-300' }
|
||||
case 'CRITICAL':
|
||||
return { bg: 'bg-red-100', text: 'text-red-800', border: 'border-red-300' }
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RISK TREND ANALYSIS
|
||||
// ==========================================
|
||||
|
||||
export interface RiskTrend {
|
||||
currentScore: number
|
||||
previousScore: number
|
||||
change: number
|
||||
trend: 'IMPROVING' | 'STABLE' | 'DETERIORATING'
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk trend
|
||||
*/
|
||||
export function calculateRiskTrend(
|
||||
currentScore: number,
|
||||
previousScore: number
|
||||
): RiskTrend {
|
||||
const change = currentScore - previousScore
|
||||
let trend: 'IMPROVING' | 'STABLE' | 'DETERIORATING'
|
||||
|
||||
if (Math.abs(change) <= 2) {
|
||||
trend = 'STABLE'
|
||||
} else if (change < 0) {
|
||||
trend = 'IMPROVING'
|
||||
} else {
|
||||
trend = 'DETERIORATING'
|
||||
}
|
||||
|
||||
return {
|
||||
currentScore,
|
||||
previousScore,
|
||||
change,
|
||||
trend,
|
||||
}
|
||||
}
|
||||
943
admin-v2/lib/sdk/vendor-compliance/risk/controls-library.ts
Normal file
943
admin-v2/lib/sdk/vendor-compliance/risk/controls-library.ts
Normal file
@@ -0,0 +1,943 @@
|
||||
/**
|
||||
* Controls Library
|
||||
*
|
||||
* Standard controls for vendor and processing activity compliance
|
||||
*/
|
||||
|
||||
import { Control, ControlDomain, ReviewFrequency, LocalizedText } from '../types'
|
||||
|
||||
// ==========================================
|
||||
// CONTROL DEFINITIONS
|
||||
// ==========================================
|
||||
|
||||
export const CONTROLS_LIBRARY: Control[] = [
|
||||
// ==========================================
|
||||
// TRANSFER - Drittlandtransfer Controls
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-TRF-01',
|
||||
domain: 'TRANSFER',
|
||||
title: {
|
||||
de: 'Drittlandtransfer nur mit Rechtsgrundlage',
|
||||
en: 'Third country transfer with legal basis',
|
||||
},
|
||||
description: {
|
||||
de: 'Drittlandtransfers erfolgen nur auf Basis von SCC, BCR oder Angemessenheitsbeschluss',
|
||||
en: 'Third country transfers only based on SCC, BCR or adequacy decision',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'SCC oder BCR vertraglich vereinbart ODER Angemessenheitsbeschluss vorhanden',
|
||||
en: 'SCC or BCR contractually agreed OR adequacy decision exists',
|
||||
},
|
||||
requirements: ['Art. 44-49 DSGVO', 'ISO 27001 A.15.1.2'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TRF-02',
|
||||
domain: 'TRANSFER',
|
||||
title: {
|
||||
de: 'Aktuelle Standardvertragsklauseln',
|
||||
en: 'Current Standard Contractual Clauses',
|
||||
},
|
||||
description: {
|
||||
de: 'Bei SCC-Nutzung: Verwendung der aktuellen EU-Kommission-Klauseln (2021)',
|
||||
en: 'When using SCC: Current EU Commission clauses (2021) are used',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'SCC 2021 (Durchführungsbeschluss (EU) 2021/914) verwendet',
|
||||
en: 'SCC 2021 (Implementing Decision (EU) 2021/914) used',
|
||||
},
|
||||
requirements: ['Art. 46 Abs. 2 lit. c DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TRF-03',
|
||||
domain: 'TRANSFER',
|
||||
title: {
|
||||
de: 'Transfer Impact Assessment (TIA)',
|
||||
en: 'Transfer Impact Assessment (TIA)',
|
||||
},
|
||||
description: {
|
||||
de: 'Bei Transfers in Drittländer ohne Angemessenheitsbeschluss ist TIA durchzuführen',
|
||||
en: 'TIA required for transfers to third countries without adequacy decision',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'TIA dokumentiert und bewertet Risiken als akzeptabel',
|
||||
en: 'TIA documented and risks assessed as acceptable',
|
||||
},
|
||||
requirements: ['Schrems II Urteil', 'EDSA Empfehlungen 01/2020'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TRF-04',
|
||||
domain: 'TRANSFER',
|
||||
title: {
|
||||
de: 'Zusätzliche Schutzmaßnahmen',
|
||||
en: 'Supplementary Measures',
|
||||
},
|
||||
description: {
|
||||
de: 'Bei Bedarf sind zusätzliche technische/organisatorische Maßnahmen implementiert',
|
||||
en: 'Supplementary technical/organizational measures implemented where needed',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Ergänzende Maßnahmen dokumentiert (Verschlüsselung, Pseudonymisierung, etc.)',
|
||||
en: 'Supplementary measures documented (encryption, pseudonymization, etc.)',
|
||||
},
|
||||
requirements: ['EDSA Empfehlungen 01/2020'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TRF-05',
|
||||
domain: 'TRANSFER',
|
||||
title: {
|
||||
de: 'Überwachung Angemessenheitsbeschlüsse',
|
||||
en: 'Monitoring Adequacy Decisions',
|
||||
},
|
||||
description: {
|
||||
de: 'Änderungen bei Angemessenheitsbeschlüssen werden überwacht',
|
||||
en: 'Changes to adequacy decisions are monitored',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Prozess zur Überwachung und Reaktion auf Änderungen etabliert',
|
||||
en: 'Process for monitoring and responding to changes established',
|
||||
},
|
||||
requirements: ['Art. 45 DSGVO'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'QUARTERLY',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// AUDIT - Auditrechte Controls
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-AUD-01',
|
||||
domain: 'AUDIT',
|
||||
title: {
|
||||
de: 'Auditrecht vertraglich vereinbart',
|
||||
en: 'Audit right contractually agreed',
|
||||
},
|
||||
description: {
|
||||
de: 'Vertrag enthält wirksames Auditrecht ohne unangemessene Einschränkungen',
|
||||
en: 'Contract contains effective audit right without unreasonable restrictions',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Auditrecht im AVV enthalten, max. 30 Tage Vorlaufzeit, keine Ausschlussklausel',
|
||||
en: 'Audit right in DPA, max 30 days notice, no exclusion clause',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. h DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-AUD-02',
|
||||
domain: 'AUDIT',
|
||||
title: {
|
||||
de: 'Vor-Ort-Inspektionen möglich',
|
||||
en: 'On-site inspections possible',
|
||||
},
|
||||
description: {
|
||||
de: 'Vertrag erlaubt Vor-Ort-Inspektionen bei dem Auftragsverarbeiter',
|
||||
en: 'Contract allows on-site inspections at the processor',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Vor-Ort-Audit explizit erlaubt, Zugang zu relevanten Bereichen',
|
||||
en: 'On-site audit explicitly allowed, access to relevant areas',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. h DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-AUD-03',
|
||||
domain: 'AUDIT',
|
||||
title: {
|
||||
de: 'Aktuelle Zertifizierungen',
|
||||
en: 'Current Certifications',
|
||||
},
|
||||
description: {
|
||||
de: 'Relevante Sicherheitszertifizierungen sind aktuell und gültig',
|
||||
en: 'Relevant security certifications are current and valid',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'ISO 27001, SOC 2 oder vergleichbar, nicht abgelaufen',
|
||||
en: 'ISO 27001, SOC 2 or equivalent, not expired',
|
||||
},
|
||||
requirements: ['Art. 32 DSGVO', 'ISO 27001 A.15.1.1'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-AUD-04',
|
||||
domain: 'AUDIT',
|
||||
title: {
|
||||
de: 'Letzte Prüfung durchgeführt',
|
||||
en: 'Last review conducted',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor wurde innerhalb des Review-Zyklus geprüft',
|
||||
en: 'Vendor was reviewed within the review cycle',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Dokumentierte Prüfung innerhalb des festgelegten Intervalls',
|
||||
en: 'Documented review within the defined interval',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. h DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-AUD-05',
|
||||
domain: 'AUDIT',
|
||||
title: {
|
||||
de: 'Prüfberichte verfügbar',
|
||||
en: 'Audit reports available',
|
||||
},
|
||||
description: {
|
||||
de: 'Aktuelle Prüfberichte (SOC 2, Penetrationstest, etc.) liegen vor',
|
||||
en: 'Current audit reports (SOC 2, penetration test, etc.) are available',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Prüfberichte nicht älter als 12 Monate',
|
||||
en: 'Audit reports not older than 12 months',
|
||||
},
|
||||
requirements: ['ISO 27001 A.18.2.1'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// DELETION - Löschung Controls
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-DEL-01',
|
||||
domain: 'DELETION',
|
||||
title: {
|
||||
de: 'Löschung/Rückgabe nach Vertragsende',
|
||||
en: 'Deletion/return after contract end',
|
||||
},
|
||||
description: {
|
||||
de: 'Klare Regelung zur Löschung oder Rückgabe aller Daten nach Vertragsende',
|
||||
en: 'Clear provision for deletion or return of all data after contract end',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Löschfrist max. 30 Tage, Löschbestätigung vorgesehen',
|
||||
en: 'Deletion within max 30 days, deletion confirmation provided',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. g DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-DEL-02',
|
||||
domain: 'DELETION',
|
||||
title: {
|
||||
de: 'Löschbestätigung',
|
||||
en: 'Deletion confirmation',
|
||||
},
|
||||
description: {
|
||||
de: 'Schriftliche Bestätigung der vollständigen Datenlöschung',
|
||||
en: 'Written confirmation of complete data deletion',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Löschbestätigung vertraglich vereinbart und einforderbar',
|
||||
en: 'Deletion confirmation contractually agreed and enforceable',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. g DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-DEL-03',
|
||||
domain: 'DELETION',
|
||||
title: {
|
||||
de: 'Löschung bei Unterauftragnehmern',
|
||||
en: 'Deletion at sub-processors',
|
||||
},
|
||||
description: {
|
||||
de: 'Löschpflicht erstreckt sich auf alle Unterauftragnehmer',
|
||||
en: 'Deletion obligation extends to all sub-processors',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Weitergabe der Löschpflicht an Unterauftragnehmer vertraglich vereinbart',
|
||||
en: 'Transfer of deletion obligation to sub-processors contractually agreed',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. g, d DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-DEL-04',
|
||||
domain: 'DELETION',
|
||||
title: {
|
||||
de: 'Backup-Löschung',
|
||||
en: 'Backup deletion',
|
||||
},
|
||||
description: {
|
||||
de: 'Daten werden auch aus Backups gelöscht',
|
||||
en: 'Data is also deleted from backups',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Backup-Löschung geregelt, max. Aufbewahrungsfrist für Backups definiert',
|
||||
en: 'Backup deletion regulated, max retention period for backups defined',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. g DSGVO'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// INCIDENT - Incident Response Controls
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-INC-01',
|
||||
domain: 'INCIDENT',
|
||||
title: {
|
||||
de: 'Meldepflicht bei Datenpannen',
|
||||
en: 'Data breach notification obligation',
|
||||
},
|
||||
description: {
|
||||
de: 'Unverzügliche Meldung von Datenschutzverletzungen',
|
||||
en: 'Immediate notification of data protection violations',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Meldepflicht vereinbart, Frist max. 24-48h, Mindestinhalte definiert',
|
||||
en: 'Notification obligation agreed, deadline max 24-48h, minimum content defined',
|
||||
},
|
||||
requirements: ['Art. 33 Abs. 2 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-INC-02',
|
||||
domain: 'INCIDENT',
|
||||
title: {
|
||||
de: 'Incident Response Plan',
|
||||
en: 'Incident Response Plan',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor hat dokumentierten Incident Response Plan',
|
||||
en: 'Vendor has documented incident response plan',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Incident Response Plan liegt vor und wurde getestet',
|
||||
en: 'Incident response plan exists and has been tested',
|
||||
},
|
||||
requirements: ['ISO 27001 A.16.1'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-INC-03',
|
||||
domain: 'INCIDENT',
|
||||
title: {
|
||||
de: 'Kontaktstelle für Incidents',
|
||||
en: 'Contact point for incidents',
|
||||
},
|
||||
description: {
|
||||
de: 'Definierte Kontaktstelle für Datenschutzvorfälle',
|
||||
en: 'Defined contact point for data protection incidents',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Kontaktdaten für Incident-Meldungen bekannt und aktuell',
|
||||
en: 'Contact details for incident reporting known and current',
|
||||
},
|
||||
requirements: ['Art. 33 Abs. 2 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'QUARTERLY',
|
||||
},
|
||||
{
|
||||
id: 'VND-INC-04',
|
||||
domain: 'INCIDENT',
|
||||
title: {
|
||||
de: 'Unterstützung bei Incident-Dokumentation',
|
||||
en: 'Support with incident documentation',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor unterstützt bei der Dokumentation von Vorfällen',
|
||||
en: 'Vendor supports documentation of incidents',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Unterstützungspflicht bei Dokumentation vertraglich vereinbart',
|
||||
en: 'Support obligation for documentation contractually agreed',
|
||||
},
|
||||
requirements: ['Art. 33 Abs. 5 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// SUBPROCESSOR - Unterauftragnehmer Controls
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-SUB-01',
|
||||
domain: 'SUBPROCESSOR',
|
||||
title: {
|
||||
de: 'Genehmigungspflicht für Unterauftragnehmer',
|
||||
en: 'Approval requirement for sub-processors',
|
||||
},
|
||||
description: {
|
||||
de: 'Einsatz von Unterauftragnehmern nur mit Genehmigung',
|
||||
en: 'Use of sub-processors only with approval',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Genehmigungserfordernis (spezifisch oder allgemein mit Widerspruchsrecht) vereinbart',
|
||||
en: 'Approval requirement (specific or general with objection right) agreed',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 2, 4 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-SUB-02',
|
||||
domain: 'SUBPROCESSOR',
|
||||
title: {
|
||||
de: 'Aktuelle Unterauftragnehmer-Liste',
|
||||
en: 'Current sub-processor list',
|
||||
},
|
||||
description: {
|
||||
de: 'Vollständige und aktuelle Liste aller Unterauftragnehmer',
|
||||
en: 'Complete and current list of all sub-processors',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Liste liegt vor mit Name, Sitz, Verarbeitungszweck',
|
||||
en: 'List available with name, location, processing purpose',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 2 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'QUARTERLY',
|
||||
},
|
||||
{
|
||||
id: 'VND-SUB-03',
|
||||
domain: 'SUBPROCESSOR',
|
||||
title: {
|
||||
de: 'Informationspflicht bei Änderungen',
|
||||
en: 'Notification obligation for changes',
|
||||
},
|
||||
description: {
|
||||
de: 'Information über neue oder geänderte Unterauftragnehmer',
|
||||
en: 'Information about new or changed sub-processors',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Vorabinformation vereinbart, ausreichende Frist für Widerspruch',
|
||||
en: 'Advance notification agreed, sufficient time for objection',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 2 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-SUB-04',
|
||||
domain: 'SUBPROCESSOR',
|
||||
title: {
|
||||
de: 'Weitergabe der Datenschutzpflichten',
|
||||
en: 'Transfer of data protection obligations',
|
||||
},
|
||||
description: {
|
||||
de: 'Datenschutzpflichten werden an Unterauftragnehmer weitergegeben',
|
||||
en: 'Data protection obligations are transferred to sub-processors',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Vertraglich vereinbart, dass Unterauftragnehmer gleichen Pflichten unterliegen',
|
||||
en: 'Contractually agreed that sub-processors are subject to same obligations',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 4 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-SUB-05',
|
||||
domain: 'SUBPROCESSOR',
|
||||
title: {
|
||||
de: 'Haftung für Unterauftragnehmer',
|
||||
en: 'Liability for sub-processors',
|
||||
},
|
||||
description: {
|
||||
de: 'Klare Haftungsregelung für Unterauftragnehmer',
|
||||
en: 'Clear liability provision for sub-processors',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Auftragsverarbeiter haftet für Unterauftragnehmer wie für eigenes Handeln',
|
||||
en: 'Processor is liable for sub-processors as for own actions',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 4 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// TOM - Technische/Organisatorische Maßnahmen
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-TOM-01',
|
||||
domain: 'TOM',
|
||||
title: {
|
||||
de: 'TOM-Dokumentation vorhanden',
|
||||
en: 'TOM documentation available',
|
||||
},
|
||||
description: {
|
||||
de: 'Vollständige Dokumentation der technischen und organisatorischen Maßnahmen',
|
||||
en: 'Complete documentation of technical and organizational measures',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'TOM-Anlage vorhanden, aktuell, spezifisch für die Verarbeitung',
|
||||
en: 'TOM annex available, current, specific to the processing',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. c DSGVO', 'Art. 32 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TOM-02',
|
||||
domain: 'TOM',
|
||||
title: {
|
||||
de: 'Verschlüsselung',
|
||||
en: 'Encryption',
|
||||
},
|
||||
description: {
|
||||
de: 'Angemessene Verschlüsselung für Daten in Transit und at Rest',
|
||||
en: 'Appropriate encryption for data in transit and at rest',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'TLS 1.2+ für Transit, AES-256 für at Rest',
|
||||
en: 'TLS 1.2+ for transit, AES-256 for at rest',
|
||||
},
|
||||
requirements: ['Art. 32 Abs. 1 lit. a DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TOM-03',
|
||||
domain: 'TOM',
|
||||
title: {
|
||||
de: 'Zugriffskontrolle',
|
||||
en: 'Access control',
|
||||
},
|
||||
description: {
|
||||
de: 'Angemessene Zugriffskontrollmechanismen',
|
||||
en: 'Appropriate access control mechanisms',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Rollenbasierte Zugriffskontrolle, Least Privilege, Logging',
|
||||
en: 'Role-based access control, least privilege, logging',
|
||||
},
|
||||
requirements: ['Art. 32 Abs. 1 lit. b DSGVO', 'ISO 27001 A.9'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TOM-04',
|
||||
domain: 'TOM',
|
||||
title: {
|
||||
de: 'Verfügbarkeit und Wiederherstellung',
|
||||
en: 'Availability and recovery',
|
||||
},
|
||||
description: {
|
||||
de: 'Maßnahmen zur Sicherstellung der Verfügbarkeit und Wiederherstellung',
|
||||
en: 'Measures to ensure availability and recovery',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Backup-Konzept, DR-Plan, RTO/RPO definiert',
|
||||
en: 'Backup concept, DR plan, RTO/RPO defined',
|
||||
},
|
||||
requirements: ['Art. 32 Abs. 1 lit. b, c DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TOM-05',
|
||||
domain: 'TOM',
|
||||
title: {
|
||||
de: 'Regelmäßige TOM-Überprüfung',
|
||||
en: 'Regular TOM review',
|
||||
},
|
||||
description: {
|
||||
de: 'Regelmäßige Überprüfung und Aktualisierung der TOM',
|
||||
en: 'Regular review and update of TOM',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'TOM werden mindestens jährlich überprüft und bei Bedarf aktualisiert',
|
||||
en: 'TOM are reviewed at least annually and updated as needed',
|
||||
},
|
||||
requirements: ['Art. 32 Abs. 1 lit. d DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-TOM-06',
|
||||
domain: 'TOM',
|
||||
title: {
|
||||
de: 'Penetrationstest',
|
||||
en: 'Penetration testing',
|
||||
},
|
||||
description: {
|
||||
de: 'Regelmäßige Penetrationstests der relevanten Systeme',
|
||||
en: 'Regular penetration testing of relevant systems',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Jährlicher Pentest, kritische Findings behoben',
|
||||
en: 'Annual pentest, critical findings resolved',
|
||||
},
|
||||
requirements: ['ISO 27001 A.12.6.1'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// CONTRACT - Vertragliche Grundlagen
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-CON-01',
|
||||
domain: 'CONTRACT',
|
||||
title: {
|
||||
de: 'Weisungsgebundenheit',
|
||||
en: 'Instruction binding',
|
||||
},
|
||||
description: {
|
||||
de: 'Auftragsverarbeiter ist an Weisungen gebunden',
|
||||
en: 'Processor is bound by instructions',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Weisungsgebundenheit explizit vereinbart, Hinweispflicht bei rechtswidrigen Weisungen',
|
||||
en: 'Instruction binding explicitly agreed, notification obligation for unlawful instructions',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. a DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-CON-02',
|
||||
domain: 'CONTRACT',
|
||||
title: {
|
||||
de: 'Vertraulichkeitsverpflichtung',
|
||||
en: 'Confidentiality obligation',
|
||||
},
|
||||
description: {
|
||||
de: 'Mitarbeiter sind zur Vertraulichkeit verpflichtet',
|
||||
en: 'Employees are obligated to confidentiality',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Vertraulichkeitsverpflichtung für alle Mitarbeiter mit Datenzugriff',
|
||||
en: 'Confidentiality obligation for all employees with data access',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. b DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-CON-03',
|
||||
domain: 'CONTRACT',
|
||||
title: {
|
||||
de: 'Gegenstand und Dauer der Verarbeitung',
|
||||
en: 'Subject and duration of processing',
|
||||
},
|
||||
description: {
|
||||
de: 'Klare Definition von Gegenstand und Dauer der Verarbeitung',
|
||||
en: 'Clear definition of subject and duration of processing',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Verarbeitungsgegenstand, Dauer, Art der Daten, Betroffene definiert',
|
||||
en: 'Processing subject, duration, type of data, data subjects defined',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-CON-04',
|
||||
domain: 'CONTRACT',
|
||||
title: {
|
||||
de: 'Schriftform/Textform',
|
||||
en: 'Written/text form',
|
||||
},
|
||||
description: {
|
||||
de: 'AVV in Schriftform oder elektronischem Format',
|
||||
en: 'DPA in written or electronic format',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'AVV in Schriftform oder elektronisch mit qualifizierter Signatur',
|
||||
en: 'DPA in written form or electronically with qualified signature',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 9 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// DATA_SUBJECT - Betroffenenrechte
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-DSR-01',
|
||||
domain: 'DATA_SUBJECT',
|
||||
title: {
|
||||
de: 'Unterstützung bei Betroffenenrechten',
|
||||
en: 'Support for data subject rights',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor unterstützt bei der Erfüllung von Betroffenenrechten',
|
||||
en: 'Vendor supports fulfillment of data subject rights',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Unterstützungspflicht vereinbart, Prozess zur Weiterleitung definiert',
|
||||
en: 'Support obligation agreed, process for forwarding defined',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. e DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-DSR-02',
|
||||
domain: 'DATA_SUBJECT',
|
||||
title: {
|
||||
de: 'Reaktionszeit für Anfragen',
|
||||
en: 'Response time for requests',
|
||||
},
|
||||
description: {
|
||||
de: 'Definierte Reaktionszeit für Betroffenenanfragen',
|
||||
en: 'Defined response time for data subject requests',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Reaktionszeit max. 5 Werktage, um Frist von 1 Monat einhalten zu können',
|
||||
en: 'Response time max. 5 business days to meet 1 month deadline',
|
||||
},
|
||||
requirements: ['Art. 12 Abs. 3 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// SECURITY - Sicherheit
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-SEC-01',
|
||||
domain: 'SECURITY',
|
||||
title: {
|
||||
de: 'Sicherheitsbewertung',
|
||||
en: 'Security assessment',
|
||||
},
|
||||
description: {
|
||||
de: 'Regelmäßige Sicherheitsbewertung des Vendors',
|
||||
en: 'Regular security assessment of the vendor',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Sicherheitsfragebogen ausgefüllt, keine kritischen Lücken',
|
||||
en: 'Security questionnaire completed, no critical gaps',
|
||||
},
|
||||
requirements: ['Art. 32 DSGVO', 'ISO 27001 A.15.2.1'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-SEC-02',
|
||||
domain: 'SECURITY',
|
||||
title: {
|
||||
de: 'Vulnerability Management',
|
||||
en: 'Vulnerability management',
|
||||
},
|
||||
description: {
|
||||
de: 'Etabliertes Vulnerability Management beim Vendor',
|
||||
en: 'Established vulnerability management at the vendor',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Regelmäßige Schwachstellen-Scans, Patch-Management dokumentiert',
|
||||
en: 'Regular vulnerability scans, patch management documented',
|
||||
},
|
||||
requirements: ['ISO 27001 A.12.6'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-SEC-03',
|
||||
domain: 'SECURITY',
|
||||
title: {
|
||||
de: 'Mitarbeiter-Schulung',
|
||||
en: 'Employee training',
|
||||
},
|
||||
description: {
|
||||
de: 'Datenschutz-Schulung für Mitarbeiter des Vendors',
|
||||
en: 'Data protection training for vendor employees',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Regelmäßige Schulungen (mind. jährlich), Nachweis verfügbar',
|
||||
en: 'Regular training (at least annually), proof available',
|
||||
},
|
||||
requirements: ['Art. 39 Abs. 1 lit. b DSGVO'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// GOVERNANCE - Governance
|
||||
// ==========================================
|
||||
{
|
||||
id: 'VND-GOV-01',
|
||||
domain: 'GOVERNANCE',
|
||||
title: {
|
||||
de: 'Datenschutzbeauftragter benannt',
|
||||
en: 'Data protection officer appointed',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor hat DSB benannt (wenn erforderlich)',
|
||||
en: 'Vendor has appointed DPO (if required)',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'DSB benannt und Kontaktdaten verfügbar',
|
||||
en: 'DPO appointed and contact details available',
|
||||
},
|
||||
requirements: ['Art. 37 DSGVO'],
|
||||
isRequired: false,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-GOV-02',
|
||||
domain: 'GOVERNANCE',
|
||||
title: {
|
||||
de: 'Verzeichnis der Verarbeitungstätigkeiten',
|
||||
en: 'Records of processing activities',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor führt eigenes Verarbeitungsverzeichnis',
|
||||
en: 'Vendor maintains own processing records',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Verzeichnis nach Art. 30 Abs. 2 DSGVO vorhanden',
|
||||
en: 'Records according to Art. 30(2) GDPR available',
|
||||
},
|
||||
requirements: ['Art. 30 Abs. 2 DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
{
|
||||
id: 'VND-GOV-03',
|
||||
domain: 'GOVERNANCE',
|
||||
title: {
|
||||
de: 'Unterstützung bei DSFA',
|
||||
en: 'Support for DPIA',
|
||||
},
|
||||
description: {
|
||||
de: 'Vendor unterstützt bei Datenschutz-Folgenabschätzung',
|
||||
en: 'Vendor supports data protection impact assessment',
|
||||
},
|
||||
passCriteria: {
|
||||
de: 'Unterstützungspflicht bei DSFA vertraglich vereinbart',
|
||||
en: 'Support obligation for DPIA contractually agreed',
|
||||
},
|
||||
requirements: ['Art. 28 Abs. 3 lit. f DSGVO'],
|
||||
isRequired: true,
|
||||
defaultFrequency: 'ANNUAL',
|
||||
},
|
||||
]
|
||||
|
||||
// ==========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* Get all controls
|
||||
*/
|
||||
export function getAllControls(): Control[] {
|
||||
return CONTROLS_LIBRARY
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controls by domain
|
||||
*/
|
||||
export function getControlsByDomain(domain: ControlDomain): Control[] {
|
||||
return CONTROLS_LIBRARY.filter((c) => c.domain === domain)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get control by ID
|
||||
*/
|
||||
export function getControlById(id: string): Control | undefined {
|
||||
return CONTROLS_LIBRARY.find((c) => c.id === id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required controls
|
||||
*/
|
||||
export function getRequiredControls(): Control[] {
|
||||
return CONTROLS_LIBRARY.filter((c) => c.isRequired)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controls by frequency
|
||||
*/
|
||||
export function getControlsByFrequency(frequency: ReviewFrequency): Control[] {
|
||||
return CONTROLS_LIBRARY.filter((c) => c.defaultFrequency === frequency)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controls applicable to vendors
|
||||
*/
|
||||
export function getVendorControls(): Control[] {
|
||||
return CONTROLS_LIBRARY.filter((c) =>
|
||||
['TRANSFER', 'AUDIT', 'DELETION', 'INCIDENT', 'SUBPROCESSOR', 'TOM', 'CONTRACT', 'DATA_SUBJECT', 'SECURITY', 'GOVERNANCE'].includes(c.domain)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get controls applicable to processing activities
|
||||
*/
|
||||
export function getProcessingActivityControls(): Control[] {
|
||||
return CONTROLS_LIBRARY.filter((c) =>
|
||||
['TOM', 'DATA_SUBJECT', 'GOVERNANCE', 'SECURITY'].includes(c.domain)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Group controls by domain
|
||||
*/
|
||||
export function getControlsGroupedByDomain(): Map<ControlDomain, Control[]> {
|
||||
const grouped = new Map<ControlDomain, Control[]>()
|
||||
|
||||
for (const control of CONTROLS_LIBRARY) {
|
||||
const existing = grouped.get(control.domain) || []
|
||||
grouped.set(control.domain, [...existing, control])
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain metadata
|
||||
*/
|
||||
export function getControlDomainMeta(domain: ControlDomain): LocalizedText {
|
||||
const meta: Record<ControlDomain, LocalizedText> = {
|
||||
TRANSFER: { de: 'Drittlandtransfer', en: 'Third Country Transfer' },
|
||||
AUDIT: { de: 'Audit & Prüfung', en: 'Audit & Review' },
|
||||
DELETION: { de: 'Löschung', en: 'Deletion' },
|
||||
INCIDENT: { de: 'Incident Response', en: 'Incident Response' },
|
||||
SUBPROCESSOR: { de: 'Unterauftragnehmer', en: 'Sub-Processors' },
|
||||
TOM: { de: 'Technische/Org. Maßnahmen', en: 'Technical/Org. Measures' },
|
||||
CONTRACT: { de: 'Vertragliche Grundlagen', en: 'Contractual Basics' },
|
||||
DATA_SUBJECT: { de: 'Betroffenenrechte', en: 'Data Subject Rights' },
|
||||
SECURITY: { de: 'Sicherheit', en: 'Security' },
|
||||
GOVERNANCE: { de: 'Governance', en: 'Governance' },
|
||||
}
|
||||
|
||||
return meta[domain]
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate control coverage
|
||||
*/
|
||||
export function calculateControlCoverage(
|
||||
controlIds: string[],
|
||||
domain?: ControlDomain
|
||||
): { covered: number; total: number; percentage: number } {
|
||||
const targetControls = domain
|
||||
? getControlsByDomain(domain)
|
||||
: getRequiredControls()
|
||||
|
||||
const covered = targetControls.filter((c) => controlIds.includes(c.id)).length
|
||||
|
||||
return {
|
||||
covered,
|
||||
total: targetControls.length,
|
||||
percentage: targetControls.length > 0 ? Math.round((covered / targetControls.length) * 100) : 0,
|
||||
}
|
||||
}
|
||||
6
admin-v2/lib/sdk/vendor-compliance/risk/index.ts
Normal file
6
admin-v2/lib/sdk/vendor-compliance/risk/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Risk & Controls exports
|
||||
*/
|
||||
|
||||
export * from './calculator'
|
||||
export * from './controls-library'
|
||||
1217
admin-v2/lib/sdk/vendor-compliance/types.ts
Normal file
1217
admin-v2/lib/sdk/vendor-compliance/types.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user