/** * 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 = { 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 = { 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 = { 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, } }