Split these files that exceeded the 500-line hard cap: - privacy-policy.ts (965 LOC) -> sections + renderers - academy/api.ts (787 LOC) -> courses + mock-data - whistleblower/api.ts (755 LOC) -> operations + mock-data - vvt-profiling.ts (659 LOC) -> data + logic - cookie-banner.ts (595 LOC) -> config + embed - dsr/types.ts (581 LOC) -> core + api types - tom-generator/rules-engine.ts (560 LOC) -> evaluator + gap-analysis - datapoint-helpers.ts (548 LOC) -> generators + validators Each original file becomes a barrel re-export for backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
/**
|
|
* VVT Profiling — Generator Logic, Enrichment & Helpers
|
|
*
|
|
* Generates baseline VVT activities from profiling answers.
|
|
*/
|
|
|
|
import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog'
|
|
import { generateVVTId } from './vvt-types'
|
|
import type { VVTActivity } from './vvt-types'
|
|
import {
|
|
PROFILING_QUESTIONS,
|
|
type ProfilingAnswers,
|
|
type ProfilingResult,
|
|
} from './vvt-profiling-data'
|
|
|
|
// =============================================================================
|
|
// GENERATOR LOGIC
|
|
// =============================================================================
|
|
|
|
export function generateActivities(answers: ProfilingAnswers): ProfilingResult {
|
|
const triggeredIds = new Set<string>()
|
|
|
|
for (const question of PROFILING_QUESTIONS) {
|
|
const answer = answers[question.id]
|
|
if (!answer) continue
|
|
|
|
if (question.type === 'boolean' && answer === true) {
|
|
question.triggersTemplates.forEach(id => triggeredIds.add(id))
|
|
}
|
|
}
|
|
|
|
// Always add IT baseline templates
|
|
triggeredIds.add('it-systemadministration')
|
|
triggeredIds.add('it-backup')
|
|
triggeredIds.add('it-logging')
|
|
triggeredIds.add('it-iam')
|
|
|
|
const existingIds: string[] = []
|
|
const activities: VVTActivity[] = []
|
|
|
|
for (const templateId of triggeredIds) {
|
|
const template = VVT_BASELINE_CATALOG.find(t => t.templateId === templateId)
|
|
if (!template) continue
|
|
|
|
const vvtId = generateVVTId(existingIds)
|
|
existingIds.push(vvtId)
|
|
|
|
const activity = templateToActivity(template, vvtId)
|
|
enrichActivityFromAnswers(activity, answers)
|
|
activities.push(activity)
|
|
}
|
|
|
|
// Calculate coverage score
|
|
const totalFields = activities.length * 12
|
|
let filledFields = 0
|
|
for (const a of activities) {
|
|
if (a.name) filledFields++
|
|
if (a.description) filledFields++
|
|
if (a.purposes.length > 0) filledFields++
|
|
if (a.legalBases.length > 0) filledFields++
|
|
if (a.dataSubjectCategories.length > 0) filledFields++
|
|
if (a.personalDataCategories.length > 0) filledFields++
|
|
if (a.recipientCategories.length > 0) filledFields++
|
|
if (a.retentionPeriod.description) filledFields++
|
|
if (a.tomDescription) filledFields++
|
|
if (a.businessFunction !== 'other') filledFields++
|
|
if (a.structuredToms.accessControl.length > 0) filledFields++
|
|
if (a.responsible || a.owner) filledFields++
|
|
}
|
|
|
|
const coverageScore = totalFields > 0 ? Math.round((filledFields / totalFields) * 100) : 0
|
|
|
|
const employeeCount = typeof answers.org_employees === 'number' ? answers.org_employees : 0
|
|
const hasSpecialCategories = answers.data_health === true || answers.data_biometric === true || answers.data_criminal === true
|
|
const art30Abs5Exempt = employeeCount < 250 && !hasSpecialCategories
|
|
|
|
return {
|
|
answers,
|
|
generatedActivities: activities,
|
|
coverageScore,
|
|
art30Abs5Exempt,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// ENRICHMENT
|
|
// =============================================================================
|
|
|
|
function enrichActivityFromAnswers(activity: VVTActivity, answers: ProfilingAnswers): void {
|
|
if (answers.transfer_cloud_us === true) {
|
|
activity.thirdCountryTransfers.push({
|
|
country: 'US',
|
|
recipient: 'Cloud-Dienstleister (USA)',
|
|
transferMechanism: 'SCC_PROCESSOR',
|
|
additionalMeasures: ['Verschluesselung at-rest', 'Transfer Impact Assessment'],
|
|
})
|
|
}
|
|
|
|
if (answers.data_health === true) {
|
|
if (!activity.personalDataCategories.includes('HEALTH_DATA')) {
|
|
if (activity.businessFunction === 'hr') {
|
|
activity.personalDataCategories.push('HEALTH_DATA')
|
|
if (!activity.legalBases.some(lb => lb.type.startsWith('ART9_'))) {
|
|
activity.legalBases.push({
|
|
type: 'ART9_EMPLOYMENT',
|
|
description: 'Arbeitsrechtliche Verarbeitung',
|
|
reference: 'Art. 9 Abs. 2 lit. b DSGVO',
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (answers.data_minors === true) {
|
|
if (!activity.dataSubjectCategories.includes('MINORS')) {
|
|
if (activity.businessFunction === 'support' || activity.businessFunction === 'product_engineering') {
|
|
activity.dataSubjectCategories.push('MINORS')
|
|
}
|
|
}
|
|
}
|
|
|
|
if (answers.special_ai === true || answers.special_video_surveillance === true || answers.special_tracking === true) {
|
|
if (answers.special_ai === true && activity.businessFunction === 'product_engineering') {
|
|
activity.dpiaRequired = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPERS
|
|
// =============================================================================
|
|
|
|
export function getQuestionsForStep(step: number) {
|
|
return PROFILING_QUESTIONS.filter(q => q.step === step)
|
|
}
|
|
|
|
export function getStepProgress(answers: ProfilingAnswers, step: number): number {
|
|
const questions = getQuestionsForStep(step)
|
|
if (questions.length === 0) return 100
|
|
|
|
const answered = questions.filter(q => {
|
|
const a = answers[q.id]
|
|
return a !== undefined && a !== null && a !== ''
|
|
}).length
|
|
|
|
return Math.round((answered / questions.length) * 100)
|
|
}
|
|
|
|
export function getTotalProgress(answers: ProfilingAnswers): number {
|
|
const total = PROFILING_QUESTIONS.length
|
|
if (total === 0) return 100
|
|
|
|
const answered = PROFILING_QUESTIONS.filter(q => {
|
|
const a = answers[q.id]
|
|
return a !== undefined && a !== null && a !== ''
|
|
}).length
|
|
|
|
return Math.round((answered / total) * 100)
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMPLIANCE SCOPE INTEGRATION
|
|
// =============================================================================
|
|
|
|
export function prefillFromScopeAnswers(
|
|
scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[]
|
|
): ProfilingAnswers {
|
|
const { exportToVVTAnswers } = require('./compliance-scope-profiling')
|
|
const exported = exportToVVTAnswers(scopeAnswers) as Record<string, unknown>
|
|
const prefilled: ProfilingAnswers = {}
|
|
|
|
for (const [key, value] of Object.entries(exported)) {
|
|
if (value !== undefined && value !== null) {
|
|
prefilled[key] = value as string | string[] | number | boolean
|
|
}
|
|
}
|
|
|
|
return prefilled
|
|
}
|
|
|
|
export const SCOPE_PREFILLED_VVT_QUESTIONS = [
|
|
'org_industry', 'org_employees', 'org_b2b_b2c',
|
|
'dept_hr', 'dept_finance', 'dept_marketing',
|
|
'data_health', 'data_minors', 'data_biometric', 'data_criminal',
|
|
'special_ai', 'special_video_surveillance', 'special_tracking',
|
|
'transfer_cloud_us', 'transfer_subprocessor', 'transfer_support_non_eu',
|
|
]
|