Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dc0afc7a0 | |||
| 0942a94edd |
@@ -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';
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit Trail Logboek (SOC 2 / ISO 27001) -->
|
||||
<div class="fociz-card p-6 sm:p-7 space-y-5">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-200 dark:border-slate-800 pb-5">
|
||||
<div>
|
||||
<div class="flex items-center space-x-2 text-indigo-600 dark:text-indigo-400 text-xs font-bold uppercase tracking-wider mb-1">
|
||||
<i data-lucide="shield-check" class="w-4 h-4"></i>
|
||||
<span>Compliance & Veiligheidslogboek (SOC 2 / ISO 27001)</span>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-slate-900 dark:text-white tracking-tight">
|
||||
Onwijzigbare Audit Trail
|
||||
</h2>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
Centrale registratie van alle inlogpogingen, 2FA-verificaties, IP-detecties en beheerdersacties conform NIST SP 800-63B.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 flex-shrink-0">
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5 animate-pulse"></span>
|
||||
Live Audit Logging Actief
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-xs text-slate-600 dark:text-slate-300">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-200 dark:border-slate-800 text-[11px] font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500">
|
||||
<th class="py-3 px-3">Tijdstip</th>
|
||||
<th class="py-3 px-3">Gebruiker / Doel</th>
|
||||
<th class="py-3 px-3">Actie</th>
|
||||
<th class="py-3 px-3">IP-adres</th>
|
||||
<th class="py-3 px-3">Status</th>
|
||||
<th class="py-3 px-3">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100 dark:divide-slate-800/60 font-sans">
|
||||
<?php if (empty($auditLogs)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="py-8 text-center text-slate-400 dark:text-slate-500">
|
||||
<i data-lucide="inbox" class="w-8 h-8 mx-auto mb-2 opacity-40"></i>
|
||||
Nog geen beveiligingsgebeurtenissen geregistreerd.
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($auditLogs as $log):
|
||||
$status = strtoupper($log['status'] ?? 'SUCCESS');
|
||||
$statusColor = match($status) {
|
||||
'SUCCESS' => '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'];
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/40 transition">
|
||||
<td class="py-2.5 px-3 font-mono text-[11px] text-slate-500 whitespace-nowrap">
|
||||
<?= date('d-m-Y H:i:s', strtotime($log['created_at'])) ?>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 font-medium text-slate-800 dark:text-slate-200 max-w-[200px] truncate" title="<?= htmlspecialchars($log['user_email'] ?? 'Systeem') ?>">
|
||||
<?= htmlspecialchars($log['user_email'] ?? 'Systeem / Onbekend') ?>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-mono font-medium border <?= $actionBadge ?>">
|
||||
<?= htmlspecialchars($action) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 font-mono text-[11px] text-slate-500 whitespace-nowrap">
|
||||
<?= htmlspecialchars($log['ip_address'] ?? '—') ?>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 whitespace-nowrap">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold border <?= $statusColor ?>">
|
||||
<?= htmlspecialchars($status) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 text-[11px] text-slate-500 max-w-[280px] truncate" title="<?= htmlspecialchars($detailsStr) ?>">
|
||||
<?= htmlspecialchars($detailsStr ?: '—') ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
+15
-5
@@ -274,7 +274,9 @@ try {
|
||||
$stmt = $db->prepare("UPDATE board_lanes SET title = ?, color = ? WHERE id = ? AND board_id = ?");
|
||||
$stmt->execute([$title, $color, $laneId, $boardId]);
|
||||
} else {
|
||||
$maxPos = (int)$db->query("SELECT COALESCE(MAX(position), -1) FROM board_lanes WHERE board_id = {$boardId}")->fetchColumn();
|
||||
$stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_lanes WHERE board_id = ?");
|
||||
$stmtMax->execute([$boardId]);
|
||||
$maxPos = (int)$stmtMax->fetchColumn();
|
||||
$stmt = $db->prepare("INSERT INTO board_lanes (board_id, title, color, position) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$boardId, $title, $color, $maxPos + 1]);
|
||||
$laneId = (int)$db->lastInsertId();
|
||||
@@ -296,7 +298,9 @@ try {
|
||||
}
|
||||
|
||||
// Controleer of er kaarten in staan
|
||||
$cardCount = (int)$db->query("SELECT COUNT(*) FROM board_cards WHERE lane_id = {$laneId}")->fetchColumn();
|
||||
$stmtCardCnt = $db->prepare("SELECT COUNT(*) FROM board_cards WHERE lane_id = ?");
|
||||
$stmtCardCnt->execute([$laneId]);
|
||||
$cardCount = (int)$stmtCardCnt->fetchColumn();
|
||||
if ($cardCount > 0) {
|
||||
echo json_encode(['success' => false, 'error' => 'Verplaats of verwijder eerst de taken in deze kolom alvorens de kolom te verwijderen.']);
|
||||
exit;
|
||||
@@ -337,7 +341,9 @@ try {
|
||||
");
|
||||
$stmt->execute([$title, $description, $assigneeId, $priority, $color, $laneId, $cardId, $boardId]);
|
||||
} else {
|
||||
$maxPos = (int)$db->query("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = {$laneId}")->fetchColumn();
|
||||
$stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = ?");
|
||||
$stmtMax->execute([$laneId]);
|
||||
$maxPos = (int)$stmtMax->fetchColumn();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
@@ -563,10 +569,14 @@ try {
|
||||
|
||||
// Als geen lane_id is meegegeven, pak de 1e lane van het bord
|
||||
if ($laneId <= 0) {
|
||||
$laneId = (int)$db->query("SELECT id FROM board_lanes WHERE board_id = {$boardId} ORDER BY position ASC, id ASC LIMIT 1")->fetchColumn();
|
||||
$stmtLane = $db->prepare("SELECT id FROM board_lanes WHERE board_id = ? ORDER BY position ASC, id ASC LIMIT 1");
|
||||
$stmtLane->execute([$boardId]);
|
||||
$laneId = (int)$stmtLane->fetchColumn();
|
||||
}
|
||||
|
||||
$maxPos = (int)$db->query("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = {$laneId}")->fetchColumn();
|
||||
$stmtMax = $db->prepare("SELECT COALESCE(MAX(position), -1) FROM board_cards WHERE lane_id = ?");
|
||||
$stmtMax->execute([$laneId]);
|
||||
$maxPos = (int)$stmtMax->fetchColumn();
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO board_cards (board_id, lane_id, title, description, assignee_id, priority, color, position, created_by_id)
|
||||
VALUES (?, ?, ?, '', NULL, 'P2', 'yellow', ?, ?)
|
||||
|
||||
+4
-2
@@ -39,8 +39,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if (empty($name) || empty($password) || empty($totpCode)) {
|
||||
$error = "Alle velden (inclusief 2FA verificatiecode) zijn verplicht.";
|
||||
} elseif (strlen($password) < 8) {
|
||||
$error = "Het wachtwoord moet minimaal 8 tekens lang zijn.";
|
||||
} elseif (($pwErr = fociz_validate_password($password)) !== null) {
|
||||
$error = $pwErr;
|
||||
} elseif ($password !== $confirmPassword) {
|
||||
$error = "Wachtwoorden komen niet overeen.";
|
||||
} elseif (!TOTP::verifyCode($secret, $totpCode)) {
|
||||
@@ -75,6 +75,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
$db->commit();
|
||||
|
||||
fociz_log_audit($newUserId, 'USER_REGISTERED_VIA_INVITE', 'user', $newUserId, ['email' => $invite['email'], 'role' => $invite['role']], 'SUCCESS', $invite['email']);
|
||||
|
||||
// Start dagsessie (geldig tot 23:59:59)
|
||||
$_SESSION['user_id'] = $newUserId;
|
||||
$_SESSION['user_name'] = $name;
|
||||
|
||||
@@ -74,6 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$isIpKnown) {
|
||||
// Stuur verificatie-mail en informeer gebruiker
|
||||
fociz_send_new_ip_verification((int)$user['id'], $user['email'], $user['name'], $clientIp);
|
||||
fociz_log_audit((int)$user['id'], 'LOGIN_NEW_IP_CHALLENGE', 'user', (int)$user['id'], ['ip' => $clientIp], 'PENDING', $user['email']);
|
||||
$error = "Nieuwe inloglocatie gedetecteerd ({$clientIp}). Er is een verificatie-e-mail gestuurd naar {$user['email']}. Klik op de link in de e-mail om deze locatie toe te staan.";
|
||||
} else {
|
||||
// Succesvol stap 1: zet tijdelijke pending sessie voor 2FA
|
||||
@@ -87,6 +88,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!empty($email)) {
|
||||
fociz_hit_rate_limit($emailRateKey, 900);
|
||||
}
|
||||
fociz_log_audit($user ? (int)$user['id'] : null, 'LOGIN_FAILED', 'user', $user ? (int)$user['id'] : null, ['reason' => 'Invalid credentials', 'ip' => $clientIp], 'FAILURE', $email);
|
||||
$error = "Onjuist e-mailadres of wachtwoord.";
|
||||
}
|
||||
}
|
||||
@@ -159,6 +161,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// Dagsessie-token uitgeven (strikt geldig tot 23:59:59 van vandaag)
|
||||
issue_day_session_token((int)$user['id']);
|
||||
|
||||
fociz_log_audit((int)$user['id'], 'LOGIN_SUCCESS', 'user', (int)$user['id'], ['ip' => $clientIp, 'auth_method' => 'PASSWORD+2FA'], 'SUCCESS', $user['email']);
|
||||
|
||||
unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_user_email'], $_SESSION['pending_2fa_user_name']);
|
||||
|
||||
header('Location: /planner.php');
|
||||
@@ -166,6 +170,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} else {
|
||||
fociz_hit_rate_limit($totpRateKey, 900);
|
||||
fociz_hit_rate_limit($ipRateKey, 900);
|
||||
fociz_log_audit((int)$userId, 'LOGIN_MFA_FAILED', 'user', (int)$userId, ['ip' => $clientIp], 'FAILURE', $_SESSION['pending_2fa_user_email'] ?? null);
|
||||
$error = "Onjuiste 2FA code. Controleer uw authenticator app en probeer opnieuw.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $resetRecord) {
|
||||
|
||||
if (empty($password)) {
|
||||
$error = "Vul een nieuw wachtwoord in.";
|
||||
} elseif (strlen($password) < 8) {
|
||||
$error = "Het wachtwoord moet minimaal 8 tekens lang zijn.";
|
||||
} elseif (($pwErr = fociz_validate_password($password)) !== null) {
|
||||
$error = $pwErr;
|
||||
} elseif ($password !== $confirmPassword) {
|
||||
$error = "De wachtwoorden komen niet overeen.";
|
||||
} else {
|
||||
@@ -62,6 +62,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $resetRecord) {
|
||||
$db->commit();
|
||||
$success = true;
|
||||
|
||||
fociz_log_audit(null, 'PASSWORD_RESET_COMPLETED', 'user', null, ['email' => $resetRecord['user_email']], 'SUCCESS', $resetRecord['user_email']);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($db->inTransaction()) $db->rollBack();
|
||||
error_log("Fout bij resetten wachtwoord: " . $e->getMessage());
|
||||
|
||||
+4
-2
@@ -35,8 +35,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$error = "Alle velden (inclusief de 2FA verificatiecode) zijn verplicht.";
|
||||
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = "Voer een geldig e-mailadres in.";
|
||||
} elseif (strlen($password) < 8) {
|
||||
$error = "Het wachtwoord moet minimaal 8 tekens bevatten.";
|
||||
} elseif (($pwErr = fociz_validate_password($password)) !== null) {
|
||||
$error = $pwErr;
|
||||
} elseif ($password !== $confirmPassword) {
|
||||
$error = "De ingevoerde wachtwoorden komen niet overeen.";
|
||||
} elseif (!TOTP::verifyCode($secret, $totpCode)) {
|
||||
@@ -73,6 +73,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
clear_remember_cookie();
|
||||
unset($_SESSION['setup_totp_secret']);
|
||||
|
||||
fociz_log_audit($adminId, 'INITIAL_ADMIN_SETUP', 'user', $adminId, ['email' => $email, 'name' => $name], 'SUCCESS', $email);
|
||||
|
||||
$success = true;
|
||||
} catch (Exception $e) {
|
||||
$error = "Fout bij aanmaken beheerder in MariaDB: " . $e->getMessage();
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
/**
|
||||
* Fociz Nieuwe Inloglocatie / IP Verificatie
|
||||
*/
|
||||
@@ -43,6 +43,7 @@ if (empty($token) || !preg_match('/^[a-f0-9]{64}$/', $token)) {
|
||||
$success = true;
|
||||
$userEmail = $record['email'];
|
||||
$ipAddress = $record['ip_address'];
|
||||
fociz_log_audit((int)$record['user_id'], 'IP_VERIFIED', 'user_known_ips', (int)$record['id'], ['ip' => $ipAddress], 'SUCCESS', $userEmail);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
error_log("verify-ip.php error: " . $e->getMessage());
|
||||
|
||||
@@ -160,6 +160,8 @@ function ensure_tables_exist(PDO $pdo): void {
|
||||
"ALTER TABLE users ADD COLUMN last_login_ip VARCHAR(45) NULL AFTER last_jira_check_at",
|
||||
"ALTER TABLE users ADD COLUMN last_login_at DATETIME NULL AFTER last_login_ip",
|
||||
"CREATE TABLE IF NOT EXISTS `user_known_ips` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NOT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `is_verified` TINYINT(1) NOT NULL DEFAULT 1, `verify_token` VARCHAR(64) NULL, `verify_token_expires_at` DATETIME NULL, `first_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_seen_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_user_ip` (`user_id`, `ip_address`), INDEX (`verify_token`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
// SOC 2 & ISO 27001: Onwijzigbare Audit Trail
|
||||
"CREATE TABLE IF NOT EXISTS `audit_logs` (`id` INT AUTO_INCREMENT PRIMARY KEY, `user_id` INT NULL, `user_email` VARCHAR(191) NULL, `action` VARCHAR(64) NOT NULL, `resource_type` VARCHAR(64) NULL, `resource_id` INT NULL, `details` TEXT NULL, `ip_address` VARCHAR(45) NOT NULL, `user_agent` VARCHAR(255) NULL, `status` ENUM('SUCCESS', 'FAILED', 'WARNING', 'PENDING') NOT NULL DEFAULT 'SUCCESS', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX (`user_id`), INDEX (`action`), INDEX (`created_at`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
];
|
||||
foreach ($colUpgrades as $sql) {
|
||||
try {
|
||||
|
||||
@@ -275,3 +275,21 @@ CREATE TABLE IF NOT EXISTS `user_known_ips` (
|
||||
INDEX (`verify_token`),
|
||||
CONSTRAINT `fk_known_ips_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Fociz SOC 2 & ISO 27001: Onwijzigbare Audit Trail (Beveiligings- en beheeracties)
|
||||
CREATE TABLE IF NOT EXISTS `audit_logs` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`user_id` INT NULL,
|
||||
`user_email` VARCHAR(191) NULL,
|
||||
`action` VARCHAR(64) NOT NULL,
|
||||
`resource_type` VARCHAR(64) NULL,
|
||||
`resource_id` INT NULL,
|
||||
`details` TEXT NULL,
|
||||
`ip_address` VARCHAR(45) NOT NULL,
|
||||
`user_agent` VARCHAR(255) NULL,
|
||||
`status` ENUM('SUCCESS', 'FAILED', 'WARNING', 'PENDING') NOT NULL DEFAULT 'SUCCESS',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX (`user_id`),
|
||||
INDEX (`action`),
|
||||
INDEX (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -87,6 +87,83 @@ function fociz_get_client_ip(): string {
|
||||
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* SOC 2 & ISO 27001 Audit Trail logger
|
||||
* Schrijft alle beveiligings- en beheeracties onweerlegbaar weg naar de audit_logs tabel
|
||||
*/
|
||||
function fociz_log_audit(?int $userId, string $action, string $resourceType = '', ?int $resourceId = null, $details = null, string $status = 'SUCCESS', ?string $userEmail = null): void {
|
||||
try {
|
||||
$db = get_db_connection();
|
||||
$ip = fociz_get_client_ip();
|
||||
$ua = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);
|
||||
|
||||
// Indien email niet meegegeven, zoek op basis van userId
|
||||
if (empty($userEmail) && !empty($userId)) {
|
||||
$stmtU = $db->prepare("SELECT email FROM users WHERE id = ?");
|
||||
$stmtU->execute([$userId]);
|
||||
$userEmail = $stmtU->fetchColumn() ?: null;
|
||||
}
|
||||
|
||||
$detailsJson = is_array($details) || is_object($details) ? json_encode($details, JSON_UNESCAPED_UNICODE) : (is_string($details) ? $details : null);
|
||||
|
||||
$statusNormalized = strtoupper($status);
|
||||
if ($statusNormalized === 'FAILURE') {
|
||||
$statusNormalized = 'FAILED';
|
||||
}
|
||||
if (!in_array($statusNormalized, ['SUCCESS', 'FAILED', 'WARNING', 'PENDING'])) {
|
||||
$statusNormalized = 'SUCCESS';
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("
|
||||
INSERT INTO audit_logs (user_id, user_email, action, resource_type, resource_id, details, ip_address, user_agent, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
$userId ?: null,
|
||||
$userEmail ?: null,
|
||||
$action,
|
||||
$resourceType ?: null,
|
||||
$resourceId ?: null,
|
||||
$detailsJson,
|
||||
$ip,
|
||||
$ua,
|
||||
$statusNormalized
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
// Audit log mag NOOIT de applicatiewerking breken
|
||||
error_log("fociz_log_audit failure: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NIST SP 800-63B / ISO 27001 Wachtwoordvalidator
|
||||
* Vereist:
|
||||
* - Minimaal 12 tekens
|
||||
* - Minstens 1 hoofdletter (A-Z)
|
||||
* - Minstens 1 kleine letter (a-z)
|
||||
* - Minstens 1 cijfer (0-9)
|
||||
* - Minstens 1 speciaal teken (!@#$%^&*()_+-=[]{}|;:,.<>?)
|
||||
* Geeft null terug indien geldig, of een duidelijke foutmelding in het Nederlands
|
||||
*/
|
||||
function fociz_validate_password(string $password): ?string {
|
||||
if (strlen($password) < 12) {
|
||||
return "Het wachtwoord moet minimaal 12 tekens lang zijn conform het beveiligingsbeleid.";
|
||||
}
|
||||
if (!preg_match('/[A-Z]/', $password)) {
|
||||
return "Het wachtwoord moet minimaal één hoofdletter (A-Z) bevatten.";
|
||||
}
|
||||
if (!preg_match('/[a-z]/', $password)) {
|
||||
return "Het wachtwoord moet minimaal één kleine letter (a-z) bevatten.";
|
||||
}
|
||||
if (!preg_match('/[0-9]/', $password)) {
|
||||
return "Het wachtwoord moet minimaal één cijfer (0-9) bevatten.";
|
||||
}
|
||||
if (!preg_match('/[^A-Za-z0-9]/', $password)) {
|
||||
return "Het wachtwoord moet minimaal één speciaal teken of leesteken bevatten.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genereert een sessie fingerprint op basis van IP en browser/platform
|
||||
*/
|
||||
@@ -395,6 +472,18 @@ function get_logged_in_user(): ?array {
|
||||
|
||||
// 2. Controleer actieve PHP sessie (mits behorend bij vandaag én hetzelfde IP/fingerprint)
|
||||
if (!empty($_SESSION['user_id']) && !empty($_SESSION['is_2fa_verified'])) {
|
||||
// Inactiviteitstimeout (ISO 27001 / SOC 2): 120 minuten (7200s)
|
||||
$maxIdleSeconds = 7200;
|
||||
if (!empty($_SESSION['last_activity']) && ($now - (int)$_SESSION['last_activity']) > $maxIdleSeconds) {
|
||||
fociz_log_audit((int)$_SESSION['user_id'], 'SESSION_IDLE_TIMEOUT', 'users', (int)$_SESSION['user_id'], ['idle_seconds' => $now - (int)$_SESSION['last_activity']], 'WARNING');
|
||||
$_SESSION = ['session_was_expired' => true, 'idle_timeout' => true];
|
||||
clear_day_session_cookie();
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
@session_regenerate_id(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// IP-verificatie: als het IP/locatie tussentijds verandert, moet er opnieuw worden ingelogd
|
||||
if (!empty($_SESSION['auth_fingerprint']) && $_SESSION['auth_fingerprint'] !== $clientFingerprint) {
|
||||
$_SESSION = ['session_was_expired' => true, 'ip_changed' => true];
|
||||
|
||||
+3
-2
@@ -175,8 +175,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!password_verify($currentPassword, $storedHash)) {
|
||||
$message = "Uw huidige wachtwoord is niet correct ingevoerd.";
|
||||
$messageType = 'error';
|
||||
} elseif (strlen($newPassword) < 8) {
|
||||
$message = "Het nieuwe wachtwoord moet minimaal 8 tekens lang zijn.";
|
||||
} elseif (($pwErr = fociz_validate_password($newPassword)) !== null) {
|
||||
$message = $pwErr;
|
||||
$messageType = 'error';
|
||||
} elseif ($newPassword !== $confirmPassword) {
|
||||
$message = "Het nieuwe wachtwoord en de bevestiging komen niet overeen.";
|
||||
@@ -185,6 +185,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$newHash = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
$stmtUp = $db->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
|
||||
$stmtUp->execute([$newHash, $userId]);
|
||||
fociz_log_audit($userId, 'PASSWORD_CHANGED', 'user', $userId, ['method' => 'profile'], 'SUCCESS', $user['email']);
|
||||
$message = "Uw wachtwoord is succesvol gewijzigd.";
|
||||
$messageType = 'success';
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
// Verstuur de gestylede uitnodigingsmail direct via SMTP
|
||||
$mailResult = FocizMailer::sendInvitationEmail($email, $adminUser['name'], $role, $generatedLink);
|
||||
fociz_log_audit((int)$adminUser['id'], 'USER_INVITED', 'invite', null, ['email' => $email, 'role' => $role], 'SUCCESS', $adminUser['email']);
|
||||
if ($mailResult['success']) {
|
||||
$message = "Uitnodigingslink succesvol aangemaakt én per e-mail verzonden naar {$email}!";
|
||||
} else {
|
||||
@@ -55,6 +56,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$baseUrl = rtrim(APP_URL, '/');
|
||||
$link = "{$baseUrl}/auth/invite.php?token={$inv['token']}";
|
||||
$mailResult = FocizMailer::sendInvitationEmail($inv['email'], $adminUser['name'], $inv['role'], $link);
|
||||
fociz_log_audit((int)$adminUser['id'], 'INVITE_RESENT', 'invite', $inviteId, ['email' => $inv['email'], 'role' => $inv['role']], 'SUCCESS', $adminUser['email']);
|
||||
if ($mailResult['success']) {
|
||||
$message = "Uitnodiging succesvol opnieuw per e-mail verzonden naar {$inv['email']}!";
|
||||
} else {
|
||||
@@ -67,13 +69,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$inviteId = (int)($_POST['invite_id'] ?? 0);
|
||||
$stmtDel = $db->prepare("DELETE FROM invites WHERE id = ?");
|
||||
$stmtDel->execute([$inviteId]);
|
||||
fociz_log_audit((int)$adminUser['id'], 'INVITE_REVOKED', 'invite', $inviteId, [], 'SUCCESS', $adminUser['email']);
|
||||
header('Location: /users.php');
|
||||
exit;
|
||||
} elseif ($action === 'delete_user') {
|
||||
$delUserId = (int)($_POST['user_id'] ?? 0);
|
||||
if ($delUserId !== (int)$adminUser['id']) {
|
||||
$stmtUserToDel = $db->prepare("SELECT email, name FROM users WHERE id = ?");
|
||||
$stmtUserToDel->execute([$delUserId]);
|
||||
$delUser = $stmtUserToDel->fetch();
|
||||
|
||||
$stmtDelUser = $db->prepare("DELETE FROM users WHERE id = ?");
|
||||
$stmtDelUser->execute([$delUserId]);
|
||||
|
||||
fociz_log_audit((int)$adminUser['id'], 'USER_DELETED', 'user', $delUserId, ['deleted_email' => $delUser['email'] ?? null, 'deleted_name' => $delUser['name'] ?? null], 'SUCCESS', $adminUser['email']);
|
||||
}
|
||||
header('Location: /users.php');
|
||||
exit;
|
||||
|
||||
Reference in New Issue
Block a user