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