Files
breakpilot-compliance/admin-compliance/lib/sdk/loeschfristen-compliance.ts
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

326 lines
10 KiB
TypeScript

// =============================================================================
// Loeschfristen Module - Compliance Check Engine
// Prueft Policies auf Vollstaendigkeit, Konsistenz und DSGVO-Konformitaet
// =============================================================================
import {
LoeschfristPolicy,
PolicyStatus,
isPolicyOverdue,
getActiveLegalHolds,
} from './loeschfristen-types'
// =============================================================================
// TYPES
// =============================================================================
export type ComplianceIssueType =
| 'MISSING_TRIGGER'
| 'MISSING_LEGAL_BASIS'
| 'OVERDUE_REVIEW'
| 'NO_RESPONSIBLE'
| 'LEGAL_HOLD_CONFLICT'
| 'STALE_DRAFT'
| 'UNCOVERED_VVT_CATEGORY'
export type ComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
export interface ComplianceIssue {
id: string
policyId: string
policyName: string
type: ComplianceIssueType
severity: ComplianceIssueSeverity
title: string
description: string
recommendation: string
}
export interface ComplianceCheckResult {
issues: ComplianceIssue[]
score: number // 0-100
stats: {
total: number
passed: number
failed: number
bySeverity: Record<ComplianceIssueSeverity, number>
}
}
// =============================================================================
// HELPERS
// =============================================================================
let issueCounter = 0
function createIssueId(): string {
issueCounter++
return `CI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
}
function createIssue(
policy: LoeschfristPolicy,
type: ComplianceIssueType,
severity: ComplianceIssueSeverity,
title: string,
description: string,
recommendation: string
): ComplianceIssue {
return {
id: createIssueId(),
policyId: policy.policyId,
policyName: policy.dataObjectName || policy.policyId,
type,
severity,
title,
description,
recommendation,
}
}
function daysBetween(dateStr: string, now: Date): number {
const date = new Date(dateStr)
const diffMs = now.getTime() - date.getTime()
return Math.floor(diffMs / (1000 * 60 * 60 * 24))
}
// =============================================================================
// INDIVIDUAL CHECKS
// =============================================================================
/**
* Check 1: MISSING_TRIGGER (HIGH)
* Policy has no deletionTrigger set, or trigger is PURPOSE_END but no startEvent defined.
*/
function checkMissingTrigger(policy: LoeschfristPolicy): ComplianceIssue | null {
if (!policy.deletionTrigger) {
return createIssue(
policy,
'MISSING_TRIGGER',
'HIGH',
'Kein Loeschtrigger definiert',
`Die Policy "${policy.dataObjectName}" hat keinen Loeschtrigger gesetzt. Ohne Trigger ist unklar, wann die Daten geloescht werden.`,
'Definieren Sie einen Loeschtrigger (Zweckende, Aufbewahrungspflicht oder Legal Hold) fuer diese Policy.'
)
}
if (policy.deletionTrigger === 'PURPOSE_END' && !policy.startEvent.trim()) {
return createIssue(
policy,
'MISSING_TRIGGER',
'HIGH',
'Zweckende ohne Startereignis',
`Die Policy "${policy.dataObjectName}" nutzt "Zweckende" als Trigger, hat aber kein Startereignis definiert. Ohne Startereignis laesst sich der Loeschzeitpunkt nicht berechnen.`,
'Definieren Sie ein konkretes Startereignis (z.B. "Vertragsende", "Abmeldung", "Projektabschluss").'
)
}
return null
}
/**
* Check 2: MISSING_LEGAL_BASIS (HIGH)
* Policy with RETENTION_DRIVER trigger but no retentionDriver set.
*/
function checkMissingLegalBasis(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.deletionTrigger === 'RETENTION_DRIVER' && !policy.retentionDriver) {
return createIssue(
policy,
'MISSING_LEGAL_BASIS',
'HIGH',
'Aufbewahrungspflicht ohne Rechtsgrundlage',
`Die Policy "${policy.dataObjectName}" hat "Aufbewahrungspflicht" als Trigger, aber keinen konkreten Aufbewahrungstreiber (z.B. AO 147, HGB 257) zugeordnet.`,
'Waehlen Sie den passenden gesetzlichen Aufbewahrungstreiber aus oder wechseln Sie den Trigger-Typ.'
)
}
return null
}
/**
* Check 3: OVERDUE_REVIEW (MEDIUM)
* Policy where nextReviewDate is in the past.
*/
function checkOverdueReview(policy: LoeschfristPolicy): ComplianceIssue | null {
if (isPolicyOverdue(policy)) {
const overdueDays = daysBetween(policy.nextReviewDate, new Date())
return createIssue(
policy,
'OVERDUE_REVIEW',
'MEDIUM',
'Ueberfaellige Pruefung',
`Die Policy "${policy.dataObjectName}" haette am ${new Date(policy.nextReviewDate).toLocaleDateString('de-DE')} geprueft werden muessen. Die Pruefung ist ${overdueDays} Tag(e) ueberfaellig.`,
'Fuehren Sie umgehend eine Pruefung dieser Policy durch und aktualisieren Sie das naechste Pruefungsdatum.'
)
}
return null
}
/**
* Check 4: NO_RESPONSIBLE (MEDIUM)
* Policy with no responsiblePerson AND no responsibleRole.
*/
function checkNoResponsible(policy: LoeschfristPolicy): ComplianceIssue | null {
if (!policy.responsiblePerson.trim() && !policy.responsibleRole.trim()) {
return createIssue(
policy,
'NO_RESPONSIBLE',
'MEDIUM',
'Keine verantwortliche Person/Rolle',
`Die Policy "${policy.dataObjectName}" hat weder eine verantwortliche Person noch eine verantwortliche Rolle zugewiesen. Ohne Verantwortlichkeit kann die Loeschung nicht zuverlaessig durchgefuehrt werden.`,
'Weisen Sie eine verantwortliche Person oder zumindest eine verantwortliche Rolle (z.B. "Datenschutzbeauftragter", "IT-Leitung") zu.'
)
}
return null
}
/**
* Check 5: LEGAL_HOLD_CONFLICT (CRITICAL)
* Policy has active legal hold but deletionMethod is AUTO_DELETE.
*/
function checkLegalHoldConflict(policy: LoeschfristPolicy): ComplianceIssue | null {
const activeHolds = getActiveLegalHolds(policy)
if (activeHolds.length > 0 && policy.deletionMethod === 'AUTO_DELETE') {
const holdReasons = activeHolds.map((h) => h.reason).join(', ')
return createIssue(
policy,
'LEGAL_HOLD_CONFLICT',
'CRITICAL',
'Legal Hold mit automatischer Loeschung',
`Die Policy "${policy.dataObjectName}" hat ${activeHolds.length} aktive(n) Legal Hold(s) (${holdReasons}), aber die Loeschmethode ist auf "Automatische Loeschung" gesetzt. Dies kann zu unbeabsichtigter Vernichtung von Beweismitteln fuehren.`,
'Aendern Sie die Loeschmethode auf "Manuelle Pruefung & Loeschung" oder deaktivieren Sie die automatische Loeschung, solange der Legal Hold aktiv ist.'
)
}
return null
}
/**
* Check 6: STALE_DRAFT (LOW)
* Policy in DRAFT status older than 90 days.
*/
function checkStaleDraft(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status === 'DRAFT') {
const ageInDays = daysBetween(policy.createdAt, new Date())
if (ageInDays > 90) {
return createIssue(
policy,
'STALE_DRAFT',
'LOW',
'Veralteter Entwurf',
`Die Policy "${policy.dataObjectName}" ist seit ${ageInDays} Tagen im Entwurfsstatus. Entwuerfe, die laenger als 90 Tage nicht finalisiert werden, deuten auf unvollstaendige Dokumentation hin.`,
'Finalisieren Sie den Entwurf und setzen Sie den Status auf "Aktiv", oder archivieren Sie die Policy, falls sie nicht mehr benoetigt wird.'
)
}
}
return null
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
/**
* Fuehrt einen vollstaendigen Compliance-Check ueber alle Policies durch.
*
* @param policies - Alle Loeschfrist-Policies
* @param vvtDataCategories - Optionale Datenkategorien aus dem VVT (localStorage)
* @returns ComplianceCheckResult mit Issues, Score und Statistiken
*/
export function runComplianceCheck(
policies: LoeschfristPolicy[],
vvtDataCategories?: string[]
): ComplianceCheckResult {
// Reset counter for deterministic IDs within a single check run
issueCounter = 0
const issues: ComplianceIssue[] = []
// Run checks 1-6 for each policy
for (const policy of policies) {
const checks = [
checkMissingTrigger(policy),
checkMissingLegalBasis(policy),
checkOverdueReview(policy),
checkNoResponsible(policy),
checkLegalHoldConflict(policy),
checkStaleDraft(policy),
]
for (const issue of checks) {
if (issue !== null) {
issues.push(issue)
}
}
}
// Check 7: UNCOVERED_VVT_CATEGORY (MEDIUM)
if (vvtDataCategories && vvtDataCategories.length > 0) {
const coveredCategories = new Set<string>()
for (const policy of policies) {
for (const category of policy.dataCategories) {
coveredCategories.add(category.toLowerCase().trim())
}
}
for (const vvtCategory of vvtDataCategories) {
const normalized = vvtCategory.toLowerCase().trim()
if (!coveredCategories.has(normalized)) {
issues.push({
id: createIssueId(),
policyId: '-',
policyName: '-',
type: 'UNCOVERED_VVT_CATEGORY',
severity: 'MEDIUM',
title: `Datenkategorie ohne Loeschfrist: "${vvtCategory}"`,
description: `Die Datenkategorie "${vvtCategory}" ist im Verzeichnis der Verarbeitungstaetigkeiten (VVT) erfasst, hat aber keine zugehoerige Loeschfrist-Policy. Gemaess DSGVO Art. 5 Abs. 1 lit. e muss fuer jede Datenkategorie eine Speicherbegrenzung definiert sein.`,
recommendation: `Erstellen Sie eine neue Loeschfrist-Policy fuer die Datenkategorie "${vvtCategory}" oder ordnen Sie sie einer bestehenden Policy zu.`,
})
}
}
}
// Calculate score
const bySeverity: Record<ComplianceIssueSeverity, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0,
CRITICAL: 0,
}
for (const issue of issues) {
bySeverity[issue.severity]++
}
const rawScore =
100 -
(bySeverity.CRITICAL * 15 +
bySeverity.HIGH * 10 +
bySeverity.MEDIUM * 5 +
bySeverity.LOW * 2)
const score = Math.max(0, rawScore)
// Calculate pass/fail per policy
const failedPolicyIds = new Set(
issues.filter((i) => i.policyId !== '-').map((i) => i.policyId)
)
const totalPolicies = policies.length
const failedCount = failedPolicyIds.size
const passedCount = totalPolicies - failedCount
return {
issues,
score,
stats: {
total: totalPolicies,
passed: passedCount,
failed: failedCount,
bySeverity,
},
}
}