feat(security): implement SOC 2 & ISO 27001 audit logging, password policy, and session timeout

This commit is contained in:
2026-09-11 15:35:47 +02:00
parent 88ec1418cf
commit 0942a94edd
12 changed files with 283 additions and 14 deletions
+89
View File
@@ -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];