feat(security): implement SOC 2 & ISO 27001 audit logging, password policy, and session timeout
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user