Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
489 lines
14 KiB
TypeScript
489 lines
14 KiB
TypeScript
/**
|
|
* 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,
|
|
}
|
|
}
|