From 0942a94edd0425968cc224d9e3b403c5701bf10f Mon Sep 17 00:00:00 2001 From: Niek Rabelink Date: Fri, 11 Sep 2026 15:35:47 +0200 Subject: [PATCH] feat(security): implement SOC 2 & ISO 27001 audit logging, password policy, and session timeout --- admin.php | 128 ++++++++++++++++++++++++++++++++++++++++ api/board.php | 20 +++++-- auth/invite.php | 6 +- auth/login.php | 5 ++ auth/reset-password.php | 6 +- auth/setup.php | 6 +- auth/verify-ip.php | 3 +- config/database.php | 2 + config/schema.sql | 18 ++++++ includes/auth.php | 89 ++++++++++++++++++++++++++++ profile.php | 5 +- users.php | 9 +++ 12 files changed, 283 insertions(+), 14 deletions(-) diff --git a/admin.php b/admin.php index a9a6958..8f6fdfc 100644 --- a/admin.php +++ b/admin.php @@ -68,6 +68,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { "); $stmt->execute([$userId, $isDemo, json_encode($newSettings)]); + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'integration', null, ['provider' => 'OUTLOOK', 'tenant_id' => $tenantId], 'SUCCESS', $adminUser['email']); + $message = "Centrale Microsoft 365 instellingen (Agenda, E-mail & Teams) zijn succesvol opgeslagen."; $messageType = 'success'; } @@ -93,6 +95,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { "); $stmt->execute([$userId, json_encode($existingSettings)]); + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'integration', null, ['provider' => 'JIRA', 'domain' => $companyDomain], 'SUCCESS', $adminUser['email']); + $message = "Standaard Atlassian Jira bedrijfsdomein is succesvol opgeslagen."; $messageType = 'success'; } @@ -136,6 +140,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $stmt->execute([$filterCode, $filterName, $projectKey ?: null, $jqlClause, $description, $isActive, $sortOrder]); $message = "Nieuw wachtrijfilter '{$filterName}' is succesvol toegevoegd."; } + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_queue_filter', $filterId ?: null, ['filter_code' => $filterCode, 'filter_name' => $filterName], 'SUCCESS', $adminUser['email']); $messageType = 'success'; } } @@ -146,6 +151,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($filterId > 0) { $stmt = $db->prepare("DELETE FROM jira_queue_filters WHERE id = ?"); $stmt->execute([$filterId]); + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_queue_filter', $filterId, ['action' => 'delete'], 'SUCCESS', $adminUser['email']); $message = "Wachtrijfilter is succesvol verwijderd."; $messageType = 'success'; } @@ -160,6 +166,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $fCode = trim($fCode); $upd->execute([$fCode !== '' ? $fCode : null, (int)$uId]); } + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'jira_assignments', null, [], 'SUCCESS', $adminUser['email']); $message = "Standaard Jira wachtrijtoewijzingen voor het team zijn succesvol opgeslagen."; $messageType = 'success'; } @@ -185,6 +192,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { set_system_setting('pomodoro_break', $pomoBreak); set_system_setting('default_task_duration', $taskDuration); + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'workday_settings', null, ['workday_start' => $workdayStart, 'workday_end' => $workdayEnd], 'SUCCESS', $adminUser['email']); + $message = "Organisatie werkdagstandaarden zijn succesvol bijgewerkt."; $messageType = 'success'; } @@ -205,6 +214,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { set_system_setting('ai_provider', $aiProvider); set_system_setting('ai_model', $aiModel); + fociz_log_audit($userId, 'SETTINGS_UPDATED', 'ai_settings', null, ['ai_enabled' => $aiEnabled, 'ai_provider' => $aiProvider, 'ai_model' => $aiModel], 'SUCCESS', $adminUser['email']); + $message = "AI Co-Pilot instellingen zijn succesvol opgeslagen."; $messageType = 'success'; } @@ -297,6 +308,13 @@ try { error_log("Fout bij ophalen jira filters/users: " . $e->getMessage()); } +$auditLogs = []; +try { + $auditLogs = $db->query("SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 50")->fetchAll() ?: []; +} catch (Throwable $e) { + error_log("Fout bij ophalen audit logs: " . $e->getMessage()); +} + $pageTitle = 'Systeembeheer & Integraties — Fociz'; require_once __DIR__ . '/includes/header.php'; ?> @@ -1194,6 +1212,116 @@ require_once __DIR__ . '/includes/header.php'; + + +
+
+
+
+ + Compliance & Veiligheidslogboek (SOC 2 / ISO 27001) +
+

+ Onwijzigbare Audit Trail +

+

+ Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B. +

+
+
+ + + Live Audit Logging Actief + +
+
+ +
+ + + + + + + + + + + + + + + + + + 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20', + 'FAILED', 'FAILURE' => 'bg-rose-500/10 text-rose-600 dark:text-rose-400 border-rose-500/20', + 'WARNING' => 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20', + 'PENDING' => 'bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20', + default => 'bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20', + }; + $action = $log['action'] ?? ''; + $actionBadge = match($action) { + 'LOGIN_SUCCESS' => 'bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800/60', + 'LOGIN_FAILED', 'LOGIN_MFA_FAILED' => 'bg-rose-50 dark:bg-rose-950/40 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800/60', + 'LOGIN_NEW_IP_CHALLENGE', 'IP_VERIFIED' => 'bg-sky-50 dark:bg-sky-950/40 text-sky-700 dark:text-sky-300 border-sky-200 dark:border-sky-800/60', + 'SETTINGS_UPDATED' => 'bg-purple-50 dark:bg-purple-950/40 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800/60', + 'USER_INVITED', 'INVITE_RESENT', 'USER_REGISTERED_VIA_INVITE' => 'bg-indigo-50 dark:bg-indigo-950/40 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-800/60', + 'USER_DELETED', 'INVITE_REVOKED' => 'bg-amber-50 dark:bg-amber-950/40 text-amber-700 dark:text-amber-300 border-amber-200 dark:border-amber-800/60', + default => 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border-slate-200 dark:border-slate-700', + }; + $detailsStr = ''; + if (!empty($log['details'])) { + $decoded = json_decode($log['details'], true); + if (is_array($decoded)) { + $parts = []; + foreach ($decoded as $k => $v) { + if (is_scalar($v)) { + $parts[] = "{$k}: {$v}"; + } + } + $detailsStr = implode(', ', $parts); + } else { + $detailsStr = (string)$log['details']; + } + } + ?> + + + + + + + + + + + +
TijdstipGebruiker / DoelActieIP-adresStatusDetails
+ + Nog geen beveiligingsgebeurtenissen geregistreerd. +
+ + + + + + + + + + + + + + + +
+
+