obligations-document, tom-document, loeschfristen-document, compliance-scope-triggers, sdk-flow/flow-data, processing-activities, loeschfristen-baseline-catalog, catalog-registry, dsfa mitigation-library + risk-catalog, vvt-baseline-catalog, vendor contract-review checklists + findings, demo-data, tom-compliance. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
329 lines
14 KiB
TypeScript
329 lines
14 KiB
TypeScript
// =============================================================================
|
|
// TOM Compliance Checks — Per-TOM and Aggregate checks
|
|
//
|
|
// Barrel-split from tom-compliance.ts. Do NOT import directly; use tom-compliance.ts.
|
|
// =============================================================================
|
|
|
|
import type {
|
|
TOMGeneratorState,
|
|
DerivedTOM,
|
|
RiskProfile,
|
|
DataProfile,
|
|
ControlCategory,
|
|
} from './tom-generator/types'
|
|
|
|
import { getControlById } from './tom-generator/controls/loader'
|
|
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
|
|
import type { TOMComplianceIssue, TOMComplianceIssueType, TOMComplianceIssueSeverity } from './tom-compliance'
|
|
|
|
// =============================================================================
|
|
// HELPERS
|
|
// =============================================================================
|
|
|
|
let issueCounter = 0
|
|
|
|
export function resetIssueCounter(): void {
|
|
issueCounter = 0
|
|
}
|
|
|
|
export function createIssue(
|
|
controlId: string,
|
|
controlName: string,
|
|
type: TOMComplianceIssueType,
|
|
severity: TOMComplianceIssueSeverity,
|
|
title: string,
|
|
description: string,
|
|
recommendation: string
|
|
): TOMComplianceIssue {
|
|
issueCounter++
|
|
const id = `TCI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
|
|
return { id, controlId, controlName, type, severity, title, description, recommendation }
|
|
}
|
|
|
|
function daysBetween(date: Date, now: Date): number {
|
|
const diffMs = now.getTime() - date.getTime()
|
|
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
}
|
|
|
|
// =============================================================================
|
|
// PER-TOM CHECKS (1-3, 11)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Check 1: MISSING_RESPONSIBLE (MEDIUM)
|
|
*/
|
|
export function checkMissingResponsible(tom: DerivedTOM): TOMComplianceIssue | null {
|
|
if (tom.applicability !== 'REQUIRED') return null
|
|
|
|
if (!tom.responsiblePerson && !tom.responsibleDepartment) {
|
|
return createIssue(
|
|
tom.controlId, tom.name, 'MISSING_RESPONSIBLE', 'MEDIUM',
|
|
'Keine verantwortliche Person/Abteilung',
|
|
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, hat aber weder eine verantwortliche Person noch eine verantwortliche Abteilung zugewiesen. Ohne klare Verantwortlichkeit kann die Massnahme nicht zuverlaessig umgesetzt und gepflegt werden.`,
|
|
'Weisen Sie eine verantwortliche Person oder Abteilung zu, die fuer die Umsetzung und regelmaessige Pruefung dieser Massnahme zustaendig ist.'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check 2: OVERDUE_REVIEW (MEDIUM)
|
|
*/
|
|
export function checkOverdueReview(tom: DerivedTOM): TOMComplianceIssue | null {
|
|
if (!tom.reviewDate) return null
|
|
|
|
const reviewDate = new Date(tom.reviewDate)
|
|
const now = new Date()
|
|
|
|
if (reviewDate < now) {
|
|
const overdueDays = daysBetween(reviewDate, now)
|
|
return createIssue(
|
|
tom.controlId, tom.name, 'OVERDUE_REVIEW', 'MEDIUM',
|
|
'Ueberfaellige Pruefung',
|
|
`Die TOM "${tom.name}" haette am ${reviewDate.toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig. Gemaess Art. 32 Abs. 1 lit. d DSGVO ist eine regelmaessige Ueberpruefung der Wirksamkeit von TOMs erforderlich.`,
|
|
'Fuehren Sie umgehend eine Wirksamkeitspruefung dieser Massnahme durch und aktualisieren Sie das naechste Pruefungsdatum.'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check 3: MISSING_EVIDENCE (HIGH)
|
|
*/
|
|
export function checkMissingEvidence(tom: DerivedTOM): TOMComplianceIssue | null {
|
|
if (tom.implementationStatus !== 'IMPLEMENTED') return null
|
|
if (tom.linkedEvidence.length > 0) return null
|
|
|
|
const control = getControlById(tom.controlId)
|
|
if (!control || control.evidenceRequirements.length === 0) return null
|
|
|
|
return createIssue(
|
|
tom.controlId, tom.name, 'MISSING_EVIDENCE', 'HIGH',
|
|
'Kein Nachweis hinterlegt',
|
|
`Die TOM "${tom.name}" ist als IMPLEMENTED markiert, hat aber keine verknuepften Nachweisdokumente. Der Control erfordert ${control.evidenceRequirements.length} Nachweis(e): ${control.evidenceRequirements.join(', ')}. Ohne Nachweise ist die Umsetzung nicht auditfaehig.`,
|
|
'Laden Sie die erforderlichen Nachweisdokumente hoch und verknuepfen Sie sie mit dieser Massnahme.'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Check 11: STALE_NOT_IMPLEMENTED (LOW)
|
|
*/
|
|
export function checkStaleNotImplemented(tom: DerivedTOM, state: TOMGeneratorState): TOMComplianceIssue | null {
|
|
if (tom.applicability !== 'REQUIRED') return null
|
|
if (tom.implementationStatus !== 'NOT_IMPLEMENTED') return null
|
|
if (tom.implementationDate !== null) return null
|
|
|
|
const referenceDate = state.createdAt ? new Date(state.createdAt) : (state.updatedAt ? new Date(state.updatedAt) : null)
|
|
if (!referenceDate) return null
|
|
|
|
const ageInDays = daysBetween(referenceDate, new Date())
|
|
if (ageInDays <= 90) return null
|
|
|
|
return createIssue(
|
|
tom.controlId, tom.name, 'STALE_NOT_IMPLEMENTED', 'LOW',
|
|
'Langfristig nicht umgesetzte Pflichtmassnahme',
|
|
`Die TOM "${tom.name}" ist als REQUIRED eingestuft, aber seit ${ageInDays} Tagen nicht umgesetzt. Pflichtmassnahmen, die laenger als 90 Tage nicht implementiert werden, deuten auf organisatorische Blockaden oder unzureichende Priorisierung hin.`,
|
|
'Pruefen Sie, ob die Massnahme weiterhin erforderlich ist, und erstellen Sie einen konkreten Umsetzungsplan mit Verantwortlichkeiten und Fristen.'
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// AGGREGATE CHECKS (4-10)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Check 4: INCOMPLETE_CATEGORY (HIGH)
|
|
*/
|
|
export function checkIncompleteCategory(toms: DerivedTOM[]): TOMComplianceIssue[] {
|
|
const issues: TOMComplianceIssue[] = []
|
|
const categoryMap = new Map<ControlCategory, DerivedTOM[]>()
|
|
|
|
for (const tom of toms) {
|
|
const control = getControlById(tom.controlId)
|
|
if (!control) continue
|
|
const category = control.category
|
|
if (!categoryMap.has(category)) categoryMap.set(category, [])
|
|
categoryMap.get(category)!.push(tom)
|
|
}
|
|
|
|
for (const [category, categoryToms] of Array.from(categoryMap.entries())) {
|
|
const requiredToms = categoryToms.filter((t: DerivedTOM) => t.applicability === 'REQUIRED')
|
|
if (requiredToms.length === 0) continue
|
|
|
|
const allNotImplemented = requiredToms.every((t: DerivedTOM) => t.implementationStatus === 'NOT_IMPLEMENTED')
|
|
if (allNotImplemented) {
|
|
issues.push(createIssue(
|
|
category, category, 'INCOMPLETE_CATEGORY', 'HIGH',
|
|
`Kategorie "${category}" vollstaendig ohne Umsetzung`,
|
|
`Alle ${requiredToms.length} Pflichtmassnahme(n) in der Kategorie "${category}" sind nicht umgesetzt. Eine vollstaendig unabgedeckte Kategorie stellt eine erhebliche Luecke im TOM-Konzept dar.`,
|
|
`Setzen Sie mindestens die wichtigsten Massnahmen in der Kategorie "${category}" um, um eine Grundabdeckung sicherzustellen.`
|
|
))
|
|
}
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
/**
|
|
* Check 5: NO_ENCRYPTION_MEASURES (CRITICAL)
|
|
*/
|
|
export function checkNoEncryption(toms: DerivedTOM[]): TOMComplianceIssue | null {
|
|
const hasImplementedEncryption = toms.some((tom) => {
|
|
const control = getControlById(tom.controlId)
|
|
return control?.category === 'ENCRYPTION' && tom.implementationStatus === 'IMPLEMENTED'
|
|
})
|
|
|
|
if (!hasImplementedEncryption) {
|
|
return createIssue(
|
|
'ENCRYPTION', 'Verschluesselung', 'NO_ENCRYPTION_MEASURES', 'CRITICAL',
|
|
'Keine Verschluesselungsmassnahmen umgesetzt',
|
|
'Es ist keine einzige Verschluesselungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. a DSGVO nennt Verschluesselung explizit als geeignete technische Massnahme. Ohne Verschluesselung sind personenbezogene Daten bei Zugriff oder Verlust ungeschuetzt.',
|
|
'Implementieren Sie umgehend Verschluesselungsmassnahmen fuer Daten im Ruhezustand (Encryption at Rest) und waehrend der Uebertragung (Encryption in Transit).'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check 6: NO_PSEUDONYMIZATION (MEDIUM)
|
|
*/
|
|
export function checkNoPseudonymization(toms: DerivedTOM[], dataProfile: DataProfile | null): TOMComplianceIssue | null {
|
|
if (!dataProfile || !dataProfile.hasSpecialCategories) return null
|
|
|
|
const hasImplementedPseudonymization = toms.some((tom) => {
|
|
const control = getControlById(tom.controlId)
|
|
return control?.category === 'PSEUDONYMIZATION' && tom.implementationStatus === 'IMPLEMENTED'
|
|
})
|
|
|
|
if (!hasImplementedPseudonymization) {
|
|
return createIssue(
|
|
'PSEUDONYMIZATION', 'Pseudonymisierung', 'NO_PSEUDONYMIZATION', 'MEDIUM',
|
|
'Keine Pseudonymisierung bei besonderen Datenkategorien',
|
|
'Das Datenprofil enthaelt besondere Kategorien personenbezogener Daten (Art. 9 DSGVO), aber keine Pseudonymisierungsmassnahme ist umgesetzt. Art. 32 Abs. 1 lit. a DSGVO empfiehlt Pseudonymisierung ausdruecklich als Schutzmassnahme.',
|
|
'Implementieren Sie Pseudonymisierungsmassnahmen fuer die Verarbeitung besonderer Datenkategorien, um das Risiko fuer betroffene Personen zu minimieren.'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check 7: MISSING_AVAILABILITY (HIGH)
|
|
*/
|
|
export function checkMissingAvailability(toms: DerivedTOM[], state: TOMGeneratorState): TOMComplianceIssue | null {
|
|
const hasAvailabilityOrRecovery = toms.some((tom) => {
|
|
const control = getControlById(tom.controlId)
|
|
return (
|
|
(control?.category === 'AVAILABILITY' || control?.category === 'RECOVERY') &&
|
|
tom.implementationStatus === 'IMPLEMENTED'
|
|
)
|
|
})
|
|
|
|
const hasDRPlan = state.securityProfile?.hasDRPlan ?? false
|
|
|
|
if (!hasAvailabilityOrRecovery && !hasDRPlan) {
|
|
return createIssue(
|
|
'AVAILABILITY', 'Verfuegbarkeit / Wiederherstellbarkeit', 'MISSING_AVAILABILITY', 'HIGH',
|
|
'Keine Verfuegbarkeits- oder Wiederherstellungsmassnahmen',
|
|
'Weder Verfuegbarkeits- noch Wiederherstellungsmassnahmen sind umgesetzt, und es existiert kein Disaster-Recovery-Plan im Security-Profil. Art. 32 Abs. 1 lit. b und c DSGVO verlangen die Faehigkeit zur raschen Wiederherstellung der Verfuegbarkeit personenbezogener Daten.',
|
|
'Implementieren Sie Backup-Konzepte, Redundanzloesungen und einen Disaster-Recovery-Plan, um die Verfuegbarkeit und Wiederherstellbarkeit sicherzustellen.'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check 8: NO_REVIEW_PROCESS (MEDIUM)
|
|
*/
|
|
export function checkNoReviewProcess(toms: DerivedTOM[]): TOMComplianceIssue | null {
|
|
const hasImplementedReview = toms.some((tom) => {
|
|
const control = getControlById(tom.controlId)
|
|
return control?.category === 'REVIEW' && tom.implementationStatus === 'IMPLEMENTED'
|
|
})
|
|
|
|
if (!hasImplementedReview) {
|
|
return createIssue(
|
|
'REVIEW', 'Ueberpruefung & Bewertung', 'NO_REVIEW_PROCESS', 'MEDIUM',
|
|
'Kein Verfahren zur regelmaessigen Ueberpruefung',
|
|
'Es ist keine Ueberpruefungsmassnahme als IMPLEMENTED markiert. Art. 32 Abs. 1 lit. d DSGVO verlangt ein Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung der Wirksamkeit der technischen und organisatorischen Massnahmen.',
|
|
'Implementieren Sie einen regelmaessigen Review-Prozess (z.B. quartalsweise TOM-Audits, jaehrliche Wirksamkeitspruefung) und dokumentieren Sie die Ergebnisse.'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Check 9: UNCOVERED_SDM_GOAL (HIGH)
|
|
*/
|
|
export function checkUncoveredSDMGoal(toms: DerivedTOM[]): TOMComplianceIssue[] {
|
|
const issues: TOMComplianceIssue[] = []
|
|
|
|
const sdmGoals = [
|
|
'Verfuegbarkeit', 'Integritaet', 'Vertraulichkeit',
|
|
'Nichtverkettung', 'Intervenierbarkeit', 'Transparenz', 'Datenminimierung',
|
|
] as const
|
|
|
|
const goalToCategoriesMap = new Map<string, ControlCategory[]>()
|
|
for (const goal of sdmGoals) goalToCategoriesMap.set(goal, [])
|
|
|
|
for (const [category, goals] of Object.entries(SDM_CATEGORY_MAPPING)) {
|
|
for (const goal of goals) {
|
|
const existing = goalToCategoriesMap.get(goal)
|
|
if (existing) existing.push(category as ControlCategory)
|
|
}
|
|
}
|
|
|
|
const implementedCategories = new Set<ControlCategory>()
|
|
for (const tom of toms) {
|
|
if (tom.implementationStatus !== 'IMPLEMENTED') continue
|
|
const control = getControlById(tom.controlId)
|
|
if (control) implementedCategories.add(control.category)
|
|
}
|
|
|
|
for (const goal of sdmGoals) {
|
|
const coveringCategories = goalToCategoriesMap.get(goal) ?? []
|
|
const hasCoverage = coveringCategories.some((cat) => implementedCategories.has(cat))
|
|
|
|
if (!hasCoverage) {
|
|
issues.push(createIssue(
|
|
`SDM-${goal}`, goal, 'UNCOVERED_SDM_GOAL', 'HIGH',
|
|
`SDM-Gewaehrleistungsziel "${goal}" nicht abgedeckt`,
|
|
`Das Gewaehrleistungsziel "${goal}" des Standard-Datenschutzmodells (SDM) ist durch keine umgesetzte Massnahme abgedeckt. Zugehoerige Kategorien (${coveringCategories.join(', ')}) haben keine IMPLEMENTED Controls. Das SDM ist die anerkannte Methodik zur Umsetzung der DSGVO-Anforderungen.`,
|
|
`Setzen Sie mindestens eine Massnahme aus den Kategorien ${coveringCategories.join(', ')} um, um das SDM-Ziel "${goal}" abzudecken.`
|
|
))
|
|
}
|
|
}
|
|
|
|
return issues
|
|
}
|
|
|
|
/**
|
|
* Check 10: HIGH_RISK_WITHOUT_MEASURES (CRITICAL)
|
|
*/
|
|
export function checkHighRiskWithoutMeasures(toms: DerivedTOM[], riskProfile: RiskProfile | null): TOMComplianceIssue | null {
|
|
if (!riskProfile || riskProfile.protectionLevel !== 'VERY_HIGH') return null
|
|
|
|
const requiredToms = toms.filter((t) => t.applicability === 'REQUIRED')
|
|
if (requiredToms.length === 0) return null
|
|
|
|
const implementedCount = requiredToms.filter((t) => t.implementationStatus === 'IMPLEMENTED').length
|
|
const implementationRate = implementedCount / requiredToms.length
|
|
|
|
if (implementationRate < 0.5) {
|
|
const percentage = Math.round(implementationRate * 100)
|
|
return createIssue(
|
|
'RISK-PROFILE', 'Risikoprofil VERY_HIGH', 'HIGH_RISK_WITHOUT_MEASURES', 'CRITICAL',
|
|
'Sehr hoher Schutzbedarf bei niedriger Umsetzungsrate',
|
|
`Der Schutzbedarf ist als VERY_HIGH eingestuft, aber nur ${implementedCount} von ${requiredToms.length} Pflichtmassnahmen (${percentage}%) sind umgesetzt. Bei sehr hohem Schutzbedarf muessen mindestens 50% der Pflichtmassnahmen implementiert sein, um ein angemessenes Schutzniveau gemaess Art. 32 DSGVO zu gewaehrleisten.`,
|
|
'Priorisieren Sie die Umsetzung der verbleibenden Pflichtmassnahmen. Beginnen Sie mit CRITICAL- und HIGH-Priority Controls. Erwaeegen Sie einen Umsetzungsplan mit klaren Meilensteinen.'
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|