Phase A: TOM document HTML generator (12 sections, inline CSS, A4 print) Phase B: TOMDocumentTab component (org-header form, revisions, print/download) Phase C: 11 compliance checks with severity-weighted scoring Phase D: MkDocs documentation for TOM module Phase E: 25 new controls (63 → 88) in 13 categories Canonical Control Mapping (three-layer architecture): - Migration 068: tom_control_mappings + tom_control_sync_state tables - 6 API endpoints: sync, list, by-tom, stats, manual add, delete - Category mapping: 13 TOM categories → 17 canonical categories - Frontend: sync button + coverage card (Overview), drill-down (Editor), belegende Controls count (Document) - 20 tests (unit + API with mocked DB) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
554 lines
20 KiB
TypeScript
554 lines
20 KiB
TypeScript
// =============================================================================
|
|
// TOM Module - Compliance Check Engine
|
|
// Prueft Technische und Organisatorische Massnahmen auf Vollstaendigkeit,
|
|
// Konsistenz und DSGVO-Konformitaet (Art. 32 DSGVO)
|
|
// =============================================================================
|
|
|
|
import type {
|
|
TOMGeneratorState,
|
|
DerivedTOM,
|
|
RiskProfile,
|
|
DataProfile,
|
|
ControlCategory,
|
|
ImplementationStatus,
|
|
} from './tom-generator/types'
|
|
|
|
import { getControlById, getControlsByCategory, getAllCategories } from './tom-generator/controls/loader'
|
|
import { SDM_CATEGORY_MAPPING } from './tom-generator/types'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
export type TOMComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
|
|
|
|
export type TOMComplianceIssueType =
|
|
| 'MISSING_RESPONSIBLE'
|
|
| 'OVERDUE_REVIEW'
|
|
| 'MISSING_EVIDENCE'
|
|
| 'INCOMPLETE_CATEGORY'
|
|
| 'NO_ENCRYPTION_MEASURES'
|
|
| 'NO_PSEUDONYMIZATION'
|
|
| 'MISSING_AVAILABILITY'
|
|
| 'NO_REVIEW_PROCESS'
|
|
| 'UNCOVERED_SDM_GOAL'
|
|
| 'HIGH_RISK_WITHOUT_MEASURES'
|
|
| 'STALE_NOT_IMPLEMENTED'
|
|
|
|
export interface TOMComplianceIssue {
|
|
id: string
|
|
controlId: string
|
|
controlName: string
|
|
type: TOMComplianceIssueType
|
|
severity: TOMComplianceIssueSeverity
|
|
title: string
|
|
description: string
|
|
recommendation: string
|
|
}
|
|
|
|
export interface TOMComplianceCheckResult {
|
|
issues: TOMComplianceIssue[]
|
|
score: number // 0-100
|
|
stats: {
|
|
total: number
|
|
passed: number
|
|
failed: number
|
|
bySeverity: Record<TOMComplianceIssueSeverity, number>
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONSTANTS
|
|
// =============================================================================
|
|
|
|
export const TOM_SEVERITY_LABELS_DE: Record<TOMComplianceIssueSeverity, string> = {
|
|
CRITICAL: 'Kritisch',
|
|
HIGH: 'Hoch',
|
|
MEDIUM: 'Mittel',
|
|
LOW: 'Niedrig',
|
|
}
|
|
|
|
export const TOM_SEVERITY_COLORS: Record<TOMComplianceIssueSeverity, string> = {
|
|
CRITICAL: '#dc2626',
|
|
HIGH: '#ea580c',
|
|
MEDIUM: '#d97706',
|
|
LOW: '#6b7280',
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPERS
|
|
// =============================================================================
|
|
|
|
let issueCounter = 0
|
|
|
|
function createIssueId(): string {
|
|
issueCounter++
|
|
return `TCI-${Date.now()}-${String(issueCounter).padStart(4, '0')}`
|
|
}
|
|
|
|
function createIssue(
|
|
controlId: string,
|
|
controlName: string,
|
|
type: TOMComplianceIssueType,
|
|
severity: TOMComplianceIssueSeverity,
|
|
title: string,
|
|
description: string,
|
|
recommendation: string
|
|
): TOMComplianceIssue {
|
|
return { id: createIssueId(), 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)
|
|
* REQUIRED TOM without responsiblePerson AND responsibleDepartment.
|
|
*/
|
|
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)
|
|
* TOM with reviewDate in the past.
|
|
*/
|
|
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)
|
|
* IMPLEMENTED TOM where linkedEvidence is empty but the control has evidenceRequirements.
|
|
*/
|
|
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)
|
|
* REQUIRED TOM that has been NOT_IMPLEMENTED for >90 days.
|
|
* Uses implementationDate === null and state.createdAt / state.updatedAt as reference.
|
|
*/
|
|
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)
|
|
* Category where ALL applicable (REQUIRED) controls are NOT_IMPLEMENTED.
|
|
*/
|
|
function checkIncompleteCategory(toms: DerivedTOM[]): TOMComplianceIssue[] {
|
|
const issues: TOMComplianceIssue[] = []
|
|
|
|
// Group applicable TOMs by category
|
|
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())) {
|
|
// Only check categories that have at least one REQUIRED control
|
|
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)
|
|
* No ENCRYPTION control with status IMPLEMENTED.
|
|
*/
|
|
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)
|
|
* DataProfile has special categories (Art. 9) but no PSEUDONYMIZATION control implemented.
|
|
*/
|
|
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)
|
|
* No AVAILABILITY or RECOVERY control implemented AND no DR plan in securityProfile.
|
|
*/
|
|
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)
|
|
* No REVIEW control implemented (Art. 32 Abs. 1 lit. d requires periodic review).
|
|
*/
|
|
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)
|
|
* SDM goal with 0% coverage — no implemented control maps to it via SDM_CATEGORY_MAPPING.
|
|
*/
|
|
function checkUncoveredSDMGoal(toms: DerivedTOM[]): TOMComplianceIssue[] {
|
|
const issues: TOMComplianceIssue[] = []
|
|
|
|
// Build reverse mapping: SDM goal -> ControlCategories that cover it
|
|
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, [])
|
|
}
|
|
|
|
// Build reverse lookup from SDM_CATEGORY_MAPPING
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect implemented categories
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Check each SDM goal
|
|
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)
|
|
* Protection level VERY_HIGH but < 50% of REQUIRED controls implemented.
|
|
*/
|
|
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
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPLIANCE CHECK
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Fuehrt einen vollstaendigen Compliance-Check ueber alle TOMs durch.
|
|
*
|
|
* @param state - Der vollstaendige TOMGeneratorState
|
|
* @returns TOMComplianceCheckResult mit Issues, Score und Statistiken
|
|
*/
|
|
export function runTOMComplianceCheck(state: TOMGeneratorState): TOMComplianceCheckResult {
|
|
// Reset counter for deterministic IDs within a single check run
|
|
issueCounter = 0
|
|
|
|
const issues: TOMComplianceIssue[] = []
|
|
|
|
// Filter to applicable TOMs only (REQUIRED or RECOMMENDED, exclude NOT_APPLICABLE)
|
|
const applicableTOMs = state.derivedTOMs.filter(
|
|
(tom) => tom.applicability === 'REQUIRED' || tom.applicability === 'RECOMMENDED'
|
|
)
|
|
|
|
// Run per-TOM checks (1-3, 11) on each applicable TOM
|
|
for (const tom of applicableTOMs) {
|
|
const perTomChecks = [
|
|
checkMissingResponsible(tom),
|
|
checkOverdueReview(tom),
|
|
checkMissingEvidence(tom),
|
|
checkStaleNotImplemented(tom, state),
|
|
]
|
|
|
|
for (const issue of perTomChecks) {
|
|
if (issue !== null) {
|
|
issues.push(issue)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run aggregate checks (4-10)
|
|
issues.push(...checkIncompleteCategory(applicableTOMs))
|
|
|
|
const aggregateChecks = [
|
|
checkNoEncryption(applicableTOMs),
|
|
checkNoPseudonymization(applicableTOMs, state.dataProfile),
|
|
checkMissingAvailability(applicableTOMs, state),
|
|
checkNoReviewProcess(applicableTOMs),
|
|
checkHighRiskWithoutMeasures(applicableTOMs, state.riskProfile),
|
|
]
|
|
|
|
for (const issue of aggregateChecks) {
|
|
if (issue !== null) {
|
|
issues.push(issue)
|
|
}
|
|
}
|
|
|
|
issues.push(...checkUncoveredSDMGoal(applicableTOMs))
|
|
|
|
// Calculate score
|
|
const bySeverity: Record<TOMComplianceIssueSeverity, 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 TOM
|
|
const failedControlIds = new Set(
|
|
issues.filter((i) => !i.controlId.startsWith('SDM-') && i.controlId !== 'RISK-PROFILE').map((i) => i.controlId)
|
|
)
|
|
const totalTOMs = applicableTOMs.length
|
|
const failedCount = failedControlIds.size
|
|
const passedCount = Math.max(0, totalTOMs - failedCount)
|
|
|
|
return {
|
|
issues,
|
|
score,
|
|
stats: {
|
|
total: totalTOMs,
|
|
passed: passedCount,
|
|
failed: failedCount,
|
|
bySeverity,
|
|
},
|
|
}
|
|
}
|